feat: Chrome Performance panel tracks for Solid (@solidjs/web/performance-tracks) - #3580
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: 5e7ebb5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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
force-pushed
the
feat/performance-tracks
branch
from
September 22, 2026 19:00
34fc470 to
a69f52d
Compare
ryansolid
added a commit
that referenced
this pull request
Sep 22, 2026
…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>
Coverage Report for CI Build 35791922360Coverage increased (+0.08%) to 72.852%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will not alter performance
Comparing Footnotes
|
ryansolid
added a commit
that referenced
this pull request
Sep 22, 2026
…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>
ryansolid
force-pushed
the
feat/performance-tracks
branch
from
September 22, 2026 21:03
76ae0e4 to
f491538
Compare
ryansolid
added a commit
that referenced
this pull request
Sep 22, 2026
…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>
ryansolid
force-pushed
the
feat/performance-tracks
branch
from
September 22, 2026 22:04
f491538 to
a2634a3
Compare
…ion, 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>
…s, 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>
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>
…ir 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>
…red 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>
…r 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>
…aps 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>
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>
…nert 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>
…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>
…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>
… 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>
…emanding 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>
…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>
Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid
force-pushed
the
feat/performance-tracks
branch
from
September 22, 2026 22:21
a2634a3 to
5e7ebb5
Compare
This was referenced Sep 22, 2026
This was referenced Sep 23, 2026
ryansolid
added a commit
that referenced
this pull request
Sep 24, 2026
…he correction An optimistic override the screen displayed is replaced by a different value on two roads: the guess lifts at settle back to the committed value it covered (the action failed, or never wrote what it promised), or an authoritative value lands that differs from it. Both are the runtime doing what optimistic UI promises, so the finding is `info` and structured-channel only — a count that grows for one source is what says the guess, or the failure rate, is wrong. Judged by the node's own equality. `AttributionHooks.optimisticReverted(el, shown, truth, how)` is the seam, fired from supersedeOverride (how derived from whether a landing is staged) and from the non-superseded drop in resolveOptimisticNodes. Not covered yet, and said so: optimistic stores, and the interaction that wrote the guess (optimistic writes bypass the write hook, so the node carries no origin). The plan marks item 2 satisfied by #3580 — `at` equals the Event Timing entry's `startTime`; `interactionId` is not readable at dispatch. Size: engine +266 B, tier +38 B; the engine cap moved with a note. Co-Authored-By: Claude via Cursor <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid
added a commit
that referenced
this pull request
Sep 24, 2026
…he correction An optimistic override the screen displayed is replaced by a different value on two roads: the guess lifts at settle back to the committed value it covered (the action failed, or never wrote what it promised), or an authoritative value lands that differs from it. Both are the runtime doing what optimistic UI promises, so the finding is `info` and structured-channel only — a count that grows for one source is what says the guess, or the failure rate, is wrong. Judged by the node's own equality. `AttributionHooks.optimisticReverted(el, shown, truth, how)` is the seam, fired from supersedeOverride (how derived from whether a landing is staged) and from the non-superseded drop in resolveOptimisticNodes. Not covered yet, and said so: optimistic stores, and the interaction that wrote the guess (optimistic writes bypass the write hook, so the node carries no origin). The plan marks item 2 satisfied by #3580 — `at` equals the Event Timing entry's `startTime`; `interactionId` is not readable at dispatch. Size: engine +266 B, tier +38 B; the engine cap moved with a note. Co-Authored-By: Claude via Cursor <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid
added a commit
that referenced
this pull request
Sep 24, 2026
…howed was replaced (#3607) * feat(signals): OPTIMISTIC_REVERTED — the person saw the guess, then the correction An optimistic override the screen displayed is replaced by a different value on two roads: the guess lifts at settle back to the committed value it covered (the action failed, or never wrote what it promised), or an authoritative value lands that differs from it. Both are the runtime doing what optimistic UI promises, so the finding is `info` and structured-channel only — a count that grows for one source is what says the guess, or the failure rate, is wrong. Judged by the node's own equality. `AttributionHooks.optimisticReverted(el, shown, truth, how)` is the seam, fired from supersedeOverride (how derived from whether a landing is staged) and from the non-superseded drop in resolveOptimisticNodes. Not covered yet, and said so: optimistic stores, and the interaction that wrote the guess (optimistic writes bypass the write hook, so the node carries no origin). The plan marks item 2 satisfied by #3580 — `at` equals the Event Timing entry's `startTime`; `interactionId` is not readable at dispatch. Size: engine +266 B, tier +38 B; the engine cap moved with a note. Co-Authored-By: Claude via Cursor <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com> * chore(size): re-measure OPTIMISTIC_REVERTED at landing; engine cap 31.45 -> 31.65 KB Co-authored-by: Claude via Cursor <noreply@cursor.com> * fix(signals): OPTIMISTIC_REVERTED never judges the runtime's own optimistic nodes The finding fired on every acknowledged hold: isPending()'s companion is an optimistic signal that goes true while pending and back to false at commit by design — the acknowledgement SILENT_HOLD asks for — so the repair for one finding produced another. The check now skips companions and derived overrides by the predicate the hold census already uses (`isCompanion`), and `optimisticReverts: false` disables it like the other verdicts. The regression test is the acknowledged hold itself (isPending and latest readers over a held write): silent with the guard, one false finding without it. Co-Authored-By: Claude via Cursor <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com> * chore(size): re-measure the OPTIMISTIC_REVERTED companion guard; engine 31,625 B under the 31.65 KB cap The `optimisticReverts` option and the `isCompanion` skip in checkOptimisticRevert are +35 B on the engine scenario (31,590 -> 31,625 B). The cap holds; the note records the new measurement. Tier unchanged at 17,618. Co-authored-by: Claude via Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude via Cursor <cursoragent@cursor.com> Co-authored-by: Claude via Cursor <noreply@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@solidjs/web/performance-tracks: Solid's attribution records rendered as custom tracks on Chrome's Performance panel, through the panel's extensibility API (console.timeStamp/performance.measurewithdetail.devtools) — the same mechanism React 19.2's Performance Tracks use fromreact-dom. Same records, two renderings: the diagnostics artifact for an agent, the track for the human, consistent because they are one stream and every label comes from the shared formatters (formatOrigin,formatRerun,ownerPath).Plan and decisions:
documentation/plans/chrome-performance-tracks-plan.md. User docs:documentation/solid-2.0/08-dev-diagnostics.md, "Chrome Performance panel".InteractionswarningPropagationcount 0 → 1 — click on button#next · 5 runs, 1 unchanged), the runs beneath it labellednode ← causeEffects/Memoswarningwhen the run changed nothingAsyncabandoned=warning); fallbacks displayed → hiddenHoldswarningsilent,errorlong — the engine's own verdictsNavigationsServerperformanceIssueatwarn+Where this differs from React
Propagationtrack paints the graph a write travelled: a wide flat wave is a coarse signal everyone depends on, a deep one a chain of memos, awarningnode with nothing after it the equality cutoff at work.enable()/disable()are ref-counted so consumers coexist.DiagnosticEvent(silent hold, waterfall, unstable memo…) is a marker with aperformanceIssueannotation linking its repair guide; React's tracks carry no verdicts.InteractionEvent.atis the browser event'stimeStamp, soPerformanceEventTiming.startTime === interaction.atjoins Solid's interaction span to Chrome's INP entry directly.console.createTaskon the component record.Commits
One per stage; each with its changeset (all
patch, prerelease).b512cb30dStage 0–2 — ref-counted engineenable/disable,checksoption,isSilentHold/isLongHold,at/inputDelayMs; the adapter; five listener-gated timeline records (flush,create,effect,flight,fallback) and theflushStarthook.effectRunStart/Endmove from__DEV__to__OBSERVE__; a 27% enabled-path regression found and removed (effect frames registered lazily).fc03377b1Stage 3 —Propagationtrack;ChangeRecord.nodeId; flow-control internals named in observe (children,boundary,value,reveal order,conditions).8cbe1e6133c285b2a2ed273ecf3Stage 3.5 — compilers:componentNames→sourceNames: boolean | { components?, bindings? }. Binding effects named by target (span.textContent,div.class:active,div.spread/div.children); a separatetransformSourceNames(code, opts)pass (Oxc, plain JS in/out, runs on.tstoo — not atransform()option, since primitives live outside components) names primitives after their declared identifier; stores honourname. The@solidjs/vite-pluginside is staged separately and waits on the compiler release.1e26978edStage 4 — findings as Timings markers +performanceIssue;console.createTaskstacks;Owner path/Node id/Originproperties;diagnosticGuideUrlexport.a23eb4d04Size audit — all source-name plumbing folded out of prod (spread labels via a module-levelspreadName, boundary names gate the call,createStore's name branch under__OBSERVE__). Minified prod bundles are structurally identical tonext(same byte count, identifier-normalised diff empty); the ±50 B brotli movement is esbuild's name assignment, documented in the caps.34fc47036Fallback record fix (found rebasing over fix(signals): Loadingonfollows the frame; DEV LOADING_ON_OUTSIDE_HOLD (#3540) #3575) — a<Loading>swap is a staged write that lands with its frame; the engine timed the fallback from the swap and so recorded spinners that were never on screen (theLOADING_ON_OUTSIDE_HOLDshape: committed and cleared by the same sweep before any effect ran).boundaryFallbacknow carries the transaction the swap is staged in; the engine stampsatat theflushEndof the drain that rendered it and drops an open hidden before that. Two tests pin both product-page shapes.Review fixes (the six commits after the stage commits)
enablePerformanceTracks()while enabled joins the running instance (its options are not re-read) and returns its own release; the instance is torn down at the last release. An HMR re-evaluation no longer paints twice or strands a hold../performance-trackscarriesnode/worker/denoconditions resolving to the inert artifact under every posture — verified undernode --conditions=development: no closure, no measures, no hold. Pinned by a test on the manifest.performance/consolecall: a throw drops the entry (dev warns once) and never reaches the engine's record loop.rich: falsefalls back toperformance.measurewhereconsole.timeStampis missing (mirror of the existing fallback); rich mode clears a User Timing name only while the timeline holds no more entries of it than the adapter made, so an app measure sharing a label is left alone.stagedFallbacksis aWeakMapkeyed by the transaction (DRAINis an object key): a transaction dropped without settling or merging releases its staged opens.Propagation; docs now saytransformSourceNamesis the compiler pass andsourceNames.primitivesthe Vite plugin's option.attribution.enable(opts)returns the release of its hold (idempotent, likesubscribe);disable()is the full teardown whatever holds are outstanding, soenable(); enable(); disable()cannot strand a hold (the provenance test idiom). Options combine across holds by the most demanding request per key (booleans OR,historyLimitmax, a threshold config overfalse, between configs the bound that fires sooner) — a hold adds to what the engine does and never takes away what another asked for, so the result is independent of enable order: a track'slog: falsebeside a console session leaves the log alone; a capture with tight thresholds beside a records-only adapter runs the checks for its own duration. Prior art: wake-lock sentinels / RxrefCountfor the hold, Nodetrace_events(union of enabled categories) and CDP per-session enablement for the merge. The adapter and@solidjs/diagnosticscaptures release through the token.console.createTasktask only while an attribution engine is installed (OBSERVE.attribution.installed !== null). A dev session with nothing enabled pays nothing; the tracks enabled at bootstrap see every component's site. Observe and prod artifacts never contained the call (verified:createTaskappears insolid.dev.jsonly).Public API changes
Breaking
@solidjs/babel-plugin/@solidjs/compiler: optioncomponentNames: booleanremoved (no alias) →sourceNames: boolean | { components?: boolean; bindings?: boolean }.trueturns every kind on.bindingsis DOM output only.@solidjs/signalsattribution.enable(opts): () => void— each call is a hold and returns its release (wasvoid); options combine across live holds by the most demanding request per key (a released hold withdraws its requests; an explicitundefinedis unsaid and takes the default); the engine is uninstalled at the last release.disable()is the full teardown whatever holds are outstanding (unchanged in effect fromnext, where onedisable()uninstalled). Prod twin:enablereturns a no-op.@solidjs/signalsInteractionEvent.atis now the browser event'stimeStamp(the INP join) rather than the engine'sperformance.now()at dispatch; the dispatch-side delay is the newinputDelayMs.Added
@solidjs/web: new export path./performance-tracks—enablePerformanceTracks(options?: PerformanceTracksOptions): () => void,PerformanceTracksOptions { attribution?, minMs?, rich?, group?, scrub? }. Browser dev/observe artifacts; inert undernode/worker/denoand in prod.@solidjs/signals: five record types onAttributionRecords/attribution.subscribe(type, …)—create(CreateEvent),effect(EffectRunEvent),flush(FlushEvent),flight(FlightEvent),fallback(FallbackEvent). Built only while a listener exists.@solidjs/signals:isSilentHold(hold),isLongHold(hold)(the engine's hold verdicts, exported;() => falsein the prod twin);AttributionOptions.checks?: boolean(records only whenfalse);InteractionEvent.inputDelayMs?;ChangeRecord.nodeId;StoreOptions.namehonoured for property-node labels (todos.title).@solidjs/compiler:transformSourceNames(code, { filename?, sourceMap? })/transformSourceNamesAsyncandTransformSourceNamesOptions— the primitives-naming pass.solid-js(client and server):ownerPath(owner)re-exported from@solidjs/signals;diagnosticGuideUrl(code)— the repair guide's section URL for a diagnostic code.@solidjs/webcompile targets:effect(fn, effectFn, options?: { scope?, name? })gainsname;insert(parent, accessor, marker?, init?, options?)gainsoptions.name;spread(node, props, skipChildren?, skip?, name?)gains a fifthnameargument. Emitted by the compilers undersourceNames.bindings; ignored by production runtimes (prod output is structurally identical tonext)._component.task(aconsole.createTasktask, present only for components rendered while an attribution engine is installed) — internal shape, listed for completeness.Internal hooks changed (
@solidjs/signalsAttributionHooks, not a public export):flushStartadded;boundaryFallback(boundary, tree, shown, transition?)gains the transaction argument;effectRunStart/Endnow fire under__OBSERVE__(were__DEV__).Cost
next; app scenarios structurally identical — re-verified after the third rebase (over fix(signals): rewriting an orphan override opens no transaction (#3576) #3586, fix(signals): a reawakened lazy memo rejoins its owner's chain #3555): CSR minified bundles 44,649 B on both sides, +51 B brotli name-assignment noise, CSR cap 15.80 → 15.85 KB with the note.nextat the fix(signals): Loadingonfollows the frame; DEV LOADING_ON_OUTSIDE_HOLD (#3540) #3575 merge — 2,000 effects on one signal, and 200<Loading>boundaries over memos — disabled / enabledchecks:false/checks:true, min-of-5 medians: every cell within ±6%, signs both ways (parity within noise).Verification
CI green on the head (test, coverage, types, size, benchmarks). Locally after the second rebase (over #3559, #3562, #3584): compiler 5844 / babel-plugin 265 / signals 3537 / solid 655 / web 894 / diagnostics 34 green;
typesclean; size-limit green. Verified visually in Chrome on a scratch Vite page: tracks, waves, markers with the Insights annotation, and the JSX-site stack on a span.Follow-ups (not in this PR)
@solidjs/vite-pluginsourceNamesintegration (worktree staged; needs the compiler release).checksdefaults to under a records-only consumer) is still open there.