@zeix/cause-effect 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/eslint.config.js +35 -0
- package/index.d.ts +7 -7
- package/index.js +1 -1
- package/index.ts +19 -9
- package/package.json +32 -29
- package/{lib → src}/computed.ts +83 -61
- package/{lib → src}/effect.ts +27 -19
- package/{lib → src}/scheduler.d.ts +1 -1
- package/{lib → src}/scheduler.ts +30 -35
- package/{lib → src}/signal.d.ts +1 -1
- package/{lib → src}/signal.ts +1 -1
- package/{lib → src}/state.ts +35 -34
- package/{lib → src}/util.d.ts +1 -1
- package/{lib → src}/util.ts +21 -11
- package/test/batch.test.ts +18 -19
- package/test/benchmark.test.ts +36 -36
- package/test/computed.test.ts +43 -42
- package/test/effect.test.ts +18 -21
- package/test/state.test.ts +15 -31
- package/test/util/dependency-graph.ts +147 -145
- package/test/util/framework-types.ts +22 -22
- package/test/util/perf-tests.ts +28 -28
- package/test/util/reactive-framework.ts +11 -12
- /package/{lib → src}/computed.d.ts +0 -0
- /package/{lib → src}/effect.d.ts +0 -0
- /package/{lib → src}/state.d.ts +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { TestConfig } from
|
|
2
|
-
import { Computed, ReactiveFramework, Signal } from
|
|
3
|
-
import { Random } from
|
|
1
|
+
import { TestConfig } from './framework-types'
|
|
2
|
+
import { Computed, ReactiveFramework, Signal } from './reactive-framework'
|
|
3
|
+
import { Random } from 'random'
|
|
4
4
|
|
|
5
5
|
export interface Graph {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
sources: Signal<number>[]
|
|
7
|
+
layers: Computed<number>[][]
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -17,25 +17,27 @@ export interface Graph {
|
|
|
17
17
|
* @returns the graph
|
|
18
18
|
*/
|
|
19
19
|
export function makeGraph(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
framework: ReactiveFramework,
|
|
21
|
+
config: TestConfig,
|
|
22
|
+
counter: Counter,
|
|
23
23
|
): Graph {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
const { width, totalLayers, staticFraction, nSources } = config
|
|
25
|
+
|
|
26
|
+
return framework.withBuild(() => {
|
|
27
|
+
const sources = new Array(width)
|
|
28
|
+
.fill(0)
|
|
29
|
+
.map((_, i) => framework.signal(i))
|
|
30
|
+
const rows = makeDependentRows(
|
|
31
|
+
sources,
|
|
32
|
+
totalLayers - 1,
|
|
33
|
+
counter,
|
|
34
|
+
staticFraction,
|
|
35
|
+
nSources,
|
|
36
|
+
framework,
|
|
37
|
+
)
|
|
38
|
+
const graph = { sources, layers: rows }
|
|
39
|
+
return graph
|
|
40
|
+
})
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -44,146 +46,146 @@ export function makeGraph(
|
|
|
44
46
|
* @return the sum of all leaf values
|
|
45
47
|
*/
|
|
46
48
|
export function runGraph(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
graph: Graph,
|
|
50
|
+
iterations: number,
|
|
51
|
+
readFraction: number,
|
|
52
|
+
framework: ReactiveFramework,
|
|
51
53
|
): number {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
54
|
+
const rand = new Random('seed')
|
|
55
|
+
const { sources, layers } = graph
|
|
56
|
+
const leaves = layers[layers.length - 1]
|
|
57
|
+
const skipCount = Math.round(leaves.length * (1 - readFraction))
|
|
58
|
+
const readLeaves = removeElems(leaves, skipCount, rand)
|
|
59
|
+
const frameworkName = framework.name.toLowerCase()
|
|
60
|
+
// const start = Date.now();
|
|
61
|
+
let sum = 0
|
|
62
|
+
|
|
63
|
+
if (frameworkName === 's-js' || frameworkName === 'solidjs') {
|
|
64
|
+
// [S.js freeze](https://github.com/adamhaile/S#sdatavalue) doesn't allow different values to be set during a single batch, so special case it.
|
|
65
|
+
for (let i = 0; i < iterations; i++) {
|
|
66
|
+
framework.withBatch(() => {
|
|
67
|
+
const sourceDex = i % sources.length
|
|
68
|
+
sources[sourceDex].write(i + sourceDex)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
for (const leaf of readLeaves) {
|
|
72
|
+
leaf.read()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sum = readLeaves.reduce((total, leaf) => leaf.read() + total, 0)
|
|
77
|
+
} else {
|
|
78
|
+
framework.withBatch(() => {
|
|
79
|
+
for (let i = 0; i < iterations; i++) {
|
|
80
|
+
// Useful for debugging edge cases for some frameworks that experience
|
|
81
|
+
// dramatic slow downs for certain test configurations. These are generally
|
|
82
|
+
// due to `computed` effects not being cached efficiently, and as the number
|
|
83
|
+
// of layers increases, the uncached `computed` effects are re-evaluated in
|
|
84
|
+
// an `O(n^2)` manner where `n` is the number of layers.
|
|
85
|
+
/* if (i % 100 === 0) {
|
|
84
86
|
console.log("iteration:", i, "delta:", Date.now() - start);
|
|
85
87
|
} */
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const sourceDex = i % sources.length
|
|
90
|
+
sources[sourceDex].write(i + sourceDex)
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
sum = readLeaves.reduce((total, leaf) => leaf.read() + total, 0);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
92
|
+
for (const leaf of readLeaves) {
|
|
93
|
+
leaf.read()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
sum = readLeaves.reduce((total, leaf) => leaf.read() + total, 0)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return sum
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
function removeElems<T>(src: T[], rmCount: number, rand: Random): T[] {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const copy = src.slice()
|
|
106
|
+
for (let i = 0; i < rmCount; i++) {
|
|
107
|
+
const rmDex = rand.int(0, copy.length - 1)
|
|
108
|
+
copy.splice(rmDex, 1)
|
|
109
|
+
}
|
|
110
|
+
return copy
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export class Counter {
|
|
112
|
-
|
|
114
|
+
count = 0
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
function makeDependentRows(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
sources: Computed<number>[],
|
|
119
|
+
numRows: number,
|
|
120
|
+
counter: Counter,
|
|
121
|
+
staticFraction: number,
|
|
122
|
+
nSources: number,
|
|
123
|
+
framework: ReactiveFramework,
|
|
122
124
|
): Computed<number>[][] {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
125
|
+
let prevRow = sources
|
|
126
|
+
const rand = new Random('seed')
|
|
127
|
+
const rows = []
|
|
128
|
+
for (let l = 0; l < numRows; l++) {
|
|
129
|
+
const row = makeRow(
|
|
130
|
+
prevRow,
|
|
131
|
+
counter,
|
|
132
|
+
staticFraction,
|
|
133
|
+
nSources,
|
|
134
|
+
framework,
|
|
135
|
+
l,
|
|
136
|
+
rand,
|
|
137
|
+
)
|
|
138
|
+
rows.push(row as never)
|
|
139
|
+
prevRow = row
|
|
140
|
+
}
|
|
141
|
+
return rows
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
function makeRow(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
sources: Computed<number>[],
|
|
146
|
+
counter: Counter,
|
|
147
|
+
staticFraction: number,
|
|
148
|
+
nSources: number,
|
|
149
|
+
framework: ReactiveFramework,
|
|
150
|
+
_layer: number,
|
|
151
|
+
random: Random,
|
|
150
152
|
): Computed<number>[] {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
153
|
+
return sources.map((_, myDex) => {
|
|
154
|
+
const mySources: Computed<number>[] = []
|
|
155
|
+
for (let sourceDex = 0; sourceDex < nSources; sourceDex++) {
|
|
156
|
+
mySources.push(sources[(myDex + sourceDex) % sources.length])
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const staticNode = random.float() < staticFraction
|
|
160
|
+
if (staticNode) {
|
|
161
|
+
// static node, always reference sources
|
|
162
|
+
return framework.computed(() => {
|
|
163
|
+
counter.count++
|
|
164
|
+
|
|
165
|
+
let sum = 0
|
|
166
|
+
for (const src of mySources) {
|
|
167
|
+
sum += src.read()
|
|
168
|
+
}
|
|
169
|
+
return sum
|
|
170
|
+
})
|
|
171
|
+
} else {
|
|
172
|
+
// dynamic node, drops one of the sources depending on the value of the first element
|
|
173
|
+
const first = mySources[0]
|
|
174
|
+
const tail = mySources.slice(1)
|
|
175
|
+
const node = framework.computed(() => {
|
|
176
|
+
counter.count++
|
|
177
|
+
let sum = first.read()
|
|
178
|
+
const shouldDrop = sum & 0x1
|
|
179
|
+
const dropDex = sum % tail.length
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < tail.length; i++) {
|
|
182
|
+
if (shouldDrop && i === dropDex) continue
|
|
183
|
+
sum += tail[i].read()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return sum
|
|
187
|
+
})
|
|
188
|
+
return node
|
|
189
|
+
}
|
|
190
|
+
})
|
|
189
191
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TestResult } from
|
|
2
|
-
import { ReactiveFramework } from
|
|
1
|
+
import { TestResult } from './perf-tests'
|
|
2
|
+
import { ReactiveFramework } from './reactive-framework'
|
|
3
3
|
|
|
4
4
|
/** Parameters for a running a performance benchmark test
|
|
5
5
|
*
|
|
@@ -19,35 +19,35 @@ import { ReactiveFramework } from "./reactive-framework";
|
|
|
19
19
|
* number of non-signal updated.
|
|
20
20
|
*/
|
|
21
21
|
export interface TestConfig {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/** friendly name for the test, should be unique */
|
|
23
|
+
name?: string
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/** width of dependency graph to construct */
|
|
26
|
+
width: number
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/** depth of dependency graph to construct */
|
|
29
|
+
totalLayers: number
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/** fraction of nodes that are static */ // TODO change to dynamicFraction
|
|
32
|
+
staticFraction: number
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/** construct a graph with number of sources in each node */
|
|
35
|
+
nSources: number
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/** fraction of [0, 1] elements in the last layer from which to read values in each test iteration */
|
|
38
|
+
readFraction: number
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/** number of test iterations */
|
|
41
|
+
iterations: number
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
/** sum and count of all iterations, for verification */
|
|
44
|
+
expected: Partial<TestResult>
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export interface FrameworkInfo {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/** wrapper/adapter for a benchmarking a reactive framework */
|
|
49
|
+
framework: ReactiveFramework
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
/** verify the number of nodes executed matches the expected number */
|
|
52
|
+
testPullCounts?: boolean
|
|
53
53
|
}
|
package/test/util/perf-tests.ts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { FrameworkInfo, TestConfig } from
|
|
1
|
+
import { FrameworkInfo, TestConfig } from './framework-types'
|
|
2
2
|
|
|
3
3
|
export interface TestResult {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
sum: number
|
|
5
|
+
count: number
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface TimingResult<T> {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
result: T
|
|
10
|
+
timing: TestTiming
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface TestTiming {
|
|
14
|
-
|
|
14
|
+
time: number
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function verifyBenchResult(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
perfFramework: FrameworkInfo,
|
|
19
|
+
config: TestConfig,
|
|
20
|
+
timedResult: TimingResult<TestResult>,
|
|
21
21
|
): void {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const { testPullCounts, framework } = perfFramework
|
|
23
|
+
const { expected } = config
|
|
24
|
+
const { result } = timedResult
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
if (expected.sum) {
|
|
27
|
+
console.assert(
|
|
28
|
+
result.sum == expected.sum,
|
|
29
|
+
`sum ${framework.name} ${config.name} result:${result.sum} expected:${expected.sum}`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
expected.count &&
|
|
34
|
+
(config.readFraction === 1 || testPullCounts) &&
|
|
35
|
+
testPullCounts !== false
|
|
36
|
+
) {
|
|
37
|
+
console.assert(
|
|
38
|
+
result.count === expected.count,
|
|
39
|
+
`count ${framework.name} ${config.name} result:${result.count} expected:${expected.count}`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
42
|
}
|
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
* Implement this interface to add a new reactive framework to the test and performance test suite.
|
|
4
4
|
*/
|
|
5
5
|
export interface ReactiveFramework {
|
|
6
|
-
name: string
|
|
7
|
-
signal<T>(initialValue: T): Signal<T
|
|
8
|
-
computed<T>(fn: () => T): Computed<T
|
|
9
|
-
effect(fn: () => void): void
|
|
10
|
-
withBatch<T>(fn: () => T): void
|
|
11
|
-
withBuild<T>(fn: () => T): T
|
|
6
|
+
name: string
|
|
7
|
+
signal<T>(initialValue: T): Signal<T>
|
|
8
|
+
computed<T>(fn: () => T): Computed<T>
|
|
9
|
+
effect(fn: () => void): void
|
|
10
|
+
withBatch<T>(fn: () => T): void
|
|
11
|
+
withBuild<T>(fn: () => T): T
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
export interface Signal<T> {
|
|
15
|
-
read(): T
|
|
16
|
-
write(v: T): void
|
|
15
|
+
read(): T
|
|
16
|
+
write(v: T): void
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
export interface Computed<T> {
|
|
20
|
-
read(): T
|
|
20
|
+
read(): T
|
|
21
21
|
}
|
|
22
|
-
|
|
File without changes
|
/package/{lib → src}/effect.d.ts
RENAMED
|
File without changes
|
/package/{lib → src}/state.d.ts
RENAMED
|
File without changes
|