Skip to content

Feat: Signal Safety Presets#211

Closed
jlukic wants to merge 8 commits into
mainfrom
feat/signal-safety-bisect
Closed

Feat: Signal Safety Presets#211
jlukic wants to merge 8 commits into
mainfrom
feat/signal-safety-bisect

Conversation

@jlukic
Copy link
Copy Markdown
Member

@jlukic jlukic commented May 16, 2026

Existing signals safety PR #148 #150 have too many confounding performance issues with baseline signals implementation which has been separately optimized. This PR intends to reimplement safety presets with a careful look at performance tradeoffs for all hotpaths from the increased overhead of protect().

This PR will help decide whether we ship TC39 reference safety (none), freeze, or the ultra safe clone based on real world bench.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
semantic-next Ready Ready Preview, Comment May 17, 2026 0:26am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mcp Ignored Ignored Preview May 17, 2026 0:26am

Request Review

@github-actions github-actions Bot added the Reactivity Modifies reactivity package label May 16, 2026
@jlukic jlukic marked this pull request as ready for review May 16, 2026 21:57
@semantic-performance-bot
Copy link
Copy Markdown

semantic-performance-bot Bot commented May 16, 2026

🟡 Mixed (mostly slower) for 206be6d on Benchmark Suite 📊

Base: main · Action: #25976810328 · Raw: bench-report.json

Feat: Signal Safety Presets

Warning

This PR improves ✅ 2 tests while regressing on ❌ 3 tests.

✅ 2 faster · ❌ 3 slower · 🔍 29 unsure · ⚪ 33 no change · 🏆 2 new peaks · 📜 14 reopened


✅ Faster (2)

Metrics where this PR confidently improved performance compared to main.

metric Improvement
renderer-micros:expr-simple-100k -17% (4ms) ⭐
renderer-micros:expr-js-10k -8% (1ms)

❌ Slower (3)

Metrics where this PR confidently regressed performance compared to main.

metric Regression
todo:edit-cycle-5 +73% (48ms) ‼️
todo:edit-start-10 +9% (6ms)
template:each-mount-1000 +4% (2ms)

🏆 New peaks (2)

These metrics hit a new best on this PR. The most recent candidate is usually the cause.

metric improvement prior peak likely candidates
renderer-micros:expr-simple-100k 16% 8a1f872 8fe7563, a93fec4, fdf6b77 (+3 more)
renderer-micros:expr-js-10k 7% 8a1f872 8fe7563, a93fec4, fdf6b77 (+3 more)

📜 Regressions from peak (14)

These metrics were faster on an earlier push to this PR. The most recent candidate is usually where to look.

metric regression prior peak likely candidates
todo:edit-cycle-5 73% 6e576a5 8fe7563, a93fec4, fdf6b77
signal:computed-subscribe-unsubscribe-10k 24% fdf6b77 8fe7563, a93fec4
signal:computed-unobserved-200x500 15% fdf6b77 8fe7563, a93fec4
signal:computed-chain-10x60k 10% a93fec4 8fe7563
todo:edit-start-10 9% 6e576a5 8fe7563, a93fec4, fdf6b77
signal:reaction-dep-diff-45k 9% fdf6b77 8fe7563, a93fec4
hydrate:helper-100-state-change-1k 7% a93fec4 8fe7563
signal:reactive-set-index-300 6% a93fec4 8fe7563
signal:reaction-coalesce-400x100 5% a93fec4 8fe7563
signal:reactive-list-filter-1000x300 5% a93fec4 8fe7563
template:each-mount-1000 4% 6e576a5 8fe7563, a93fec4, fdf6b77
signal:reactive-multi-read-5x160k 4% a93fec4 8fe7563
template:subtemplate-helpers-heavy-100x500 3% fdf6b77 8fe7563, a93fec4
signal:reactive-set-property-by-id-200 2% fdf6b77 8fe7563, a93fec4
⚪ No Change (33)

Metrics where this PR measured within ±2% of main — no meaningful performance change detected.

metric Change
todo:add-20 -0.8% – +1.9%
todo:bulk-add-500 -0.6% – +0.5%
krausest:clear-10k -1.7% – +1.4%
todo:clear-completed-250 -1.2% – +1.6%
signal:computed-chain-10x60k -1.5% – +1.1%
krausest:create-10k -0.2% – +0.7%
krausest:create-1k -1.1% – +1.2%
renderer-micros:dom-walker-1000x15 -1.9% – +0.9%
hydrate:each-100 -1.1% – +0.1%
hydrate:each-100-mount -1.0% – +0.5%
todo:filter-cycle-20 -0.6% – +0.8%
signal:flush-fanout-allocation-1000x500 -0.8% – +1.3%
hydrate:helper-100-mount -1.3% – +0.6%
signal:reaction-coalesce-400x100 -0.3% – +0.9%
signal:reaction-flush-noop-5m -1.6% – +0.8%
signal:reactive-fanout-500x1200 -1.1% – +1.5%
signal:reactive-multi-read-5x160k -1.6% – +1.9%
signal:reactive-push-2000x20 -1.4% – +1.0%
signal:reactive-set-property-by-id-200 -1.3% – +0.4%
signal:reactive-stable-fanout-5000x100 -1.0% – +1.2%
todo:remove-50-back -0.7% – +1.2%
todo:remove-last-100 -1.1% – +1.4%
todo:rename-500 -1.1% – +0.7%
krausest:select-40 -0.6% – +1.1%
signal:set-same-10m -0.8% – +0.9%
template:snippet-in-subtemplate-100x1k -2.0% – +1.7%
template:subtemplate-data-blob-100 -0.9% – +1.2%
template:subtemplate-helpers-heavy-100x500 -0.2% – +1.3%
template:subtemplate-helpers-light-100x500 -0.3% – +1.9%
template:subtemplate-reactive-data-100x500 -0.9% – +1.8%
todo:toggle-all-200 -1.1% – +0.9%
todo:toggle-last-100 -1.7% – +1.6%
todo:toggle-middle-100 -1.5% – +1.8%
🔍 Unsure (29)

Too Fast to Measure Precisely (29)

On benches this short, OS jitter, GC, and JIT pauses drown out anything under 4%. Bigger changes than that still show up.

metric Change Test Time Expected Noise
template:active-indicator-200 -2.6% – +0.5% ~32ms ±5%
template:active-indicator-nested-200 -3.2% – +1.0% ~16ms ±7%
krausest:append-1k +0.0% – +5.4% ~96ms ±7%
renderer-micros:build-html-string-10k +0.1% – +2.3% ~48ms ±6%
signal:computed-subscribe-unsubscribe-10k -6.2% – +4.3% ~11ms ±12%
signal:computed-unobserved-200x500 -2.3% – +1.5% ~23ms ±4%
renderer-micros:expr-lisp-50k -3.5% – -1.6% ~40ms ±5%
hydrate:helper-100-state-change-1k -0.3% – +3.6% ~3ms ±9%
signal:reaction-dep-diff-45k -1.8% – +2.2% ~36ms ±5%
signal:reactive-list-filter-1000x300 -2.5% – +0.5% ~122ms ±4%
signal:reactive-list-replace-1000x1000 -1.7% – +2.2% ~322ms ±5%
signal:reactive-set-index-300 -2.0% – +0.9% ~115ms ±4%
signal:reactive-stable-deps-3reads-5000x100 -1.7% – +2.6% ~199ms ±5%
todo:remove-50-front -3.1% – +5.9% ~16ms ±12%
todo:remove-50-middle -3.2% – +1.8% ~14ms ±4%
todo:remove-first-100 -1.4% – +2.4% ~57ms ±4%
todo:remove-middle-100 -2.2% – +2.4% ~53ms ±6%
krausest:remove-row-back-100 -12.9% – +7.5% ~18ms ±28%
krausest:remove-row-front-20 -7.2% – +6.6% ~9ms ±18%
krausest:remove-row-middle-20 -15.5% – +10.0% ~8ms ±32%
krausest:replace-1k -2.6% – +0.3% ~114ms ±4%
template:snippet-args-per-key-100x500 -0.7% – +2.7% ~33ms ±7%
template:stable-ref-mutate-500 -4.4% – +1.7% ~13ms ±11%
signal:sub-unsub-100k -0.3% – +2.7% ~27ms ±4%
template:subtemplate-shorthand-props-100x500 -2.6% – +0.3% ~43ms ±5%
krausest:swap-rows-20 -4.0% – +3.4% ~102ms ±9%
todo:toggle-100 -1.8% – +2.2% ~42ms ±5%
todo:toggle-first-100 -0.8% – +2.9% ~58ms ±5%
krausest:update-10th-50 -7.8% – +4.0% ~25ms ±16%
📖 Bench glossary (67 metrics)
metric what it tests
hydrate:each-100 Reassigns the items of a hydrated 1000-item list to a fresh array with the same keys and data.
hydrate:each-100-mount Hydrates a server-rendered 1000-item list and waits for it to become interactive without re-rendering.
hydrate:helper-100-mount Hydrates a 1000-item list where each item calls a helper that reads state shared across the list.
hydrate:helper-100-state-change-1k Walks the shared activeID across every item in a hydrated 1000-item list so two items repaint per cycle.
krausest:append-1k Appends 1000 new rows onto an existing 1000-row table.
krausest:clear-10k Clears a 10000-row table back to empty in a single operation.
krausest:create-10k Renders a fresh 10000-row table into an empty parent at ten times the create-1k scale.
krausest:create-1k Renders a fresh 1000-row table into an empty parent.
krausest:remove-row-back-100 Removes the last row 100 times from a 1000-row table, with no other rows needing to move.
krausest:remove-row-front-20 Removes the first row 20 times from a 1000-row table, with all remaining rows sliding up each time.
krausest:remove-row-middle-20 Removes the middle row 20 times from a 1000-row table, with the rows below it sliding up each time.
krausest:replace-1k Replaces 1000 rows with a fresh 1000-row set, diffing the keyed list against a populated table.
krausest:select-40 Highlights one row at a time across 40 rows so only the previous and newly highlighted rows update.
krausest:swap-rows-20 Swaps the second and second-to-last rows in a 1000-row table, repeated 20 times.
krausest:update-10th-50 Updates the label on every tenth row of a 1000-row table, looped 50 times to lift the work above noise.
renderer-micros:build-html-string-10k Builds the HTML string for a realistic card AST 10000 times. Raw assembly throughput.
renderer-micros:dom-walker-1000x15 Runs bindMarkers across a 1000-node card fragment 15 times. TreeWalker pass and binding dispatch.
renderer-micros:expr-js-10k Evaluates one arithmetic expression and one ternary 10000 times each. JS-eval hot path.
renderer-micros:expr-lisp-50k Evaluates one Lisp-style helper call 50000 times. Parse-cache lookup and helper dispatch.
renderer-micros:expr-simple-100k Evaluates one simple identifier and one dotted path 100000 times each. Property-lookup hot path.
signal:computed-chain-10x60k Propagates a value change from root to leaf through a 10-deep chain of derived signals 60000 times.
signal:computed-subscribe-unsubscribe-10k 10000 create-computed + attach-observer + detach cycles. Lifecycle cost the refcount path must keep acceptable.
signal:computed-unobserved-200x500 200 unobserved computed signals, root updated 500 times. Measures the eager-recompute cost the refcount removes.
signal:flush-fanout-allocation-1000x500 500 subscribers fanout across 1000 flush cycles. Each flush spreads pendingReactions; tests per-flush allocation churn.
signal:reaction-coalesce-400x100 Sets one signal 100 times then flushes once across 400 bursts so 100 subscribers wake one time per burst.
signal:reaction-dep-diff-45k Toggles which of two signals a subscriber reads across 45000 cycles. Per-run dep-set diffing.
signal:reaction-flush-noop-5m Calls Reaction.flush() 5000000 times with no pending work. Scheduler dispatch overhead.
signal:reactive-fanout-500x1200 Fans out one signal's value change to 500 subscribers across 1200 successive updates.
signal:reactive-list-filter-1000x300 Changes a search-term signal 300 times, re-scanning a 1000-item list on each change.
signal:reactive-list-replace-1000x1000 Replaces a 1000-item list signal with a fresh 1000-item array and rescans it 1000 times.
signal:reactive-multi-read-5x160k Changes five signals in turn for 32000 rounds with one subscriber reading all five.
signal:reactive-push-2000x20 Appends 20 items onto an empty list signal with a subscriber, across 2000 reset cycles.
signal:reactive-set-index-300 Replaces one item by index in a 1000-item list signal across 300 updates, with a subscriber.
signal:reactive-set-property-by-id-200 Finds an item by id and updates one field in a 1000-item list signal across 200 alternating updates.
signal:reactive-stable-deps-3reads-5000x100 5000 reactions × 3 signals × 100 cycles. Each run clears + re-adds 3 stable dep edges.
signal:reactive-stable-fanout-5000x100 5000 reactions × 1 signal × 100 invalidations. Per-run Set.delete + add on a stable dep edge.
signal:set-same-10m Sets a signal to its current value 10000000 times. Exercises the no-op fast path when nothing changes.
signal:sub-unsub-100k Creates and tears down a subscriber on one signal across 100000 cycles. Subscription churn cost.
template:active-indicator-200 Cycles selectedId across 200 list items. Only the previously and newly active items update their class.
template:active-indicator-nested-200 Cycles currentUrl through 50 leaf urls in a 5×10×4 nav. Only the previously and newly active leaves should update their…
template:each-mount-1000 Mounts a fresh 1000-item each block with five-field items so per-record allocation cost dominates the wall clock.
template:snippet-args-per-key-100x500 Mutates one snippet arg's source across 100 invocations, 500 cycles. Adjacent no-signal expressions stay quiet.
template:snippet-in-subtemplate-100x1k Mutates one subtemplate prop's source across 25 cards each invoking 4 inner snippets, 1000 cycles. Snippet bodies shoul…
template:stable-ref-mutate-500 Replaces one item by index in a 500-item list across 100 cycles. Only that item's expressions re-render.
template:subtemplate-data-blob-100 Mutates one field inside data=expression on 100 children. Every child re-renders by design.
template:subtemplate-helpers-heavy-100x500 100 subtemplates, 4 inner bindings where three call helpers shaped like userland reality — Intl.NumberFormat, Array.fin…
template:subtemplate-helpers-light-100x500 100 subtemplates, 4 inner bindings each calling formatDate / classIf / capitalize, 500 cycles. Mutates one source signa…
template:subtemplate-reactive-data-100x500 Mutates one verbose reactiveData field across 100 child subtemplates, 500 cycles. Only the changed field re-evaluates.
template:subtemplate-shorthand-props-100x500 Mutates one shorthand prop's source across 100 child subtemplates, 500 cycles. Only that prop re-evaluates.
todo:add-20 Appends 20 todo items one at a time, like a user typing entries in a row.
todo:bulk-add-500 Renders 500 todo items added at once from a single data load.
todo:clear-completed-250 Clears 250 completed items from a 500-item list in one action, like clicking clear completed.
todo:edit-cycle-5 Runs 5 full edit-then-save cycles on different items, like editing a row and saving it.
todo:edit-start-10 Enters edit mode on 10 different items in a row, like double-clicking each one.
todo:filter-cycle-20 Cycles through active, completed, and all filters 20 times on a 100-item list.
todo:remove-50-back Deletes 50 items from the end of a 100-item list, one click at a time.
todo:remove-50-front Deletes 50 items from the front of a 100-item list, one click at a time.
todo:remove-50-middle Deletes 50 items from the middle of a 100-item list, one click at a time.
todo:remove-first-100 Deletes the first item 100 times from a 200-item list, with remaining items moving up each time.
todo:remove-last-100 Deletes the last item 100 times from a 200-item list, with no other items needing to move.
todo:remove-middle-100 Deletes the middle item 100 times from a 200-item list, walking halfway through to find each target.
todo:rename-500 Renames items in a 100-item list 500 times via single-field setProperty without editingId co-fires.
todo:toggle-100 Cycles through the first 10 items 10 times each, like a user toggling items repeatedly down a list.
todo:toggle-all-200 Toggles all 100 items completed and back across 200 cycles via the master checkbox.
todo:toggle-first-100 Toggles the first item in a 100-item list 100 times, alternating completed on and off.
todo:toggle-last-100 Toggles the last item in a 100-item list 100 times, alternating completed on and off.
todo:toggle-middle-100 Toggles a middle item in a 100-item list 100 times, alternating completed on and off.

Sample size: 80 floor / 280 max · Noise floor: ±2% · Timeout: 3min · Wall-clock: 13m08s

@jlukic
Copy link
Copy Markdown
Member Author

jlukic commented May 17, 2026

Closing — bisect drifted into a subclass refactor that diverged from the source branch's design. Restarting with a clean approach: safety presets as a thin construction-time alias over the existing internal fields (no hot-path dispatch, no subclasses).

@jlukic jlukic closed this May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Reactivity Modifies reactivity package Templating Modifies templating package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant