Skip to content

fix(signals): Loading on follows the frame; DEV LOADING_ON_OUTSIDE_HOLD (#3540) - #3575

Merged
ryansolid merged 12 commits into
nextfrom
fix/loading-on-frame-following
Sep 22, 2026
Merged

ryansolid merged 12 commits into
nextfrom
fix/loading-on-frame-following

Conversation

@ryansolid

@ryansolid ryansolid commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #3556, from the discussion on #3540 with @GabbeV and @mizulu.

Public API changes

What changes

on on Loading is still a dependency list and still releases the boundary's hold the moment a dependency notifies. What moves is where the fallback swap lands: with the frame the notifying write belongs to, instead of in the current frame beside content that write is still holding.

Product A → B, shell reads product(id) outside a <Loading on={id()}> whose content reads comments(id), written inside an action (or any write whose async is in flight):

frames
no on [A] → [B + comments] — the initialized boundary holds; the frame waits for both
on in #3556 (trigger) [A] → [A + spinner] → [B + spinner] → [B + comments]
on here (frame-following) [A] → [B + spinner] → [B + comments]

The [A + spinner] frame was B's loading state inside A's page — a fallback for a change nothing on screen reflected yet. That's the frame this removes. If comments land before the shell, the sweep clears the swap before the commit and no fallback is ever shown. Both semantics release the hold and commit at the same time; the only difference is that frame.

  • Nothing else holding (carousel, setId with the boundary's content as the only async reader): the release ends the hold, the frame commits in the same pass, fallback and the committed id land together — now. Unchanged; pinned.
  • on={latest(id)}: fallback now, beside the still-held frame. latest means here what it means everywhere else.
  • Same data also read outside the boundary, or the write's action outlasts the data (2.0.0-rc8 | <Loading> fallback reset does not detect changes when a signal source is pending , but does when it is from a memo #3524's shape): the frame waits on it and the fallback is never seen. That's the trade, now by design. DEV warns LOADING_ON_OUTSIDE_HOLD — at re-arm when an outside reporter holds the same source, and after the fact at a parked finalize when a staged swap's content settled with nothing but the action holding the frame — with the fix (latest() in on, or move the outside read under the boundary).

Mechanism

One drain. onNode runs the on thunk tracked; every run after the first queues the boundary (pendingRearms) and records the optimistic lane the pass ran under, if any. drainRearms() runs in GlobalQueue.run after the heap and before the verdict, under the notifying write's transaction. _rearm() collects the boundary's held readers from the live transactions' reporter registrations, releases (_initialized = false, wakeParked), and swaps: with no lane, a plain setSignal(_disabled, true) staged into the frame; with a lane, the value is committed outright and notifyOnLane (new, core.ts) walks the subscribers on the lane's channel so the output pass and its effects run from the lane queue at the park. finalizePureQueue no longer drains anything. Net −227 lines versus the two-drain version this PR started with.

Fallout

  • loading-on-rearm-3540.test.ts: held-action pin flips to fallback-at-commit; latest variant keeps the old expectation; Errored on cases removed.
  • packages/web/test/loading-on-keyed-boundary-3540.spec.tsx: matrix on-committed EARLY → HELD (a sibling reads the same data() and holds the frame; the source rule warns); held-action case flips; new rows for action-outlasts-data, latest, mainline; rows assert diagnostics; Errored on case removed.
  • ispending-in-boundary-on-3528.test.ts: intra-pass effect order for the isPending-driven on rows changed from count=1, A=Loading to A=Loading, count=1. Same pass, same end state; a lane-shown swap's effects run from the lane queue ahead of the regular queue.
  • New pins: loading-on-frame-following-3540.test.ts (all four cases, both orderings, diagnostics).
  • Docs: 05-async-data.md (rewritten on section with the three-frame example), 03-control-flow.md, MIGRATION.md, 08-dev-diagnostics.md, CHEATSHEET.md, JSDoc.
  • Size vs next (brotli): core +13 B, +createStore −18, +isPending/latest −21, hydrating +121, hydrating+store +105, CSR +49, CSR observe +120, CSR observe+attribution +87. Caps re-set with notes.

Known follow-up (not in this PR)

Reveal-hold bug, found while pinning this: a boundary whose fallback reads something not ready doesn't reveal landed content until the fallback's flight lands. Reproduces without on (general boundary bug). Pins on test/on-rearm-reveal-pins; rebases onto this.

Suites: signals 208/3500, solid 33/654, web 97/844; tsc and test-types green.

Refs #3540, #3524, #3529. Supersedes the trigger semantics of #3556.

ryansolid and others added 9 commits September 21, 2026 12:52
Co-authored-by: Cursor <cursoragent@cursor.com>
…ads stay display-ahead; DEV LOADING_ON_OUTSIDE_HOLD (#3540)

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…r +46 B (#3540)

Co-authored-by: Cursor <cursoragent@cursor.com>
…ction outlasts the data (#3540)

A frame-following swap staged by an `on` re-arm can be cleared before
any effect phase runs when the transaction parks past the content's
landing on nothing but the write's own action: no fallback is ever
displayed, and the source rule in `_rearm` cannot see it (nothing
outside the boundary reads the source). A DEV-only read-only boundary
sweep at the parked finalize (`_devHeldSweep`) reports it once per
re-arm; a frame held by other async data is a race the fallback may
still win and stays silent.

Co-authored-by: Cursor <cursoragent@cursor.com>
Re-derive each expectation from frame-following: a write inside a held
action holds the old DOM and lands the fallback WITH the commit; the
action outlasting the data shows no fallback and warns
LOADING_ON_OUTSIDE_HOLD; `latest()` in `on` shows the fallback now
beside the held DOM (new variant); a mainline write with nothing else
holding lands the fallback now (unchanged); the matrix's `on-committed`
row is held mid-flight because sibling B reads the same data (source
rule warns); Errored `on` unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ation, cheatsheet, JSDoc, LOADING_ON_OUTSIDE_HOLD (#3540)

Co-authored-by: Cursor <cursoragent@cursor.com>
…le out of prod bytes (#3540)

The DEV sweep is its own scheduler function (shaken in prod; the settling
sweep is unchanged), the emit and the rule are module-level functions
called only under __DEV__, and the per-re-arm flag is a bare optional
field. Prod's scheduler is byte-identical to before the rule.

Co-authored-by: Cursor <cursoragent@cursor.com>
Eight entries re-measured against next on this machine: core floor 9,640
→ 9,663; +createStore 16,811 → 16,851; +isPending/latest 12,391 → 12,415;
simple app 12,472 → 12,484 (within cap); hydrating 20,542 → 20,666;
hydrating+store 30,739 → 30,824; CSR 15,637 → 15,752; CSR observe 17,127
→ 17,272; CSR observe+attribution 27,614 → 27,698; frames unchanged.
The second re-arm drain in the scheduler and `_rearm(mainline)` /
`_swap` / the lane-aware on-node in boundaries; the DEV sweep is shaken.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 07e2a10

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

This PR includes changesets to release 11 packages
Name Type
@solidjs/signals Patch
solid-js Patch
test-integration Patch
@solidjs/web Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/universal Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics 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

@codspeed

codspeed Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 172 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing fix/loading-on-frame-following (07e2a10) with next (37fd1e6)2

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on next (07e2a10) during the generation of this report, so 37fd1e6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

…swap on the lane channel; drop Errored `on` (#3540)

The re-arm drains once, in flush after the heap and before the verdict.
A boundary whose on-node ran under an optimistic lane swaps by writing
_disabled committed and notifying its subscribers on that lane
(notifyOnLane), so the fallback shows now beside the held frame with no
second finalize drain, _rearmAhead/_swapOwed flags, or re-queue.

Errored's `on` prop / createErrorBoundary's `on` option are removed: a
retry recompute under the frame stages the recovered content with the
frame instead of retrying now, and keeping it would need a second drain
point nothing else uses.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@coveralls

coveralls commented Sep 22, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 35697053607

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.009%) to 72.775%

Details

  • Coverage increased (+0.009%) from the base build.
  • Patch coverage: 7 of 7 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1070
Covered Lines: 824
Line Coverage: 77.01%
Relevant Branches: 818
Covered Branches: 550
Branch Coverage: 67.24%
Branches in Coverage %: Yes
Coverage Strength: 17.38 hits per line

💛 - Coveralls

ryansolid and others added 2 commits September 21, 2026 23:30
… mention; changeset notes the removal (#3540)

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 4b01d81 into next Sep 22, 2026
6 checks passed
ryansolid added a commit that referenced this pull request Sep 22, 2026
…wing (#3540, #3575)

The two reveal-timing pins were written against #3556's mainline swap. Under
#3575 the swap is staged into the notifying write's transaction, and the
boundary's output is that frame's from then on:

- staged source: the action outlasts the data, so old content holds through
  the flight, `data 2` lands at the commit, the fallback is never shown, and
  LOADING_ON_OUTSIDE_HOLD reports once after the fact.
- independent source: the content's own hold (the pending write it derives
  from) is joined to the action's frame when the staged output pass reads
  its landing — reveal at the commit, no fallback, same diagnostic. The
  pre-#3575 sequence is the display-ahead read's (`latest()`), pinned
  beside it.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Sep 22, 2026
…wing (#3540, #3575)

The two reveal-timing pins were written against #3556's mainline swap. Under
#3575 the swap is staged into the notifying write's transaction, and the
boundary's output is that frame's from then on:

- staged source: the action outlasts the data, so old content holds through
  the flight, `data 2` lands at the commit, the fallback is never shown, and
  LOADING_ON_OUTSIDE_HOLD reports once after the fact.
- independent source: the content's own hold (the pending write it derives
  from) is joined to the action's frame when the staged output pass reads
  its landing — reveal at the commit, no fallback, same diagnostic. The
  pre-#3575 sequence is the display-ahead read's (`latest()`), pinned
  beside it.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Sep 22, 2026
…tent without waiting for the fallback's flight (#3540) (#3581)

* test(signals): pin what an on= re-arm does not license (#3540)

Reveal-timing and fallback-pending pins for re-armed Loading boundaries.
One pins a live defect: a re-armed boundary whose fallback is itself not
ready does not reveal landed content until the fallback's read lands.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(signals): re-derive the on= reveal-timing pins under frame-following (#3540, #3575)

The two reveal-timing pins were written against #3556's mainline swap. Under
#3575 the swap is staged into the notifying write's transaction, and the
boundary's output is that frame's from then on:

- staged source: the action outlasts the data, so old content holds through
  the flight, `data 2` lands at the commit, the fallback is never shown, and
  LOADING_ON_OUTSIDE_HOLD reports once after the fact.
- independent source: the content's own hold (the pending write it derives
  from) is joined to the action's frame when the staged output pass reads
  its landing — reveal at the commit, no fallback, same diagnostic. The
  pre-#3575 sequence is the display-ahead read's (`latest()`), pinned
  beside it.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(signals): judge a boundary whose fallback is pending before the verdict (#3540)

A collecting Loading boundary whose FALLBACK read something not ready left
its output pass pending on that read; an initialized parent held the frame
on it. When the content landed, the boundary was ready — but its output pass
derives from `_disabled`, not the tree, so the tree settling never re-ran
it; only the boundary sweep does, and that sweep ran at the commit
(finalizePureQueue), after the verdict the output's own read kept parking.
The content waited for the fallback's flight.

Judge that one shape before the verdict, under the transaction
(`CollectionQueue._judgeHeld`, walked by `checkBoundaryChildren(this, true)`
after the re-arm drain): a collecting boundary whose output is pending
stages `_disabled` false with the frame and its output re-runs in the same
heap, reads the tree, and drops the fallback's read through recompute's
ordinary settle of a pass's outgoing pending sources — the verdict sees the
release. Boundaries with a ready fallback park nothing and keep the commit
sweep's reveal unchanged.

The DEV after-the-fact LOADING_ON_OUTSIDE_HOLD rule (devHeldSweep) now also
recognizes a swap the pre-verdict sweep cleared while still staged
(`_disabled` staged false over a never-committed true) as never displayed;
`_checkSources` drops `_swapUnseen` only for a swap that committed.

Pins: the on= case (loading-on-rearm-reveal-3540 'content lands before the
fallback') and its no-on analogue (loading-fallback-release-3540, whose
second test shows the general computed case always released — the bug was
boundary-specific). Core floor +63 B, documented in treeshake.test.ts.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(size): re-set four caps for the pre-verdict boundary judgment (#3540)

+38..+81 B brotli per scenario, re-measured rebased over #3577 (origin/next
709c02b, same solid/web dist): the flush's pre-verdict boundary walk in
core, and `_judgeHeld` / `_output` in boundaries. Re-set: isPending/latest
(12.40 -> 12.45), hydrating no-stores (20.70 -> 20.75), CSR (15.75 ->
15.80, over #3577's own bump) and the CSR observe tier (17.30 -> 17.35,
no observe-gated bytes; brotli layout). Each carries its measurement
note. The simple-app floor (12,493 / 12.50) and observe + attribution
(27,743 / 27.75) now land within `next`'s caps and keep them; the other
four scenarios are within their caps too.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
ryansolid added a commit that referenced this pull request Sep 22, 2026
A <Loading> swap is a staged write that lands with its frame (#3575); the
engine held the fallback open from the swap and so timed — and, for a swap
the content outran or the commit's sweep cleared before any effect ran,
recorded — a fallback that was never on screen. The show now carries the
transaction the swap is staged in; the engine moves the open to the drain at
transitionSettled, follows transitionMerged, stamps `at` at flushEnd, and
drops an open hidden before that. The folds hear show/hide from the same
gate. Observe caps ratcheted (+72 B tier, +253 B engine); prod unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
ryansolid added a commit that referenced this pull request Sep 22, 2026
A <Loading> swap is a staged write that lands with its frame (#3575); the
engine held the fallback open from the swap and so timed — and, for a swap
the content outran or the commit's sweep cleared before any effect ran,
recorded — a fallback that was never on screen. The show now carries the
transaction the swap is staged in; the engine moves the open to the drain at
transitionSettled, follows transitionMerged, stamps `at` at flushEnd, and
drops an open hidden before that. The folds hear show/hide from the same
gate. Observe caps ratcheted (+72 B tier, +253 B engine); prod unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
ryansolid added a commit that referenced this pull request Sep 22, 2026
A <Loading> swap is a staged write that lands with its frame (#3575); the
engine held the fallback open from the swap and so timed — and, for a swap
the content outran or the commit's sweep cleared before any effect ran,
recorded — a fallback that was never on screen. The show now carries the
transaction the swap is staged in; the engine moves the open to the drain at
transitionSettled, follows transitionMerged, stamps `at` at flushEnd, and
drops an open hidden before that. The folds hear show/hide from the same
gate. Observe caps ratcheted (+72 B tier, +253 B engine); prod unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
ryansolid added a commit that referenced this pull request Sep 22, 2026
A <Loading> swap is a staged write that lands with its frame (#3575); the
engine held the fallback open from the swap and so timed — and, for a swap
the content outran or the commit's sweep cleared before any effect ran,
recorded — a fallback that was never on screen. The show now carries the
transaction the swap is staged in; the engine moves the open to the drain at
transitionSettled, follows transitionMerged, stamps `at` at flushEnd, and
drops an open hidden before that. The folds hear show/hide from the same
gate. Observe caps ratcheted (+72 B tier, +253 B engine); prod unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
ryansolid added a commit that referenced this pull request Sep 22, 2026
…ance-tracks) (#3580)

* feat(web): Chrome Performance panel tracks for Solid — engine foundation, adapter, timeline records

Stage 0 — shared engine foundation (@solidjs/signals, @solidjs/web):
- attribution.enable()/disable() are ref-counted: the engine is shared by
  every consumer on the page (a profiler track, an APM adapter, a
  diagnostics capture); the first hold installs, each enable() opens a fresh
  window over the ring buffers and folds without disturbing live tracking or
  others' subscriptions, the last disable() uninstalls.
- `checks` option: one switch for the five thresholded cost checks, for
  records-only consumers.
- isSilentHold / isLongHold exported, so adapters paint the engine's own
  verdicts rather than re-deriving thresholds.
- dispatchAsInteraction passes at: e.timeStamp; InteractionEvent gains
  inputDelayMs (event creation → handler entry, the INP input-delay term) and
  handlerMs measures from the handler's actual start.
- ownerPath re-exported from solid-js (client from @solidjs/signals, server
  via the ownerLabels twin).

Stage 1 — @solidjs/web/performance-tracks:
- enablePerformanceTracks() paints rerun / interaction / hold / navigation
  records and the web runtime's call / frame records as tracks in the
  `Solid` group, retroactively from the records' own performance.now()
  stamps: performance.measure with detail.devtools (rich: tooltips,
  properties; dev default) or the six-argument console.timeStamp (observe
  default). Track seeding for stable display order, minMs floor, scrub mode
  for shared traces, ref-counted disable. Prod artifact folds to a no-op.
- rollup dev/observe/prod entries, ./performance-tracks export conditions,
  types script.

Stage 2 — timeline records (@solidjs/signals, solid-js, @solidjs/web):
- Five listener-gated records on attribution.subscribe: create (a
  computation's creation run), effect (an effect callback, timed and joined
  to its compute run), flush (one scheduler drain), flight (an async flight
  to landing or abandonment), fallback (a loading boundary's fallback from
  show to hide). None is built, logged or folded unless a listener for its
  type exists; OBSERVE.subjectOf answers for the node-bearing ones.
- Core: flushStart hook beside flushEnd (one drain, never nested);
  effectRunStart now fires in observe like its effectRunEnd twin, so writes
  inside effect callbacks carry their effect origin in observe too. Observe
  core +67 B minified; prod byte-identical.
- The effect-frame → node map is filled by the first write inside a
  callback rather than by every callback (every reader resolves it through
  a write's origin; most callbacks never write). Enabled-engine cost per
  effect callback is at or below `next` on every configuration measured.
- Adapter: creation runs and callbacks on Effects/Memos, drains on a new
  Scheduler track, flights and fallbacks on a new Async track.

Tests: ref-counting, checks, inputDelayMs, the adapter spec (18), the
timeline records (12), dist-artifact coverage of both new hooks per tier.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): Propagation track — each drain as a wave of its root writes, runs labelled by cause, flow internals folded

Replaces the Scheduler track. Nothing re-renders in Solid, so React's
component flame has no counterpart; the picture is the graph a write
travelled. Each drain paints as a wave named by its root writes and the
interaction (`count 0 → 1 — click on button#next · 5 runs, 1 unchanged`);
compute runs, creations and effect callbacks paint inside it at their own
time labelled `node ← cause`, and the panel stacks them beneath the wave.

Labels fold framework structure into what the developer wrote: flow-control
internals (Show/Switch/Loading/Errored/Reveal) present as the tag,
`primitive.local` names as the primitive, the runtime name kept in a
`Node` property. The boundary nodes and Switch's condition builder are now
named in observe builds so the fold has names to key on.

Engine: ChangeRecord.nodeId on writes and derived changes, same id space as
RerunEvent.nodeId, so a derived cause joins the run that produced it.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* compiler: rename componentNames → sourceNames (boolean | { components })

The option becomes the home for every kind of source name the compilers
carry into output for the dev/observe runtimes to label the graph with;
`components` is the existing behavior, further kinds follow. Object form
picks kinds; `true` turns on every kind.

Both compilers, the node adapter's validation (rejects the old name and
malformed shapes), typings, READMEs, tests, and the web server test config.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* compiler: sourceNames.bindings — name compiled binding effects by their target

Attribute effects get `<tag>.<attribute>` as written (`span.textContent`,
`div.class:active`; a merged effect lists all its bindings), holes
`<tag>.children` through insert's options, spreads pass the tag and the
runtime labels `<tag>.spread` / `<tag>.children`. Static holes (component
calls, literals) create no effect and get no name. `prop:` the locked DOM
property pre-pass added is undone in the label (`input.value`).

@solidjs/web: `effect`/`insert` take `{ name }`, `spread` a trailing tag;
names land on the render effect nodes for the dev/observe tiers.

Both compilers, parity fixtures (dom-source-names suite replaces
dom-component-names, run with `sourceNames: true`), option validation,
runtime spec, READMEs.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* compiler: transformSourceNames pass names primitives from their declared identifier

The sourceNames.primitives half of Stage 3.5, as its own native pass since
it is plain JavaScript in and out and applies to .ts/.js modules: calls
resolving to solid-js / @solidjs/signals imports (createSignal, createMemo,
createOptimistic, createStore, createOptimisticStore, createProjection) gain
{ name } from the array pattern, binding, property key, or class field they
initialise, prefixed with the enclosing non-component function so a composed
primitive's nodes fold under it. Never overrides an explicit name; skips
spread/opaque options and the ambiguous two-argument createStore(x, y).

Stores now honour options.name in the observe tiers: property nodes read
todos.title instead of store.title, derived/optimistic stores included.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(performance-tracks): findings as markers, JSX-site stacks, richer node properties

Stage 4 of the Chrome performance tracks plan.

- Every DiagnosticEvent delivered while the tracks are enabled is a marker
  on the panel's Timings track (`SILENT_HOLD — <App> › <Search>`), coloured
  by severity; at warn or worse it carries detail.devtools.performanceIssue
  for the Insights sidebar, with the repair guide's section as learnMoreUrl.
  info stays a plain marker. Under the scrub only code, kind and owner travel.
  Plain mode uses the one-argument console.timeStamp.
- Dev: observedComponent stores console.createTask(label) on _component.task;
  the adapter emits every span and marker inside the nearest component's task
  so the entry's stack in the panel is the JSX site (React's _debugTask).
- Rich mode adds Owner path (unfolded), Node id and the root write's Origin
  to node spans.
- solid-js exports diagnosticGuideUrl(code), shared with the console footer.
- Docs: 08-dev-diagnostics gains the timeline records, the ref-counted
  enable, the INP join recipe and a performance-tracks section; the plan
  lands as documentation/plans/chrome-performance-tracks-plan.md.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(size): fold source-name plumbing out of prod; ratchet observe caps for the tracks records

Measured against the branch base (49887e1) with the size scenarios:
prod core floor, + createStore, + isPending and frames byte-identical;
simple app -26 B, CSR -7 B, hydrating + stores -38 B, hydrating +40 B (two
`computed(fn, void 0)` trailing arguments the boundary names fold to; the
restructured alternatives measured larger under brotli).

- createStoreNext loses its `name` parameter - a parameter survives into
  the prod artifact; the public createStore records the name through
  nameStore (same attrHooks gate) under __OBSERVE__ instead.
- web spread's `.spread`/`.children` labels sit behind "_SOLID_OBSERVE_" so
  prod folds them to undefined rather than testing the argument at runtime.
- Observe tier 17.00 -> 17.15 KB (17,063 measured, +92 B: flushStart site,
  effectRun hooks under __OBSERVE__, flow-internal and binding names, store
  names). Observe + attribution 27.50 -> 28.30 KB (28,200 measured, +738 B:
  ref-counted enable, checks, hold verdicts, at/inputDelayMs, the flush /
  create / effect / flight / fallback records, nodeId on derived causes).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(signals): fallback records time the display, not the swap

A <Loading> swap is a staged write that lands with its frame (#3575); the
engine held the fallback open from the swap and so timed — and, for a swap
the content outran or the commit's sweep cleared before any effect ran,
recorded — a fallback that was never on screen. The show now carries the
transaction the swap is staged in; the engine moves the open to the drain at
transitionSettled, follows transitionMerged, stamps `at` at flushEnd, and
drops an open hidden before that. The folds hear show/hide from the same
gate. Observe caps ratcheted (+72 B tier, +253 B engine); prod unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>

* fix(web): performance-tracks review fixes — single instance, server-inert export, guarded host calls, owned clearing

- enablePerformanceTracks: one instance per page; a call while enabled joins
  it and returns its own release; torn down at the last release
- ./performance-tracks: node/worker/deno conditions resolve to the inert
  artifact under every posture (the tracks are a browser view)
- Emitter guards performance/console calls; a throw drops the entry and
  never reaches the engine's record loop (dev warns once)
- rich:false falls back to performance.measure when console.timeStamp is
  missing (the mirror of the existing fallback)
- rich mode clears a User Timing name only while the timeline holds no more
  of it than the adapter made; an app measure sharing a label is left alone
- signals: stagedFallbacks is a WeakMap keyed by transaction (DRAIN is an
  object key) so a dropped transaction releases its staged opens
- changeset: stale 'Scheduler track' wording -> Propagation
- docs: transformSourceNames is the compiler pass, sourceNames.primitives
  the Vite plugin option

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(signals): attribution.enable returns its release; options layer per hold; dev component tasks only under an installed engine

Review items 5 and 6 on #3580.

- attribution.enable(opts) returns an idempotent release for the hold it
  takes. Options in effect = defaults with each live hold's opts layered in
  hold order, recomputed on release — a track's log:false beside a console
  session gives the log back when it leaves. disable() is the full teardown
  whatever holds are outstanding, so enable();enable();disable() cannot
  strand a hold. Prod twin: enable returns noop.
- performance-tracks releases through the token and layers log:false only
  when it is the consumer installing the engine (OBSERVE.attribution.installed
  === null); joining leaves a co-holder's options alone.
- solid-js dev: the component wrapper creates the console.createTask task
  only while an attribution engine is installed. The per-call stack capture
  roughly doubled dev mount (10k components 1.9 -> 4.1 ms) for sessions with
  nothing enabled; observe and prod builds never contained the call.
- Tests: layered options + release, disable teardown, adapter join vs
  install log default, task gating; docs and changesets updated.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* test: CI fixes — tuple typing in the tracks harness, fallback timing asserted against the commit stamp

- web test-types: the stubbed-API list is typed [object, string][] (the
  inferred (string | Console | Performance)[] failed tsc); getEntriesByName
  is restored with the rest
- signals: the shell-landed-first fallback test asserts at >= the landing
  stamp and shownMs >= the wait instead of a wall-clock upper bound, which a
  slow runner exceeded

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): captures hold the engine through enable()'s release token

capture.ts and the browser bridge release their own hold instead of calling
disable(), which under the token contract is the full teardown — a track
or APM adapter enabled beside a capture now survives it.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(signals): attribution options combine across holds by the most demanding request

Replaces last-wins layering. Each hold's request is resolved against the
defaults (an explicit undefined is unsaid; checks:false folds its five
checks first), then combined per key: booleans OR, historyLimit max, a
threshold config over false, and between configs the bound that fires
sooner (lower count/budget/ms, longer windowMs). A hold adds to what the
engine does and never takes away what another asked for, so the result is
independent of the order holds were taken — a track enabled with log:false
beside a console session leaves its log alone in either order; a capture
with tight thresholds beside a records-only adapter runs the checks for its
own duration. Prior art: Node trace_events (union of every enabled
Tracing's categories), CDP per-session domain enablement.

- performance-tracks: always asks {log:false, ...options.attribution};
  the install-time heuristic is gone
- tests: order independence, checks while any holder wants them,
  explicit undefined; adapter beside a console session
- size: engine scenario 28.85 -> 29.00 KB (+194 B, engine-only), noted

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(size): CSR prod cap 15.80 -> 15.85 KB after rebasing over #3586/#3555

The merge with the new next measured 15,815 B against next's 15,764: the
same +51 B brotli name-assignment noise this branch has carried (minified
bundles 44,649 B on both sides), now on top of upstream bytes those PRs
left 36 B under the cap. No prod source changed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: name the Vite plugin as the owner of sourceNames.primitives

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants