Skip to content

fix(attribution): GRAPH_GROWTH also requires what outlived each navigation to climb (#4007) - #4020

Merged
ryansolid merged 3 commits into
nextfrom
claude/project-thread-w8qjvi-4007
Oct 13, 2026
Merged

ryansolid merged 3 commits into
nextfrom
claude/project-thread-w8qjvi-4007

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Requested by Ryan · project thread

Summary

Fixes #4007.

Before: GRAPH_GROWTH compared the whole graph size at each arrival on a route pattern. A page whose correctly owned content is bigger on each visit (/rows/1, /rows/3, /rows/5) was reported as a leak.

After: a series reports only when it climbs both in the totals and in what outlived each navigation. Content that differs per visit stays quiet. A root, effect or signal that a visit leaves behind still reports, even when the page's content shrinks.

How (the design proposed in the issue, in packages/signals/src/core/attribution.ts):

  • While graphGrowth is on, a non-initial navigation snapshots the live graph into a WeakSet as it opens.
  • Its settle walk also counts snapshot nodes still alive (survived; an edge counts when both ends are), then drops the set.
  • A navigation that stays on the current route pattern (params or query only) continues the visit and isn't compared.
  • A superseded navigation doesn't change the current route.

Public API changes

No export, option, type or diagnostic code changes. Diagnostic surface changes to review:

  • GRAPH_GROWTH's data gains survived beside history (additive).
  • Its message adds the survived series, e.g. owners 3 → 5 → 7 (of 6 → 10 → 14).
  • It reports in fewer cases. A leak made only while staying on one route pattern is now reported at the next real entry into the route, not during the same-pattern moves.
  • Each non-initial navigation costs a second graph walk while the check is on (observe tier only).

Size

Prod and the observe core are byte-identical.

The "observe tier + attribution engine" scenario grows from 28,984 to 29,230 B brotli (+246 B) and from 87,305 to 87,899 B minified (+594 B), measured against next @ c9027a9. Its cap goes 29.00 → 29.24 KB. The Size-Exception note in scripts/size/scenarios.js records the maintainer's acceptance on 2026-10-13. The cap is higher than the 29.17 KB first proposed because #4010 (#4027) used up that scenario's headroom on next; this PR's own growth is unchanged.

How did you test this change?

  • packages/signals/tests/attribution-graph-growth.test.ts:
  • Full signals suite: 5205 passed, 20 expected fail, 2 skipped (unmodified next: 5197 / 20 / 2).
  • 08-dev-diagnostics.md and the skill's GRAPH_GROWTH section updated; changeset added.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w


Generated by Claude Code

…ation to climb (#4007)

GRAPH_GROWTH compared the whole graph's size at each settle of a route
pattern, so a page whose correctly owned content is bigger on each visit
(/rows/1, /rows/3, /rows/5) read as a leak although leaving disposed it.

While graphGrowth is on, a non-initial navigation now walks the graph as it
opens and adds every node to a WeakSet; its settle's walk also counts the
nodes from that set still alive (an edge when both ends are) and drops the
set. A series reports only when it climbs in the totals, as before, and in
that survived count. A navigation that stays on the route pattern the app
is on continues the visit and is not compared. The finding's data gains
`survived` beside `history`; the message shows both.

Engine-only: prod artifacts byte-identical. The observe + attribution size
scenario grows +230 B brotli (+594 B minified); cap raised with an audit
note pending the maintainer's acceptance.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
@changeset-bot

changeset-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f1df5e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch
board-example Patch
todos-server-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ryansolid ryansolid self-assigned this Oct 13, 2026
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.47 KB 0 B 0 B 0 B 7.48 KB ✅
signals: + createStore 14.93 KB 0 B 0 B 0 B 14.94 KB ✅
signals: + isPending/latest 9.75 KB 0 B 0 B 0 B 9.77 KB ✅
app: render + one signal (the simple-app floor) 9.97 KB 0 B 0 B 0 B 9.98 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.89 KB 0 B 0 B −77 B 17.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.44 KB 0 B 0 B +128 B 29.50 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.05 KB 0 B 0 B 0 B 13.07 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.62 KB 0 B 0 B 0 B 14.64 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.23 KB +246 B (+0.8%) +594 B 0 B 29.24 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.15 KB 0 B 0 B 0 B 10.17 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.74 KB 0 B 0 B 0 B 25.75 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.61 KB 0 B 0 B 0 B 31.62 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.43 KB 0 B 0 B 0 B 11.43 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.17 KB 0 B 0 B −1626 B 34.33 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.99 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.87 KB 0 B 0 B −1682 B 38.15 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.99 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 34.95 KB 0 B 0 B −2704 B 35.66 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.95 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.55 KB 0 B 0 B −2697 B 41.21 KB ✅ eager (counted): web.js 21.50 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.93 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.49 KB 0 B 0 B −1619 B 41.79 KB ✅ assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, serverForms.js 3.58 KB, trace.js 8.98 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.21 KB 0 B 0 B −1612 B 47.49 KB ✅ eager (counted): client.js 27.69 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 8.96 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.47 KB 0 B 0 B 0 B 20.48 KB ✅

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

Copy link
Copy Markdown
Member Author

The red Size / check here is "app: compiled hydrating", over its brotli cap by 31 B with a 0 B delta against the base. It isn't from this PR: the same scenario fails on next itself (the Size runs for the last three pushes to next, through d8ba741, are red). No fix exists yet; it needs either a trim or a recorded cap change on next. This PR's own size change, the attribution scenario's +230 B, passes under its raised cap, and that raise still needs maintainer acceptance.

— Claude via Claude Code


Generated by Claude Code

@coveralls

coveralls commented Oct 13, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38440670300

Coverage remained the same at 77.429%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1321
Covered Lines: 1079
Line Coverage: 81.68%
Relevant Branches: 1036
Covered Branches: 746
Branch Coverage: 72.01%
Branches in Coverage %: Yes
Coverage Strength: 27.21 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 2 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 195 untouched benchmarks


Comparing claude/project-thread-w8qjvi-4007 (4f1df5e) with next (c9027a9)

Open in CodSpeed

Copy link
Copy Markdown
Member Author

CodSpeed flags two regressions here, "mount 4000 rows › memo + sync render effect only" (−18%) and "projection derive: write one NESTED field" (−12%). Neither bench reaches this PR's code. The diff only touches core/attribution.ts, the attribution engine, which core never imports (core only uses attribution-hooks.ts), and neither bench enables attribution. CodSpeed's own report also notes that the compared runs used different runtime environments. I'm re-running the CodSpeed workflow once to confirm. If both regressions repeat on this commit, I'll dig into them rather than call them noise.

— Claude via Claude Code


Generated by Claude Code

Copy link
Copy Markdown
Member Author

CodSpeed flags dbmon shallow full tick at −6.8% after the merge of next. The same benchmark reads −7% on every PR measured against this base (be845cb), #4021 and #4027 included, and CodSpeed warns that the comparison crossed runtime environments. This PR changes the attribution diagnostic, which that store benchmark never loads. Size and tests are green. I've re-run the benchmarks once.

— Claude via Claude Code


Generated by Claude Code

…-w8qjvi-4007

Resolves the attribution scenario cap conflict with #4027: keeps next's
#4010 note and re-measures this PR on top of it (28,984 -> 29,230 B, cap
29.24 KB), accepted by the maintainer 2026-10-13.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
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.

3 participants