Skip to content

bench: construct subjects don't return proxies to await - #3926

Open
ryansolid wants to merge 3 commits into
nextfrom
bench/construct-no-thenable-probe
Open

ryansolid wants to merge 3 commits into
nextfrom
bench/construct-no-thenable-probe

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Summary

packages/signals/tests/store/utilities.bench.ts wraps every subject as () => subject.func(...args), so the benchmark function returns its result. CodSpeed's analysis runner times await fn() (@codspeed/vitest-plugin, runAnalysisBench). When the result is an object, await reads its .then before stopBenchmark(). For the construct subjects the result is a merge or omit view, so that read goes through the view's get trap, and for store-backed inputs through the store's read path, all inside the timed window.

This PR makes the subject wrapper a block body that assigns the result to a module-level sink. The construction stays reachable, so it can't be eliminated as dead code, and await sees undefined. Nothing else about what each subject measures changes.

Evidence (#3915)

#3915 showed omit-proxy-store(25, 5) › construct going from 341.1 µs to 373.2 µs (−8.6%). CodSpeed's call graphs for that run against next (893834c) show:

  • The subject itself is unchanged. func → omit costs about 20 µs on both sides. omit, OmitView, recordOf, readSource, isHidden, viewSource, getObserver and isWrappable all match.
  • There's a stray root. A separate root named get (store/utils.js, the omit view's trap) sits outside the subject's call tree. It calls readSource, getObserver and isWrappable, which is the .then probe going through the store. Its self cost is 308.8 µs on base and 340.8 µs on head, about 90% of the benchmark, and it holds the whole delta along with all of the run's system calls (8 to 10). That's one-off engine work landing in the window the probe holds open, not work in the subject.
  • The (100, 5) control doesn't have it. omit-proxy-store(100, 5) › construct runs the same code on four times the keys and costs 33.8 µs on both sides, with no stray get root. So about 90% of the (25, 5) figure is the probe window, not construction.
  • fix(signals): retain store affects registration in production bundles #3912 shows identical numbers. #3912, a different change, shows the same 340.8 µs to 373.2 µs on this subject. It shifts with module layout, not with the code under test.

Benchmarks touched

All of them are in packages/signals/tests/store/utilities.bench.ts. The wrapper is shared, so every subject in the file now goes through the sink:

  • omit-static(…), omit-signal(…), omit-mixed(…) › omit
  • merge-static(…), merge-signal(…), merge-mixed(…) › merge
  • merge-merge-static(…), merge-merge-signal(…), merge-merge-mixed(…) › merge
  • omit-proxy-store(…) › construct, readAllowed, readBlocked, hasAllowed, ownKeys
  • merge-proxy-keys-store(…) › construct, ownKeys, read

Baselines for these will shift once. The other bench files already use block bodies or a local sink, and none of them returns a proxy to await. They're unchanged.

CodSpeed result on this PR

All changes are in utilities.bench.ts. Outside it, nothing moved beyond noise (dbmon shallow full tick changed 2–5% and was marked NoChange).

  • 77 subjects that return a view got faster. Most omit / merge / merge-merge subjects run about 37–44 µs on next and about 18–27 µs here. The probe was roughly 15–20 µs of each measurement. The construct subjects on store-backed inputs dropped 15–38%, and omit-proxy-store(25, 5) › construct went from 341.1 µs to 30.4 µs.
  • The ~300 µs one-off moved; it didn't go away. omit-proxy-store(25, 5) › readAllowed went from 24.9 µs to 337.7 µs. Its call graph shows the same signature the stray get root had on next: about 205 µs of instructions plus 8 system calls, now as self cost in serveDataKey (store/store.js). On next, the .then probe was just the first store-trap read in that group's timed windows, so the one-off landed in construct. With the probe gone, the first read is readAllowed's. The one-off is engine work tied to that first read, not the probe and not work in the subject. Moving it out of the timed windows entirely would need the group to warm the store read path before measuring, which this PR doesn't do.
  • Small read-side shifts. A few primitive-returning subjects in the store-backed groups got 1–2 µs slower: readBlocked and hasAllowed 5–9%, merge-proxy-keys-store › read 7–9%. Their call graphs show the extra cost as self time in the omit view's get and in isHidden. That's consistent with the trap's inline caches no longer having been primed by the construct probe; it isn't new work. Giving each subject its own sink made no difference (tried and reverted on this branch).

Public API changes

None. This is tooling only: no packages/*/src changes, so there's no changeset.

The CodSpeed analysis runner times `await fn()`; a subject returning a
merge/omit view had its `.then` probed through the proxy (and store)
traps inside the timed window. Assign results to a module-level sink.

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

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 2e46cf6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions

github-actions Bot commented Oct 8, 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.43 KB 0 B 0 B +15 B 7.45 KB ✅
signals: + createStore 14.70 KB 0 B 0 B +10 B 14.70 KB ✅
signals: + isPending/latest 9.63 KB 0 B 0 B +15 B 9.65 KB ✅
app: render + one signal (the simple-app floor) 9.92 KB 0 B 0 B +15 B 9.93 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.89 KB 0 B 0 B +15 B 17.91 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.15 KB 0 B 0 B +65 B 29.19 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.92 KB 0 B 0 B +15 B 12.96 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.53 KB 0 B 0 B +15 B 14.53 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.85 KB 0 B 0 B +15 B 28.89 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.12 KB 0 B 0 B +15 B 10.13 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.22 KB 0 B 0 B +65 B 25.24 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.21 KB 0 B 0 B +10 B 31.17 KB ⚠️ over by 43 B, 10 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.11 KB 0 B 0 B 0 B 11.13 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.91 KB 0 B 0 B +161 B 33.92 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.21 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.61 KB 0 B 0 B +15 B 37.59 KB ⚠️ over by 20 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.20 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) 35.15 KB 0 B 0 B +15 B 35.13 KB ⚠️ over by 16 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.20 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.65 KB 0 B 0 B +15 B 40.66 KB ✅ eager (counted): web.js 22.02 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.21 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.24 KB 0 B 0 B 0 B 41.26 KB ✅ assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.24 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.23 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.92 KB 0 B 0 B 0 B 46.93 KB ✅ eager (counted): client.js 27.80 KB; assets.js 0.78 KB, bind.js 1.86 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, serverForms.js 3.31 KB, trace.js 8.20 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.40 KB 0 B 0 B +4 B 20.42 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 43 B; minified 99,682 B vs 99,672 B recorded with the cap (+10 B) — 10 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 20 B; minified 117,456 B vs 117,441 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread): over brotli cap by 16 B; minified 109,461 B vs 109,446 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base

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).

@coveralls

coveralls commented Oct 8, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37835203882

Coverage remained the same at 76.43%

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: 1227
Covered Lines: 996
Line Coverage: 81.17%
Relevant Branches: 958
Covered Branches: 674
Branch Coverage: 70.35%
Branches in Coverage %: Yes
Coverage Strength: 28.51 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 10 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 77 improved benchmarks
❌ 10 regressed benchmarks
✅ 101 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ readAllowed 24.9 µs 337.4 µs -92.61%
❌ memo + sync render effect only (reference) 28 ms 32.3 ms -13.28%
❌ readBlocked 14.6 µs 16.1 µs -9.18%
❌ read 24.4 µs 26.7 µs -8.63%
❌ hasAllowed 20.6 µs 22.5 µs -8.41%
❌ read 21.4 µs 23 µs -7.01%
❌ hasAllowed 20.1 µs 21.3 µs -5.52%
❌ readBlocked 13.5 µs 14.3 µs -5.19%
❌ readBlocked 13.6 µs 14.3 µs -5.17%
❌ hasAllowed 20.5 µs 21.6 µs -5.17%
⚡ merge 819.8 µs 27.4 µs ×30
⚡ construct 341.1 µs 30.3 µs ×11
⚡ merge 42.5 µs 21.8 µs +94.45%
⚡ merge 39.8 µs 20.9 µs +90.1%
⚡ merge 39.7 µs 20.9 µs +89.66%
⚡ merge 39.6 µs 21 µs +88.96%
⚡ merge 39 µs 20.7 µs +88.19%
⚡ omit 36.1 µs 19.2 µs +88.13%
⚡ merge 38.9 µs 20.7 µs +87.48%
⚡ omit 38 µs 20.3 µs +87.11%
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing bench/construct-no-thenable-probe (7551870) with next (893834c)

Open in CodSpeed

ryansolid and others added 2 commits October 8, 2026 12:24
A shared sink took objects, arrays, booleans and numbers across subjects;
the primitive-returning read subjects measured 5-9% slower under it.

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