Skip to content

Signals: Adds Reference Signals Implementation#148

Open
jlukic wants to merge 38 commits intomainfrom
perf/signal-safety-v2
Open

Signals: Adds Reference Signals Implementation#148
jlukic wants to merge 38 commits intomainfrom
perf/signal-safety-v2

Conversation

@jlukic
Copy link
Copy Markdown
Member

@jlukic jlukic commented Apr 15, 2026

Adds safety param to signals, currently defaults to reference

Foundation for three-mode signal safety (freeze/reference/none).
defaultSafety starts at 'freeze'; setDefaultSafety validates against
the valid set. No consumers yet — the Signal class will wire up in a
follow-up.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 15, 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 7, 2026 8:13pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mcp Ignored Ignored Preview May 7, 2026 8:13pm

Request Review

Three-mode safety: 'freeze' (default), 'reference', 'none'. Reads return
the protected ref directly; no more per-read cloning. Mutations on frozen
values throw at the call site instead of being silently swallowed.

- protect() gate: freeze → deepFreeze, otherwise pass-through
- value getter/peek() return currentValue directly (no readCopy)
- get() loses its options form — use .peek() or .clone() for escapes
- new .clone() method returns a deep copy; tracked like .get()
- in-place helpers (push/unshift/splice/setIndex/removeIndex/
  setArrayProperty/setProperty) branch on safety so reference/none
  retain O(1) mutation; freeze path rebuilds via spread
- mutate(fn): return-value path sets; undefined path notifies
  (in-place mutation only works under reference/none; throws on freeze)
- legacy allowClone: false maps to 'reference' for back-compat

Reactivity package tests pass. Downstream callsites that mutated through
peek() or the data context will surface as loud failures under freeze;
those need per-callsite fixes in follow-up commits.
Replace mutate-in-map with spread-in-map so enrichment helpers don't
mutate the caller's input. Two callsites surfaced under freeze-on-set:

- mobile-menu addNavIcons: was mutating item.navIcon on items from
  settings.menu, silently modifying the caller's menu tree
- nav-menu addSelectedIndex: was mutating item.selectedIndex on items
  from settings.menu, same issue; inner .map passes now use the
  returned value instead of relying on side effects
@github-actions github-actions Bot added the UI Components Modifies UI components label Apr 15, 2026
jlukic added a commit that referenced this pull request Apr 15, 2026
PR #148 (signal-safety refactor under packages/reactivity) exposed the
per-package scoping bug in discover. The prior logic looked up tachometer
configs inside the specific changed package and ran their matrix cells —
but every tachometer config in this repo lives under packages/renderer,
so a PR that only changes packages/reactivity resolved to an empty matrix
and the bench workflow skipped silently.

This is wrong in the base case: reactivity imports renderer consumes via
workspace symlinks, so a signal-path change moves renderer's bench
numbers. Same for templating → rendering, utils → everything, etc. The
benches are end-to-end measurements that pull the whole framework
through a workload; treating them as scoped-by-touched-package was
premature.

New logic: any trigger that reaches this workflow (push to main, or a
pull_request that matched our path filter) runs every tachometer-ci*.json
it finds under packages/*/bench/tachometer/. Path filter already restricts
triggers to perf-relevant changes, so this doesn't over-fire.

The push-branch and pull-request branches collapse to the same one-liner,
which is a good sign. Ship.
jlukic added a commit that referenced this pull request Apr 15, 2026
Build: Discover runs all benchmarkable packages, not just touched ones

PR #148 (signal-safety refactor under packages/reactivity) exposed the
per-package scoping bug in discover. The prior logic looked up tachometer
configs inside the specific changed package and ran their matrix cells —
but every tachometer config in this repo lives under packages/renderer,
so a PR that only changes packages/reactivity resolved to an empty matrix
and the bench workflow skipped silently.

This is wrong in the base case: reactivity imports renderer consumes via
workspace symlinks, so a signal-path change moves renderer's bench
numbers. Same for templating → rendering, utils → everything, etc. The
benches are end-to-end measurements that pull the whole framework
through a workload; treating them as scoped-by-touched-package was
premature.

New logic: any trigger that reaches this workflow (push to main, or a
pull_request that matched our path filter) runs every tachometer-ci*.json
it finds under packages/*/bench/tachometer/. Path filter already restricts
triggers to perf-relevant changes, so this doesn't over-fire.

The push-branch and pull-request branches collapse to the same one-liner,
which is a good sign. Ship.
Build: Discover runs all benchmarkable packages, not just touched ones

PR #148 (signal-safety refactor under packages/reactivity) exposed the
per-package scoping bug in discover. The prior logic looked up tachometer
configs inside the specific changed package and ran their matrix cells —
but every tachometer config in this repo lives under packages/renderer,
so a PR that only changes packages/reactivity resolved to an empty matrix
and the bench workflow skipped silently.

This is wrong in the base case: reactivity imports renderer consumes via
workspace symlinks, so a signal-path change moves renderer's bench
numbers. Same for templating → rendering, utils → everything, etc. The
benches are end-to-end measurements that pull the whole framework
through a workload; treating them as scoped-by-touched-package was
premature.

New logic: any trigger that reaches this workflow (push to main, or a
pull_request that matched our path filter) runs every tachometer-ci*.json
it finds under packages/*/bench/tachometer/. Path filter already restricts
triggers to perf-relevant changes, so this doesn't over-fire.

The push-branch and pull-request branches collapse to the same one-liner,
which is a good sign. Ship.
@semantic-performance-bot
Copy link
Copy Markdown

semantic-performance-bot Bot commented Apr 15, 2026

🟡 Mixed (mostly faster) for e402028 on Benchmark Suite 📊

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

Signals: Adds Reference Signals Implementation

Warning

This PR improves ✅ 19 tests while regressing on ❌ 17 tests.

✅ 19 faster · ❌ 17 slower · 🔍 13 unsure · ⚪ 17 no change


✅ Faster (19) — top 5 shown

Metrics where this PR confidently improved performance compared to main.

metric Improvement
signal:reactive-set-index-300 -99% (114ms) 🏆
signal:reactive-list-filter-1000x300 -95% (119ms) 🏆
signal:reactive-set-property-by-id-200 -88% (195ms) 🏆
signal:reactive-push-2000x20 -83% (190ms) 🏆
todo:remove-last-100 -73% (36ms) 🌟
Show all 19 faster metrics
metric Improvement
signal:reactive-set-index-300 -99% (114ms) 🏆
signal:reactive-list-filter-1000x300 -95% (119ms) 🏆
signal:reactive-set-property-by-id-200 -88% (195ms) 🏆
signal:reactive-push-2000x20 -83% (190ms) 🏆
todo:remove-last-100 -73% (36ms) 🌟
todo:rename-500 -66% (193ms) 🌟
todo:remove-50-back -66% (11ms) 🌟
todo:toggle-first-100 -63% (40ms) 🌟
todo:toggle-middle-100 -47% (21ms) 🌟
todo:toggle-last-100 -45% (26ms) 🌟
todo:toggle-100 -44% (19ms) 🌟
todo:add-20 -13% (1ms)
signal:computed-chain-10x60k -11% (23ms)
signal:set-same-10m -7% (2ms)
signal:reactive-multi-read-5x160k -6% (13ms)
hydrate:helper-100-state-change-1k -6% (0ms)
todo:bulk-add-500 -6% (11ms)
template:subtemplate-helpers-light-100 -4% (14ms)
signal:reaction-coalesce-400x100 -3% (1ms)

❌ Slower (17) — top 5 shown

Metrics where this PR confidently regressed performance compared to main.

metric Regression
todo:filter-cycle-20 +25% (86ms) ❗
todo:remove-50-front +22% (86ms) ❗
todo:remove-50-middle +20% (39ms) ❗
todo:edit-cycle-5 +19% (25ms) ❗
todo:edit-start-10 +18% (25ms) ❗
Show all 17 slower metrics
metric Regression
todo:filter-cycle-20 +25% (86ms) ❗
todo:remove-50-front +22% (86ms) ❗
todo:remove-50-middle +20% (39ms) ❗
todo:edit-cycle-5 +19% (25ms) ❗
todo:edit-start-10 +18% (25ms) ❗
todo:toggle-all-200 +16% (338ms) ❗
krausest:clear-10k +15% (16ms) ❗
todo:remove-first-100 +9% (128ms)
template:each-mount-1000 +8% (3ms)
signal:reactive-list-replace-1000x1000 +7% (21ms)
template:subtemplate-reactive-data-100 +6% (17ms)
todo:remove-middle-100 +6% (47ms)
todo:clear-completed-250 +5% (4ms)
template:subtemplate-data-blob-100 +5% (12ms)
template:subtemplate-shorthand-props-100 +5% (12ms)
template:snippet-in-subtemplate-100x1k +3% (39ms)
template:subtemplate-helpers-heavy-100 +3% (14ms)
⚪ No Change (17)

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

metric Change
compiler-micros:ast-walk-15k -1.2% – +0.7%
renderer-micros:build-html-string-10k -0.3% – +1.8%
krausest:create-10k -1.3% – -0.3%
renderer-micros:dom-walker-1000x15 -1.3% – +1.1%
hydrate:each-100 -1.6% – -0.0%
hydrate:each-100-mount -1.3% – +0.4%
renderer-micros:expr-lisp-50k -2.0% – +0.2%
hydrate:helper-100-mount -1.9% – +0.3%
compiler-micros:parse-cold-complex-200 -1.6% – +0.5%
compiler-micros:parse-cold-normal-500 -0.8% – +1.9%
signal:reaction-flush-noop-5m -0.8% – +1.9%
signal:reactive-fanout-500x1200 -0.8% – +1.1%
krausest:remove-row-front-20 -0.9% – +1.3%
krausest:remove-row-middle-20 -0.8% – +1.9%
krausest:replace-1k -1.3% – +1.0%
krausest:select-40 -1.1% – +0.3%
compiler-micros:snippet-args-5k -1.1% – +0.5%
🔍 Unsure (13)

Too Fast to Measure Precisely (13)

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 -3.2% – +0.6% ~32ms ±5%
template:active-indicator-nested-200 -5.0% – -0.9% ~22ms ±5%
krausest:append-1k -6.0% – -1.4% ~171ms ±5%
krausest:create-1k -3.7% – -0.7% ~127ms ±2%
renderer-micros:expr-js-10k -2.9% – -0.5% ~18ms ±6%
renderer-micros:expr-simple-100k -2.7% – -0.7% ~32ms ±5%
signal:reaction-dep-diff-45k -4.5% – -1.4% ~34ms ±4%
krausest:remove-row-back-100 -0.1% – +2.4% ~36ms ±2%
template:snippet-args-per-key-100 -3.3% – +4.2% ~6ms ±9%
template:stable-ref-mutate-500 -7.5% – -0.8% ~13ms ±9%
signal:sub-unsub-100k -4.0% – -0.5% ~22ms ±6%
krausest:swap-rows-20 -3.0% – +3.1% ~116ms ±7%
krausest:update-10th-50 -2.0% – +0.6% ~150ms ±2%
📖 Bench glossary (66 metrics)
metric what it tests
compiler-micros:ast-walk-15k Walks a kitchen-sink AST through optimizeAST 15000 times. Merge, hoist, and recurse pass.
compiler-micros:parse-cold-complex-200 Compiles a feature-dense kitchen-sink template 200 times. Catches parser regressions on uncommon block paths.
compiler-micros:parse-cold-normal-500 Compiles a TodoMVC-style component template 500 times. Headline metric for normal-component compile throughput.
compiler-micros:snippet-args-5k Parses four representative subtemplate-call shapes 5000 times each. Snippet args extraction.
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: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: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-100 Mutates one snippet arg's source across 100 invocations. 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-100 100 subtemplates, 4 inner bindings where three call helpers shaped like userland reality — Intl.NumberFormat, Array.fin…
template:subtemplate-helpers-light-100 100 subtemplates, 4 inner bindings each calling formatDate / classIf / capitalize. Mutates one source signal — under pe…
template:subtemplate-reactive-data-100 Mutates one verbose reactiveData field across 100 child subtemplates. Only the changed field re-evaluates.
template:subtemplate-shorthand-props-100 Mutates one shorthand prop's source across 100 child subtemplates. 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: 50 floor / 290 max · Noise floor: ±2% · Timeout: 3min · Wall-clock: 19m34s

jlukic added 2 commits April 15, 2026 17:13
Surfaces when a signal-backed data blob flows into the templating
boundary under freeze-default:

- extend was copying source property descriptors verbatim, so a frozen
  source produced a fresh target with {writable: false, configurable:
  false} on every property. Target was extensible but key-level writes
  threw. Now extend installs a fresh writable descriptor for data
  properties and only preserves descriptors for accessor (getter/setter)
  properties.
- template.setDataContext thaws this.data lazily when it arrives frozen
  (via a Template clone with the frozen data argument); assignInPlace
  then mutates in place as before, preserving reference identity and
  key-level diffing semantics that the spurious-reactivity guards
  depend on.
- ssr-hydration test updated to clone the array before mutating (the
  old pattern silently mutated the peek() return).
@github-actions github-actions Bot added Templating Modifies templating package Utils Modifies utilities package labels Apr 15, 2026
jlukic added 2 commits April 15, 2026 18:19
Static surface for configuring defaults, discoverable via Signal.* tab
completion and validated on assignment:
- Signal.equalityFunction / Signal.cloneFunction — accessors with
  typeof check; throw TypeError on non-function
- Signal.safety / Signal.tracing / Signal.stackCapture — accessors
  delegating to helpers module-level state
- Signal.configure({...}) — bulk wrapper; forwards each key through
  its accessor so validation runs
- Signal.defaults — snapshot getter of current settings

Drop the allowClone option. The transitional mapping from
allowClone: false → safety: 'reference' is removed. Callsites migrated:
- lit/renderer.js dataVersion — collapses cleanly to safety: 'none'
  (allowClone:false + equalityFunction always-false)
- native/blocks/each.js itemSignal — safety: 'reference'
- templating/src/template.js settingsVars — safety: 'reference'
- bench.js / bench-todo.js / tools/benchmark — safety: 'none'

Drop legacy function-form statics (Signal.setTracing, Signal.isTracing,
Signal.setStackCapture, Signal.isStackCapture). The tracing flag
feature was never released; no compat needed.
noop becomes a true no-op — returns undefined, swallows arguments.
identity is the new export for the "return first argument unchanged"
semantic the old noop was secretly providing.

Callsites that depended on the identity behavior migrated to identity:
- utils/src/strings.js joinWords transform — the return value feeds
  the quoting path
- component/define-component.js createComponent — return becomes the
  instance methods (and when no createComponent is provided, the
  identity default carries callParams into the instance, which the
  SSR env-guard path depends on)
- query/behavior.js createBehavior — same shape as createComponent
- query/behavior.js customInvocation — fallback method-not-found
  handler; return value is the found result

All other `= noop` callsites are lifecycle side-effect callbacks
(onCreated, onRendered, etc.) whose return values are discarded —
true noop is correct there.

CHANGELOG entries added for the Reactivity + Utils breaking changes
that landed in this release.
@github-actions github-actions Bot added Query Modifies query package Component Component Rendering Docs Modifies documentation labels Apr 15, 2026
…aths

- template.js setDataContext: remove Object.isExtensible check. Ran
  200-300x per each-block reconcile on subtemplate renders. Local 4-sample
  A/B measurements show removing it eliminates the remove-middle +11.7%
  and remove-last +13% regressions identified from aggregated CI artifacts.
- signal.js: revert inlined protect() in value setter (speculative, not
  diagnostic-backed). Keep protect() as a method.
- signal.js: revert setArrayProperty single-index fast-path (speculative).
- bench-todo.js: scale short benches (toggle/edit/remove single-ops) 10x
  so they run 120-360ms instead of 6-20ms. Short benches were in tachometer's
  noise band and rotated between confident-regressed and unsure across runs.

With the default safety now 'reference', no signal values are frozen under
default use, so template.data will be mutable from the start and setDataContext
never needs to detect+replace a frozen target. Users who opt into safety='freeze'
can still hit this edge case — if that surfaces, fix it at the signal boundary
(don't pass frozen values to Template constructor) rather than defensive checks
in a per-call path.
jlukic added 7 commits April 16, 2026 10:19
…xtend docstring

hasProperty becomes a thin re-export of Object.hasOwn (ES2022); proxyObject default switches from noop to an explicit () => ({}); extend gets a docstring capturing its modern-JS shallow-merge semantics.
Shared mutable state now lives on a single config object in helpers.js; Signal's tracing/safety/stackCapture accessors touch it directly instead of routing through setter helpers. Housekeeping statics moved to the bottom of signal.js so the constructor and happy path read at the top, and the mutate() reference/none dedupe path restores its snapshot+equality guard.
Captures decisions for the next utils release window (returnsSelf/returnsTrue/etc. family, remove `any` alias, fix first/last return-type polymorphism). Agent lessons gains "Quiet Code Over Ornamented Code" warning against SCREAMING_CAPS, underscore-prefixes, and dispatcher-layer patterns.
The test enumerated packages/ raw, so stray harness dirs like .claude/ broke the dist/cdn/ existence check.
Bracket assignment already routes through the safety/tracing/stackCapture setters, so the explicit if-ladder was ornament. Agent lessons updated to frame the quiet-code check as a changeset review pass, not just a write-time discipline.
jlukic added 5 commits April 16, 2026 11:59
Drops session-scaffolding narration from signal.js, helpers.js, and siblings; keeps load-bearing WHY notes (WeakRef self-stop, captureStackTrace cost, mutate behavior by safety preset). Signal class reorganized with section dividers (Core / Complex / Mutation Helpers / Tracing / Instance of / Configuration), signalTag moved to helpers.js, scheduler's flushTask wrapper replaced by direct Scheduler.flush reference.
Captures the bar for shipped comments (non-obvious to someone who doesn't know the codebase, or a weird trick) with an IS_SIGNAL before/after example, and clarifies that level-2 section dividers for multi-method conceptual clusters are distinct from single-declaration narration labels.
@jlukic jlukic changed the title Refactor: Add safety preset state to reactivity helpers Signals: Adds Reference Signals Implementation Apr 18, 2026
jlukic added a commit that referenced this pull request Apr 18, 2026
Walk prior bench runs on this PR's branch to build a per-iteration
history. The reporter merges it with bench-history.json (main commits)
so peak attribution spans BOTH main AND this PR's iterations.

An agent iterating on a perf branch now sees: "create-1k peaked at
104.7ms on commit 9bcd3f1 (iteration 10), now at 143.9ms (+37%).
Bisect candidates: 442446e, 64c6820, 5595cbd +7 more."

New files:
- tools/bench-reporter/fetch-pr-history.js — walks gh API for prior
  successful Benchmarks runs on the PR branch, downloads results-*
  artifacts, extracts this-change absolute CIs, outputs pr-history.json
  in bench-history.json schema.

Changed files:
- reporter.js — new --pr-history flag, mergeHistories() combines main
  + PR iteration histories sorted by timestamp with SHA dedup. The
  existing computeHistoryStatus + REOPENED rendering work unchanged.
- benchmarks-report.yml — new "Fetch PR iteration history" step before
  "Generate report" in the comment job. Uses the bot token for API auth.

Tested against PR #148's 17 prior runs: 13 REOPENED metrics surfaced
with correct peak attribution and bisect candidates.
jlukic added a commit that referenced this pull request Apr 18, 2026
Walk prior bench runs on this PR's branch to build a per-iteration
history. The reporter merges it with bench-history.json (main commits)
so peak attribution spans BOTH main AND this PR's iterations.

An agent iterating on a perf branch now sees: "create-1k peaked at
104.7ms on commit 9bcd3f1 (iteration 10), now at 143.9ms (+37%).
Bisect candidates: 442446e, 64c6820, 5595cbd +7 more."

New files:
- tools/bench-reporter/fetch-pr-history.js — walks gh API for prior
  successful Benchmarks runs on the PR branch, downloads results-*
  artifacts, extracts this-change absolute CIs, outputs pr-history.json
  in bench-history.json schema.

Changed files:
- reporter.js — new --pr-history flag, mergeHistories() combines main
  + PR iteration histories sorted by timestamp with SHA dedup. The
  existing computeHistoryStatus + REOPENED rendering work unchanged.
- benchmarks-report.yml — new "Fetch PR iteration history" step before
  "Generate report" in the comment job. Uses the bot token for API auth.

Tested against PR #148's 17 prior runs: 13 REOPENED metrics surfaced
with correct peak attribution and bisect candidates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Component Component Rendering Docs Modifies documentation Query Modifies query package Reactivity Modifies reactivity package Templating Modifies templating package Tests Modifies tests UI Components Modifies UI components Utils Modifies utilities package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant