Skip to content

Commit 27c412b

Browse files
committed
Add experimental new memoizers: autotrack, weakmap, and signalis
1 parent 4163131 commit 27c412b

File tree

10 files changed

+1405
-5
lines changed

10 files changed

+1405
-5
lines changed

.github/workflows/build-and-test-types.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ jobs:
3737
- name: Run linter
3838
run: yarn lint
3939

40+
# Note: This shouldn't be necessary here, but Yarn 3
41+
# is creating a symlink from RTK's `node_modules/reselect/`
42+
# back to the root, and that fails because we haven't built yet.
43+
# So, built here and then try testing.
44+
- name: Build package
45+
run: yarn build
46+
4047
- name: Run tests
4148
run: yarn test
4249

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createNode, updateNode } from './proxy'
2+
import { Node } from './tracking'
3+
4+
import { createCache } from './autotracking'
5+
import {
6+
createCacheKeyComparator,
7+
defaultEqualityCheck
8+
} from '@internal/defaultMemoize'
9+
10+
export function autotrackMemoize<F extends (...args: any[]) => any>(func: F) {
11+
// we reference arguments instead of spreading them for performance reasons
12+
13+
// console.log('Creating autotrack memoizer node')
14+
const node: Node<Record<string, unknown>> = createNode(
15+
[] as unknown as Record<string, unknown>
16+
)
17+
18+
let lastArgs: IArguments | null = null
19+
20+
const shallowEqual = createCacheKeyComparator(defaultEqualityCheck)
21+
22+
// console.log('Creating cache')
23+
const cache = createCache(() => {
24+
// console.log('Executing cache: ', node.value)
25+
const res = func.apply(null, node.proxy as unknown as any[])
26+
// console.log('Res: ', res)
27+
return res
28+
})
29+
30+
// console.log('Creating memoized function')
31+
function memoized() {
32+
// console.log('Memoized running')
33+
if (!shallowEqual(lastArgs, arguments)) {
34+
// console.log(
35+
// 'Args are different: lastArgs =',
36+
// lastArgs,
37+
// 'newArgs =',
38+
// arguments
39+
// )
40+
updateNode(node, arguments as unknown as Record<string, unknown>)
41+
lastArgs = arguments
42+
} else {
43+
// console.log('Same args: ', lastArgs, arguments)
44+
}
45+
// const start = performance.now()
46+
// console.log('Calling memoized: ', arguments)
47+
48+
// const end = performance.now()
49+
// console.log('Memoized execution time: ', end - start)
50+
return cache.value
51+
}
52+
53+
memoized.clearCache = () => cache.clear()
54+
55+
return memoized as F & { clearCache: () => void }
56+
}

src/autotrackMemoize/autotracking.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { assert } from './utils'
2+
3+
// The global revision clock. Every time state changes, the clock increments.
4+
export let $REVISION = 0
5+
6+
// The current dependency tracker. Whenever we compute a cache, we create a Set
7+
// to track any dependencies that are used while computing. If no cache is
8+
// computing, then the tracker is null.
9+
let CURRENT_TRACKER: Set<Cell<any> | TrackingCache> | null = null
10+
11+
type EqualityFn = (a: any, b: any) => boolean
12+
13+
// Storage represents a root value in the system - the actual state of our app.
14+
export class Cell<T> {
15+
revision = $REVISION
16+
17+
_value: T
18+
_lastValue: T
19+
_isEqual: EqualityFn = tripleEq
20+
21+
constructor(initialValue: T, isEqual: EqualityFn = tripleEq) {
22+
// console.log('Constructing cell: ', initialValue)
23+
this._value = this._lastValue = initialValue
24+
this._isEqual = isEqual
25+
}
26+
27+
// Whenever a storage value is read, it'll add itself to the current tracker if
28+
// one exists, entangling its state with that cache.
29+
get value() {
30+
// console.log('Getting cell value: ', this._value)
31+
CURRENT_TRACKER?.add(this)
32+
33+
return this._value
34+
}
35+
36+
// Whenever a storage value is updated, we bump the global revision clock,
37+
// assign the revision for this storage to the new value, _and_ we schedule a
38+
// rerender. This is important, and it's what makes autotracking _pull_
39+
// based. We don't actively tell the caches which depend on the storage that
40+
// anything has happened. Instead, we recompute the caches when needed.
41+
set value(newValue) {
42+
// console.log('Setting value: ', this.value, newValue)
43+
// if (this.value === newValue) return
44+
45+
this._value = newValue
46+
this.revision = ++$REVISION
47+
// scheduleRerender()
48+
}
49+
}
50+
51+
function tripleEq(a: unknown, b: unknown) {
52+
return a === b
53+
}
54+
55+
// Caches represent derived state in the system. They are ultimately functions
56+
// that are memoized based on what state they use to produce their output,
57+
// meaning they will only rerun IFF a storage value that could affect the output
58+
// has changed. Otherwise, they'll return the cached value.
59+
export class TrackingCache {
60+
_cachedValue: any
61+
_cachedRevision = -1
62+
_deps: any[] = []
63+
hits = 0
64+
65+
fn: () => any
66+
67+
constructor(fn: () => any) {
68+
this.fn = fn
69+
}
70+
71+
clear() {
72+
this._cachedValue = undefined
73+
this._cachedRevision = -1
74+
this._deps = []
75+
this.hits = 0
76+
}
77+
78+
get value() {
79+
// When getting the value for a Cache, first we check all the dependencies of
80+
// the cache to see what their current revision is. If the current revision is
81+
// greater than the cached revision, then something has changed.
82+
if (this.revision > this._cachedRevision) {
83+
const { fn } = this
84+
85+
// We create a new dependency tracker for this cache. As the cache runs
86+
// its function, any Storage or Cache instances which are used while
87+
// computing will be added to this tracker. In the end, it will be the
88+
// full list of dependencies that this Cache depends on.
89+
const currentTracker = new Set<Cell<any>>()
90+
const prevTracker = CURRENT_TRACKER
91+
92+
CURRENT_TRACKER = currentTracker
93+
94+
// try {
95+
this._cachedValue = fn()
96+
// } finally {
97+
CURRENT_TRACKER = prevTracker
98+
this.hits++
99+
this._deps = Array.from(currentTracker)
100+
101+
// Set the cached revision. This is the current clock count of all the
102+
// dependencies. If any dependency changes, this number will be less
103+
// than the new revision.
104+
this._cachedRevision = this.revision
105+
// }
106+
}
107+
108+
// If there is a current tracker, it means another Cache is computing and
109+
// using this one, so we add this one to the tracker.
110+
CURRENT_TRACKER?.add(this)
111+
112+
// Always return the cached value.
113+
return this._cachedValue
114+
}
115+
116+
get revision() {
117+
// The current revision is the max of all the dependencies' revisions.
118+
return Math.max(...this._deps.map(d => d.revision), 0)
119+
}
120+
}
121+
122+
export function getValue<T>(cell: Cell<T>): T {
123+
// console.trace('Cell: ', cell)
124+
if (!(cell instanceof Cell)) {
125+
console.warn('Not a valid cell! ', cell)
126+
}
127+
// assert(
128+
// cell instanceof Cell,
129+
// 'getValue must be passed a tracked store created with `createStorage`.'
130+
// )
131+
132+
// console.log('Storage value: ', cell.value)
133+
134+
return cell.value
135+
}
136+
137+
type CellValue<T extends Cell<unknown>> = T extends Cell<infer U> ? U : never
138+
139+
export function setValue<T extends Cell<unknown>>(
140+
storage: T,
141+
value: CellValue<T>
142+
): void {
143+
assert(
144+
storage instanceof Cell,
145+
'setValue must be passed a tracked store created with `createStorage`.'
146+
)
147+
148+
// console.log('setValue: ', storage, value)
149+
150+
// console.log('Setting value: ', storage.value, value)
151+
// storage.value = value
152+
153+
const { _isEqual: isEqual, _lastValue: lastValue } = storage
154+
155+
// if (!isEqual(value, lastValue)) {
156+
storage.value = storage._lastValue = value
157+
// }
158+
}
159+
160+
export function createCell<T = unknown>(
161+
initialValue: T,
162+
isEqual: EqualityFn = tripleEq
163+
): Cell<T> {
164+
// assert(
165+
// typeof isEqual === 'function',
166+
// 'the second parameter to `createStorage` must be an equality function or undefined'
167+
// )
168+
169+
return new Cell(initialValue, isEqual)
170+
}
171+
172+
export function createCache<T = unknown>(fn: () => T): TrackingCache {
173+
assert(
174+
typeof fn === 'function',
175+
'the first parameter to `createCache` must be a function'
176+
)
177+
178+
return new TrackingCache(fn)
179+
}

0 commit comments

Comments
 (0)