Skip to content

Commit 322f6cf

Browse files
committed
perf: version based tracking and link reuse
1 parent 66e44b6 commit 322f6cf

2 files changed

Lines changed: 240 additions & 60 deletions

File tree

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

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,73 @@ let flag_dirty = 1
66
let flag_pending = 2
77
let flag_running = 4
88

9+
// Global tracking version
10+
let trackingVersion: ref<int> = ref(0)
11+
912
// Observer kind tag
1013
type kind = [#Effect | #Computed]
1114

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-
}
15+
// Mutually recursive types using recursive modules
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 version: int,
40+
// === Observer fields (only used for computeds) ===
41+
// If compute is Some, this subs is a computed signal
42+
mutable compute: option<unit => unit>,
43+
mutable firstDep: option<Link.t>,
44+
mutable lastDep: option<Link.t>,
45+
mutable flags: int,
46+
mutable level: int,
47+
// Current tracking version for this compute cycle (for link reuse)
48+
mutable currentTrackingVersion: int,
49+
// Whether the last recompute changed the value (for equality short-circuit)
50+
mutable valueChanged: bool,
51+
}
52+
} = Subs
4053

4154
// 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-
}
55+
and Observer: {
56+
type t = {
57+
id: int,
58+
kind: kind,
59+
run: unit => unit,
60+
mutable firstDep: option<Link.t>,
61+
mutable lastDep: option<Link.t>,
62+
mutable flags: int,
63+
mutable level: int,
64+
name: option<string>,
65+
// For computed observers: direct reference to backing subs (the combined object)
66+
mutable backingSubs: option<Subs.t>,
67+
// Current tracking version for this run cycle (for link reuse)
68+
mutable currentTrackingVersion: int,
69+
}
70+
} = Observer
71+
72+
// Type aliases for convenience
73+
type link = Link.t
74+
type subs = Subs.t
75+
type observer = Observer.t
5476

5577
// Create empty subscriber list (for plain signals)
5678
let makeSubs = (): subs => {
@@ -62,6 +84,8 @@ let makeSubs = (): subs => {
6284
lastDep: None,
6385
flags: 0,
6486
level: 0,
87+
currentTrackingVersion: 0,
88+
valueChanged: true, // Plain signals always "changed"
6589
}
6690

6791
// Create subs for a computed (with compute function)
@@ -74,6 +98,8 @@ let makeComputedSubs = (compute: unit => unit): subs => {
7498
lastDep: None,
7599
flags: flag_dirty, // start dirty
76100
level: 0,
101+
currentTrackingVersion: 0,
102+
valueChanged: true, // Start as changed for initial computation
77103
}
78104

79105
// Create observer
@@ -93,9 +119,10 @@ let makeObserver = (
93119
level: 0,
94120
name,
95121
backingSubs,
122+
currentTrackingVersion: 0,
96123
}
97124

98-
// Flag operations for observer (using Int.Bitwise module)
125+
// Flag operations for observer
99126
let isDirty = (o: observer): bool => Int.bitwiseAnd(o.flags, flag_dirty) !== 0
100127
let setDirty = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_dirty)
101128
let clearDirty = (o: observer): unit =>
@@ -105,7 +132,7 @@ let setPending = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_pe
105132
let clearPending = (o: observer): unit =>
106133
o.flags = Int.bitwiseAnd(o.flags, Int.bitwiseNot(flag_pending))
107134

108-
// Flag operations for subs (for computeds - subs IS the observer)
135+
// Flag operations for subs
109136
let isSubsDirty = (s: subs): bool => Int.bitwiseAnd(s.flags, flag_dirty) !== 0
110137
let setSubsDirty = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_dirty)
111138
let clearSubsDirty = (s: subs): unit =>
@@ -115,7 +142,7 @@ let setSubsPending = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_pe
115142
let clearSubsPending = (s: subs): unit =>
116143
s.flags = Int.bitwiseAnd(s.flags, Int.bitwiseNot(flag_pending))
117144

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

121148
// Create a link node
@@ -126,6 +153,7 @@ let makeLink = (subs: subs, observer: observer): link => {
126153
prevDep: None,
127154
nextSub: None,
128155
prevSub: None,
156+
lastTrackedVersion: 0,
129157
}
130158

131159
// Add link to signal's subscriber list
@@ -179,6 +207,20 @@ let unlinkFromDeps = (observer: observer, link: link): unit => {
179207
link.nextDep = None
180208
}
181209

210+
// Remove link from subs's dependency list (for computeds - subs IS the observer)
211+
let unlinkFromSubsDeps = (s: subs, link: link): unit => {
212+
switch link.prevDep {
213+
| Some(prev) => prev.nextDep = link.nextDep
214+
| None => s.firstDep = link.nextDep
215+
}
216+
switch link.nextDep {
217+
| Some(next) => next.prevDep = link.prevDep
218+
| None => s.lastDep = link.prevDep
219+
}
220+
link.prevDep = None
221+
link.nextDep = None
222+
}
223+
182224
// Clear all dependencies from observer (unlinks from all signals)
183225
let clearDeps = (observer: observer): unit => {
184226
let link = ref(observer.firstDep)

0 commit comments

Comments
 (0)