sleuth 0.30.1
sleuth: ^0.30.1 copied to clipboard
In-app performance diagnostics overlay for Flutter. Surfaces jank, memory leaks, slow networks, GPU pressure, and widget anti-patterns. Every issue ships with a fix hint.
0.30.1 #
pub.dev README polish — no detector or distribution change.
doc/logo.pngnow ships in the published archive (.pubignorewhitelist) so the README hero image renders on pub.dev instead of falling back to the alt text.- Tests-passing badge refreshed to the current count (3,001).
0.30.0 #
TrackedResourceDetector.tracked_resource_long_lived.warning raised to runtimeVerified via additionalBrackets[0]. Distribution: 15/20 effective runtimeVerified family-severity pairs across 12 unique stableIds.
- Long-lived bracket: threshold 300 (matches default
longLivedSeconds), unitseconds, atTolerance 0.5 (at-band [300, 450]), aboveCeilingMultiplier 3.0 (ceiling 900).observedAxisArgKey: 'oldestInstanceAgeSeconds',requireUniqueDetectedAtMicros: true. Three iPhone 12 / iOS 17.5 / Flutter 3.41.4 captures with real-time waits past the 300 s production threshold. - Detector behaviour change:
_evaluateLongLivedoverwriteslongLivedFirstCrossMicros = nowMicroseach sweep (was??=first-cross-only). A long-lived overshoot now produces an emission per sweep with monotonically-increasing age — captures get a real ascending-age series (observedAxisReduction: 'max'picks the leg-end value), and a lingering leak re-flags every sweep with the current elapsed retention. UI cards unchanged (same stableId, age refreshes). captureTraceStableId: longLivedStableIdre-added to_evaluateLongLivedso parametrictracked_resource_long_lived:<name>emissions route through the bare family for the bracket validator's byte-exact filter.- New public API:
lastObservedAgeSecondsFor(name)/peakObservedAgeSecondsFor(name)per-name age observables._sweep()records each pass;untrackAll(name)drops entries;resetCaptureState()clears them. - Capture screen long-lived legs: register 1 ref + real-time wait (250 / 380 / 600 s for below / at / above) at the production threshold.
dispose()clears per-name override defensively viaSleuth.setResourceThreshold(_kResourceName)(both null = remove).
0.29.1 #
IssueEncyclopediaPage "Learn more" navigation now resolves to the correct entry for parametric stableIds (tracked_resource_concurrent:<name>, excessive_keep_alive:<i>, excessive_global_keys:<i>) and dynamic-suffix stableIds (repaint_debug_<typeName>, rebuild_debug_<typeName>). Previously the page used byte-exact scrollToStableId against IssueExplanationBuilder.allExplanations (bare-family keys), so parametric/dynamic variants never expanded or scrolled the target entry.
- New public
IssueExplanationBuilder.canonicalId(String)— strips parametric:<param>and dynamic widget-type suffixes, mapping aPerformanceIssue.stableIdto the encyclopedia key. IssueEncyclopediaPage._scrollTargetKeygetter resolveswidget.scrollToStableIdthroughcanonicalId;initStatecontainsKeycheck,_scrollToTarget, and per-rowisScrollTargetcomparison all use the normalized key.
0.29.0 #
TrackedResourceDetector.tracked_resource_concurrent.warning raised to runtimeVerified via perStableIdTier. Distribution: 14/20 effective runtimeVerified family-severity pairs across 11 unique stableIds.
- Bracket: threshold 6 (smallest count > default
maxConcurrent5 that triggers emission), unitinstances, atTolerance 0.5 (at-band [6, 9]), aboveCeilingMultiplier 3.0 (ceiling 18).observedAxisArgKey: 'liveInstanceCount',requireUniqueDetectedAtMicros: true. Three iPhone 12 / iOS 17.5 / Flutter 3.41.4 captures. - New
PerformanceIssue.captureTraceStableIdoptional field. When set,CaptureHelper.composeIssueEventuses it (instead ofstableId) to compose thesleuth.issue.<id>.<severity>trace-event name. Parametric stableId detectors (tracked_resource_concurrent:<name>) route the trace event through the bare family so the bracket validator's byte-exact filter matches every member. UI cards still key on the parametricstableId; equality + hashCode unchanged. - Detector capture plumbing:
flushConcurrentEvaluation()(synchronous sweep, bypasses the 10 s sweep-timer);untrackAll(name)(drop bucket + detach Finalizers — leg isolation for capture screens);resetCaptureState()(clears per-name observables + every bucket'sconcurrentFirstCrossMicros/longLivedFirstCrossMicros, propagated fromSleuthController.resetCaptureState); per-name getterslastObservedLiveCountFor(name)/peakObservedLiveCountFor(name)plus aggregatelastObservedLiveCount/peakObservedLiveCountfor back-compat. Capture screens MUST use the per-name getter — the aggregate would track an unrelated bucket if anotherSleuth.trackResource(...)registration is active. tracked_resource_long_livedfamily stays reproducerOnly — 300 s threshold exceeds an on-device scenario window._evaluateLongLiveddoes NOT setcaptureTraceStableId, so its emissions never land as bare-familysleuth.issue.tracked_resource_long_lived.warningevents in capture mode (which would be unclaimed evidence: no bracket, nocoveredThresholdsentry).- New
example/lib/demos/tracked_resource_capture_screen.dart. Per-leg flow:untrackAll+ clear strong-refs →suspendNonEssentialTimelineStreams→markScenarioBegin→ synchronous allocate + register →flushConcurrentEvaluation→ 3 × 32 ms frame yields →flushTimelineNow→ readpeakObservedLiveCountFor(name)→markScenarioEnd→ 600 ms drain →exportCaptureJson.
0.28.0 #
New Sleuth.setResourceThreshold(name, {int? maxConcurrent, int? longLivedSeconds}) per-name threshold override for TrackedResourceDetector. trackResource / untrackResource API unchanged.
- Merge semantics: omitted or invalid axis preserves the prior value for that axis. Explicit both-null clears the override. Subsequent calls update one axis without losing the other.
- Override is bucket-independent — survives empty-bucket sweep eviction, LRU bucket drops, and
isEnabled = falsetoggle.dispose()clears. - Per-axis validation: invalid values (
<= 0) drop that axis (counted viadroppedOverridesCount). Cap at 1000 distinct names — new-name overflow silently drops; updates to existing names always succeed. Runtime guard (release-safe). - Issue
extraTraceArgsalways stampseffectiveMaxConcurrent/effectiveLongLivedSeconds+thresholdSource('override'or'global'). - Pre-init calls (before
Sleuth.init) drop with a once-per-session debug warning. - Cross-isolate /
kReleaseModeno-op (matchestrackResourceshape).
0.27.0 #
New TrackedResourceDetector (runtime, opt-in) + public Sleuth.trackResource / Sleuth.untrackResource API. 19 → 20 detectors.
Sleuth.trackResource(name, resource)registers; tracker keepsWeakReference+ Finalizer token + first-seen timestamp per registration. Token is the registration identity (allocation-unique, collision-resistant); sharedFinalizerdispatches release on GC reclaim.Sleuth.untrackResource(name, resource)is the optional explicit decrement.- Two emission paths, both
confirmed:tracked_resource_concurrent.warning— live count under one name >trackedResourceMaxConcurrent(default 5).tracked_resource_long_lived.warning— single instance alive pasttrackedResourceLongLivedSeconds(default 300 s).
- LRU cap (
trackedResourceMaxDistinctNames, default 1000) bounds the in-memory bucket map; eviction detaches per-ref Finalizer entries so VM-side state stays bounded. Periodic sweep (trackedResourceSweepIntervalSeconds, default 10 s) drives evaluation. - Pure Dart — no VM service dependency. Cross-isolate registration is a no-op (one controller per isolate).
- Primitive / record targets silently dropped via
droppedTargetsCount. - New
CausalGraphRuleedgestracked_resource_concurrent → heap_growingandtracked_resource_long_lived → heap_growing. - Tier
reproducerOnly.
0.26.0 #
stream_resource_growth.warning raised to runtimeVerified; gc_pressure default 30 → 60/min.
StreamResourceDetector:stream_resource_growth.warning→ runtimeVerified viaperStableIdTier. Three iPhone 12 / iOS 17.5 / Flutter 3.41.4 captures bracket threshold 50 (unitinstances) ontopGrowthDeltaaxis; atTolerance 0.6, aboveCeilingMultiplier 3.0.- BREAKING-ISH: magnitude gate switched from summed
netDeltato dominant-classtop.deltaso the firing axis matches the bracketed axis. Multi-class growth (≥2 watchlist classes ascending) stays as a structural precondition. A balanced 25+25 multi-class workload no longer fires; a single 60-instance leak with any other grower still does. MemoryPressureDetector.gcRateThresholdPerMindefault 30 → 60. Dart'sEventStreams.kGCemits per young-gen scavenge; ~30/min is steady-state for a moderately allocating UI. Pre-v0.26.0 sensitivity available viaSleuthConfig(gcRateThresholdPerMin: 30).StreamResourceCaptureScreen: 1024 KB/sec byte pressure (256 KB × 4 Hz, 1024-entry rotating cap) reliably re-arms heap_growing inside scenario. Heap-growing readiness wait moved INSIDE scenario span (markScenarioBegin → resetCaptureStatewipes the prior latch). DirectflushStreamResourceEvaluation()dropped — emissions route throughpollStreamResourceAllocationProfileNowWithCapture. JSON post-process alignsexpectedMagnitude.observedto detector-stampedtopGrowthDelta.'instances'added toProfileCaptureSchema.approvedUnits.
0.25.0 (BREAKING) #
Multi-parent causal UI + removal of deprecated rootCauseId singular field.
BREAKING — PerformanceIssue.rootCauseId (deprecated since v0.24.2) and effectiveRootCauseIds getter removed. JSON rootCauseId key no longer read or emitted. Migration:
PerformanceIssue(rootCauseId: 'x')→rootCauseIds: ['x'](also coverscopyWith).issue.rootCauseIdgetter →issue.rootCauseIds?.firstOrNull.- v0.24.x-or-earlier snapshots carrying only the singular key must re-export through v0.24.2 (singular → plural coercion) before importing on v0.25.0+. Debug builds emit a warning when fromJson sees the legacy key without the plural.
UI:
IssueCard.parentIssues+_causedBySectionwidget (mirrors_downstreamSection; cap at 5 + "and N more"; "(+N suppressed)" annotation when resolved parents <rootCauseIds.length).computeVisibleIssues: ≥2 parents always visible (multi-parent badge); 1 parent collapses under visible parent or surfaces as orphan; 0 parents visible.FloatingIssuesCard: resolvesparentIssuesviastableIdToIssuemap; counts unresolved parents.AiContextBuilderreadsrootCauseIdsdirectly.
Contract:
rootCauseIdsdocumented invariant: null or non-empty.fromJsoncoerces empty/all-non-string lists to null._resortRootCauseIdsByCurrentSeveritykeepsrootCauseIds[0]highest-severity post-escalation so the "Caused by" badge and AI-prompt cap-at-5 truncation stay accurate.
Tests: +9 (5 _causedBySection render + 4 fromJson normalization). Visibility-filter triad updated. ~10 sites migrated singular→plural; singular-only regression tests removed (now compile errors).
0.24.2 #
Multi-parent causal-graph annotation (metadata layer). CausalGraphRule.apply now claims every reaching root for each downstream effect, removing the v0.24.1 export-vs-UI asymmetry at the data model layer. Top-level UI rendering of multi-parent badges is deferred to v0.25.0+ — the visibility filter still collapses each downstream under any visible reaching root.
PerformanceIssue.rootCauseIds: List<String>?(plural) joins the schema; singularrootCauseIdis@Deprecatedand removed in v0.25.0. Constructor accepts both for back-compat.fromJsonreadsrootCauseIdsif present, falls back to a singleton-list coercion ofrootCauseIdfor v0.24.1-and-earlier snapshots.toJsonderives singular fromrootCauseIds.first(post-v0.24.2 canonical) so v0.24.1 readers see the highest-severity root after re-export — eliminates singular/plural drift.CausalGraphRule.apply():downstreamOwnersis nowMap<int, Set<int>>(multi-parent) instead ofMap<int, int>(single-owner). BFS from each root accumulates every reach. Each downstream issue carries every reaching root, sorted severity desc then stableId asc. Confidence suppression skips the root'sdownstreamIdslisting for apossibledownstream when any reaching root isconfirmedorlikely. Intermediate nodes in multi-hop chains are not surfaced as parents — only originating roots are (matches BFS-from-roots model; surfacing intermediates ships in v0.25.0+).FloatingIssuesCard: precomputedstableIdToIssuemap (O(1) downstream lookup, drops itemBuilder cost from O(n²) to O(n)).computeVisibleIssuesfilter extended for multi-parent semantics: a downstream is hidden from top-level when any reaching root is visible; surfaces standalone only when every parent is suppressed.AiContextBuilder: prompt section uses singular "Root cause issue" / plural "Root cause issues" label depending onrootCauseIds.length; caps the joined list at 5 with(+N more)suffix.- Tests: +5 (multi-parent 3×3 fan-in pin, rule-ordering invariant under input-shuffle, multi-parent confidence suppression, full-pipeline integration via correlator, multi-parent visibility-filter triad). ~70 existing assertions migrated from
.rootCauseId→.rootCauseIds.
0.24.1 #
Cross-detector polish for stream_resource_growth.
CausalGraphRule: 3 new edges so retained-stream emissions surface as causes of co-firing memory issues.stream_resource_growth → heap_growing,stream_resource_growth → heap_near_capacity,stream_resource_growth → gc_pressure. Mirrors theuncached_imagesandexcessive_keep_alive:*patterns.- Edge enumeration is asymmetric across consumers:
CausalGraphRule.activeEdges(Markdown export, session summaries) returns every distinct cause→effect pair, so a 3-cause × 3-effect memory co-fire surfaces all 9 edges.CausalGraphRule.apply(UI annotation) remains single-owner — each downstream gets onerootCauseIdchosen by severity-then-index, and losing roots render as standalone cards. Multi-parent UI rendering is deferred to a future cut. - Schema regression guard:
ProfileCaptureSchema.parseFileround-trip test for the 4 detector-sideextraTraceArgskeys (topGrowthClass,topGrowthDelta,watchlistClassesGrowing,samplesInWindow) so a future schema tightening with a key allowlist cannot silently disable the detector's trace args. - Tests: +6 (4
activeEdgesedge tests + 1 negative control, 1apply()single-owner pin for the 3-cause memory fan-in, 1 schema round-trip).
0.24.0 #
New StreamResourceDetector (vmOnly) flags likely retained async resources via getAllocationProfile class-instance diff, gated on a recent MemoryPressureDetector.heap_growing emission. 18 → 19 detectors.
StreamResourceDetector: polls allocation profile at most once perstreamResourceSampleSeconds(default 10s); tracksinstancesCurrentfor a hardcoded watchlist of dart:async / dart:io / web_socket_channel suffixes (StreamSubscription,_BroadcastSubscription,_ControllerSubscription,StreamController,_SyncBroadcastStreamController,_AsyncBroadcastStreamController,_WebSocketImpl,WebSocketChannel) plus rxdartPublishSubject/BehaviorSubject/ReplaySubjectwhenclassRef.library.uricontains rxdart. Emitsstream_resource_growth.warningonly when (a)MemoryPressureDetector.isHeapGrowingActivereturns true within the recency window (default 30s), (b) ≥2 watchlist classes show ≥3 of 3 ascending transitions across a K=4 sample window, (c) sum of per-class net deltas exceedsstreamResourceMinDelta(default 50). Confidencelikely. TierreproducerOnly.- Suffix-match (
endsWith) shields against private-class renames across Flutter SDK versions. 20s warmup window suppresses cold-start subscription accumulation; window/warmup re-engage onpause()/resume()/resetCaptureState(). Re-entrancy guard (_pollInFlight) + 3-failure backoff (60s default). 3-cycle cooldown holdsdedupIdentityMicrosstable so the controller dedup composite key collapses successive fires to one trace record. MemoryPressureDetector: new publicbool isHeapGrowingActive([int? windowMicros])getter backed by_lastHeapGrowingEmittedAtMicrosstamp. Decoupled from_issues.any(...)retention so a long-resolved heap_growing cannot latch downstream gating. Cleared onvmConnected=false/reset()/dispose().Sleuth.streamResourceDetectorstatic accessor (kReleaseMode-guarded).StreamResourceDetectorexported from the public barrel.- 5 new
DetectorThresholdsfields:streamResourceSampleSeconds,streamResourceMinDelta,streamResourceWarmupSeconds,streamResourceHeapGrowingRecencyMicros,streamResourcePollFailureBackoffSeconds. - New
FixHintBuilder.streamResourceGrowthcross-referencesheap_growing/native_memory_growingas alternative memory-pressure causes. - IssueEncyclopediaPage entry for
stream_resource_growthinissue_explanation_builder.dart. - Library-URI gate on core watchlist:
endsWithmatches only fire whenclassRef.library.uriisdart:async,package:web_socket_channel, or (for WebSocket only)dart:io. dart:io's_HttpClientStreamSubscriptionis explicitly excluded — it self-cancels on response completion and would otherwise produce false positives on every network-heavy app. - Cooldown semantics: wall-clock deadline (
cooldownSeconds, default 30 s) — survives VmService disconnect mid-cooldown without leaving a stale issue pinned to_issuesuntil the next non-null poll arrives. Re-emit during cooldown refreshesdetectedAt(so UI does not show a stale stamp) while preservingdedupIdentityMicrosfor controller composite-key dedup. - Reset-generation guard: in-flight
_pollAllocationProfilesnapshots_resetGenerationat start; if_clearRetainedStateruns between theawaitand the result handler, the result is discarded. Without this, leg-N-1 sample data could write into leg-N's freshly-cleared_perClassWindowand break capture-mode scenario isolation. windowSizeconstructor assertion:assert(windowSize >= 2)rules out the empty-listRangeErrorpath in_evaluateWindowif a future caller passes 0 or 1._ingestProfileper-poll aggregation: sumsinstancesCurrentacross every class that maps to the same suffix bucket and appends exactly one sample per suffix per poll. For suffixes previously seen but absent from the current poll (the leak was fixed and GC reclaimed every instance), appends0so a stale ascending window ages out instead of re-firing every cooldown cycle. All-zero windows are dropped to bound map growth._matchWatchlistlongest-suffix-match: a class named_SyncBroadcastStreamControllermatches the specific suffix instead of being shadowed by the genericStreamControllerbucket. First-match would also collapse multiple distinct controller flavors into one window, corrupting the ascending-transitions check._dropEmissionStatehelper consolidates clears across cooldown lapse + transient gate failure + window underflow paths, eliminating drift between code paths that previously cleared a subset of emission fields.@visibleForTestingannotation onallocationProfileFetcherForTestconstructor parameter so production callers cannot inject a custom fetcher.- Tests: +17 unit (warmup, sample-rate gate, single-class-no-emit, heap_growing-off-no-emit, sub-threshold-no-emit, co-fire emission, extraTraceArgs key set, cooldown stable identity within window, cooldown detectedAt refresh, wall-clock cooldown expiry, non-monotone-no-emit, null-fetcher backoff, rxdart library-URI gate × 2,
_HttpClientStreamSubscriptionexclusion, resetCaptureState, disabled, vmConnected-false). +5 reproducer (deliberate-leak harness, heap_growing-off, flat-no-emit, rxdart, cooldown).
0.23.0 #
GpuPressureDetector.raster_dominance idle false-positive fixed; HeavyComputeDetector issues persist past one VM batch.
GpuPressureDetector: ratio numerator uses MAX-of-frame raster gated bymaxFrameRasterFloorUs(default 8000us). New ctor param tunable for 120Hz / Impeller / low-power-mode.RenderPipelineAnalyzer: raster admitted assuspectedPhaseonly when one frame crosses 8000us.HeavyComputeDetector: emissions persistemissionPersistence(default 10s) via monotonicStopwatch— survives VM poll cadence + system clock jumps. Retained state clears onisEnabled=false/vmConnected=false.PerformanceIssue.sourceRoute: detectors that retain issues stamp the route at emission. Aggregator preferssourceRouteover live route, so post-emission navigation cannot reattribute. Wired throughHeavyComputeDetector+PlatformChannelDetectorviasourceRouteProvider.- CSV Import demo row choices
[50K, 200K, 500K]+ post-parse sort. 500K cap avoids OOM / iOS watchdog. - Tests: +5 gpu_pressure (idle-suppression, floor-triad, spike+idle, 12ms critical); +7 persistence (heavy_compute Stopwatch TTL × 3, lifecycle clear × 2, route-during-TTL × 2; platform_channel route-during-cooldown × 2).
- Doc cleanup: 21 historical spec files +
HANDOFF.mdremoved; example/README aligned with 18-detector + 500K demo cap; README logo path switched to relative (doc/logo.png) for pub.dev rendering against private repo. Added FastlaneTRACK_WIDGET_CREATIONpatch tip for iOS profile archives. README accuracy fixes: Repaint detector moved from VM-Only to Hybrid section (matchesDetectorLifecycle.hybrid);heavyComputeGapMsconfig example corrected to 8 (was drift-stamped 200). Pubspec description sharpened — leads with in-app overlay differentiator, drops abstract layer names. doc/validation_ledger.mdNon-Detector Components: dropped stale v0.16.7 promise; framework live, 0 components registered; tier raises deferred to next non-detector formula change (4 candidates listed:IssueRanker,RouteSession.healthScore,RecurrenceTrend, FPS formulas).- Reference-device matrix slimmed to iPhone 12 / iOS 17.5 only (
approvedDevicePairs). iPhone 13 mini + Pixel 7 removed — never used by real captures (only synthetic fixtures, swapped). Anchor fixture re-pinned (SHA-256 updated). Android coverage gap explicitly documented indoc/reference_devices.md. 5 device-mismatch tests skipped pending second approved device pair.doc/validation_matrix.md+doc/capture_procedure.md+example/lib/custom_detectors/README.mdswept for stale 23-detector / iPhone 13 mini / Pixel 7 references.
2,883 tests; fvm flutter analyze clean.
0.22.0 #
sustained_jank.critical runtimeVerified raise withdrawn. Bracket axis (sliding 240-frame-window severeCount) cannot composably bracket against operator-claimed K — ambient severe frames accumulate in the same window. Future raise needs detector-level baseline subtraction (RebuildDetector.setBaseline(int) pattern).
- Removed: 3
sustained_jankcapture JSONs,frame_timing_sustained_jank_capture_screen.dart, example-app tile, retainedOrphans manifest entries. - Reproducer-tier coverage of
sustained_jankretained intest/validation/frame_timing_reproducer_test.dart. - Distribution unchanged (12 family-severity pairs across 9 stableIds).
- README distribution paragraph + frame_timing_detector source comment refreshed to current state.
0.21.0 #
RepaintDetector.excessive_repaint.warning raised to runtimeVerified via perStableIdTier on three iPhone 12 / iOS 17.5 / Flutter 3.41.4 captures. Base tier stays reproducerOnly; excessive_repaint_debug and parametric repaint_debug_<typeName> are not over-claimed.
- Capture-mode plumbing:
lastObservedPaintCount+peakObservedPaintCountgetters,flushPaintEvaluation()(refreshes onlylastObservedPaintCount; never updates peak so the exported magnitude always matches an emittedobservedPaintCountarg),resetCaptureState()(per-leg accumulator clear, also called fromSleuthController.resetCaptureStatefor cross-detector parity). VM emission stampsextraTraceArgs.observedPaintCount+dedupIdentityMicros. - Bracket:
threshold: 30 paints,bracketAtTolerance: 0.50(at-band [30, 45]),aboveCeilingMultiplier: 2.0(above-band ceiling 60 sits strictly under the> 60critical-tier fire boundary). Capture screen mounts 32 distinctCustomPaintwidget classes so the per-widget debug gate stays sub-threshold and emission flows through the VM aggregate path. Sleuth.repaintDetectorstatic getter (capture-screen access).Sleuth.lastCaptureExportFailuresurfaces the most-recentexportCaptureJsonnull-return reason in-app.- 12 effective runtimeVerified family-severity pairs across 9 unique stableIds. Base distribution unchanged (16/18 reproducerOnly, 2/18 runtimeVerified).
2,870 tests passing; fvm flutter analyze clean.
0.20.2 #
Example-app polish. No detector logic, public API, or schema change.
example/lib/main.darttile subtitles trimmed to ≤40 chars so 360 dp phones render single-line without ellipsis. Combined-chat tile keepsSetState(dropsImage) to advertise actual detector coverage.example/lib/demos/heavy_compute_demo.dartdescription drops the hard "300 ms" claim → "complete in under a few hundred ms on modern devices" so CPU-throttled devices don't break the promise.example/lib/demos/network_stress_demo.dartsearch builds URL viaUri.parse(...).replace(queryParameters: {'q': query})— RFC 3986 percent-encoding for special chars (+,&,=,#, unicode).
2,862 unit + integration tests passing; fvm flutter analyze clean.
0.20.1 #
FrameTimingDetector and RebuildDetector stamp extraTraceArgs.lifecyclePhase: 'startup' | 'steady' on each emission. README + dartdoc gain a "Measurement window" note: Sleuth reports frame total duration from FrameTiming (build-to-raster span), not vsync delivery cadence.
- New
DetectorThresholds.startupPhaseWindowSeconds(default 5). Classification readsTimeline.nowat emission time — emission-time semantics, not event-time. A startup-phase frame whose callback delivery is delayed past the window boundary tags'steady'. Differs fromShaderJankDetector.shaderWarmupContext(per-event timestamp); the two tags are related but not aligned at the boundary. - Buffer-aggregated emissions (
sustained_jank60-frame,rebuild_activity1-second, raster-cache 30+ frames) tag from emission-timeTimeline.now. A buffer straddling the boundary tags'steady'onceTimeline.nowexceeds the threshold. - Null
Sleuth.dartEntryMonotonicUs(init not called) or negative delta omits the key rather than fabricating a value. rebuild_activityruntimeVerified bracket axis (observedRebuildRate) co-exists with the new key. Audit-gatevalidateBracketreads named keys directly; multi-key emissions remain extractable.- The tag is observable in capture-mode trace records and audit-gate replay; not serialized into saved JSON snapshots.
- Both detectors expose
appStartMonotonicUsForTestconstructor parameter for deterministic tests.
2,862 unit + integration tests passing; fvm flutter analyze clean. No detector logic, public API, or schema-version change.
0.20.0 #
BREAKING: 5 low-value detectors removed. Distribution: 23 → 18 detectors.
Removed #
DetectorType.animatedBuilder— subset ofrebuild_detector(AnimatedBuilder misuse manifests as rebuild storms; covered upstream).DetectorType.opacity— symptom-of-symptom (Opacity→saveLayer→ jank already caught byframe_timing.jank_detected).DetectorType.shallowRebuildRisk— predictive heuristic; real signal caught byrebuild_detectorfrom VM-timeline evidence.DetectorType.nestedScroll— Flutter's ownVertical viewport was given unbounded heightdiagnostic is more authoritative.DetectorType.globalKey— correctness lint, not perf; framework throws on duplicateGlobalKey.
Orphaned config fields removed: SleuthConfig.maxGlobalKeys, DetectorThresholds.shallowRebuildMaxDepth, DetectorThresholds.animatedBuilderMinSubtreeSize.
Migration #
Drop the 5 removed DetectorType references from enabledDetectors. rebuild_detector + frame_timing still surface AnimatedBuilder, opacity-jank, and rebuild-storm patterns from runtime evidence.
// BEFORE (v0.19.x):
SleuthConfig(enabledDetectors: {
DetectorType.opacity, DetectorType.rebuild, DetectorType.frameTiming,
});
// AFTER (v0.20.0):
SleuthConfig(enabledDetectors: {
DetectorType.rebuild, DetectorType.frameTiming,
});
v0.19 snapshots remain readable in v0.20 — serialization is stableId-keyed; encyclopedia + causal-graph rules retain removed-stableId entries for replay context. Users pinned at ^0.19.x will not auto-upgrade.
Distribution #
16/18 reproducerOnly base + 2/18 runtimeVerified base. 11 effective runtimeVerified family-severity pairs across 8 unique stableIds (unchanged — none of the 5 removed carried raises).
2,851 unit + integration tests passing; fvm flutter analyze clean. Benchmark thresholds in test/benchmark/ are machine-load-sensitive and may flake on slower hardware.
Releases prior to v0.20.0 are archived in CHANGELOG.archive.md.
