Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/settle-fast-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

The seam (`settle`) no longer pays the effect-queue merge on a plain flush — nothing parked, no lane reveal, no landing: this flush's runs are the queue as they stand instead of a `concat` copy, a parked flush stashes its runs in place, and the per-flush list resets run only when a list is non-empty. About 100 ns less fixed cost per flush (−11% on a 20k-key store commit, −45% on a one-signal flush), with the effect order of every other seam unchanged.
26 changes: 18 additions & 8 deletions packages/signals/src/core/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,8 +786,7 @@ export class GlobalQueue implements IQueue {
)
recompute(zombies[i]);
// The unchanged passes' tails stay linked (A30, #3469): their inputs
// are held, and the committed frame still derives from them.
heldTrims.length = 0;
// are held, and the committed frame still derives from them — no trim.
} else {
commitPendingNodes();
// The flush committed: an unchanged pass's stale tail goes now. Not
Expand All @@ -799,8 +798,12 @@ export class GlobalQueue implements IQueue {
const t = heldTrims[i];
if (t._x?._error == null && !(t as any)._modified) trimStaleDeps(t);
}
heldTrims.length = 0;
}
// The list is the flush's either way. Reset only when there is something
// to reset (`stagedReaders` below likewise): `length = 0` is a runtime
// call even on an empty array, and the plain flush — no unchanged pass,
// no lane work — has nothing in either.
if (heldTrims.length) heldTrims.length = 0;
// Lanes: a blocked one parks its frame (its own flight is up); an
// unblocked one reveals this round's work. Then land every transaction
// no frame is waiting on — its lanes end with it. Backwards: a landing
Expand All @@ -813,7 +816,7 @@ export class GlobalQueue implements IQueue {
// writes if it parked: the lane seam re-derives them on the committed
// world (a lane's runs wait this round).
GlobalQueue._laneSeams?.(t !== null ? stagedReaders : null);
stagedReaders.length = 0;
if (stagedReaders.length) stagedReaders.length = 0;
this._queues = [[], []];
for (let k = transactions.length - 1; k >= 0; k--) {
const u = transactions[k];
Expand Down Expand Up @@ -848,8 +851,15 @@ export class GlobalQueue implements IQueue {
holding = true;
}
for (let i = 0; i < 2; i++) {
if (parked) u!._queues[i] = u!._queues[i].concat(own[i]);
this._queues[i] = lanes[i].concat(parked ? [] : own[i], this._queues[i]);
if (parked) append(u!._queues[i], own[i]);
// The plain flush — no reveal, no landing, not parked — has nothing to
// order around its own runs: they are the queue as they stand (the
// same array, not a copy; `run` takes it whole). Every other seam
// builds the ordered queue above.
this._queues[i] =
parked || lanes[i].length || this._queues[i].length
? lanes[i].concat(parked ? [] : own[i], this._queues[i])
: own[i];
}
if (deferredZombies.length !== 0) {
// Survivors are the displayed frame of a held pass: they rerun for the
Expand All @@ -864,8 +874,8 @@ export class GlobalQueue implements IQueue {
GlobalQueue._boundarySeam?.();
}
run(type: number) {
if (this._queues[type - 1].length) {
const effects = this._queues[type - 1];
const effects = this._queues[type - 1];
if (effects.length) {
this._queues[type - 1] = [];
runQueue(effects, type);
}
Expand Down
4 changes: 2 additions & 2 deletions scripts/size/floor-caps.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"signals: core floor (createSignal/Memo/Effect/Root/flush)": "7.33 KB",
"app: render + one signal (the simple-app floor)": "9.81 KB",
"app: hydrating (no stores) with Show/For/Loading/Errored/lazy": "17.64 KB",
"app: render + one signal (the simple-app floor)": "9.83 KB",
"app: hydrating (no stores) with Show/For/Loading/Errored/lazy": "17.66 KB",
"page: base server components (hydrating + dynamic + frames + sf reference)": "44.05 KB",
"page: live server components (base + live/GET + action + isPending/latest)": "47.67 KB",
"server: floor (getRequestEvent + isServer)": "1.34 KB",
Expand Down
28 changes: 27 additions & 1 deletion scripts/size/scenarios.js
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,19 @@ module.exports = [
// -3 B minified). The same parking-gate change as the `+ isPending/latest`
// note; brotli layout turns the -3 B minified into a saving here. Not
// lowered (frozen caps only fall on a deliberate ratchet).
// Size-Exception (settle fast path, 2026-10-04): 9.81 -> 9.83 KB, measured
// at 9,812 B against `next` @ 924d909d9's 9,801 (+11 B; 2 B over the cap;
// +5 B minified). The seam's effect-queue merge is skipped on the plain
// flush — nothing parked, no lane reveal, no landing — and the per-flush
// list resets run only on a non-empty list: ~100 ns less fixed cost per
// flush (one signal + one render effect + flush: 240 -> 133 ns; a 20k-key
// store commit: 844 -> 748 ns). Brotli layout: the same +5 B minified
// measures -16 B on `+ createStore`, 0 B on the CSR app, +28 B on
// `+ every store primitive family` and +17/+21 B on the two pages (both
// within their caps); 44 equivalent encodings measured, none under every
// cap at once, this one the smallest minified. Accepted by the
// maintainer (2026-10-04). The cap is frozen again at 9.83 KB
// (measured + 10 B).
limit: floorCaps["app: render + one signal (the simple-app floor)"],
alias
},
Expand Down Expand Up @@ -1573,6 +1586,12 @@ module.exports = [
// above is gone (no heap refusal, no `_manualWriteTime`, no
// `deleteFromHeap` in the setter). Reverts the #3740 raise. The cap is
// frozen again at 19.69 KB.
// Size-Exception (settle fast path, 2026-10-04): 17.64 -> 17.66 KB,
// measured at 17,650 B against `next` @ 924d909d9's 17,632 (+18 B; 10 B
// over the cap; +4 B minified). The same seam change as the simple-app
// floor note; brotli layout over the larger bundle. Accepted by the
// maintainer (2026-10-04). The cap is frozen again at 17.66 KB
// (measured + 10 B).
limit: floorCaps["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"],
alias
},
Expand Down Expand Up @@ -1975,7 +1994,14 @@ module.exports = [
// Lowered (#3774, 2026-10-04): 30.82 -> 28.78 KB, measured at 28.77 KB by CI
// at 22c3d3e14 — the hold model (L2): the signals core rebuilt from its
// rulings. Lowering is the ratchet; raise only with a Size-Exception.
limit: "28.78 KB",
// Size-Exception (settle fast path, 2026-10-04): 28.78 -> 28.80 KB,
// measured at 28,785 B against `next` @ 924d909d9's 28,757 (+28 B; 5 B
// over the cap; +5 B minified, all of it the signals core). The same
// seam change as the simple-app floor note; brotli layout over this
// bundle (before #3791 the same +5 B measured +2 B here). Accepted by
// the maintainer (2026-10-04). The cap is frozen again at 28.80 KB
// (measured + 10 B).
limit: "28.80 KB",
alias
},
{
Expand Down
Loading