Skip to content

Commit 456f1c4

Browse files
committed
perf: optimize scheduler fast paths
- Keep dependency links across runs and clean stale links by tracking version instead of full clear/rebuild. - Add per-run dependency cursors and fast paths for computed/effect tracking to reduce fallback scans. - Improve propagation/flush behavior with queue-draining fixes, selective computed deferral, and ordering optimizations. - Add low-overhead freshness/notify fast paths (global version stamp, direct-effect notify path, computed-subscriber metadata).
1 parent 4f8c43c commit 456f1c4

4 files changed

Lines changed: 591 additions & 171 deletions

File tree

packages/rescript-signals/src/signals/Core.res

Lines changed: 101 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,74 +6,99 @@ let flag_dirty = 1
66
let flag_pending = 2
77
let flag_running = 4
88

9-
// Observer kind tag
9+
// Global tracking version
10+
let trackingVersion: ref<int> = ref(0)
11+
// Global mutation version (increments on real signal writes)
12+
let globalVersion: ref<int> = ref(0)
13+
1014
type kind = [#Effect | #Computed]
1115

12-
// Forward declare mutually recursive types
13-
type rec link = {
14-
// Direct reference to signal's subscriber list (type-erased)
15-
mutable subs: subs,
16-
// Direct reference to observer
17-
mutable observer: observer,
18-
// Links in the observer's dependency chain
19-
mutable nextDep: option<link>,
20-
mutable prevDep: option<link>,
21-
// Links in the signal's subscriber chain
22-
mutable nextSub: option<link>,
23-
mutable prevSub: option<link>,
24-
}
16+
module rec Link: {
17+
type t = {
18+
// Direct reference to signal's subscriber list (type-erased)
19+
mutable subs: Subs.t,
20+
// Direct reference to observer
21+
mutable observer: Observer.t,
22+
// Links in the observer's dependency chain
23+
mutable nextDep: option<Link.t>,
24+
mutable prevDep: option<Link.t>,
25+
// Links in the signal's subscriber chain
26+
mutable nextSub: option<Link.t>,
27+
mutable prevSub: option<Link.t>,
28+
// Version stamp for duplicate detection within a compute cycle
29+
mutable lastTrackedVersion: int,
30+
}
31+
} = Link
2532

2633
// Signal subscriber list (head/tail of linked list)
2734
// For computeds, this same object also serves as the observer (combined structure)
28-
and subs = {
29-
mutable first: option<link>,
30-
mutable last: option<link>,
31-
mutable version: int,
32-
// === Observer fields (only used for computeds) ===
33-
// If compute is Some, this subs is a computed signal
34-
mutable compute: option<unit => unit>,
35-
mutable firstDep: option<link>,
36-
mutable lastDep: option<link>,
37-
mutable flags: int,
38-
mutable level: int,
39-
}
35+
and Subs: {
36+
type t = {
37+
mutable first: option<Link.t>,
38+
mutable last: option<Link.t>,
39+
mutable computedSubscriberCount: int,
40+
mutable version: int,
41+
// === Observer fields (only used for computeds) ===
42+
// If compute is Some, this subs is a computed signal
43+
mutable compute: option<unit => unit>,
44+
mutable firstDep: option<Link.t>,
45+
mutable lastDep: option<Link.t>,
46+
mutable flags: int,
47+
mutable level: int,
48+
mutable deferEffectsUntilRecompute: bool,
49+
mutable lastGlobalVersion: int,
50+
}
51+
} = Subs
4052

4153
// Observer for effects only (computeds use subs directly)
42-
and observer = {
43-
id: int,
44-
kind: kind,
45-
run: unit => unit,
46-
mutable firstDep: option<link>,
47-
mutable lastDep: option<link>,
48-
mutable flags: int,
49-
mutable level: int,
50-
name: option<string>,
51-
// For computed observers: direct reference to backing subs (the combined object)
52-
mutable backingSubs: option<subs>,
53-
}
54+
and Observer: {
55+
type t = {
56+
id: int,
57+
kind: kind,
58+
run: unit => unit,
59+
mutable firstDep: option<Link.t>,
60+
mutable lastDep: option<Link.t>,
61+
mutable flags: int,
62+
mutable level: int,
63+
name: option<string>,
64+
// For computed observers: direct reference to backing subs (the combined object)
65+
mutable backingSubs: option<Subs.t>,
66+
}
67+
} = Observer
68+
69+
// Type aliases for convenience
70+
type link = Link.t
71+
type subs = Subs.t
72+
type observer = Observer.t
5473

5574
// Create empty subscriber list (for plain signals)
5675
let makeSubs = (): subs => {
5776
first: None,
5877
last: None,
78+
computedSubscriberCount: 0,
5979
version: 0,
6080
compute: None,
6181
firstDep: None,
6282
lastDep: None,
6383
flags: 0,
6484
level: 0,
85+
deferEffectsUntilRecompute: false,
86+
lastGlobalVersion: 0,
6587
}
6688

6789
// Create subs for a computed (with compute function)
68-
let makeComputedSubs = (compute: unit => unit): subs => {
90+
let makeComputedSubs = (compute: unit => unit, ~deferEffectsUntilRecompute: bool=false): subs => {
6991
first: None,
7092
last: None,
93+
computedSubscriberCount: 0,
7194
version: 0,
7295
compute: Some(compute),
7396
firstDep: None,
7497
lastDep: None,
7598
flags: flag_dirty, // start dirty
7699
level: 0,
100+
deferEffectsUntilRecompute,
101+
lastGlobalVersion: 0,
77102
}
78103

79104
// Create observer
@@ -95,7 +120,7 @@ let makeObserver = (
95120
backingSubs,
96121
}
97122

98-
// Flag operations for observer (using Int.Bitwise module)
123+
// Flag operations for observer
99124
let isDirty = (o: observer): bool => Int.bitwiseAnd(o.flags, flag_dirty) !== 0
100125
let setDirty = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_dirty)
101126
let clearDirty = (o: observer): unit =>
@@ -105,7 +130,7 @@ let setPending = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_pe
105130
let clearPending = (o: observer): unit =>
106131
o.flags = Int.bitwiseAnd(o.flags, Int.bitwiseNot(flag_pending))
107132

108-
// Flag operations for subs (for computeds - subs IS the observer)
133+
// Flag operations for subs
109134
let isSubsDirty = (s: subs): bool => Int.bitwiseAnd(s.flags, flag_dirty) !== 0
110135
let setSubsDirty = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_dirty)
111136
let clearSubsDirty = (s: subs): unit =>
@@ -115,17 +140,20 @@ let setSubsPending = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_pe
115140
let clearSubsPending = (s: subs): unit =>
116141
s.flags = Int.bitwiseAnd(s.flags, Int.bitwiseNot(flag_pending))
117142

118-
// Check if subs is a computed (has compute function)
143+
// Check if subs is a computed
119144
let isComputed = (s: subs): bool => s.compute !== None
120145

121146
// Create a link node
122-
let makeLink = (subs: subs, observer: observer): link => {
123-
subs,
124-
observer,
125-
nextDep: None,
126-
prevDep: None,
127-
nextSub: None,
128-
prevSub: None,
147+
let makeLink = (sourceSubs: subs, linkedObserver: observer): link => {
148+
{
149+
subs: sourceSubs,
150+
observer: linkedObserver,
151+
nextDep: None,
152+
prevDep: None,
153+
nextSub: None,
154+
prevSub: None,
155+
lastTrackedVersion: 0,
156+
}
129157
}
130158

131159
// Add link to signal's subscriber list
@@ -137,6 +165,11 @@ let linkToSubs = (subs: subs, link: link): unit => {
137165
| None => subs.first = Some(link)
138166
}
139167
subs.last = Some(link)
168+
169+
let linkedSubs = (Obj.magic(link.observer): subs)
170+
if isComputed(linkedSubs) {
171+
subs.computedSubscriberCount = subs.computedSubscriberCount + 1
172+
}
140173
}
141174

142175
// Add link to observer's dependency list
@@ -163,6 +196,11 @@ let unlinkFromSubs = (link: link): unit => {
163196
}
164197
link.prevSub = None
165198
link.nextSub = None
199+
200+
let linkedSubs = (Obj.magic(link.observer): subs)
201+
if isComputed(linkedSubs) && subs.computedSubscriberCount > 0 {
202+
subs.computedSubscriberCount = subs.computedSubscriberCount - 1
203+
}
166204
}
167205

168206
// Remove link from dependency list
@@ -179,6 +217,20 @@ let unlinkFromDeps = (observer: observer, link: link): unit => {
179217
link.nextDep = None
180218
}
181219

220+
// Remove link from subs's dependency list (for computeds - subs IS the observer)
221+
let unlinkFromSubsDeps = (s: subs, link: link): unit => {
222+
switch link.prevDep {
223+
| Some(prev) => prev.nextDep = link.nextDep
224+
| None => s.firstDep = link.nextDep
225+
}
226+
switch link.nextDep {
227+
| Some(next) => next.prevDep = link.prevDep
228+
| None => s.lastDep = link.prevDep
229+
}
230+
link.prevDep = None
231+
link.nextDep = None
232+
}
233+
182234
// Clear all dependencies from observer (unlinks from all signals)
183235
let clearDeps = (observer: observer): unit => {
184236
let link = ref(observer.firstDep)

0 commit comments

Comments
 (0)