diff --git a/CHANGELOG.md b/CHANGELOG.md index 0051e048..44d10136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,315 +1,87 @@ -## Unreleased +## 1.0.0 + +A redesigned overlay, issues that carry the number behind them, and a +public API that follows semantic versioning from this release. The +matching sidecar is `sleuth_mcp` 0.10.0. Publish both together, because +sidecar 0.9.0 refuses 1.0.0 apps. The +[full release notes](https://github.com/Harrys76/sleuth/blob/main/doc/release_notes/1.0.0.md) +list every change. ### Breaking - `package:sleuth/sleuth.dart` exports only the names listed in - `doc/api_1_0_0.md`, each through a `show` list. + `doc/api_1_0_0.md`, each through a `show` list, and follows semantic + versioning. - Capture and validation tooling moved to the new - `package:sleuth/validation.dart`, which is outside the semver promise - and whose declarations are `@experimental`: - `EvidenceTier`, `DetectorMetadata`, `DetectorMetadataProvider`, - `BracketSpec`, `ComponentMetadata`, `ComponentMetadataProvider`, - `ProfileCaptureSchema`, `kSleuthPackageVersion`, `PollTimings`, - `StreamResourcePollResult`, and the detector types `RepaintDetector`, - `MemoryPressureDetector`, `StartupDetector`, `TrackedResourceDetector`, - `ImageMemoryDetector`, `GpuPressureDetector` and `FrameTimingDetector`. -- No longer exported: `ServiceExtensionRegistry`, `FixHintBuilder`, - `SessionMarkdownExporter`, `JankCaptureBuffer`, `FrameBudget`, - `FrameRateSource`, `resolveFrameBudget`, `RecurrenceTrend`, - `RecurrenceEntry`, `TrendDirection`, `buildWidgetHeatMap`, - `InteractionContextDisplay`, `ObservationSourceDisplay`, - `ConnectionMode` and `kMcpEnvelopeSchemaVersion`. -- `Sleuth.enrichStartupWithVmData`, `Sleuth.dartEntryMonotonicUs` and - `Sleuth.notifyControllerDisposed` are gone; only the package called - them. `StartupMetrics.dartEntryMonotonicUs` still carries the Dart entry - time. -- The capture statics on `Sleuth` (`markScenarioBegin`, `exportCaptureJson`, - `flushTimelineNow` and the others) and the accessors typed with - `validation.dart` types are `@experimental`. -- `RouteSession.endedAt` and `RouteSession.scanCycleCount` are read-only. -- `SleuthThemeData` drops the tokens the overlay no longer draws: the - confidence, effort and source colours with `confidenceColor`, - `effortColor` and `sourceAccentColor`, `sourceAccentWidth`, the VM and - FRAME badge pairs, the instrumentation banner pair, the three AI shimmer - colours, `aboutBackground`, `cardJankFlash`, `effectsBadge`, - `fixHintText`, `disclaimerText`, `highlightLabelText`, `fontXxl`, - `fontDisplay` and `focusRingWidth`. `fromColorScheme` no longer maps - `errorContainer`. + `package:sleuth/validation.dart`, which is outside the semver promise. + Import it for `DetectorMetadata`, `BracketSpec`, `EvidenceTier`, + `ProfileCaptureSchema`, `PollTimings`, `kSleuthPackageVersion` and the + detector types the capture screens use, such as `RepaintDetector`. +- Fifteen internal names are no longer exported, among them + `FixHintBuilder`, `SessionMarkdownExporter`, `JankCaptureBuffer`, + `FrameBudget` and `ConnectionMode`. `Sleuth.enrichStartupWithVmData`, + `Sleuth.dartEntryMonotonicUs` and `Sleuth.notifyControllerDisposed` are + gone, and the capture statics on `Sleuth` are `@experimental`. +- `RouteSession.endedAt` and `scanCycleCount` are read-only, and + `SleuthThemeData` drops 29 tokens and 3 helpers the overlay no longer + draws. - A minor release can add values to `DetectorType`, `IssueCategory`, `ObservationSource`, `PipelinePhase`, `InteractionContext` and - `FixEffort`; a `switch` over them needs a default branch. + `FixEffort`, so a `switch` over them needs a default branch. +- `FrameVerdict.relatedIssues` lists only the issues that can explain the + verdict's phase. +- Highlighting an issue outlines only its own widget, never another + widget of the same category. ### Added -- Issues carry the number their detector reports with the detector's - warning and critical lines: `PerformanceIssue.measurement`, a final - class `IssueMeasurement` with `value`, `unit`, `warningAt`, `criticalAt`, - `window`, `warningStrict` and `criticalStrict`. A value reaches a line at - or above it, or only above it when that line is strict, and every - measurement is higher-is-worse. A line that is absent or not finite has - no flag. Issue JSON gains an optional `measurement` object; `windowMs` - keeps microseconds as a fraction. -- Built-in detectors set the measurement on 43 issue families. Most titles - show the value; `raster_dominance` titles show the raster-to-UI ratio and - `heap_near_capacity` titles a percent of the budget instead. Six - families report a pattern rather than an amount and leave it null - (`always_repaint_painter`, `runtime_font_loading`, `expensive_gpu_nodes`, - `layout_bottleneck`, `sliver_fill_remaining_scrollable`, - `sliver_to_box_adapter_shrinkwrap`), as do `event_loop_stall` when the - VM gave no stall length and `non_lazy_shrinkwrap` when the child count is - unknown. -- The issue's severity is the detector's and can disagree with the - severity the value reaches, in either direction. `rebuild_activity`, - `excessive_repaint` and the debug rates hold a severity until the value - stays low, and `heap_near_capacity` tolerates one sample under its line, - so a value can sit below the line of the severity shown. The debug rates - go critical only on the latest window while the value averages two, so a - warning can also carry a value above the critical line. -- `jank_detected` and `sustained_jank` measure over the vsync span of the - frames they judged. The 240-frame buffer can make it shorter than 10 s, - capture mode judges the whole buffer, and frames without vsync times - give no window, as in `raster_dominance`. `rebuild_activity` and - `excessive_repaint` give the length of the VM window behind the share, - 1 to 1.5 s. -- `MeasurementUnit` is a class with constant instances instead of an enum, - so a later release can add a unit without breaking a `switch`. Units - compare by name, a custom detector can name its own, and a reader keeps - a unit it does not know by name instead of dropping the measurement. -- `SimpleStructuralDetector.report` takes an optional `measurement`. -- Snapshots and `ext.sleuth.issues` carry `notUsefulMarks`, the issues - marked Not useful in the overlay this session (oldest first, at most - 200), each with the issue's key, title, category, severity, confidence, - measurement, route, `markedAt` and whether it is still reported - (`live`). Like `suppressedCount` the key is metadata: written only when - there is a mark, whatever the projection, and never a `SnapshotSection`. - `SessionSnapshot.notUsefulMarks` and `NotUsefulMark` read it back. - `doc/mcp_schema.{json,md}` document it and the issue `measurement` - shape, and the sidecar lists it in `snapshotMetadataKeys`. -- `WidgetHighlight.occurrenceId`: the identity of the element a highlight - was measured from, equal to the `occurrenceId` of its issue. -- `DebugSnapshot.forcedRebuildsOf` (debug callbacks): for each widget type - a parent's build rebuilt, the types whose builds forced it, framework - widgets included, and how often. `forcedTypesCapped` reports its caps. -- `SleuthThemeData.severityGraphicColor` and the optional - `severityCriticalGraphic`, `severityWarningGraphic` and - `severityOkGraphic` tokens: the colour of severity borders, icons and - dots, which need 3:1 where text needs 4.5:1. The dark default uses the - severity fills, brighter than its pale text shades; the light preset - sets red-600, amber-600 and emerald-600, so its chips read red and - orange instead of two dark browns; the high-contrast presets and - `fromColorScheme` fall back to the severity text colours. -- `SleuthThemeData.cardDivider`, the line under the status row. It - defaults to `0xFF4B5563` in dark and `0xFFB4BAC4` in light, `border` in - the high-contrast presets and `outlineVariant` in `fromColorScheme`. - -### Changed - -- The card header shows a run label, such as "Debug · VM" or "Profile · no - VM", in place of the VM+, FRAME and DBG badges. A narrow card drops the - "Sleuth" name first, then "· VM", and at last keeps only the dot, which - is filled with the VM and a ring without it. A screen reader always - hears the full label. -- A More button replaces the theme button. Its menu holds Startup (TTFF), - Rebuild stats (the route's count, opening the drilldown), Hidden issues - (with a count, only when there are any), Encyclopedia, Guide, Copy - snapshot and Theme (Auto, Light, Dark). It opens as a panel inside the - card. Back, Escape, a tap on the scrim or a tap on the app closes it, - and while it is open the app and the card body are hidden from screen - readers. Opening it moves keyboard focus to the first row that can act - and screen reader focus to the panel. More, the rows and the theme - segments draw a 2 px `checkboxActive` outline while the keyboard has - focus. A failed copy offers Try again. -- Tab and Shift+Tab move focus, and Enter and Space activate the focused - control, inside the card and its pages. The overlay sits above the - app's `WidgetsApp`, so the app's default shortcuts never reached it; a - focused text field keeps Enter and Space. -- The footer, the startup banner, the rebuild stats banner, the confidence - caption and the frame-rate detail row are gone. Hidden issues, Copy - snapshot, startup and rebuild stats moved into the More menu. -- The severity chips lead the status row ("2 critical"), with a filled - icon while on and an outlined one while off. The frame rate sits at the - end of the row. "Showing X of Y" appears only while a severity is off, - since the More menu counts hidden cards. A screen reader hears the line - change, and Show all moves its focus to the first chip. -- The card is at least 320 px wide, or the screen less 16 px margins on - a narrower screen, and stored narrower widths are raised. Its background - is opaque. -- The resize grip works on a maximized card too: the first drag or resize - step makes it a normal card at the size and place it has, then resizes - that. The list ends with 48 px of padding, so the grip does not cover - the last card. -- The overlay no longer uses `TweenAnimationBuilder`, so it is off the - debug instrumentation's framework denylist and an app's own uses count. -- The collapsed issue card is flat, with no source bar on its left edge. - A severity mark replaces the emoji: a disc or triangle filled with the - severity colour, with a white or near-black mark inside at 3:1 or more - against the fill; the discs carry a white ring. Where neither the fill - nor the ring reaches 3:1 on the card (the light presets' warning and OK - marks), the shape is outlined in a darker shade of its colour. The - title runs to 2 lines (3 above 1.3x text), or in full while the card is - expanded. -- One meta line under the title replaces the badges: category, - confidence, "seen X of Y scans, label" and "N effects", led by the Jank - and New tags. New shows for 2 seconds in place of the wider accent. -- A Debug timing tag replaces the confidence on a timing issue in a debug - run: a time-based measurement (milliseconds, seconds, percent or - frames) from the VM timeline or `FrameTiming`. The per-card "Debug mode - result" note is gone. -- `jank_detected`, `sustained_jank` and `slow_startup_ttff` carry - `observationSource: frameTiming`, the source of their times. Startup - said `structural` before and the jank issues carried none. -- `wrap_layout_bottleneck`, `excessive_repaint_boundary` and issues from - `SimpleStructuralDetector.report` carry the `widgetName` their highlight - carries (`Wrap`, the scrollable's type, the reported widget's runtime - type), so their cards keep the crosshair. Their overlay keys now - include the name, so such a card hidden under an older release shows - again. -- A crosshair replaces the highlight checkbox, labelled "Show on - screen". Before the first scan it reads "Finding" until the scan looks - the widget up, and a second tap cancels. It shows for an issue that - names a widget, and for the issues that list several: - `always_repaint_painter`, `missing_repaint_boundary`, - `layout_bottleneck`, `uncached_images` and `expensive_gpu_nodes` - ("Show this issue's widgets on screen"). Other cards without a widget name no - longer offer a highlight that could not find anything. -- On a card that lists several widgets the crosshair outlines the first - one on screen, and the bar shows its place ("1 of 3") with Next, which - moves the outline to the next widget and wraps after the last. The - shown place changes with Next, not with each scan, so a screen reader - does not read the bar again after every scroll. A bar that steps opens - with focus on Next, keeps its edge unless the new outline runs under - it, and shields its old place for a double tap's span when it moves. - When the outlined widget leaves the screen, Next goes to the widget - that followed it, and shows while any other widget remains. The detectors tie those highlights to their issue - with the new `WidgetHighlight.stableId` and an element identity in - `WidgetHighlight.occurrenceId`. The card body - and the crosshair draw the focus outline in keyboard mode. -- An expanded card no longer draws the pin icon. Its semantics hint - reads "Pinned while expanded". -- An expanded card stays until it is collapsed, even when its issue stops - being reported or folds under another card. A card whose issue is gone - leads its meta line with "Not detected right now" and offers no - crosshair, and the empty states wait until it is collapsed. -- A press on a card, a More menu row, a theme segment or the crosshair - draws a light tint instead of the fallback theme's 40 % grey ripple. -- The overlay no longer uses `Checkbox`, so it is off the framework - denylist and an app's own checkboxes count. -- The guide's legend describes the new card. The "Jank flash" card - colour, which the overlay never drew, is gone from it. -- `badgeDbgBg` and `badgeDbgText` draw the Debug timing tag. -- The expanded card shows its details below a divider, at the full card - width. An issue with a measurement opens with a Measured row: the value - and what it is ("10.0 ms in one build on the UI thread", with the - measurement's window where it has one), a gauge with the warning and - critical lines, and a line when the severity is held above the value or - judged on another reading. The issue's detail - follows, with what the number does not say (failing URLs and status - codes, for instance). The issues notifier fires when the Measured row's - text, a gauge line or the issue's scaffold changes, and not for a change - the row rounds away. -- Labelled rows follow: Where (the widget, and its `file:line` with Copy - when the run tracks widget creation), Route, When ("Last seen 2 s ago · - in 5 of the last 8 scans", with the interaction; the time is the - card's own when cards share a stable id), Evidence (the - detector's reason and the confidence) and Check in (the DevTools view). - The labels stack above their values when large text makes the label - column wider than a third of the card. "About this detection" is gone, - with its profile-mode line that said timing "matches production", and - so are the Ancestors, Widget, During and Source lines and the debug - fidelity tags. -- Caused by and Effects rows open the related card: a card in the list - expands, scrolls into view and takes focus; a hidden or filtered-out one - shows a toast with Show, which keeps the open cards expanded; an effect - folded under a card opens its detail and fix under its row. A related - issue no longer reported stays as plain text. Rebuild and setState - cards from the current screen link to its Rebuild stats once it has - counted rebuilds, with the count kept current on every scan, and the - slow startup card links to Startup metrics. -- A tap on an expanded card's details no longer collapses it; the header - does. Hide moves focus to the card that takes its place, scrolled into - view, and hiding a card whose issue is gone offers no Undo. -- The fix opens with "Fix" and its effort ("Fix · medium effort"). Code - lines after a line ending in a colon sit in a monospace block with Copy - code; a code run is lines with brackets, `;` or `=>`, commands, comments - and the indented lines of a call, and a bullet or a sentence ending in a - full stop is prose. The effort badges, and the keyword guess for an - issue without `fixEffort`, are gone. -- Labelled buttons replace the links and icons: Show on screen, Learn - more, Ask AI, Copy details, Hide and Not useful, each a 48 px target. - Not useful marks the issue for the session's exports and MCP output and - says so; a second tap removes the mark. Ask AI's icon no longer - shimmers, so `ShaderMask` and `AnimatedBuilder` are off the framework - denylist and an app's own uses count. -- In a debug run with `enableDebugCallbacks`, rebuild and setState cards - show Why it rebuilds: the rebuilds the widget started itself (a - setState, a changed dependency or a listenable) and those its parents - forced, naming the parent that forced the most and its share. The row - reads only counts from the card's own screen (a card kept from another - screen asks to open it), says when the debug counters left widget - types out, and says when rebuilds are not counted: the callbacks are - off, `advanced.rebuildAttribution` is off, or DevTools holds the - rebuild callback. -- Status lines replace "No issues detected". Starting; Warming up, with - a progress bar; No VM link, with the checks off and partly off counted - from the session's coverage, a copyable `flutter run --profile --no-dds` - and a Guide link; No issues found, with the checks on and the opt-in - checks off; VM link lost, with the reconnect attempt; and stopped - retrying, with Reconnect. With cards listed, an expanded card whose - issue is gone included, only a lost link takes a line above them, and - the status row names the state when it counts no card. Each line is a - live region: its label names the state (stopped retrying has its own) - and its value holds the numbers, so the warm-up countdown does not - repeat it. -- One debug banner replaces the debug-mode and instrumentation banners. - Its text stops growing at the chrome limit, dismissing it lasts for the - session, and focus then moves to the severity chips. -- The trigger shows its count in debug runs too; the warning badge is - gone. -- While an issue's widget is outlined, the card shrinks to a bar at the - screen edge away from the widget: "Showing ", or " is - off screen" once it scrolls away, detaches or sits behind the keyboard, - until a later scan finds that element again. Back to issues, system - back and Escape (also while an app text field has focus) bring the - card back as it was (window state, size and list offset) with focus on - the crosshair. The bar's close ends the outline with the dashboard. - When the issue is no longer reported the card comes back and a toast - says so. The bar is never stored. Outlines follow a rotation or a - keyboard at once instead of at the next scan, and the outline's label - stays below the status bar. -- An outline no longer turns the flagged-widget markers on: while the - bar is up only the issue's widget is outlined, unless the markers were - already on. Sleuth collects highlights while an outline is shown or - looked up, and finds the issue's own element again after it detaches. -- The header's markers toggle is named "Mark flagged widgets" and read as - on or off. It turns the markers alone on and off, never an outline or - a lookup, and turning it off hides them at once instead of at the next - scan. -- Toasts use the theme's text and card colours swapped, regular 12 px - text and a bold action label in the accent at 4.5:1, and span the - screen up to 480 px. -- The outline's label sizes to its text, up to 350 px wide and two - lines whatever the width of the widget it names, follows the text scale - up to the chrome limit, and draws its text in the colour that reads on - its severity fill. It stays below the highlight bar when - the bar sits at the top. -- The guide describes the new expanded card and Not useful. +- `PerformanceIssue.measurement` (`IssueMeasurement`, `MeasurementUnit`) + holds the number a detector reported, with its warning and critical + lines, on 43 issue families. Issue JSON gains an optional + `measurement`. +- The overlay's Not useful button marks an issue for the session, and + snapshots and `ext.sleuth.issues` list the marks in `notUsefulMarks`. +- `DebugSnapshot.forcedRebuildsOf`, `WidgetHighlight.occurrenceId` and + `WidgetHighlight.stableId`, `SleuthThemeData.cardDivider` and + `SleuthThemeData.severityGraphicColor`. +- A test that pins the public names, and a CI job that diffs the API + against the latest published 1.x release and fails on a breaking change + without a major version bump. + +### Overlay + +- A run label ("Profile · VM") replaces the VM+, FRAME and DBG badges. + A More menu holds Startup, Rebuild stats, Hidden issues, Encyclopedia, + Guide, Copy snapshot and Theme, and the footer and banners it replaced + are gone. +- The collapsed card has a severity mark, a title of up to two lines + (three at large text sizes) and one meta line. The expanded card shows the measured value on a gauge, + Where, Route, When, Evidence and Check in rows, the fix with copyable + code, Why it rebuilds in debug runs, and labelled buttons including + Not useful. +- Status lines replace "No issues detected": Starting, Warming up, No VM + link, No issues found and VM link lost. One debug banner replaces two. +- The crosshair outlines a card's widget and shrinks the card to a bar. + On a card that lists several widgets, Next steps through them. +- Keyboard navigation inside the card, a 320 px minimum width, toasts in + the theme's inverted colours, and outlined warning and OK marks in the + light themes. +- An app's own `AnimatedBuilder`, `TweenAnimationBuilder`, `Checkbox` and + `ShaderMask` rebuilds count in the per-route rebuild counts again. The + overlay no longer uses them, so they left the framework denylist. +- A new issue list rebuilds only the cards it changes. +- Cards hidden under 0.38 stay hidden. The stored overlay state moves to + `schemaVersion` 2, which 0.38 reads as empty and leaves as it is. The + Hidden page names each key by its id and widget. +- An issue keeps the Jank tag for 10 s after the last late frame that + named it on the current route, instead of following only the latest + frame. -- `FrameVerdict.relatedIssues` lists only the issues that can explain the - verdict's phase: the issues of the phase's category, `event_loop_stall` - for a start delay, and none for a stall or an unknown phase. Jank - symptoms are never listed, so exports carry fewer issues, and the - reason's `Related:` line names an issue of the phase. The overlay's JANK - tag shows only while `jank_detected` or `sustained_jank` is reported. -- Highlighting an issue outlines only its own widget: the element it was - reported for, else the first widget with its name. Another widget of - the same category is no longer outlined in its place. When the widget - is not on screen a toast says "Couldn't find on screen. It may - be scrolled away or not built yet.", and no outline appears later. -- Copying the snapshot from the overlay counts as Sleuth's own work, so - it is left out of the next frame's start delay. A second tap during a - copy is ignored. -- A dismissed debug-mode banner stays dismissed when the dashboard is - closed and reopened. +### Fixed + +- Ancestor chains, fix hints and the card's Where row name each widget's + own `file:line`. A widget type built in several places reported the + first place Sleuth saw. ## 0.38.0 diff --git a/CLAUDE.md b/CLAUDE.md index dbe7816e..b690c429 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ Runtime performance diagnostics package for Flutter mobile apps. 21 detectors ac ```bash # Always use fvm for all Flutter/Dart commands -fvm flutter test --exclude-tags benchmark # Default run (~4,761 tests; wall-clock benchmarks excluded) -fvm flutter test --tags benchmark --concurrency=1 # Wall-clock benchmarks, serial (40 tests) +fvm flutter test --exclude-tags benchmark # Default run (~5,099 tests; wall-clock benchmarks excluded) +fvm flutter test --tags benchmark --concurrency=1 # Wall-clock benchmarks, serial (44 tests) fvm flutter test test/detectors/ # Run detector tests only fvm flutter analyze # Static analysis (must be 0 issues) fvm flutter pub publish --dry-run # Verify publish readiness @@ -21,14 +21,14 @@ rm -rf build/unit_test_assets && fvm flutter pub get # Restore 3.47.6 resoluti # Example app cd example && fvm flutter run --profile # Profile mode (recommended) cd example && fvm flutter run # Debug mode -cd example && fvm flutter test # Cookbook smoke, demo and capture-screen widget tests (348 tests) +cd example && fvm flutter test # Cookbook smoke, demo and capture-screen widget tests (353 tests) # Demo walk on a device (iOS or Android): runs each demo's controls from tool/demo_walk_plan.json # and records issues per phase; start the app with `fvm flutter run --profile --no-dds -d ` fvm dart run tool/demo_walk.dart ws://://ws [--android ] [--only "Title|Title"] # MCP sidecar (packages/sleuth_mcp/) -cd packages/sleuth_mcp && dart test # Sidecar tests (653 tests) +cd packages/sleuth_mcp && dart test # Sidecar tests (659 tests) cd packages/sleuth_mcp && dart analyze # Sidecar static analysis ``` @@ -36,7 +36,8 @@ cd packages/sleuth_mcp && dart analyze # Sidecar static analysis ``` lib/ - sleuth.dart # Public API barrel file + Sleuth entry point + sleuth.dart # Public API barrel file (show lists only) + Sleuth entry point + validation.dart # Capture/validation tooling + 7 detector types; outside semver, @experimental src/ models/ # Data classes: PerformanceIssue, FrameStats, FrameVerdict, BaseDetector detectors/ # 21 detector implementations (one file per detector) @@ -46,7 +47,8 @@ lib/ vm/ # VmServiceClient, TimelineParser debug/ # DebugInstrumentationCoordinator, DebugSnapshot ranking/ # IssueRanker (weighted composite scoring) - ui/ # Overlay widgets: FloatingIssuesCard, IssueCard, AiChatPage, IssueEncyclopediaPage, HiddenIssuesPage, TriggerButton, OverlayToast, SleuthTheme; OverlayUiState + overlay_filters + ui/ # Overlay widgets: FloatingIssuesCard, IssueCard (+ issue_card_details), MoreMenu, status_lines, SleuthHighlightBar, HighlightOverlay, MeasurementGauge, SleuthSeverityIcon, AiChatPage, IssueEncyclopediaPage, HiddenIssuesPage, TriggerButton, OverlayToast, SleuthTheme; OverlayUiState + overlay_filters + startup/ # StartupTracker (startup tracing state behind Sleuth.init / startupMetrics) persistence/ # SleuthStateStore seam + InMemorySleuthStateStore utils/ # WidgetLocation helper, FixHintBuilder, TypeNameCache, OverlayOwnership, RateHysteresis validation/ # EvidenceTier, DetectorMetadata, ComponentMetadata, ProfileCaptureSchema @@ -75,7 +77,13 @@ test/ ## Current state -**v0.38.0** (current): detectors-only release on top of 0.37.0. Jank is a missed deadline with Sleuth's own start delay excluded, the VM timeline comes from the `Timeline` stream outside capture mode, new `event_loop_stall`, `material_ui` / `cupertino_ui` support, GC counted once per collection and young-only, `heap_growing` floor leg, 12 more runtimeVerified pairs (27 across 19 stable ids). Sidecar `sleuth_mcp` 0.9.0. The paragraphs below hold the detail per area. +**v1.0.0** (current): redesigned overlay, issue measurements, Not useful marks and the 1.0 public API on top of the 0.38.0 detectors. Sidecar `sleuth_mcp` 0.10.0 (pins 1.0.0, lineage = major from 1.0, accepts 0.38; `compare_snapshots` adds `versionWarning` `sleuth_version_differs:` when the two releases differ, build metadata ignored; the three prompts have the model quote `measurement` and match `notUsefulMarks` keys, listing marked issues last in the triage, setting them aside in the memory audit and keeping them in the release gate's verdict). Release notes `doc/release_notes/1.0.0.md`; plan `doc/plan_overlay_1_0_0.md`. + +**v1.0.0 API**: `lib/sleuth.dart` exports with `show` lists only (62 names + `Sleuth`); `lib/validation.dart` holds `EvidenceTier`, `DetectorMetadata`(+Provider), `BracketSpec`, `ComponentMetadata`(+Provider), `ProfileCaptureSchema`, `PollTimings`, `StreamResourcePollResult`, `kSleuthPackageVersion` and Repaint/MemoryPressure/Startup/TrackedResource/ImageMemory/GpuPressure/FrameTiming detectors, all `@experimental` (example `analysis_options.yaml` ignores `experimental_member_use`); 15 internal names hidden (tests import `src/`). `StartupTracker` (`lib/src/startup/`) owns startup state (`metrics`, `dartEntryMonotonicUs`, `enrichWithVmData`); `ActiveController.current` / `release` replace `Sleuth._controller` / `notifyControllerDisposed`. Capture statics on `Sleuth` `@experimental`, test statics `@visibleForTesting`. `RouteSession.end()` / `countScanCycle()` `@internal`. Theme: 29 unread tokens + `confidenceColor`/`effortColor`/`sourceAccentColor` removed. Growing enums documented. `test/validation/public_api_test.dart` pins names against `test/validation/_fixtures/public_api_1_0_0.txt` (80 names); CI `api-diff` job: `dart pub unpack` newest 1.x, drop `lib/validation.dart` from both trees, `dart_apitool` 0.23.2 `--version-check-mode onlyBreakingChanges` (fails only a breaking change without a major bump; `fully` also failed minor changes) (added enum value = minor `CF02`; removing an `@experimental` top-level is still breaking), skips before 1.0.0 is on pub.dev, fails on a pub.dev error. API table `doc/api_1_0_0.md`. + +**v1.0.0 overlay**: header run label ("Profile · VM", dot filled with VM / amber ring without) + `MoreMenu` (Startup, Rebuild stats, Hidden issues, Encyclopedia, Guide, Copy snapshot, Theme Auto/Light/Dark); severity chips lead the status row (turning one off is refused when no card would remain; the frame rate and the chipless row label are semantics containers); Rebuild stats reads "Off" unless `SleuthController.countsRouteRebuilds` (profile + `enableDeepDebugInstrumentation`); 320 px min width; no footer. `IssueCard`: `SleuthSeverityIcon` (light presets outline warning/OK via `edgeOf`), meta line (category · confidence or Debug timing tag · "seen X of Y scans, label" · effects, Jank/New tags), crosshair 48 px. Expanded: Measured row with `MeasurementGauge` (scale 1.25 × the higher line, 100 for a percent; a value past it fills the track with two slanted cuts; a debug rate critical on its latest window, `HeldRate.latestRate` → `extraTraceArgs.latestRatePerSec`, reads "Critical: the latest second went over the line" instead of the held note), Where (`file:line` per element) / Route / When / Evidence ("... Sleuth rates it possible, since ...") / Check in rows, Copy details and the Ask AI prompt add `measuredSentence` (`copyDetailsText`), Fix with code blocks, buttons Show on screen / Learn more / Ask AI / Copy details / Hide / Not useful, Why it rebuilds (`DebugSnapshot.forcedRebuildsOf`). `status_lines.dart`: Starting / Warming up / No VM link / No issues found / VM link lost / stopped retrying (no VM-lost toast), one debug banner. Highlight flow: crosshair → `selectHighlightForIssue` (`HighlightLookup` found/notFound/pending, no category fallback) → card shrinks to `SleuthHighlightBar` ("Showing X" / "X is off screen", Back to issues, close; edge away from the widget, shield for a double tap, `highlightBarAtTopNotifier` for the label); outline only (markers = "Mark flagged widgets" toggle), in the target issue's severity (`highlightTargetSeverity`), not painted while the widget is off screen; an issue with an `occurrenceId` takes only its element's highlight, a name match skips list-tagged highlights; with the bar up Escape leaves an app dialog or sheet to the app; controller collects highlights while `_highlightTarget` / pending. List cards (`SleuthController.listsWidgets`: always_repaint_painter, missing_repaint_boundary, layout_bottleneck, uncached_images, expensive_gpu_nodes) step through highlights tagged `WidgetHighlight.stableId` + element `occurrenceId`: first on screen, `highlightStepNotifier` (shown place frozen between Next/lookup, refreshed on 0/1/many count change), `nextHighlightStep` (off screen → widget now at its place), `canStepNext`; bar shows "· 1 of 3" + Next and a second row only with Next (GlobalKey keeps Back to issues), shields its old rect when it moves or shrinks. Card list: each card sits in `_CardSlot`, which keeps its `IssueCard` widget while `IssueCard.sameInputsAs` holds (every constructor field; issues by `utils/issue_fingerprint.dart` `issueFingerprint` + `occurrenceId`, callbacks by presence; `issue_card_test` reads the constructor to pin the list) and `_cardEpoch` stays (moves on severity filter / hidden-key change, a change of the shown `(listKey, occurrenceId)` set, `didUpdateWidget`, `didChangeDependencies`, `reassemble`: what `_relatedLinkOf` reads while a card builds); kept cards' callbacks act on the row's latest report (`latest()`). `OverlayUiState.schemaVersion` 2: `loadJson` maps a version 1 hide key without a widget name to an any-widget key (`legacyHideKey`, `base|*` / `base|*!critical`), which `isHiddenBy` / `hideKeysOf` / the Hidden page match for every widget of that id. The Hidden page labels keys with `hideKeyLabel` (`id · Widget`, `id · every widget`, critical suffix left out). Jank tag: `SleuthController.heldJankCauses()` (issues a verdict named in the last `FrameTimingDetector.jankWindow`, recorded by a `verdictNotifier` listener on the controller's monotonic clock or `clockOverrideForTest`, cleared on a route change beside `markRouteEpoch`, so the hold survives closing the dashboard), shown only while a jank symptom is reported; the card arms one timer for `nextExpiry`. Toasts in inverted theme colours (`toastColorsOf`). Example `ext.sleuthDemo.overlay` actions `menu` / `notUseful`. The 1.0 card replaced 0.37 internals named in the v0.37.0 paragraphs below: the frozen zone is `_frozen` (`List`, built by `applyFreezeZone` and `_expandInZone`) in place of `_expandedIndices` / `_orderSnapshot` / `_repointExpansions`; the status row's `_SeverityChip`s replace `_IssuesSummaryBar` / `_SeverityChipPill`; the meta line replaces `_badgeLine` / `_inlineBadgesWidth`; `_LeadTrailLine` is gone. Example `FileSleuthStateStore` writes under `overlayStateDirectory` (Android `files` beside `code_cache`, iOS `Library/Application Support` beside `tmp`; installs empty `code_cache`). + +**v0.38.0**: detectors-only release on top of 0.37.0. Jank is a missed deadline with Sleuth's own start delay excluded, the VM timeline comes from the `Timeline` stream outside capture mode, new `event_loop_stall`, `material_ui` / `cupertino_ui` support, GC counted once per collection and young-only, `heap_growing` floor leg, 12 more runtimeVerified pairs (27 across 19 stable ids). Sidecar `sleuth_mcp` 0.9.0. The paragraphs below hold the detail per area. **v0.38.0 detector accuracy**: jank = deadline rule (`FrameStats.isJank`: `(vsyncOverhead − ownStartDelayUs) + build > budget || raster > budget`; hand-off gap ignored; `isSevereJank` at 2×); `ownStartDelayUs` = overlap of `[vsyncStart, buildStart]` with `utils/own_work_log.dart` `ownWorkLog` (spans kept by time: 10 s after the newest span, cap 4096; scan `measure`, VM `onReceive` → `recordUntilMicrotask`, parse/dispatch/heap-sample/CPU aggregate/allocation ingest/`ext.sleuth.*` handler + encode `measure`, and VM-held time via `open`/`close`: the poll's `getVMTimeline` from `onSend`, cap `VmServiceClient.ownTimelineRpcCapUs` 25 ms (`ownTimelineFullReadCapUs` 60 ms for a full read), cut at the matched response, because the VM holds its timeline lock while writing and the merged UI/platform thread (iOS and Android) blocks on its next event block; `getCpuSamples` capped at `ownCpuSamplesCapUs` 600 ms; the next poll's `ParsedTimelineData.uiThreadGaps` (no-event stretches ≥ 2 ms on the `Animator::BeginFrame` thread; `PlatformVsync` spans not busy; a cross-batch `E` drops earlier gaps) intersected with `_RequestWindow`s are logged too, and the controller's `_onTimelineData` calls `FrameTimingDetector.refreshOwnStartDelays` (+ active route session buffer) to re-judge buffered frames); frames late only from own work are `isSleuthOverhead`, out of the jank percentage and route health (`sleuthOverheadFrames` stamp, `FrameStatsSummary.sleuthOverheadFrames` and route `frameStats.sleuthOverheadFrames` written only > 0, `FrameStatsBuffer.sleuthOverheadCount`); bottleneck buckets UI-bound / raster-bound / late start; `RenderPipelineAnalyzer._framePhase` decides every tier from the frame's own threads (`schedulerDelay` when both fit, raster when raster > UI, else the largest UI phase; Full-tier batch totals only rank UI phases; no `pipelineStall`), reasons print start delay with Sleuth's share; per-frame `Bottleneck:` line and ranking fallback follow it. `SleuthConfig.cpuAttribution` (default false) gates `_enrichVerdictWithCpuAttribution` (each `getCpuSamples` froze the UI isolate 250 to 410 ms on iPhone 12). New `event_loop_stall` (`detectors/event_loop_stall_detector.dart`, vmOnly, `DetectorType.eventLoopStall`): `VmServiceClient(onTimerEvent:)` subscribes `streamListen('Timer')` + `onEvent('Timer')` (vm_service 15.0.0 floor has no `kTimer`) before the Extension early return, own isolate only, RPC error → `timerStreamAvailable` false (reset to null in `_cleanup`); with `onPauseEvent` also the `Debug` stream (`Pause*`/`Resume`) and `Isolate` stream `IsolateReload`; `TimerSignificantlyOverdue` `details` "N ms" parsed; merge 50 ms; each stall clipped to the boundary stamped at the first lifecycle change after hidden/paused/detached (no grace; `inactive` counts; ignored while suspended; initial state seeded from `SchedulerBinding.lifecycleState`), then debugger pauses, reload windows (`IsolateReload` ±, reassemble −3 s/+2 s) and own work subtracted; ≥ `stallWarningMs` 100 warning, ≥ `stallCriticalMs` 700 critical; hold 10 s on the monotonic clock; per-route episodes; identity = first stall of the episode; `observedStallMs` / `stallCount`, title "at least N ms"; `excusedStallCount` / `ignoredForLifecycleCount` → diagnose `eventLoopStallExcused` / `eventLoopStallIgnored` / `eventLoopStallTimerStream`; controller runs an empty 50 ms `Timer.periodic` probe (`_syncStallProbe`) while in the foreground (`_appInBackground`), connected, stream available and detector enabled, re-synced after `reconnect`; causal `event_loop_stall → sustained_jank` (42 rules). `material_ui` / `cupertino_ui`: `utils/framework_types.dart` type-or-exact-name checks (Scaffold only via `isScaffoldElement` with `ScaffoldState`; `SetStateScopeDetector.isFrameworkElement` for setstate and the scan-root walk; `movedFrameworkOwnerNames` AppBar/Material/TabBarView/TabBar; painter owners; CPU prefixes `package:material_ui/`, `package:cupertino_ui/`; debug `_isUserWidget(Element)` drops widgets whose creation file (`widgetCreationFile` via `@visibleForTesting` `InspectorSerializationDelegate.additionalNodeProperties`) matches `isMovedFrameworkFile` (hosted version, 40-hex git sha, `material_ui/lib/` path dep, package URI), cached per widget instance in `_movedFrameworkCreated`). Blur: `utils/blur_sigma.dart` `maxBlurSigma` reads private filter fields dynamically (profile strips `toString`; ColorFilter 0, blur∘blur sqrt(a²+b²), matrix → null, non-finite/negative → 0, depth 8); `!enabled` BackdropFilters and painterless `CustomPaint`s (Android 3.47 scroll stretch) skipped in the repaint-boundary detector (`!enabled` also in GPU); `cupertinoBlurOwner` (nearest BackdropFilter only; hop budgets TabBar 3, PopupSurface 3, NavigationBar 11, SliverNavigationBar 15, ActionSheet 17) → "drawn by X", skipped by `missing_repaint_boundary`, `FixHintBuilder.expensiveGpuNodes(blurChromeOwners:, onlyChrome:)`. Example: migrated-app (`material_ui`) and Cupertino blur demos, `ext.sleuthDemo.uiBlock ms=` / `blurProbe` (onstage only). @@ -99,9 +107,9 @@ test/ **v0.37.0 overlay core**: `OverlayUiState` (`ui/overlay_ui_state.dart`, exported with `TriggerEdge` / `CardWindowState` / `SleuthThemeMode`) owned by `SleuthController.overlayUiState` (`Sleuth.overlayUiState`, `Sleuth.isOverlayUiStateReady`): `dashboardOpen` (session only), trigger anchor `(edge, fraction)`, card offset/width/height/window state + restore geometry, `hiddenKeys` (LinkedHashSet, cap 200, oldest evicted), `severityFilter` (never empty), `themeMode` (System/Light/Dark, own dirty flag, unknown → system); `toJson`/`loadJson` with `schemaVersion: 1`, malformed fields default, unknown keys ignored. `SleuthConfig.stateStore` (`persistence/sleuth_state_store.dart`): `initialize()` starts the load before its `_initialized` guard (2 s timer, defaults + one debugPrint on error), `uiStateReady` gates the trigger's first paint; `loadJson` skips fields dirtied since construction (hidden keys merged); writes use a trailing 500 ms debounce, one in flight (`_uiStateWritePending` queues one more), skipped when the JSON is unchanged, never after a read that timed out, threw, or returned a newer `schemaVersion` (`_stateStoreWritesDisabled`); contents no release can read (not a JSON object, no valid `schemaVersion`) are replaced by the next change; a write is given up after 5 s; `paused`/`hidden` lifecycle flushes the debounce; a change made during the load is written once when the merge differs from the stored state; dispose flushes a pending change with one unawaited write. `ui/overlay_filters.dart`: `computeVisibleIssues`, `listKeyFor` (`stableId ?? title` + `|widgetName`), `hideKeyFor` (list key + `!critical` for a critical card; `isHiddenBy`: a critical key hides at any severity, a plain key only below critical), `occurrenceKeysFor` (repeats get `#2`, `#3`; keys the sliver child, expansion, freeze snapshot and highlight; chat and history use `listKeyFor`), `applyOverlayFilters` (severity → collapse → hide), re-exported by `floating_issues_card.dart`; `_buildIssuesList` and `_pruneStaleState` both use it; a severity change clears `_expandedIndices` + `_orderSnapshot` and bumps `IssueCard.collapseEpoch`; an expansion below the frozen zone re-captures `_orderSnapshot` from the built ordered list and `_repointExpansions`; prune drops non-visible cards from the snapshot (a restored card lands below the zone); hide goes through `_toggle` first; prune clears the controller highlight when the selected card leaves the visible list (gone, hidden, filtered); chips count per severity with that severity added to the filter; Restore all offers Undo; a maximized card refits when screen size or safe area changes (`_maximizedFor`); geometry set on `OverlayUiState` elsewhere (late store read) is adopted (`_seenGeometry`). Back: `_SleuthOverlayState.didPopRoute` → card `OverlayLayerHost.closeInnermostLayer()` (focused `EditableText` → page → Hidden list) else dashboard; `handleStartBackGesture` / `handleCommitBackGesture`; Android `setFrameworkHandlesBack(true)` post-frame per layer change, and while a layer is open a post-frame chain checks the app's navigators in a microtask (after the app's own navigation notification) and asks again when any `canPop` or overlay entry count changed (`_watchAppNavigation`) (card reports via `onLayersChanged`); page `PopScope`s removed. `OverlayToast` / `OverlayToastController` single slot (2 s info, 4 s action, ×`durationScale` = 3 under `accessibleNavigationOf`, 200 ms fade, identity-guarded action); `AiChatPage.onNotify`. Summary bar `36 × chromeScale` laid over the list (`_IssuesSummaryBar.above(chromeScale:)`) with `max(48, bar)` chip hit boxes (`_SeverityChip`), "Showing X of Y" only when fewer cards than the default state, `_minCardHeight` 300 × chromeScale (minimized 48 × chromeScale), three empty states, footer `hiddenFooterLabel` → `HiddenIssuesPage`. Overlay avoids `ListenableBuilder` / `AnimatedContainer` / `AnimatedSwitcher` / `MergeSemantics` / `CustomSingleChildLayout` (audit-enforced, not denylisted): `SleuthListenableBuilder`, `_ToastFade`, `_SeverityChipPill`, `_TriggerLayout`. Trigger: `_TriggerLayout` + `TriggerBounds` (view padding + `spacingXl`, above `viewInsetsOf`, `max` guards), snap on drag end; config alignment/offset measured from the safe area until the first drag. `PerformanceIssue.toClipboardText()`. Example: `FileSleuthStateStore`, `ext.sleuthDemo.back` / `clipboard` / `overlay` / `overlayState`. Test helper `test/helpers/overlay_harness.dart` (`textScale` / `accessibilityFeatures` / `platformBrightness` / `themeMode`, `mixedOverlayIssues`, `performCustomAction`). Held order: `holdIssueOrder` runs before `applyFreezeZone` (`_heldKeys` / `_heldSeverities`; first build grouped by key; new keys top + `IssueCard.isNew` 2 s per key (`_newKeyTimers`, departed keys pruned) double accent as `foregroundDecoration` + semantics hint "New", severity promotion immediate and only upward (`promote: false` under a screen reader), other changes after `_quietPeriod` 10 s re-armed by any pointer / pan-zoom event on the list's `Listener` (denylisted) or a depth-0 `ScrollNotification`, applied only with pointers up and the list at offset ≤ 0 (`_listAtTop`), never while `MediaQuery.accessibleNavigationOf` or `SemanticsBinding.instance.semanticsEnabled` (`_screenReader`; widget tests run with semantics on, so hold tests pass `semanticsEnabled: false`); under a screen reader new keys enter at their rank position (`newAtTop: false`); 1→0 collapse sets `_heldKeys` to the on-screen order; reset on open (new State), severity filter, hidden-key change); export/MCP keep ranker order. AI chat: `_ReplyState` (idle/waiting/streaming/done/stopped/failed), page-local `_ReplyFailure` (short reason via `aiFailureReason`, full text only behind Copy error, never in history), idempotent `_finish` (stop/close/dispose commit partial + " (stopped)" once), a dangling user turn on open → quiet row "Reply did not finish" + Retry (amber failure row only for a failure in this page instance); a send after it adds its own bubble and `_requestHistory` joins consecutive user turns; failure row = reason line + `Wrap` of Copy error / Retry; in-flight send toast offers Stop via `onNotifyAction`; Stop/Retry haptic; stream cancel errors swallowed; page timers 30 s first token / 15 s stall / 5 s "Still waiting", streaming live region label throttled 2 s, `maxLength` 4000; chat keyed by `(listKeyFor, PerformanceIssue.occurrenceId)` (element `identityHashCode` stamped by `non_lazy_*`, `wrap_layout_bottleneck`, `excessive_repaint_boundary` and `SimpleStructuralDetector.report`; not in JSON or equality) and resolved by that identity only (`_findLiveChatIssue`, captured `_chatIssue` kept otherwise); card's `onHistoryChanged` captures the chat key and the controller it was built for (a write after a controller swap stays with the old one) and drops writes for unreported issues; histories live in `SleuthController.aiChatHistories` (session memory only, cleared on dispose) so they survive the dashboard closing; `AiChatAdapter.firstTokenTimeout` / `stallTimeout` (null or zero off; empty chunks restart the wait); `AiChatMessage.stopped` (provider gets `stoppedReplyForProvider`: open fence closed, note on its own line); `redactSecrets` before log/Copy error; `AiProviderException` exported; SSE events join data lines (`SseLineParser.flush`); prompt routes via `AiSessionContext.promptRoute` (no query/fragment), related ids only for listed issues; the card passes only non-hidden issues as `allIssues` and counts reported hidden issues for "Hidden by user"; `AiSessionContext.summarizeReason` keeps the verdict's first line before `Related:`; `sessionContext` → `AiSessionContext` (`utils/ai_session_context.dart`) rendered as "## Session" by `AiContextBuilder`; SSE `sseTokens` ends at `[DONE]` and strips a leading BOM, `extractStreamError` → `AiProviderException` only for `type: error` or a non-empty `error` map/string. `OverlayUiState.loadJson` drops legacy `excessive_keep_alive:` hide keys. -**v0.37.0 overlay accessibility**: `SleuthOverlay` wraps only its two overlay branches in `SleuthTextScaleClamp` (`ui/text_scale_clamp.dart`, Sleuth-named so no `Builder` lands in app rebuild counts) at 0.8 to 2.0×; chrome regions (header, status row, summary bar, footer, see-all row, trigger, badge `textScaler`) clamp at 1.3× and `chromeScaleOf` grows their minimum heights; the card minimum stops at the usable screen height (down to header + summary hit + footer), a maximized card floors at header + footer + summary-bar hit height, and resizes store the unscaled 300 px minimum. Header controls 36 × 48 (`_compactHeaderButton`), close 48 × 48, title `Expanded` gives way first; VM+/FRAME and DBG badges live in the wrapping `_StatusRow` (`_LeadTrailLine` keeps the count at the right edge); status row + banners sit in a `SingleChildScrollView` capped at `max(middle/2, middle − (hit + 96))`. `IssueCard`: one `Semantics` button (label = title, `expanded`, long-press copy, `InkWell(excludeFromSemantics)`), category/confidence inline (intrinsic width, no `Flexible`) only at ≤ 1.3× and when the measured title room is ≥ 96 px (`_inlineBadgesWidth`), else on the `_badgeLine` Wrap with JANK / ↳N / recurrence. Theme: `_resolveTheme` = `themeMode` light/dark (HC variant when `maybeHighContrastOf`) > `updateTheme` > `config.theme` > brightness/HC preset, const presets only; `updateTheme(theme)` sets `themeMode` System, the toggle never clears the override; `_onUiStateChanged` watches `themeMode`; `didChangeAccessibilityFeatures` rebuilds. `SleuthThemeData`: `brightness`, `severity*Text`, `badgeFillAlpha`, `focusRingWidth`, `sourceAccentWidth`, `triggerIconOnLightFill`, `badgeFill` / `badgeTextOn` / `onColor` / `contrastRatio`, const `highContrastDark` / `highContrastLight`, `fromColorScheme` / `fromSeed` (text-group fallback by surface brightness). Custom semantics actions: header Move up/down/left/right/to top left, resize Taller/Shorter/Wider/Narrower (48 px, normal state only), trigger Move to left/right edge, issue card Copy details; header and grip `value` = card size (header adds position), pan recognizers `excludeFromSemantics`. Pages: `Semantics(scopesRoute, namesRoute, label)`; `SleuthOverlay._AppSemanticsBlocker` (render-level, above the overlay `Localizations`, which is a semantics container on 3.47) blocks app semantics while `OverlayLayerHost.openLayerDepth > 0`. Escape via `HardwareKeyboard.instance.addHandler` in the card (with no page open, outside focus in an `EditableText` or a `barrierDismissible` route keeps it). While a full-screen layer is open the app child sits in an always-present keyed `ExcludeFocus` switched after the frame (layer changes are reported from the card's build), the page's `FocusScope` takes focus, and the captured app node is restored on close only if mounted, attached and focusable; the issue list keeps its offset (`_IssueListScrollController`, dropped when no issue is shown) and a `FocusSemanticEvent` returns screen-reader focus to the opener card (else the header). Moves/resizes commit through `_fitToUsableArea`; maximized cards have no move actions. Toasts: ×3 under `accessibleNavigation` or `semanticsEnabled`, `holdActions` keeps an action toast until used or dismissed; AI chat hides its Stop notice when the reply ends. Reduced motion finishes running controllers and scrolls live (`motion.dart` watcher, `animateScrollTo` / `ensureVisibleWithMotion`). `fromColorScheme` rechecks every drawn pair per surface and falls back the failing surface with its text. Shimmer rests under `disableAnimations`. Tests: `test/ui/text_scale_test.dart` (320 × 568, 220 px card, 1.3× / 2.0×, no exceptions), `test/ui/accessibility_guideline_test.dart` (`SleuthTapTargetGuideline` with the 36 × 48 header allowlist, labels, contrast over black/white hosts), token-pair contrast table in `sleuth_theme_test.dart` (`test/helpers/contrast_helpers.dart`). Example: `ext.sleuthDemo.a11y` / `theme` / `orientation`, `overlay action=setTheme`; `DemoScaffold` header capped at half the body height and scrollable. +**v0.37.0 overlay accessibility**: `SleuthOverlay` wraps only its two overlay branches in `SleuthTextScaleClamp` (`ui/text_scale_clamp.dart`, Sleuth-named so no `Builder` lands in app rebuild counts) at 0.8 to 2.0×; chrome regions (header, status row, summary bar, footer, see-all row, trigger, badge `textScaler`) clamp at 1.3× and `chromeScaleOf` grows their minimum heights; the card minimum stops at the usable screen height (down to header + summary hit + footer), a maximized card floors at header + footer + summary-bar hit height, and resizes store the unscaled 300 px minimum. Header controls 36 × 48 (`_compactHeaderButton`), close 48 × 48, title `Expanded` gives way first; VM+/FRAME and DBG badges live in the wrapping `_StatusRow` (`_LeadTrailLine` keeps the count at the right edge); status row + banners sit in a `SingleChildScrollView` capped at `max(middle/2, middle − (hit + 96))`. `IssueCard`: one `Semantics` button (label = title, `expanded`, long-press copy, `InkWell(excludeFromSemantics)`), category/confidence inline (intrinsic width, no `Flexible`) only at ≤ 1.3× and when the measured title room is ≥ 96 px (`_inlineBadgesWidth`), else on the `_badgeLine` Wrap with JANK / ↳N / recurrence. Theme: `_resolveTheme` = `themeMode` light/dark (HC variant when `maybeHighContrastOf`) > `updateTheme` > `config.theme` > brightness/HC preset, const presets only; `updateTheme(theme)` sets `themeMode` System, the toggle never clears the override; `_onUiStateChanged` watches `themeMode`; `didChangeAccessibilityFeatures` rebuilds. `SleuthThemeData`: `brightness`, `severity*Text`, `badgeFillAlpha`, `focusRingWidth`, `sourceAccentWidth`, `triggerIconOnLightFill`, `badgeFill` / `badgeTextOn` / `onColor` / `contrastRatio`, const `highContrastDark` / `highContrastLight`, `fromColorScheme` / `fromSeed` (text-group fallback by surface brightness). Custom semantics actions: header Move up/down/left/right/to top left, resize Taller/Shorter/Wider/Narrower (48 px, normal state only), trigger Move to left/right edge, issue card Copy details; header and grip `value` = card size (header adds position), pan recognizers `excludeFromSemantics`. Pages: `Semantics(scopesRoute, namesRoute, label)`; `SleuthOverlay._AppSemanticsBlocker` (render-level, above the overlay `Localizations`, which is a semantics container on 3.47) blocks app semantics while `OverlayLayerHost.openLayerDepth > 0`. Escape via `HardwareKeyboard.instance.addHandler` in the card (with no page open, outside focus in an `EditableText` or a `barrierDismissible` route keeps it). While a full-screen layer is open the app child sits in an always-present keyed `ExcludeFocus` switched after the frame (layer changes are reported from the card's build), the page's `FocusScope` takes focus, and the captured app node is restored on close only if mounted, attached and focusable; the issue list keeps its offset (`IssueListScrollController`, dropped when no issue is shown) and a `FocusSemanticEvent` returns screen-reader focus to the opener card (else the header). Moves/resizes commit through `_fitToUsableArea`; maximized cards have no move actions. Toasts: ×3 under `accessibleNavigation` or `semanticsEnabled`, `holdActions` keeps an action toast until used or dismissed; AI chat hides its Stop notice when the reply ends. Reduced motion finishes running controllers and scrolls live (`motion.dart` watcher, `animateScrollTo` / `ensureVisibleWithMotion`). `fromColorScheme` rechecks every drawn pair per surface and falls back the failing surface with its text. Shimmer rests under `disableAnimations`. Tests: `test/ui/text_scale_test.dart` (320 × 568, 220 px card, 1.3× / 2.0×, no exceptions), `test/ui/accessibility_guideline_test.dart` (`SleuthTapTargetGuideline` with the 36 × 48 header allowlist, labels, contrast over black/white hosts), token-pair contrast table in `sleuth_theme_test.dart` (`test/helpers/contrast_helpers.dart`). Example: `ext.sleuthDemo.a11y` / `theme` / `orientation`, `overlay action=setTheme`; `DemoScaffold` header capped at half the body height and scrollable. -**v0.37.0 VM poll pipeline**: `VmServiceClient._pollTimeline` never calls `clearVMTimeline`. First poll of a session (`_lastMaxTs == null`, reset in `_cleanup`) is a full fetch; a clock reading bounds `_lastMaxTs` and rewinds outlier cursors (`TimelineParser.clampOutlierCursors`), and three window fallbacks in a row force a full read; later polls read `getVMTimelineMicros` and fetch `[max(0, lastMaxTs − VmServiceClient.fetchOverlapMicros), clock + 1 s]` (overlap 500 ms = one poll interval; split B/E pairs join through the pending-begin maps); a failed or behind clock read falls back to a full read with `minTimestampUs` floor (`PollTimings.windowFallback`, diagnose `pollWindowFallbacks`). `TimelineCursor` is a class: `ts < lastTs` drops before any other field read, signatures built only for ties at `lastTs`; `ParsedTimelineData.maxTimestampUs` / `duplicatesDropped`; `_sweepStalePendingBegins(maxTs)` evicts idle cursors in both modes (`retainTimeline` only documents the export expectation). `PollTimings` (`lib/src/vm/poll_timings.dart`, exported) per poll: rpc (incl. decode) / parse / dispatch / tail µs, event count, raw response chars (request id from `onSend`, matched in `onReceive` within the first and last 64 chars, `responseCarriesId`), duplicates; `Sleuth.lastPollTimings` + 20 diagnose keys (14 `lastPoll*`, 4 `maxPoll*`, `pollDuplicatesDropped`, `pollWindowFallbacks`; all null until the session's first poll, `_cleanup` resets `_lastPollTimings`) (`lastPoll*` incl. dispatch split detectors/correlate/aggregate/other read through `VmServiceClient.readDispatchSegments` from `SleuthController._lastDispatchSegments`, and tail memory / cpuSamples / allocationProfile via `RpcSpanTracker` overlap; `maxPoll*` over 32 polls, `pollDuplicatesDropped`, `pollWindowFallbacks`). `getCpuSamples` spaced by `cpuSamplesMinInterval` (10 s) and single-flight (a timed-out request stays in flight until the VM answers or `cpuSamplesInFlightStaleAfter` (30 s) passes, `RpcSpanTracker.abandonOpenBefore`): the VM builds the profile on the UI isolate and the ~3.3 MB response decodes there. Debug paint attribution cached per Element (`Expando<_PaintAttribution>`, stamp = weak refs to every ancestor the chain walk or the 16-deep owner walk read (`buildAncestorChain(visitedAncestors:)`), all identical and mounted, + depth + epoch; `invalidatePaintAttribution()` on hot reload; descendant leg still per paint); debug rebuild/paint callbacks skip Sleuth's overlay (`utils/overlay_ownership.dart` `OverlayOwnership`: `SleuthOverlay` registers its element + the app `ExcludeFocus` key, walk up to the first registered overlay element or app key, cached per element by registration generation; `isAppBoundary` skips the keyed wrapper itself); per-widget debug maps keep only user widgets (`debugIsWidgetLocalCreation`, when `isWidgetCreationTracked`; `DebugInstrumentationConfig.userWidgetsOnly`, default true), paint participation (every hook call) feeds the aggregate, its animation-owned total and repaint_boundary_detector, while `repaint_debug_*` cards (`likely`, "Likely Repaint Origin") and custom_painter's frequent/always-repaint (each through its own `RateHysteresis`, strictly above 30 and 10/sec) read `DebugSnapshot.paintOrigins` (`PaintOriginStats`: busiest-instance `maxCount`, `instanceCount`, `busiest` up to 3, 128 instances per type, `paintOriginTypesCapped`): `debugNeedsPaint` read in the hook is the pre-paint flag, a marked node clears its marked parent, the deepest marked node per layer is the origin, `flushPaint` boundary roots count when no child was marked, a boundary already repainted this frame (deepest first) counts as a marked child of its parent, origins map to the nearest app-created widget once per instance per frame (frame closes post-frame, on a stamp change or at `snapshot()`), and slivers, viewports, framework control painters, Material's `_RenderInkFeatures` and origins under a scrolling `Scrollable` are left out, a capped type still counts toward ownership (`paintTypesCapped` / `rebuildTypesCapped`); rebuild callbacks split self-dirtied roots (`element.dirty`, counted) from forced updates (credited to the innermost root on a per-frame depth stack, `forcedRebuildsByRoot`; seen-marking before the split); rebuild-driven owners (`rebuildDrivenOwnerNames`) own paints only in a frame where they rebuilt (`_ownerRebuildFrame` by `currentSystemFrameTimeStamp`; presence decides without the rebuild slot); `RebuildDetector` / `RepaintDetector` hold per-type rates in `utils/rate_hysteresis.dart` (`RateHysteresis`: enter on one window ≥ threshold, leave when two-window summed rate < 0.75× or on a valid zero, capped-missing keeps state, severity likewise; highlights read the held set), evaluate every VM window (`_evaluateVmData` returns the issue), hold the last VM issue until two windows < 0.8× and its critical until two windows < 0.8× the critical boundary (`vmExitFactor`; off with `captureMode`), compose per-widget debug > VM (repaint Gate B: aggregate owned == total) > debug aggregate only without VM; `markRouteEpoch` (controller evidence key `(route, scaffold hash, hot-reload generation)`, which also skips that scan's snapshot fan-out), `discardDebugEvidence` (aborted scan), `resetCaptureState` clears held state, coordinator `discardWindow` after the reload frame; disconnect drops VM-sourced issues; correlator keeps the more severe of `rebuild_debug_T` / `repaint_debug_T`; `SourceLocationCache` keyed by `Type`. `CorrelatedFrameData.isTrustworthy` needs ≥ 2 matched events (`minTrustworthyEvents`) and batch coverage ≥ 0.5. Repaint capture leg 6 s, above 2.0×; `endScenarioInCleanup`; example legs start through `CaptureDriver.runLeg` (a throw or no result fails the leg) and VM calls go through `withCallTimeout` (10 s); capture legs need their screen in front (popped to or pushed, route current and tickers on, checked every 100 ms), check the bracket's `minInBandSamples` before done, and stamp provenance from `SLEUTH_CAPTURE_DEVICE`, `Platform.operatingSystemVersion` and `FlutterVersion.version` (`currentCaptureProvenance`; every capture screen checks it when a leg starts, before any workload, via `provenanceRefusal` / `requireCaptureProvenance` (`CaptureProvenanceUnavailable`), and unapproved values refuse the leg); composed captures pass `checkCaptureRecords` / `checkCaptureJson` before they are stashed (instant events only, every in-span record stamped, reduced value in `inRoleBand`, `requiredInBand`; below spans hold none), and leg-end measurements go through `CaptureBracket.acceptsMeasurement` (`measurementRefusal` / `recordRefusal` log UNMEASURED or OUT-OF-BAND); stream triad re-recorded on 3.47.6 at 8 / 72 / 110 (at leg 150 subscriptions, growth about 0.43 x N); `ext.sleuthDemo.*` lookups go through `example/lib/harness_targeting.dart` (foreground only: current route's overlay entry, onstage path, last in tree order, hit-test before tapping, 2 s reveal and frame-wait bounds); `vmAxes` reset busy guard; `checkBracketValidation` forwards `observedAxisReduction`. +**v0.37.0 VM poll pipeline**: `VmServiceClient._pollTimeline` never calls `clearVMTimeline`. First poll of a session (`_lastMaxTs == null`, reset in `_cleanup`) is a full fetch; a clock reading bounds `_lastMaxTs` and rewinds outlier cursors (`TimelineParser.clampOutlierCursors`), and three window fallbacks in a row force a full read; later polls read `getVMTimelineMicros` and fetch `[max(0, lastMaxTs − VmServiceClient.fetchOverlapMicros), clock + 1 s]` (overlap 500 ms = one poll interval; split B/E pairs join through the pending-begin maps); a failed or behind clock read falls back to a full read with `minTimestampUs` floor (`PollTimings.windowFallback`, diagnose `pollWindowFallbacks`). `TimelineCursor` is a class: `ts < lastTs` drops before any other field read, signatures built only for ties at `lastTs`; `ParsedTimelineData.maxTimestampUs` / `duplicatesDropped`; `_sweepStalePendingBegins(maxTs)` evicts idle cursors in both modes (`retainTimeline` only documents the export expectation). `PollTimings` (`lib/src/vm/poll_timings.dart`, exported) per poll: rpc (incl. decode) / parse / dispatch / tail µs, event count, raw response chars (request id from `onSend`, matched in `onReceive` within the first and last 64 chars, `responseCarriesId`), duplicates; `Sleuth.lastPollTimings` + 20 diagnose keys (14 `lastPoll*`, 4 `maxPoll*`, `pollDuplicatesDropped`, `pollWindowFallbacks`; all null until the session's first poll, `_cleanup` resets `_lastPollTimings`) (`lastPoll*` incl. dispatch split detectors/correlate/aggregate/other read through `VmServiceClient.readDispatchSegments` from `SleuthController._lastDispatchSegments`, and tail memory / cpuSamples / allocationProfile via `RpcSpanTracker` overlap; `maxPoll*` over 32 polls, `pollDuplicatesDropped`, `pollWindowFallbacks`). `getCpuSamples` spaced by `cpuSamplesMinInterval` (10 s) and single-flight (a timed-out request stays in flight until the VM answers or `cpuSamplesInFlightStaleAfter` (30 s) passes, `RpcSpanTracker.abandonOpenBefore`): the VM builds the profile on the UI isolate and the ~3.3 MB response decodes there. Debug paint attribution cached per Element (`Expando<_PaintAttribution>`, stamp = weak refs to every ancestor the chain walk or the 16-deep owner walk read (`buildAncestorChain(visitedAncestors:)`), all identical and mounted, + depth + epoch; `invalidatePaintAttribution()` on hot reload; descendant leg still per paint); debug rebuild/paint callbacks skip Sleuth's overlay (`utils/overlay_ownership.dart` `OverlayOwnership`: `SleuthOverlay` registers its element + the app `ExcludeFocus` key, walk up to the first registered overlay element or app key, cached per element by registration generation; `isAppBoundary` skips the keyed wrapper itself); per-widget debug maps keep only user widgets (`debugIsWidgetLocalCreation`, when `isWidgetCreationTracked`; `DebugInstrumentationConfig.userWidgetsOnly`, default true), paint participation (every hook call) feeds the aggregate, its animation-owned total and repaint_boundary_detector, while `repaint_debug_*` cards (`likely`, "Likely Repaint Origin") and custom_painter's frequent/always-repaint (each through its own `RateHysteresis`, strictly above 30 and 10/sec) read `DebugSnapshot.paintOrigins` (`PaintOriginStats`: busiest-instance `maxCount`, `instanceCount`, `busiest` up to 3, 128 instances per type, `paintOriginTypesCapped`): `debugNeedsPaint` read in the hook is the pre-paint flag, a marked node clears its marked parent, the deepest marked node per layer is the origin, `flushPaint` boundary roots count when no child was marked, a boundary already repainted this frame (deepest first) counts as a marked child of its parent, origins map to the nearest app-created widget once per instance per frame (frame closes post-frame, on a stamp change or at `snapshot()`), and slivers, viewports, framework control painters, Material's `_RenderInkFeatures` and origins under a scrolling `Scrollable` are left out, a capped type still counts toward ownership (`paintTypesCapped` / `rebuildTypesCapped`); rebuild callbacks split self-dirtied roots (`element.dirty`, counted) from forced updates (credited to the innermost root on a per-frame depth stack, `forcedRebuildsByRoot`; seen-marking before the split); rebuild-driven owners (`rebuildDrivenOwnerNames`) own paints only in a frame where they rebuilt (`_ownerRebuildFrame` by `currentSystemFrameTimeStamp`; presence decides without the rebuild slot); `RebuildDetector` / `RepaintDetector` hold per-type rates in `utils/rate_hysteresis.dart` (`RateHysteresis`: enter on one window ≥ threshold, leave when two-window summed rate < 0.75× or on a valid zero, capped-missing keeps state, severity likewise; highlights read the held set), evaluate every VM window (`_evaluateVmData` returns the issue), hold the last VM issue until two windows < 0.8× and its critical until two windows < 0.8× the critical boundary (`vmExitFactor`; off with `captureMode`), compose per-widget debug > VM (repaint Gate B: aggregate owned == total) > debug aggregate only without VM; `markRouteEpoch` (controller evidence key `(route, scaffold hash, hot-reload generation)`, which also skips that scan's snapshot fan-out), `discardDebugEvidence` (aborted scan), `resetCaptureState` clears held state, coordinator `discardWindow` after the reload frame; disconnect drops VM-sourced issues; correlator keeps the more severe of `rebuild_debug_T` / `repaint_debug_T`; `SourceLocationCache` keyed by `Type` (per element in an `Expando` since 1.0.0). `CorrelatedFrameData.isTrustworthy` needs ≥ 2 matched events (`minTrustworthyEvents`) and batch coverage ≥ 0.5. Repaint capture leg 6 s, above 2.0×; `endScenarioInCleanup`; example legs start through `CaptureDriver.runLeg` (a throw or no result fails the leg) and VM calls go through `withCallTimeout` (10 s); capture legs need their screen in front (popped to or pushed, route current and tickers on, checked every 100 ms), check the bracket's `minInBandSamples` before done, and stamp provenance from `SLEUTH_CAPTURE_DEVICE`, `Platform.operatingSystemVersion` and `FlutterVersion.version` (`currentCaptureProvenance`; every capture screen checks it when a leg starts, before any workload, via `provenanceRefusal` / `requireCaptureProvenance` (`CaptureProvenanceUnavailable`), and unapproved values refuse the leg); composed captures pass `checkCaptureRecords` / `checkCaptureJson` before they are stashed (instant events only, every in-span record stamped, reduced value in `inRoleBand`, `requiredInBand`; below spans hold none), and leg-end measurements go through `CaptureBracket.acceptsMeasurement` (`measurementRefusal` / `recordRefusal` log UNMEASURED or OUT-OF-BAND); stream triad re-recorded on 3.47.6 at 8 / 72 / 110 (at leg 150 subscriptions, growth about 0.43 x N); `ext.sleuthDemo.*` lookups go through `example/lib/harness_targeting.dart` (foreground only: current route's overlay entry, onstage path, last in tree order, hit-test before tapping, 2 s reveal and frame-wait bounds); `vmAxes` reset busy guard; `checkBracketValidation` forwards `observedAxisReduction`. **v0.37.0 time-share axes**: `rebuild_activity` / `excessive_repaint` VM paths measure the share of UI-thread wall time inside BUILD / PAINT scopes per ~1 s window (`totalBuildScopeUs` / `totalFlushPaintUs` over the measured elapsed µs of the detector's monotonic clock, `utils/monotonic_clock.dart`); warning > 10 %, critical > 3× (`DetectorThresholds.buildTimePercentThreshold` / `paintTimePercentThreshold`, plumbed by the controller). `SleuthConfig.rebuildThreshold` and `RepaintDetector.paintFrequencyThreshold` gate only the debug count paths. Emissions stamp `observedBuildPercent` / `observedPaintPercent` (`toStringAsFixed(1)`); `last*Percent` tracks every closed window, rebuild peak moves in `_evaluateVmData`, repaint peak on natural window close (`flushPaintEvaluation` updates last only). `setBaseline` and the count observables are removed. Brackets `unit: 'percent'`, threshold 10 (warning, primary) / 30 (rebuild critical, `minInBandSamples: 2`), atTolerance 0.5, ceiling 2.7, observedAxisTolerance 0.25. Example capture screens use cost knobs (`CaptureBuildLoad(work)` per leaf over 64 leaves, `CapturePaintLoad.ops`) with a 3 s calibration pre-pass and a flush → reset → 1.5 s dwell boundary before `markScenarioBegin` (`example/lib/demos/capture_driver.dart`); hands-free via `ext.sleuthDemo.captureLeg` / `captureResult` / `vmAxes`. @@ -161,6 +169,7 @@ test/ ### Recent releases (one-line) +- **v1.0.0**: overlay redesign (run label, More menu, collapsed/expanded cards with measurements, status lines, highlight bar with list-card stepping), `PerformanceIssue.measurement`, Not useful marks, 1.0 public API (`show` lists, `package:sleuth/validation.dart`, API golden test + `api-diff` CI). Sidecar v0.10.0 pins 1.0.0 and accepts 0.38. - **v0.38.0**: deadline-based jank with Sleuth's own start delay excluded, VM timeline stream, `event_loop_stall`, `material_ui` / `cupertino_ui`, GC per-isolate dedup and young-only rate, `heap_growing` floor leg, `raster_dominance` missed-deadline rule; 12 more runtimeVerified pairs (27 across 19 stable ids). Sidecar v0.9.0 pins 0.38.0 and accepts 0.37. - **v0.37.0**: Flutter 3.47 compatibility (scan root descends only into the selected `IndexedStack` child); floors Dart `^3.8.0` / Flutter `>=3.32.0`; text corrections. Evidence-tier ranking, duration escalation removed, possible-root causal guard, single-parent collapse requires parent severity ≥ child, 41 causal rules. Framework painters by name + owner, sliver boundary frames, measured image waste, `non_lazy_shrinkwrap`. Incremental VM timeline fetch (no clear), poll timings in `diagnose`, debug paint attribution cache. Sidecar v0.8.0. - **v0.35.0**: MCP snapshot projection + pagination. `ext.sleuth.snapshot` accepts optional `sections` / `maxIssueCount` / `maxRouteCount`; backward-compat (no args = full payload). New `SnapshotSection` enum (14 projectable keys, exported) + drift test vs `toJson` and `doc/mcp_schema.json`. Projected envelopes carry `_projectedSections` / `_projectionLimits` / `_projectionApplied`. Typed errors `arg_invalid_section` / `arg_invalid_int` / `arg_pagination_unused`. `kSleuthPackageVersion` 0.34.0 → 0.35.0. Sidecar v0.6.0: forwards args + `diskHandoff: bool` (temp-file pointer `{path, sizeBytes, sha256}`, cleaned on detach/shutdown/age-sweep); `compare_snapshots` `arg_section_mismatch` guard; `evaluateBudgets` `arg_missing_required_section`; `acceptedPriorLineages = {0.34}`. @@ -209,4 +218,4 @@ test/ - **v0.18.1**: `Sleuth.flushTimelineNow()`; producer-side dedup; `requireUniqueDetectedAtMicros` opt-in. - **v0.18.0**: `NetworkMonitorDetector.slow_request.warning` first runtimeVerified raise; capture infrastructure (markScenarioBegin/End, exportCaptureJson, schemaVersion v1). -For release notes see `CHANGELOG.md`; the full 0.38.0 notes are in `doc/release_notes/0.38.0.md` (0.37.0 in `doc/release_notes/0.37.0.md`), and the sidecar's in `packages/sleuth_mcp/CHANGELOG.md` (0.8.0 detail in `packages/sleuth_mcp/doc/release_notes/0.8.0.md`). +For release notes see `CHANGELOG.md`; the full 1.0.0 notes are in `doc/release_notes/1.0.0.md` (0.38.0 and 0.37.0 beside it), and the sidecar's in `packages/sleuth_mcp/CHANGELOG.md` (0.10.0 and 0.8.0 detail in `packages/sleuth_mcp/doc/release_notes/`). diff --git a/README.md b/README.md index cea38a79..df0901fc 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Pub Version](https://img.shields.io/pub/v/sleuth)](https://pub.dev/packages/sleuth) [![Flutter](https://img.shields.io/badge/Flutter-3.32%2B-blue?logo=flutter)](https://flutter.dev) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) -[![Tests](https://img.shields.io/badge/tests-4%2C761_passing-brightgreen)]() +[![Tests](https://img.shields.io/badge/tests-5%2C099_passing-brightgreen)]() [![Analysis](https://img.shields.io/badge/analysis-0_issues-brightgreen)]() Sleuth is an in-app performance diagnostics overlay for Flutter. It shows jank, memory leaks, slow network requests, GPU pressure and widget anti-patterns inside your app, and every issue comes with a fix hint. @@ -26,7 +26,7 @@ Where Sleuth helps more than DevTools: - **Structural checks.** It flags problems DevTools does not: non-lazy lists, images decoded above display size, missing `RepaintBoundary`, intrinsic layout and retained stream subscriptions. - **Explained issues.** Each card names the widget and its source line (`file.dart:42`), gives a fix hint, and says what evidence it is based on and how confident it is: confirmed, likely or possible. - **Causes linked to effects.** 42 causal rules link a root cause to the issues it causes, so a card shows why an issue matters. -- **Fix verification and trends.** Capture a baseline, apply the fix and compare. A `Seen X/Y` badge and a trend label show whether an issue is getting better or worse across scans. +- **Fix verification and trends.** Capture a baseline, apply the fix and compare. Each card's meta line ("seen 19 of 20 scans, persistent") shows whether an issue is getting better or worse across scans. - **AI help.** Ask the AI provider of your choice about any issue from its card, or let an assistant query the running app through the `sleuth_mcp` sidecar. What DevTools still does better: @@ -87,7 +87,7 @@ Both modes run the full overlay, all 21 detectors and the AI chat. They differ i | VM timeline (build, layout and paint durations) | Yes | Yes | n/a | | Source location in issues (`file.dart:42`) | Yes | Yes, except iOS builds from `flutter build ios` or `ipa` | n/a | | Per-widget rebuild and paint issues (`enableDebugCallbacks`) | Yes (opt-in) | No | n/a | -| Rebuild stats panel with per-widget rebuild counts (`enableDeepDebugInstrumentation`) | No | Yes (opt-in) | n/a | +| Rebuild stats page (More menu) with per-widget rebuild counts (`enableDeepDebugInstrumentation`) | No | Yes (opt-in) | n/a | | Widget dirty-state arguments on timeline events (`DebugInstrumentationConfig.timelineEnrichment`) | Yes (opt-in) | No | n/a | Use profile mode to measure, and debug mode with `enableDebugCallbacks: true` to find which widget rebuilds or repaints. Then check the timing fix in profile mode. Both instrumentation options add overhead and are off by default: @@ -95,7 +95,7 @@ Use profile mode to measure, and debug mode with `enableDebugCallbacks: true` to ```dart SleuthConfig( enableDebugCallbacks: true, // debug: per-widget rebuild and paint counts (widgets your code creates) - enableDeepDebugInstrumentation: true, // per-widget build timeline events; in profile mode, the Rebuild stats panel + enableDeepDebugInstrumentation: true, // per-widget build timeline events; in profile mode, the Rebuild stats page ) ``` @@ -211,11 +211,11 @@ final markdown = Sleuth.exportSummary(topN: 5); // paste into Slack or a PR desc final score = Sleuth.routeHealthScore('/settings'); // 0 to 100 ``` -The dashboard's Export button copies the JSON snapshot to the clipboard. These calls return `null` in release mode, before `track()` is called, or after the overlay is disposed. +Copy snapshot in the dashboard's More menu copies the JSON snapshot to the clipboard. These calls return `null` in release mode, before `track()` is called, or after the overlay is disposed. - **Routes.** Sleuth detects route changes from the element tree, with no `NavigatorObserver`, and keeps a `RouteSession` per route with FPS, jank ratio, issues and a health score. Tab shells (`IndexedStack`, `StatefulShellRoute.indexedStack`, `CupertinoTabScaffold`) get one session per tab. [Internals](https://github.com/Harrys76/sleuth/blob/main/doc/internals.md#route-sessions) covers the session keys and tracking options. - **Fix verification.** Call `Sleuth.captureBaseline()` before a fix and `Sleuth.compareToBaseline()` after it. An issue counts as resolved after it stays absent for five scans, with a three-scan grace period after hot reload. -- **Recurrence.** The `Seen X/Y` badge counts how many of the last 60 scans saw the issue, and its label (worsening, persistent, stable, improving, flaky) shows the recent trend. [Internals](https://github.com/Harrys76/sleuth/blob/main/doc/internals.md#recurrence-badge) has the thresholds. +- **Recurrence.** The card's meta line counts how many of the last 60 scans saw the issue ("seen 12 of 20 scans"), and its label (worsening, persistent, stable, improving, flaky) shows the recent trend. [Internals](https://github.com/Harrys76/sleuth/blob/main/doc/internals.md#recurrence) has the thresholds. - **Startup.** Call `Sleuth.init()` as the first line of `main()` to measure time to first frame, split into engine and Dart phases. [Internals](https://github.com/Harrys76/sleuth/blob/main/doc/internals.md#startup-tracing) explains each metric. ## Confidence and ranking @@ -233,7 +233,7 @@ Every detector declares the evidence behind its thresholds, and a CI audit enfor ## Limitations - Sleuth does not replace DevTools heap snapshots or interactive flame charts. -- Per-widget attribution depends on the mode. Debug mode gives per-widget rebuild and paint counts. Profile mode gives per-widget rebuild counts only in the Rebuild stats panel, and its rebuild and repaint issues measure the share of UI-thread time, not single widgets. +- Per-widget attribution depends on the mode. Debug mode gives per-widget rebuild and paint counts. Profile mode gives per-widget rebuild counts only on the Rebuild stats page, and its rebuild and repaint issues measure the share of UI-thread time, not single widgets. - VM full mode depends on the platform and on how the app was launched. - Memory detection watches young-generation GC frequency, heap growth and, when you set a budget, process memory. When the heap grows it adds per-class allocation deltas to the issue, but it does not track individual object leaks or retention paths. - CPU attribution of jank frames is off by default (`SleuthConfig(cpuAttribution: true)` turns it on): the VM builds each answer on the app's UI isolate, which froze the UI for about 300 ms per request on an iPhone 12. It samples at about 1 kHz, so functions that run for less than 1 ms may not appear. @@ -252,7 +252,7 @@ cd example && flutter run --profile --no-dds `package:sleuth/sleuth.dart` follows semantic versioning, so a breaking change needs a major release. These are outside that promise and can change in a minor release: - `package:sleuth/validation.dart`, the capture and validation tooling the example's capture screens use, and main-API members typed with its names (such as `Sleuth.repaintDetector`). -- Members marked `@experimental` (the capture statics on `Sleuth`) or `@visibleForTesting`. +- Members marked `@experimental` (the capture statics on `Sleuth`) or `@visibleForTesting`. Their signatures can change in a minor release, but removing one from `Sleuth` or another `sleuth.dart` class waits for a major release, because the CI API diff counts that removal as breaking. - New values in `DetectorType`, `IssueCategory`, `ObservationSource`, `PipelinePhase`, `InteractionContext` and `FixEffort`. A `switch` over them needs a default branch. `ParsedTimelineData` carries `vm_service` types, so a `vm_service` major release means a Sleuth major release. [The API list](https://github.com/Harrys76/sleuth/blob/main/doc/api_1_0_0.md) lists every public name and its library. diff --git a/doc/accessibility.md b/doc/accessibility.md index a39ab19e..97f27944 100644 --- a/doc/accessibility.md +++ b/doc/accessibility.md @@ -14,13 +14,13 @@ Toasts are live regions and stay three times longer while a screen reader or oth ## Text size -Overlay text follows the system setting between 0.8× and 2.0×; the app below keeps its own scale. The chrome (card header, status row, summary bar, footer, badges, trigger) stops growing at 1.3× so the issue list stays visible; above that, issue titles take two lines and detail text wraps. +Overlay text follows the system setting between 0.8× and 2.0×; the app below keeps its own scale. The chrome (card header with its run label, status row with the severity chips, trigger) stops growing at 1.3× so the issue list stays visible; above that, issue titles take two lines and detail text wraps. Fixed heights grow with the chrome but never past the screen (on a landscape phone, for example), and the status row and banners scroll when they would squeeze the list. Moving or resizing the card, by touch or with a screen-reader action, keeps the whole card on screen above the keyboard. In AI chat the issue context gives way to the input and Send button when the keyboard is up. A card resized under large text returns to its own height at 1.0×. The smallest text is 10 px. ## Touch targets -Controls are at least 48 × 48 dp. The card header's compact controls (highlight, theme, minimize, maximize, restore) are 36 × 48 so that they fit the default 300 dp card, which is above the WCAG 2.5.8 minimum of 24 dp. Below 280 dp the minimize and maximize controls are hidden. Close is 48 × 48. +Controls are at least 48 × 48 dp. The card header's compact controls (Mark flagged widgets, More, minimize, maximize, restore) are 36 × 48 so that they fit the default 320 dp card, which is above the WCAG 2.5.8 minimum of 24 dp. A card narrower than 280 dp, which only a very narrow screen allows, hides the minimize and maximize controls. Close is 48 × 48. ## Contrast @@ -28,12 +28,12 @@ Text tokens meet WCAG AA (4.5:1) on every overlay surface in the dark, light and ## High contrast -`SleuthThemeData.highContrastDark()` and `highContrastLight()` raise secondary text, strengthen borders, make badge fills opaque and widen the source accent. Sleuth picks them when `MediaQuery.highContrastOf` is true (reported on iOS) and no theme is set, and for the toggle's Light or Dark. On other platforms, pass one to `Sleuth.updateTheme`. State cues (chip borders, chevrons, the pin) stay at full opacity. +`SleuthThemeData.highContrastDark()` and `highContrastLight()` raise secondary text, strengthen borders and the card divider, and make badge fills opaque. Sleuth picks them when `MediaQuery.highContrastOf` is true (reported on iOS) and no theme is set, and for Light or Dark under Theme in the More menu. On other platforms, pass one to `Sleuth.updateTheme`. State cues such as the chip borders stay at full opacity. ## Reduced motion -Sleuth honours both the Android animator duration scale (`disableAnimations`) and iOS Reduce Motion (`AccessibilityFeatures.reduceMotion`). Page entrances, expand and collapse, scrolls to an entry, the toast fade, severity chips and the rebuild count change at once, and the Ask AI shimmer stops. An animation that is already running when the setting changes finishes at its old speed. +Sleuth honours both the Android animator duration scale (`disableAnimations`) and iOS Reduce Motion (`AccessibilityFeatures.reduceMotion`). Page entrances, the More menu, expand and collapse, scrolls to an entry and the toast fade change at once. An animation that is already running when the setting changes finishes at its old speed. ## Keyboard -Escape first unfocuses a focused overlay text field, then closes the open page, then the dashboard. A focused text field or an open dialog or sheet in your app keeps its Escape. +Escape first unfocuses a focused overlay text field, then closes the open page, the More menu or the highlight bar, then the dashboard. An open dialog or sheet in your app keeps its Escape, and so does a focused app text field unless the highlight bar is up. diff --git a/doc/api_1_0_0.md b/doc/api_1_0_0.md index 1f2627c0..9e9eb427 100644 --- a/doc/api_1_0_0.md +++ b/doc/api_1_0_0.md @@ -44,7 +44,7 @@ Every `export` line in both libraries has a `show` list. `test/validation/public ## Moved to `package:sleuth/validation.dart` -Each of these is marked `@experimental`, as are the `validationMetadata` getters of the main-API detectors. The CI API diff leaves `validation.dart` out and rates changes to `@experimental` declarations as minor. It fails only on a breaking change without a major version bump. +Each of these is marked `@experimental`, as are the `validationMetadata` getters of the main-API detectors. The CI API diff leaves `validation.dart` out. It rates a change to an `@experimental` declaration, or to a member of one, as minor. Removing an `@experimental` member of a class that is not experimental, such as a capture static on `Sleuth`, counts as breaking. The diff fails only on a breaking change without a major version bump. | Name | Kind | Used by | |---|---|---| @@ -55,7 +55,7 @@ Each of these is marked `@experimental`, as are the `validationMetadata` getters | `kSleuthPackageVersion` | const | Capture exports (the sidecar reads the source, not the export) | | `PollTimings` | class | `Sleuth.lastPollTimings` | | `StreamResourcePollResult` | class | `StreamResourceDetector` capture polls | -| `RepaintDetector`, `MemoryPressureDetector`, `StartupDetector`, `TrackedResourceDetector`, `ImageMemoryDetector`, `GpuPressureDetector`, `FrameTimingDetector` | class | Example capture screens (imported from `src/` today) | +| `RepaintDetector`, `MemoryPressureDetector`, `StartupDetector`, `TrackedResourceDetector`, `ImageMemoryDetector`, `GpuPressureDetector`, `FrameTimingDetector` | class | Example capture screens | ## No longer exported diff --git a/doc/capture_procedure.md b/doc/capture_procedure.md index adb169a9..b4b33336 100644 --- a/doc/capture_procedure.md +++ b/doc/capture_procedure.md @@ -7,9 +7,9 @@ > every tap-driven capture screen in the example app, exports each leg > from inside the app, so the procedure needs no DevTools step. > -> Sleuth's own `VmServiceClient` must be connected (VM+ mode). By +> Sleuth's own `VmServiceClient` must be connected (VM mode). By > default `flutter run` starts DDS, which takes the VM service as its -> only client and leaves Sleuth in FRAME mode. Launch with `--no-dds`, +> only client and leaves Sleuth in no-VM mode. Launch with `--no-dds`, > or quit `flutter run` and reopen the app from the home screen: > > 1. `cd example && fvm flutter run --profile --no-dds -d "iPhone 12" \ @@ -20,7 +20,7 @@ > 2. If you launched without `--no-dds`, quit `flutter run` (`q`). DDS > stops with it. > 3. In that case, reopen the app from the iPhone home screen. No DDS -> attaches, so Sleuth's `VmServiceClient` connects (VM+ mode), and +> attaches, so Sleuth's `VmServiceClient` connects (VM mode), and > the real `NetworkMonitorDetector` observes HTTP completions and > emits `sleuth.issue.slow_request.warning` through the > `_recordIssuesForCapture` pipeline. @@ -43,7 +43,7 @@ > to `tool/wrap_capture.dart`, which refuses input that already > carries a `sleuthMetadata` block. > -> A capture recorded in FRAME mode cannot back `runtimeVerified`. The +> A capture recorded in no-VM mode cannot back `runtimeVerified`. The > detector pipeline does not run, the capture has no detector trace > record, and the schema audit rejects it with "Missing detector trace > record." @@ -131,7 +131,7 @@ bottom of this file. `runtimeVerified` evidence is the trace record that the real detector pipeline writes, not a proxy that a screen synthesises. The pipeline runs only while `SleuthController`'s `VmServiceClient` is connected -(VM+ mode, not FRAME mode). Without that connection the `vmOnly` and +(VM mode, not no-VM mode). Without that connection the `vmOnly` and `hybrid` detectors never see their inputs, and `Sleuth.exportCaptureJson` cannot read the trace buffer at all. It returns null with "VM service client disconnected". A capture made any @@ -144,7 +144,7 @@ service. Two launch setups allow that: - **`flutter run --profile --no-dds`.** Without `--no-dds`, `flutter run` starts DDS (Dart Development Service), which takes the - VM service as its only client. Sleuth then stays in FRAME mode for + VM service as its only client. Sleuth then stays in no-VM mode for the whole session. - **An app started without the flutter tool.** Quit `flutter run` and reopen the app from the home screen. No DDS attaches, so Sleuth @@ -157,7 +157,7 @@ Android has no approved reference device yet (see `doc/reference_devices.md`), so Android captures cannot back a tier raise. -A FRAME-mode capture cannot back `runtimeVerified`. Earlier versions of +A no-VM capture cannot back `runtimeVerified`. Earlier versions of this procedure had a `Sleuth.markCaptureIssue` fallback that synthesised the trace record from a Stopwatch around the workload, copying the detector's threshold ladder. The project removed it @@ -232,7 +232,7 @@ do nothing, the capture lacks both the scenario span and the detector trace record, and `validateBracket` rejects it with a precise error. `SLEUTH_CAPTURE_DEVICE` names the phone model that exports stamp as `device` (see Provenance in the time-share section). `--no-dds` keeps -Sleuth in VM+ mode, as the VM-service requirement above explains. If +Sleuth in VM mode, as the VM-service requirement above explains. If `flutter run` cannot start the app on the phone, use the build, install and attach sequence under Launch in the time-share section. @@ -609,7 +609,7 @@ v0.18.1 or later with the procedure above: 1. Note which leg failed and its observed magnitude. 2. Kill the app from the iOS app switcher (swipe it away). -3. Cold-launch it from the home screen icon. No DDS attaches, so VM+ +3. Cold-launch it from the home screen icon. No DDS attaches, so VM mode comes back. 4. Run the legs again in order, starting with the first. @@ -669,7 +669,7 @@ differs from HeavyCompute and NetworkMonitor in these ways: --dart-define=SLEUTH_CAPTURE_MODE=true \ --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12" ``` - Without `--no-dds` Sleuth stays in FRAME mode; see the VM-service + Without `--no-dds` Sleuth stays in no-VM mode; see the VM-service requirement above. 2. Wait at least 35 s after launch: the floor leg ignores idle collections from its first 30 s, and the sample leg needs its 3 s @@ -788,7 +788,7 @@ HeavyCompute and MemoryPressure in three ways: --dart-define=SLEUTH_CAPTURE_MODE=true \ --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12" ``` - Without `--no-dds` Sleuth stays in FRAME mode, as for MemoryPressure. + Without `--no-dds` Sleuth stays in no-VM mode, as for MemoryPressure. 2. Wait at least 3 s after launch so the VM service connection settles. 3. Open Capture Helpers, then PlatformChannel. 4. Run each leg (Below, At, Above). The screen has no calibration @@ -1372,7 +1372,7 @@ cd example && fvm flutter run --profile --no-dds -d "iPhone 12" \ --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12" ``` -Without `--no-dds` Sleuth stays in FRAME mode; see the VM-service +Without `--no-dds` Sleuth stays in no-VM mode; see the VM-service requirement above. Open Capture Helpers, then StreamResource. From a VM service client, `ext.sleuthDemo.open` with `demo=streamresource` opens the same screen. `ext.sleuthDemo.captureLeg` drives only the rebuild @@ -1525,7 +1525,7 @@ cd example && fvm flutter run --profile --no-dds -d "iPhone 12" \ --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12" ``` -Without `--no-dds` Sleuth stays in FRAME mode. Open Capture Helpers, +Without `--no-dds` Sleuth stays in no-VM mode. Open Capture Helpers, then TrackedResource (the screen's title is "Tracked resource capture helper"). From a VM service client, `ext.sleuthDemo.open` with `demo=trackedresource` opens it. As with StreamResource, @@ -1635,7 +1635,7 @@ again. | `[/] UNMEASURED: ...` or `[/] OUT-OF-BAND: ... Nothing to export; re-run the leg.` | The peak live count or peak age was never measured, was not positive, or fell outside the leg's band. | | `[] OUT-OF-BAND: . Nothing copied; re-run the leg.` | At the Export tap the composed capture failed the record check: an in-span record without the arg key, a largest value outside the band or too far from `observed`, or too few in-band records. | | `[] FAILED: ` | The leg threw. The screen closes any open span and restores the streams it narrowed. | -| `[] Export FAILED. initialized=... captureMode=... vmConnected=... Reason: ` | `exportCaptureJson` refused. `vmConnected=false` means Sleuth has no VM connection (FRAME mode). "Scenario markers not found" usually means the markers left the buffer before the tap. The bracket check refuses a below span that holds a record and an at or above span that holds none. | +| `[] Export FAILED. initialized=... captureMode=... vmConnected=... Reason: ` | `exportCaptureJson` refused. `vmConnected=false` means Sleuth has no VM connection (no-VM mode). "Scenario markers not found" usually means the markers left the buffer before the tap. The bracket check refuses a below span that holds a record and an at or above span that holds none. | | `[] Export FAILED: capture provenance: ` | The provenance check failed again at the Export tap. No export runs. | | `Export: no completed leg yet. ...` | Export was tapped before a leg finished. | @@ -1675,8 +1675,9 @@ fvm flutter run --profile --no-dds -d \ ``` `--no-dds` keeps the VM service open to more than one client, so -Sleuth's own VM client stays connected (VM+ mode). With DDS the service -has a single client and Sleuth drops to Basic. Connect any +Sleuth's own VM client stays connected (VM mode). With DDS the service +has a single client, so Sleuth gets no VM link and the run label reads +"Profile · no VM". Connect any `package:vm_service` client to the printed `ws://.../ws` URI. Leave the phone idle for two minutes before the first leg, because build and paint durations drift with temperature. diff --git a/doc/configuration.md b/doc/configuration.md index 1c32d65c..7edb2523 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -87,7 +87,7 @@ Sleuth.track( ## Suppressing issues -`suppressedIssues` removes matching issues before ranking, so they leave the overlay, `ext.sleuth.*`, snapshots and budgets alike, and the overlay footer counts them (`3 suppressed`). To move a card out of the way only while you work, hide it in the overlay instead ([Using the overlay](overlay.md#hiding-cards)). +`suppressedIssues` removes matching issues before ranking, so they leave the overlay, `ext.sleuth.*`, snapshots and budgets alike. Hidden issues in the overlay's More menu counts them, and its page says how many were removed before ranking. To move a card out of the way only while you work, hide it in the overlay instead ([Using the overlay](overlay.md#hiding-cards)). ## Saving overlay state across restarts @@ -126,7 +126,7 @@ A widget's card appears when one scan's rate reaches the threshold, and it stays ## Overlay theming -The overlay follows the platform brightness and switches to a high-contrast preset when the platform asks for high contrast (iOS Increase Contrast). The header toggle cycles System, Light and Dark and remembers the choice (`OverlayUiState.themeMode`, persisted with the rest of the overlay state). The order of precedence, from highest, is the toggle's Light or Dark, then `Sleuth.updateTheme`, then `SleuthConfig.theme`, then automatic selection. Choosing Light or Dark shows the Sleuth preset (high-contrast when the platform asks for it) in place of an `updateTheme` or configured theme; System shows that theme again. Calling `Sleuth.updateTheme` with a theme sets the toggle to System. +The overlay follows the platform brightness and switches to a high-contrast preset when the platform asks for high contrast (iOS Increase Contrast). Theme in the overlay's More menu picks Auto, Light or Dark and remembers the choice (`OverlayUiState.themeMode`, persisted with the rest of the overlay state). The order of precedence, from highest, is Theme's Light or Dark, then `Sleuth.updateTheme`, then `SleuthConfig.theme`, then automatic selection. Choosing Light or Dark shows the Sleuth preset (high-contrast when the platform asks for it) in place of an `updateTheme` or configured theme; Auto shows that theme again. Calling `Sleuth.updateTheme` with a theme sets Theme to Auto. ```dart // Static config at initialization @@ -140,9 +140,9 @@ Sleuth.track( ), ); -// Surfaces and text from your app's colour scheme (severity, category, -// confidence and source colours stay Sleuth's). Build it once: the overlay -// compares themes by identity. +// Surfaces and text from your app's colour scheme (severity and category +// colours stay Sleuth's). Build it once: the overlay compares themes by +// identity. final sleuthTheme = SleuthThemeData.fromColorScheme(Theme.of(context).colorScheme); final seeded = SleuthThemeData.fromSeed(Colors.teal, brightness: Brightness.dark); diff --git a/doc/internals.md b/doc/internals.md index 0112da9d..8aeb9656 100644 --- a/doc/internals.md +++ b/doc/internals.md @@ -12,7 +12,7 @@ Jank does not use the build-to-raster span. A frame is late when the app's start ## Scan loop -The structural scan runs on a self-rescheduling timer. Its callback scans in a post-frame callback, so a tick needs a frame to run. The interval starts at `treeScanInterval` (1 s). After three consecutive scans with no issues it doubles, up to 2 s and never below the base. Each tick times its unified walk plus aggregation. A tick over 4 ms stretches the next interval to `treeScanInterval × ceil(cost / 4 ms)`, capped at 5 s and never below the back-off interval; capture mode turns the stretch off. With `maxElementsPerScan > 0`, a walk over the cap skips the next tick once and schedules the following one at twice the interval. Walks are never cut short, and the previous issues stay visible. While the user scrolls, a tick retries after 250 ms, at most three times in a row, and a scroll with no start or update notification for 2 s counts as ended. Scroll notifications only re-measure highlight rects from the render objects they were measured from, and one early tick runs 300 ms after a scroll ends. `issuesNotifier` fires only when a rendered field of the ranked list changes. `scanTickNotifier` pulses once per tick for panels that re-read live state, such as rebuild counts and recurrence badges. +The structural scan runs on a self-rescheduling timer. Its callback scans in a post-frame callback, so a tick needs a frame to run. The interval starts at `treeScanInterval` (1 s). After three consecutive scans with no issues it doubles, up to 2 s and never below the base. Each tick times its unified walk plus aggregation. A tick over 4 ms stretches the next interval to `treeScanInterval × ceil(cost / 4 ms)`, capped at 5 s and never below the back-off interval; capture mode turns the stretch off. With `maxElementsPerScan > 0`, a walk over the cap skips the next tick once and schedules the following one at twice the interval. Walks are never cut short, and the previous issues stay visible. While the user scrolls, a tick retries after 250 ms, at most three times in a row, and a scroll with no start or update notification for 2 s counts as ended. Scroll notifications only re-measure highlight rects from the render objects they were measured from, and one early tick runs 300 ms after a scroll ends. `issuesNotifier` fires only when a rendered field of the ranked list changes. `scanTickNotifier` pulses once per tick for panels that re-read live state, such as rebuild counts and the recurrence text. A scan keys rebuild and repaint evidence on the screen: the route name, the scaffold hash and the hot-reload generation. When the key changes, `RebuildDetector` and `RepaintDetector` drop what they hold and restart their VM window, and no detector receives that scan's debug counts, which span the change (a hot reload rebuilds every element once). A scan that cannot find a single visible page, such as two Scaffolds side by side during a transition, drops the held per-widget debug cards. @@ -149,29 +149,29 @@ If the overlay shows unexpected FPS, check these causes: | RepaintBoundary | Element and render tree | Expensive GPU widget without a RepaintBoundary ancestor; too many boundaries in a scrollable | Possible to Likely | Rises to Likely with debug paint-rate evidence, never Confirmed, because paint rates are per type, not per instance. ColorFiltered is detected by widget type. Framework painters and Material's own ClipPath are skipped; boundaries a default sliver delegate adds are never counted | | Startup | `Sleuth.init()` and FrameTiming | TTFF over budget, dominant phase attribution | Confirmed | One-shot; requires `Sleuth.init()` before `runApp()`. The wall-clock measurement has about 5 to 50 ms of inherent skew | -## Recurrence badge +## Recurrence -Each issue card shows a `Seen X/Y · {label}` badge once Sleuth has observed the issue across at least two scan cycles. It shows how sticky the issue is and whether it is getting better or worse. +Each issue card's meta line reads `seen X of Y scans, {label}` once Sleuth has observed the issue across at least two scan cycles. It shows how sticky the issue is and whether it is getting better or worse. - X is the number of scan cycles in which the issue fired (`presentCount`). - Y is the number of scan cycles in the ring buffer, which holds 60 and evicts the oldest. The label summarises the trend over the most recent window, 10 entries by default: -| Label | Color | When it appears | -|-------|-------|-----------------| -| worsening | red | Average severity in the second half of the window exceeds the first half by more than `0.3`. | -| persistent | amber | The trend is `stable` and `X / Y ≥ 0.9`, so the issue fires in almost every cycle. | -| stable | neutral | The issue is present consistently and its severity is not trending. | -| improving | green | Average severity in the second half of the window is lower than the first half by more than `0.3`. | -| flaky | neutral | The issue toggles between present and absent 3 or more times in the window (`intermittent` internally). | +| Label | When it appears | +|-------|-----------------| +| worsening | Average severity in the second half of the window exceeds the first half by more than `0.3`. | +| persistent | The trend is `stable` and `X / Y ≥ 0.9`, so the issue fires in almost every cycle. | +| stable | The issue is present consistently and its severity is not trending. | +| improving | Average severity in the second half of the window is lower than the first half by more than `0.3`. | +| flaky | The issue toggles between present and absent 3 or more times in the window (`intermittent` internally). | Two labels differ from the JSON export: - `flaky` is the display label for the `intermittent` enum value; JSON exports use `intermittent`. - The UI derives `persistent` from a `stable` trend plus the 90 % presence ratio. The JSON export reports the underlying enum (`stable`) and a separate `totalOccurrences / totalObserved` pair, so you can recompute it downstream. -The badge and trend show persistence; severity always comes from the detector. See [`RecurrenceTrend`](../lib/src/models/recurrence_trend.dart) for the thresholds. +The count and label show persistence; severity always comes from the detector. See [`RecurrenceTrend`](../lib/src/models/recurrence_trend.dart) for the thresholds. ## Ranking and causal graph @@ -209,11 +209,11 @@ Bottom-navigation apps that use `IndexedStack`, `StatefulShellRoute.indexedStack The controller owns an `OverlayUiState`: dashboard open, trigger anchor, card geometry and window state, hidden keys, severity filter, and theme mode. None of it resets when the dashboard closes or on hot reload, and the dashboard-open flag is never saved. The trigger's anchor is the nearest horizontal edge plus the vertical position as a fraction of the safe-area height. It is resolved on every layout inside `MediaQuery.viewPaddingOf` plus a 16 px margin and above `viewInsetsOf`; rotation keeps the edge and the fraction. -With `SleuthConfig.stateStore` set, `initialize()` reads the store once with a 2 s timeout, applies it, and only then lets the trigger paint. A field changed before the read finishes keeps its value, and hidden keys are merged. Later changes are written after a trailing 500 ms debounce, with one write in flight at a time; a change during a write queues one more write with the latest state. A write still running after 5 s is given up. A pending change is written when the app goes to the background (`paused` or `hidden`) and on dispose. A read that times out, throws, or returns state from a newer `schemaVersion` turns writes off for the session, so the stored value is left as it is. Contents no release can read (not a JSON object, or no valid `schemaVersion`) keep the defaults and are replaced by the next change. The JSON carries `schemaVersion: 1`; unknown keys are ignored, a malformed field takes its default, and at most 200 hidden keys are kept. +With `SleuthConfig.stateStore` set, `initialize()` reads the store once with a 2 s timeout, applies it, and only then lets the trigger paint. A field changed before the read finishes keeps its value, and the read's hidden keys merge with the session's. The controller writes later changes after a trailing 500 ms debounce, with one write in flight at a time; a change during a write queues one more write with the latest state. It gives up on a write still running after 5 s. It writes a pending change when the app goes to the background (`paused` or `hidden`) and on dispose. A read that times out, throws, or returns state from a newer `schemaVersion` turns writes off for the session, so the stored value stays as it is. Contents no release can read (not a JSON object, or no valid `schemaVersion`) leave the defaults in place, and the next change replaces them. The JSON carries `schemaVersion: 2`; the reader skips unknown keys, a malformed field takes its default, and the state keeps at most 200 hidden keys. In a version 1 store, written before 1.0.0, a hide key that names no widget is read as `|*`, which hides that id's cards whatever widget they name, or none. Wrap layout, excessive RepaintBoundary and custom structural issues gained a widget name in 1.0.0, so their old keys would match no card otherwise. The card list is `applyOverlayFilters`. The severity filter runs before `computeVisibleIssues`, so an effect whose only cause is filtered out shows as its own card. Hiding runs after it, so a hidden root takes its collapsed effects along. The list key is `stableId` (or `title`) plus `|widgetName` when the issue names a widget. A critical card's hide key adds `!critical`: a key taken from a critical card hides the issue at any severity, while a key taken from a warning or ok card does not hide it once it turns critical. A severity change clears every expansion together with the freeze snapshot. Hiding is overlay-only: aggregation, `latestIssues`, `suppressedCount`, `ext.sleuth.*`, snapshots, route sessions and recurrence never read the overlay state. -The overlay sits above the app's `MaterialApp` with no `Navigator` or `Router` of its own, so back handling goes through `WidgetsBindingObserver.didPopRoute`. `SleuthOverlay` registers its observer before the app's `WidgetsApp`, and `handlePopRoute` asks observers in registration order, stopping at the first `true`. While the dashboard is open, back unfocuses a focused text field, else closes the open full-screen page or the Hidden list, else closes the dashboard. With the dashboard closed it returns `false`. An observer the app registers before `runApp` is asked first. For Android predictive back, the overlay claims `handleStartBackGesture` while a layer is open and closes the innermost layer on commit. It requests `SystemNavigator.setFrameworkHandlesBack(true)` after each layer change (post-frame), because `WidgetsApp` resets the flag on its own navigation notifications. While a layer is open, it also checks the app's navigators after each frame and requests the flag again when one of them changes whether it can pop or how many overlay entries it holds. Flutter 3.32 gives a predictive swipe to the first observer that claims it; later versions offer it to every observer, so an app route that can pop may pop together with the overlay layer. +The overlay sits above the app's `MaterialApp` with no `Navigator` or `Router` of its own, so back handling goes through `WidgetsBindingObserver.didPopRoute`. `SleuthOverlay` registers its observer before the app's `WidgetsApp`, and `handlePopRoute` asks observers in registration order, stopping at the first `true`. While the dashboard is open, back unfocuses a focused text field, else closes the open full-screen page or the Hidden list, else the More menu, else the highlight bar, else closes the dashboard. With the dashboard closed it returns `false`. An observer the app registers before `runApp` is asked first. For Android predictive back, the overlay claims `handleStartBackGesture` while a layer is open and closes the innermost layer on commit. It requests `SystemNavigator.setFrameworkHandlesBack(true)` after each layer change (post-frame), because `WidgetsApp` resets the flag on its own navigation notifications. While a layer is open, it also checks the app's navigators after each frame and requests the flag again when one of them changes whether it can pop or how many overlay entries it holds. Flutter 3.32 gives a predictive swipe to the first observer that claims it; later versions offer it to every observer, so an app route that can pop may pop together with the overlay layer. ## Startup tracing diff --git a/doc/mcp_schema_derivation.md b/doc/mcp_schema_derivation.md index d9dd82c2..7e15d8f2 100644 --- a/doc/mcp_schema_derivation.md +++ b/doc/mcp_schema_derivation.md @@ -6,7 +6,7 @@ were derived and how to regenerate them when fields change. ## Source captures The nested-shape entries (`recurrenceTrends`, `sessionSummary`, -`routeSessions` and the `routes` list inside `routeHealth`) come from six +`routeSessions` and the `routes` list inside `routeHealth`) come from seven on-device snapshots in `test/validation/captures/mcp_snapshots/`: - `snapshot_idle.json`: a fresh launch with no workload. @@ -18,17 +18,25 @@ on-device snapshots in `test/validation/captures/mcp_snapshots/`: produces several `RouteSession` entries. - `snapshot_memory.json`: a memory-pressure workload for the `memoryTrendSummary` shape. +- `snapshot_measurement.json`: the CSV Import route on 1.0.0, with a + `measurement` on every issue and one `notUsefulMarks` entry, whose + `measurement` the schema shapes. Each capture is the raw `ext.sleuth.snapshot.data` payload, that is, the -contents of the OK envelope's `data` field. All six report -`packageVersion: "0.34.0"`, `isDebugMode: false` and -`isVmConnected: true`. +contents of the OK envelope's `data` field. All seven report +`isDebugMode: false` and `isVmConnected: true`. +`snapshot_measurement.json` reports `packageVersion: "1.0.0"` and the +other six `"0.34.0"`. ## Source device The captures were collected on a real iPhone (iOS 17.5) running the example app in profile mode with `SLEUTH_CAPTURE_MODE=true`, attached -over USB through `iproxy` (libimobiledevice). These snapshots check +over USB through `iproxy` (libimobiledevice). `snapshot_measurement.json` +came from an iPhone 12 (iOS 17.5, Flutter 3.47.6) started with +`flutter run --profile --no-dds` and the same define. The run visits the +Always-Repaint CustomPainter and CSV Import demos, imports the CSV twice, +and marks one card Not useful from the overlay. These snapshots check payload shape only. The detector-capture audits skip `mcp_snapshots/`, so the files do not feed the validation ledger. @@ -41,31 +49,32 @@ under separate scenarios. `recurrenceTrends.` (Map): -- `trend`: String, required. Values seen: `stable` in every capture, and - `worsening` in the repaint capture. +- `trend`: String, required. Values seen: `stable` in every capture, + `worsening` in the repaint capture, and `intermittent` in the + measurement capture. - `totalOccurrences`: int, required. - `totalObserved`: int, required. - `lastSeenCycle`: int, required. No capture has a null value. The schema marks it nullable because the code emits null when the buffer is empty. - `severityStats`: Map, optional. Emitted only when the trend has at least one present observation. Shape: `{min: int, max: int}`, where 1 - is ok, 2 is warning and 3 is critical. Every capture has trends with it - and trends without it. + is ok, 2 is warning and 3 is critical. Every capture has trends with + it, and all but the measurement capture also have trends without it. `sessionSummary` (Map): -- `topIssues`: List, present in all six captures. Item keys seen: +- `topIssues`: List, present in all seven captures. Item keys seen: `stableId`, `title`, `severity`, `confidence`, `confidenceReason` and `rankingScore` in every capture, and `widgetName` in every capture - except the idle one. -- `frameHistogram`: Map, present in all six captures. Its + except the idle and measurement ones. +- `frameHistogram`: Map, present in all seven captures. Its bucket keys are fixed: `<16ms`, `16-33ms`, `33-50ms`, `50-100ms`, `>100ms`. -- `detectorHitRates`: Map, present in all six captures. -- `memoryTrendSummary`: Map, present in all six captures. Shape: +- `detectorHitRates`: Map, present in all seven captures. +- `memoryTrendSummary`: Map, present in all seven captures. Shape: `{startBytes, endBytes, peakBytes, growthRatePerSec, sampleCount}`. -- `causalEdges`: List, present in the repaint and recurrence - captures and absent from the other four. Item shape: +- `causalEdges`: List, present in the repaint, recurrence and + measurement captures and absent from the other four. Item shape: `{cause: String, effect: String}`. Every capture had ranked issues, recorded frames and at least two heap @@ -122,7 +131,8 @@ Present in some sessions only, or in none of the captures: session holds at least two frames. `widgetHeatMap` (top level): List, optional. It is present in five -of the six captures, all except `snapshot_idle.json`. The schema marks it +of the seven captures, all except `snapshot_idle.json` and +`snapshot_measurement.json`. The schema marks it opaque and leaves the item shape undocumented, because no documented consumer relies on its keys yet. diff --git a/doc/overlay.md b/doc/overlay.md index 613e2d42..0a663a4f 100644 --- a/doc/overlay.md +++ b/doc/overlay.md @@ -1,6 +1,6 @@ # Using the overlay -Tap the trigger button to open the dashboard. Each issue is a card: tap it to expand the detail, fix hint, causes and "About this detection" section. Drag the trigger to either screen edge; drag the card header to move the card and the corner grip to resize it. +Tap the trigger button to open the dashboard. Each issue is a card: tap it to expand the measured value with its warning and critical lines, the detail, where and when Sleuth saw the issue, the evidence behind it, the DevTools view to check it in, the fix, and its causes and effects. Drag the trigger to either screen edge; drag the card header to move the card and the corner grip to resize it. The More button in the header opens Startup, Rebuild stats, Hidden issues, Encyclopedia, Guide, Copy snapshot and Theme. ## The FPS number @@ -9,7 +9,7 @@ Sleuth reports two frame-rate metrics: - **Actual FPS** is the number of frames presented in the last second, counted from `FrameTiming.rasterFinish` timestamps in a rolling window. It is what the device drew. - **Throughput FPS** is a capacity estimate from the average frame duration (`1e6 / avg(frame_duration_us)`). It is what the engine could produce at the current per-frame cost. -The overlay shows **Throughput FPS** as the main number, coloured against `fpsTarget`. Idle screens read as smooth because Flutter repaints only on change; Actual FPS would drop to a few frames per second on a static screen even though rendering is healthy. Tap the info icon to show both metrics side by side (ACTUAL and TPUT). Session exports (`SessionSnapshot` schema v5) carry both metrics plus `actualFpsRaw`, the device rate capped at 240 Hz. It matters on 120 Hz ProMotion hardware, where the overlay clamps to `fpsTarget`. +The overlay shows **Throughput FPS** as the main number, coloured against `fpsTarget`. Idle screens read as smooth because Flutter repaints only on change; Actual FPS would drop to a few frames per second on a static screen even though rendering is healthy. Session exports (`SessionSnapshot` schema v5) carry both metrics plus `actualFpsRaw`, the device rate capped at 240 Hz. It matters on 120 Hz ProMotion hardware, where the overlay clamps to `fpsTarget`. Jank thresholds follow the frame rate the app actually renders at. Sleuth measures the vsync cadence from the fastest recent frames and uses it as the budget, bounded below by `fpsTarget` and above by the display's reported refresh rate. Sleuth judges a 120 Hz device rendering at 120 against 8.33 ms, and a ProMotion device rendering at 60 keeps 16.67 ms. iOS reports 120 Hz for ProMotion panels even while the app renders at 60, so the display rate alone never tightens the budget. `raster_dominance` counts a frame only when its raster time misses that frame's budget, and when the budget tightens the default heavy-compute threshold becomes three quarters of it, at most 8 ms. `fpsTarget` still caps the overlay FPS number and its colours. Set `autoFrameBudget: false` to always use `1000 / fpsTarget` ms; capture mode always does. `ext.sleuth.diagnose` reports `frameBudgetUs`, `effectiveFrameRateHz` and `frameRateSource`. @@ -17,26 +17,36 @@ Jank thresholds follow the frame rate the app actually renders at. Sleuth measur ## Filtering by severity -The counts in the summary bar are toggles. Tap one to show or hide that severity; at least one stays on. "Showing X of Y" appears when the list shows fewer cards than it would with every severity on and nothing hidden. The filter is saved with the rest of the overlay state. +The severity chips at the start of the status row ("2 critical") are toggles. Tap one to show or hide that severity; at least one stays on. "Showing X of Y" with Show all appears while a severity is off. The filter is saved with the rest of the overlay state. ## Hiding cards -To move a card out of the way only while you work, expand it and tap **Hide**. The card leaves the overlay (with Undo for 4 s) and the footer shows `N hidden`; tap the footer to restore hidden cards (Restore all also offers Undo). A hide covers the card at the severity you hid it at, so a hidden warning shows again if the same card turns critical. Hiding affects only the overlay: `ext.sleuth.issues`, snapshots, MCP budgets, route sessions and recurrence still see the issue. To drop an issue everywhere, use [`suppressedIssues`](configuration.md#suppressing-issues). +To move a card out of the way only while you work, expand it and tap **Hide**. The card leaves the overlay (with Undo for 4 s), and Hidden issues in the More menu shows the count; open it to restore hidden cards (Restore all also offers Undo). A hide covers the card at the severity you hid it at, so a hidden warning shows again if the same card turns critical. Hiding affects only the overlay: `ext.sleuth.issues`, snapshots, MCP budgets, route sessions and recurrence still see the issue. To drop an issue everywhere, use [`suppressedIssues`](configuration.md#suppressing-issues). Keep-alive issue ids name the scrollable (`excessive_keep_alive:PageView~1`, or `~k-feed` for a `ValueKey('feed')`). When the overlay state loads, it drops hides saved under the older positional ids (`excessive_keep_alive:3`). ## Card order -While the dashboard is open, the overlay holds the order of collapsed cards, so a card does not move under your finger. New issues enter at the top (below any expanded cards) with a wider accent for 2 s, a severity promotion moves a card up at once (never down), and other rank changes apply after 10 s of quiet while the list is scrolled to the top. Any touch, scroll or trackpad gesture on the list restarts the 10 s wait, and a list scrolled away from the top keeps the order until it is back at the top. Collapsing the last expanded card keeps the order on screen. +While the dashboard is open, the overlay holds the order of collapsed cards, so a card does not move under your finger. New issues enter at the top (below any expanded cards) with a New tag for 2 s, a severity promotion moves a card up at once (never down), and other rank changes apply after 10 s of quiet while the list is scrolled to the top. Any touch, scroll or trackpad gesture on the list restarts the 10 s wait, and a list scrolled away from the top keeps the order until it is back at the top. Collapsing the last expanded card keeps the order on screen. Under a screen reader, held cards do not move on their own. Opening the dashboard, changing the severity filter, and hiding or restoring a card are then the only points that show the ranker's order, as they are without a screen reader, and a new issue enters at its rank position instead of the top. Exports, `ext.sleuth.*` and MCP always use the ranker's order. +## Showing a widget on screen + +The crosshair on a card, or Show on screen in an expanded card, outlines the issue's widget in the app. It shows on a card whose issue names a widget, and on the cards that list several widgets (always-repaint painters, missing RepaintBoundaries, intrinsic layout passes, uncached images and expensive GPU nodes). Before the first scan the crosshair stays pressed and the card tinted until the scan looks the widget up; a screen reader hears "Finding". When the widget is found, the card shrinks to a bar at the screen edge away from it: "Showing PriceTag", or "PriceTag is off screen" once it scrolls away. A card that lists several widgets starts on the first one on screen, and the bar shows its place ("1 of 3") with Next, which moves the outline to the next widget and wraps after the last. Back to issues brings the card back as it was; the bar's close ends the outline with the dashboard. When no widget is on screen, a toast says so and the card stays. + +Mark flagged widgets (the layers icon in the header) puts a corner marker on every flagged widget on screen, whatever the outline. + +## When the list is empty + +An empty list shows the run state instead of a blank card: Starting, Warming up with a countdown, No VM link (with the checks that are off and the `flutter run --profile --no-dds` command to copy), or No issues found with the number of checks on. A lost VM link shows the reconnect attempt, then that Sleuth stopped retrying, with Reconnect; this line also shows above a list of cards. A debug run adds one banner: "Debug build. Timings run slower than release; confirm with flutter run --profile." + ## System back and Escape -With the dashboard open, the system back gesture or button closes the innermost overlay layer before the app's own navigation sees it: first a focused text field, then a full-screen page (encyclopedia, guide, AI chat, Hidden list), then the dashboard. With the dashboard closed, back goes to the app unchanged. On Android, Sleuth claims predictive back swipes while a layer is open, also after the app navigates underneath it (for example back to its root route). On Flutter versions that offer the swipe to every listener, an app route that can pop may pop as well. After the dashboard closes at the app's root route, Android's back-to-home preview returns once the app navigates; back itself still leaves the app. +With the dashboard open, the system back gesture or button closes the innermost overlay layer before the app's own navigation sees it: first a focused text field, then a full-screen page (encyclopedia, guide, AI chat, Startup, Rebuild stats, Hidden list), then the More menu, then the highlight bar, then the dashboard. With the dashboard closed, back goes to the app unchanged. On Android, Sleuth claims predictive back swipes while a layer is open, also after the app navigates underneath it (for example back to its root route). On Flutter versions that offer the swipe to every listener, an app route that can pop may pop as well. After the dashboard closes at the app's root route, Android's back-to-home preview returns once the app navigates; back itself still leaves the app. -On a hardware keyboard, Escape first unfocuses a focused overlay text field, then closes the open page, then the dashboard. A focused text field or an open dialog or sheet in your app keeps its Escape. +On a hardware keyboard, Escape first unfocuses a focused overlay text field, then closes the open page, the More menu or the highlight bar, then the dashboard. An open dialog or sheet in your app keeps its Escape, and so does a focused app text field unless the highlight bar is up. ## Theme and accessibility -The header toggle cycles the overlay theme through System, Light and Dark. [Configuration](configuration.md#overlay-theming) covers custom themes and how they combine with the toggle. [Overlay accessibility](accessibility.md) covers screen readers, text size, touch targets, contrast and reduced motion. +Theme in the More menu sets the overlay theme to Auto, Light or Dark. [Configuration](configuration.md#overlay-theming) covers custom themes and how they combine with the toggle. [Overlay accessibility](accessibility.md) covers screen readers, text size, touch targets, contrast and reduced motion. diff --git a/doc/release_notes/1.0.0.md b/doc/release_notes/1.0.0.md new file mode 100644 index 00000000..87ce42df --- /dev/null +++ b/doc/release_notes/1.0.0.md @@ -0,0 +1,382 @@ +# sleuth 1.0.0 release notes + +The full list of changes in sleuth 1.0.0. [CHANGELOG.md](../../CHANGELOG.md) has the summary. The matching sidecar is `sleuth_mcp` 0.10.0; publish both together, because sidecar 0.9.0 refuses 1.0.0 apps. + +## Public API + +- `package:sleuth/sleuth.dart` exports only the names listed in + `doc/api_1_0_0.md`, each through a `show` list. +- Capture and validation tooling moved to the new + `package:sleuth/validation.dart`, which is outside the semver promise + and whose declarations are `@experimental`: + `EvidenceTier`, `DetectorMetadata`, `DetectorMetadataProvider`, + `BracketSpec`, `ComponentMetadata`, `ComponentMetadataProvider`, + `ProfileCaptureSchema`, `kSleuthPackageVersion`, `PollTimings`, + `StreamResourcePollResult`, and the detector types `RepaintDetector`, + `MemoryPressureDetector`, `StartupDetector`, `TrackedResourceDetector`, + `ImageMemoryDetector`, `GpuPressureDetector` and `FrameTimingDetector`. +- No longer exported: `ServiceExtensionRegistry`, `FixHintBuilder`, + `SessionMarkdownExporter`, `JankCaptureBuffer`, `FrameBudget`, + `FrameRateSource`, `resolveFrameBudget`, `RecurrenceTrend`, + `RecurrenceEntry`, `TrendDirection`, `buildWidgetHeatMap`, + `InteractionContextDisplay`, `ObservationSourceDisplay`, + `ConnectionMode` and `kMcpEnvelopeSchemaVersion`. +- `Sleuth.enrichStartupWithVmData`, `Sleuth.dartEntryMonotonicUs` and + `Sleuth.notifyControllerDisposed` are gone; only the package called + them. `StartupMetrics.dartEntryMonotonicUs` still carries the Dart entry + time. +- The capture statics on `Sleuth` (`markScenarioBegin`, `exportCaptureJson`, + `flushTimelineNow` and the others) and the accessors typed with + `validation.dart` types are `@experimental`. +- `RouteSession.endedAt` and `RouteSession.scanCycleCount` are read-only. +- `SleuthThemeData` drops the tokens the overlay no longer draws: the + confidence, effort and source colours with `confidenceColor`, + `effortColor` and `sourceAccentColor`, `sourceAccentWidth`, the VM and + FRAME badge pairs, the instrumentation banner pair, the three AI shimmer + colours, `aboutBackground`, `cardJankFlash`, `effectsBadge`, + `fixHintText`, `disclaimerText`, `highlightLabelText`, `fontXxl`, + `fontDisplay` and `focusRingWidth`. `fromColorScheme` no longer maps + `errorContainer`. +- A minor release can add values to `DetectorType`, `IssueCategory`, + `ObservationSource`, `PipelinePhase`, `InteractionContext` and + `FixEffort`; a `switch` over them needs a default branch. +- `test/validation/public_api_test.dart` checks the exports of both + libraries against the name list in + `test/validation/_fixtures/public_api_1_0_0.txt`. A CI job (`api-diff`) + compares `sleuth.dart` with the newest 1.x release on pub.dev using + `dart_apitool` 0.23.2 and fails on a breaking change without a major + version bump; it skips until 1.0.0 is on pub.dev. It counts a change to + an `@experimental` declaration as minor, but removing an + `@experimental` member of `Sleuth` or another class that is not + experimental as breaking. + +## Issue data and exports + +- Issues carry the number their detector reports with the detector's + warning and critical lines: `PerformanceIssue.measurement`, a final + class `IssueMeasurement` with `value`, `unit`, `warningAt`, `criticalAt`, + `window`, `warningStrict` and `criticalStrict`. A value reaches a line at + or above it, or only above it when that line is strict, and every + measurement is higher-is-worse. A line that is absent or not finite has + no flag. Issue JSON gains an optional `measurement` object; `windowMs` + keeps microseconds as a fraction. +- Built-in detectors set the measurement on 43 issue families. Most titles + show the value; `raster_dominance` titles show the raster-to-UI ratio and + `heap_near_capacity` titles a percent of the budget instead. Six + families report a pattern rather than an amount and leave it null + (`always_repaint_painter`, `runtime_font_loading`, `expensive_gpu_nodes`, + `layout_bottleneck`, `sliver_fill_remaining_scrollable`, + `sliver_to_box_adapter_shrinkwrap`), as do `event_loop_stall` when the + VM gave no stall length and `non_lazy_shrinkwrap` when the child count is + unknown. +- The issue's severity is the detector's and can disagree with the + severity the value reaches, in either direction. `rebuild_activity`, + `excessive_repaint` and the debug rates hold a severity until the value + stays low, and `heap_near_capacity` tolerates one sample under its line, + so a value can sit below the line of the severity shown. The debug rates + go critical only on the latest window while the value averages two, so a + warning can also carry a value above the critical line, and a critical + card a value below it; the card then says the latest second went over + the line. +- `jank_detected` and `sustained_jank` measure over the vsync span of the + frames they judged. The 240-frame buffer can make it shorter than 10 s, + capture mode judges the whole buffer, and frames without vsync times + give no window, as in `raster_dominance`. `rebuild_activity` and + `excessive_repaint` give the length of the VM window behind the share, + 1 to 1.5 s. +- `MeasurementUnit` is a class with constant instances instead of an enum, + so a later release can add a unit without breaking a `switch`. Units + compare by name, a custom detector can name its own, and a reader keeps + a unit it does not know by name instead of dropping the measurement. +- `SimpleStructuralDetector.report` takes an optional `measurement`. +- Snapshots and `ext.sleuth.issues` carry `notUsefulMarks`, the issues + marked Not useful in the overlay this session (oldest first, at most + 200), each with the issue's key, title, category, severity, confidence, + measurement, route, `markedAt` and whether it is still reported + (`live`). Like `suppressedCount` the key is metadata: written only when + there is a mark, whatever the projection, and never a `SnapshotSection`. + `SessionSnapshot.notUsefulMarks` and `NotUsefulMark` read it back. + `doc/mcp_schema.{json,md}` document it and the issue `measurement` + shape, and the sidecar lists it in `snapshotMetadataKeys`. +- `WidgetHighlight.occurrenceId`: the identity of the element a highlight + was measured from, equal to the `occurrenceId` of its issue. +- `DebugSnapshot.forcedRebuildsOf` (debug callbacks): for each widget type + a parent's build rebuilt, the types whose builds forced it, framework + widgets included, and how often. `forcedTypesCapped` reports its caps. +- `jank_detected`, `sustained_jank` and `slow_startup_ttff` carry + `observationSource: frameTiming`, the source of their times. Startup + said `structural` before and the jank issues carried none. +- `wrap_layout_bottleneck`, `excessive_repaint_boundary` and issues from + `SimpleStructuralDetector.report` carry the `widgetName` their highlight + carries (`Wrap`, the scrollable's type, the reported widget's runtime + type), so their cards keep the crosshair. Their overlay keys now + include the name. A card hidden under an older release stays hidden. + The stored overlay state moves to `schemaVersion` 2. A version 1 hide + key that names no widget now hides its issue's cards whatever widget + they name; one that names a widget stays as it is. 0.38 reads a + version 2 store as empty and leaves it as it is. The Hidden page names + each key by its id and widget (`wrap_layout_bottleneck · Wrap`), or + `· every widget` for a converted version 1 key. +- `FrameVerdict.relatedIssues` lists only the issues that can explain the + verdict's phase: the issues of the phase's category, `event_loop_stall` + for a start delay, and none for a stall or an unknown phase. Jank + symptoms are never listed, so exports carry fewer issues, and the + reason's `Related:` line names an issue of the phase. The overlay's Jank + tag shows on an issue for 10 s after the last late frame that named + it, the window `jank_detected` judges, and only while `jank_detected` + or `sustained_jank` is reported. Late frames alternate between phases, + so a tag that followed only the latest frame went on and off. A route + change drops the tags, as it drops the jank issues. +- Copying the snapshot from the overlay counts as Sleuth's own work, so + it is left out of the next frame's start delay. A second tap during a + copy is ignored. +- Sleuth resolves source locations per element. Earlier releases kept one + `file:line` per widget type. A type built in several places, such as a + `ValueListenableBuilder` on two screens, reported the first place + Sleuth saw in ancestor chains and fix hints. + +## Overlay + +### Header, menu and status row + +- The card header shows a run label, such as "Debug · VM" or "Profile · no + VM", in place of the VM+, FRAME and DBG badges. A narrow card drops the + "Sleuth" name first, then "· VM", and at last keeps only the dot, which + is filled with the VM and a ring without it. A screen reader always + hears the full label. +- A More button replaces the theme button. Its menu holds Startup (TTFF), + Rebuild stats (the route's count, opening the drilldown; "Off" in a run + that does not count rebuilds per route, which takes a profile run with + `enableDeepDebugInstrumentation`), Hidden issues + (with a count, only when there are any), Encyclopedia, Guide, Copy + snapshot and Theme (Auto, Light, Dark). It opens as a panel inside the + card. Back, Escape, a tap on the scrim or a tap on the app closes it, + and while it is open the app and the card body are hidden from screen + readers. Opening it moves keyboard focus to the first row that can act + and screen reader focus to the panel. More, the rows and the theme + segments draw a 2 px `checkboxActive` outline while the keyboard has + focus. A failed copy offers Try again. +- Tab and Shift+Tab move focus, and Enter and Space activate the focused + control, inside the card and its pages. The overlay sits above the + app's `WidgetsApp`, so the app's default shortcuts never reached it; a + focused text field keeps Enter and Space. +- The footer, the startup banner, the rebuild stats banner, the confidence + caption and the frame-rate detail row are gone. Hidden issues, Copy + snapshot, startup and rebuild stats moved into the More menu. +- The severity chips lead the status row ("2 critical"), with a filled + icon while on and an outlined one while off. The frame rate sits at the + end of the row. "Showing X of Y" appears only while a severity is off, + since the More menu counts hidden cards. A screen reader hears the line + change, and Show all moves its focus to the first chip. The overlay + refuses to turn a chip off when that would leave no card. The frame + rate, and the row's label when no chip shows, are semantics nodes of + their own. +- The card is at least 320 px wide, or the screen less 16 px margins on + a narrower screen, and stored narrower widths are raised. Its background + is opaque. +- The resize grip works on a maximized card too: the first drag or resize + step makes it a normal card at the size and place it has, then resizes + that. The list ends with 48 px of padding, so the grip does not cover + the last card. +- The overlay no longer uses `TweenAnimationBuilder`, so it is off the + debug instrumentation's framework denylist and an app's own uses count. +- The overlay no longer uses `Checkbox`, so it is off the framework + denylist and an app's own checkboxes count. + +### Collapsed card + +- The collapsed issue card is flat, with no source bar on its left edge. + A severity mark replaces the emoji: a disc or triangle filled with the + severity colour, with a white or near-black mark inside at 3:1 or more + against the fill; the discs carry a white ring. Where neither the fill + nor the ring reaches 3:1 on the card (the light presets' warning and OK + marks), the shape is outlined in a darker shade of its colour. The + title runs to 2 lines (3 above 1.3x text), or in full while the card is + expanded. +- One meta line under the title replaces the badges: category, + confidence, "seen X of Y scans, label" and "N effects", led by the Jank + and New tags. New shows for 2 seconds in place of the wider accent. +- A Debug timing tag replaces the confidence on a timing issue in a debug + run: a time-based measurement (milliseconds, seconds, percent or + frames) from the VM timeline or `FrameTiming`. The per-card "Debug mode + result" note is gone. +- An expanded card no longer draws the pin icon. Its semantics hint + reads "Pinned while expanded". +- An expanded card stays until it is collapsed, even when its issue stops + being reported or folds under another card. A card whose issue is gone + leads its meta line with "Not detected right now" and offers no + crosshair, and the empty states wait until it is collapsed. +- A press on a card, a More menu row, a theme segment or the crosshair + draws a light tint instead of the fallback theme's 40 % grey ripple. +- The guide's legend describes the new card. The "Jank flash" card + colour, which the overlay never drew, is gone from it. +- `badgeDbgBg` and `badgeDbgText` draw the Debug timing tag. +- A new issue list rebuilds only the cards it changes. A card whose + issue, tags, expansion and related rows stay the same keeps its widget, + where 0.38 rebuilt every card the list had built. + +### Expanded card + +- The expanded card shows its details below a divider, at the full card + width. An issue with a measurement opens with a Measured row: the value + and what it is ("10.0 ms in one build on the UI thread", with the + measurement's window where it has one), a gauge with the warning and + critical lines, and a line when the severity is held above the value or + judged on another reading. The issue's detail + follows, with what the number does not say (failing URLs and status + codes, for instance). The issues notifier fires when the Measured row's + text, a gauge line or the issue's scaffold changes, and not for a change + the row rounds away. +- Labelled rows follow: Where (the widget, and its `file:line` with Copy + when the run tracks widget creation), Route, When ("Last seen 2 s ago · + in 5 of the last 8 scans", with the interaction; the time is the + card's own when cards share a stable id), Evidence (the + detector's reason, then the confidence as a sentence of its own) and + Check in (the DevTools view). + The labels stack above their values when large text makes the label + column wider than a third of the card. "About this detection" is gone, + with its profile-mode line that said timing "matches production", and + so are the Ancestors, Widget, During and Source lines and the debug + fidelity tags. +- Caused by and Effects rows open the related card: a card in the list + expands, scrolls into view and takes focus; a hidden or filtered-out one + shows a toast with Show, which keeps the open cards expanded; an effect + folded under a card opens its detail and fix under its row. A related + issue no longer reported stays as plain text. Rebuild and setState + cards from the current screen link to its Rebuild stats once it has + counted rebuilds, with the count kept current on every scan, and the + slow startup card links to Startup metrics. +- A tap on an expanded card's details no longer collapses it; the header + does. Hide moves focus to the card that takes its place, scrolled into + view, and hiding a card whose issue is gone offers no Undo. +- The fix opens with "Fix" and its effort ("Fix · medium effort"). Code + lines after a line ending in a colon sit in a monospace block with Copy + code; a code run is lines with brackets, `;` or `=>`, commands, comments + and the indented lines of a call, and a bullet or a sentence ending in a + full stop is prose. The effort badges, and the keyword guess for an + issue without `fixEffort`, are gone. +- Labelled buttons replace the links and icons: Show on screen, Learn + more, Ask AI, Copy details, Hide and Not useful, each a 48 px target. + Not useful marks the issue for the session's exports and MCP output and + says so; a second tap removes the mark. Copy details and the Ask AI + prompt include the Measured row's value and lines. Ask AI's icon no + longer shimmers, so `ShaderMask` and `AnimatedBuilder` are off the + framework denylist and an app's own uses count. +- In a debug run, rebuild and setState cards show Why it rebuilds: the + rebuilds the widget started itself (a setState, a changed dependency + or a listenable) and those its parents forced, naming the parent that + forced the most and its share. The row reads only counts from the + card's own screen (a card kept from another screen asks to open it) + and says when the debug counters left widget types out. With + `enableDebugCallbacks` off, the row asks you to turn it on. With it + on, the row says when rebuilds are not counted: + `advanced.rebuildAttribution` is off, or DevTools holds the rebuild + callback. +- The guide describes the new expanded card and Not useful. + +### Status lines, banner and toasts + +- Status lines replace "No issues detected". Starting; Warming up, with + a progress bar; No VM link, with the checks off and partly off counted + from the session's coverage, a copyable `flutter run --profile --no-dds` + and a Guide link; No issues found, with the checks on and the opt-in + checks off; VM link lost, with the reconnect attempt; and stopped + retrying, with Reconnect. With cards listed, an expanded card whose + issue is gone included, only a lost link takes a line above them, and + the status row names the state when it counts no card. Each line is a + live region: its label names the state (stopped retrying has its own) + and its value holds the numbers, so the warm-up countdown does not + repeat it. +- One debug banner replaces the debug-mode and instrumentation banners. + Its text stops growing at the chrome limit, dismissing it lasts for the + session, and focus then moves to the severity chips. +- The trigger shows its count in debug runs too; the warning badge is + gone. +- Toasts use the theme's text and card colours swapped, regular 12 px + text and a bold action label in the accent at 4.5:1, and span the + screen up to 480 px. + +### Showing a widget on screen + +- A crosshair replaces the highlight checkbox, labelled "Show on + screen". Before the first scan it reads "Finding" until the scan looks + the widget up, and a second tap cancels. It shows for an issue that + names a widget, and for the issues that list several: + `always_repaint_painter`, `missing_repaint_boundary`, + `layout_bottleneck`, `uncached_images` and `expensive_gpu_nodes` + ("Show this issue's widgets on screen"). Other cards without a widget name no + longer offer a highlight that could not find anything. +- On a card that lists several widgets the crosshair outlines the first + one on screen, and the bar shows its place ("1 of 3") with Next, which + moves the outline to the next widget and wraps after the last. The + shown place changes with Next, not with each scan, so a screen reader + does not read the bar again after every scroll. A bar that steps opens + with focus on Next, keeps its edge unless the new outline runs under + it, and shields its old place for a double tap's span when it moves. + When the outlined widget leaves the screen, Next goes to the widget + that followed it, and shows while any other widget remains. The detectors tie those highlights to their issue + with the new `WidgetHighlight.stableId` and an element identity in + `WidgetHighlight.occurrenceId`. The card body + and the crosshair draw the focus outline in keyboard mode. +- While an issue's widget is outlined, the card shrinks to a bar at the + screen edge away from the widget: "Showing ", or " is + off screen" once it scrolls away, detaches or sits behind the keyboard, + until a later scan finds that element again. Back to issues, system + back and Escape (also while an app text field has focus, but not while + focus is in an app dialog or sheet that a tap outside it would close) + bring the card back as it + was (window state, size and list offset) with focus on + the crosshair. The bar's close ends the outline with the dashboard. + When the issue is no longer reported the card comes back and a toast + says so. The bar is never stored. Outlines follow a rotation or a + keyboard at once instead of at the next scan, and the outline's label + stays below the status bar. The outline takes the card's severity + colour; a widget's own highlight can be a warning on a critical card. + While the widget is off screen, the overlay neither dims the screen nor + labels the widget. +- An outline no longer turns the flagged-widget markers on: while the + bar is up only the issue's widget is outlined, unless the markers were + already on. Sleuth collects highlights while an outline is shown or + looked up, and finds the issue's own element again after it detaches. +- The header's markers toggle is named "Mark flagged widgets" and read as + on or off. It turns the markers alone on and off, never an outline or + a lookup, and turning it off hides them at once instead of at the next + scan. +- The outline's label sizes to its text, up to 350 px wide and two + lines whatever the width of the widget it names, follows the text scale + up to the chrome limit, and draws its text in the colour that reads on + its severity fill. It stays below the highlight bar when + the bar sits at the top. It names the widget once: the GPU, Wrap and + shrinkWrap details no longer repeat it ("GPU: RenderOpacity, 23 + descendants"). +- Highlighting an issue outlines only its own widget. An issue reported + for an element is outlined there or nowhere; one that names only a + widget type takes the first widget with that name, leaving out a list + card's widgets. Another widget of the same category is no longer + outlined in its place. When the widget + is not on screen a toast says "Couldn't find on screen. It may + be scrolled away or not built yet.", and no outline appears later. + +### Theme + +- `SleuthThemeData.severityGraphicColor`, set through the optional + `severityCriticalGraphic`, `severityWarningGraphic` and + `severityOkGraphic` constructor and `copyWith` parameters: the colour + of severity borders, icons and dots, which need 3:1 where text needs 4.5:1. The dark default uses the + severity fills, brighter than its pale text shades; the light preset + sets red-600, amber-600 and emerald-600, so its chips read red and + orange instead of two dark browns; the high-contrast presets and + `fromColorScheme` fall back to the severity text colours. +- `SleuthThemeData.cardDivider`, the colour of the card's dividers and + outlines: the line under the status row and above an expanded card's + details, the More menu's separators and border, and the borders of + code blocks, action buttons, status lines and the run label. It + defaults to `0xFF4B5563` in dark and `0xFFB4BAC4` in light, `border` in + the high-contrast presets and `outlineVariant` in `fromColorScheme`. + +## Example app + +- The example's `FileSleuthStateStore` keeps its file in the app's own + data directory: `files` on Android, `Library/Application Support` on + iOS. It used the system temp directory, which on Android is the app's + `code_cache`, so every install of a new build reset the overlay. diff --git a/doc/screenshots/overlay-dark.png b/doc/screenshots/overlay-dark.png index 2dfc2cdc..8624ec1e 100644 Binary files a/doc/screenshots/overlay-dark.png and b/doc/screenshots/overlay-dark.png differ diff --git a/doc/screenshots/overlay-light.png b/doc/screenshots/overlay-light.png index 4057670b..87961a90 100644 Binary files a/doc/screenshots/overlay-light.png and b/doc/screenshots/overlay-light.png differ diff --git a/doc/validation_matrix.md b/doc/validation_matrix.md index 5aa4a4e9..2290f7f9 100644 --- a/doc/validation_matrix.md +++ b/doc/validation_matrix.md @@ -21,8 +21,8 @@ against this written matrix, not against ad hoc spot checks. ``` Without `--no-dds`, `flutter run` starts DDS, which takes the VM service as its only client. Sleuth then cannot connect and stays in - FRAME mode on every platform. Run without `--no-dds` only when you - want FRAME mode, as in the degradation checks below. + no-VM mode on every platform. Run without `--no-dds` only when you + want no-VM mode, as in the degradation checks below. 2. For each platform and mode combination, work through the validation grid below. @@ -39,14 +39,16 @@ against this written matrix, not against ad hoc spot checks. 1. Launch the example app. Check that it boots without a crash. 2. Tap the dog button. Check that the dashboard opens and responds. 3. Check that the FPS value in the dashboard status row updates. -4. Check the mode badge next to the FPS value (VM+ or FRAME). +4. Check the run label in the card header ("Profile · VM" or "Profile · + no VM"). 5. Read the issue list in the dashboard. Note which issues appear; structural detectors should always fire. 6. Open a jank-producing demo (for example, "CSV Import"). Return to the dashboard and check the verdict: issue cards linked to the jank - frame carry a JANK badge. -7. In debug mode with `enableDebugCallbacks` on, check that the "DBG" - badge appears. + frame carry a Jank tag. +7. In a debug run, check that the run label starts with "Debug", that + the "Debug build" banner shows, and that timing issues carry a Debug + timing tag. 8. Record all results in the grid. --- @@ -66,7 +68,7 @@ against this written matrix, not against ad hoc spot checks. **Key:** - "Best-effort": the VM may or may not connect. When it does not, the - package falls back to FRAME mode. + package falls back to no-VM mode. - "Good": the VM connects reliably in practice when the app runs with `--no-dds`. - "Available if enabled": debug callbacks work with @@ -87,11 +89,11 @@ against this written matrix, not against ad hoc spot checks. | 2 | Overlay renders (dog button visible) | | | | | 3 | Dashboard opens and responds | | | | | 4 | FrameTiming produces frame data (FPS value updates) | | | | -| 5 | VM connection status | | | Record: VM+ or FRAME | +| 5 | VM connection status | | | Record: VM or no VM | | 6 | Structural detector issues appear | | | Read the issue list after visiting demos | | 7 | VM-backed issues appear (if VM connected) | | | ShaderJank, HeavyCompute, etc. | -| 8 | Mode badge matches connection state | | | VM+ (green) or FRAME (blue) | -| 9 | Debug mode warning shown (debug only) | | N/A | Yellow "Debug mode" banner | +| 8 | Run label matches connection state | | | "· VM" (filled green dot) or "· no VM" (amber ring) | +| 9 | Debug mode warning shown (debug only) | | N/A | "Debug build" banner | | 10 | Verdict appears on jank frame | | | Open the CSV Import demo and trigger jank | | 11 | Confidence wording matches mode | | | Confirmed/Likely with VM, Possible without | @@ -105,10 +107,10 @@ against this written matrix, not against ad hoc spot checks. | 2 | Overlay renders (dog button visible) | | | | | 3 | Dashboard opens and responds | | | | | 4 | FrameTiming produces frame data | | | | -| 5 | VM connection status | | | Record: VM+ or FRAME | +| 5 | VM connection status | | | Record: VM or no VM | | 6 | Structural detector issues appear | | | | | 7 | VM-backed issues appear (if VM connected) | | | | -| 8 | Mode badge matches connection state | | | | +| 8 | Run label matches connection state | | | | | 9 | Debug mode warning shown (debug only) | | N/A | | | 10 | Verdict appears on jank frame | | | | | 11 | Confidence wording matches mode | | | | @@ -123,10 +125,10 @@ against this written matrix, not against ad hoc spot checks. | 2 | Overlay renders (dog button visible) | | | | | 3 | Dashboard opens and responds | | | | | 4 | FrameTiming produces frame data | | | | -| 5 | VM connection status | | | Expect: VM+ | +| 5 | VM connection status | | | Expect: "· VM" | | 6 | Structural detector issues appear | | | | | 7 | VM-backed issues appear | | | Expect: yes (VM should connect) | -| 8 | Mode badge matches connection state | | | Expect: VM+ (green) | +| 8 | Run label matches connection state | | | Expect: "· VM" (filled green dot) | | 9 | Debug mode warning shown (debug only) | | N/A | | | 10 | Verdict appears on jank frame | | | | | 11 | Confidence wording matches mode | | | Expect: Confirmed/Likely verdicts | @@ -141,10 +143,10 @@ against this written matrix, not against ad hoc spot checks. | 2 | Overlay renders (dog button visible) | | | | | 3 | Dashboard opens and responds | | | | | 4 | FrameTiming produces frame data | | | | -| 5 | VM connection status | | | Expect: VM+ | +| 5 | VM connection status | | | Expect: "· VM" | | 6 | Structural detector issues appear | | | | | 7 | VM-backed issues appear | | | Expect: yes | -| 8 | Mode badge matches connection state | | | Expect: VM+ (green) | +| 8 | Run label matches connection state | | | Expect: "· VM" (filled green dot) | | 9 | Debug mode warning shown (debug only) | | N/A | | | 10 | Verdict appears on jank frame | | | | | 11 | Confidence wording matches mode | | | | @@ -158,18 +160,18 @@ against this written matrix, not against ad hoc spot checks. These checks cover how the package behaves without a VM connection. Desktop debug mode is the best place to test them, because the VM connects reliably there and you can watch the behavior change. Running -without `--no-dds` gives FRAME mode on any platform. +without `--no-dds` gives no-VM mode on any platform. ### Forced degradation test -On a platform where VM+ connects: +On a platform where the VM connects: | # | Check | Result | Notes | |---|-------|--------|-------| -| 1 | With VM+ active, the issue list shows VM-backed issues (e.g., build or paint share of UI time) | | | +| 1 | With the VM connected, the issue list shows VM-backed issues (e.g., build or paint share of UI time) | | | | 2 | Structural issues (ListView, RepaintBoundary, etc.) are always present regardless of VM | | | -| 3 | Verdict includes phase breakdown (build/layout/paint/raster) in VM+ mode | | | -| 4 | Mode badge shows "VM+" in green | | | +| 3 | Verdict includes phase breakdown (build/layout/paint/raster) with the VM | | | +| 4 | Run label shows "· VM" with a filled green dot | | | ### Natural degradation (Android without VM) @@ -177,7 +179,7 @@ On Android where the VM fails to connect: | # | Check | Result | Notes | |---|-------|--------|-------| -| 1 | Mode badge shows "FRAME" in blue | | | +| 1 | Run label shows "· no VM" with an amber ring | | | | 2 | FrameTiming data still flows (FPS value updates) | | | | 3 | Structural issues appear (visit demos, read the issue list) | | | | 4 | No VM-backed issues present (no shader jank, no heavy compute, no memory pressure, no event loop stall) | | | @@ -194,26 +196,26 @@ issue list. | Detector | Demo Screen | Expected Issue | Verified | |----------|------------|----------------|----------| -| Rebuild | High-Level setState | "Rebuild Activity: build phase N% of UI time" (VM+) or `rebuild_debug_*` (debug) | | +| Rebuild | High-Level setState | "Rebuild Activity: build phase N% of UI time" (VM) or `rebuild_debug_*` (debug) | | | SetStateScope | High-Level setState | "Wide setState Scope" | | | ListView | Non-Lazy ListView | "Non-lazy ListView" with child count | | | LayoutBottleneck | IntrinsicHeight Abuse | "Layout Bottleneck: N intrinsic nodes" | | | CustomPainter | Always-Repaint CustomPainter | "Always-Repaint CustomPainter" | | | ImageMemory | Uncached Images | "Oversized Images" with decoded vs shown size | | -| HeavyCompute | CSV Import (VM+ only) | "Heavy Build" or "Heavy Computation" on the main thread | | +| HeavyCompute | CSV Import (VM only) | "Heavy Build" or "Heavy Computation" on the main thread | | | KeepAlive | KeepAlive Overuse | "Excessive Keep-Alive" | | | FontLoading | Font Loading Stress | "Multiple Custom Fonts" with family count | | | RepaintBoundary | Missing RepaintBoundary | "Missing RepaintBoundary: N expensive widgets unprotected" | | -| Repaint | Live Waveform (VM+/debug) | "Likely Repaint Origin" (`repaint_debug_*`) or `excessive_repaint_debug` (debug); "Excessive Repainting: paint phase N% of UI time" only above 10 % (VM+) | | -| ShaderJank | Shader Jank (first run) | "Shader Compilation" (VM+ only) | | -| MemoryPressure | Memory Pressure | "Heap Growing" or "High GC Pressure" (VM+ only) | | -| PlatformChannel | Platform Channel Traffic | "High Platform Channel Traffic" (VM+ only) | | -| GpuPressure | GPU Pressure | "Raster Dominance" (FrameTiming leg `likely`, `confirmed` when a VM+ raster frame also misses the budget) | | +| Repaint | Live Waveform (VM or debug) | "Likely Repaint Origin" (`repaint_debug_*`) or `excessive_repaint_debug` (debug); "Excessive Repainting: paint phase N% of UI time" only above 10 % (VM) | | +| ShaderJank | Shader Jank (first run) | "Shader Compilation" (VM only) | | +| MemoryPressure | Memory Pressure | "Heap Growing" or "High GC Pressure" (VM only) | | +| PlatformChannel | Platform Channel Traffic | "High Platform Channel Traffic" (VM only) | | +| GpuPressure | GPU Pressure | "Raster Dominance" (FrameTiming leg `likely`, `confirmed` when a VM raster frame also misses the budget) | | | FrameTiming | CSV Import | "Jank Detected" with the share of frames over budget | | | NetworkMonitor | Search + Gallery | "Slow Request", "Large Response" or "Request Frequency Spike" | | | NetworkMonitor | NetworkMonitor capture helper, HTTP error spike and same-path modes | "HTTP Error Spike" or "High-Frequency Requests" | | -| EventLoopStall | Any screen, `ext.sleuthDemo.uiBlock ms=300` (VM+ only, Flutter 3.35 and later) | "Event Loop Stall: at least N ms" | | -| StreamResource | Stream Resource Leaks (VM+ only) | "Stream Resources Growing" | | +| EventLoopStall | Any screen, `ext.sleuthDemo.uiBlock ms=300` (VM only, Flutter 3.35 and later) | "Event Loop Stall: at least N ms" | | +| StreamResource | Stream Resource Leaks (VM only) | "Stream Resources Growing" | | | TrackedResource | Tracked Resource Leaks | "Tracked Resource Concurrent" or "Tracked Resource Long-Lived" | | | Startup | (any cold launch) | "Slow Startup" when time to first frame passes the warning threshold | | diff --git a/example/README.md b/example/README.md index 6516c09e..189754b6 100644 --- a/example/README.md +++ b/example/README.md @@ -22,7 +22,7 @@ event-loop and time-share detectors need. | # | Screen | Detectors triggered | Category | |---|--------|--------------------|----------| | 1 | High-Level setState | SetStateScope; Rebuild in debug builds at 10 or more taps a second | Build | -| 2 | Rebuild Hotspot (Dashboard) | Rebuild Stats banner (profile); Rebuild (debug) | Build | +| 2 | Rebuild Hotspot (Dashboard) | Rebuild stats in the More menu (profile); Rebuild (debug) | Build | | 3 | Non-Lazy ListView | ListView | Build | | 4 | Shrink-wrapped Sections | ListView (two cards, one per list) | Build | | 5 | CSV Import | HeavyCompute | Build | @@ -103,8 +103,10 @@ swaps in a scripted adapter (`lib/fake_ai_adapter.dart`): The app passes `FileSleuthStateStore` (`lib/file_state_store.dart`) as `SleuthConfig.stateStore`, so the trigger edge, card geometry, hidden -issues and severity filter survive restarts. The file lives in the system -temp directory, which the OS may clear. +issues and severity filter survive restarts. On Android and iOS the file +lives in the app's own `files` or `Library/Application Support` +directory, so it also survives a new install; elsewhere it uses the +system temp directory. The app registers service extensions for driving it from a VM service client in debug and profile builds. Each one returns JSON, and a failure @@ -122,8 +124,8 @@ of the app. | `ext.sleuthDemo.scroll` | Scrolls by `pixels` (default 600) over `ms` (default 600; `0` jumps), `axis=horizontal` for a horizontal list; picks the largest on-screen scrollable outside the demo's instructions; returns `{from, to}`, or `{error: timeout}` with `at` when the scroll does not finish | | `ext.sleuthDemo.fling` | `dx`, `dy` (logical px), optional `ms`; drags and releases on that scrollable | | `ext.sleuthDemo.orientation` | `value` = `portrait` \| `landscape` \| `all`; waits for the view to change and returns its size with `settled` | -| `ext.sleuthDemo.theme` | `mode` = `system` \| `light` \| `dark`; sets the overlay's theme mode | -| `ext.sleuthDemo.overlay` | `action` = `open` \| `close` \| `hide` (first visible card; returns its key) \| `undo` (most recently hidden key) \| `restoreAll` \| `toggleSeverity` (`severity` = `critical` \| `warning` \| `ok`) \| `setTheme` (`preset` = `hc_dark` \| `hc_light` \| `seed:` \| `none`); returns the state below | +| `ext.sleuthDemo.theme` | `mode` = `system` (Auto) \| `light` \| `dark`; sets the overlay's theme mode | +| `ext.sleuthDemo.overlay` | `action` = `open` \| `close` \| `hide` (first visible card; returns its key) \| `undo` (most recently hidden key) \| `restoreAll` \| `toggleSeverity` (`severity` = `critical` \| `warning` \| `ok`) \| `setTheme` (`preset` = `hc_dark` \| `hc_light` \| `seed:` \| `none`) \| `menu` (taps More, adds `menuOpen`) \| `notUseful` (taps Not useful on the expanded card on screen, adds `notUseful`, or `no_expanded_card`); returns the state below | | `ext.sleuthDemo.overlayState` | Persisted overlay state plus `dashboardOpen`, `uiStateReady` and `visibleIssueCount` | | `ext.sleuthDemo.clipboard` | Returns `{text}`, the current clipboard text | | `ext.sleuthDemo.a11y` | Platform accessibility settings, the overlay's text scale, overflow reports and the labelled semantics nodes | diff --git a/example/lib/demos/frame_timing_capture_screen.dart b/example/lib/demos/frame_timing_capture_screen.dart index 5568a425..dafaa12a 100644 --- a/example/lib/demos/frame_timing_capture_screen.dart +++ b/example/lib/demos/frame_timing_capture_screen.dart @@ -35,7 +35,7 @@ import 'capture_driver.dart'; /// 1. `cd example && fvm flutter run --profile --no-dds -d "iPhone 12" \ /// --dart-define=SLEUTH_CAPTURE_MODE=true \ /// --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12"`. The VM -/// service must be connected (VM+), because the export reads the +/// service must be connected (run label "· VM"), because the export reads the /// detector's records from the VM timeline. /// 2. Check that the pre-flight banner shows 60 Hz, capture mode on and /// the provenance the export stamps. diff --git a/example/lib/demos/frame_timing_sustained_jank_capture_screen.dart b/example/lib/demos/frame_timing_sustained_jank_capture_screen.dart index 42d8a346..7265a05f 100644 --- a/example/lib/demos/frame_timing_sustained_jank_capture_screen.dart +++ b/example/lib/demos/frame_timing_sustained_jank_capture_screen.dart @@ -39,7 +39,7 @@ import 'capture_driver.dart'; /// 1. `cd example && fvm flutter run --profile --no-dds -d "iPhone 12" \ /// --dart-define=SLEUTH_CAPTURE_MODE=true \ /// --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12"`. The VM -/// service must be connected (VM+), because the export reads the +/// service must be connected (run label "· VM"), because the export reads the /// detector's records from the VM timeline. /// 2. Open Capture Helpers, then FrameTiming (sustained_jank). Check /// that the refresh-rate line shows 60 Hz and the provenance line diff --git a/example/lib/demos/heavy_compute_capture_screen.dart b/example/lib/demos/heavy_compute_capture_screen.dart index a972b72d..382dde69 100644 --- a/example/lib/demos/heavy_compute_capture_screen.dart +++ b/example/lib/demos/heavy_compute_capture_screen.dart @@ -29,14 +29,14 @@ import 'capture_driver.dart'; /// **VM-service connection required.** This screen produces captures /// that satisfy `ProfileCaptureSchema.validateBracket(... /// requireDetectorTraceRecord: true, ...)` ONLY when SleuthController's -/// VmServiceClient is connected — i.e. the run is in VM+ mode, not -/// FRAME mode. Sleuth connects to the app's own VM service from inside +/// VmServiceClient is connected, that is, the run label reads "· VM". +/// Sleuth connects to the app's own VM service from inside /// the app, so USB and wireless both work. `flutter run` starts DDS by /// default, and DDS keeps the VM service as its only client, which -/// leaves Sleuth in FRAME mode. Launch with `--no-dds` (step 1), or +/// leaves Sleuth in no-VM mode. Launch with `--no-dds` (step 1), or /// start the installed app from the home screen. /// -/// In FRAME mode, the `HeavyComputeDetector` (vmOnly lifecycle) never +/// In no-VM mode, the `HeavyComputeDetector` (vmOnly lifecycle) never /// observes BUILD events, so the detector never emits the required /// `sleuth.issue.heavy_compute.` trace record. The schema /// audit will reject the resulting capture as "Missing detector trace @@ -296,8 +296,8 @@ class _HeavyComputeCaptureScreenState extends State { /// Refuses when: /// - No leg has completed in-band since screen open (or since the /// last leg tap). - /// - VM service is not connected — the procedure requires VM+ - /// mode (launched with `--no-dds`, or started from the home screen). + /// - VM service is not connected: the procedure requires the VM link + /// (launched with `--no-dds`, or started from the home screen). Future _exportLastLeg() async { final leg = _lastCompletedLeg; final tier = _lastCompletedTier; @@ -489,7 +489,7 @@ class _HeavyComputeCaptureScreenState extends State { // Timeline → SleuthController → `_recordIssuesForCapture` → // `CaptureHelper.recordIssue`. That pipeline only runs when // VM service is connected (launched with `--no-dds`, or from the - // home screen). FRAME-mode runs WILL NOT produce a + // home screen). no-VM runs WILL NOT produce a // `sleuth.issue.heavy_compute.*` trace record, and the // resulting capture is NOT acceptable for `runtimeVerified` // — the schema audit will reject it as "Missing detector diff --git a/example/lib/demos/memory_pressure_capture_screen.dart b/example/lib/demos/memory_pressure_capture_screen.dart index 5226ca0b..f97e721b 100644 --- a/example/lib/demos/memory_pressure_capture_screen.dart +++ b/example/lib/demos/memory_pressure_capture_screen.dart @@ -15,14 +15,14 @@ import 'heap_growing_capture_check.dart'; /// **VM-service connection required.** This screen produces captures that /// satisfy `ProfileCaptureSchema.validateBracket(... /// requireDetectorTraceRecord: true, ...)` ONLY when SleuthController's -/// VmServiceClient is connected — i.e. the run is in VM+ mode, not FRAME -/// mode. Sleuth connects to the app's own VM service from inside the app, +/// VmServiceClient is connected, that is, the run label reads "· VM". +/// Sleuth connects to the app's own VM service from inside the app, /// so USB and wireless both work. What blocks it is DDS: `flutter run` /// starts DDS by default and DDS keeps the VM service as its only client, -/// which leaves Sleuth in FRAME mode. Launch with `--no-dds` (step 1), or +/// which leaves Sleuth in no-VM mode. Launch with `--no-dds` (step 1), or /// start the installed app from the home screen. /// -/// In FRAME mode, the `MemoryPressureDetector` (vmOnly lifecycle) never +/// In no-VM mode, the `MemoryPressureDetector` (vmOnly lifecycle) never /// receives heap samples, so `_recordIssuesForCapture` never emits the /// required `sleuth.issue.heap_growing.warning` trace record. The schema /// audit will reject the resulting capture as "Missing detector trace @@ -465,7 +465,7 @@ class _MemoryPressureCaptureScreenState _log.add( '[${leg.label}] ABORT: Sleuth could not hold its ' 'allocation-profile reads. The VM service is not connected ' - '(FRAME mode: relaunch with --no-dds), its session changed, or ' + '(relaunch with --no-dds), its session changed, or ' 'a read was still in flight after $_profileDrainTimeoutSec s. ' 'Re-tap the leg.', ); @@ -724,8 +724,8 @@ class _MemoryPressureCaptureScreenState 'the app with `--dart-define=SLEUTH_CAPTURE_MODE=true`. ' 'Without it, markScenarioBegin/End emit nothing and the ' 'export has no markers to wrap. (2) The VM service ' - 'disconnected (FRAME mode). Kill the app from Xcode and ' - 're-open it from the home screen so VM+ mode activates. ' + 'disconnected (run label "· no VM"). Kill the app from Xcode and ' + 're-open it from the home screen so the VM link comes back. ' 'Re-tap the leg after fixing.', ); }); diff --git a/example/lib/demos/network_monitor_capture_screen.dart b/example/lib/demos/network_monitor_capture_screen.dart index 1c4f3dfd..64acc736 100644 --- a/example/lib/demos/network_monitor_capture_screen.dart +++ b/example/lib/demos/network_monitor_capture_screen.dart @@ -29,10 +29,10 @@ import 'capture_driver.dart'; /// --dart-define=SLEUTH_CAPTURE_MODE=true \ /// --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12"`. /// 2. Sleuth's VmServiceClient connects to the app's own VM service -/// (VM+ mode), so the real `NetworkMonitorDetector` observes HTTP +/// (VM mode), so the real `NetworkMonitorDetector` observes HTTP /// completions and emits the three target trace records. Without /// `--no-dds`, `flutter run` starts DDS, which keeps the VM service -/// as its only client and leaves Sleuth in FRAME mode. In that case +/// as its only client and leaves Sleuth in no-VM mode. In that case /// quit `flutter run` (`q`) and re-open the app from the home /// screen, so no DDS attaches. /// 3. Navigate to "NetworkMonitor capture helper" → pick a mode → diff --git a/example/lib/demos/platform_channel_capture_screen.dart b/example/lib/demos/platform_channel_capture_screen.dart index 60cea172..a3e4870c 100644 --- a/example/lib/demos/platform_channel_capture_screen.dart +++ b/example/lib/demos/platform_channel_capture_screen.dart @@ -15,12 +15,12 @@ import 'capture_driver.dart'; /// /// **VM-service connection required.** Same constraint as the other /// runtimeVerified capture screens: SleuthController's VmServiceClient -/// must be connected (VM+ overlay) so `_recordIssuesForCapture` lands +/// must be connected (overlay with a VM link) so `_recordIssuesForCapture` lands /// the `sleuth.issue.platform_channel_traffic.` trace event /// inside the scenario span. Sleuth connects to the app's own VM service /// from inside the app, so USB and wireless both work. `flutter run` /// starts DDS by default, and DDS keeps the VM service as its only -/// client, which leaves Sleuth in FRAME mode. Launch with `--no-dds` +/// client, which leaves Sleuth in no-VM mode. Launch with `--no-dds` /// (step 1), or start the installed app from the home screen. /// /// **Why the procedure differs from `MemoryPressureCaptureScreen`.** @@ -542,8 +542,8 @@ class _PlatformChannelCaptureScreenState "completed but Sleuth.exportCaptureJson returned null at " 'compose-time. Common causes: (1) captureMode is OFF. ' 'Restart with --dart-define=SLEUTH_CAPTURE_MODE=true. ' - '(2) The VM service disconnected (FRAME mode). Re-launch from ' - 'the home screen so VM+ mode activates. Re-tap after fixing.', + '(2) The VM service disconnected (run label "· no VM"). Re-launch from ' + 'the home screen so the VM link comes back. Re-tap after fixing.', ); }); return; diff --git a/example/lib/demos/raster_dominance_capture_screen.dart b/example/lib/demos/raster_dominance_capture_screen.dart index 3edc42bf..c29af672 100644 --- a/example/lib/demos/raster_dominance_capture_screen.dart +++ b/example/lib/demos/raster_dominance_capture_screen.dart @@ -39,7 +39,7 @@ import 'capture_driver.dart'; /// 1. `cd example && fvm flutter run --profile --no-dds -d "iPhone 12" \ /// --dart-define=SLEUTH_CAPTURE_MODE=true \ /// --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12"`. The VM -/// service must be connected (VM+), because the export reads the +/// service must be connected (run label "· VM"), because the export reads the /// detector's records from the VM timeline. /// 2. Open Capture Helpers, then GpuPressure (raster_dominance), in /// portrait at the default text size. Check that the refresh-rate diff --git a/example/lib/demos/tracked_resource_capture_screen.dart b/example/lib/demos/tracked_resource_capture_screen.dart index 697ae904..424a506d 100644 --- a/example/lib/demos/tracked_resource_capture_screen.dart +++ b/example/lib/demos/tracked_resource_capture_screen.dart @@ -20,9 +20,9 @@ import 'capture_driver.dart'; /// 1. `cd example && fvm flutter run --profile --no-dds -d "iPhone 12" \ /// --dart-define=SLEUTH_CAPTURE_MODE=true \ /// --dart-define=SLEUTH_CAPTURE_DEVICE="iPhone 12"`. -/// 2. Sleuth connects to the app's own VM service (VM+ mode). Without +/// 2. Sleuth connects to the app's own VM service (VM mode). Without /// `--no-dds`, `flutter run` starts DDS, which keeps the VM service -/// as its only client and leaves Sleuth in FRAME mode. In that case +/// as its only client and leaves Sleuth in no-VM mode. In that case /// quit `flutter run` (`q`) and re-open the app from the home screen. /// 3. Navigate to "Tracked resource capture helper" → tap a leg → /// wait for "tap Export now" → tap **Export last leg** → paste diff --git a/example/lib/file_state_store.dart b/example/lib/file_state_store.dart index eb380994..9e59ecbb 100644 --- a/example/lib/file_state_store.dart +++ b/example/lib/file_state_store.dart @@ -1,20 +1,24 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:sleuth/sleuth.dart'; /// Keeps the Sleuth overlay state (trigger position, card geometry, /// hidden issues, severity filter) in a JSON file, so it survives app -/// restarts. +/// restarts and new installs of the app. /// -/// Uses the system temp directory to avoid a `path_provider` dependency; -/// the OS may clear it, which only resets the overlay to its defaults. An -/// app would usually store this under its documents or support directory. +/// The file goes in the directory [overlayStateDirectory] picks, to avoid +/// a `path_provider` dependency. An app would usually ask `path_provider` +/// for its application support directory. class FileSleuthStateStore implements SleuthStateStore { FileSleuthStateStore([File? file]) - : _file = - file ?? - File('${Directory.systemTemp.path}/sleuth_overlay_state.json'); + : _file = file ?? File('${_defaultDirectory.path}/$_fileName'); + + static const _fileName = 'sleuth_overlay_state.json'; + + static Directory get _defaultDirectory => + overlayStateDirectory(Directory.systemTemp, Platform.operatingSystem); final File _file; @@ -51,6 +55,7 @@ class FileSleuthStateStore implements SleuthStateStore { // Renames are queued, and one older than the newest promoted write is // dropped, so a late write never replaces newer state. final sequence = ++_started; + await _file.parent.create(recursive: true); final tmp = File('${_file.path}.$sequence.tmp'); await tmp.writeAsString(json, flush: true); final rename = _renames.then((_) async { @@ -65,3 +70,25 @@ class FileSleuthStateStore implements SleuthStateStore { await rename; } } + +/// The directory [FileSleuthStateStore] keeps its file in by default, from +/// the system temp directory [temp] on [operatingSystem] +/// (`Platform.operatingSystem`). +/// +/// On Android the temp directory is the app's `code_cache`, which every +/// install of a new build empties, and on iOS it is the container's `tmp`, +/// which the OS empties when it needs space. The file goes beside them in +/// the app's own `files` and `Library/Application Support` directories. +/// Elsewhere, or when the temp directory is not where these layouts put +/// it, the temp directory itself is used. +@visibleForTesting +Directory overlayStateDirectory(Directory temp, String operatingSystem) { + final segments = temp.uri.pathSegments.where((s) => s.isNotEmpty); + final name = segments.isEmpty ? null : segments.last; + final app = temp.parent.path; + return switch ((operatingSystem, name)) { + ('android', 'code_cache') => Directory('$app/files'), + ('ios', 'tmp') => Directory('$app/Library/Application Support'), + _ => temp, + }; +} diff --git a/example/lib/harness_targeting.dart b/example/lib/harness_targeting.dart index 44cb620d..33fbc9ac 100644 --- a/example/lib/harness_targeting.dart +++ b/example/lib/harness_targeting.dart @@ -66,6 +66,16 @@ Element? findSemanticsLabel(String label) { findForeground((e) => of(e.widget)?.startsWith(label) ?? false); } +/// The foreground [Semantics] button labelled exactly [label]. A panel +/// that carries the same label, such as the overlay's More menu next to +/// its More button, is not a button and is skipped. +Element? findSemanticsButton(String label) => findForeground((e) { + final w = e.widget; + return w is Semantics && + w.properties.button == true && + w.properties.label == label; +}); + /// The field `type` writes to: the focused [EditableText] when it is in /// the foreground, else the last foreground one a tap at its centre would /// reach (a field behind an open overlay page is skipped). diff --git a/example/lib/main.dart b/example/lib/main.dart index 390c7ff4..f5b8ac23 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1144,6 +1144,26 @@ void _registerDemoExtensions() { Sleuth.updateTheme(theme); } result['preset'] = preset; + case 'menu': + // Taps More: opens the menu, or closes it when it is open. + final error = await _tapLabel('More'); + if (error != null) return _demoError({'error': error, 'text': 'More'}); + await waitForFrame(); + result['menuOpen'] = findSemanticsLabel('Copy snapshot') != null; + case 'notUseful': + // Taps Not useful on the expanded card on screen, which marks the + // issue for the session or removes the mark. + final error = await _tapLabel('Not useful'); + if (error != null) { + return _demoError({ + 'error': error == 'not_found' ? 'no_expanded_card' : error, + }); + } + await waitForFrame(); + final button = findSemanticsButton('Not useful')?.widget; + result['notUseful'] = button is Semantics + ? button.properties.toggled + : null; case 'toggleSeverity': final severity = IssueSeverity.values .where((s) => s.name == params['severity']) @@ -1163,8 +1183,8 @@ void _registerDemoExtensions() { jsonEncode({...result, ..._overlayStateJson(state)}), ); }); - // Theme mode as the header toggle sets it: `mode` = system | light | - // dark. An `updateTheme` override is kept and shows under System. + // Theme mode as Theme in the More menu sets it: `mode` = system (Auto) | + // light | dark. An `updateTheme` override is kept and shows under Auto. developer.registerExtension('ext.sleuthDemo.theme', (method, params) async { final state = Sleuth.overlayUiState; if (state == null) return _demoError({'error': 'no_controller'}); @@ -1605,6 +1625,19 @@ class _MetricsObserver with WidgetsBindingObserver { int _syntheticPointer = 900; +/// Taps the foreground button whose semantics label is [label]; a panel +/// that carries the same label, such as the More menu, is skipped. +/// Returns an error code, or null after the tap. +Future _tapLabel(String label) async { + final element = findSemanticsButton(label); + if (element == null) return 'not_found'; + final target = await prepareTap(element); + final position = target.position; + if (position == null) return target.error; + await _tapAt(position, viewId: target.viewId); + return null; +} + /// Down, 60 ms, up at [position] in the view [viewId] (0, the implicit /// view, for a bare point). Future _tapAt(Offset position, {int viewId = 0}) async { diff --git a/example/pubspec.lock b/example/pubspec.lock index 9e668985..5da52407 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -187,7 +187,7 @@ packages: path: ".." relative: true source: path - version: "0.38.0" + version: "1.0.0" source_span: dependency: transitive description: diff --git a/example/test/file_state_store_test.dart b/example/test/file_state_store_test.dart index 08a97866..d6bb0e71 100644 --- a/example/test/file_state_store_test.dart +++ b/example/test/file_state_store_test.dart @@ -35,4 +35,41 @@ void main() { ..writeAsBytesSync([0xff, 0xfe, 0x00, 0x7b]); expect(await FileSleuthStateStore(file).read(), isNull); }); + + test('a write creates the directory the file goes in', () async { + final store = FileSleuthStateStore(File('${dir.path}/files/state.json')); + await store.write('{"schemaVersion":2}'); + expect(await store.read(), '{"schemaVersion":2}'); + }); + + group('overlayStateDirectory', () { + // Android empties code_cache on every install of a new build. + test('Android keeps the file in files beside code_cache', () { + final picked = overlayStateDirectory( + Directory('/data/user/0/com.example.example/code_cache'), + 'android', + ); + expect(picked.path, '/data/user/0/com.example.example/files'); + }); + + test('iOS keeps the file in Library/Application Support beside tmp', () { + final picked = overlayStateDirectory( + Directory('/var/mobile/Containers/Data/Application/ABC/tmp/'), + 'ios', + ); + expect( + picked.path, + '/var/mobile/Containers/Data/Application/ABC/Library/' + 'Application Support', + ); + }); + + test('another layout or platform keeps the temp directory', () { + final elsewhere = Directory('/tmp/sleuth'); + expect(overlayStateDirectory(elsewhere, 'android'), same(elsewhere)); + expect(overlayStateDirectory(elsewhere, 'ios'), same(elsewhere)); + final tmp = Directory('/var/folders/x/T/tmp'); + expect(overlayStateDirectory(tmp, 'macos'), same(tmp)); + }); + }); } diff --git a/example/test/harness_targeting_test.dart b/example/test/harness_targeting_test.dart index 32e70ee8..9dd85654 100644 --- a/example/test/harness_targeting_test.dart +++ b/example/test/harness_targeting_test.dart @@ -292,4 +292,34 @@ void main() { expect(result.error, 'reveal_timeout'); await tester.pumpWidget(const SizedBox()); }); + + testWidgets('a button finder skips a panel with the same label', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Column( + children: [ + Semantics( + key: const ValueKey('button'), + button: true, + label: 'More', + onTap: () {}, + child: const SizedBox(width: 48, height: 48), + ), + // The open menu, after the button in the tree. + Semantics( + key: const ValueKey('panel'), + container: true, + label: 'More', + child: const SizedBox(width: 200, height: 200), + ), + ], + ), + ), + ); + expect(findSemanticsLabel('More')?.widget.key, const ValueKey('panel')); + expect(findSemanticsButton('More')?.widget.key, const ValueKey('button')); + expect(findSemanticsButton('Less'), isNull); + }); } diff --git a/lib/src/controller/sleuth_controller.dart b/lib/src/controller/sleuth_controller.dart index 98b97357..5c6af0c8 100644 --- a/lib/src/controller/sleuth_controller.dart +++ b/lib/src/controller/sleuth_controller.dart @@ -1882,8 +1882,8 @@ class SleuthController { }; /// Unmodifiable view of the current recurrence trend map, keyed by - /// issue `stableId`. Intended for the floating card to render "Seen X/Y" - /// badges. Trends update on every scan cycle; listeners should use + /// issue `stableId`. The floating card renders it as "seen X of Y scans" + /// in each meta line. Trends update on every scan cycle; listeners use /// [scanTickNotifier] as the rebuild trigger. Map get recurrenceTrends => Map.unmodifiable(_recurrenceTrends); diff --git a/lib/src/models/recurrence_trend.dart b/lib/src/models/recurrence_trend.dart index fa6bf8c0..11afe04a 100644 --- a/lib/src/models/recurrence_trend.dart +++ b/lib/src/models/recurrence_trend.dart @@ -1,6 +1,7 @@ /// Trend direction for an issue's recurrence over time. /// -/// Rendered on the issue card as a "Seen X/Y · {label}" badge, where: +/// Rendered in the issue card's meta line as "seen X of Y scans, {label}", +/// where: /// - **X** = [RecurrenceTrend.presentCount] (cycles where the issue fired) /// - **Y** = [RecurrenceTrend.length] (total cycles observed in the ring buffer) /// diff --git a/lib/src/ui/guide_page.dart b/lib/src/ui/guide_page.dart index c2c3e905..21deab83 100644 --- a/lib/src/ui/guide_page.dart +++ b/lib/src/ui/guide_page.dart @@ -327,7 +327,10 @@ class _GuidePageState extends State _GuideStep( step: '1', title: 'Run in profile mode', - detail: 'Use flutter run --profile for accurate timing data.', + detail: + 'Use flutter run --profile --no-dds. Profile mode gives ' + 'accurate timings, and --no-dds lets Sleuth reach the VM ' + 'service for the checks that need it.', ), _GuideStep( step: '2', @@ -379,7 +382,8 @@ class _GuidePageState extends State 'The header names the build mode and whether Sleuth reached the ' 'VM service, for example "Debug · VM" or "Profile · no VM". ' 'Without the VM, the checks that need it are off and some run ' - 'in part. The dot is green with the VM and amber without it. ' + 'in part. The dot is filled green with the VM and an amber ' + 'ring without it. ' 'A narrow card shortens the text, down to the dot alone.', ), _infoTile( @@ -450,8 +454,9 @@ class _GuidePageState extends State theme, '\u{2B05}\u{FE0F}', 'Back', - 'The system back gesture or button closes the open page, then ' - 'the card, before your app\'s navigation sees it.', + 'The system back gesture or button closes the open page, the ' + 'More menu or the highlight bar, then the card, before your ' + 'app\'s navigation sees it.', ), _infoTile( theme, @@ -910,7 +915,8 @@ class _GuidePageState extends State _tipRow( theme, Icons.layers_outlined, - 'The layers icon in the header marks every flagged widget on screen with a corner marker.', + 'Mark flagged widgets (the layers icon in the header) puts a corner ' + 'marker on every flagged widget on screen. Tap it again to hide them.', ), _tipRow( theme, @@ -951,8 +957,9 @@ class _GuidePageState extends State _tipRow( theme, Icons.format_size, - 'Text follows the system size up to 2 times. The header, counts ' - 'and badges stop at 1.3 times so the issue list keeps its room.', + 'Text follows the system size up to 2 times. The header, the status ' + 'row with its severity chips, and the trigger stop at 1.3 times so ' + 'the issue list keeps its room.', ), _tipRow( theme, @@ -964,8 +971,9 @@ class _GuidePageState extends State theme, Icons.keyboard_outlined, 'Escape first leaves a focused Sleuth text field, then closes ' - 'the open page, then the card. A dialog or text field in your ' - 'app keeps its Escape.', + 'the open page, the More menu or the highlight bar, then the ' + 'card. A dialog in your app keeps its Escape, and so does a text ' + 'field unless the highlight bar is up.', ), ], ); diff --git a/lib/src/ui/issue_card.dart b/lib/src/ui/issue_card.dart index d13e0a31..935766c0 100644 --- a/lib/src/ui/issue_card.dart +++ b/lib/src/ui/issue_card.dart @@ -83,15 +83,16 @@ class IssueCard extends StatefulWidget { final PerformanceIssue issue; - /// Recurrence trend for this issue, used to render "Seen X/Y" badge. + /// Recurrence trend for this issue, for the meta line's "seen X of Y + /// scans" text. /// Null when the issue has no trend data (e.g. first scan). /// /// Fixed at build time. Prefer [recurrenceTrendOf] when the trend /// changes between rebuilds of the parent. final RecurrenceTrend? recurrenceTrend; - /// Live source for the "Seen X/Y" badge. Called on every [scanTick] - /// notification, so the badge tracks the latest trend without the + /// Live source for the "seen X of Y scans" text. Called on every + /// [scanTick] notification, so the text tracks the latest trend without the /// parent list rebuilding. Takes precedence over [recurrenceTrend] /// when it returns non-null. final RecurrenceTrend? Function()? recurrenceTrendOf; @@ -937,8 +938,8 @@ class _IssueCardState extends State { /// See [RecurrenceTrend.computeTrend] for the window (default 10 /// entries) and the ±0.3 severity-delta thresholds. // NOTE: These labels are documented. If you rename one, update the - // table in `RecurrenceTrend`'s enum dartdoc, the "Recurrence badge" - // section of doc/internals.md and the label list in README.md. + // table in `RecurrenceTrend`'s enum dartdoc, the "Recurrence" section + // of doc/internals.md and the label list in README.md. static String _trendLabel(RecurrenceTrend trend) { final total = trend.length; final ratio = total == 0 ? 0.0 : trend.presentCount / total; diff --git a/lib/src/vm/service_extension_handlers.dart b/lib/src/vm/service_extension_handlers.dart index 134f1888..fcbc72f4 100644 --- a/lib/src/vm/service_extension_handlers.dart +++ b/lib/src/vm/service_extension_handlers.dart @@ -18,7 +18,7 @@ const int kMcpEnvelopeSchemaVersion = 1; /// Stamped on `ext.sleuth.diagnose`. Keep in sync with `pubspec.yaml`. @experimental -const String kSleuthPackageVersion = '0.38.0'; +const String kSleuthPackageVersion = '1.0.0'; typedef ExtensionHandler = FutureOr> Function( diff --git a/packages/sleuth_mcp/CHANGELOG.md b/packages/sleuth_mcp/CHANGELOG.md index 26686366..81dc8cd5 100644 --- a/packages/sleuth_mcp/CHANGELOG.md +++ b/packages/sleuth_mcp/CHANGELOG.md @@ -1,11 +1,32 @@ -## Unreleased +## 0.10.0 -- `snapshotMetadataKeys` includes `notUsefulMarks`, the envelope key in - which newer apps list the issues marked Not useful in the overlay. The - sidecar's own projection keeps it like `suppressedCount`. +Pins sleuth 1.0.0, accepts 0.38 apps with a warning and refuses 0.37 +apps. Publish it together with sleuth 1.0.0. The +[full release notes](https://github.com/Harrys76/sleuth/blob/main/packages/sleuth_mcp/doc/release_notes/0.10.0.md) +list every change. + +- From 1.0.0 a lineage is the app's major version (`versionLineage` + returns `1` for `1.4.2`), because sleuth 1.x follows semver. Any 1.x + app connects, with `version_skew_minor` when it is not the pinned + version. Below 1.0.0 the lineage stays `major.minor`. +- `compare_snapshots` adds `versionWarning` when the two snapshots come + from different releases of one lineage, such as 1.0.0 and 1.2.0. A + release can add detectors, move thresholds or fix false positives, so + part of the diff may come from the upgrade. +- Compact issues in `get_issues` and `get_snapshot` keep the issue's + `measurement` object: the number its detector reported, with the + warning and critical lines. +- `snapshotMetadataKeys` includes `notUsefulMarks`, the `data` key in + which the app lists the issues marked Not useful in the overlay. The + sidecar's own projection keeps it like `suppressedCount`, and + `get_issues` passes it through. +- The `triage_performance`, `audit_memory` and `release_check` prompts + tell the model to quote each issue's `measurement` against its warning + or critical line, and how to match `notUsefulMarks` to issues. The + triage lists marked issues last, the memory audit sets them aside, and + the release gate keeps them in its verdict. - The schema copy documents `notUsefulMarks` and the issue `measurement` - shape, and the tool schema describes the `measurement` object of a - verbose issue. + shape. ## 0.9.0 diff --git a/packages/sleuth_mcp/README.md b/packages/sleuth_mcp/README.md index cf442648..78a39978 100644 --- a/packages/sleuth_mcp/README.md +++ b/packages/sleuth_mcp/README.md @@ -28,7 +28,7 @@ sleuth_mcp install # adds mcpServers.sleuth to ~/.claude.json You can run `install` again. It takes a lock, writes the config through an atomic rename and keeps a `.bak` copy. For a project-local install, add -`sleuth_mcp: ^0.9.0` to `dev_dependencies` instead. The server also sends +`sleuth_mcp: ^0.10.0` to `dev_dependencies` instead. The server also sends this workflow to the client in the `instructions` field of its `initialize` result. @@ -61,12 +61,14 @@ child or `iproxy` tunnel it started. ## Version compatibility -sleuth_mcp 0.9.0 is built against sleuth 0.38.0. When it connects, it -reads the app's `packageVersion` from `ext.sleuth.diagnose`. Another 0.38 -version connects with `warning: version_skew_minor`, and a 0.37 app with -`warning: version_skew_prior_lineage`. The sidecar refuses any other -lineage (`version_skew_major`) and a version that is not -`major.minor.patch` (`version_skew_unknown`), and disconnects. +sleuth_mcp 0.10.0 is built against sleuth 1.0.0. When it connects, it +reads the app's `packageVersion` from `ext.sleuth.diagnose`. Sleuth follows +semver from 1.0.0, so the sidecar compares major versions from there on. +Any other 1.x version connects with `warning: version_skew_minor`, and a +0.38 app with `warning: version_skew_prior_lineage`. The sidecar refuses +any other version line (`version_skew_major`, including 0.37 and 2.x) and a +version that is not `major.minor.patch` (`version_skew_unknown`), and +disconnects. ## Tools @@ -78,7 +80,7 @@ lineage (`version_skew_major`) and a version that is not | `detach_app` | Ends the session in any state and stops an attach in progress. You can call it when nothing is attached. | | `app_status` | Reports whether a session is attached, which tool connected it, and its device, app and session ids. | | `hot_reload` | Hot reloads an app attached with `attach_app(device:)` and keeps its state. Other sessions get `hot_reload_unsupported`. | -| `diagnose` | Reports the package version and the VM link (`vmConnected`), the frame budget and VM poll timings, and on sleuth 0.38 the timeline stream and event-loop stall counters. Call it first when other tools return nothing. | +| `diagnose` | Reports the package version and the VM link (`vmConnected`), the frame budget, VM poll timings, and the timeline stream and event-loop stall counters. Call it first when other tools return nothing. | | `get_issues` | Returns the current issues in rank order, optionally filtered by `route` or `severityAtLeast`. | | `get_snapshot` | Returns the performance snapshot: issues, frame stats, route history, session summary and recurrence trends. | | `get_route_health` | Returns the health score, FPS and issue counts for each route. | @@ -100,8 +102,8 @@ return shape and error code. `full: true` for everything, `sections` to pick sections, or `diskHandoff: true` to write the whole snapshot to a temp file and get its path. -- **Compact issues.** `get_issues` and `get_snapshot` return 12 fields per - issue, including `stableId` and `severity`, and `verbose: true` returns +- **Compact issues.** `get_issues` and `get_snapshot` return 13 fields per + issue, including `stableId`, `severity` and `measurement`, and `verbose: true` returns all of them. `get_issues` returns the top 50 and adds `_truncated` and `_totalCount` when it drops any. `maxIssueCount` changes the cap, and `0` removes it. @@ -122,7 +124,10 @@ stableId) and `sleuth://causal-graph` (the rules that link causes to effects) are cached per app session. The prompts `triage_performance`, `audit_memory` and `release_check` tell the client's model which tools to call in order, and end with ranked fixes, memory fixes, or a PASS, FAIL or -NOT RUN verdict. +NOT RUN verdict. They have the model quote each issue's measured value +against its warning or critical line and handle the issues the developer +marked Not useful in the overlay: listed last in the triage, set aside in +the memory audit, and still counted by the release gate. ## Attaching diff --git a/packages/sleuth_mcp/doc/mcp_tool_schema.json b/packages/sleuth_mcp/doc/mcp_tool_schema.json index 25d9674e..9547ea26 100644 --- a/packages/sleuth_mcp/doc/mcp_tool_schema.json +++ b/packages/sleuth_mcp/doc/mcp_tool_schema.json @@ -22,7 +22,7 @@ "The error envelopes describe how the sidecar handlers behave today. Each handler returns ToolCallResult.text(...) with `isError: true` and one text content that carries the message string. The codes in `errors[].code` appear verbatim as the prefix of that text. There is no central typed error class yet, which is known debt.", "Passthrough tools call bridge.callExtension and return the ext.sleuth.* envelope unchanged. Each shim is documented on its own so the audit can check its behavior: the get_issues severityAtLeast filter, the compact-issue projection on get_issues and get_snapshot, the get_snapshot default projection and disk handoff, and the launchModeAdvisory stamp.", "The nested snapshot and route-session shapes are in mcp_schema.{json,md}. This file documents only how the tools wrap them.", - "schemaVersion 2 adds the compact-issue projection. By default get_issues and get_snapshot trim each issue to the actionable subset (severity, category, confidence, title, detail, fixHint, stableId, widgetName, routeName, sourceRoute, confidenceReason, rootCauseIds). Pass verbose:true for the full shape of up to 27 keys, which includes the optional measurement object (value, unit, warningAt, criticalAt, windowMs, warningStrict, criticalStrict). Compaction removes fields only and keeps the diff key (stableId and severity), so compare_snapshots and check_budgets work on compact snapshots.", + "schemaVersion 2 adds the compact-issue projection. By default get_issues and get_snapshot trim each issue to the actionable subset (severity, category, confidence, title, detail, fixHint, stableId, widgetName, routeName, sourceRoute, confidenceReason, rootCauseIds, measurement). The optional measurement object (value, unit, warningAt, criticalAt, windowMs, warningStrict, criticalStrict) is part of the compact set. Pass verbose:true for the full shape of up to 27 keys. Compaction removes fields only and keeps the diff key (stableId and severity), so compare_snapshots and check_budgets work on compact snapshots.", "Structured content applies to every tool rather than to one tool's shape. A client that negotiates MCP protocol 2025-06-18 or later also gets a top-level `structuredContent` field on every successful tools/call result. It holds the same JSON as the text content block, as an object. Clients on older protocol versions get the text content only. Error results (isError:true) never carry structuredContent. The field is additive and changes no per-tool data, args or errors shape below, so schemaVersion stays 2.", "toolAnnotations locks three MCP annotation hints per tool: destructiveHint, idempotentHint and openWorldHint. readOnlyHint comes from readOnlyTools and is not repeated here. Only non-default hints are listed. openWorldHint is true for tools whose output reflects external or runtime state: the live-app readers get_snapshot, get_issues, get_route_health, explain_issue, diagnose and check_budgets, and list_devices, which lists the host's devices. It is false for self-contained tools: compare_snapshots diffs its input locally, and app_status reports the server's own status. destructiveHint is true for detach_app, because it ends the session and deletes disk-handoff files. Read-only tools omit destructiveHint and idempotentHint, which the spec ignores unless readOnlyHint is false. Annotations are advisory descriptor metadata, not a return or args shape, so schemaVersion stays 2.", "serverErrors lists the errors McpServer returns for any tools/call before or around the handler: argument validation against the tool's inputSchema (missing_required_arg, arg_unknown, arg_type_mismatch, arg_enum_violation, arg_min_length_violation) and dispatch failures. They share the per-tool error envelope (isError:true, code as the message prefix) and apply to every tool, so per-tool `errors` lists only handler-level codes. The audit checks every literal error prefix in the handler and server source against these lists.", @@ -64,7 +64,7 @@ "required": false, "nullable": false, "values": ["version_skew_minor", "version_skew_prior_lineage"], - "presence": "when the connected app's packageVersion lineage differs from the sidecar pin within accepted bounds" + "presence": "when the app's packageVersion is not the pinned version but is accepted: version_skew_minor for another version in the pin's lineage, version_skew_prior_lineage for the 0.38 lineage" }, "launchModeAdvisory": {"type": "String", "required": false, "nullable": false, "presence": "when connectionMode is warmup or disconnected, or basic with vmConnected false, which means the VM-only detectors are off; the basic text suggests reopening the app and names DDS as one possible cause. A basic session with vmConnected true gets none."} }, @@ -281,14 +281,15 @@ "fpsDelta": {"type": "double", "required": true, "nullable": true, "presence": "null when either snapshot lacked frameStatsSummary.averageFps/actualFps"}, "beforeFps": {"type": "double", "required": true, "nullable": true}, "afterFps": {"type": "double", "required": true, "nullable": true}, - "coverageWarning": {"type": "String", "required": false, "nullable": false, "presence": "when neither snapshot had a VM service link (both report isVmConnected false); text starts `vm_detectors_not_observed:` and names the VM-only stableIds the diff cannot cover"} + "coverageWarning": {"type": "String", "required": false, "nullable": false, "presence": "when neither snapshot had a VM service link (both report isVmConnected false); text starts `vm_detectors_not_observed:` and names the VM-only stableIds the diff cannot cover"}, + "versionWarning": {"type": "String", "required": false, "nullable": false, "presence": "when the two packageVersion values name different releases of one lineage (build metadata ignored), such as 1.0.0 and 1.2.0; text starts `sleuth_version_differs:` and names both versions, because a release can add detectors, move thresholds or fix false positives"} }, "errors": [ {"code": "arg_before_not_object", "doc": "the `before` arg is not a Map", "messagePrefix": "arg \"before\" must be object"}, {"code": "arg_after_not_object", "doc": "the `after` arg is not a Map", "messagePrefix": "arg \"after\" must be object"}, {"code": "arg_capped_issues_uncomparable", "doc": "one or both snapshots were projected with maxIssueCount. A truncated top-N issue window cannot be diffed, because an issue that left the window looks the same as one that resolved", "messagePrefix": "arg_capped_issues_uncomparable:"}, {"code": "arg_section_mismatch", "doc": "the two snapshots were projected to different sections or different pagination limits", "messagePrefix": "arg_section_mismatch:"}, - {"code": "arg_lineage_mismatch", "doc": "the two snapshots' packageVersion values fall in different sleuth major.minor lineages, or either is missing or not semver; detector ids and defaults change between lineages", "messagePrefix": "arg_lineage_mismatch:"}, + {"code": "arg_lineage_mismatch", "doc": "the two snapshots' packageVersion values fall in different sleuth lineages (the major from 1.0.0, major.minor below it), or either is missing or not semver; detector ids and defaults change between lineages", "messagePrefix": "arg_lineage_mismatch:"}, {"code": "arg_snapshot_in_warmup", "doc": "one or both snapshots were taken while Sleuth was still warming up (connectionMode warmup, or the warmup launchModeAdvisory that get_snapshot adds), so their issue lists may be incomplete; take that snapshot again after a few seconds", "messagePrefix": "arg_snapshot_in_warmup:"}, {"code": "arg_coverage_mismatch", "doc": "only one snapshot had a VM service link (the other reports isVmConnected false), or either lacks a boolean isVmConnected; VM-only detector issues would read as resolved or new. Coverage reads isVmConnected only, so a basic session with a VM link counts as covered.", "messagePrefix": "arg_coverage_mismatch:"}, {"code": "snapshot_schema_drift", "doc": "a snapshot lacks or mistypes currentIssues, currentIssues[].stableId/severity, or frameStatsSummary averageFps/actualFps", "messagePrefix": "snapshot "} @@ -405,7 +406,7 @@ }, { "name": "compact_issues", - "doc": "Unless verbose:true, the sidecar trims every data.currentIssues entry to the compact key set (severity, category, confidence, title, detail, fixHint, stableId, widgetName, routeName, sourceRoute, confidenceReason, rootCauseIds), copying only keys that are present. It runs on both the inline and the disk-handoff path and builds a fresh map, so the bridge envelope is never changed. It does nothing when `sections` left out currentIssues, or when currentIssues is absent or not a list. It keeps stableId and severity, so compare_snapshots and check_budgets still work on compact snapshots." + "doc": "Unless verbose:true, the sidecar trims every data.currentIssues entry to the compact key set (severity, category, confidence, title, detail, fixHint, stableId, widgetName, routeName, sourceRoute, confidenceReason, rootCauseIds, measurement), copying only keys that are present. It runs on both the inline and the disk-handoff path and builds a fresh map, so the bridge envelope is never changed. It does nothing when `sections` left out currentIssues, or when currentIssues is absent or not a list. It keeps stableId and severity, so compare_snapshots and check_budgets still work on compact snapshots." }, { "name": "disk_handoff", @@ -446,7 +447,7 @@ "severityAtLeast": {"type": "String", "required": false, "nullable": false, "presence": "echo stamped only when the severity filter was applied"}, "launchModeAdvisory": {"type": "String", "required": false, "nullable": false, "presence": "stamped in data when connectionMode is warmup or disconnected, or basic with no VM service link; warns the issue list is incomplete because the VM-only detectors are off. See the launch_mode_advisory shim for where the VM flag comes from."} }, - "compactIssueKeys": ["severity", "category", "confidence", "title", "detail", "fixHint", "stableId", "widgetName", "routeName", "sourceRoute", "confidenceReason", "rootCauseIds"], + "compactIssueKeys": ["severity", "category", "confidence", "title", "detail", "fixHint", "stableId", "widgetName", "routeName", "sourceRoute", "confidenceReason", "rootCauseIds", "measurement"], "shims": [ { "name": "severity_filter", @@ -484,7 +485,7 @@ "stableId": {"type": "String", "required": true, "nullable": false, "minLength": 1} }, "data": { - "_doc": "Passthrough of ext.sleuth.explain.data. Reads live app state: sleuth 0.37+ fills the route, widget and count text from the first live issue matching the stableId (else its canonical family), with neutral wording when none is live; sleuth 0.36 apps return the raw placeholders ({widgetName}, {routeName}, {count})." + "_doc": "Passthrough of ext.sleuth.explain.data. Reads live app state: sleuth 0.37+ fills the route, widget and count text from the first live issue matching the stableId (else its canonical family), with neutral wording when none is live. Older apps return the raw placeholders ({widgetName}, {routeName}, {count}); the sidecar refuses them at connect." }, "shims": [], "errors": [ diff --git a/packages/sleuth_mcp/doc/mcp_tool_schema.md b/packages/sleuth_mcp/doc/mcp_tool_schema.md index 173d9b3d..547da335 100644 --- a/packages/sleuth_mcp/doc/mcp_tool_schema.md +++ b/packages/sleuth_mcp/doc/mcp_tool_schema.md @@ -2,7 +2,7 @@ This file describes the return shapes of the 14 MCP tools that `sleuth_mcp` exposes. AI clients, and CI scripts that use `sleuth_check`, can rely on these shapes within `schemaVersion: 2`. -**Compact issues (`schemaVersion: 2`).** By default `get_issues` and `get_snapshot` trim each issue to `severity`, `category`, `confidence`, `title`, `detail`, `fixHint`, `stableId`, `widgetName`, `routeName`, `sourceRoute`, `confidenceReason` and `rootCauseIds`, copying only the keys that are present. Pass `verbose: true` for the full issue, which has up to 27 keys, including the optional `measurement` object (`value`, `unit`, `warningAt`, `criticalAt`, `windowMs`, `warningStrict`, `criticalStrict`). Compaction removes fields only and keeps `stableId` and `severity`, so `compare_snapshots` and `check_budgets` work on compact snapshots. +**Compact issues (`schemaVersion: 2`).** By default `get_issues` and `get_snapshot` trim each issue to `severity`, `category`, `confidence`, `title`, `detail`, `fixHint`, `stableId`, `widgetName`, `routeName`, `sourceRoute`, `confidenceReason`, `rootCauseIds` and `measurement` (`value`, `unit`, `warningAt`, `criticalAt`, `windowMs`, `warningStrict`, `criticalStrict`), copying only the keys that are present. Pass `verbose: true` for the full issue, which has up to 27 keys. Compaction removes fields only and keeps `stableId` and `severity`, so `compare_snapshots` and `check_budgets` work on compact snapshots. [`mcp_tool_schema.json`](mcp_tool_schema.json) is the source of truth, and the audit test parses it. This markdown is a readable copy. @@ -44,7 +44,7 @@ Kind `direct`. Args: `uri` (String, required). It takes the http URI that `flutt | `vmConnected` | bool | yes (nullable) | the app's own VM link, from `ext.sleuth.diagnose` `data.vmConnected`; null when that envelope has no boolean flag. `basic` with `vmConnected: true` is a healthy session that has had no VM-tier frame verdict yet. | | `sidecarVersion` | String | yes | the sidecar's own version (`sleuthMcpVersion`) | | `appPackageVersion` | String | yes (nullable) | the app's reported `kSleuthPackageVersion` | -| `warning` | String | no | `version_skew_minor` (a different version in the pinned lineage) or `version_skew_prior_lineage` (the accepted prior lineage, 0.37) | +| `warning` | String | no | `version_skew_minor` (a different version in the pinned lineage) or `version_skew_prior_lineage` (the accepted prior lineage, 0.38) | | `launchModeAdvisory` | String | no | present when `connectionMode` is `warmup` or `disconnected`, or `basic` with `vmConnected: false`, which means the VM-only detectors are off. The `basic` text suggests reopening the app and names DDS as one possible cause. A `basic` session with `vmConnected: true` gets none. | **Errors:** @@ -210,6 +210,7 @@ Issues aggregate per stableId into the highest severity across its occurrences a | `beforeFps` | double | yes (nullable) | | | `afterFps` | double | yes (nullable) | | | `coverageWarning` | String | no | present when neither snapshot had a VM service link (both report `isVmConnected: false`). It starts with `vm_detectors_not_observed:` and names the VM-only stableIds the diff cannot cover. | +| `versionWarning` | String | no | present when the two `packageVersion` values name different releases of one lineage, such as `1.0.0` and `1.2.0`; build metadata (`+...`) does not count. It starts with `sleuth_version_differs:` and names both versions, because a release can add detectors, move thresholds or fix false positives. | **Errors:** @@ -217,7 +218,7 @@ Issues aggregate per stableId into the highest severity across its occurrences a - `arg "after" must be object (SessionSnapshot data)` - `arg_capped_issues_uncomparable`: one or both inputs were projected with `maxIssueCount`. A truncated top-N window cannot be diffed, because an issue that left the window looks the same as one that was resolved. - `arg_section_mismatch`: the inputs were projected to different sections or with different pagination limits. -- `arg_lineage_mismatch`: the two `packageVersion` values fall in different sleuth `major.minor` lineages, or either is missing or not semver. Detector ids and defaults change between lineages, so the diff would report instrumentation changes as app changes. +- `arg_lineage_mismatch`: the two `packageVersion` values fall in different sleuth lineages (the major from 1.0.0, `major.minor` below it), or either is missing or not semver. Detector ids and defaults change between lineages, so the diff would report instrumentation changes as app changes. - `arg_snapshot_in_warmup`: one or both snapshots were taken while Sleuth was still warming up, so their issue lists may be incomplete. A snapshot counts as a warmup snapshot when it carries `connectionMode: warmup` or the warmup `launchModeAdvisory` that `get_snapshot` adds. Wait a few seconds and take that snapshot again. - `arg_coverage_mismatch`: only one snapshot had a VM service link, or either lacks a boolean `isVmConnected`. Coverage reads `isVmConnected` only, so a `basic` session with a VM link counts as covered. VM-only detectors report nothing without a link, so their issues would read as resolved or new. - `snapshot …`: a snapshot lacks or mistypes `currentIssues`, `currentIssues[].stableId` or `severity`, or the `frameStatsSummary` fps (schema drift). @@ -258,7 +259,7 @@ The tool returns the extension's `data` block with two keys the sidecar adds, pl | `sidecarBuiltAgainstSleuth` | String | yes | `sleuthPackageVersionPin` | | `launchModeAdvisory` | String | no | present when `connectionMode` is `warmup` or `disconnected`, or `basic` with `vmConnected: false`, which means the VM-only detectors are off. The `basic` text suggests reopening the app and names DDS as one possible cause. A `basic` session with `vmConnected: true` gets none. | -Apps on sleuth 0.37 do not report `eventLoopStallTimerStream`, `eventLoopStallExcused`, `eventLoopStallIgnored`, `timelineSource`, `streamBlocks`, `streamEvents`, `streamEventsDropped`, `catchUpReads` and `lastStreamLagMs`. +Every app this sidecar accepts (0.38 and 1.x) reports `eventLoopStallTimerStream`, `eventLoopStallExcused`, `eventLoopStallIgnored`, `timelineSource`, `streamBlocks`, `streamEvents`, `streamEventsDropped`, `catchUpReads` and `lastStreamLagMs`. They are null until a VM session sets them. ## get_snapshot @@ -275,11 +276,11 @@ An app error envelope (one with a top-level `error`) comes back inline and is ne - Two default snapshots carry the same `_projectedSections`, so `compare_snapshots` accepts them (`arg_section_mismatch` only fires when the two section sets or limits differ). The default set holds every section that `compare_snapshots` and `check_budgets` read: `currentIssues`, `frameStatsSummary` and the metadata keys. - An app that ignores the projection args (older than sleuth 0.35) gets the same cut applied by the sidecar, stamped `_projectionApplied: by_sidecar_fallback`. -**Shim `compact_issues`.** Unless `verbose: true`, the sidecar trims every `data.currentIssues` entry to the compact key set (`severity`, `category`, `confidence`, `title`, `detail`, `fixHint`, `stableId`, `widgetName`, `routeName`, `sourceRoute`, `confidenceReason`, `rootCauseIds`), copying only the keys that are present. It runs on both the inline and the disk-handoff path and builds a new map, so the bridge envelope is never changed. It does nothing when `sections` left out `currentIssues`, or when `currentIssues` is missing or not a list. It changes field shape only. `maxIssueCount` caps `currentIssues` in the app whether or not `verbose` is set. +**Shim `compact_issues`.** Unless `verbose: true`, the sidecar trims every `data.currentIssues` entry to the compact key set (`severity`, `category`, `confidence`, `title`, `detail`, `fixHint`, `stableId`, `widgetName`, `routeName`, `sourceRoute`, `confidenceReason`, `rootCauseIds`, `measurement`), copying only the keys that are present. It runs on both the inline and the disk-handoff path and builds a new map, so the bridge envelope is never changed. It does nothing when `sections` left out `currentIssues`, or when `currentIssues` is missing or not a list. It changes field shape only. `maxIssueCount` caps `currentIssues` in the app whether or not `verbose` is set. **Shim `disk_handoff`.** When `diskHandoff` is true, the sidecar writes the envelope to `Directory.systemTemp/sleuth_snapshot_/.json` and returns `{path, sizeBytes, sha256}`, plus any projection metadata, instead of the inline `data` block. Use it for snapshots larger than the client's response token cap. The file name is 128 random bits from `Random.secure()`. The sidecar creates the per-process directory with mode `0700` and the file with mode `0600`, and checks both with `FileStat`. On POSIX, if it cannot set and confirm owner-only permissions, it deletes the file and returns `disk_handoff_failed`. Windows has no POSIX modes, so the check is skipped there. The payload can hold sensitive data, such as query tokens in `recentRequests[].url`, which is why a permission failure stops the handoff. The sidecar deletes the files on `detach_app` and at shutdown, then removes its process directory once it is empty, and each new write removes files older than 30 minutes from its own process directory. Each process has its own directory, so one sidecar never removes another's in-flight file. After its shutdown cleanup the sidecar writes no new file, so a `get_snapshot` still running at exit returns `disk_handoff_failed` instead of leaving a file behind, and a call the client cancelled before the write writes nothing. At startup the sidecar removes the empty `sleuth_snapshot_` directories that earlier processes left behind, only when they are older than 30 minutes. In a non-empty one whose process is gone, which happens when a sidecar crashed or was killed, it deletes the `.json` files older than 30 minutes and then the directory once it is empty. It checks the process through `/proc` on Linux and `ps -p` on other POSIX hosts; on Windows it does not check, so those files stay. Without `diskHandoff` the envelope comes back inline. -**Lineage fallback (an app older than sleuth 0.35).** Such an app ignores the projection args and returns the full payload. When the caller asked for a projection (`sections`, `maxIssueCount` or `maxRouteCount`), the disk-handoff path writes the payload and stamps `_projectionApplied: by_sidecar_fallback`, and the inline path returns `projection_unsupported_by_app` instead, because the full inline payload would overflow the response cap that projection exists to avoid. The default projection is applied by the sidecar instead (see above). Sidecar 0.9.0 refuses apps older than the 0.37 lineage when it connects, so a connected app does not reach this path. +**Lineage fallback (an app older than sleuth 0.35).** Such an app ignores the projection args and returns the full payload. When the caller asked for a projection (`sections`, `maxIssueCount` or `maxRouteCount`), the disk-handoff path writes the payload and stamps `_projectionApplied: by_sidecar_fallback`, and the inline path returns `projection_unsupported_by_app` instead, because the full inline payload would overflow the response cap that projection exists to avoid. The sidecar applies the default projection instead (see above). Sidecar 0.10.0 refuses apps older than the 0.38 lineage when it connects, so a connected app does not reach this path. **Shim `launch_mode_advisory`.** On a degraded session (`connectionMode` `warmup` or `disconnected`, or `basic` with `isVmConnected: false`), the sidecar adds `data.launchModeAdvisory`. A client that reads data here, not only through `connect` or `diagnose`, learns that the VM-only detectors are off. On the disk-handoff path the advisory is in the written file's `data`. @@ -300,9 +301,9 @@ Kind `passthrough` (`ext.sleuth.issues`). Args: `route` (String, optional; an em **Shim `severity_filter`.** When `severityAtLeast` is `warning` or `critical`, the sidecar keeps only the entries in `data.issues` at or above that severity. Whenever `severityAtLeast` is set, including `ok`, the sidecar adds `data.severityAtLeast` with the requested level; `ok` applies no filter. When it is absent there is no filter and no echo. -**Shim `compact_projection`.** After the severity filter, the sidecar keeps the first `maxIssueCount` entries of `data.issues` in the app's ranked order. The default is 50, `0` removes the cap, and a negative value returns `arg_invalid_int`, as in `get_snapshot`. Unless `verbose: true`, it then trims each kept entry to the compact key set (`severity`, `category`, `confidence`, `title`, `detail`, `fixHint`, `stableId`, `widgetName`, `routeName`, `sourceRoute`, `confidenceReason`, `rootCauseIds`). The cap and the trim are independent: `verbose` changes field shape only and never disables the cap. Compaction drops fields but never shortens a value, so it does not bound the response size; use `maxIssueCount` here, or `get_snapshot` with `diskHandoff`. When the cap drops at least one issue, `data` gains `_truncated: true` and `_totalCount`, the count after the filter and before the cap. An envelope without a `data` map or an `issues` list, such as an error envelope, comes back unchanged. +**Shim `compact_projection`.** After the severity filter, the sidecar keeps the first `maxIssueCount` entries of `data.issues` in the app's ranked order. The default is 50, `0` removes the cap, and a negative value returns `arg_invalid_int`, as in `get_snapshot`. Unless `verbose: true`, it then trims each kept entry to the compact key set (`severity`, `category`, `confidence`, `title`, `detail`, `fixHint`, `stableId`, `widgetName`, `routeName`, `sourceRoute`, `confidenceReason`, `rootCauseIds`, `measurement`). The cap and the trim are independent: `verbose` changes field shape only and never disables the cap. Compaction drops fields but never shortens a value, so it does not bound the response size; use `maxIssueCount` here, or `get_snapshot` with `diskHandoff`. When the cap drops at least one issue, `data` gains `_truncated: true` and `_totalCount`, the count after the filter and before the cap. An envelope without a `data` map or an `issues` list, such as an error envelope, comes back unchanged. -**Shim `launch_mode_advisory`.** On a degraded session (`connectionMode` `warmup` or `disconnected`, or `basic` with `vmConnected: false`) the sidecar adds `data.launchModeAdvisory`, warning that the issue list is incomplete because the VM-only detectors are off. Apps on sleuth 0.37 and later send `data.vmConnected` in the `ext.sleuth.issues` payload, and the tool passes it through. For an older app the tool reads `ext.sleuth.diagnose` first on the same session and uses its `data.vmConnected`; sidecar 0.9.0 refuses such apps at connect, so a connected app does not reach this path. The issues call runs second and its errors are never swallowed, so a session change between the two calls fails the tool call instead of pairing two sessions. A `session_changed` or `version_skew_*` error from the diagnose read also propagates. A diagnose read that fails or takes longer than 2 seconds drops the advisory, and `basic` with no readable flag gets none. +**Shim `launch_mode_advisory`.** On a degraded session (`connectionMode` `warmup` or `disconnected`, or `basic` with `vmConnected: false`) the sidecar adds `data.launchModeAdvisory`, warning that the issue list is incomplete because the VM-only detectors are off. Apps on sleuth 0.37 and later send `data.vmConnected` in the `ext.sleuth.issues` payload, and the tool passes it through. For an older app the tool reads `ext.sleuth.diagnose` first on the same session and uses its `data.vmConnected`; sidecar 0.10.0 refuses such apps at connect, so a connected app does not reach this path. The issues call runs second and its errors are never swallowed, so a session change between the two calls fails the tool call instead of pairing two sessions. A `session_changed` or `version_skew_*` error from the diagnose read also propagates. A diagnose read that fails or takes longer than 2 seconds drops the advisory, and `basic` with no readable flag gets none. **Errors:** `arg_invalid_int` for a negative `maxIssueCount`. An `ext.sleuth.issues` error envelope comes back unchanged. @@ -320,7 +321,7 @@ The underlying shape is in the `ext.sleuth.routeHealth` section of `mcp_schema.m Kind `passthrough` (`ext.sleuth.explain`). Args: `stableId` (String, required, `minLength: 1`). -The text depends on live app state. Sleuth 0.37 and later fill the route, widget and count text from the live issue with that exact stableId. A canonical (bare) id with no exact match uses the first live issue of its family. Any other id with no exact match, such as an occurrence id, gets neutral wording, as does a canonical id with no live issue in its family. Older apps return the raw placeholders (`{widgetName}`, `{routeName}`, `{count}`); sidecar 0.9.0 refuses them at connect. +The text depends on live app state. Sleuth 0.37 and later fill the route, widget and count text from the live issue with that exact stableId. A canonical (bare) id with no exact match uses the first live issue of its family. Any other id with no exact match, such as an occurrence id, gets neutral wording, as does a canonical id with no live issue in its family. Older apps return the raw placeholders (`{widgetName}`, `{routeName}`, `{count}`); sidecar 0.10.0 refuses them at connect. **Errors:** diff --git a/packages/sleuth_mcp/doc/release_notes/0.10.0.md b/packages/sleuth_mcp/doc/release_notes/0.10.0.md new file mode 100644 index 00000000..135ee14e --- /dev/null +++ b/packages/sleuth_mcp/doc/release_notes/0.10.0.md @@ -0,0 +1,12 @@ +# sleuth_mcp 0.10.0 release notes + +The full list of changes in sleuth_mcp 0.10.0. [CHANGELOG.md](../../CHANGELOG.md) has the summary. + +Pins sleuth 1.0.0 and accepts the 0.38 lineage as the prior fallback, so the sidecar now refuses 0.37 apps (`version_skew_major`). Publish it together with sleuth 1.0.0, because sidecar 0.9.0 refuses 1.0.0 apps. + +- **Lineage by major from 1.0.0.** sleuth 1.x follows semver, so a breaking change takes a major release. `versionLineage` returns the major for versions from 1.0.0 up (`1.4.2` gives `1`) and `major.minor` below it (`0.38.1` gives `0.38`). Any 1.x app connects, with `warning: version_skew_minor` when it is not the pinned version. The sidecar refuses a 2.x app and anything that is not `major.minor.patch`. +- **Version warning in `compare_snapshots`.** One lineage now spans every 1.x release, so `compare_snapshots` diffs a 1.0.0 snapshot against a 1.2.0 one. The result then carries `versionWarning`, which starts with `sleuth_version_differs:` and names both versions. A release can add detectors, move thresholds or fix false positives, so part of the diff may come from the upgrade rather than the app. Build metadata (`+...`) does not count as a different release. Snapshots from different lineages are still refused with `arg_lineage_mismatch`. +- **`measurement` in compact issues.** `get_issues` and `get_snapshot` keep each issue's `measurement` object in their default compact shape: `value`, `unit`, `warningAt`, `criticalAt`, `windowMs`, `warningStrict` and `criticalStrict`. Issues without a number leave it out. +- **Not useful marks.** The app lists the issues marked Not useful in the overlay under `notUsefulMarks`, in both `ext.sleuth.issues` and `ext.sleuth.snapshot`. `get_issues` passes the key through, and the sidecar's snapshot projection keeps it like `suppressedCount`. +- **Prompts use the 1.0.0 data.** `triage_performance`, `audit_memory` and `release_check` tell the model that an issue's `measurement` holds its value, unit and warning and critical lines, and to quote the value against its line. They explain that a `notUsefulMarks` entry's `key` is the issue's `stableId`, plus `|` and the `widgetName` when the issue names a widget. The triage lists marked issues last under their own heading, the memory audit sets them aside and names them at the end, and the release gate names the mark but keeps the issue in its verdict, since a mark is one developer's call for one session. +- **Docs.** The schema copy documents `notUsefulMarks` and the `measurement` shape; the tool schema lists `measurement` among the compact keys, documents `versionWarning`, and describes `arg_lineage_mismatch` with lineages by major from 1.0.0. diff --git a/packages/sleuth_mcp/lib/src/mcp/mcp_server.dart b/packages/sleuth_mcp/lib/src/mcp/mcp_server.dart index 264fbfe7..556ff79b 100644 --- a/packages/sleuth_mcp/lib/src/mcp/mcp_server.dart +++ b/packages/sleuth_mcp/lib/src/mcp/mcp_server.dart @@ -74,7 +74,7 @@ const Duration defaultExitDetachTimeout = Duration(seconds: 10); /// starts before this wait and runs alongside it. const Duration defaultExitDrainTimeout = Duration(seconds: 10); -const String sleuthMcpVersion = '0.9.0'; +const String sleuthMcpVersion = '0.10.0'; /// Sleuth package version this sidecar is wire-compatible with. Must equal /// `kSleuthPackageVersion` in `lib/src/vm/service_extension_handlers.dart` @@ -84,7 +84,7 @@ const String sleuthMcpVersion = '0.9.0'; /// `pinned version matches sleuth source` audit in /// `test/sleuth_mcp_smoke_test.dart`, which parses the sleuth source file /// and asserts byte-equality with this constant. -const String sleuthPackageVersionPin = '0.38.0'; +const String sleuthPackageVersionPin = '1.0.0'; /// Tool handler signature. Returns either a `data` map (wrapped as text /// content) or a `ToolCallResult` directly when the handler needs full diff --git a/packages/sleuth_mcp/lib/src/prompts/diagnostic_prompts.dart b/packages/sleuth_mcp/lib/src/prompts/diagnostic_prompts.dart index 3e48e635..b8445ac9 100644 --- a/packages/sleuth_mcp/lib/src/prompts/diagnostic_prompts.dart +++ b/packages/sleuth_mcp/lib/src/prompts/diagnostic_prompts.dart @@ -36,7 +36,12 @@ const _triagePerformance = DiagnosticPrompt( 'You are triaging a Flutter app\'s runtime performance using the ' 'Sleuth MCP tools. Work through these steps:\n' '1. Call `get_issues` for the current issues. They arrive ranked, ' - 'most important first.\n' + 'most important first. An issue\'s `measurement`, when present, holds ' + 'the measured `value` and `unit` and the `warningAt` and `criticalAt` ' + 'lines it is judged against. `notUsefulMarks` lists the issues the ' + 'developer marked Not useful in the overlay; a mark\'s `key` is the ' + 'issue\'s `stableId`, plus `|` and the `widgetName` when the issue ' + 'names a widget.\n' '2. Call `get_snapshot` with sections ["frameStatsSummary", ' '"sessionSummary", "recurrenceTrends", "routeSessions"] for frame ' 'stats, the frame-time histogram, the memory trend, issues that are ' @@ -45,7 +50,9 @@ const _triagePerformance = DiagnosticPrompt( '3. For the most severe issue, call `explain_issue` with its stableId ' 'to get the cause and fix guidance.\n' 'Then summarize the top problems, their likely causes, the ' - 'worst-scoring route, and concrete fixes, ordered by severity.', + 'worst-scoring route, and concrete fixes, ordered by severity. Quote ' + 'each problem\'s measured value against its warning or critical line. ' + 'List the issues marked Not useful last, under their own heading.', ); const _auditMemory = DiagnosticPrompt( @@ -61,12 +68,17 @@ const _auditMemory = DiagnosticPrompt( 'Sleuth MCP tools. Work through these steps:\n' '1. Call `get_issues` and keep only the memory issues: heap growth, ' 'retained streams, and tracked resources that live too long or have ' - 'too many live instances.\n' - '2. For each memory issue, call `explain_issue` with its stableId for ' - 'the cause and fix.\n' + 'too many live instances. Set aside the issues listed in ' + '`notUsefulMarks`, which the developer marked Not useful in the ' + 'overlay; a mark\'s `key` is the issue\'s `stableId`, plus `|` and ' + 'the `widgetName` when the issue names a widget.\n' + '2. For each memory issue you kept, call `explain_issue` with its ' + 'stableId for the cause and fix.\n' 'Then recommend concrete fixes, such as disposing controllers and ' - 'streams, bounding caches and untracking resources. Ignore frame and ' - 'jank issues.', + 'streams, bounding caches and untracking resources. Quote each ' + 'issue\'s `measurement` (the growth rate or count against its ' + 'warning line) when it has one. Name the issues set aside as Not ' + 'useful at the end. Ignore frame and jank issues.', ); const _releaseCheck = DiagnosticPrompt( @@ -88,7 +100,11 @@ const _releaseCheck = DiagnosticPrompt( '`coverage_degraded` means the app has no VM service link, so the ' 'memory, CPU and repaint detectors never ran. Report NOT RUN with the ' 'error\'s remedy and stop. Never report PASS in that case.\n' - '3. Call `get_issues` and list any critical-severity issues.\n' + '3. Call `get_issues` and list any critical-severity issues, with ' + 'each one\'s measured value against its critical line when it has a ' + '`measurement`. An issue listed in `notUsefulMarks` (marked Not ' + 'useful in the overlay) still counts: name the mark next to it, but ' + 'keep it in the verdict.\n' 'Then report a clear PASS or FAIL verdict. Name the budget violations ' 'and the critical issues to resolve before release.', ); diff --git a/packages/sleuth_mcp/lib/src/tools/compare_snapshots.dart b/packages/sleuth_mcp/lib/src/tools/compare_snapshots.dart index df3649f1..e4713cd4 100644 --- a/packages/sleuth_mcp/lib/src/tools/compare_snapshots.dart +++ b/packages/sleuth_mcp/lib/src/tools/compare_snapshots.dart @@ -6,7 +6,8 @@ import 'launch_mode_advisory.dart'; /// Diff two `SessionSnapshot` `data` payloads. Pure client-side; no /// bridge call. Returns `{added, removed, elevatedSeverity, countChanged, /// fpsDelta, beforeFps, afterFps}`, plus `coverageWarning` when neither -/// snapshot had a VM service link. +/// snapshot had a VM service link and `versionWarning` when the two come +/// from different sleuth releases of one lineage. /// /// Issues aggregate per stableId: the highest severity across its /// occurrences and the occurrence count. A new critical occurrence beside @@ -123,6 +124,8 @@ Future compareSnapshotsHandler( if (_vmCoverage(before) == false) { result['coverageWarning'] = noVmCoverageWarning; } + final versionWarning = _versionWarning(before, after); + if (versionWarning != null) result['versionWarning'] = versionWarning; return result; } @@ -246,6 +249,27 @@ ToolCallResult? _lineageMismatch( return null; } +/// `versionWarning` text when the two snapshots come from different sleuth +/// releases, else null. The lineage check above passed, so the diff still +/// runs. A minor release can add a detector or move a threshold, and a +/// patch can fix a false positive, so part of the diff may come from the +/// upgrade. Build metadata (`+...`) does not name a different release. +String? _versionWarning( + Map before, + Map after, +) { + String release(Map s) => + (s['packageVersion'] as String).split('+').first; + final b = release(before); + final a = release(after); + if (a == b) return null; + return 'sleuth_version_differs: before comes from sleuth $b and after ' + 'from $a. A release in one lineage can add detectors, move thresholds ' + 'or fix false positives, so an added, removed or changed issue may ' + 'come from the sleuth upgrade rather than the app. Re-capture both ' + 'runs with the same sleuth version to compare the app alone.'; +} + /// Returns an `arg_snapshot_in_warmup` error envelope when either snapshot /// was taken while Sleuth was still warming up, else null. During warmup /// the detectors have not had a full window, so a diff would report diff --git a/packages/sleuth_mcp/lib/src/tools/issue_projection.dart b/packages/sleuth_mcp/lib/src/tools/issue_projection.dart index e123f143..8b77fe89 100644 --- a/packages/sleuth_mcp/lib/src/tools/issue_projection.dart +++ b/packages/sleuth_mcp/lib/src/tools/issue_projection.dart @@ -21,6 +21,7 @@ const Set compactIssueKeys = { 'sourceRoute', 'confidenceReason', 'rootCauseIds', + 'measurement', }; /// Trims one full issue map to [compactIssueKeys]. Only copies keys that are diff --git a/packages/sleuth_mcp/lib/src/tools/tools.dart b/packages/sleuth_mcp/lib/src/tools/tools.dart index caeeb8c5..ca684915 100644 --- a/packages/sleuth_mcp/lib/src/tools/tools.dart +++ b/packages/sleuth_mcp/lib/src/tools/tools.dart @@ -894,8 +894,7 @@ final Map builtInTools = { 'Returns the encyclopedia entry for a stableId. Parametric ' 'stableIds also resolve. On sleuth 0.37 and later, the app fills ' 'the route, widget and count text from the matching live issue, ' - 'or uses neutral wording when none is live. Sleuth 0.36 apps ' - 'return raw placeholders such as {widgetName} and {routeName}.', + 'or uses neutral wording when none is live.', inputSchema: { 'type': 'object', 'properties': { diff --git a/packages/sleuth_mcp/lib/src/util/version_lineage.dart b/packages/sleuth_mcp/lib/src/util/version_lineage.dart index 620fe88a..8485a62c 100644 --- a/packages/sleuth_mcp/lib/src/util/version_lineage.dart +++ b/packages/sleuth_mcp/lib/src/util/version_lineage.dart @@ -8,17 +8,18 @@ final RegExp _semver = RegExp( r'(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$', ); -/// Returns the `major.minor` pair that defines a sleuth pre-1.0 -/// compatibility lineage, or null when [version] is not a semver -/// `major.minor.patch` (with optional `-prerelease` / `+build`). Pre-1.0 -/// semver treats minor bumps as breaking, so the lineage boundary sits at -/// the minor level. A prerelease or build of a version stays in that -/// version's lineage (`0.37.0-dev.1` → `0.37`). Once sleuth reaches 1.0, -/// switch this to return the major part only. +/// Returns the compatibility lineage of a sleuth [version]: the major from +/// 1.0.0 on (`1.4.2` gives `1`), where a breaking change takes a major +/// release, and `major.minor` below 1.0.0 (`0.38.1` gives `0.38`), where a +/// minor release could break. A prerelease or build of a version stays in +/// that version's lineage (`1.0.0-dev.1` gives `1`). Returns null when +/// [version] is not a semver `major.minor.patch` with optional +/// `-prerelease` and `+build`. String? versionLineage(String version) { final match = _semver.firstMatch(version); if (match == null) return null; - return '${match[1]}.${match[2]}'; + final major = match[1]!; + return major == '0' ? '$major.${match[2]}' : major; } /// Lineages this sidecar tolerates alongside its own pin. Each entry @@ -34,5 +35,5 @@ const Set acceptedPriorLineages = { // One-cycle fallback for apps still on the previous sleuth lineage // while the sidecar pin has advanced. Drop the entry one release // after the matching app version stops being supported. - '0.37', + '0.38', }; diff --git a/packages/sleuth_mcp/pubspec.yaml b/packages/sleuth_mcp/pubspec.yaml index 8297e397..2f61d3e7 100644 --- a/packages/sleuth_mcp/pubspec.yaml +++ b/packages/sleuth_mcp/pubspec.yaml @@ -1,6 +1,6 @@ name: sleuth_mcp description: MCP stdio sidecar exposing ext.sleuth.* VM service extensions to AI clients. -version: 0.9.0 +version: 0.10.0 homepage: https://github.com/Harrys76/sleuth repository: https://github.com/Harrys76/sleuth issue_tracker: https://github.com/Harrys76/sleuth/issues diff --git a/packages/sleuth_mcp/test/bridge/vm_bridge_test.dart b/packages/sleuth_mcp/test/bridge/vm_bridge_test.dart index f8f081a0..6ec70c04 100644 --- a/packages/sleuth_mcp/test/bridge/vm_bridge_test.dart +++ b/packages/sleuth_mcp/test/bridge/vm_bridge_test.dart @@ -154,7 +154,7 @@ void main() { // `acceptedPriorLineages` contains the previous lineage — transition window. final refusal = await defaultVersionSkewValidator({ 'sessionUuid': 'uuid', - 'data': {'packageVersion': '0.37.5'}, + 'data': {'packageVersion': '0.38.5'}, }); expect( refusal, diff --git a/packages/sleuth_mcp/test/helpers/fake_vm_bridge.dart b/packages/sleuth_mcp/test/helpers/fake_vm_bridge.dart index 807bab48..2cefded6 100644 --- a/packages/sleuth_mcp/test/helpers/fake_vm_bridge.dart +++ b/packages/sleuth_mcp/test/helpers/fake_vm_bridge.dart @@ -65,7 +65,7 @@ Map fakeSnapshotEnvelope({bool isVmConnected = false}) => { 'data': { 'schemaVersion': 5, 'exportedAt': '2026-05-17T00:00:00.000Z', - 'packageVersion': '0.38.0', + 'packageVersion': '1.0.0', 'isVmConnected': isVmConnected, 'currentIssues': fullFakeIssues(), 'frameStatsSummary': {'averageFps': 59.5, 'jankFrames': 0}, @@ -77,7 +77,7 @@ Map fakeSnapshotEnvelope({bool isVmConnected = false}) => { Map fullFakeSnapshotData({bool isVmConnected = true}) => { 'schemaVersion': 5, 'exportedAt': '2026-05-17T00:00:00.000Z', - 'packageVersion': '0.38.0', + 'packageVersion': '1.0.0', 'isVmConnected': isVmConnected, 'isDebugMode': false, 'frameStatsSummary': {'averageFps': 59.5, 'jankFrames': 0}, @@ -201,7 +201,7 @@ FakeVmBridge defaultFakeBridge() { 'schemaVersion': 1, 'sessionUuid': 'fake-uuid', 'data': { - 'packageVersion': '0.38.0', + 'packageVersion': '1.0.0', 'initializedAtMicros': 0, 'vmConnected': true, 'captureMode': false, diff --git a/packages/sleuth_mcp/test/mcp_server_test.dart b/packages/sleuth_mcp/test/mcp_server_test.dart index f7410c34..d64399ee 100644 --- a/packages/sleuth_mcp/test/mcp_server_test.dart +++ b/packages/sleuth_mcp/test/mcp_server_test.dart @@ -796,6 +796,21 @@ void main() { } }); + test('prompts that read get_issues use the measurement and the Not ' + 'useful marks', () { + for (final prompt in builtInPrompts.values) { + if (!prompt.usesTools.contains('get_issues')) continue; + final name = prompt.descriptor.name; + expect(prompt.text, contains('`measurement`'), reason: name); + expect(prompt.text, contains('`notUsefulMarks`'), reason: name); + } + // The release gate keeps marked issues in its verdict. + expect( + builtInPrompts['release_check']!.text, + contains('keep it in the verdict'), + ); + }); + test('prompt usesTools matches registered tools + text (drift guard)', () { final registered = {...builtInTools.keys, ...lifecycleTools(server).keys}; for (final prompt in builtInPrompts.values) { diff --git a/packages/sleuth_mcp/test/schema/mcp_tool_schema_audit_test.dart b/packages/sleuth_mcp/test/schema/mcp_tool_schema_audit_test.dart index 504217ce..22f55765 100644 --- a/packages/sleuth_mcp/test/schema/mcp_tool_schema_audit_test.dart +++ b/packages/sleuth_mcp/test/schema/mcp_tool_schema_audit_test.dart @@ -505,6 +505,26 @@ void main() { }); } + test('versionWarning is documented and present iff the releases ' + 'differ', () async { + final handler = builtInTools['compare_snapshots']!.handler; + final doc = tools['compare_snapshots'] as Map; + for (final (after, warns) in [('1.0.0', false), ('1.1.0', true)]) { + final result = + await handler(defaultFakeBridge(), { + 'before': {...snap('warning'), 'packageVersion': '1.0.0'}, + 'after': {...snap('warning'), 'packageVersion': after}, + }) + as Map; + expect( + result.keys.toSet().difference(_documentedKeys(doc)), + isEmpty, + reason: 'compare_snapshots emitted undocumented keys', + ); + expect(result.containsKey('versionWarning'), warns); + } + }); + test('error: arg before not object', () async { final bridge = defaultFakeBridge(); final handler = builtInTools['compare_snapshots']!.handler; diff --git a/packages/sleuth_mcp/test/tools/attach_version_skew_test.dart b/packages/sleuth_mcp/test/tools/attach_version_skew_test.dart index c34207f9..ddbf1600 100644 --- a/packages/sleuth_mcp/test/tools/attach_version_skew_test.dart +++ b/packages/sleuth_mcp/test/tools/attach_version_skew_test.dart @@ -93,7 +93,7 @@ void main() { 'connectionMode': 'basic', 'schemaVersion': 1, 'sessionUuid': 'fake-uuid', - 'data': {'packageVersion': '0.38.99'}, + 'data': {'packageVersion': '1.0.99'}, }); final server = McpServer(bridge: bridge)..registerDefaults(); await server.handleForTest( @@ -302,7 +302,7 @@ void main() { 'connectionMode': 'basic', 'schemaVersion': 1, 'sessionUuid': 'fake-uuid', - 'data': {'packageVersion': '0.37.0'}, + 'data': {'packageVersion': '0.38.0'}, }); final server = McpServer(bridge: bridge)..registerDefaults(); await server.handleForTest( diff --git a/packages/sleuth_mcp/test/tools/compare_snapshots_test.dart b/packages/sleuth_mcp/test/tools/compare_snapshots_test.dart index f5f521fa..d3eba5f4 100644 --- a/packages/sleuth_mcp/test/tools/compare_snapshots_test.dart +++ b/packages/sleuth_mcp/test/tools/compare_snapshots_test.dart @@ -66,6 +66,7 @@ void main() { expect(result['countChanged'], isEmpty); expect(result['fpsDelta'], -15.0); expect(result.containsKey('coverageWarning'), isFalse); + expect(result.containsKey('versionWarning'), isFalse); }, ); @@ -310,12 +311,43 @@ void main() { expect(text, contains('0.37.0')); }); - test('accepts the same lineage at different patches', () async { + test('accepts the same lineage at different patches, with a version ' + 'warning', () async { final result = await _compare( _snap([], packageVersion: '0.37.0'), _snap([], packageVersion: '0.37.2'), ); expect(result, isA>()); + expect( + (result as Map)['versionWarning'], + startsWith('sleuth_version_differs:'), + ); + }); + + test('a 1.0 snapshot and a later 1.x one compare with a version ' + 'warning', () async { + final result = + await _compare( + _snap([ + {'stableId': 'a', 'severity': 'warning'}, + ], packageVersion: '1.0.0'), + _snap([], packageVersion: '1.2.0'), + ) + as Map; + expect(result['removed'], ['a']); + final warning = result['versionWarning'] as String; + expect(warning, startsWith('sleuth_version_differs:')); + expect(warning, contains('sleuth 1.0.0 and after from 1.2.0')); + }); + + test('one release with different build metadata does not warn', () async { + final result = + await _compare( + _snap([], packageVersion: '1.0.0'), + _snap([], packageVersion: '1.0.0+ci.7'), + ) + as Map; + expect(result.containsKey('versionWarning'), isFalse); }); test('a prerelease or build stays in its lineage', () async { diff --git a/packages/sleuth_mcp/test/tools/connect_test.dart b/packages/sleuth_mcp/test/tools/connect_test.dart index 04f515ba..66f8a725 100644 --- a/packages/sleuth_mcp/test/tools/connect_test.dart +++ b/packages/sleuth_mcp/test/tools/connect_test.dart @@ -27,7 +27,7 @@ void main() { 'connectionMode': 'basic', 'schemaVersion': 1, 'sessionUuid': 'uuid', - 'data': {'packageVersion': '0.38.99'}, + 'data': {'packageVersion': '1.0.99'}, }); final handler = builtInTools['connect']!.handler; final result = await handler(bridge, {'uri': 'ws://localhost/ws'}); @@ -155,7 +155,7 @@ void main() { 'connectionMode': 'basic', 'schemaVersion': 1, 'sessionUuid': 'uuid', - 'data': {'packageVersion': '0.37.0'}, + 'data': {'packageVersion': '0.38.0'}, }); final handler = builtInTools['connect']!.handler; final result = await handler(bridge, {'uri': 'ws://localhost/ws'}); diff --git a/packages/sleuth_mcp/test/tools/diagnose_test.dart b/packages/sleuth_mcp/test/tools/diagnose_test.dart index be750846..d8b064d4 100644 --- a/packages/sleuth_mcp/test/tools/diagnose_test.dart +++ b/packages/sleuth_mcp/test/tools/diagnose_test.dart @@ -13,6 +13,6 @@ void main() { final data = result['data'] as Map; expect(data['sidecarVersion'], sleuthMcpVersion); expect(data['sidecarBuiltAgainstSleuth'], isNotNull); - expect(data['packageVersion'], '0.38.0'); + expect(data['packageVersion'], '1.0.0'); }); } diff --git a/packages/sleuth_mcp/test/tools/get_issues_test.dart b/packages/sleuth_mcp/test/tools/get_issues_test.dart index 47c0f326..05d6ea0d 100644 --- a/packages/sleuth_mcp/test/tools/get_issues_test.dart +++ b/packages/sleuth_mcp/test/tools/get_issues_test.dart @@ -152,4 +152,38 @@ void main() { expect(result['error'], 'some_extension_error'); expect(result.containsKey('data'), isFalse); }); + + test('compact issues keep the measurement; the Not useful marks pass ' + 'through', () async { + final bridge = defaultFakeBridge(); + await bridge.connect(Uri.parse('ws://localhost/ws')); + const measurement = { + 'value': 42.0, + 'unit': 'count', + 'warningAt': 30.0, + 'criticalAt': 90.0, + }; + const marks = [ + {'key': 'jank_detected', 'markedAtMicros': 1, 'live': true}, + ]; + bridge.setEnvelope('ext.sleuth.issues', { + 'connectionMode': 'basic', + 'schemaVersion': 1, + 'sessionUuid': 'fake-uuid', + 'data': { + 'issues': [ + for (final i in fullFakeIssues()) {...i, 'measurement': measurement}, + ], + 'vmConnected': true, + 'notUsefulMarks': marks, + }, + }); + final result = + await builtInTools['get_issues']!.handler(bridge, {}) + as Map; + final data = result['data'] as Map; + final issues = (data['issues'] as List).cast>(); + expect(issues.first['measurement'], measurement); + expect(data['notUsefulMarks'], marks); + }); } diff --git a/packages/sleuth_mcp/test/tools/get_snapshot_test.dart b/packages/sleuth_mcp/test/tools/get_snapshot_test.dart index 83943860..64947dc2 100644 --- a/packages/sleuth_mcp/test/tools/get_snapshot_test.dart +++ b/packages/sleuth_mcp/test/tools/get_snapshot_test.dart @@ -132,7 +132,7 @@ void main() { expect(data.containsKey(section), isFalse, reason: section); } expect(data.containsKey('currentIssues'), isTrue); - expect(data['packageVersion'], '0.38.0'); + expect(data['packageVersion'], '1.0.0'); expect(data['_projectionApplied'], 'by_sidecar_fallback'); expect( data['_projectedSections'], diff --git a/packages/sleuth_mcp/test/tools/launch_mode_advisory_test.dart b/packages/sleuth_mcp/test/tools/launch_mode_advisory_test.dart index fdba2478..a3aaef05 100644 --- a/packages/sleuth_mcp/test/tools/launch_mode_advisory_test.dart +++ b/packages/sleuth_mcp/test/tools/launch_mode_advisory_test.dart @@ -10,7 +10,7 @@ import '../helpers/fake_vm_bridge.dart'; FakeVmBridge _bridgeWithMode( String connectionMode, { - String packageVersion = '0.38.0', + String packageVersion = '1.0.0', bool vmConnected = false, }) { return FakeVmBridge(fakeSessionUuid: 'uuid') @@ -118,7 +118,7 @@ void main() { () async { final bridge = _bridgeWithMode( 'basic', - packageVersion: '0.38.99', + packageVersion: '1.0.99', vmConnected: false, ); final handler = builtInTools['connect']!.handler; @@ -165,7 +165,7 @@ void main() { 'connectionMode': 'basic', 'schemaVersion': 1, 'sessionUuid': 'fake-uuid', - 'data': {'packageVersion': '0.37.0', 'vmConnected': false}, + 'data': {'packageVersion': '0.38.0', 'vmConnected': false}, }); final server = McpServer(bridge: bridge)..registerDefaults(); await server.handleForTest( diff --git a/packages/sleuth_mcp/test/util/version_lineage_test.dart b/packages/sleuth_mcp/test/util/version_lineage_test.dart index 2c6ac921..05f795e4 100644 --- a/packages/sleuth_mcp/test/util/version_lineage_test.dart +++ b/packages/sleuth_mcp/test/util/version_lineage_test.dart @@ -13,16 +13,21 @@ Map _diagnose(Object? packageVersion) => { void main() { group('versionLineage', () { - test('returns major.minor for a semver version', () { + test('returns major.minor below 1.0.0 and the major from 1.0.0', () { expect(versionLineage('0.37.0'), '0.37'); expect(versionLineage('0.36.12'), '0.36'); - expect(versionLineage('1.0.0'), '1.0'); + expect(versionLineage('1.0.0'), '1'); + expect(versionLineage('1.4.2'), '1'); + expect(versionLineage('2.0.0'), '2'); + expect(versionLineage('10.3.1'), '10'); }); test('classifies a prerelease or build by its major.minor', () { expect(versionLineage('0.37.0-dev.1'), '0.37'); expect(versionLineage('0.36.0+build'), '0.36'); expect(versionLineage('0.37.1-rc.2+sha.5114f85'), '0.37'); + expect(versionLineage('1.0.0-dev.1'), '1'); + expect(versionLineage('1.2.0+build'), '1'); }); test('returns null for anything that is not semver major.minor.patch', () { @@ -56,24 +61,34 @@ void main() { }); group('defaultVersionSkewValidator', () { - test('accepts the pin, its patches and prereleases, and 0.37', () async { - for (final ok in [ - sleuthPackageVersionPin, - '0.38.4', - '0.38.0-dev.1', + test( + 'accepts the pin, its 1.x releases and prereleases, and 0.38', + () async { + for (final ok in [ + sleuthPackageVersionPin, + '1.0.4', + '1.3.0', + '1.0.0-dev.1', + '0.38.0', + '0.38.1+build', + ]) { + expect( + await defaultVersionSkewValidator(_diagnose(ok)), + isNull, + reason: '$ok must connect', + ); + } + }, + ); + + test('refuses 0.37, 0.39 and 2.x as version_skew_major', () async { + for (final refused in [ '0.37.0', - '0.37.0+build', + '0.37.9', + '0.39.0', + '0.39.0-dev.1', + '2.0.0', ]) { - expect( - await defaultVersionSkewValidator(_diagnose(ok)), - isNull, - reason: '$ok must connect', - ); - } - }); - - test('refuses 0.36 and 0.39 lineages as version_skew_major', () async { - for (final refused in ['0.36.0', '0.36.9', '0.39.0', '0.39.0-dev.1']) { expect( await defaultVersionSkewValidator(_diagnose(refused)), startsWith('version_skew_major:'), @@ -104,8 +119,9 @@ void main() { group('connect warning by lineage', () { for (final (version, warning) in <(String, String)>[ - ('0.38.0-dev.1', 'version_skew_minor'), - ('0.37.0+build', 'version_skew_prior_lineage'), + ('1.0.0-dev.1', 'version_skew_minor'), + ('1.2.0', 'version_skew_minor'), + ('0.38.0+build', 'version_skew_prior_lineage'), ]) { test('$version connects with $warning', () async { final bridge = FakeVmBridge(fakeSessionUuid: 'uuid') diff --git a/pubspec.yaml b/pubspec.yaml index 5784d280..980aa00d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: sleuth description: "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." -version: 0.38.0 +version: 1.0.0 homepage: https://github.com/Harrys76/sleuth repository: https://github.com/Harrys76/sleuth issue_tracker: https://github.com/Harrys76/sleuth/issues @@ -17,9 +17,9 @@ platforms: screenshots: - description: 'Sleuth, an in-app performance diagnostics overlay for Flutter.' path: doc/screenshots/cover.png - - description: 'Live issue list in the dark theme, ranked by severity and confidence, with severity filter chips and recurrence badges.' + - description: 'The issue list in the dark theme, with the run label, severity chips, and a meta line per card for category, confidence and how often the issue was seen.' path: doc/screenshots/overlay-dark.png - - description: 'The light theme, with one card expanded to show what was measured, where, and the two issues that caused it.' + - description: 'The light theme, with one card expanded to its measured value against the warning and critical lines, its evidence, and the two issues that caused it.' path: doc/screenshots/overlay-light.png environment: diff --git a/test/ui/guide_page_test.dart b/test/ui/guide_page_test.dart index 9e627086..a11120a0 100644 --- a/test/ui/guide_page_test.dart +++ b/test/ui/guide_page_test.dart @@ -86,6 +86,28 @@ void main() { expect(pageText, contains('83')); }); + testWidgets('the Back tip names the menu and the highlight bar', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: GuidePage(onClose: () {})), + ), + ); + await tester.pumpAndSettle(); + + final tip = find.textContaining( + 'back gesture or button closes the open page, the More menu or the ' + 'highlight bar, then the card', + ); + await tester.scrollUntilVisible( + tip, + 300, + scrollable: find.byType(Scrollable).first, + ); + expect(tip, findsOneWidget); + }); + testWidgets('back button calls onClose', (tester) async { var closed = false; await tester.pumpWidget( diff --git a/test/validation/captures/mcp_snapshots/snapshot_measurement.json b/test/validation/captures/mcp_snapshots/snapshot_measurement.json new file mode 100644 index 00000000..30326db2 --- /dev/null +++ b/test/validation/captures/mcp_snapshots/snapshot_measurement.json @@ -0,0 +1,6792 @@ +{ + "schemaVersion": 5, + "exportedAt": "2026-10-27T01:07:42.424282", + "packageVersion": "1.0.0", + "isVmConnected": true, + "isDebugMode": false, + "frameStatsSummary": { + "totalFrames": 240, + "jankFrames": 4, + "actualFps": 14.0, + "actualFpsRaw": 14.0, + "throughputFps": 60.0, + "averageFps": 60.0, + "worstFrameTimeUs": 265186, + "fpsPercentiles": { + "p50": 60.0, + "p95": 60.0, + "p99": 60.0 + } + }, + "capturedFrames": [ + { + "frameStats": { + "frameNumber": 546, + "uiDurationUs": 18434, + "rasterDurationUs": 1126, + "timestamp": "2026-10-27T01:06:46.483935", + "vsyncOverheadUs": 235, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 19901, + "buildToRasterGapUs": 106, + "vsyncStartUs": 311354582823, + "buildStartUs": 311354583058, + "buildFinishUs": 311354601492, + "rasterStartUs": 311354601598, + "rasterFinishUs": 311354602724 + }, + "verdict": { + "frameNumber": 546, + "totalFrameTimeUs": 18434, + "uiThreadTimeUs": 18434, + "rasterThreadTimeUs": 1126, + "buildScopeTimeUs": 9073, + "flushLayoutTimeUs": 13942, + "flushPaintTimeUs": 47, + "totalSpanUs": 19901, + "buildToRasterGapTimeUs": 106, + "suspectedPhase": "layout", + "reason": "Correlated to frame #546: LAYOUT\n build: 9.1ms\n layout: 13.9ms\n paint: 0.0ms\n raster: 1.1ms\n total span: 19.9ms", + "isFullMode": true, + "isCorrelated": true, + "correlationCoverage": 0.696969696969697 + }, + "relatedIssues": [], + "capturedAt": "2026-10-27T01:06:46.508858" + }, + { + "frameStats": { + "frameNumber": 585, + "uiDurationUs": 257890, + "rasterDurationUs": 528, + "timestamp": "2026-10-27T01:06:49.908829", + "vsyncOverheadUs": 434, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 258909, + "buildToRasterGapUs": 57, + "vsyncStartUs": 311357768997, + "buildStartUs": 311357769431, + "buildFinishUs": 311358027321, + "rasterStartUs": 311358027378, + "rasterFinishUs": 311358027906 + }, + "verdict": { + "frameNumber": 585, + "totalFrameTimeUs": 257890, + "uiThreadTimeUs": 257890, + "rasterThreadTimeUs": 528, + "buildScopeTimeUs": 256132, + "flushLayoutTimeUs": 1648, + "flushPaintTimeUs": 130, + "totalSpanUs": 258909, + "buildToRasterGapTimeUs": 57, + "suspectedPhase": "build", + "reason": "Correlated to frame #585: BUILD\n build: 256.1ms\n layout: 1.6ms\n paint: 0.1ms\n raster: 0.5ms\n total span: 258.9ms\nRelated: Heavy Computation: 255.8ms", + "isFullMode": true, + "isCorrelated": true, + "correlationCoverage": 0.5522388059701493 + }, + "relatedIssues": [ + { + "severity": "critical", + "category": "build", + "confidence": "confirmed", + "title": "Heavy Computation: 255.8ms", + "detail": "A long-running operation ran on the UI thread (255.8ms). It blocks frame rendering.", + "fixHint": "Split the widget so changes rebuild a smaller subtree, mark static subtrees const, and defer below-the-fold work. Move non-UI work (JSON parsing, image processing, complex calculations) off the UI thread with Isolate.run() or compute():\nfinal result = await Isolate.run(() => parseJson(data));", + "stableId": "heavy_compute", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:06:50.023201", + "fixEffort": "involved", + "rootCauseIds": [ + "rebuild_activity" + ], + "confidenceReason": "Measured directly from VM timeline long UI-thread event", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "sourceRoute": "/demo/CSV Import", + "measurement": { + "value": 255.809, + "unit": "milliseconds", + "warningAt": 8.0, + "criticalAt": 16.0, + "warningStrict": true, + "criticalStrict": true + } + }, + { + "severity": "warning", + "category": "build", + "confidence": "confirmed", + "title": "Event Loop Stall: at least 214 ms", + "detail": "The UI isolate ran work for at least 214 ms without returning to its event loop, so frames, input and timers waited. The VM measures from the moment the first waiting timer was due, so the work may have started earlier. Look for synchronous work in callbacks and builds: JSON decoding, file reads, large list sorting or image processing.", + "fixHint": "Find the synchronous work that held the UI isolate (214ms): a long loop, JSON decoding, file or database reads, or image processing in a widget build or a callback (a tap handler, a timer, a stream listener). A slow build also shows as heavy_compute. Move the work off the UI isolate:\nfinal result = await Isolate.run(() => parseJson(data));\nor split it into smaller pieces that yield between them.", + "stableId": "event_loop_stall", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:06:49.807829", + "fixEffort": "medium", + "confidenceReason": "The VM reported a timer that fired at least 100 ms late", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "sourceRoute": "/demo/CSV Import", + "measurement": { + "value": 214.0, + "unit": "milliseconds", + "warningAt": 100.0, + "criticalAt": 700.0 + } + }, + { + "severity": "warning", + "category": "build", + "confidence": "confirmed", + "title": "Rebuild Activity: build phase 25.3% of UI time", + "detail": "Widget rebuilding (BUILD scopes on the UI thread) took 25.3% of wall time in the last window of about 1 s (threshold 10%).\nMost common StatefulWidget on screen: Focus (9 instances). This is screen context, not a proven rebuild source.", + "fixHint": "Rebuilding widgets took 25.3% of UI-thread time. Shrink what rebuilds each frame. Use const constructors for static widgets:\nconst MyWidget({super.key});\nExtract child widgets, or scope rebuilds with Selector or Consumer instead of BlocBuilder or Provider.of.", + "stableId": "rebuild_activity", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:06:50.023242", + "fixEffort": "medium", + "downstreamIds": [ + "heavy_compute" + ], + "confidenceReason": "Measured directly from VM timeline BUILD durations", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "measurement": { + "value": 25.302520315304523, + "unit": "percent", + "warningAt": 10.0, + "criticalAt": 30.0, + "windowMs": 1018.444, + "warningStrict": true, + "criticalStrict": true + } + } + ], + "capturedAt": "2026-10-27T01:06:50.023417" + }, + { + "frameStats": { + "frameNumber": 611, + "uiDurationUs": 235323, + "rasterDurationUs": 359, + "timestamp": "2026-10-27T01:06:54.356858", + "vsyncOverheadUs": 242, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 236024, + "buildToRasterGapUs": 100, + "vsyncStartUs": 311362240690, + "buildStartUs": 311362240932, + "buildFinishUs": 311362476255, + "rasterStartUs": 311362476355, + "rasterFinishUs": 311362476714 + }, + "verdict": { + "frameNumber": 611, + "totalFrameTimeUs": 235323, + "uiThreadTimeUs": 235323, + "rasterThreadTimeUs": 359, + "buildScopeTimeUs": 234995, + "flushLayoutTimeUs": 483, + "flushPaintTimeUs": 99, + "totalSpanUs": 236024, + "buildToRasterGapTimeUs": 100, + "suspectedPhase": "build", + "reason": "Correlated to frame #611: BUILD\n build: 235.0ms\n layout: 0.5ms\n paint: 0.1ms\n raster: 0.4ms\n total span: 236.0ms\nRelated: Heavy Computation: 234.6ms", + "isFullMode": true, + "isCorrelated": true, + "correlationCoverage": 0.6944444444444444 + }, + "relatedIssues": [ + { + "severity": "critical", + "category": "build", + "confidence": "confirmed", + "title": "Heavy Computation: 234.6ms", + "detail": "A long-running operation ran on the UI thread (234.6ms). It blocks frame rendering.", + "fixHint": "Split the widget so changes rebuild a smaller subtree, mark static subtrees const, and defer below-the-fold work. Move non-UI work (JSON parsing, image processing, complex calculations) off the UI thread with Isolate.run() or compute():\nfinal result = await Isolate.run(() => parseJson(data));", + "stableId": "heavy_compute", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:06:54.519586", + "fixEffort": "involved", + "confidenceReason": "Measured directly from VM timeline long UI-thread event", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "sourceRoute": "/demo/CSV Import", + "measurement": { + "value": 234.611, + "unit": "milliseconds", + "warningAt": 8.0, + "criticalAt": 16.0, + "warningStrict": true, + "criticalStrict": true + } + }, + { + "severity": "warning", + "category": "build", + "confidence": "confirmed", + "title": "Event Loop Stalls: 2, longest at least 214 ms", + "detail": "The UI isolate ran work for at least 214 ms without returning to its event loop, so frames, input and timers waited. The VM measures from the moment the first waiting timer was due, so the work may have started earlier. 2 stalls in this episode. Look for synchronous work in callbacks and builds: JSON decoding, file reads, large list sorting or image processing.", + "fixHint": "Find the synchronous work that held the UI isolate (214ms): a long loop, JSON decoding, file or database reads, or image processing in a widget build or a callback (a tap handler, a timer, a stream listener). A slow build also shows as heavy_compute. Move the work off the UI isolate:\nfinal result = await Isolate.run(() => parseJson(data));\nor split it into smaller pieces that yield between them.", + "stableId": "event_loop_stall", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:06:54.256572", + "fixEffort": "medium", + "confidenceReason": "The VM reported a timer that fired at least 100 ms late", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "sourceRoute": "/demo/CSV Import", + "measurement": { + "value": 214.0, + "unit": "milliseconds", + "warningAt": 100.0, + "criticalAt": 700.0 + } + } + ], + "capturedAt": "2026-10-27T01:06:54.519774" + } + ], + "currentIssues": [ + { + "severity": "critical", + "category": "build", + "confidence": "confirmed", + "title": "Heavy Computation: 246.3ms", + "detail": "A long-running operation ran on the UI thread (246.3ms). It blocks frame rendering.", + "fixHint": "Split the widget so changes rebuild a smaller subtree, mark static subtrees const, and defer below-the-fold work. Move non-UI work (JSON parsing, image processing, complex calculations) off the UI thread with Isolate.run() or compute():\nfinal result = await Isolate.run(() => parseJson(data));", + "stableId": "heavy_compute", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:07:42.229433", + "fixEffort": "involved", + "rankingScore": 618, + "rankingBreakdown": { + "severity": 400, + "frameImpact": 8, + "confidence": 200, + "recurrence": 10 + }, + "rootCauseIds": [ + "rebuild_activity" + ], + "confidenceReason": "Measured directly from VM timeline long UI-thread event", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "sourceRoute": "/demo/CSV Import", + "measurement": { + "value": 246.298, + "unit": "milliseconds", + "warningAt": 8.0, + "criticalAt": 16.0, + "warningStrict": true, + "criticalStrict": true + } + }, + { + "severity": "critical", + "category": "build", + "confidence": "confirmed", + "title": "Sustained Jank (UI-bound): 3 severe frames (1% janky)", + "detail": "UI Thread: 264ms\nRaster: 0ms\nTotal span: 265.2ms\nStart delay: 0.4ms\n buildScope: 0.3ms\n flushLayout: 0.0ms\n flushPaint: 0.0ms\nBottleneck: UI thread (build/layout/paint)\nThread attribution: 3 UI-bound, 0 raster-bound, 0 late start. Reduce build, layout and paint work.", + "fixHint": "Look for heavy computation in build(), setState() calls with a wide scope, and offscreen painting. Run in profile mode:\nflutter run --profile\nThen open DevTools > Performance to find the expensive frames.", + "stableId": "sustained_jank", + "routeName": "/demo/CSV Import", + "observationSource": "frameTiming", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:07:42.224894", + "fixEffort": "medium", + "rankingScore": 616, + "rankingBreakdown": { + "severity": 400, + "frameImpact": 8, + "confidence": 200, + "recurrence": 8 + }, + "rootCauseIds": [ + "event_loop_stall" + ], + "confidenceReason": "Measured directly from FrameTiming API", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "sourceRoute": "/demo/CSV Import", + "measurement": { + "value": 3.0, + "unit": "frames", + "criticalAt": 3.0, + "windowMs": 54480.064 + } + }, + { + "severity": "warning", + "category": "build", + "confidence": "confirmed", + "title": "Event Loop Stalls: 2, longest at least 260 ms", + "detail": "The UI isolate ran work for at least 260 ms without returning to its event loop, so frames, input and timers waited. The VM measures from the moment the first waiting timer was due, so the work may have started earlier. 2 stalls in this episode. Look for synchronous work in callbacks and builds: JSON decoding, file reads, large list sorting or image processing.", + "fixHint": "Find the synchronous work that held the UI isolate (260ms): a long loop, JSON decoding, file or database reads, or image processing in a widget build or a callback (a tap handler, a timer, a stream listener). A slow build also shows as heavy_compute. Move the work off the UI isolate:\nfinal result = await Isolate.run(() => parseJson(data));\nor split it into smaller pieces that yield between them.", + "stableId": "event_loop_stall", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:07:42.225199", + "fixEffort": "medium", + "rankingScore": 418, + "rankingBreakdown": { + "severity": 200, + "frameImpact": 8, + "confidence": 200, + "recurrence": 10 + }, + "downstreamIds": [ + "sustained_jank" + ], + "confidenceReason": "The VM reported a timer that fired at least 100 ms late", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "sourceRoute": "/demo/CSV Import", + "measurement": { + "value": 260.0, + "unit": "milliseconds", + "warningAt": 100.0, + "criticalAt": 700.0 + } + }, + { + "severity": "warning", + "category": "build", + "confidence": "confirmed", + "title": "Rebuild Activity: build phase 14.4% of UI time", + "detail": "Widget rebuilding (BUILD scopes on the UI thread) took 14.4% of wall time in the last window of about 1 s (threshold 10%).\nMost common StatefulWidget on screen: AnimatedDefaultTextStyle (28 instances). This is screen context, not a proven rebuild source.", + "fixHint": "Rebuilding widgets took 14.4% of UI-thread time. Shrink what rebuilds each frame. Use const constructors for static widgets:\nconst MyWidget({super.key});\nExtract child widgets, or scope rebuilds with Selector or Consumer instead of BlocBuilder or Provider.of.", + "stableId": "rebuild_activity", + "routeName": "/demo/CSV Import", + "observationSource": "vmTimeline", + "interactionContext": "idle", + "debugModeDisclaimer": false, + "detectedAt": "2026-10-27T01:07:42.229449", + "fixEffort": "medium", + "rankingScore": 418, + "rankingBreakdown": { + "severity": 200, + "frameImpact": 8, + "confidence": 200, + "recurrence": 10 + }, + "downstreamIds": [ + "heavy_compute" + ], + "confidenceReason": "Measured directly from VM timeline BUILD durations", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "measurement": { + "value": 14.410251103431918, + "unit": "percent", + "warningAt": 10.0, + "criticalAt": 30.0, + "windowMs": 1720.088, + "warningStrict": true, + "criticalStrict": true + } + } + ], + "heapSamples": [ + { + "heapUsage": 55512112, + "heapCapacity": 64618496, + "externalUsage": 1320160, + "timestamp": "2026-10-27T01:07:12.638779", + "rssBytes": 218677248, + "nativeBytes": 163165136 + }, + { + "heapUsage": 46667344, + "heapCapacity": 50987008, + "externalUsage": 205696, + "timestamp": "2026-10-27T01:07:13.016838", + "rssBytes": 195624960, + "nativeBytes": 148957616 + }, + { + "heapUsage": 49554000, + "heapCapacity": 53608448, + "externalUsage": 399312, + "timestamp": "2026-10-27T01:07:13.515732", + "rssBytes": 198934528, + "nativeBytes": 149380528 + }, + { + "heapUsage": 50709472, + "heapCapacity": 54657024, + "externalUsage": 453984, + "timestamp": "2026-10-27T01:07:14.006566", + "rssBytes": 200261632, + "nativeBytes": 149552160 + }, + { + "heapUsage": 51616448, + "heapCapacity": 55705600, + "externalUsage": 508528, + "timestamp": "2026-10-27T01:07:14.507151", + "rssBytes": 201342976, + "nativeBytes": 149726528 + }, + { + "heapUsage": 52822336, + "heapCapacity": 56754176, + "externalUsage": 566768, + "timestamp": "2026-10-27T01:07:15.006876", + "rssBytes": 198754304, + "nativeBytes": 145931968 + }, + { + "heapUsage": 59193824, + "heapCapacity": 63209472, + "externalUsage": 907584, + "timestamp": "2026-10-27T01:07:15.527486", + "rssBytes": 205881344, + "nativeBytes": 146687520 + }, + { + "heapUsage": 48314480, + "heapCapacity": 52363264, + "externalUsage": 339760, + "timestamp": "2026-10-27T01:07:16.022494", + "rssBytes": 194772992, + "nativeBytes": 146458512 + }, + { + "heapUsage": 48846080, + "heapCapacity": 52887552, + "externalUsage": 373344, + "timestamp": "2026-10-27T01:07:16.503380", + "rssBytes": 195624960, + "nativeBytes": 146778880 + }, + { + "heapUsage": 43282048, + "heapCapacity": 50987008, + "externalUsage": 57680, + "timestamp": "2026-10-27T01:07:17.003261", + "rssBytes": 195821568, + "nativeBytes": 152539520 + }, + { + "heapUsage": 47301312, + "heapCapacity": 55181312, + "externalUsage": 294160, + "timestamp": "2026-10-27T01:07:17.519418", + "rssBytes": 197902336, + "nativeBytes": 150601024 + }, + { + "heapUsage": 50293600, + "heapCapacity": 58327040, + "externalUsage": 499936, + "timestamp": "2026-10-27T01:07:18.016449", + "rssBytes": 201326592, + "nativeBytes": 151032992 + }, + { + "heapUsage": 50528272, + "heapCapacity": 58327040, + "externalUsage": 513792, + "timestamp": "2026-10-27T01:07:18.502239", + "rssBytes": 201605120, + "nativeBytes": 151076848 + }, + { + "heapUsage": 43333824, + "heapCapacity": 45219840, + "externalUsage": 52080, + "timestamp": "2026-10-27T01:07:19.004560", + "rssBytes": 188940288, + "nativeBytes": 145606464 + }, + { + "heapUsage": 44285696, + "heapCapacity": 46268416, + "externalUsage": 109616, + "timestamp": "2026-10-27T01:07:19.508828", + "rssBytes": 190054400, + "nativeBytes": 145768704 + }, + { + "heapUsage": 44841680, + "heapCapacity": 46792704, + "externalUsage": 145600, + "timestamp": "2026-10-27T01:07:20.004584", + "rssBytes": 190742528, + "nativeBytes": 145900848 + }, + { + "heapUsage": 45082768, + "heapCapacity": 47316992, + "externalUsage": 159904, + "timestamp": "2026-10-27T01:07:20.502238", + "rssBytes": 190988288, + "nativeBytes": 145905520 + }, + { + "heapUsage": 45610672, + "heapCapacity": 47841280, + "externalUsage": 179152, + "timestamp": "2026-10-27T01:07:21.007810", + "rssBytes": 191627264, + "nativeBytes": 146016592 + }, + { + "heapUsage": 46638144, + "heapCapacity": 48889856, + "externalUsage": 233824, + "timestamp": "2026-10-27T01:07:21.508562", + "rssBytes": 192823296, + "nativeBytes": 146185152 + }, + { + "heapUsage": 47444704, + "heapCapacity": 49414144, + "externalUsage": 287472, + "timestamp": "2026-10-27T01:07:22.006247", + "rssBytes": 193773568, + "nativeBytes": 146328864 + }, + { + "heapUsage": 47688944, + "heapCapacity": 49938432, + "externalUsage": 302000, + "timestamp": "2026-10-27T01:07:22.502091", + "rssBytes": 194068480, + "nativeBytes": 146379536 + }, + { + "heapUsage": 47967008, + "heapCapacity": 49938432, + "externalUsage": 316752, + "timestamp": "2026-10-27T01:07:23.002094", + "rssBytes": 194412544, + "nativeBytes": 146445536 + }, + { + "heapUsage": 49189728, + "heapCapacity": 51511296, + "externalUsage": 372096, + "timestamp": "2026-10-27T01:07:23.508384", + "rssBytes": 195854336, + "nativeBytes": 146664608 + }, + { + "heapUsage": 50003840, + "heapCapacity": 52035584, + "externalUsage": 426176, + "timestamp": "2026-10-27T01:07:24.005426", + "rssBytes": 196804608, + "nativeBytes": 146800768 + }, + { + "heapUsage": 50253152, + "heapCapacity": 52559872, + "externalUsage": 441152, + "timestamp": "2026-10-27T01:07:24.502713", + "rssBytes": 197099520, + "nativeBytes": 146846368 + }, + { + "heapUsage": 52909024, + "heapCapacity": 55181312, + "externalUsage": 456128, + "timestamp": "2026-10-27T01:07:25.003023", + "rssBytes": 200441856, + "nativeBytes": 147532832 + }, + { + "heapUsage": 54156336, + "heapCapacity": 56229888, + "externalUsage": 512608, + "timestamp": "2026-10-27T01:07:25.507315", + "rssBytes": 201850880, + "nativeBytes": 147694544 + }, + { + "heapUsage": 54969760, + "heapCapacity": 57278464, + "externalUsage": 566688, + "timestamp": "2026-10-27T01:07:26.005626", + "rssBytes": 202817536, + "nativeBytes": 147847776 + }, + { + "heapUsage": 55214400, + "heapCapacity": 57278464, + "externalUsage": 581216, + "timestamp": "2026-10-27T01:07:26.502040", + "rssBytes": 203112448, + "nativeBytes": 147898048 + }, + { + "heapUsage": 55486288, + "heapCapacity": 57802752, + "externalUsage": 596208, + "timestamp": "2026-10-27T01:07:27.003020", + "rssBytes": 203423744, + "nativeBytes": 147937456 + }, + { + "heapUsage": 43495712, + "heapCapacity": 45744128, + "externalUsage": 69696, + "timestamp": "2026-10-27T01:07:27.508861", + "rssBytes": 191037440, + "nativeBytes": 147541728 + }, + { + "heapUsage": 44370096, + "heapCapacity": 46268416, + "externalUsage": 128240, + "timestamp": "2026-10-27T01:07:28.006299", + "rssBytes": 192069632, + "nativeBytes": 147699536 + }, + { + "heapUsage": 44622336, + "heapCapacity": 46792704, + "externalUsage": 143328, + "timestamp": "2026-10-27T01:07:28.502512", + "rssBytes": 192380928, + "nativeBytes": 147758592 + }, + { + "heapUsage": 44895264, + "heapCapacity": 46792704, + "externalUsage": 158416, + "timestamp": "2026-10-27T01:07:29.003043", + "rssBytes": 192708608, + "nativeBytes": 147813344 + }, + { + "heapUsage": 46113552, + "heapCapacity": 48365568, + "externalUsage": 213312, + "timestamp": "2026-10-27T01:07:29.507507", + "rssBytes": 194101248, + "nativeBytes": 147987696 + }, + { + "heapUsage": 46926736, + "heapCapacity": 48889856, + "externalUsage": 267408, + "timestamp": "2026-10-27T01:07:30.005987", + "rssBytes": 195084288, + "nativeBytes": 148157552 + }, + { + "heapUsage": 47169232, + "heapCapacity": 49414144, + "externalUsage": 281824, + "timestamp": "2026-10-27T01:07:30.500485", + "rssBytes": 195362816, + "nativeBytes": 148193584 + }, + { + "heapUsage": 47464512, + "heapCapacity": 49414144, + "externalUsage": 297584, + "timestamp": "2026-10-27T01:07:31.003404", + "rssBytes": 195723264, + "nativeBytes": 148258752 + }, + { + "heapUsage": 48670784, + "heapCapacity": 50987008, + "externalUsage": 351584, + "timestamp": "2026-10-27T01:07:31.505298", + "rssBytes": 197115904, + "nativeBytes": 148445120 + }, + { + "heapUsage": 49475408, + "heapCapacity": 51511296, + "externalUsage": 405008, + "timestamp": "2026-10-27T01:07:32.005476", + "rssBytes": 198082560, + "nativeBytes": 148607152 + }, + { + "heapUsage": 49726528, + "heapCapacity": 52035584, + "externalUsage": 420096, + "timestamp": "2026-10-27T01:07:32.502756", + "rssBytes": 198361088, + "nativeBytes": 148634560 + }, + { + "heapUsage": 49997280, + "heapCapacity": 52035584, + "externalUsage": 434960, + "timestamp": "2026-10-27T01:07:33.002912", + "rssBytes": 198672384, + "nativeBytes": 148675104 + }, + { + "heapUsage": 51220288, + "heapCapacity": 53084160, + "externalUsage": 490192, + "timestamp": "2026-10-27T01:07:33.506473", + "rssBytes": 200097792, + "nativeBytes": 148877504 + }, + { + "heapUsage": 52038176, + "heapCapacity": 54132736, + "externalUsage": 544608, + "timestamp": "2026-10-27T01:07:34.007158", + "rssBytes": 201064448, + "nativeBytes": 149026272 + }, + { + "heapUsage": 52284112, + "heapCapacity": 54132736, + "externalUsage": 559248, + "timestamp": "2026-10-27T01:07:34.502516", + "rssBytes": 201375744, + "nativeBytes": 149091632 + }, + { + "heapUsage": 52553760, + "heapCapacity": 54657024, + "externalUsage": 574112, + "timestamp": "2026-10-27T01:07:35.003093", + "rssBytes": 201687040, + "nativeBytes": 149133280 + }, + { + "heapUsage": 53770336, + "heapCapacity": 55705600, + "externalUsage": 628880, + "timestamp": "2026-10-27T01:07:35.505986", + "rssBytes": 203096064, + "nativeBytes": 149325728 + }, + { + "heapUsage": 54603488, + "heapCapacity": 56754176, + "externalUsage": 684432, + "timestamp": "2026-10-27T01:07:36.005520", + "rssBytes": 204079104, + "nativeBytes": 149475616 + }, + { + "heapUsage": 54842688, + "heapCapacity": 56754176, + "externalUsage": 698608, + "timestamp": "2026-10-27T01:07:36.502241", + "rssBytes": 204374016, + "nativeBytes": 149531328 + }, + { + "heapUsage": 55109808, + "heapCapacity": 57278464, + "externalUsage": 713248, + "timestamp": "2026-10-27T01:07:37.002681", + "rssBytes": 204701696, + "nativeBytes": 149591888 + }, + { + "heapUsage": 88246448, + "heapCapacity": 90406912, + "externalUsage": 287296, + "timestamp": "2026-10-27T01:07:37.762249", + "rssBytes": 233537536, + "nativeBytes": 145291088 + }, + { + "heapUsage": 49727536, + "heapCapacity": 67600384, + "externalUsage": 442064, + "timestamp": "2026-10-27T01:07:38.023690", + "rssBytes": 211238912, + "nativeBytes": 161511376 + }, + { + "heapUsage": 47257728, + "heapCapacity": 49152000, + "externalUsage": 390640, + "timestamp": "2026-10-27T01:07:38.524464", + "rssBytes": 196788224, + "nativeBytes": 149530496 + }, + { + "heapUsage": 48587936, + "heapCapacity": 50200576, + "externalUsage": 468272, + "timestamp": "2026-10-27T01:07:39.005620", + "rssBytes": 194871296, + "nativeBytes": 146283360 + }, + { + "heapUsage": 50445280, + "heapCapacity": 52297728, + "externalUsage": 563056, + "timestamp": "2026-10-27T01:07:39.510185", + "rssBytes": 196968448, + "nativeBytes": 146523168 + }, + { + "heapUsage": 51997664, + "heapCapacity": 53870592, + "externalUsage": 656464, + "timestamp": "2026-10-27T01:07:40.010443", + "rssBytes": 198754304, + "nativeBytes": 146756640 + }, + { + "heapUsage": 53819680, + "heapCapacity": 55443456, + "externalUsage": 749440, + "timestamp": "2026-10-27T01:07:40.510489", + "rssBytes": 200835072, + "nativeBytes": 147015392 + }, + { + "heapUsage": 54811744, + "heapCapacity": 56492032, + "externalUsage": 816336, + "timestamp": "2026-10-27T01:07:41.007281", + "rssBytes": 202014720, + "nativeBytes": 147202976 + }, + { + "heapUsage": 55795200, + "heapCapacity": 57540608, + "externalUsage": 857456, + "timestamp": "2026-10-27T01:07:41.505937", + "rssBytes": 203128832, + "nativeBytes": 147333632 + }, + { + "heapUsage": 81345504, + "heapCapacity": 86982656, + "externalUsage": 7280, + "timestamp": "2026-10-27T01:07:42.235417", + "rssBytes": 249610240, + "nativeBytes": 168264736 + } + ], + "notUsefulMarks": [ + { + "key": "heap_growing", + "stableId": "heap_growing", + "title": "Heap Growing: +1017 KB/s for 20s", + "category": "memory", + "severity": "warning", + "confidence": "likely", + "measurement": { + "value": 1041578.2669486084, + "unit": "bytesPerSecond", + "warningAt": 512000.0, + "warningStrict": true + }, + "route": "/demo/CSV Import", + "markedAt": "2026-10-27T01:07:15.238484", + "live": false + } + ], + "phaseEvents": [ + { + "phase": "paint", + "timestampUs": 311406126896, + "durationUs": 143 + }, + { + "phase": "build", + "timestampUs": 311406143532, + "durationUs": 71 + }, + { + "phase": "layout", + "timestampUs": 311406143529, + "durationUs": 77 + }, + { + "phase": "paint", + "timestampUs": 311406143611, + "durationUs": 131 + }, + { + "phase": "build", + "timestampUs": 311406160252, + "durationUs": 58 + }, + { + "phase": "layout", + "timestampUs": 311406160246, + "durationUs": 67 + }, + { + "phase": "paint", + "timestampUs": 311406160318, + "durationUs": 176 + }, + { + "phase": "raster", + "timestampUs": 311406143860, + "durationUs": 435 + }, + { + "phase": "raster", + "timestampUs": 311406160571, + "durationUs": 653 + }, + { + "phase": "build", + "timestampUs": 311406177184, + "durationUs": 189 + }, + { + "phase": "layout", + "timestampUs": 311406177166, + "durationUs": 215 + }, + { + "phase": "paint", + "timestampUs": 311406177389, + "durationUs": 525 + }, + { + "phase": "raster", + "timestampUs": 311406178114, + "durationUs": 1024 + }, + { + "phase": "build", + "timestampUs": 311406193826, + "durationUs": 259 + }, + { + "phase": "layout", + "timestampUs": 311406194088, + "durationUs": 8 + }, + { + "phase": "paint", + "timestampUs": 311406194103, + "durationUs": 3 + }, + { + "phase": "raster", + "timestampUs": 311406194280, + "durationUs": 1209 + }, + { + "phase": "build", + "timestampUs": 311406243602, + "durationUs": 145 + }, + { + "phase": "layout", + "timestampUs": 311406243748, + "durationUs": 181 + }, + { + "phase": "paint", + "timestampUs": 311406243932, + "durationUs": 56 + }, + { + "phase": "raster", + "timestampUs": 311406244060, + "durationUs": 714 + }, + { + "phase": "build", + "timestampUs": 311406760843, + "durationUs": 384 + }, + { + "phase": "layout", + "timestampUs": 311406761230, + "durationUs": 4 + }, + { + "phase": "paint", + "timestampUs": 311406761245, + "durationUs": 3 + }, + { + "phase": "build", + "timestampUs": 311406877698, + "durationUs": 266 + }, + { + "phase": "layout", + "timestampUs": 311406877970, + "durationUs": 10 + }, + { + "phase": "paint", + "timestampUs": 311406877995, + "durationUs": 7 + }, + { + "phase": "raster", + "timestampUs": 311406761391, + "durationUs": 1343 + }, + { + "phase": "raster", + "timestampUs": 311406878270, + "durationUs": 2486 + }, + { + "phase": "build", + "timestampUs": 311407227847, + "durationUs": 342 + }, + { + "phase": "layout", + "timestampUs": 311407228192, + "durationUs": 5 + }, + { + "phase": "paint", + "timestampUs": 311407228204, + "durationUs": 3 + }, + { + "phase": "raster", + "timestampUs": 311407228380, + "durationUs": 1491 + }, + { + "phase": "build", + "timestampUs": 311407344660, + "durationUs": 175 + }, + { + "phase": "layout", + "timestampUs": 311407344838, + "durationUs": 5 + }, + { + "phase": "paint", + "timestampUs": 311407344851, + "durationUs": 3 + }, + { + "phase": "raster", + "timestampUs": 311407344980, + "durationUs": 1459 + }, + { + "phase": "raster", + "timestampUs": 311407378686, + "durationUs": 2353 + }, + { + "phase": "layout", + "timestampUs": 311407378229, + "durationUs": 12 + }, + { + "phase": "paint", + "timestampUs": 311407378403, + "durationUs": 7 + }, + { + "phase": "build", + "timestampUs": 311407395190, + "durationUs": 399 + }, + { + "phase": "layout", + "timestampUs": 311407395593, + "durationUs": 6 + }, + { + "phase": "paint", + "timestampUs": 311407395625, + "durationUs": 6 + }, + { + "phase": "raster", + "timestampUs": 311407395899, + "durationUs": 2260 + }, + { + "phase": "build", + "timestampUs": 311407745170, + "durationUs": 425 + }, + { + "phase": "layout", + "timestampUs": 311407745599, + "durationUs": 212 + }, + { + "phase": "paint", + "timestampUs": 311407745824, + "durationUs": 150 + }, + { + "phase": "raster", + "timestampUs": 311407746194, + "durationUs": 1827 + }, + { + "phase": "build", + "timestampUs": 311407861906, + "durationUs": 271 + }, + { + "phase": "layout", + "timestampUs": 311407862183, + "durationUs": 11 + }, + { + "phase": "paint", + "timestampUs": 311407862209, + "durationUs": 7 + }, + { + "phase": "raster", + "timestampUs": 311407862503, + "durationUs": 2488 + }, + { + "phase": "build", + "timestampUs": 311408245526, + "durationUs": 327 + }, + { + "phase": "layout", + "timestampUs": 311408245856, + "durationUs": 5 + }, + { + "phase": "paint", + "timestampUs": 311408245869, + "durationUs": 4 + }, + { + "phase": "raster", + "timestampUs": 311408246033, + "durationUs": 1784 + }, + { + "phase": "build", + "timestampUs": 311408362377, + "durationUs": 172 + }, + { + "phase": "layout", + "timestampUs": 311408362554, + "durationUs": 10 + }, + { + "phase": "paint", + "timestampUs": 311408362578, + "durationUs": 6 + }, + { + "phase": "raster", + "timestampUs": 311408362858, + "durationUs": 2454 + }, + { + "phase": "raster", + "timestampUs": 311408396054, + "durationUs": 2115 + }, + { + "phase": "layout", + "timestampUs": 311408395809, + "durationUs": 8 + }, + { + "phase": "paint", + "timestampUs": 311408395827, + "durationUs": 6 + }, + { + "phase": "build", + "timestampUs": 311408412702, + "durationUs": 428 + }, + { + "phase": "layout", + "timestampUs": 311408413134, + "durationUs": 8 + }, + { + "phase": "paint", + "timestampUs": 311408413160, + "durationUs": 7 + }, + { + "phase": "raster", + "timestampUs": 311408413368, + "durationUs": 2444 + }, + { + "phase": "layout", + "timestampUs": 311409413396, + "durationUs": 15 + }, + { + "phase": "paint", + "timestampUs": 311409413425, + "durationUs": 7 + }, + { + "phase": "raster", + "timestampUs": 311409413716, + "durationUs": 2441 + }, + { + "phase": "build", + "timestampUs": 311409530003, + "durationUs": 289 + }, + { + "phase": "layout", + "timestampUs": 311409530298, + "durationUs": 11 + }, + { + "phase": "paint", + "timestampUs": 311409530322, + "durationUs": 8 + }, + { + "phase": "raster", + "timestampUs": 311409530598, + "durationUs": 2462 + }, + { + "phase": "build", + "timestampUs": 311409730061, + "durationUs": 288 + }, + { + "phase": "layout", + "timestampUs": 311409730351, + "durationUs": 7 + }, + { + "phase": "paint", + "timestampUs": 311409730368, + "durationUs": 6 + }, + { + "phase": "raster", + "timestampUs": 311409730578, + "durationUs": 1781 + }, + { + "phase": "build", + "timestampUs": 311409846971, + "durationUs": 247 + }, + { + "phase": "layout", + "timestampUs": 311409847223, + "durationUs": 18 + }, + { + "phase": "paint", + "timestampUs": 311409847253, + "durationUs": 6 + }, + { + "phase": "layout", + "timestampUs": 311410130931, + "durationUs": 8 + }, + { + "phase": "paint", + "timestampUs": 311410130954, + "durationUs": 7 + }, + { + "phase": "raster", + "timestampUs": 311409847513, + "durationUs": 2501 + }, + { + "phase": "raster", + "timestampUs": 311410131228, + "durationUs": 2405 + }, + { + "phase": "build", + "timestampUs": 311410147679, + "durationUs": 367 + }, + { + "phase": "layout", + "timestampUs": 311410147657, + "durationUs": 400 + }, + { + "phase": "paint", + "timestampUs": 311410148065, + "durationUs": 539 + }, + { + "phase": "layout", + "timestampUs": 311410164561, + "durationUs": 6 + }, + { + "phase": "paint", + "timestampUs": 311410164580, + "durationUs": 609 + }, + { + "phase": "raster", + "timestampUs": 311410148955, + "durationUs": 1521 + }, + { + "phase": "raster", + "timestampUs": 311410165462, + "durationUs": 2204 + }, + { + "phase": "layout", + "timestampUs": 311410181511, + "durationUs": 12 + }, + { + "phase": "paint", + "timestampUs": 311410181536, + "durationUs": 693 + }, + { + "phase": "raster", + "timestampUs": 311410182470, + "durationUs": 2305 + }, + { + "phase": "build", + "timestampUs": 311410197887, + "durationUs": 246298 + }, + { + "phase": "build", + "timestampUs": 311410444192, + "durationUs": 380 + }, + { + "phase": "layout", + "timestampUs": 311410444187, + "durationUs": 477 + }, + { + "phase": "paint", + "timestampUs": 311410444671, + "durationUs": 102 + }, + { + "phase": "raster", + "timestampUs": 311410444893, + "durationUs": 375 + } + ], + "gcEvents": [ + { + "timestampUs": 311410348156, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410348248, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410348467, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410348527, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410348761, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410348828, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410349063, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410349193, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410349436, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410349508, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410349754, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410349862, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350061, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350162, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350362, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350418, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350637, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350690, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350882, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410350931, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351121, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351168, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351359, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351406, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351598, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351677, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351871, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410351945, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410352156, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410352206, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410352400, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410352538, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410352675, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410352710, + "durationUs": 0, + "category": "GC", + "name": "Sweep" + }, + { + "timestampUs": 311410356082, + "durationUs": 0, + "category": "GC", + "name": "CollectNewGeneration" + }, + { + "timestampUs": 311410356083, + "durationUs": 0, + "category": "GC", + "name": "Prologue" + }, + { + "timestampUs": 311410356084, + "durationUs": 0, + "category": "GC", + "name": "Prologue" + }, + { + "timestampUs": 311410356091, + "durationUs": 0, + "category": "GC", + "name": "Scavenge" + }, + { + "timestampUs": 311410356091, + "durationUs": 0, + "category": "GC", + "name": "IterateIsolateRoots" + }, + { + "timestampUs": 311410356228, + "durationUs": 0, + "category": "GC", + "name": "IterateIsolateRoots" + }, + { + "timestampUs": 311410356228, + "durationUs": 0, + "category": "GC", + "name": "IterateStoreBuffers" + }, + { + "timestampUs": 311410356228, + "durationUs": 0, + "category": "GC", + "name": "IterateStoreBuffers" + }, + { + "timestampUs": 311410356229, + "durationUs": 0, + "category": "GC", + "name": "IterateRememberedCards" + }, + { + "timestampUs": 311410357282, + "durationUs": 0, + "category": "GC", + "name": "IterateRememberedCards" + }, + { + "timestampUs": 311410360107, + "durationUs": 0, + "category": "GC", + "name": "MournWeakHandles" + }, + { + "timestampUs": 311410360113, + "durationUs": 0, + "category": "GC", + "name": "MournWeakHandles" + }, + { + "timestampUs": 311410360113, + "durationUs": 0, + "category": "GC", + "name": "Scavenge" + }, + { + "timestampUs": 311410360127, + "durationUs": 0, + "category": "GC", + "name": "Epilogue" + }, + { + "timestampUs": 311410360245, + "durationUs": 0, + "category": "GC", + "name": "Epilogue" + }, + { + "timestampUs": 311410360276, + "durationUs": 0, + "category": "GC", + "name": "CollectNewGeneration" + } + ], + "recentFrames": [ + { + "frameNumber": 581, + "uiDurationUs": 346, + "rasterDurationUs": 2133, + "timestamp": "2026-10-27T01:06:49.807695", + "vsyncOverheadUs": 216, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2881, + "buildToRasterGapUs": 186, + "vsyncStartUs": 311357702261, + "buildStartUs": 311357702477, + "buildFinishUs": 311357702823, + "rasterStartUs": 311357703009, + "rasterFinishUs": 311357705142 + }, + { + "frameNumber": 582, + "uiDurationUs": 1534, + "rasterDurationUs": 1512, + "timestamp": "2026-10-27T01:06:49.807700", + "vsyncOverheadUs": 1692, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4854, + "buildToRasterGapUs": 116, + "vsyncStartUs": 311357718945, + "buildStartUs": 311357720637, + "buildFinishUs": 311357722171, + "rasterStartUs": 311357722287, + "rasterFinishUs": 311357723799 + }, + { + "frameNumber": 583, + "uiDurationUs": 745, + "rasterDurationUs": 1334, + "timestamp": "2026-10-27T01:06:49.807702", + "vsyncOverheadUs": 317, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2535, + "buildToRasterGapUs": 139, + "vsyncStartUs": 311357735629, + "buildStartUs": 311357735946, + "buildFinishUs": 311357736691, + "rasterStartUs": 311357736830, + "rasterFinishUs": 311357738164 + }, + { + "frameNumber": 584, + "uiDurationUs": 1067, + "rasterDurationUs": 2116, + "timestamp": "2026-10-27T01:06:49.807703", + "vsyncOverheadUs": 489, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3867, + "buildToRasterGapUs": 195, + "vsyncStartUs": 311357752313, + "buildStartUs": 311357752802, + "buildFinishUs": 311357753869, + "rasterStartUs": 311357754064, + "rasterFinishUs": 311357756180 + }, + { + "frameNumber": 585, + "uiDurationUs": 257890, + "rasterDurationUs": 528, + "timestamp": "2026-10-27T01:06:49.908829", + "vsyncOverheadUs": 434, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 258909, + "buildToRasterGapUs": 57, + "vsyncStartUs": 311357768997, + "buildStartUs": 311357769431, + "buildFinishUs": 311358027321, + "rasterStartUs": 311358027378, + "rasterFinishUs": 311358027906 + }, + { + "frameNumber": 586, + "uiDurationUs": 1513, + "rasterDurationUs": 641, + "timestamp": "2026-10-27T01:06:49.908840", + "vsyncOverheadUs": 7790, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 9991, + "buildToRasterGapUs": 47, + "vsyncStartUs": 311358028322, + "buildStartUs": 311358036112, + "buildFinishUs": 311358037625, + "rasterStartUs": 311358037672, + "rasterFinishUs": 311358038313 + }, + { + "frameNumber": 587, + "uiDurationUs": 188, + "rasterDurationUs": 232, + "timestamp": "2026-10-27T01:06:49.908842", + "vsyncOverheadUs": 269, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 786, + "buildToRasterGapUs": 97, + "vsyncStartUs": 311358052624, + "buildStartUs": 311358052893, + "buildFinishUs": 311358053081, + "rasterStartUs": 311358053178, + "rasterFinishUs": 311358053410 + }, + { + "frameNumber": 588, + "uiDurationUs": 147, + "rasterDurationUs": 260, + "timestamp": "2026-10-27T01:06:49.908843", + "vsyncOverheadUs": 176, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 616, + "buildToRasterGapUs": 33, + "vsyncStartUs": 311358069308, + "buildStartUs": 311358069484, + "buildFinishUs": 311358069631, + "rasterStartUs": 311358069664, + "rasterFinishUs": 311358069924 + }, + { + "frameNumber": 589, + "uiDurationUs": 153, + "rasterDurationUs": 265, + "timestamp": "2026-10-27T01:06:49.908845", + "vsyncOverheadUs": 145, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 588, + "buildToRasterGapUs": 25, + "vsyncStartUs": 311358085992, + "buildStartUs": 311358086137, + "buildFinishUs": 311358086290, + "rasterStartUs": 311358086315, + "rasterFinishUs": 311358086580 + }, + { + "frameNumber": 590, + "uiDurationUs": 225, + "rasterDurationUs": 309, + "timestamp": "2026-10-27T01:06:49.908846", + "vsyncOverheadUs": 178, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 810, + "buildToRasterGapUs": 98, + "vsyncStartUs": 311358102676, + "buildStartUs": 311358102854, + "buildFinishUs": 311358103079, + "rasterStartUs": 311358103177, + "rasterFinishUs": 311358103486 + }, + { + "frameNumber": 591, + "uiDurationUs": 202, + "rasterDurationUs": 299, + "timestamp": "2026-10-27T01:06:49.908848", + "vsyncOverheadUs": 142, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 738, + "buildToRasterGapUs": 95, + "vsyncStartUs": 311358119360, + "buildStartUs": 311358119502, + "buildFinishUs": 311358119704, + "rasterStartUs": 311358119799, + "rasterFinishUs": 311358120098 + }, + { + "frameNumber": 592, + "uiDurationUs": 228, + "rasterDurationUs": 374, + "timestamp": "2026-10-27T01:06:50.024097", + "vsyncOverheadUs": 167, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 875, + "buildToRasterGapUs": 106, + "vsyncStartUs": 311358136044, + "buildStartUs": 311358136211, + "buildFinishUs": 311358136439, + "rasterStartUs": 311358136545, + "rasterFinishUs": 311358136919 + }, + { + "frameNumber": 593, + "uiDurationUs": 295, + "rasterDurationUs": 438, + "timestamp": "2026-10-27T01:06:50.024102", + "vsyncOverheadUs": 250, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1092, + "buildToRasterGapUs": 109, + "vsyncStartUs": 311358152728, + "buildStartUs": 311358152978, + "buildFinishUs": 311358153273, + "rasterStartUs": 311358153382, + "rasterFinishUs": 311358153820 + }, + { + "frameNumber": 594, + "uiDurationUs": 295, + "rasterDurationUs": 451, + "timestamp": "2026-10-27T01:06:50.024104", + "vsyncOverheadUs": 162, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1018, + "buildToRasterGapUs": 110, + "vsyncStartUs": 311358169412, + "buildStartUs": 311358169574, + "buildFinishUs": 311358169869, + "rasterStartUs": 311358169979, + "rasterFinishUs": 311358170430 + }, + { + "frameNumber": 595, + "uiDurationUs": 366, + "rasterDurationUs": 603, + "timestamp": "2026-10-27T01:06:50.024105", + "vsyncOverheadUs": 248, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1337, + "buildToRasterGapUs": 120, + "vsyncStartUs": 311358186096, + "buildStartUs": 311358186344, + "buildFinishUs": 311358186710, + "rasterStartUs": 311358186830, + "rasterFinishUs": 311358187433 + }, + { + "frameNumber": 596, + "uiDurationUs": 463, + "rasterDurationUs": 684, + "timestamp": "2026-10-27T01:06:50.024107", + "vsyncOverheadUs": 273, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1550, + "buildToRasterGapUs": 130, + "vsyncStartUs": 311358202780, + "buildStartUs": 311358203053, + "buildFinishUs": 311358203516, + "rasterStartUs": 311358203646, + "rasterFinishUs": 311358204330 + }, + { + "frameNumber": 597, + "uiDurationUs": 370, + "rasterDurationUs": 636, + "timestamp": "2026-10-27T01:06:50.024109", + "vsyncOverheadUs": 3046, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4120, + "buildToRasterGapUs": 68, + "vsyncStartUs": 311358219464, + "buildStartUs": 311358222510, + "buildFinishUs": 311358222880, + "rasterStartUs": 311358222948, + "rasterFinishUs": 311358223584 + }, + { + "frameNumber": 598, + "uiDurationUs": 811, + "rasterDurationUs": 678, + "timestamp": "2026-10-27T01:06:50.127048", + "vsyncOverheadUs": 8394, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 9931, + "buildToRasterGapUs": 48, + "vsyncStartUs": 311358236147, + "buildStartUs": 311358244541, + "buildFinishUs": 311358245352, + "rasterStartUs": 311358245400, + "rasterFinishUs": 311358246078 + }, + { + "frameNumber": 599, + "uiDurationUs": 337, + "rasterDurationUs": 2470, + "timestamp": "2026-10-27T01:06:51.639669", + "vsyncOverheadUs": 272, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3265, + "buildToRasterGapUs": 186, + "vsyncStartUs": 311359755369, + "buildStartUs": 311359755641, + "buildFinishUs": 311359755978, + "rasterStartUs": 311359756164, + "rasterFinishUs": 311359758634 + }, + { + "frameNumber": 600, + "uiDurationUs": 539, + "rasterDurationUs": 2005, + "timestamp": "2026-10-27T01:06:51.755833", + "vsyncOverheadUs": 309, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3035, + "buildToRasterGapUs": 182, + "vsyncStartUs": 311359871618, + "buildStartUs": 311359871927, + "buildFinishUs": 311359872466, + "rasterStartUs": 311359872648, + "rasterFinishUs": 311359874653 + }, + { + "frameNumber": 601, + "uiDurationUs": 997, + "rasterDurationUs": 2151, + "timestamp": "2026-10-27T01:06:52.123636", + "vsyncOverheadUs": 209, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3504, + "buildToRasterGapUs": 147, + "vsyncStartUs": 311360238867, + "buildStartUs": 311360239076, + "buildFinishUs": 311360240073, + "rasterStartUs": 311360240220, + "rasterFinishUs": 311360242371 + }, + { + "frameNumber": 602, + "uiDurationUs": 477, + "rasterDurationUs": 2493, + "timestamp": "2026-10-27T01:06:52.240520", + "vsyncOverheadUs": 233, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3372, + "buildToRasterGapUs": 169, + "vsyncStartUs": 311360355711, + "buildStartUs": 311360355944, + "buildFinishUs": 311360356421, + "rasterStartUs": 311360356590, + "rasterFinishUs": 311360359083 + }, + { + "frameNumber": 603, + "uiDurationUs": 295, + "rasterDurationUs": 2382, + "timestamp": "2026-10-27T01:06:52.657294", + "vsyncOverheadUs": 251, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3116, + "buildToRasterGapUs": 188, + "vsyncStartUs": 311360772828, + "buildStartUs": 311360773079, + "buildFinishUs": 311360773374, + "rasterStartUs": 311360773562, + "rasterFinishUs": 311360775944 + }, + { + "frameNumber": 604, + "uiDurationUs": 528, + "rasterDurationUs": 2418, + "timestamp": "2026-10-27T01:06:52.774203", + "vsyncOverheadUs": 219, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3329, + "buildToRasterGapUs": 164, + "vsyncStartUs": 311360889632, + "buildStartUs": 311360889851, + "buildFinishUs": 311360890379, + "rasterStartUs": 311360890543, + "rasterFinishUs": 311360892961 + }, + { + "frameNumber": 605, + "uiDurationUs": 363, + "rasterDurationUs": 2379, + "timestamp": "2026-10-27T01:06:53.674578", + "vsyncOverheadUs": 278, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3214, + "buildToRasterGapUs": 194, + "vsyncStartUs": 311361790439, + "buildStartUs": 311361790717, + "buildFinishUs": 311361791080, + "rasterStartUs": 311361791274, + "rasterFinishUs": 311361793653 + }, + { + "frameNumber": 606, + "uiDurationUs": 163, + "rasterDurationUs": 839, + "timestamp": "2026-10-27T01:06:53.789434", + "vsyncOverheadUs": 183, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1276, + "buildToRasterGapUs": 91, + "vsyncStartUs": 311361907029, + "buildStartUs": 311361907212, + "buildFinishUs": 311361907375, + "rasterStartUs": 311361907466, + "rasterFinishUs": 311361908305 + }, + { + "frameNumber": 607, + "uiDurationUs": 302, + "rasterDurationUs": 2546, + "timestamp": "2026-10-27T01:06:54.256491", + "vsyncOverheadUs": 202, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3218, + "buildToRasterGapUs": 168, + "vsyncStartUs": 311362173961, + "buildStartUs": 311362174163, + "buildFinishUs": 311362174465, + "rasterStartUs": 311362174633, + "rasterFinishUs": 311362177179 + }, + { + "frameNumber": 608, + "uiDurationUs": 1651, + "rasterDurationUs": 2238, + "timestamp": "2026-10-27T01:06:54.256497", + "vsyncOverheadUs": 439, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4517, + "buildToRasterGapUs": 189, + "vsyncStartUs": 311362190643, + "buildStartUs": 311362191082, + "buildFinishUs": 311362192733, + "rasterStartUs": 311362192922, + "rasterFinishUs": 311362195160 + }, + { + "frameNumber": 609, + "uiDurationUs": 954, + "rasterDurationUs": 2164, + "timestamp": "2026-10-27T01:06:54.256498", + "vsyncOverheadUs": 418, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3791, + "buildToRasterGapUs": 255, + "vsyncStartUs": 311362207326, + "buildStartUs": 311362207744, + "buildFinishUs": 311362208698, + "rasterStartUs": 311362208953, + "rasterFinishUs": 311362211117 + }, + { + "frameNumber": 610, + "uiDurationUs": 954, + "rasterDurationUs": 1470, + "timestamp": "2026-10-27T01:06:54.256499", + "vsyncOverheadUs": 7679, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 10240, + "buildToRasterGapUs": 137, + "vsyncStartUs": 311362224008, + "buildStartUs": 311362231687, + "buildFinishUs": 311362232641, + "rasterStartUs": 311362232778, + "rasterFinishUs": 311362234248 + }, + { + "frameNumber": 611, + "uiDurationUs": 235323, + "rasterDurationUs": 359, + "timestamp": "2026-10-27T01:06:54.356858", + "vsyncOverheadUs": 242, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 236024, + "buildToRasterGapUs": 100, + "vsyncStartUs": 311362240690, + "buildStartUs": 311362240932, + "buildFinishUs": 311362476255, + "rasterStartUs": 311362476355, + "rasterFinishUs": 311362476714 + }, + { + "frameNumber": 612, + "uiDurationUs": 1136, + "rasterDurationUs": 290, + "timestamp": "2026-10-27T01:06:54.356866", + "vsyncOverheadUs": 192, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1659, + "buildToRasterGapUs": 41, + "vsyncStartUs": 311362490927, + "buildStartUs": 311362491119, + "buildFinishUs": 311362492255, + "rasterStartUs": 311362492296, + "rasterFinishUs": 311362492586 + }, + { + "frameNumber": 613, + "uiDurationUs": 498, + "rasterDurationUs": 417, + "timestamp": "2026-10-27T01:06:54.356867", + "vsyncOverheadUs": 281, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1257, + "buildToRasterGapUs": 61, + "vsyncStartUs": 311362507609, + "buildStartUs": 311362507890, + "buildFinishUs": 311362508388, + "rasterStartUs": 311362508449, + "rasterFinishUs": 311362508866 + }, + { + "frameNumber": 614, + "uiDurationUs": 325, + "rasterDurationUs": 330, + "timestamp": "2026-10-27T01:06:54.356868", + "vsyncOverheadUs": 197, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 955, + "buildToRasterGapUs": 103, + "vsyncStartUs": 311362524292, + "buildStartUs": 311362524489, + "buildFinishUs": 311362524814, + "rasterStartUs": 311362524917, + "rasterFinishUs": 311362525247 + }, + { + "frameNumber": 615, + "uiDurationUs": 207, + "rasterDurationUs": 252, + "timestamp": "2026-10-27T01:06:54.356869", + "vsyncOverheadUs": 152, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 709, + "buildToRasterGapUs": 98, + "vsyncStartUs": 311362540974, + "buildStartUs": 311362541126, + "buildFinishUs": 311362541333, + "rasterStartUs": 311362541431, + "rasterFinishUs": 311362541683 + }, + { + "frameNumber": 616, + "uiDurationUs": 175, + "rasterDurationUs": 284, + "timestamp": "2026-10-27T01:06:54.356870", + "vsyncOverheadUs": 134, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 627, + "buildToRasterGapUs": 34, + "vsyncStartUs": 311362557656, + "buildStartUs": 311362557790, + "buildFinishUs": 311362557965, + "rasterStartUs": 311362557999, + "rasterFinishUs": 311362558283 + }, + { + "frameNumber": 617, + "uiDurationUs": 230, + "rasterDurationUs": 300, + "timestamp": "2026-10-27T01:06:54.356871", + "vsyncOverheadUs": 157, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 789, + "buildToRasterGapUs": 102, + "vsyncStartUs": 311362574339, + "buildStartUs": 311362574496, + "buildFinishUs": 311362574726, + "rasterStartUs": 311362574828, + "rasterFinishUs": 311362575128 + }, + { + "frameNumber": 618, + "uiDurationUs": 230, + "rasterDurationUs": 322, + "timestamp": "2026-10-27T01:06:54.472738", + "vsyncOverheadUs": 157, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 811, + "buildToRasterGapUs": 102, + "vsyncStartUs": 311362591021, + "buildStartUs": 311362591178, + "buildFinishUs": 311362591408, + "rasterStartUs": 311362591510, + "rasterFinishUs": 311362591832 + }, + { + "frameNumber": 619, + "uiDurationUs": 258, + "rasterDurationUs": 360, + "timestamp": "2026-10-27T01:06:54.472758", + "vsyncOverheadUs": 137, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 793, + "buildToRasterGapUs": 38, + "vsyncStartUs": 311362607704, + "buildStartUs": 311362607841, + "buildFinishUs": 311362608099, + "rasterStartUs": 311362608137, + "rasterFinishUs": 311362608497 + }, + { + "frameNumber": 620, + "uiDurationUs": 237, + "rasterDurationUs": 368, + "timestamp": "2026-10-27T01:06:54.472761", + "vsyncOverheadUs": 168, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 881, + "buildToRasterGapUs": 108, + "vsyncStartUs": 311362624386, + "buildStartUs": 311362624554, + "buildFinishUs": 311362624791, + "rasterStartUs": 311362624899, + "rasterFinishUs": 311362625267 + }, + { + "frameNumber": 621, + "uiDurationUs": 326, + "rasterDurationUs": 459, + "timestamp": "2026-10-27T01:06:54.472764", + "vsyncOverheadUs": 198, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1103, + "buildToRasterGapUs": 120, + "vsyncStartUs": 311362641068, + "buildStartUs": 311362641266, + "buildFinishUs": 311362641592, + "rasterStartUs": 311362641712, + "rasterFinishUs": 311362642171 + }, + { + "frameNumber": 622, + "uiDurationUs": 271, + "rasterDurationUs": 446, + "timestamp": "2026-10-27T01:06:54.472767", + "vsyncOverheadUs": 161, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 922, + "buildToRasterGapUs": 44, + "vsyncStartUs": 311362657751, + "buildStartUs": 311362657912, + "buildFinishUs": 311362658183, + "rasterStartUs": 311362658227, + "rasterFinishUs": 311362658673 + }, + { + "frameNumber": 623, + "uiDurationUs": 373, + "rasterDurationUs": 615, + "timestamp": "2026-10-27T01:06:54.472770", + "vsyncOverheadUs": 205, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1325, + "buildToRasterGapUs": 132, + "vsyncStartUs": 311362674433, + "buildStartUs": 311362674638, + "buildFinishUs": 311362675011, + "rasterStartUs": 311362675143, + "rasterFinishUs": 311362675758 + }, + { + "frameNumber": 624, + "uiDurationUs": 1075, + "rasterDurationUs": 1289, + "timestamp": "2026-10-27T01:06:54.574889", + "vsyncOverheadUs": 343, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2871, + "buildToRasterGapUs": 164, + "vsyncStartUs": 311362691116, + "buildStartUs": 311362691459, + "buildFinishUs": 311362692534, + "rasterStartUs": 311362692698, + "rasterFinishUs": 311362693987 + }, + { + "frameNumber": 625, + "uiDurationUs": 379, + "rasterDurationUs": 1328, + "timestamp": "2026-10-27T01:06:54.574900", + "vsyncOverheadUs": 364, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2197, + "buildToRasterGapUs": 126, + "vsyncStartUs": 311362707798, + "buildStartUs": 311362708162, + "buildFinishUs": 311362708541, + "rasterStartUs": 311362708667, + "rasterFinishUs": 311362709995 + }, + { + "frameNumber": 626, + "uiDurationUs": 143, + "rasterDurationUs": 543, + "timestamp": "2026-10-27T01:06:54.574902", + "vsyncOverheadUs": 203, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 970, + "buildToRasterGapUs": 81, + "vsyncStartUs": 311362741163, + "buildStartUs": 311362741366, + "buildFinishUs": 311362741509, + "rasterStartUs": 311362741590, + "rasterFinishUs": 311362742133 + }, + { + "frameNumber": 627, + "uiDurationUs": 84, + "rasterDurationUs": 549, + "timestamp": "2026-10-27T01:06:54.689957", + "vsyncOverheadUs": 230, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 955, + "buildToRasterGapUs": 92, + "vsyncStartUs": 311362807893, + "buildStartUs": 311362808123, + "buildFinishUs": 311362808207, + "rasterStartUs": 311362808299, + "rasterFinishUs": 311362808848 + }, + { + "frameNumber": 628, + "uiDurationUs": 403, + "rasterDurationUs": 1774, + "timestamp": "2026-10-27T01:06:54.808547", + "vsyncOverheadUs": 256, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2556, + "buildToRasterGapUs": 123, + "vsyncStartUs": 311362924670, + "buildStartUs": 311362924926, + "buildFinishUs": 311362925329, + "rasterStartUs": 311362925452, + "rasterFinishUs": 311362927226 + }, + { + "frameNumber": 629, + "uiDurationUs": 754, + "rasterDurationUs": 1140, + "timestamp": "2026-10-27T01:06:55.142019", + "vsyncOverheadUs": 191, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2157, + "buildToRasterGapUs": 72, + "vsyncStartUs": 311363258516, + "buildStartUs": 311363258707, + "buildFinishUs": 311363259461, + "rasterStartUs": 311363259533, + "rasterFinishUs": 311363260673 + }, + { + "frameNumber": 630, + "uiDurationUs": 516, + "rasterDurationUs": 2548, + "timestamp": "2026-10-27T01:06:55.259915", + "vsyncOverheadUs": 239, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3470, + "buildToRasterGapUs": 167, + "vsyncStartUs": 311363375162, + "buildStartUs": 311363375401, + "buildFinishUs": 311363375917, + "rasterStartUs": 311363376084, + "rasterFinishUs": 311363378632 + }, + { + "frameNumber": 631, + "uiDurationUs": 367, + "rasterDurationUs": 2504, + "timestamp": "2026-10-27T01:06:55.710343", + "vsyncOverheadUs": 288, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3356, + "buildToRasterGapUs": 197, + "vsyncStartUs": 311363825689, + "buildStartUs": 311363825977, + "buildFinishUs": 311363826344, + "rasterStartUs": 311363826541, + "rasterFinishUs": 311363829045 + }, + { + "frameNumber": 632, + "uiDurationUs": 532, + "rasterDurationUs": 2469, + "timestamp": "2026-10-27T01:06:55.827164", + "vsyncOverheadUs": 198, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3368, + "buildToRasterGapUs": 169, + "vsyncStartUs": 311363942486, + "buildStartUs": 311363942684, + "buildFinishUs": 311363943216, + "rasterStartUs": 311363943385, + "rasterFinishUs": 311363945854 + }, + { + "frameNumber": 633, + "uiDurationUs": 379, + "rasterDurationUs": 2517, + "timestamp": "2026-10-27T01:06:56.727839", + "vsyncOverheadUs": 227, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3314, + "buildToRasterGapUs": 191, + "vsyncStartUs": 311364843253, + "buildStartUs": 311364843480, + "buildFinishUs": 311364843859, + "rasterStartUs": 311364844050, + "rasterFinishUs": 311364846567 + }, + { + "frameNumber": 634, + "uiDurationUs": 520, + "rasterDurationUs": 2588, + "timestamp": "2026-10-27T01:06:56.844630", + "vsyncOverheadUs": 230, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3504, + "buildToRasterGapUs": 166, + "vsyncStartUs": 311364959821, + "buildStartUs": 311364960051, + "buildFinishUs": 311364960571, + "rasterStartUs": 311364960737, + "rasterFinishUs": 311364963325 + }, + { + "frameNumber": 635, + "uiDurationUs": 921, + "rasterDurationUs": 1890, + "timestamp": "2026-10-27T01:06:57.111043", + "vsyncOverheadUs": 181, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3145, + "buildToRasterGapUs": 153, + "vsyncStartUs": 311365226761, + "buildStartUs": 311365226942, + "buildFinishUs": 311365227863, + "rasterStartUs": 311365228016, + "rasterFinishUs": 311365229906 + }, + { + "frameNumber": 636, + "uiDurationUs": 513, + "rasterDurationUs": 2617, + "timestamp": "2026-10-27T01:06:57.228334", + "vsyncOverheadUs": 266, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3564, + "buildToRasterGapUs": 168, + "vsyncStartUs": 311365343539, + "buildStartUs": 311365343805, + "buildFinishUs": 311365344318, + "rasterStartUs": 311365344486, + "rasterFinishUs": 311365347103 + }, + { + "frameNumber": 637, + "uiDurationUs": 356, + "rasterDurationUs": 2535, + "timestamp": "2026-10-27T01:06:57.745177", + "vsyncOverheadUs": 230, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3311, + "buildToRasterGapUs": 190, + "vsyncStartUs": 311365860588, + "buildStartUs": 311365860818, + "buildFinishUs": 311365861174, + "rasterStartUs": 311365861364, + "rasterFinishUs": 311365863899 + }, + { + "frameNumber": 638, + "uiDurationUs": 527, + "rasterDurationUs": 2625, + "timestamp": "2026-10-27T01:06:57.862125", + "vsyncOverheadUs": 235, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3554, + "buildToRasterGapUs": 167, + "vsyncStartUs": 311365977347, + "buildStartUs": 311365977582, + "buildFinishUs": 311365978109, + "rasterStartUs": 311365978276, + "rasterFinishUs": 311365980901 + }, + { + "frameNumber": 639, + "uiDurationUs": 366, + "rasterDurationUs": 2479, + "timestamp": "2026-10-27T01:06:58.762769", + "vsyncOverheadUs": 389, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3412, + "buildToRasterGapUs": 178, + "vsyncStartUs": 311366878210, + "buildStartUs": 311366878599, + "buildFinishUs": 311366878965, + "rasterStartUs": 311366879143, + "rasterFinishUs": 311366881622 + }, + { + "frameNumber": 640, + "uiDurationUs": 522, + "rasterDurationUs": 2628, + "timestamp": "2026-10-27T01:06:58.879779", + "vsyncOverheadUs": 200, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3532, + "buildToRasterGapUs": 182, + "vsyncStartUs": 311366994985, + "buildStartUs": 311366995185, + "buildFinishUs": 311366995707, + "rasterStartUs": 311366995889, + "rasterFinishUs": 311366998517 + }, + { + "frameNumber": 641, + "uiDurationUs": 341, + "rasterDurationUs": 2482, + "timestamp": "2026-10-27T01:06:59.779953", + "vsyncOverheadUs": 278, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3284, + "buildToRasterGapUs": 183, + "vsyncStartUs": 311367895836, + "buildStartUs": 311367896114, + "buildFinishUs": 311367896455, + "rasterStartUs": 311367896638, + "rasterFinishUs": 311367899120 + }, + { + "frameNumber": 642, + "uiDurationUs": 503, + "rasterDurationUs": 2627, + "timestamp": "2026-10-27T01:06:59.897129", + "vsyncOverheadUs": 218, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3521, + "buildToRasterGapUs": 173, + "vsyncStartUs": 311368012347, + "buildStartUs": 311368012565, + "buildFinishUs": 311368013068, + "rasterStartUs": 311368013241, + "rasterFinishUs": 311368015868 + }, + { + "frameNumber": 643, + "uiDurationUs": 341, + "rasterDurationUs": 2437, + "timestamp": "2026-10-27T01:07:00.797666", + "vsyncOverheadUs": 285, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3248, + "buildToRasterGapUs": 185, + "vsyncStartUs": 311368913216, + "buildStartUs": 311368913501, + "buildFinishUs": 311368913842, + "rasterStartUs": 311368914027, + "rasterFinishUs": 311368916464 + }, + { + "frameNumber": 644, + "uiDurationUs": 599, + "rasterDurationUs": 2444, + "timestamp": "2026-10-27T01:07:00.914768", + "vsyncOverheadUs": 321, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3537, + "buildToRasterGapUs": 173, + "vsyncStartUs": 311369029994, + "buildStartUs": 311369030315, + "buildFinishUs": 311369030914, + "rasterStartUs": 311369031087, + "rasterFinishUs": 311369033531 + }, + { + "frameNumber": 645, + "uiDurationUs": 79, + "rasterDurationUs": 530, + "timestamp": "2026-10-27T01:07:01.812542", + "vsyncOverheadUs": 170, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 824, + "buildToRasterGapUs": 45, + "vsyncStartUs": 311369930737, + "buildStartUs": 311369930907, + "buildFinishUs": 311369930986, + "rasterStartUs": 311369931031, + "rasterFinishUs": 311369931561 + }, + { + "frameNumber": 646, + "uiDurationUs": 255, + "rasterDurationUs": 330, + "timestamp": "2026-10-27T01:07:01.812560", + "vsyncOverheadUs": 198, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 840, + "buildToRasterGapUs": 57, + "vsyncStartUs": 311369947417, + "buildStartUs": 311369947615, + "buildFinishUs": 311369947870, + "rasterStartUs": 311369947927, + "rasterFinishUs": 311369948257 + }, + { + "frameNumber": 647, + "uiDurationUs": 432, + "rasterDurationUs": 1446, + "timestamp": "2026-10-27T01:07:01.930777", + "vsyncOverheadUs": 264, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2258, + "buildToRasterGapUs": 116, + "vsyncStartUs": 311370047497, + "buildStartUs": 311370047761, + "buildFinishUs": 311370048193, + "rasterStartUs": 311370048309, + "rasterFinishUs": 311370049755 + }, + { + "frameNumber": 648, + "uiDurationUs": 548, + "rasterDurationUs": 1810, + "timestamp": "2026-10-27T01:07:02.631932", + "vsyncOverheadUs": 261, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2771, + "buildToRasterGapUs": 152, + "vsyncStartUs": 311370748277, + "buildStartUs": 311370748538, + "buildFinishUs": 311370749086, + "rasterStartUs": 311370749238, + "rasterFinishUs": 311370751048 + }, + { + "frameNumber": 649, + "uiDurationUs": 148, + "rasterDurationUs": 713, + "timestamp": "2026-10-27T01:07:02.746663", + "vsyncOverheadUs": 167, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1110, + "buildToRasterGapUs": 82, + "vsyncStartUs": 311370865065, + "buildStartUs": 311370865232, + "buildFinishUs": 311370865380, + "rasterStartUs": 311370865462, + "rasterFinishUs": 311370866175 + }, + { + "frameNumber": 650, + "uiDurationUs": 239, + "rasterDurationUs": 1756, + "timestamp": "2026-10-27T01:07:02.746681", + "vsyncOverheadUs": 158, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2303, + "buildToRasterGapUs": 150, + "vsyncStartUs": 311370948485, + "buildStartUs": 311370948643, + "buildFinishUs": 311370948882, + "rasterStartUs": 311370949032, + "rasterFinishUs": 311370950788 + }, + { + "frameNumber": 651, + "uiDurationUs": 573, + "rasterDurationUs": 1750, + "timestamp": "2026-10-27T01:07:02.849403", + "vsyncOverheadUs": 520, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3050, + "buildToRasterGapUs": 207, + "vsyncStartUs": 311370965169, + "buildStartUs": 311370965689, + "buildFinishUs": 311370966262, + "rasterStartUs": 311370966469, + "rasterFinishUs": 311370968219 + }, + { + "frameNumber": 652, + "uiDurationUs": 517, + "rasterDurationUs": 2439, + "timestamp": "2026-10-27T01:07:02.966604", + "vsyncOverheadUs": 274, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3405, + "buildToRasterGapUs": 175, + "vsyncStartUs": 311371081957, + "buildStartUs": 311371082231, + "buildFinishUs": 311371082748, + "rasterStartUs": 311371082923, + "rasterFinishUs": 311371085362 + }, + { + "frameNumber": 653, + "uiDurationUs": 397, + "rasterDurationUs": 2514, + "timestamp": "2026-10-27T01:07:03.850893", + "vsyncOverheadUs": 225, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3324, + "buildToRasterGapUs": 188, + "vsyncStartUs": 311371966297, + "buildStartUs": 311371966522, + "buildFinishUs": 311371966919, + "rasterStartUs": 311371967107, + "rasterFinishUs": 311371969621 + }, + { + "frameNumber": 654, + "uiDurationUs": 640, + "rasterDurationUs": 2157, + "timestamp": "2026-10-27T01:07:03.850919", + "vsyncOverheadUs": 422, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3382, + "buildToRasterGapUs": 163, + "vsyncStartUs": 311371982984, + "buildStartUs": 311371983406, + "buildFinishUs": 311371984046, + "rasterStartUs": 311371984209, + "rasterFinishUs": 311371986366 + }, + { + "frameNumber": 655, + "uiDurationUs": 401, + "rasterDurationUs": 1871, + "timestamp": "2026-10-27T01:07:03.966504", + "vsyncOverheadUs": 233, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2628, + "buildToRasterGapUs": 123, + "vsyncStartUs": 311372082778, + "buildStartUs": 311372083011, + "buildFinishUs": 311372083412, + "rasterStartUs": 311372083535, + "rasterFinishUs": 311372085406 + }, + { + "frameNumber": 656, + "uiDurationUs": 386, + "rasterDurationUs": 2530, + "timestamp": "2026-10-27T01:07:04.868411", + "vsyncOverheadUs": 292, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3351, + "buildToRasterGapUs": 143, + "vsyncStartUs": 311372983733, + "buildStartUs": 311372984025, + "buildFinishUs": 311372984411, + "rasterStartUs": 311372984554, + "rasterFinishUs": 311372987084 + }, + { + "frameNumber": 657, + "uiDurationUs": 904, + "rasterDurationUs": 1463, + "timestamp": "2026-10-27T01:07:04.868436", + "vsyncOverheadUs": 390, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2882, + "buildToRasterGapUs": 125, + "vsyncStartUs": 311373000416, + "buildStartUs": 311373000806, + "buildFinishUs": 311373001710, + "rasterStartUs": 311373001835, + "rasterFinishUs": 311373003298 + }, + { + "frameNumber": 658, + "uiDurationUs": 421, + "rasterDurationUs": 1900, + "timestamp": "2026-10-27T01:07:04.984375", + "vsyncOverheadUs": 191, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2647, + "buildToRasterGapUs": 135, + "vsyncStartUs": 311373100521, + "buildStartUs": 311373100712, + "buildFinishUs": 311373101133, + "rasterStartUs": 311373101268, + "rasterFinishUs": 311373103168 + }, + { + "frameNumber": 659, + "uiDurationUs": 1070, + "rasterDurationUs": 1866, + "timestamp": "2026-10-27T01:07:05.118556", + "vsyncOverheadUs": 226, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3303, + "buildToRasterGapUs": 141, + "vsyncStartUs": 311373233993, + "buildStartUs": 311373234219, + "buildFinishUs": 311373235289, + "rasterStartUs": 311373235430, + "rasterFinishUs": 311373237296 + }, + { + "frameNumber": 660, + "uiDurationUs": 563, + "rasterDurationUs": 2617, + "timestamp": "2026-10-27T01:07:05.235672", + "vsyncOverheadUs": 214, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3557, + "buildToRasterGapUs": 163, + "vsyncStartUs": 311373350782, + "buildStartUs": 311373350996, + "buildFinishUs": 311373351559, + "rasterStartUs": 311373351722, + "rasterFinishUs": 311373354339 + }, + { + "frameNumber": 661, + "uiDurationUs": 557, + "rasterDurationUs": 1924, + "timestamp": "2026-10-27T01:07:05.618545", + "vsyncOverheadUs": 172, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2798, + "buildToRasterGapUs": 145, + "vsyncStartUs": 311373734469, + "buildStartUs": 311373734641, + "buildFinishUs": 311373735198, + "rasterStartUs": 311373735343, + "rasterFinishUs": 311373737267 + }, + { + "frameNumber": 662, + "uiDurationUs": 542, + "rasterDurationUs": 2582, + "timestamp": "2026-10-27T01:07:05.735996", + "vsyncOverheadUs": 238, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3525, + "buildToRasterGapUs": 163, + "vsyncStartUs": 311373851246, + "buildStartUs": 311373851484, + "buildFinishUs": 311373852026, + "rasterStartUs": 311373852189, + "rasterFinishUs": 311373854771 + }, + { + "frameNumber": 663, + "uiDurationUs": 370, + "rasterDurationUs": 2551, + "timestamp": "2026-10-27T01:07:05.886125", + "vsyncOverheadUs": 241, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3329, + "buildToRasterGapUs": 167, + "vsyncStartUs": 311374001397, + "buildStartUs": 311374001638, + "buildFinishUs": 311374002008, + "rasterStartUs": 311374002175, + "rasterFinishUs": 311374004726 + }, + { + "frameNumber": 664, + "uiDurationUs": 705, + "rasterDurationUs": 2165, + "timestamp": "2026-10-27T01:07:05.886157", + "vsyncOverheadUs": 485, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3516, + "buildToRasterGapUs": 161, + "vsyncStartUs": 311374018080, + "buildStartUs": 311374018565, + "buildFinishUs": 311374019270, + "rasterStartUs": 311374019431, + "rasterFinishUs": 311374021596 + }, + { + "frameNumber": 665, + "uiDurationUs": 397, + "rasterDurationUs": 1812, + "timestamp": "2026-10-27T01:07:06.001868", + "vsyncOverheadUs": 257, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2603, + "buildToRasterGapUs": 137, + "vsyncStartUs": 311374118178, + "buildStartUs": 311374118435, + "buildFinishUs": 311374118832, + "rasterStartUs": 311374118969, + "rasterFinishUs": 311374120781 + }, + { + "frameNumber": 666, + "uiDurationUs": 373, + "rasterDurationUs": 2477, + "timestamp": "2026-10-27T01:07:06.904180", + "vsyncOverheadUs": 286, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3310, + "buildToRasterGapUs": 174, + "vsyncStartUs": 311375019485, + "buildStartUs": 311375019771, + "buildFinishUs": 311375020144, + "rasterStartUs": 311375020318, + "rasterFinishUs": 311375022795 + }, + { + "frameNumber": 667, + "uiDurationUs": 667, + "rasterDurationUs": 2100, + "timestamp": "2026-10-27T01:07:06.904211", + "vsyncOverheadUs": 412, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3339, + "buildToRasterGapUs": 160, + "vsyncStartUs": 311375036175, + "buildStartUs": 311375036587, + "buildFinishUs": 311375037254, + "rasterStartUs": 311375037414, + "rasterFinishUs": 311375039514 + }, + { + "frameNumber": 668, + "uiDurationUs": 527, + "rasterDurationUs": 2506, + "timestamp": "2026-10-27T01:07:07.020290", + "vsyncOverheadUs": 297, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3500, + "buildToRasterGapUs": 170, + "vsyncStartUs": 311375136085, + "buildStartUs": 311375136382, + "buildFinishUs": 311375136909, + "rasterStartUs": 311375137079, + "rasterFinishUs": 311375139585 + }, + { + "frameNumber": 669, + "uiDurationUs": 378, + "rasterDurationUs": 2460, + "timestamp": "2026-10-27T01:07:07.921618", + "vsyncOverheadUs": 280, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3294, + "buildToRasterGapUs": 176, + "vsyncStartUs": 311376037185, + "buildStartUs": 311376037465, + "buildFinishUs": 311376037843, + "rasterStartUs": 311376038019, + "rasterFinishUs": 311376040479 + }, + { + "frameNumber": 670, + "uiDurationUs": 612, + "rasterDurationUs": 2143, + "timestamp": "2026-10-27T01:07:07.921647", + "vsyncOverheadUs": 446, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3374, + "buildToRasterGapUs": 173, + "vsyncStartUs": 311376053873, + "buildStartUs": 311376054319, + "buildFinishUs": 311376054931, + "rasterStartUs": 311376055104, + "rasterFinishUs": 311376057247 + }, + { + "frameNumber": 671, + "uiDurationUs": 552, + "rasterDurationUs": 2484, + "timestamp": "2026-10-27T01:07:08.037438", + "vsyncOverheadUs": 286, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3493, + "buildToRasterGapUs": 171, + "vsyncStartUs": 311376153684, + "buildStartUs": 311376153970, + "buildFinishUs": 311376154522, + "rasterStartUs": 311376154693, + "rasterFinishUs": 311376157177 + }, + { + "frameNumber": 672, + "uiDurationUs": 330, + "rasterDurationUs": 2453, + "timestamp": "2026-10-27T01:07:08.939116", + "vsyncOverheadUs": 277, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3245, + "buildToRasterGapUs": 185, + "vsyncStartUs": 311377054475, + "buildStartUs": 311377054752, + "buildFinishUs": 311377055082, + "rasterStartUs": 311377055267, + "rasterFinishUs": 311377057720 + }, + { + "frameNumber": 673, + "uiDurationUs": 733, + "rasterDurationUs": 2075, + "timestamp": "2026-10-27T01:07:08.939145", + "vsyncOverheadUs": 433, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3402, + "buildToRasterGapUs": 161, + "vsyncStartUs": 311377071156, + "buildStartUs": 311377071589, + "buildFinishUs": 311377072322, + "rasterStartUs": 311377072483, + "rasterFinishUs": 311377074558 + }, + { + "frameNumber": 674, + "uiDurationUs": 515, + "rasterDurationUs": 2468, + "timestamp": "2026-10-27T01:07:09.055535", + "vsyncOverheadUs": 273, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3429, + "buildToRasterGapUs": 173, + "vsyncStartUs": 311377171247, + "buildStartUs": 311377171520, + "buildFinishUs": 311377172035, + "rasterStartUs": 311377172208, + "rasterFinishUs": 311377174676 + }, + { + "frameNumber": 675, + "uiDurationUs": 523, + "rasterDurationUs": 1838, + "timestamp": "2026-10-27T01:07:09.622278", + "vsyncOverheadUs": 222, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2658, + "buildToRasterGapUs": 75, + "vsyncStartUs": 311377738370, + "buildStartUs": 311377738592, + "buildFinishUs": 311377739115, + "rasterStartUs": 311377739190, + "rasterFinishUs": 311377741028 + }, + { + "frameNumber": 676, + "uiDurationUs": 516, + "rasterDurationUs": 2450, + "timestamp": "2026-10-27T01:07:09.739849", + "vsyncOverheadUs": 279, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3408, + "buildToRasterGapUs": 163, + "vsyncStartUs": 311377855150, + "buildStartUs": 311377855429, + "buildFinishUs": 311377855945, + "rasterStartUs": 311377856108, + "rasterFinishUs": 311377858558 + }, + { + "frameNumber": 677, + "uiDurationUs": 336, + "rasterDurationUs": 2426, + "timestamp": "2026-10-27T01:07:09.956708", + "vsyncOverheadUs": 336, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3261, + "buildToRasterGapUs": 163, + "vsyncStartUs": 311378072187, + "buildStartUs": 311378072523, + "buildFinishUs": 311378072859, + "rasterStartUs": 311378073022, + "rasterFinishUs": 311378075448 + }, + { + "frameNumber": 678, + "uiDurationUs": 665, + "rasterDurationUs": 2101, + "timestamp": "2026-10-27T01:07:09.956738", + "vsyncOverheadUs": 410, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3336, + "buildToRasterGapUs": 160, + "vsyncStartUs": 311378088877, + "buildStartUs": 311378089287, + "buildFinishUs": 311378089952, + "rasterStartUs": 311378090112, + "rasterFinishUs": 311378092213 + }, + { + "frameNumber": 679, + "uiDurationUs": 518, + "rasterDurationUs": 2454, + "timestamp": "2026-10-27T01:07:10.073501", + "vsyncOverheadUs": 273, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3407, + "buildToRasterGapUs": 162, + "vsyncStartUs": 311378189017, + "buildStartUs": 311378189290, + "buildFinishUs": 311378189808, + "rasterStartUs": 311378189970, + "rasterFinishUs": 311378192424 + }, + { + "frameNumber": 680, + "uiDurationUs": 518, + "rasterDurationUs": 1833, + "timestamp": "2026-10-27T01:07:10.623592", + "vsyncOverheadUs": 232, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2721, + "buildToRasterGapUs": 138, + "vsyncStartUs": 311378739729, + "buildStartUs": 311378739961, + "buildFinishUs": 311378740479, + "rasterStartUs": 311378740617, + "rasterFinishUs": 311378742450 + }, + { + "frameNumber": 681, + "uiDurationUs": 14087, + "rasterDurationUs": 5691, + "timestamp": "2026-10-27T01:07:10.623614", + "vsyncOverheadUs": 244, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 20112, + "buildToRasterGapUs": 90, + "vsyncStartUs": 311378789797, + "buildStartUs": 311378790041, + "buildFinishUs": 311378804128, + "rasterStartUs": 311378804218, + "rasterFinishUs": 311378809909 + }, + { + "frameNumber": 682, + "uiDurationUs": 594, + "rasterDurationUs": 2783, + "timestamp": "2026-10-27T01:07:10.741728", + "vsyncOverheadUs": 249, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3791, + "buildToRasterGapUs": 165, + "vsyncStartUs": 311378856555, + "buildStartUs": 311378856804, + "buildFinishUs": 311378857398, + "rasterStartUs": 311378857563, + "rasterFinishUs": 311378860346 + }, + { + "frameNumber": 683, + "uiDurationUs": 334, + "rasterDurationUs": 2810, + "timestamp": "2026-10-27T01:07:10.975139", + "vsyncOverheadUs": 299, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3604, + "buildToRasterGapUs": 161, + "vsyncStartUs": 311379090123, + "buildStartUs": 311379090422, + "buildFinishUs": 311379090756, + "rasterStartUs": 311379090917, + "rasterFinishUs": 311379093727 + }, + { + "frameNumber": 684, + "uiDurationUs": 4025, + "rasterDurationUs": 3747, + "timestamp": "2026-10-27T01:07:10.975177", + "vsyncOverheadUs": 416, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 8369, + "buildToRasterGapUs": 181, + "vsyncStartUs": 311379106810, + "buildStartUs": 311379107226, + "buildFinishUs": 311379111251, + "rasterStartUs": 311379111432, + "rasterFinishUs": 311379115179 + }, + { + "frameNumber": 685, + "uiDurationUs": 420, + "rasterDurationUs": 2079, + "timestamp": "2026-10-27T01:07:11.090797", + "vsyncOverheadUs": 228, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2859, + "buildToRasterGapUs": 132, + "vsyncStartUs": 311379206932, + "buildStartUs": 311379207160, + "buildFinishUs": 311379207580, + "rasterStartUs": 311379207712, + "rasterFinishUs": 311379209791 + }, + { + "frameNumber": 686, + "uiDurationUs": 2466, + "rasterDurationUs": 2530, + "timestamp": "2026-10-27T01:07:11.627374", + "vsyncOverheadUs": 217, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5374, + "buildToRasterGapUs": 161, + "vsyncStartUs": 311379740790, + "buildStartUs": 311379741007, + "buildFinishUs": 311379743473, + "rasterStartUs": 311379743634, + "rasterFinishUs": 311379746164 + }, + { + "frameNumber": 687, + "uiDurationUs": 527, + "rasterDurationUs": 2771, + "timestamp": "2026-10-27T01:07:11.742464", + "vsyncOverheadUs": 229, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3687, + "buildToRasterGapUs": 160, + "vsyncStartUs": 311379857583, + "buildStartUs": 311379857812, + "buildFinishUs": 311379858339, + "rasterStartUs": 311379858499, + "rasterFinishUs": 311379861270 + }, + { + "frameNumber": 688, + "uiDurationUs": 405, + "rasterDurationUs": 2782, + "timestamp": "2026-10-27T01:07:11.992570", + "vsyncOverheadUs": 281, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3632, + "buildToRasterGapUs": 164, + "vsyncStartUs": 311380107626, + "buildStartUs": 311380107907, + "buildFinishUs": 311380108312, + "rasterStartUs": 311380108476, + "rasterFinishUs": 311380111258 + }, + { + "frameNumber": 689, + "uiDurationUs": 3839, + "rasterDurationUs": 2510, + "timestamp": "2026-10-27T01:07:11.992603", + "vsyncOverheadUs": 430, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 6961, + "buildToRasterGapUs": 182, + "vsyncStartUs": 311380124305, + "buildStartUs": 311380124735, + "buildFinishUs": 311380128574, + "rasterStartUs": 311380128756, + "rasterFinishUs": 311380131266 + }, + { + "frameNumber": 690, + "uiDurationUs": 290, + "rasterDurationUs": 1669, + "timestamp": "2026-10-27T01:07:12.111299", + "vsyncOverheadUs": 3618, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5641, + "buildToRasterGapUs": 64, + "vsyncStartUs": 311380224379, + "buildStartUs": 311380227997, + "buildFinishUs": 311380228287, + "rasterStartUs": 311380228351, + "rasterFinishUs": 311380230020 + }, + { + "frameNumber": 691, + "uiDurationUs": 606, + "rasterDurationUs": 615, + "timestamp": "2026-10-27T01:07:12.757079", + "vsyncOverheadUs": 191, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1477, + "buildToRasterGapUs": 65, + "vsyncStartUs": 311380875105, + "buildStartUs": 311380875296, + "buildFinishUs": 311380875902, + "rasterStartUs": 311380875967, + "rasterFinishUs": 311380876582 + }, + { + "frameNumber": 692, + "uiDurationUs": 114, + "rasterDurationUs": 579, + "timestamp": "2026-10-27T01:07:12.873106", + "vsyncOverheadUs": 130, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 889, + "buildToRasterGapUs": 66, + "vsyncStartUs": 311380991897, + "buildStartUs": 311380992027, + "buildFinishUs": 311380992141, + "rasterStartUs": 311380992207, + "rasterFinishUs": 311380992786 + }, + { + "frameNumber": 693, + "uiDurationUs": 31, + "rasterDurationUs": 415, + "timestamp": "2026-10-27T01:07:12.873116", + "vsyncOverheadUs": 324, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 888, + "buildToRasterGapUs": 118, + "vsyncStartUs": 311381008582, + "buildStartUs": 311381008906, + "buildFinishUs": 311381008937, + "rasterStartUs": 311381009055, + "rasterFinishUs": 311381009470 + }, + { + "frameNumber": 694, + "uiDurationUs": 10808, + "rasterDurationUs": 6825, + "timestamp": "2026-10-27T01:07:12.873118", + "vsyncOverheadUs": 191, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 17883, + "buildToRasterGapUs": 59, + "vsyncStartUs": 311381075320, + "buildStartUs": 311381075511, + "buildFinishUs": 311381086319, + "rasterStartUs": 311381086378, + "rasterFinishUs": 311381093203 + }, + { + "frameNumber": 695, + "uiDurationUs": 120, + "rasterDurationUs": 1122, + "timestamp": "2026-10-27T01:07:13.016525", + "vsyncOverheadUs": 222, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1586, + "buildToRasterGapUs": 122, + "vsyncStartUs": 311381125374, + "buildStartUs": 311381125596, + "buildFinishUs": 311381125716, + "rasterStartUs": 311381125838, + "rasterFinishUs": 311381126960 + }, + { + "frameNumber": 696, + "uiDurationUs": 3789, + "rasterDurationUs": 2319, + "timestamp": "2026-10-27T01:07:13.016532", + "vsyncOverheadUs": 204, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 6438, + "buildToRasterGapUs": 126, + "vsyncStartUs": 311381142059, + "buildStartUs": 311381142263, + "buildFinishUs": 311381146052, + "rasterStartUs": 311381146178, + "rasterFinishUs": 311381148497 + }, + { + "frameNumber": 697, + "uiDurationUs": 288, + "rasterDurationUs": 1900, + "timestamp": "2026-10-27T01:07:13.125811", + "vsyncOverheadUs": 137, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2380, + "buildToRasterGapUs": 55, + "vsyncStartUs": 311381242166, + "buildStartUs": 311381242303, + "buildFinishUs": 311381242591, + "rasterStartUs": 311381242646, + "rasterFinishUs": 311381244546 + }, + { + "frameNumber": 698, + "uiDurationUs": 373, + "rasterDurationUs": 4038, + "timestamp": "2026-10-27T01:07:14.028985", + "vsyncOverheadUs": 226, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4834, + "buildToRasterGapUs": 197, + "vsyncStartUs": 311382143165, + "buildStartUs": 311382143391, + "buildFinishUs": 311382143764, + "rasterStartUs": 311382143961, + "rasterFinishUs": 311382147999 + }, + { + "frameNumber": 699, + "uiDurationUs": 4287, + "rasterDurationUs": 3606, + "timestamp": "2026-10-27T01:07:14.029001", + "vsyncOverheadUs": 418, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 8531, + "buildToRasterGapUs": 220, + "vsyncStartUs": 311382159851, + "buildStartUs": 311382160269, + "buildFinishUs": 311382164556, + "rasterStartUs": 311382164776, + "rasterFinishUs": 311382168382 + }, + { + "frameNumber": 700, + "uiDurationUs": 382, + "rasterDurationUs": 2327, + "timestamp": "2026-10-27T01:07:14.143945", + "vsyncOverheadUs": 186, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2972, + "buildToRasterGapUs": 77, + "vsyncStartUs": 311382259733, + "buildStartUs": 311382259919, + "buildFinishUs": 311382260301, + "rasterStartUs": 311382260378, + "rasterFinishUs": 311382262705 + }, + { + "frameNumber": 701, + "uiDurationUs": 404, + "rasterDurationUs": 4053, + "timestamp": "2026-10-27T01:07:15.045669", + "vsyncOverheadUs": 294, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4945, + "buildToRasterGapUs": 194, + "vsyncStartUs": 311383160552, + "buildStartUs": 311383160846, + "buildFinishUs": 311383161250, + "rasterStartUs": 311383161444, + "rasterFinishUs": 311383165497 + }, + { + "frameNumber": 702, + "uiDurationUs": 4228, + "rasterDurationUs": 3679, + "timestamp": "2026-10-27T01:07:15.045684", + "vsyncOverheadUs": 459, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 8544, + "buildToRasterGapUs": 178, + "vsyncStartUs": 311383177234, + "buildStartUs": 311383177693, + "buildFinishUs": 311383181921, + "rasterStartUs": 311383182099, + "rasterFinishUs": 311383185778 + }, + { + "frameNumber": 703, + "uiDurationUs": 183, + "rasterDurationUs": 2295, + "timestamp": "2026-10-27T01:07:15.045687", + "vsyncOverheadUs": 186, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2742, + "buildToRasterGapUs": 78, + "vsyncStartUs": 311383243961, + "buildStartUs": 311383244147, + "buildFinishUs": 311383244330, + "rasterStartUs": 311383244408, + "rasterFinishUs": 311383246703 + }, + { + "frameNumber": 704, + "uiDurationUs": 534, + "rasterDurationUs": 1971, + "timestamp": "2026-10-27T01:07:15.045689", + "vsyncOverheadUs": 486, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3126, + "buildToRasterGapUs": 135, + "vsyncStartUs": 311383260644, + "buildStartUs": 311383261130, + "buildFinishUs": 311383261664, + "rasterStartUs": 311383261799, + "rasterFinishUs": 311383263770 + }, + { + "frameNumber": 705, + "uiDurationUs": 586, + "rasterDurationUs": 2583, + "timestamp": "2026-10-27T01:07:15.161394", + "vsyncOverheadUs": 322, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3646, + "buildToRasterGapUs": 155, + "vsyncStartUs": 311383277325, + "buildStartUs": 311383277647, + "buildFinishUs": 311383278233, + "rasterStartUs": 311383278388, + "rasterFinishUs": 311383280971 + }, + { + "frameNumber": 706, + "uiDurationUs": 378, + "rasterDurationUs": 1512, + "timestamp": "2026-10-27T01:07:15.161414", + "vsyncOverheadUs": 405, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2390, + "buildToRasterGapUs": 95, + "vsyncStartUs": 311383294007, + "buildStartUs": 311383294412, + "buildFinishUs": 311383294790, + "rasterStartUs": 311383294885, + "rasterFinishUs": 311383296397 + }, + { + "frameNumber": 707, + "uiDurationUs": 480, + "rasterDurationUs": 2035, + "timestamp": "2026-10-27T01:07:15.161419", + "vsyncOverheadUs": 412, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3060, + "buildToRasterGapUs": 133, + "vsyncStartUs": 311383310689, + "buildStartUs": 311383311101, + "buildFinishUs": 311383311581, + "rasterStartUs": 311383311714, + "rasterFinishUs": 311383313749 + }, + { + "frameNumber": 708, + "uiDurationUs": 449, + "rasterDurationUs": 2562, + "timestamp": "2026-10-27T01:07:15.161424", + "vsyncOverheadUs": 446, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3526, + "buildToRasterGapUs": 69, + "vsyncStartUs": 311383327371, + "buildStartUs": 311383327817, + "buildFinishUs": 311383328266, + "rasterStartUs": 311383328335, + "rasterFinishUs": 311383330897 + }, + { + "frameNumber": 709, + "uiDurationUs": 495, + "rasterDurationUs": 2563, + "timestamp": "2026-10-27T01:07:15.161429", + "vsyncOverheadUs": 456, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3661, + "buildToRasterGapUs": 147, + "vsyncStartUs": 311383344053, + "buildStartUs": 311383344509, + "buildFinishUs": 311383345004, + "rasterStartUs": 311383345151, + "rasterFinishUs": 311383347714 + }, + { + "frameNumber": 710, + "uiDurationUs": 561, + "rasterDurationUs": 2561, + "timestamp": "2026-10-27T01:07:15.161433", + "vsyncOverheadUs": 444, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3814, + "buildToRasterGapUs": 248, + "vsyncStartUs": 311383360735, + "buildStartUs": 311383361179, + "buildFinishUs": 311383361740, + "rasterStartUs": 311383361988, + "rasterFinishUs": 311383364549 + }, + { + "frameNumber": 711, + "uiDurationUs": 545, + "rasterDurationUs": 2544, + "timestamp": "2026-10-27T01:07:15.161438", + "vsyncOverheadUs": 447, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3648, + "buildToRasterGapUs": 112, + "vsyncStartUs": 311383377417, + "buildStartUs": 311383377864, + "buildFinishUs": 311383378409, + "rasterStartUs": 311383378521, + "rasterFinishUs": 311383381065 + }, + { + "frameNumber": 712, + "uiDurationUs": 646, + "rasterDurationUs": 2580, + "timestamp": "2026-10-27T01:07:15.278132", + "vsyncOverheadUs": 465, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3836, + "buildToRasterGapUs": 145, + "vsyncStartUs": 311383394099, + "buildStartUs": 311383394564, + "buildFinishUs": 311383395210, + "rasterStartUs": 311383395355, + "rasterFinishUs": 311383397935 + }, + { + "frameNumber": 713, + "uiDurationUs": 6288, + "rasterDurationUs": 3044, + "timestamp": "2026-10-27T01:07:15.278193", + "vsyncOverheadUs": 164, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 9662, + "buildToRasterGapUs": 166, + "vsyncStartUs": 311383460826, + "buildStartUs": 311383460990, + "buildFinishUs": 311383467278, + "rasterStartUs": 311383467444, + "rasterFinishUs": 311383470488 + }, + { + "frameNumber": 714, + "uiDurationUs": 566, + "rasterDurationUs": 5931, + "timestamp": "2026-10-27T01:07:15.278199", + "vsyncOverheadUs": 356, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 6979, + "buildToRasterGapUs": 126, + "vsyncStartUs": 311383477508, + "buildStartUs": 311383477864, + "buildFinishUs": 311383478430, + "rasterStartUs": 311383478556, + "rasterFinishUs": 311383484487 + }, + { + "frameNumber": 715, + "uiDurationUs": 388, + "rasterDurationUs": 2622, + "timestamp": "2026-10-27T01:07:15.278204", + "vsyncOverheadUs": 398, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3543, + "buildToRasterGapUs": 135, + "vsyncStartUs": 311383494190, + "buildStartUs": 311383494588, + "buildFinishUs": 311383494976, + "rasterStartUs": 311383495111, + "rasterFinishUs": 311383497733 + }, + { + "frameNumber": 716, + "uiDurationUs": 587, + "rasterDurationUs": 2803, + "timestamp": "2026-10-27T01:07:15.395317", + "vsyncOverheadUs": 518, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4040, + "buildToRasterGapUs": 132, + "vsyncStartUs": 311383510872, + "buildStartUs": 311383511390, + "buildFinishUs": 311383511977, + "rasterStartUs": 311383512109, + "rasterFinishUs": 311383514912 + }, + { + "frameNumber": 717, + "uiDurationUs": 389, + "rasterDurationUs": 2755, + "timestamp": "2026-10-27T01:07:15.395342", + "vsyncOverheadUs": 444, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3722, + "buildToRasterGapUs": 134, + "vsyncStartUs": 311383527554, + "buildStartUs": 311383527998, + "buildFinishUs": 311383528387, + "rasterStartUs": 311383528521, + "rasterFinishUs": 311383531276 + }, + { + "frameNumber": 718, + "uiDurationUs": 458, + "rasterDurationUs": 2771, + "timestamp": "2026-10-27T01:07:15.395347", + "vsyncOverheadUs": 500, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3881, + "buildToRasterGapUs": 152, + "vsyncStartUs": 311383544236, + "buildStartUs": 311383544736, + "buildFinishUs": 311383545194, + "rasterStartUs": 311383545346, + "rasterFinishUs": 311383548117 + }, + { + "frameNumber": 719, + "uiDurationUs": 394, + "rasterDurationUs": 2719, + "timestamp": "2026-10-27T01:07:15.395352", + "vsyncOverheadUs": 514, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3755, + "buildToRasterGapUs": 128, + "vsyncStartUs": 311383560918, + "buildStartUs": 311383561432, + "buildFinishUs": 311383561826, + "rasterStartUs": 311383561954, + "rasterFinishUs": 311383564673 + }, + { + "frameNumber": 720, + "uiDurationUs": 382, + "rasterDurationUs": 2755, + "timestamp": "2026-10-27T01:07:15.395356", + "vsyncOverheadUs": 455, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3724, + "buildToRasterGapUs": 132, + "vsyncStartUs": 311383577600, + "buildStartUs": 311383578055, + "buildFinishUs": 311383578437, + "rasterStartUs": 311383578569, + "rasterFinishUs": 311383581324 + }, + { + "frameNumber": 721, + "uiDurationUs": 378, + "rasterDurationUs": 2729, + "timestamp": "2026-10-27T01:07:15.395361", + "vsyncOverheadUs": 420, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3675, + "buildToRasterGapUs": 148, + "vsyncStartUs": 311383594282, + "buildStartUs": 311383594702, + "buildFinishUs": 311383595080, + "rasterStartUs": 311383595228, + "rasterFinishUs": 311383597957 + }, + { + "frameNumber": 722, + "uiDurationUs": 454, + "rasterDurationUs": 2676, + "timestamp": "2026-10-27T01:07:15.395365", + "vsyncOverheadUs": 460, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3730, + "buildToRasterGapUs": 140, + "vsyncStartUs": 311383610964, + "buildStartUs": 311383611424, + "buildFinishUs": 311383611878, + "rasterStartUs": 311383612018, + "rasterFinishUs": 311383614694 + }, + { + "frameNumber": 723, + "uiDurationUs": 362, + "rasterDurationUs": 3169, + "timestamp": "2026-10-27T01:07:15.527124", + "vsyncOverheadUs": 413, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4087, + "buildToRasterGapUs": 143, + "vsyncStartUs": 311383627645, + "buildStartUs": 311383628058, + "buildFinishUs": 311383628420, + "rasterStartUs": 311383628563, + "rasterFinishUs": 311383631732 + }, + { + "frameNumber": 724, + "uiDurationUs": 501, + "rasterDurationUs": 2822, + "timestamp": "2026-10-27T01:07:15.527130", + "vsyncOverheadUs": 499, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3892, + "buildToRasterGapUs": 70, + "vsyncStartUs": 311383644327, + "buildStartUs": 311383644826, + "buildFinishUs": 311383645327, + "rasterStartUs": 311383645397, + "rasterFinishUs": 311383648219 + }, + { + "frameNumber": 725, + "uiDurationUs": 455, + "rasterDurationUs": 2655, + "timestamp": "2026-10-27T01:07:15.527133", + "vsyncOverheadUs": 481, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3734, + "buildToRasterGapUs": 143, + "vsyncStartUs": 311383661009, + "buildStartUs": 311383661490, + "buildFinishUs": 311383661945, + "rasterStartUs": 311383662088, + "rasterFinishUs": 311383664743 + }, + { + "frameNumber": 726, + "uiDurationUs": 363, + "rasterDurationUs": 1880, + "timestamp": "2026-10-27T01:07:15.645085", + "vsyncOverheadUs": 276, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2614, + "buildToRasterGapUs": 95, + "vsyncStartUs": 311383761101, + "buildStartUs": 311383761377, + "buildFinishUs": 311383761740, + "rasterStartUs": 311383761835, + "rasterFinishUs": 311383763715 + }, + { + "frameNumber": 727, + "uiDurationUs": 378, + "rasterDurationUs": 4068, + "timestamp": "2026-10-27T01:07:17.065353", + "vsyncOverheadUs": 244, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4880, + "buildToRasterGapUs": 190, + "vsyncStartUs": 311385179478, + "buildStartUs": 311385179722, + "buildFinishUs": 311385180100, + "rasterStartUs": 311385180290, + "rasterFinishUs": 311385184358 + }, + { + "frameNumber": 728, + "uiDurationUs": 2532, + "rasterDurationUs": 2475, + "timestamp": "2026-10-27T01:07:17.065374", + "vsyncOverheadUs": 347, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5452, + "buildToRasterGapUs": 98, + "vsyncStartUs": 311385196164, + "buildStartUs": 311385196511, + "buildFinishUs": 311385199043, + "rasterStartUs": 311385199141, + "rasterFinishUs": 311385201616 + }, + { + "frameNumber": 729, + "uiDurationUs": 359, + "rasterDurationUs": 1898, + "timestamp": "2026-10-27T01:07:17.180050", + "vsyncOverheadUs": 262, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2595, + "buildToRasterGapUs": 76, + "vsyncStartUs": 311385296278, + "buildStartUs": 311385296540, + "buildFinishUs": 311385296899, + "rasterStartUs": 311385296975, + "rasterFinishUs": 311385298873 + }, + { + "frameNumber": 730, + "uiDurationUs": 512, + "rasterDurationUs": 3795, + "timestamp": "2026-10-27T01:07:17.348513", + "vsyncOverheadUs": 312, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4791, + "buildToRasterGapUs": 172, + "vsyncStartUs": 311385463111, + "buildStartUs": 311385463423, + "buildFinishUs": 311385463935, + "rasterStartUs": 311385464107, + "rasterFinishUs": 311385467902 + }, + { + "frameNumber": 731, + "uiDurationUs": 367, + "rasterDurationUs": 4013, + "timestamp": "2026-10-27T01:07:17.348541", + "vsyncOverheadUs": 494, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5034, + "buildToRasterGapUs": 160, + "vsyncStartUs": 311385479800, + "buildStartUs": 311385480294, + "buildFinishUs": 311385480661, + "rasterStartUs": 311385480821, + "rasterFinishUs": 311385484834 + }, + { + "frameNumber": 732, + "uiDurationUs": 426, + "rasterDurationUs": 3618, + "timestamp": "2026-10-27T01:07:17.348546", + "vsyncOverheadUs": 460, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4668, + "buildToRasterGapUs": 164, + "vsyncStartUs": 311385496490, + "buildStartUs": 311385496950, + "buildFinishUs": 311385497376, + "rasterStartUs": 311385497540, + "rasterFinishUs": 311385501158 + }, + { + "frameNumber": 733, + "uiDurationUs": 354, + "rasterDurationUs": 2657, + "timestamp": "2026-10-27T01:07:17.348551", + "vsyncOverheadUs": 431, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3573, + "buildToRasterGapUs": 131, + "vsyncStartUs": 311385513179, + "buildStartUs": 311385513610, + "buildFinishUs": 311385513964, + "rasterStartUs": 311385514095, + "rasterFinishUs": 311385516752 + }, + { + "frameNumber": 734, + "uiDurationUs": 437, + "rasterDurationUs": 2841, + "timestamp": "2026-10-27T01:07:17.348555", + "vsyncOverheadUs": 469, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3889, + "buildToRasterGapUs": 142, + "vsyncStartUs": 311385529869, + "buildStartUs": 311385530338, + "buildFinishUs": 311385530775, + "rasterStartUs": 311385530917, + "rasterFinishUs": 311385533758 + }, + { + "frameNumber": 735, + "uiDurationUs": 331, + "rasterDurationUs": 2628, + "timestamp": "2026-10-27T01:07:17.348560", + "vsyncOverheadUs": 496, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3584, + "buildToRasterGapUs": 129, + "vsyncStartUs": 311385546559, + "buildStartUs": 311385547055, + "buildFinishUs": 311385547386, + "rasterStartUs": 311385547515, + "rasterFinishUs": 311385550143 + }, + { + "frameNumber": 736, + "uiDurationUs": 411, + "rasterDurationUs": 2561, + "timestamp": "2026-10-27T01:07:17.348565", + "vsyncOverheadUs": 418, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3530, + "buildToRasterGapUs": 140, + "vsyncStartUs": 311385563248, + "buildStartUs": 311385563666, + "buildFinishUs": 311385564077, + "rasterStartUs": 311385564217, + "rasterFinishUs": 311385566778 + }, + { + "frameNumber": 737, + "uiDurationUs": 548, + "rasterDurationUs": 2648, + "timestamp": "2026-10-27T01:07:17.465124", + "vsyncOverheadUs": 459, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3793, + "buildToRasterGapUs": 138, + "vsyncStartUs": 311385579938, + "buildStartUs": 311385580397, + "buildFinishUs": 311385580945, + "rasterStartUs": 311385581083, + "rasterFinishUs": 311385583731 + }, + { + "frameNumber": 738, + "uiDurationUs": 403, + "rasterDurationUs": 2608, + "timestamp": "2026-10-27T01:07:17.465147", + "vsyncOverheadUs": 455, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3593, + "buildToRasterGapUs": 127, + "vsyncStartUs": 311385596627, + "buildStartUs": 311385597082, + "buildFinishUs": 311385597485, + "rasterStartUs": 311385597612, + "rasterFinishUs": 311385600220 + }, + { + "frameNumber": 739, + "uiDurationUs": 456, + "rasterDurationUs": 2665, + "timestamp": "2026-10-27T01:07:17.465152", + "vsyncOverheadUs": 444, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3702, + "buildToRasterGapUs": 137, + "vsyncStartUs": 311385613317, + "buildStartUs": 311385613761, + "buildFinishUs": 311385614217, + "rasterStartUs": 311385614354, + "rasterFinishUs": 311385617019 + }, + { + "frameNumber": 740, + "uiDurationUs": 424, + "rasterDurationUs": 2624, + "timestamp": "2026-10-27T01:07:17.465157", + "vsyncOverheadUs": 416, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3605, + "buildToRasterGapUs": 141, + "vsyncStartUs": 311385630007, + "buildStartUs": 311385630423, + "buildFinishUs": 311385630847, + "rasterStartUs": 311385630988, + "rasterFinishUs": 311385633612 + }, + { + "frameNumber": 741, + "uiDurationUs": 574, + "rasterDurationUs": 3916, + "timestamp": "2026-10-27T01:07:17.465161", + "vsyncOverheadUs": 606, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5266, + "buildToRasterGapUs": 170, + "vsyncStartUs": 311385646696, + "buildStartUs": 311385647302, + "buildFinishUs": 311385647876, + "rasterStartUs": 311385648046, + "rasterFinishUs": 311385651962 + }, + { + "frameNumber": 742, + "uiDurationUs": 1119, + "rasterDurationUs": 3589, + "timestamp": "2026-10-27T01:07:17.465166", + "vsyncOverheadUs": 604, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5582, + "buildToRasterGapUs": 270, + "vsyncStartUs": 311385663386, + "buildStartUs": 311385663990, + "buildFinishUs": 311385665109, + "rasterStartUs": 311385665379, + "rasterFinishUs": 311385668968 + }, + { + "frameNumber": 743, + "uiDurationUs": 448, + "rasterDurationUs": 3654, + "timestamp": "2026-10-27T01:07:19.084823", + "vsyncOverheadUs": 238, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4525, + "buildToRasterGapUs": 185, + "vsyncStartUs": 311387198960, + "buildStartUs": 311387199198, + "buildFinishUs": 311387199646, + "rasterStartUs": 311387199831, + "rasterFinishUs": 311387203485 + }, + { + "frameNumber": 744, + "uiDurationUs": 2652, + "rasterDurationUs": 3778, + "timestamp": "2026-10-27T01:07:19.084852", + "vsyncOverheadUs": 569, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 7140, + "buildToRasterGapUs": 141, + "vsyncStartUs": 311387215651, + "buildStartUs": 311387216220, + "buildFinishUs": 311387218872, + "rasterStartUs": 311387219013, + "rasterFinishUs": 311387222791 + }, + { + "frameNumber": 745, + "uiDurationUs": 411, + "rasterDurationUs": 2734, + "timestamp": "2026-10-27T01:07:19.199393", + "vsyncOverheadUs": 203, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3440, + "buildToRasterGapUs": 92, + "vsyncStartUs": 311387314964, + "buildStartUs": 311387315167, + "buildFinishUs": 311387315578, + "rasterStartUs": 311387315670, + "rasterFinishUs": 311387318404 + }, + { + "frameNumber": 746, + "uiDurationUs": 1484, + "rasterDurationUs": 3891, + "timestamp": "2026-10-27T01:07:21.104406", + "vsyncOverheadUs": 651, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 6261, + "buildToRasterGapUs": 235, + "vsyncStartUs": 311389217369, + "buildStartUs": 311389218020, + "buildFinishUs": 311389219504, + "rasterStartUs": 311389219739, + "rasterFinishUs": 311389223630 + }, + { + "frameNumber": 747, + "uiDurationUs": 2789, + "rasterDurationUs": 2647, + "timestamp": "2026-10-27T01:07:21.104438", + "vsyncOverheadUs": 297, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5938, + "buildToRasterGapUs": 205, + "vsyncStartUs": 311389234057, + "buildStartUs": 311389234354, + "buildFinishUs": 311389237143, + "rasterStartUs": 311389237348, + "rasterFinishUs": 311389239995 + }, + { + "frameNumber": 748, + "uiDurationUs": 352, + "rasterDurationUs": 2613, + "timestamp": "2026-10-27T01:07:21.217987", + "vsyncOverheadUs": 187, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3285, + "buildToRasterGapUs": 133, + "vsyncStartUs": 311389333478, + "buildStartUs": 311389333665, + "buildFinishUs": 311389334017, + "rasterStartUs": 311389334150, + "rasterFinishUs": 311389336763 + }, + { + "frameNumber": 749, + "uiDurationUs": 319, + "rasterDurationUs": 3694, + "timestamp": "2026-10-27T01:07:23.121036", + "vsyncOverheadUs": 293, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4509, + "buildToRasterGapUs": 203, + "vsyncStartUs": 311391235204, + "buildStartUs": 311391235497, + "buildFinishUs": 311391235816, + "rasterStartUs": 311391236019, + "rasterFinishUs": 311391239713 + }, + { + "frameNumber": 750, + "uiDurationUs": 2128, + "rasterDurationUs": 2409, + "timestamp": "2026-10-27T01:07:23.121067", + "vsyncOverheadUs": 339, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4953, + "buildToRasterGapUs": 77, + "vsyncStartUs": 311391251886, + "buildStartUs": 311391252225, + "buildFinishUs": 311391254353, + "rasterStartUs": 311391254430, + "rasterFinishUs": 311391256839 + }, + { + "frameNumber": 751, + "uiDurationUs": 365, + "rasterDurationUs": 2411, + "timestamp": "2026-10-27T01:07:23.236264", + "vsyncOverheadUs": 359, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3234, + "buildToRasterGapUs": 99, + "vsyncStartUs": 311391351760, + "buildStartUs": 311391352119, + "buildFinishUs": 311391352484, + "rasterStartUs": 311391352583, + "rasterFinishUs": 311391354994 + }, + { + "frameNumber": 752, + "uiDurationUs": 104, + "rasterDurationUs": 1038, + "timestamp": "2026-10-27T01:07:25.135225", + "vsyncOverheadUs": 116, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1346, + "buildToRasterGapUs": 88, + "vsyncStartUs": 311393253382, + "buildStartUs": 311393253498, + "buildFinishUs": 311393253602, + "rasterStartUs": 311393253690, + "rasterFinishUs": 311393254728 + }, + { + "frameNumber": 753, + "uiDurationUs": 1822, + "rasterDurationUs": 1662, + "timestamp": "2026-10-27T01:07:25.135276", + "vsyncOverheadUs": 325, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3854, + "buildToRasterGapUs": 45, + "vsyncStartUs": 311393270063, + "buildStartUs": 311393270388, + "buildFinishUs": 311393272210, + "rasterStartUs": 311393272255, + "rasterFinishUs": 311393273917 + }, + { + "frameNumber": 754, + "uiDurationUs": 401, + "rasterDurationUs": 2796, + "timestamp": "2026-10-27T01:07:25.254296", + "vsyncOverheadUs": 239, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3571, + "buildToRasterGapUs": 135, + "vsyncStartUs": 311393369680, + "buildStartUs": 311393369919, + "buildFinishUs": 311393370320, + "rasterStartUs": 311393370455, + "rasterFinishUs": 311393373251 + }, + { + "frameNumber": 755, + "uiDurationUs": 255, + "rasterDurationUs": 1598, + "timestamp": "2026-10-27T01:07:27.154312", + "vsyncOverheadUs": 158, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2066, + "buildToRasterGapUs": 55, + "vsyncStartUs": 311395271124, + "buildStartUs": 311395271282, + "buildFinishUs": 311395271537, + "rasterStartUs": 311395271592, + "rasterFinishUs": 311395273190 + }, + { + "frameNumber": 756, + "uiDurationUs": 3964, + "rasterDurationUs": 4015, + "timestamp": "2026-10-27T01:07:27.154368", + "vsyncOverheadUs": 655, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 8814, + "buildToRasterGapUs": 180, + "vsyncStartUs": 311395287803, + "buildStartUs": 311395288458, + "buildFinishUs": 311395292422, + "rasterStartUs": 311395292602, + "rasterFinishUs": 311395296617 + }, + { + "frameNumber": 757, + "uiDurationUs": 391, + "rasterDurationUs": 2718, + "timestamp": "2026-10-27T01:07:27.271720", + "vsyncOverheadUs": 240, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3476, + "buildToRasterGapUs": 127, + "vsyncStartUs": 311395387710, + "buildStartUs": 311395387950, + "buildFinishUs": 311395388341, + "rasterStartUs": 311395388468, + "rasterFinishUs": 311395391186 + }, + { + "frameNumber": 758, + "uiDurationUs": 414, + "rasterDurationUs": 3776, + "timestamp": "2026-10-27T01:07:29.175348", + "vsyncOverheadUs": 371, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4747, + "buildToRasterGapUs": 186, + "vsyncStartUs": 311397289267, + "buildStartUs": 311397289638, + "buildFinishUs": 311397290052, + "rasterStartUs": 311397290238, + "rasterFinishUs": 311397294014 + }, + { + "frameNumber": 759, + "uiDurationUs": 3852, + "rasterDurationUs": 2355, + "timestamp": "2026-10-27T01:07:29.175375", + "vsyncOverheadUs": 498, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 6889, + "buildToRasterGapUs": 184, + "vsyncStartUs": 311397305947, + "buildStartUs": 311397306445, + "buildFinishUs": 311397310297, + "rasterStartUs": 311397310481, + "rasterFinishUs": 311397312836 + }, + { + "frameNumber": 760, + "uiDurationUs": 370, + "rasterDurationUs": 2595, + "timestamp": "2026-10-27T01:07:29.289849", + "vsyncOverheadUs": 245, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3337, + "buildToRasterGapUs": 127, + "vsyncStartUs": 311397405433, + "buildStartUs": 311397405678, + "buildFinishUs": 311397406048, + "rasterStartUs": 311397406175, + "rasterFinishUs": 311397408770 + }, + { + "frameNumber": 761, + "uiDurationUs": 390, + "rasterDurationUs": 3597, + "timestamp": "2026-10-27T01:07:31.192310", + "vsyncOverheadUs": 309, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4473, + "buildToRasterGapUs": 177, + "vsyncStartUs": 311399306935, + "buildStartUs": 311399307244, + "buildFinishUs": 311399307634, + "rasterStartUs": 311399307811, + "rasterFinishUs": 311399311408 + }, + { + "frameNumber": 762, + "uiDurationUs": 3951, + "rasterDurationUs": 2372, + "timestamp": "2026-10-27T01:07:31.192336", + "vsyncOverheadUs": 443, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 6946, + "buildToRasterGapUs": 180, + "vsyncStartUs": 311399323615, + "buildStartUs": 311399324058, + "buildFinishUs": 311399328009, + "rasterStartUs": 311399328189, + "rasterFinishUs": 311399330561 + }, + { + "frameNumber": 763, + "uiDurationUs": 384, + "rasterDurationUs": 2677, + "timestamp": "2026-10-27T01:07:31.307596", + "vsyncOverheadUs": 271, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3476, + "buildToRasterGapUs": 144, + "vsyncStartUs": 311399422984, + "buildStartUs": 311399423255, + "buildFinishUs": 311399423639, + "rasterStartUs": 311399423783, + "rasterFinishUs": 311399426460 + }, + { + "frameNumber": 764, + "uiDurationUs": 243, + "rasterDurationUs": 2080, + "timestamp": "2026-10-27T01:07:33.207866", + "vsyncOverheadUs": 208, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2658, + "buildToRasterGapUs": 127, + "vsyncStartUs": 311401324003, + "buildStartUs": 311401324211, + "buildFinishUs": 311401324454, + "rasterStartUs": 311401324581, + "rasterFinishUs": 311401326661 + }, + { + "frameNumber": 765, + "uiDurationUs": 3968, + "rasterDurationUs": 3938, + "timestamp": "2026-10-27T01:07:33.207889", + "vsyncOverheadUs": 422, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 8505, + "buildToRasterGapUs": 177, + "vsyncStartUs": 311401340679, + "buildStartUs": 311401341101, + "buildFinishUs": 311401345069, + "rasterStartUs": 311401345246, + "rasterFinishUs": 311401349184 + }, + { + "frameNumber": 766, + "uiDurationUs": 382, + "rasterDurationUs": 2591, + "timestamp": "2026-10-27T01:07:33.325189", + "vsyncOverheadUs": 213, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3320, + "buildToRasterGapUs": 134, + "vsyncStartUs": 311401440734, + "buildStartUs": 311401440947, + "buildFinishUs": 311401441329, + "rasterStartUs": 311401441463, + "rasterFinishUs": 311401444054 + }, + { + "frameNumber": 767, + "uiDurationUs": 398, + "rasterDurationUs": 3668, + "timestamp": "2026-10-27T01:07:35.227809", + "vsyncOverheadUs": 254, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4512, + "buildToRasterGapUs": 192, + "vsyncStartUs": 311403341998, + "buildStartUs": 311403342252, + "buildFinishUs": 311403342650, + "rasterStartUs": 311403342842, + "rasterFinishUs": 311403346510 + }, + { + "frameNumber": 768, + "uiDurationUs": 3813, + "rasterDurationUs": 3177, + "timestamp": "2026-10-27T01:07:35.227840", + "vsyncOverheadUs": 416, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 7595, + "buildToRasterGapUs": 189, + "vsyncStartUs": 311403358676, + "buildStartUs": 311403359092, + "buildFinishUs": 311403362905, + "rasterStartUs": 311403363094, + "rasterFinishUs": 311403366271 + }, + { + "frameNumber": 769, + "uiDurationUs": 420, + "rasterDurationUs": 2670, + "timestamp": "2026-10-27T01:07:35.343301", + "vsyncOverheadUs": 288, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3505, + "buildToRasterGapUs": 127, + "vsyncStartUs": 311403458550, + "buildStartUs": 311403458838, + "buildFinishUs": 311403459258, + "rasterStartUs": 311403459385, + "rasterFinishUs": 311403462055 + }, + { + "frameNumber": 770, + "uiDurationUs": 367, + "rasterDurationUs": 3363, + "timestamp": "2026-10-27T01:07:37.245356", + "vsyncOverheadUs": 312, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4220, + "buildToRasterGapUs": 178, + "vsyncStartUs": 311405359718, + "buildStartUs": 311405360030, + "buildFinishUs": 311405360397, + "rasterStartUs": 311405360575, + "rasterFinishUs": 311405363938 + }, + { + "frameNumber": 771, + "uiDurationUs": 2777, + "rasterDurationUs": 2402, + "timestamp": "2026-10-27T01:07:37.245388", + "vsyncOverheadUs": 310, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 5625, + "buildToRasterGapUs": 136, + "vsyncStartUs": 311405376395, + "buildStartUs": 311405376705, + "buildFinishUs": 311405379482, + "rasterStartUs": 311405379618, + "rasterFinishUs": 311405382020 + }, + { + "frameNumber": 772, + "uiDurationUs": 308, + "rasterDurationUs": 3337, + "timestamp": "2026-10-27T01:07:37.361291", + "vsyncOverheadUs": 352, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4123, + "buildToRasterGapUs": 126, + "vsyncStartUs": 311405476175, + "buildStartUs": 311405476527, + "buildFinishUs": 311405476835, + "rasterStartUs": 311405476961, + "rasterFinishUs": 311405480298 + }, + { + "frameNumber": 773, + "uiDurationUs": 2055, + "rasterDurationUs": 1773, + "timestamp": "2026-10-27T01:07:37.361316", + "vsyncOverheadUs": 307, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4239, + "buildToRasterGapUs": 104, + "vsyncStartUs": 311405559548, + "buildStartUs": 311405559855, + "buildFinishUs": 311405561910, + "rasterStartUs": 311405562014, + "rasterFinishUs": 311405563787 + }, + { + "frameNumber": 774, + "uiDurationUs": 237, + "rasterDurationUs": 1658, + "timestamp": "2026-10-27T01:07:37.754533", + "vsyncOverheadUs": 179, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2210, + "buildToRasterGapUs": 136, + "vsyncStartUs": 311405642921, + "buildStartUs": 311405643100, + "buildFinishUs": 311405643337, + "rasterStartUs": 311405643473, + "rasterFinishUs": 311405645131 + }, + { + "frameNumber": 775, + "uiDurationUs": 1170, + "rasterDurationUs": 1526, + "timestamp": "2026-10-27T01:07:37.754539", + "vsyncOverheadUs": 398, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3234, + "buildToRasterGapUs": 140, + "vsyncStartUs": 311405659596, + "buildStartUs": 311405659994, + "buildFinishUs": 311405661164, + "rasterStartUs": 311405661304, + "rasterFinishUs": 311405662830 + }, + { + "frameNumber": 776, + "uiDurationUs": 770, + "rasterDurationUs": 1451, + "timestamp": "2026-10-27T01:07:37.754539", + "vsyncOverheadUs": 387, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2739, + "buildToRasterGapUs": 131, + "vsyncStartUs": 311405676270, + "buildStartUs": 311405676657, + "buildFinishUs": 311405677427, + "rasterStartUs": 311405677558, + "rasterFinishUs": 311405679009 + }, + { + "frameNumber": 777, + "uiDurationUs": 906, + "rasterDurationUs": 1586, + "timestamp": "2026-10-27T01:07:37.754540", + "vsyncOverheadUs": 390, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3015, + "buildToRasterGapUs": 133, + "vsyncStartUs": 311405692945, + "buildStartUs": 311405693335, + "buildFinishUs": 311405694241, + "rasterStartUs": 311405694374, + "rasterFinishUs": 311405695960 + }, + { + "frameNumber": 778, + "uiDurationUs": 264294, + "rasterDurationUs": 409, + "timestamp": "2026-10-27T01:07:37.855613", + "vsyncOverheadUs": 424, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 265186, + "buildToRasterGapUs": 59, + "vsyncStartUs": 311405709620, + "buildStartUs": 311405710044, + "buildFinishUs": 311405974338, + "rasterStartUs": 311405974397, + "rasterFinishUs": 311405974806 + }, + { + "frameNumber": 779, + "uiDurationUs": 1325, + "rasterDurationUs": 312, + "timestamp": "2026-10-27T01:07:37.855621", + "vsyncOverheadUs": 1694, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3373, + "buildToRasterGapUs": 42, + "vsyncStartUs": 311405975499, + "buildStartUs": 311405977193, + "buildFinishUs": 311405978518, + "rasterStartUs": 311405978560, + "rasterFinishUs": 311405978872, + "ownStartDelayUs": 1694 + }, + { + "frameNumber": 780, + "uiDurationUs": 646, + "rasterDurationUs": 589, + "timestamp": "2026-10-27T01:07:37.855624", + "vsyncOverheadUs": 293, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1647, + "buildToRasterGapUs": 119, + "vsyncStartUs": 311405993088, + "buildStartUs": 311405993381, + "buildFinishUs": 311405994027, + "rasterStartUs": 311405994146, + "rasterFinishUs": 311405994735 + }, + { + "frameNumber": 781, + "uiDurationUs": 374, + "rasterDurationUs": 347, + "timestamp": "2026-10-27T01:07:37.855625", + "vsyncOverheadUs": 257, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1081, + "buildToRasterGapUs": 103, + "vsyncStartUs": 311406009763, + "buildStartUs": 311406010020, + "buildFinishUs": 311406010394, + "rasterStartUs": 311406010497, + "rasterFinishUs": 311406010844 + }, + { + "frameNumber": 782, + "uiDurationUs": 209, + "rasterDurationUs": 244, + "timestamp": "2026-10-27T01:07:37.855626", + "vsyncOverheadUs": 197, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 691, + "buildToRasterGapUs": 41, + "vsyncStartUs": 311406026437, + "buildStartUs": 311406026634, + "buildFinishUs": 311406026843, + "rasterStartUs": 311406026884, + "rasterFinishUs": 311406027128 + }, + { + "frameNumber": 783, + "uiDurationUs": 174, + "rasterDurationUs": 275, + "timestamp": "2026-10-27T01:07:37.855627", + "vsyncOverheadUs": 216, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 710, + "buildToRasterGapUs": 45, + "vsyncStartUs": 311406043112, + "buildStartUs": 311406043328, + "buildFinishUs": 311406043502, + "rasterStartUs": 311406043547, + "rasterFinishUs": 311406043822 + }, + { + "frameNumber": 784, + "uiDurationUs": 159, + "rasterDurationUs": 270, + "timestamp": "2026-10-27T01:07:37.855628", + "vsyncOverheadUs": 241, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 695, + "buildToRasterGapUs": 25, + "vsyncStartUs": 311406059787, + "buildStartUs": 311406060028, + "buildFinishUs": 311406060187, + "rasterStartUs": 311406060212, + "rasterFinishUs": 311406060482 + }, + { + "frameNumber": 785, + "uiDurationUs": 182, + "rasterDurationUs": 298, + "timestamp": "2026-10-27T01:07:37.958017", + "vsyncOverheadUs": 284, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 797, + "buildToRasterGapUs": 33, + "vsyncStartUs": 311406076461, + "buildStartUs": 311406076745, + "buildFinishUs": 311406076927, + "rasterStartUs": 311406076960, + "rasterFinishUs": 311406077258 + }, + { + "frameNumber": 786, + "uiDurationUs": 225, + "rasterDurationUs": 368, + "timestamp": "2026-10-27T01:07:37.958030", + "vsyncOverheadUs": 287, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 963, + "buildToRasterGapUs": 83, + "vsyncStartUs": 311406093136, + "buildStartUs": 311406093423, + "buildFinishUs": 311406093648, + "rasterStartUs": 311406093731, + "rasterFinishUs": 311406094099 + }, + { + "frameNumber": 787, + "uiDurationUs": 243, + "rasterDurationUs": 364, + "timestamp": "2026-10-27T01:07:37.958033", + "vsyncOverheadUs": 308, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1020, + "buildToRasterGapUs": 105, + "vsyncStartUs": 311406109811, + "buildStartUs": 311406110119, + "buildFinishUs": 311406110362, + "rasterStartUs": 311406110467, + "rasterFinishUs": 311406110831 + }, + { + "frameNumber": 788, + "uiDurationUs": 260, + "rasterDurationUs": 436, + "timestamp": "2026-10-27T01:07:37.958035", + "vsyncOverheadUs": 160, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 960, + "buildToRasterGapUs": 104, + "vsyncStartUs": 311406126634, + "buildStartUs": 311406126794, + "buildFinishUs": 311406127054, + "rasterStartUs": 311406127158, + "rasterFinishUs": 311406127594 + }, + { + "frameNumber": 789, + "uiDurationUs": 248, + "rasterDurationUs": 431, + "timestamp": "2026-10-27T01:07:37.958037", + "vsyncOverheadUs": 192, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 977, + "buildToRasterGapUs": 106, + "vsyncStartUs": 311406143316, + "buildStartUs": 311406143508, + "buildFinishUs": 311406143756, + "rasterStartUs": 311406143862, + "rasterFinishUs": 311406144293 + }, + { + "frameNumber": 790, + "uiDurationUs": 342, + "rasterDurationUs": 646, + "timestamp": "2026-10-27T01:07:37.958039", + "vsyncOverheadUs": 177, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1222, + "buildToRasterGapUs": 57, + "vsyncStartUs": 311406159998, + "buildStartUs": 311406160175, + "buildFinishUs": 311406160517, + "rasterStartUs": 311406160574, + "rasterFinishUs": 311406161220 + }, + { + "frameNumber": 791, + "uiDurationUs": 996, + "rasterDurationUs": 1009, + "timestamp": "2026-10-27T01:07:38.060074", + "vsyncOverheadUs": 292, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2451, + "buildToRasterGapUs": 154, + "vsyncStartUs": 311406176679, + "buildStartUs": 311406176971, + "buildFinishUs": 311406177967, + "rasterStartUs": 311406178121, + "rasterFinishUs": 311406179130 + }, + { + "frameNumber": 792, + "uiDurationUs": 424, + "rasterDurationUs": 1191, + "timestamp": "2026-10-27T01:07:38.060085", + "vsyncOverheadUs": 388, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2117, + "buildToRasterGapUs": 114, + "vsyncStartUs": 311406193361, + "buildStartUs": 311406193749, + "buildFinishUs": 311406194173, + "rasterStartUs": 311406194287, + "rasterFinishUs": 311406195478 + }, + { + "frameNumber": 793, + "uiDurationUs": 425, + "rasterDurationUs": 706, + "timestamp": "2026-10-27T01:07:38.060088", + "vsyncOverheadUs": 181, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1365, + "buildToRasterGapUs": 53, + "vsyncStartUs": 311406243406, + "buildStartUs": 311406243587, + "buildFinishUs": 311406244012, + "rasterStartUs": 311406244065, + "rasterFinishUs": 311406244771 + }, + { + "frameNumber": 794, + "uiDurationUs": 555, + "rasterDurationUs": 1321, + "timestamp": "2026-10-27T01:07:38.643834", + "vsyncOverheadUs": 224, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2173, + "buildToRasterGapUs": 73, + "vsyncStartUs": 311406760549, + "buildStartUs": 311406760773, + "buildFinishUs": 311406761328, + "rasterStartUs": 311406761401, + "rasterFinishUs": 311406762722 + }, + { + "frameNumber": 795, + "uiDurationUs": 499, + "rasterDurationUs": 2442, + "timestamp": "2026-10-27T01:07:38.761897", + "vsyncOverheadUs": 294, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3405, + "buildToRasterGapUs": 170, + "vsyncStartUs": 311406877326, + "buildStartUs": 311406877620, + "buildFinishUs": 311406878119, + "rasterStartUs": 311406878289, + "rasterFinishUs": 311406880731 + }, + { + "frameNumber": 796, + "uiDurationUs": 479, + "rasterDurationUs": 1460, + "timestamp": "2026-10-27T01:07:39.110990", + "vsyncOverheadUs": 144, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2201, + "buildToRasterGapUs": 118, + "vsyncStartUs": 311407227653, + "buildStartUs": 311407227797, + "buildFinishUs": 311407228276, + "rasterStartUs": 311407228394, + "rasterFinishUs": 311407229854 + }, + { + "frameNumber": 797, + "uiDurationUs": 343, + "rasterDurationUs": 1420, + "timestamp": "2026-10-27T01:07:39.226746", + "vsyncOverheadUs": 158, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1979, + "buildToRasterGapUs": 58, + "vsyncStartUs": 311407344435, + "buildStartUs": 311407344593, + "buildFinishUs": 311407344936, + "rasterStartUs": 311407344994, + "rasterFinishUs": 311407346414 + }, + { + "frameNumber": 798, + "uiDurationUs": 404, + "rasterDurationUs": 2330, + "timestamp": "2026-10-27T01:07:39.226764", + "vsyncOverheadUs": 323, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3226, + "buildToRasterGapUs": 169, + "vsyncStartUs": 311407377801, + "buildStartUs": 311407378124, + "buildFinishUs": 311407378528, + "rasterStartUs": 311407378697, + "rasterFinishUs": 311407381027 + }, + { + "frameNumber": 799, + "uiDurationUs": 795, + "rasterDurationUs": 2235, + "timestamp": "2026-10-27T01:07:39.226769", + "vsyncOverheadUs": 460, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3664, + "buildToRasterGapUs": 174, + "vsyncStartUs": 311407394484, + "buildStartUs": 311407394944, + "buildFinishUs": 311407395739, + "rasterStartUs": 311407395913, + "rasterFinishUs": 311407398148 + }, + { + "frameNumber": 800, + "uiDurationUs": 963, + "rasterDurationUs": 1786, + "timestamp": "2026-10-27T01:07:39.629039", + "vsyncOverheadUs": 256, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3160, + "buildToRasterGapUs": 155, + "vsyncStartUs": 311407744844, + "buildStartUs": 311407745100, + "buildFinishUs": 311407746063, + "rasterStartUs": 311407746218, + "rasterFinishUs": 311407748004 + }, + { + "frameNumber": 801, + "uiDurationUs": 517, + "rasterDurationUs": 2447, + "timestamp": "2026-10-27T01:07:39.746019", + "vsyncOverheadUs": 180, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3316, + "buildToRasterGapUs": 172, + "vsyncStartUs": 311407861651, + "buildStartUs": 311407861831, + "buildFinishUs": 311407862348, + "rasterStartUs": 311407862520, + "rasterFinishUs": 311407864967 + }, + { + "frameNumber": 802, + "uiDurationUs": 510, + "rasterDurationUs": 1742, + "timestamp": "2026-10-27T01:07:40.128966", + "vsyncOverheadUs": 236, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2582, + "buildToRasterGapUs": 94, + "vsyncStartUs": 311408245211, + "buildStartUs": 311408245447, + "buildFinishUs": 311408245957, + "rasterStartUs": 311408246051, + "rasterFinishUs": 311408247793 + }, + { + "frameNumber": 803, + "uiDurationUs": 475, + "rasterDurationUs": 2413, + "timestamp": "2026-10-27T01:07:40.245409", + "vsyncOverheadUs": 253, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3311, + "buildToRasterGapUs": 170, + "vsyncStartUs": 311408361976, + "buildStartUs": 311408362229, + "buildFinishUs": 311408362704, + "rasterStartUs": 311408362874, + "rasterFinishUs": 311408365287 + }, + { + "frameNumber": 804, + "uiDurationUs": 286, + "rasterDurationUs": 2094, + "timestamp": "2026-10-27T01:07:40.245429", + "vsyncOverheadUs": 281, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2819, + "buildToRasterGapUs": 158, + "vsyncStartUs": 311408395338, + "buildStartUs": 311408395619, + "buildFinishUs": 311408395905, + "rasterStartUs": 311408396063, + "rasterFinishUs": 311408398157 + }, + { + "frameNumber": 805, + "uiDurationUs": 772, + "rasterDurationUs": 2421, + "timestamp": "2026-10-27T01:07:40.245434", + "vsyncOverheadUs": 483, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3782, + "buildToRasterGapUs": 106, + "vsyncStartUs": 311408412019, + "buildStartUs": 311408412502, + "buildFinishUs": 311408413274, + "rasterStartUs": 311408413380, + "rasterFinishUs": 311408415801 + }, + { + "frameNumber": 806, + "uiDurationUs": 392, + "rasterDurationUs": 2395, + "timestamp": "2026-10-27T01:07:41.297181", + "vsyncOverheadUs": 289, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3251, + "buildToRasterGapUs": 175, + "vsyncStartUs": 311409412880, + "buildStartUs": 311409413169, + "buildFinishUs": 311409413561, + "rasterStartUs": 311409413736, + "rasterFinishUs": 311409416131 + }, + { + "frameNumber": 807, + "uiDurationUs": 527, + "rasterDurationUs": 2421, + "timestamp": "2026-10-27T01:07:41.414191", + "vsyncOverheadUs": 267, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3384, + "buildToRasterGapUs": 169, + "vsyncStartUs": 311409529652, + "buildStartUs": 311409529919, + "buildFinishUs": 311409530446, + "rasterStartUs": 311409530615, + "rasterFinishUs": 311409533036 + }, + { + "frameNumber": 808, + "uiDurationUs": 507, + "rasterDurationUs": 1753, + "timestamp": "2026-10-27T01:07:41.613415", + "vsyncOverheadUs": 135, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 2526, + "buildToRasterGapUs": 131, + "vsyncStartUs": 311409729819, + "buildStartUs": 311409729954, + "buildFinishUs": 311409730461, + "rasterStartUs": 311409730592, + "rasterFinishUs": 311409732345 + }, + { + "frameNumber": 809, + "uiDurationUs": 458, + "rasterDurationUs": 2460, + "timestamp": "2026-10-27T01:07:41.731158", + "vsyncOverheadUs": 286, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3368, + "buildToRasterGapUs": 164, + "vsyncStartUs": 311409846622, + "buildStartUs": 311409846908, + "buildFinishUs": 311409847366, + "rasterStartUs": 311409847530, + "rasterFinishUs": 311409849990 + }, + { + "frameNumber": 810, + "uiDurationUs": 326, + "rasterDurationUs": 2365, + "timestamp": "2026-10-27T01:07:42.224833", + "vsyncOverheadUs": 265, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3124, + "buildToRasterGapUs": 168, + "vsyncStartUs": 311410130484, + "buildStartUs": 311410130749, + "buildFinishUs": 311410131075, + "rasterStartUs": 311410131243, + "rasterFinishUs": 311410133608 + }, + { + "frameNumber": 811, + "uiDurationUs": 1114, + "rasterDurationUs": 1506, + "timestamp": "2026-10-27T01:07:42.224839", + "vsyncOverheadUs": 375, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3290, + "buildToRasterGapUs": 295, + "vsyncStartUs": 311410147178, + "buildStartUs": 311410147553, + "buildFinishUs": 311410148667, + "rasterStartUs": 311410148962, + "rasterFinishUs": 311410150468 + }, + { + "frameNumber": 812, + "uiDurationUs": 957, + "rasterDurationUs": 2181, + "timestamp": "2026-10-27T01:07:42.224840", + "vsyncOverheadUs": 445, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 3780, + "buildToRasterGapUs": 197, + "vsyncStartUs": 311410163873, + "buildStartUs": 311410164318, + "buildFinishUs": 311410165275, + "rasterStartUs": 311410165472, + "rasterFinishUs": 311410167653 + }, + { + "frameNumber": 813, + "uiDurationUs": 1167, + "rasterDurationUs": 2278, + "timestamp": "2026-10-27T01:07:42.224841", + "vsyncOverheadUs": 597, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 4196, + "buildToRasterGapUs": 154, + "vsyncStartUs": 311410180567, + "buildStartUs": 311410181164, + "buildFinishUs": 311410182331, + "rasterStartUs": 311410182485, + "rasterFinishUs": 311410184763 + }, + { + "frameNumber": 814, + "uiDurationUs": 247105, + "rasterDurationUs": 359, + "timestamp": "2026-10-27T01:07:42.326089", + "vsyncOverheadUs": 424, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 247998, + "buildToRasterGapUs": 110, + "vsyncStartUs": 311410197261, + "buildStartUs": 311410197685, + "buildFinishUs": 311410444790, + "rasterStartUs": 311410444900, + "rasterFinishUs": 311410445259 + }, + { + "frameNumber": 815, + "uiDurationUs": 1630, + "rasterDurationUs": 373, + "timestamp": "2026-10-27T01:07:42.326096", + "vsyncOverheadUs": 8445, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 10498, + "buildToRasterGapUs": 50, + "vsyncStartUs": 311410445019, + "buildStartUs": 311410453464, + "buildFinishUs": 311410455094, + "rasterStartUs": 311410455144, + "rasterFinishUs": 311410455517, + "ownStartDelayUs": 7597 + }, + { + "frameNumber": 816, + "uiDurationUs": 393, + "rasterDurationUs": 363, + "timestamp": "2026-10-27T01:07:42.326102", + "vsyncOverheadUs": 214, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 1070, + "buildToRasterGapUs": 100, + "vsyncStartUs": 311410464371, + "buildStartUs": 311410464585, + "buildFinishUs": 311410464978, + "rasterStartUs": 311410465078, + "rasterFinishUs": 311410465441 + }, + { + "frameNumber": 817, + "uiDurationUs": 357, + "rasterDurationUs": 344, + "timestamp": "2026-10-27T01:07:42.326103", + "vsyncOverheadUs": 131, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 928, + "buildToRasterGapUs": 96, + "vsyncStartUs": 311410481065, + "buildStartUs": 311410481196, + "buildFinishUs": 311410481553, + "rasterStartUs": 311410481649, + "rasterFinishUs": 311410481993 + }, + { + "frameNumber": 818, + "uiDurationUs": 158, + "rasterDurationUs": 230, + "timestamp": "2026-10-27T01:07:42.326104", + "vsyncOverheadUs": 152, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 639, + "buildToRasterGapUs": 99, + "vsyncStartUs": 311410497759, + "buildStartUs": 311410497911, + "buildFinishUs": 311410498069, + "rasterStartUs": 311410498168, + "rasterFinishUs": 311410498398 + }, + { + "frameNumber": 819, + "uiDurationUs": 150, + "rasterDurationUs": 235, + "timestamp": "2026-10-27T01:07:42.326105", + "vsyncOverheadUs": 134, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 619, + "buildToRasterGapUs": 100, + "vsyncStartUs": 311410514454, + "buildStartUs": 311410514588, + "buildFinishUs": 311410514738, + "rasterStartUs": 311410514838, + "rasterFinishUs": 311410515073 + }, + { + "frameNumber": 820, + "uiDurationUs": 175, + "rasterDurationUs": 257, + "timestamp": "2026-10-27T01:07:42.326107", + "vsyncOverheadUs": 224, + "layerCacheCount": 0, + "layerCacheBytes": 0, + "pictureCacheCount": 0, + "pictureCacheBytes": 0, + "frameBudgetMs": 16, + "frameBudgetUs": 16667, + "totalSpanUs": 755, + "buildToRasterGapUs": 99, + "vsyncStartUs": 311410531148, + "buildStartUs": 311410531372, + "buildFinishUs": 311410531547, + "rasterStartUs": 311410531646, + "rasterFinishUs": 311410531903 + } + ], + "recurrenceTrends": { + "heavy_compute": { + "trend": "stable", + "totalOccurrences": 22, + "totalObserved": 60, + "lastSeenCycle": 66, + "severityStats": { + "min": 2, + "max": 3 + } + }, + "always_repaint_painter": { + "trend": "stable", + "totalOccurrences": 6, + "totalObserved": 50, + "lastSeenCycle": 66, + "severityStats": { + "min": 2, + "max": 2 + } + }, + "missing_repaint_boundary": { + "trend": "stable", + "totalOccurrences": 6, + "totalObserved": 50, + "lastSeenCycle": 66, + "severityStats": { + "min": 2, + "max": 2 + } + }, + "event_loop_stall": { + "trend": "stable", + "totalOccurrences": 17, + "totalObserved": 39, + "lastSeenCycle": 66, + "severityStats": { + "min": 2, + "max": 2 + } + }, + "rebuild_activity": { + "trend": "intermittent", + "totalOccurrences": 5, + "totalObserved": 39, + "lastSeenCycle": 66, + "severityStats": { + "min": 2, + "max": 2 + } + }, + "heap_growing": { + "trend": "stable", + "totalOccurrences": 11, + "totalObserved": 29, + "lastSeenCycle": 66, + "severityStats": { + "min": 2, + "max": 2 + } + }, + "sustained_jank": { + "trend": "stable", + "totalOccurrences": 4, + "totalObserved": 4, + "lastSeenCycle": 66, + "severityStats": { + "min": 3, + "max": 3 + } + } + }, + "sessionSummary": { + "topIssues": [ + { + "stableId": "heavy_compute", + "title": "Heavy Computation: 246.3ms", + "severity": "critical", + "confidence": "confirmed", + "confidenceReason": "Measured directly from VM timeline long UI-thread event", + "rankingScore": 618 + }, + { + "stableId": "sustained_jank", + "title": "Sustained Jank (UI-bound): 3 severe frames (1% janky)", + "severity": "critical", + "confidence": "confirmed", + "confidenceReason": "Measured directly from FrameTiming API", + "rankingScore": 616 + }, + { + "stableId": "event_loop_stall", + "title": "Event Loop Stalls: 2, longest at least 260 ms", + "severity": "warning", + "confidence": "confirmed", + "confidenceReason": "The VM reported a timer that fired at least 100 ms late", + "rankingScore": 418 + }, + { + "stableId": "rebuild_activity", + "title": "Rebuild Activity: build phase 14.4% of UI time", + "severity": "warning", + "confidence": "confirmed", + "confidenceReason": "Measured directly from VM timeline BUILD durations", + "rankingScore": 418 + } + ], + "causalEdges": [ + { + "cause": "rebuild_activity", + "effect": "heavy_compute" + }, + { + "cause": "event_loop_stall", + "effect": "sustained_jank" + } + ], + "frameHistogram": { + "<16ms": 234, + "16-33ms": 2, + "33-50ms": 0, + "50-100ms": 0, + ">100ms": 4 + }, + "detectorHitRates": { + "heavyCompute": 1, + "frameTiming": 1, + "eventLoopStall": 1, + "rebuild": 1 + }, + "memoryTrendSummary": { + "startBytes": 55512112, + "endBytes": 81345504, + "peakBytes": 88246448, + "growthRatePerSec": 890806.6, + "sampleCount": 60 + } + }, + "startupMetrics": { + "dartEntryTimestamp": "2026-10-27T01:06:13.918418", + "ttffMs": 79.5, + "firstFrameVsyncOverheadMs": 0.48, + "firstFrameBuildMs": 0.03, + "firstFrameRasterMs": 9.54, + "firstFrameTotalMs": 10.09, + "dartEntryMonotonicUs": 311322138726, + "frameworkInitDurationUs": 2518, + "engineEnterUs": 311322052223, + "firstFrameRasterizedUs": 311322218614, + "frameworkInitMs": 2.52, + "preDartOverheadMs": 86.5, + "engineTtffMs": 166.39, + "dominantPhase": "raster", + "dominantPhasePercent": 94.5 + }, + "routeSessions": [ + { + "routeName": "/", + "scaffoldHashKey": 29725821, + "tabVisitIndex": 1, + "startedAt": "2026-10-27T01:06:15.012325", + "endedAt": "2026-10-27T01:06:37.305578", + "healthScore": 97, + "durationSeconds": 22, + "scanCycles": 16, + "frameStats": { + "totalFrames": 55, + "jankFrames": 0, + "averageFps": 60.0, + "p50": 60.0, + "p95": 60.0, + "p99": 60.0 + }, + "issueCount": 1, + "criticalCount": 0, + "warningCount": 1, + "issues": [ + "heavy_compute" + ] + }, + { + "routeName": "/demo/Always-Repaint CustomPainter", + "scaffoldHashKey": 723446902, + "tabVisitIndex": 1, + "startedAt": "2026-10-27T01:06:37.305584", + "endedAt": "2026-10-27T01:06:44.429523", + "healthScore": 94, + "durationSeconds": 7, + "scanCycles": 6, + "frameStats": { + "totalFrames": 59, + "jankFrames": 0, + "averageFps": 60.0, + "p50": 60.0, + "p95": 60.0, + "p99": 60.0 + }, + "issueCount": 2, + "criticalCount": 0, + "warningCount": 2, + "issues": [ + "always_repaint_painter", + "missing_repaint_boundary" + ] + }, + { + "routeName": "/", + "scaffoldHashKey": 29725821, + "tabVisitIndex": 2, + "startedAt": "2026-10-27T01:06:44.429530", + "endedAt": "2026-10-27T01:06:47.481604", + "healthScore": 100, + "durationSeconds": 3, + "scanCycles": 2, + "frameStats": { + "totalFrames": 10, + "jankFrames": 0, + "averageFps": 60.0, + "p50": 60.0, + "p95": 60.0, + "p99": 60.0 + }, + "issueCount": 0, + "criticalCount": 0, + "warningCount": 0, + "issues": [] + }, + { + "routeName": "/demo/CSV Import", + "scaffoldHashKey": 262330548, + "tabVisitIndex": 1, + "startedAt": "2026-10-27T01:06:47.481610", + "healthScore": 71, + "durationSeconds": 54, + "scanCycles": 42, + "frameStats": { + "totalFrames": 120, + "jankFrames": 0, + "averageFps": 60.0, + "p50": 60.0, + "p95": 60.0, + "p99": 60.0 + }, + "issueCount": 5, + "criticalCount": 2, + "warningCount": 3, + "issues": [ + "heavy_compute", + "event_loop_stall", + "rebuild_activity", + "heap_growing", + "sustained_jank" + ] + } + ] +} \ No newline at end of file