Skip to content

Overlay data for the 1.0.0 card - #22

Merged
Harrys76 merged 6 commits into
mainfrom
wp27/overlay-data
Oct 26, 2026
Merged

Harrys76 merged 6 commits into
mainfrom
wp27/overlay-data

Conversation

@Harrys76

@Harrys76 Harrys76 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Second PR of the 1.0.0 overlay stack, on top of #21. It adds the data the redesigned card and status lines will read. The overlay's look does not change yet, and no detection rule changes.

What changes

  • JANK tag by phase. FrameVerdict.relatedIssues keeps only the issues that can explain the verdict's phase: the issues of that category, event_loop_stall for a start delay, none for a stall or an unknown phase. Jank symptoms are never listed. The card shows the tag only while jank_detected or sustained_jank is in the reported list, so the tag leaves when the jank does, even though the last verdict stays. Before, every card matched because the verdict listed every issue.
  • Coverage and run state (internal). CoverageReport counts which checks run, need the VM, run in part, or need an opt-in. Default config: 11 on, 6 need the VM and 3 run in part without it; 19 on and 2 opt-ins off with it. runStateNotifier moves from starting to warming up to running and carries the reconnect attempt (n of 7) and "stopped retrying". The debug banner dismissal now lives in the controller for the session.
  • Highlight lookup. selectHighlightForIssue returns found, not found or pending. It matches the issue's own element first, through the new WidgetHighlight.occurrenceId that the detectors stamping PerformanceIssue.occurrenceId now also stamp, then the widget name. The same-category fallback is gone, so a card never outlines another widget. Pending happens only before the first scan and resolves after it. A not-found card unticks, and no outline appears later.
  • Not useful marks. A session store in the controller (cap 200, oldest evicted), NotUsefulMark, and an always-on notUsefulMarks envelope key on the snapshot and ext.sleuth.issues, written only when there is a mark. It is not a SnapshotSection, so 0.38 apps and older sidecars are unaffected. doc/mcp_schema.{json,md} (and the sidecar's byte-equal copy) document it and the issue measurement shape. The audit has presence tests driven by a real PlatformChannelDetector emission. The sidecar's snapshotMetadataKeys includes the key.
  • Why it rebuilds. DebugSnapshot.forcedRebuildsOf maps each type a parent's build rebuilt to the types whose builds forced it, framework widgets included (debug callbacks only). It caps at 8 forcing types per type and sets forcedTypesCapped. forcedRebuildsByRoot keeps its content.
  • Leaf location and copy cost. leafSourceLocation reads the leaf file:line of an ancestor chain. Copying the snapshot is logged as Sleuth's own work, so it is left out of the next frame's start delay, and a second tap during a copy is ignored.
  • Overlay build-cost benchmark. It pumps 20 issues, collapsed and with three expanded, through 50 unchanged ticks and 50 title changes, and holds the card builds to the counts recorded now (0/0, 0/200, 1/0, 1/100 over 50 ticks). Later card PRs must stay within them.

Different from the plan

  • The phase filter runs in RenderPipelineAnalyzer, where the phase is decided, instead of at the three controller call sites. The reason's Related: line then names an issue of the phase, too.
  • pendingIssueSelection is now read-only. The lookup's own callback replaces the old late selection.
  • Interim gap until the collapsed-card PR: a layout or paint card without a widget name still shows its highlight checkbox, and without the category fallback it reports "not visible". The card PR shows the locate control only for issues with a widget name.

Verification

  • fvm flutter analyze: 0 issues. Format check clean. fvm dart doc --dry-run: 0 warnings.
  • fvm flutter test --exclude-tags benchmark: 4,841 passed.
  • Benchmarks (--tags benchmark --concurrency=1): 44 passed (4 new).
  • Floor lane on Flutter 3.32.8: analyze clean, 4,841 passed.
  • Example app tests: 348 passed. Sidecar: analyze clean, 653 passed.
  • Detector, validation and replay tests pass with no expectation edits. The test changes cover the intended behaviour changes: verdict lists without jank symptoms, a highlight with no category fallback, the banner kept for the session.

Review

Codex reviewed the branch twice. The first round found three defects, fixed in c6ce3b9:

  • The debug rebuild hook looked up type names for every forced rebuild. The map is now keyed by Type, and the names are looked up only when a snapshot is taken. With 200 forced rebuilds a frame, frame time is within noise of the hook before the map: median 1,366 vs 1,347 µs over five interleaved runs.
  • A rescan or rect refresh moved the selected outline to the first widget with the same name. It now follows its own element, and clears when that element goes.
  • A pending lookup called without a callback never selected anything. The lookup now runs before the optional callback.

The second round approved with no findings.

Not in this PR

The UI that uses this data (status lines, the Not useful toggle, the Why it rebuilds block, the new card) comes in the next PRs of the stack. The device parity walk ran on the finished stack, on an iPhone 12 and a Galaxy A07; #28 has the results.

Fix from the iPhone walk

  • Source location per element. SourceLocationCache kept one file:line per widget type, so a type built in several places reported the first place it resolved: the Rebuild Hotspot screen's ValueListenableBuilder<int> showed high_level_setstate_demo.dart:75, in the Where row and its Copy. The cache now keys on the element (an Expando). The test that pinned type sharing is replaced by one with two call sites, which fails on the old cache.

Fix from the final review

  • No sibling outline. The first highlight lookup fell back to the widget name when the issue's own element had no highlight, for instance after a scroll dropped it. With two ListViews, the first list's crosshair outlined the second list and stayed there. An issue with an occurrence id now takes its element's highlight or none, as the re-acquire path already did. A controller test fails on the old lookup.

Jank tag hold (Galaxy follow-up)

On a Galaxy A07 the Jank tag went on and off about once a second on FPS Stress: its tag set changed 23 times in 30 s. Late frames alternate between phases, and a raster or start-delay frame names no build issue, so a tag that followed only the latest verdict blinked.

  • The controller notes when a verdict last named each issue. heldJankCauses() returns the issues named within the 10 s window jank_detected judges, and the time until the first one leaves.
  • The card tags those issues while a jank symptom is reported, and arms one timer for the next expiry.
  • The hold lives in the controller, so closing and reopening the dashboard neither drops a hold nor restarts one. A route change clears it, beside FrameTimingDetector.markRouteEpoch.
  • Same device after the fix: 4 changes in 30 s, each one when the event_loop_stall card itself came or went. The Rebuild Activity tag stayed on. iPhone 12, same demo: no changes in 30 s.
  • Tests cover the hold through a verdict of another phase, a later verdict restarting it, reopening the dashboard and the route reset. Each fails without its part.

🤖 Generated with Claude Code

Harrys76 and others added 3 commits October 19, 2026 12:58
- FrameVerdict.relatedIssues keeps only the issues that can explain the
  verdict's phase: the phase's category, event_loop_stall for a start
  delay, none for a stall. Jank symptoms are never listed, and the JANK
  tag shows only while jank_detected or sustained_jank is reported.
- An internal coverage report and run state for the coming status
  lines: which checks run, need the VM or an opt-in, the warm-up phase,
  and the reconnect attempt. The debug banner dismissal moves to the
  controller for the session.
- The highlight lookup returns found, not found or pending. It matches
  the issue's own element first (WidgetHighlight.occurrenceId), then the
  widget name, and no longer outlines another widget of the same
  category. A not-found card unticks instead of outlining later.
- Not useful marks: a session store in the controller, NotUsefulMark,
  and an always-on notUsefulMarks key on the snapshot and
  ext.sleuth.issues. The schema documents it and the issue measurement
  shape, with presence tests driven by a real detector emission. The
  sidecar lists the key in snapshotMetadataKeys.
- DebugSnapshot.forcedRebuildsOf records which widget's build forced
  each parent-driven rebuild, framework widgets included.
- leafSourceLocation reads the leaf file:line of an ancestor chain. The
  snapshot copy counts as Sleuth's own work and ignores a second tap.

No detection decision changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The overlay builds inside the app's frames, outside Sleuth's own-work
log, so extra card builds raise the app's rebuild_activity share. The
benchmark pumps 20 issues, collapsed and with three expanded, through 50
scan ticks without a change and 50 with a title change, and fails when
FloatingIssuesCard or IssueCard builds more often than the counts
recorded before the card redesign. Build time is printed, not asserted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The selected highlight follows its own element when the scan or a
  rect refresh republishes highlights. Before, it moved to the first
  widget of the same name and detector, and to a sibling when its
  element left; now it clears instead.
- A pending lookup selects after the first scan even without a result
  callback. The null-aware call skipped the lookup itself.
- The debug rebuild hook keys the forced-rebuild map by Type and names
  the types once per snapshot. With 200 forced rebuilds a frame, frame
  time is within measurement noise of the hook before the map (median
  1,366 vs 1,347 us over five interleaved runs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SourceLocationCache kept one location per widget type, so a type built
in several places reported the first place it resolved. On an iPhone
the Rebuild Hotspot screen's ValueListenableBuilder<int> showed
high_level_setstate_demo.dart:75, and the card's Where row and its
Copy gave that line.

The cache now keeps one entry per element in an Expando. Each element
reports its own call site, and entries go with their elements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first lookup fell back to the widget name when the issue's own
element had no highlight, for instance after a scroll dropped it. With
two ListViews, the crosshair on the first list's card then outlined
the second list, and the outline stayed on it after the first came
back. An issue with an occurrence id now takes that element's
highlight or none, as the re-acquire path already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The card tagged only the issues the latest verdict named. Late frames
alternate between phases, and a raster or start-delay frame names no
build issue, so on a busy screen the tag went on and off about once a
second (23 changes in 30 s on FPS Stress, Galaxy A07).

The controller now notes when a verdict last named each issue, and
heldJankCauses returns those named within the 10 s jank window
jank_detected judges, with the time until the first one leaves. The
card tags the held issues while a jank symptom is reported and arms one
timer for the next expiry. The hold lives in the controller, so closing
and reopening the dashboard neither drops it nor restarts it, and a
route change clears it, as it starts a new jank window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from wp26/overlay-measurement to main October 26, 2026 19:02
@Harrys76
Harrys76 merged commit d419f43 into main Oct 26, 2026
5 checks passed
@Harrys76
Harrys76 deleted the wp27/overlay-data branch October 26, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant