Skip to content

Commit b9b41b3

Browse files
committed
Bench: Move primitive-Signal benches to top of bench-signal.js
The fresh-take agents (Challenge + Survey + Neutral) all flagged that set-same-10m and sub-unsub-100k regressing under reference safety is inexplicable from the primitive-Signal hot path itself (protect() early-returns before reading this.safety; bytecode is identical to clone mode). The convergent hypothesis is that the regression is inherited cross-bench state: the upstream list-Signal benches do dramatically different allocation/cloning work in clone vs reference mode, leaving V8 with different JIT feedback / heap layout / GC pressure when the primitive benches execute later in the same Chrome session. Running the primitives first isolates them. If the regressions persist at the top of the script, the cause is intrinsic to the primitive path. If they disappear, the cause is cross-bench state — diagnostic, not a production code change.
1 parent da3551d commit b9b41b3

1 file changed

Lines changed: 39 additions & 37 deletions

File tree

packages/reactivity/bench/tachometer/bench-signal.js

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,45 @@ const makeRecords = (n) => {
2121

2222
let sink = null;
2323

24+
/*******************************
25+
Primitive Signal hot paths
26+
(placed first to isolate from
27+
cross-bench heap/JIT state)
28+
*******************************/
29+
30+
// set-same-10m — exercises the equality short-circuit. With no
31+
// subscribers attached, set(same) collapses to an equality check + early
32+
// return. V8 JIT inlines aggressively here (each set is ~8ns), so 10M
33+
// iterations are needed to land above the σ-floor. A regression that
34+
// bypasses the short-circuit (e.g., always-notify) inflates the per-set
35+
// cost an order of magnitude and lights up immediately.
36+
{
37+
const sig = new Signal(42);
38+
// purpose: Sets a signal to its current value 10000000 times. Exercises the no-op fast path when nothing changes.
39+
performance.mark(startMark('set-same-10m'));
40+
for (let i = 0; i < 10_000_000; i++) {
41+
sig.set(42);
42+
}
43+
performance.measure('set-same-10m', startMark('set-same-10m'));
44+
}
45+
46+
// sub-unsub-100k — measures the per-create/per-destroy cost of a
47+
// subscriber that reads one signal. Components with frequent mount/unmount
48+
// (modal dialogs, list virtualization, route transitions) hit this path
49+
// continuously. 100k cycles to clear the σ-floor at ~340ns/cycle.
50+
{
51+
const sig = new Signal(0);
52+
// purpose: Creates and tears down a subscriber on one signal across 100000 cycles. Subscription churn cost.
53+
performance.mark(startMark('sub-unsub-100k'));
54+
for (let i = 0; i < 100_000; i++) {
55+
const r = Reaction.create(() => {
56+
sink = sig.get();
57+
});
58+
r.stop();
59+
}
60+
performance.measure('sub-unsub-100k', startMark('sub-unsub-100k'));
61+
}
62+
2463
/*******************************
2564
Reactive machinery
2665
*******************************/
@@ -223,43 +262,6 @@ let sink = null;
223262
r.stop();
224263
}
225264

226-
/*******************************
227-
Signal hot paths
228-
*******************************/
229-
230-
// set-same-10m — exercises the equality short-circuit. With no
231-
// subscribers attached, set(same) collapses to an equality check + early
232-
// return. V8 JIT inlines aggressively here (each set is ~8ns), so 10M
233-
// iterations are needed to land above the σ-floor. A regression that
234-
// bypasses the short-circuit (e.g., always-notify) inflates the per-set
235-
// cost an order of magnitude and lights up immediately.
236-
{
237-
const sig = new Signal(42);
238-
// purpose: Sets a signal to its current value 10000000 times. Exercises the no-op fast path when nothing changes.
239-
performance.mark(startMark('set-same-10m'));
240-
for (let i = 0; i < 10_000_000; i++) {
241-
sig.set(42);
242-
}
243-
performance.measure('set-same-10m', startMark('set-same-10m'));
244-
}
245-
246-
// sub-unsub-100k — measures the per-create/per-destroy cost of a
247-
// subscriber that reads one signal. Components with frequent mount/unmount
248-
// (modal dialogs, list virtualization, route transitions) hit this path
249-
// continuously. 100k cycles to clear the σ-floor at ~340ns/cycle.
250-
{
251-
const sig = new Signal(0);
252-
// purpose: Creates and tears down a subscriber on one signal across 100000 cycles. Subscription churn cost.
253-
performance.mark(startMark('sub-unsub-100k'));
254-
for (let i = 0; i < 100_000; i++) {
255-
const r = Reaction.create(() => {
256-
sink = sig.get();
257-
});
258-
r.stop();
259-
}
260-
performance.measure('sub-unsub-100k', startMark('sub-unsub-100k'));
261-
}
262-
263265
/*******************************
264266
Reaction scheduler
265267
*******************************/

0 commit comments

Comments
 (0)