Skip to content

feat(perf): assemble the performance data path, in the one package that can see all four - #14

Merged
anilcancakir merged 8 commits into
masterfrom
feat/perf-attribution
Aug 25, 2026
Merged

feat(perf): assemble the performance data path, in the one package that can see all four#14
anilcancakir merged 8 commits into
masterfrom
feat/perf-attribution

Conversation

@anilcancakir

Copy link
Copy Markdown
Contributor

What

MagicPerfIntegration, installed from MagicDevtools.installPre(). It sets magic's notify hook, registers a route-transition observer, installs wind's perf resolver, registers telescope's frame watcher, and assigns all four of dusk's pointers.

Why

dusk reports numbers produced by telescope, wind and magic, and its frozen dependency contract forbids it from importing any of them. This package is the only place in the ecosystem where all four are visible at once, so this is where the seam gets closed.

The failure this class exists to prevent is silent. Every pointer's default is structurally complete rather than null, which is what lets both sides compile independently, and also what means an unassigned one produces a report of zeros instead of an error, in a different repository, at the end of a driven run that looked like it worked. So the tests seed the store, install, and read back THROUGH each pointer rather than asserting that install() did not throw.

Ordering is load-bearing in two places. The observer registers before the idempotency guard is armed, because MagicRouter.addObserver throws once the router has been built and marking the integration installed first would turn a legitimate retry into a silent no-op. And the whole install belongs in installPre, ahead of Magic.init(), for the same reason: the throw is deliberately not caught, since a swallowed one leaves the report with no route transitions and nothing to explain their absence.

Verified rather than assumed: MagicRouter._instance is assigned only by its lazy getter and nulled only by a reset with no production caller, so an observer registered before Magic.init() survives to the router Magic.init() populates.

The session hooks are scoped, not global. Begin zeroes wind's counters, turns counting on, clears telescope's frame buffer and clears the magic-side counters; end turns counting back off. Without the magic-side clear the magic section would report the sum of every previous session while the other sections reported only the current one, which is worse than no number. clearFramePerf() rather than clear(), so the HTTP, log and exception buffers a developer is reading alongside the session survive it.

Testing

flutter test: 109 passing. flutter analyze: clean. dart format --set-exit-if-changed: clean.

Mutation-checked during development: dropping framePerfReader turns the seam test red with Expected: an object with length of <2> Actual: []; dropping the idempotency guard produces two observers.

Depends on

All five siblings, and it is the last one to merge. It calls MagicController.onRefreshUI (magic), Wind.installPerfResolver() (wind), FramePerfWatcher and clearFramePerf() (telescope), and assigns the four pointers (dusk), which in turn need the contract from wind_diagnostics_contracts.

Note for CI: this package's pubspec.yaml still pins the released versions of its five siblings, and everything this change calls is unreleased and reachable only through pubspec_overrides.yaml. CI will not resolve until the siblings publish. That is the release train, not a defect in this change.

Siblings, in merge order: wind_diagnostics_contracts, then magic / fluttersdk_telescope / fluttersdk_wind / fluttersdk_dusk, then this.

…at can see all four

dusk reports numbers produced by telescope, wind and magic, and its frozen
dependency contract forbids it from importing any of them. This package is the
only place in the ecosystem where all four are visible at once, so this is where
the seam gets closed: four settable pointers dusk declares with no-op defaults,
assigned here to the real sources.

The failure this class exists to prevent is silent. Every pointer's default is
structurally complete rather than null, which is what lets both sides compile
independently, and also what means an unassigned one produces a report of zeros
instead of an error, in a different repository, at the end of a driven run that
looked like it worked. So the tests seed the store, install, and read back
THROUGH each pointer rather than asserting that install() did not throw.

Ordering is load-bearing in two places. The observer registers before the
idempotency guard is armed, because MagicRouter.addObserver throws once the
router has been built and marking the integration installed first would turn a
legitimate retry into a silent no-op. And the whole install belongs in
installPre, ahead of Magic.init, for the same reason: the throw is deliberately
not caught, since a swallowed one leaves the report with no route transitions
and nothing to explain their absence.

The session hooks are scoped rather than global. Begin zeroes wind's counters,
turns counting on, clears telescope's frame buffer and clears the magic-side
counters; end turns counting back off. Without the magic-side clear the magic
section would report the sum of every previous session while the wind and frame
sections reported only the current one, which is the kind of number that is
worse than no number. clearFramePerf() rather than clear(), so the HTTP, log and
exception buffers a developer may be reading alongside the session survive it.
…ese repos gates on

CI runs `dart format --output=none --set-exit-if-changed` in both ci.yml and
publish.yml here, and this branch had never had the formatter run on it. Ten
files across four repos were dirty, so four of the PRs would have gone red on a
check that was never part of my verification loop.

The omission has a specific cause worth naming. The consumer app these packages
were driven from carries a standing rule NOT to run dart format, because its
tree predates the current SDK's tall formatter and reformatting rewrites dozens
of untouched files. That rule is about that repository. I carried it into these
six, where it does not apply: they resolve to short style in-repo and they gate
on a zero diff. A project-specific rule applied outside its project.

Only branch-introduced files were formatted, so nothing untouched moved.
@kodizm

kodizm Bot commented Aug 25, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

The wiring itself reads correctly and the tests target the right thing (data through each pointer, not install() returning), but as committed the package does not compile against its own pubspec.yaml, and the caret constraints can never be satisfied by the sibling releases this depends on.

Critical

pubspec.yaml:17-21 — correctness/release. The declared constraints are magic: ^0.0.6, fluttersdk_dusk: ^0.0.9, fluttersdk_telescope: ^0.0.4. On 0.0.x, pub's caret pins to a single patch: ^0.0.6 means >=0.0.6 <0.0.7. The APIs this change calls land in the siblings' next versions, so no version inside these ranges will ever contain them - this is not just "wait for the release train", the constraints have to be bumped in this PR (or a follow-up before the tag) or CI stays red and a published magic_devtools breaks on pub get for consumers. pubspec_overrides.yaml cannot rescue CI either: it is gitignored (.gitignore:36), so the checkout in .github/workflows/ci.yml never sees it.

Evidence, on this head with a clean flutter pub get:

flutter analyze  →  64 issues found
error • The setter 'onRefreshUI' isn't defined for the type 'MagicController' • lib/src/perf_integration.dart:65:21
error • The method 'installPerfResolver' isn't defined for the type 'Wind' • lib/src/perf_integration.dart:70:10
error • Undefined class 'FramePerfWatcher' • lib/src/perf_integration.dart:71:11
warning • The library 'package:fluttersdk_dusk/dusk.dart' doesn't export a member with the shown name 'framePerfReader' • lib/src/perf_integration.dart:7:9

Minor

lib/src/magic_devtools.dart:75installPre() gained a throwing failure mode, but its own docstring still ends "Each underlying install is idempotent, so a second call in the same isolate is safe" without saying that a first call made after the router has been built now throws StateError. A host that installs behind a lazy debug toggle, after runApp, previously got harmless no-ops; now it crashes. The throw is the right call - the doc just needs the caveat next to the idempotency sentence.

lib/src/perf_integration.dart:172route.settings.name ?? '(unnamed)' records every push the navigator reports, including anonymous routes from showDialog/showModalBottomSheet. In a dialog-heavy session those (unnamed) entries share the 200-slot ring with real page transitions and can evict them. Filtering unnamed pushes, or keeping them in a separate bucket, would make the ranking mean what the report says it means.

lib/src/perf_integration.dart:60 — the comment above addObserver argues the guard is armed late so a throw leaves a retry possible, but _installed = true still precedes steps 2-4 (onRefreshUI, Wind.installPerfResolver(), registerWatcher, the four pointers). If any of those ever throws, a retry becomes exactly the silent no-op the comment is written to prevent. Moving the assignment to the end of install() costs nothing and matches the stated intent.

lib/src/perf_integration.dart:145-155resetForTesting() hand-writes dusk's no-op defaults rather than restoring them, and test/perf_integration_test.dart:272-282 then asserts against those same locally-invented literals. If dusk's real defaults change key set, both sides agree and the test still passes while production drifts. Not load-bearing today, but it makes the assertion weaker than it looks.

Tests

test/perf_integration_test.dart covers what matters: read-back through all four pointers, the double-install observer/watcher count, the StateError path with isInstalled still false, the scoped begin/end hooks including the sibling-buffer survival check, and installPre wiring. I could not execute it - it fails to compile against the resolved dependencies (see Critical), so the "109 passing" claim is unverified here.

Checks I ran

  • flutter pub get - resolved (144 deps), against the released siblings.
  • flutter analyze - 64 issues, all undefined-symbol errors from the unreleased sibling APIs; nothing else in lib/ or test/.
  • dart format --set-exit-if-changed . - clean, 16 files, 0 changed.
  • flutter test - not run; would not compile.
  • Grepped lib/ and test/ for other onRefreshUI, registerWatcher and MagicRouter callers: no other writer of onRefreshUI in this repo, so no hook conflict with MagicTelescopeIntegration or MagicDuskIntegration.
  • Not reviewed: nothing - all four changed files were read in full.

…ons being ranked

Four review points, all correct.

The idempotency guard was armed after the observer registration but before the
four steps that follow it, so a throw from Wind.installPerfResolver(),
registerWatcher or the pointer assignments left the guard set with the pointers
unassigned, and any retry became exactly the silent no-op the class exists to
prevent. It is armed at the end now, and the observer gets its own flag so a
retry does not register a second one. The original comment argued for the right
property and the code only delivered half of it.

Route transitions no longer record anonymous pushes. showDialog and
showModalBottomSheet go through the same navigator, so in a dialog-heavy
session those entries shared the bounded list with real page transitions and
could evict the ones the report is ranking. Skipped rather than bucketed: a
duration nobody can attribute to a screen is not one an agent can act on.

resetForTesting no longer hand-writes dusk's no-op defaults. It captures them
at load, before this package assigns over them, and restores those. Re-typing
them let this package and its own tests agree on a key set that had drifted
from dusk's, so the assertions would have kept passing while production drifted
with them.

And installPre's docstring gained the caveat it needed. It ends "a second call
in the same isolate is safe", which is still true, but a FIRST call made after
the router is built now throws. A host installing behind a lazy debug toggle
after runApp used to get harmless no-ops.
@anilcancakir

Copy link
Copy Markdown
Contributor Author

All four Minors fixed in 886c3aa. The Critical's conclusion is right for a different reason than the one given, and I measured it rather than reasoning about it.

The guard armed before steps 2 to 4. You are right and this one bothered me, because the comment sitting above it argued for exactly the property the code did not deliver. _installed = true now runs as the last line of install(), and the observer gets its own flag so a retry after a throw does not register a second one. Both halves were needed: arming late without the flag just moves the bug.

Anonymous pushes. Fixed by skipping them rather than bucketing them. showDialog and showModalBottomSheet go through the same navigator, so in a dialog-heavy session those entries were sharing the 200-slot ring with the transitions the report ranks and could evict them. A duration nobody can attribute to a screen is not one an agent can act on. Test added; it needs no pump, which is itself the assertion, since an unnamed push now returns before scheduling the post-frame callback that would close the span.

resetForTesting hand-writing dusk's defaults. Fixed the way you framed it: they are captured at load, before this package assigns over them, and restored from those. Your point about the test agreeing with the locally-invented literals was the decisive one, since that is a mutual-agreement failure no assertion in either repo can see.

installPre docstring. Caveat added next to the idempotency sentence. "A second call in the same isolate is safe" stayed true; what changed is that a FIRST call after the router is built now throws.

The Critical. The constraints do need to move, but not for the reason given:

On 0.0.x, pub's caret pins to a single patch: ^0.0.6 means >=0.0.6 <0.0.7

I checked this, because it decides whether this is a release-ordering matter or a dead end:

$ cat pubspec.yaml   # fluttersdk_telescope: ^0.0.1
$ dart pub get && grep -A1 version pubspec.lock
^0.0.1 resolved to: 0.0.4

^0.0.1 admitted 0.0.4, so the caret does not pin the patch and these constraints CAN be satisfied by the sibling releases. It is the release train after all.

What survives, and what I am acting on, is the half that does not depend on that: the floors should state what the code actually needs, so pub cannot legally resolve a version lacking the API and a published magic_devtools cannot break on pub get. That bump lands with the release, once there are versions to point at. Raising it now would only move the failure from dart analyze to pub get.

You are also right that pubspec_overrides.yaml cannot rescue CI: it is gitignored, deliberately, so the checkout never sees it. That is why every one of these five PRs says its CI stays red until the contract publishes.

flutter test: 110 passing. flutter analyze: clean. dart format --set-exit-if-changed: clean.

@kodizm

kodizm Bot commented Aug 25, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of 886c3aa: the guard reordering and the anonymous-push skip are both right, and the caret question is settled - but the resetForTesting fix does not do what its comment says, because Dart top-level finals initialise lazily, and today it only works by an ordering accident in the test file.

Since my last review: guard armed last with a separate observer flag, anonymous pushes skipped plus a test, dusk defaults captured instead of hand-written, installPre docstring caveat added. The caret point is answered - ^0.0.1 resolving to 0.0.4 settles it, this is release ordering.

Major

lib/src/perf_integration.dart:39-42 — correctness. "Captured before this package assigns over them" is not what happens: a top-level final in Dart is initialised on first read, and the only reader is resetForTesting(), which runs after install() has already overwritten the pointers. Verified with a standalone repro of the exact pattern:

$ dart /tmp/lz/main.dart
after install: INTEGRATION
after reset:   INTEGRATION

It passes today only because setUp at test/perf_integration_test.dart:88 calls resetForTesting() before any test installs, so the first capture happens while the pointers are still dusk's. Move an install into a setUpAll, or add a second test file without that setUp, and the four "defaults" become the integration's own live closures for the rest of the isolate - every later reset restores nothing, state leaks between tests, and no assertion in either repo can see it. Reading the four into statics at the top of install(), before the first assignment, gets the property the comment claims.

Minor

lib/src/perf_integration.dart:35 — the new doc block is contiguous with the class docstring (no blank line after "flagged as an unnecessary import."), so the whole 20-line explanation of what MagicPerfIntegration is now documents the private _duskFramePerfDefault, and the class itself has no doc comment. A blank line between them restores it.

test/perf_integration_test.dart:310(_, __, ___) trips unnecessary_underscores twice; flutter analyze reports it as info, so CI's --no-fatal-infos will not fail on it, but it is two new lint hits introduced by this commit.

Tests

The new an anonymous push is not recorded test targets the right thing, and the no-pump-needed argument holds - an unnamed push returns before scheduling the post-frame callback. Nothing covers the capture-ordering hazard above, which is exactly why it is invisible.

Checks I ran

  • flutter analyze - 70 issues; all errors are the same unreleased-sibling undefined symbols as last time (WindPerfCounters, perfSessionEndHook, ...), plus the two new unnecessary_underscores infos. Nothing else new.
  • dart format --set-exit-if-changed . - clean, 16 files, 0 changed.
  • dart repro of the lazy-final capture pattern - output above.
  • flutter test - not run; still will not compile against the released siblings, so "110 passing" is unverified here.
  • ls pubspec_overrides.yaml - absent from the checkout, as expected.
  • Reviewed all three files returned for this range in full; nothing was dropped or diffless.

The previous commit claimed to capture dusk's defaults "before this package
assigns over them" and did not. A top-level `final` in Dart initialises on
first READ, and the only reader is resetForTesting(), which runs after
install() has already overwritten the pointers. So it captured this package's
own closures and restored those, which means every "back to the default"
assertion was really asserting that install had happened.

Verified with a standalone repro of the pattern before replacing it: the reset
returned INTEGRATION, not the default. A comment asserting a language behaviour
the language does not have is worse than no comment, because it stops the next
reader checking.

Captured eagerly now, at the top of the pointer assignments, which is the last
moment they are still readable. The nullable fields make "install never ran"
representable, and in that case there is nothing to put back.

The two tests that moved wind's counters through record* now build a real WDiv
instead. Those entry points are @internal to fluttersdk_wind, so reaching for
them asserted against a surface no consumer is meant to touch, and a pump is
the honest version of that setup anyway: it is what moves these numbers in an
app.

Mutation-checked: removing the capture turns the reset test red.
@anilcancakir

Copy link
Copy Markdown
Contributor Author

Fixed in 3ba4598. You were right and I confirmed it with the same shape of repro before changing anything:

void Function() ptr = () => print('DUSK DEFAULT');
final void Function() captured = ptr;
void main() { ptr = () => print('INTEGRATION'); ptr(); ptr = captured; ptr(); }
// INTEGRATION
// INTEGRATION

A top-level final initialises on first read, the only reader is resetForTesting(), and by then install() has already overwritten the pointers. So it captured this package's own closures and restored those, which means every "back to the default" assertion was really asserting that install had happened. The comment claiming otherwise was the worse half: a comment asserting a language behaviour the language does not have stops the next reader from checking.

Captured eagerly now, at the top of the pointer assignments, which is the last moment they are still readable. The fields are nullable so "install never ran" is representable, and in that case there is nothing to put back. Mutation-checked: removing the capture turns the reset test red, which the previous version would not have done.

One knock-on worth flagging, since it crosses into the sibling PR. Marking wind's record* entry points @internal made two of this file's tests warn, because they moved wind's counters directly. They now build a real WDiv instead, which is both the surface a consumer is meant to touch and the honest version of that setup: a pump is what moves those numbers in an app. I also un-marked WindPerfCounters.reset() over in wind, because the session-begin hook this package assigns calls it, so it is cross-package contract rather than wind's own bookkeeping.

flutter test: 110 passing. flutter analyze: clean. dart format --set-exit-if-changed: clean.

@kodizm

kodizm Bot commented Aug 25, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of 3ba4598: the eager capture is correct and settles the lazy-final problem - but swapping recordCacheHit() for a real WDiv build coupled the two session tests to wind's static parse cache, so one of them now asserts nothing and the other only passes because the first one ran before it.

Since my last review: the four defaults are captured with ??= at the top of the pointer assignments (correct - install() after a resetForTesting() cannot re-capture the integration's own closures), resetForTesting() no-ops when install() never ran, two tests moved to testWidgets + _buildOneWidget, and analysis_options.yaml gained an exclude: block.

Major

test/perf_integration_test.dart:245,275 — correctness (test). WindParser._styleCache is a static final Map cleared only by WindParser.clearCache() (fluttersdk_wind/lib/src/parser/wind_parser.dart:82,152), and the hit branch is if (baseStyle == null && _styleCache.containsKey(cacheKey)) return ... (:221) - which is where the counter increments. Both tests build the same WDiv(className: 'p-4') under the same default WindThemeData(), so they share one cache key across the isolate. Consequence, in order: perfSessionBeginHook clears only the perf state builds against a cold cache, so it's a miss, cacheHits is still 0 when the begin hook runs, and expect(cacheHits, 0) at :245 passes without proving the hook cleared anything - the previous recordCacheHit() guaranteed a non-zero before the reset and this does not. Then the session pair... builds the now-warm key and gets the one hit :275 expects. Run that second test alone (flutter test --plain-name 'the session pair'), reorder them, shuffle, or add any earlier test that calls WindParser.clearCache(), and the build is a miss and cacheHits is 0 - red. Building through the public surface is the right instinct; pinning the cache state the tests depend on (warm it explicitly in each, or assert cacheMisses in the cold one) is what makes it order-independent. I could not execute this to confirm - see Checks.

Minor

lib/src/perf_integration.dart:180-181 — the comment on the restore still reads "captured once at load", which is the exact claim 3ba4598 disproved; the capture is now at first install. A comment asserting behaviour the code no longer has is the thing you named as the worse half of the last bug, and it is still here, one screen below the block that explains why.

analysis_options.yaml:8-16 — none of build/, android/, ios/, web/, windows/, macos/, linux/ exist in this package, so the block excludes nothing today (ls -d on all seven: no such file or directory). Harmless, but it is unrelated to the perf data path and, if a build/ ever lands, it silences analysis there rather than the checkout ignoring it.

Tests

The testWidgets conversion targets the right surface - a pump is what moves wind's counters in an app, and @internal on record* is a good reason to stop poking them. The cache coupling above is the cost, and nothing in the file pins the cache state it now depends on. The reset test at :293-300 is genuinely stronger than before: those literals are now checked against dusk's real defaults rather than against this package's re-typed copy.

Checks I ran

  • flutter analyze - 18 issues, down from 70: the sibling publishes landed for magic 0.0.7, fluttersdk_dusk 0.0.12, fluttersdk_telescope 0.0.5, so all of those symbols now resolve. Every remaining error is wind: installPerfResolver and 15 WindPerfCounters (resolved fluttersdk_wind 1.4.1 predates the perf release). Plus the two unnecessary_underscores infos at test/...:331, unchanged.
  • dart format --set-exit-if-changed . - clean, 16 files, 0 changed.
  • Read fluttersdk_wind-1.4.1/lib/src/parser/wind_parser.dart for the cache semantics behind the Major - lines quoted above.
  • ls -d on the seven newly-excluded directories - none present.
  • flutter test - not run; still will not compile against the released wind, so "110 passing" is unverified here.
  • Reviewed all three files in this range in full; nothing was dropped or diffless.

Review found the two session tests coupled to wind's static parse cache, and
reproducing it turned up a second order dependence underneath.

WindParser._styleCache is a static map shared across the isolate, and both
tests built the same WDiv under the same theme, so they shared one key. The
first ran against a cold cache, which made its `expect(cacheHits, 0)` assert a
value that was already 0 and prove nothing about the reset it was there to
check; the second only passed because the first had warmed the key. Verified:
`--plain-name 'the session pair'` alone was red with Expected 1, Actual 0.

Each test now warms the cache itself through a helper that clears it, builds,
and zeroes the counters. The first also asserts a non-zero BEFORE the hook, so
the reset has something to have cleared. The helper pumps a different tree
between the two builds: pumping an identical tree does not rebuild it, so the
measured build parsed nothing and the hit never happened. That cost a wrong
first attempt.

The second dependence: MagicDevtools.installPre() installs telescope's
DumpWatcher, which replaces the global debugPrint, and nothing put it back. So
Flutter's own post-test check fired on whichever testWidgets case ran NEXT,
blaming an innocent test under only some orderings. tearDown restores it now.
The whole suite is green under five shuffle seeds; before, seed 12345 was red.

Also corrects the restore comment, which still said the defaults are "captured
once at load" after the fix moved that to first install, and drops the
analysis_options exclude block: none of those seven directories exist in this
package, so it excluded nothing and was unrelated to the perf path.
@anilcancakir

Copy link
Copy Markdown
Contributor Author

Handled in 4f42c58. The Major was right, and reproducing it turned up a second order dependence sitting underneath it.

The parse-cache coupling. Confirmed before changing anything: flutter test --plain-name 'the session pair' alone was red with Expected: <1> Actual: <0>, exactly as you described. And the first test's expect(cacheHits, 0) was asserting a value that was already 0, so it proved nothing about the reset it exists to check.

Each test now pins its own cache state through a helper that clears the cache, builds, and zeroes the counters. The first also asserts a non-zero before the hook runs, so the reset has something to have cleared, which restores what the old recordCacheHit() guaranteed.

One thing worth recording, because my first attempt at the helper was wrong: pumping an identical widget tree does not rebuild it, so the measured build parsed nothing and the hit never happened. The helper pumps a SizedBox.shrink() between the two builds.

The second one, which your finding led me to. With the cache pinned, the suite still failed under --test-randomize-ordering-seed=12345, and on a different test: surfaces the StateError when the router is already built. The real error was Flutter's own The value of a foundation debug variable was changed by the test. MagicDevtools.installPre() installs telescope's DumpWatcher, which replaces the global debugPrint, and nothing put it back, so the check fired on whichever testWidgets case happened to run next. The failure landed on an innocent test and only under some orderings, which is how it survived a green suite. tearDown restores debugPrint now.

The suite is green under five shuffle seeds (12345, 777, 4242, 99, 31337), 110 tests each. Before this commit, 12345 was red.

The restore comment. Corrected. It still said "captured once at load" after the fix moved the capture to first install, and it now says why: a top-level final in Dart initialises on first read, so capturing at load would have caught this integration's own closures rather than dusk's defaults.

analysis_options.yaml. Removed. I checked all seven with ls -d: none exist here, so it excluded nothing and was unrelated to the perf data path.

On your analyze note: dart analyze is clean here because my local pubspec_overrides.yaml points fluttersdk_wind at the checkout. Against published wind 1.4.1 it is red exactly as you saw, and it stays red until wind 1.5.0 publishes. wind#188 has now merged and #189 is rebased onto master and in CI, so the release is the next step; the four floors in pubspec.yaml get bumped after it, which is the last commit this PR needs.

@kodizm

kodizm Bot commented Aug 25, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of 4f42c58: both fixes are right - the cache pinning makes the two session tests order-independent and the first one now proves something it did not before - but the analysis_options.yaml removal does not stick, because flutter pub get writes that block back.

Since my last review: _warmTheParseCache() added and used by both session tests, a non-zero cacheHits assertion before the begin hook, debugPrint captured in setUp and restored in tearDown, the restore comment corrected, and the exclude: block removed. The parse-cache Major is settled - WindParser.clearCache() then a build then an intervening SizedBox.shrink() pump pins the key each test asserts on, and the identical-tree point about pumpWidget is correct.

Minor

analysis_options.yaml:8 — the tooling owns that block, so removing it just makes every tree dirty after a dependency fetch. Verified on this checkout:

$ flutter pub get
... Upgrading analysis_options.yaml to exclude build and platform directories.
$ git status --short
 M analysis_options.yaml

flutter analyze prints the same line and rewrites it too. CI will not fail on this (dart format ignores YAML and nothing runs git diff --exit-code), but the block reappears for you and for every contributor on the next pub get. My earlier note was about the block being unrelated to this PR, which it is - it turns out it is unrelated because Flutter generated it, not the author, so the right move is to leave it.

test/perf_integration_test.dart:142 — the debugPrint restore is in tearDown, which runs after Flutter's own check, so it fixes today's suite for the wrong reason and will not cover a future case. AutomatedTestWidgetsFlutterBinding.postTest calls _verifyInvariants() - and therefore debugAssertAllFoundationVarsUnset - inside the test body, before any tearDown callback (/opt/flutter/packages/flutter_test/lib/src/binding.dart:1974,1996). It works now only because the sole installPre() call is in a plain test() at :349, which has no invariant check; move that group to testWidgets, or add any testWidgets case that calls installPre(), and it fails on itself with tearDown too late to help. Restoring at the end of the test body (or an addTearDown inside the case, which also runs after postTest, so really: restore inline) is what makes it hold. The comment at :119-125 describes the symptom accurately but not this constraint.

Tests

The two session cases are now genuinely order-independent, and expect(WindPerfCounters.cacheHits, greaterThan(0)) at :268 restores what the old recordCacheHit() guaranteed - that is the assertion the previous version was missing. I could not execute the suite to confirm the five-seed result; the shuffle claim is unverified here.

Checks I ran

  • flutter pub get - resolved, and re-added the exclude: block (output above).
  • flutter analyze - 20 issues. All 18 errors are wind: installPerfResolver plus 17 WindPerfCounters (resolved fluttersdk_wind 1.4.1 predates the perf release). The other siblings all resolve now. Plus the two known unnecessary_underscores infos at test/perf_integration_test.dart:367.
  • dart format --set-exit-if-changed . - clean, 16 files, 0 changed.
  • Read /opt/flutter/packages/flutter_test/lib/src/binding.dart:1960-2000 for the invariant-check ordering behind the second Minor.
  • flutter test - not run; still will not compile against published wind, so "110 passing" and the five shuffle seeds are unverified here.
  • Reviewed all three files returned for this range in full; nothing was dropped or diffless.

…erated yaml

Two review notes, both verified against the source before acting.

The debugPrint restore was in tearDown, which runs AFTER Flutter's end-of-test
check: AutomatedTestWidgetsFlutterBinding.runTest calls _verifyInvariants()
immediately after `await testBody()` (flutter_test/lib/src/binding.dart:1974),
so tearDown and addTearDown are both too late. It passed only because the sole
installPre() call sits in a plain test(), which has no invariant check. Proven
by converting that case to testWidgets as a probe: it failed on itself with
"the value of a foundation debug variable was changed by the test".

The restore now happens inline at the end of that body. Re-running the same
probe, the foundation error is gone. What surfaces instead is a SECOND
end-of-test invariant: installPre() leaves a SemanticsHandle active, because
dusk's snapshot pipeline enables semantics. That is latent today, since the
case is a plain test(), so it is recorded as a note rather than fixed here.
The tearDown restore stays as a net for the plain test() cases.

Reverting the analysis_options.yaml removal. The exclude block is written by
`flutter pub get` ("Upgrading analysis_options.yaml to exclude build and
platform directories"), reproduced here, so removing it just makes every tree
dirty after a dependency fetch. My earlier note that it was unrelated to this
PR was right for the wrong reason: it is unrelated because Flutter generated
it, which is exactly why it should stay.

Suite green under three shuffle seeds, 110 tests each.
@anilcancakir

Copy link
Copy Markdown
Contributor Author

Both handled in 077abf6, and the second one was right in a way I had not seen: my fix was passing for the wrong reason.

The tearDown restore being too late. Verified against the source you cited: _verifyInvariants() runs immediately after await testBody() in AutomatedTestWidgetsFlutterBinding.runTest (binding.dart:1974), so both tearDown and addTearDown are after the check. Then I proved it rather than reasoning about it: converting the installPre case to testWidgets as a probe made it fail on itself with the value of a foundation debug variable was changed by the test. So you are exactly right that it worked only because that case is a plain test().

The restore is inline at the end of that body now. Re-running the same probe, the foundation error is gone.

And the probe found a second one. With debugPrint handled, converting that case to testWidgets now fails on a different end-of-test invariant: A SemanticsHandle was active at the end of the test, because installPre() starts dusk's snapshot pipeline and that enables semantics. It is latent today, since the case is a plain test(), so it is a note at the call site rather than a change here, with the constraint written down so whoever converts it is not surprised twice.

analysis_options.yaml. Reverted, and your reproduction is exactly what I get:

$ flutter pub get
... Upgrading analysis_options.yaml to exclude build and platform directories.
$ git status --short
 M analysis_options.yaml

So the block belongs to the tooling and removing it just dirties every tree after a fetch. Worth noting this also explains something from earlier in this work: I saw the same block appear in magic during a session, decided it "wasn't mine", and discarded it. It was Flutter's, both times.

On the red CI, since it is the obvious question. It is not this PR's code. pubspec.yaml pins fluttersdk_wind: ^1.2.1, CI resolves the published 1.4.1, and WindPerfCounters and Wind.installPerfResolver land in 1.5.0, which is not released yet. That is why your flutter analyze shows 18 wind errors and mine shows none: my gitignored pubspec_overrides.yaml points at the checkout. wind#188 has merged and #189 is rebased onto master with every check green; once it lands I cut wind 1.5.0, publish, and bump the four floors here, which is the last commit this PR needs and the one that turns CI green.

Suite green under three shuffle seeds (12345, 777, 4242), 110 tests each. dart analyze and dart format clean locally.

@kodizm

kodizm Bot commented Aug 25, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of 077abf6: both Minors from the last round are settled correctly - the debugPrint restore is now inline before the invariant check, and the generated exclude: block is back so a pub get no longer dirties the tree. Nothing new of substance; two comment nits only.

Since my last review: restoreDebugPrint() extracted and called inline at the end of the installPre body (the tearDown copy kept as a net for the plain test() cases), the note about _verifyInvariants() running before any tearDown written down at the declaration, the SemanticsHandle constraint recorded at the call site for whoever converts that case, and analysis_options.yaml reverted. Verified the revert on this checkout: flutter pub get now leaves git status --short empty, where before it reported M analysis_options.yaml.

Minor

test/perf_integration_test.dart:127 — the doc reference is [_restoreDebugPrint], but the function declared nine lines below is restoreDebugPrint, no underscore, and the call site at :361 spells it without one. A dangling [] reference in the one comment whose job is to point at the fix; the analyzer does not resolve doc links in a local function, so nothing catches it.

test/perf_integration_test.dart:361-373 — the comment says "Inline, not in a tearDown", and the next statement is addTearDown(restoreDebugPrint), with the inline call four lines later. Both is harmless (the restore is idempotent), but the comment and the code directly below it disagree about which mechanism is in use, which is the shape of thing this PR has spent two rounds removing. Either drop the addTearDown or say it is deliberately belt-and-braces.

Tests

No assertions changed in this range - the installPre case gained only the restore and the two notes. The suite still cannot be executed here, so the three-seed shuffle result and "110 passing" remain your measurement, not mine.

Checks I ran

  • flutter pub get then git status --short - clean, no rewrite of analysis_options.yaml. Confirms the revert.
  • flutter analyze - 20 issues, unchanged in kind: 18 errors all wind (installPerfResolver plus 17 WindPerfCounters, resolved fluttersdk_wind 1.4.1 predates the perf release), plus the two known unnecessary_underscores infos, now at test/perf_integration_test.dart:388. Matches your account of the release train.
  • dart format --set-exit-if-changed . - clean, 16 files, 0 changed.
  • grep for the other installPre() caller: test/magic_devtools_test.dart:37. It has the same unrestored debugPrint, but that file is untouched by this PR and contains no testWidgets case, so it is out of scope and latent.
  • flutter test - not run; will not compile against published wind.
  • Reviewed both files returned for this range in full; nothing was dropped or diffless.

The block describing MagicPerfIntegration and the block describing the four
captured dusk defaults had merged into one, sitting on the private top-level
variables. The tell is mid-paragraph: a sentence about reaching wind through
magic's barrel is followed, with no break, by "dusk's own no-op defaults,
captured the first time...", so the class's own documentation read as an aside
about four private fields and the class itself carried none.

Split back to where each belongs. The class doc is on the class, the pointer
doc is on the pointers.

resetForTesting's comment went with it. It had absorbed a copy of the top-level
final explanation inside a parenthetical, which left the surrounding sentence
reading "captured on the first install rather than at load (...), rather than
hand-written here". It now states its own point and refers to the fields for
the rest.

Comments only, no behaviour change.
@kodizm

kodizm Bot commented Aug 25, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of 17a4724: a pure documentation move, and it fixes a real defect - the class docblock was previously separated from class MagicPerfIntegration by the four field declarations, so it documented nothing. No new findings.

Since my last review: the _duskFramePerfDefault group and its docblock moved above the class docblock, and the "captured on first install, not at load" rationale moved out of the resetForTesting() inline comment into a one-line pointer at the fields. No executable code changed - git diff 077abf6..17a4724 is comments and declaration order only, and the class doc at lib/src/perf_integration.dart:33-54 now sits directly above the class it describes.

Tests

No test file in this range, so no assertions changed. The two Minors from the previous round (test/perf_integration_test.dart:127, the [_restoreDebugPrint] reference that has no underscore in the declaration or at the call site, and the addTearDown sitting under a comment that says "Inline, not in a tearDown") are both still present and still accurate; nothing here touched them.

Checks I ran

  • flutter pub get then git status --short - clean, no rewrite of analysis_options.yaml.
  • flutter analyze - 20 issues, identical in kind and count to the last round: 18 errors all wind (installPerfResolver plus 17 WindPerfCounters against the resolved fluttersdk_wind 1.4.1), plus the two known unnecessary_underscores infos at test/perf_integration_test.dart:388. The docblock move introduced nothing.
  • dart format --set-exit-if-changed . - clean, 16 files, 0 changed.
  • flutter test - not run; still will not compile against published wind, so the shuffle-seed and pass-count results remain your measurement, not mine.
  • Reviewed the one file returned for this range in full; nothing was dropped or diffless.

…the wrong line

Both carried over from the previous review round.

The note explaining why the restore runs inside the test body pointed at
[_restoreDebugPrint]; the function is declared nine lines below as
restoreDebugPrint, with no underscore, and the call sites spell it that way
too. The analyzer does not resolve doc links to a local function, so nothing
was going to catch it, in the one comment whose job is to point at the fix.

In the installPre case, "Inline, not in a tearDown" sat directly above an
addTearDown call, which is the opposite of what it says. The inline restore is
the last line of that body and the tearDown is a net; the comment now says so
in that order.

Also collapsed `(_, __, ___)` to `(_, _, _)` in the pageBuilder, the two
unnecessary_underscores infos CI reports on this branch. They do not fail the
gate (analyze runs --no-fatal-infos here) but they are the only two in the
package.

Comments and wildcard names only; `flutter test` is 110 green.
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.42857% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/src/perf_integration.dart 96.36% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@anilcancakir
anilcancakir merged commit 6a99e63 into master Aug 25, 2026
3 of 4 checks passed
@anilcancakir
anilcancakir deleted the feat/perf-attribution branch August 25, 2026 21:47
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