tla-ts 0.0.3-dev.a50fc89 → 0.0.3-dev.b74f28c
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 +7 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +12 -12
- package/dist/run-cli.js +13 -13
- package/dist/transfer.cjs +3 -3
- package/dist/transfer.js +3 -3
- package/package.json +1 -1
- package/src/fingerprints.ts +46 -11
- package/src/parallel-worker.ts +163 -211
- package/src/parallel.ts +58 -33
- package/src/symmetry.ts +14 -2
- package/src/values.ts +7 -4
package/package.json
CHANGED
package/src/fingerprints.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { getValueFingerprint, getValueHash, type Value } from "./values";
|
|
2
2
|
|
|
3
3
|
const EMPTY = 0;
|
|
4
|
-
const TOMBSTONE = 1;
|
|
5
4
|
const INITIAL_CAPACITY = 1 << 12;
|
|
6
5
|
|
|
7
6
|
interface FingerprintTable {
|
|
8
7
|
insert: (first: number, second: number) => boolean;
|
|
9
|
-
remove: (first: number, second: number) => void;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
const mix = (hash: number) => {
|
|
@@ -26,15 +24,13 @@ export const writeStateFingerprint = (state: Value[], fingerprint: Int32Array) =
|
|
|
26
24
|
fingerprint[1] = mix(second);
|
|
27
25
|
};
|
|
28
26
|
|
|
29
|
-
export const getFingerprintOwner = (second: number, count: number) => (second >>> 0) % count;
|
|
30
|
-
|
|
31
27
|
export const createFingerprintTable = (): FingerprintTable => {
|
|
32
28
|
const table = { slots: new Int32Array(INITIAL_CAPACITY * 2), used: 0 };
|
|
33
29
|
const findSlot = (slots: Int32Array, first: number, second: number) => {
|
|
34
30
|
const mask = slots.length / 2 - 1;
|
|
35
31
|
for (let index = first & mask; ; index = (index + 1) & mask) {
|
|
36
32
|
const stored = slots[index * 2];
|
|
37
|
-
if (stored === EMPTY
|
|
33
|
+
if (stored === EMPTY) return index;
|
|
38
34
|
if (stored === first && slots[index * 2 + 1] === second) return index;
|
|
39
35
|
}
|
|
40
36
|
};
|
|
@@ -59,11 +55,50 @@ export const createFingerprintTable = (): FingerprintTable => {
|
|
|
59
55
|
table.used++;
|
|
60
56
|
return true;
|
|
61
57
|
},
|
|
62
|
-
remove: (first, second) => {
|
|
63
|
-
const slot = findSlot(table.slots, first, second);
|
|
64
|
-
if (table.slots[slot * 2] !== first) return;
|
|
65
|
-
table.slots[slot * 2] = EMPTY;
|
|
66
|
-
table.slots[slot * 2 + 1] = TOMBSTONE;
|
|
67
|
-
},
|
|
68
58
|
};
|
|
69
59
|
};
|
|
60
|
+
|
|
61
|
+
// Workers share one table of 64-bit fingerprints. The first word is never 0, so 0 marks an empty slot.
|
|
62
|
+
export interface SharedFingerprints {
|
|
63
|
+
slots: BigInt64Array;
|
|
64
|
+
used: Int32Array;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const INITIAL_SHARED_CAPACITY = 1 << 10;
|
|
68
|
+
|
|
69
|
+
const insertSharedKey = (slots: BigInt64Array, key: bigint) => {
|
|
70
|
+
const mask = slots.length - 1;
|
|
71
|
+
for (let index = Number(key >> 32n) & mask; ; index = (index + 1) & mask) {
|
|
72
|
+
const stored = Atomics.load(slots, index);
|
|
73
|
+
if (stored === key) return false;
|
|
74
|
+
if (stored !== 0n) continue;
|
|
75
|
+
const previous = Atomics.compareExchange(slots, index, 0n, key);
|
|
76
|
+
if (previous === 0n) return true;
|
|
77
|
+
if (previous === key) return false;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const createSharedFingerprints = (): SharedFingerprints => ({
|
|
82
|
+
slots: new BigInt64Array(new SharedArrayBuffer(INITIAL_SHARED_CAPACITY * 8)),
|
|
83
|
+
used: new Int32Array(new SharedArrayBuffer(4)),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const insertSharedFingerprint = (
|
|
87
|
+
table: SharedFingerprints,
|
|
88
|
+
first: number,
|
|
89
|
+
second: number,
|
|
90
|
+
) => {
|
|
91
|
+
const key = BigInt.asIntN(64, (BigInt(first) << 32n) | BigInt(second >>> 0));
|
|
92
|
+
if (!insertSharedKey(table.slots, key)) return false;
|
|
93
|
+
Atomics.add(table.used, 0, 1);
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const isSharedFingerprintsFull = (table: SharedFingerprints) =>
|
|
98
|
+
Atomics.load(table.used, 0) * 2 >= table.slots.length;
|
|
99
|
+
|
|
100
|
+
export const growSharedFingerprints = (table: SharedFingerprints): SharedFingerprints => {
|
|
101
|
+
const slots = new BigInt64Array(new SharedArrayBuffer(table.slots.byteLength * 2));
|
|
102
|
+
for (const key of table.slots) if (key !== 0n) insertSharedKey(slots, key);
|
|
103
|
+
return { slots, used: table.used };
|
|
104
|
+
};
|
package/src/parallel-worker.ts
CHANGED
|
@@ -1,65 +1,41 @@
|
|
|
1
1
|
import { createCanonicalizer } from "./canonical";
|
|
2
2
|
import { createSearch } from "./check";
|
|
3
3
|
import { createEnvironment } from "./environment";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
createFingerprintTable,
|
|
6
|
+
insertSharedFingerprint,
|
|
7
|
+
isSharedFingerprintsFull,
|
|
8
|
+
writeStateFingerprint,
|
|
9
|
+
} from "./fingerprints";
|
|
10
|
+
import type {
|
|
11
|
+
CoordinatorMessage,
|
|
12
|
+
LevelMessage,
|
|
13
|
+
Port,
|
|
14
|
+
StartMessage,
|
|
15
|
+
WorkerMessage,
|
|
16
|
+
} from "./parallel";
|
|
17
|
+
import { getSuccessors, type Successor } from "./successors";
|
|
7
18
|
import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
|
|
8
19
|
import { OUT_DEGREE_BUCKETS } from "./tlc-summary";
|
|
9
20
|
import { createStateDecoder, createStateEncoder, type EncodedStates } from "./transfer";
|
|
10
21
|
import { getRankedStringCount, setStringOrder, type Value } from "./values";
|
|
11
22
|
|
|
12
23
|
const WORKER_THREADS_MODULE = "node:worker_threads";
|
|
24
|
+
const LOCKED = -1;
|
|
13
25
|
|
|
14
|
-
interface
|
|
26
|
+
interface Expansion {
|
|
15
27
|
states: Value[][];
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
origin: number;
|
|
29
|
+
end: number;
|
|
30
|
+
successors: Successor[] | undefined;
|
|
31
|
+
index: number;
|
|
32
|
+
added: number;
|
|
33
|
+
generated: number;
|
|
34
|
+
outDegrees: number[];
|
|
35
|
+
next: Value[][];
|
|
36
|
+
fingerprintStates: Value[][];
|
|
18
37
|
}
|
|
19
38
|
|
|
20
|
-
interface PeerMessage {
|
|
21
|
-
phase: "query" | "answer" | "sizes" | "balance";
|
|
22
|
-
level: number;
|
|
23
|
-
from: number;
|
|
24
|
-
fingerprints?: Int32Array;
|
|
25
|
-
bits?: Uint8Array;
|
|
26
|
-
states?: EncodedStates;
|
|
27
|
-
unmarks?: Int32Array;
|
|
28
|
-
size?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface Transfer {
|
|
32
|
-
from: number;
|
|
33
|
-
to: number;
|
|
34
|
-
count: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const getBalancePlan = (sizes: number[]) => {
|
|
38
|
-
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
39
|
-
const average = total / sizes.length;
|
|
40
|
-
if (Math.max(...sizes) - Math.min(...sizes) <= Math.max(64, average / 4)) return [];
|
|
41
|
-
const targets = sizes.map(
|
|
42
|
-
(_, index) => Math.floor(average) + (index < total % sizes.length ? 1 : 0),
|
|
43
|
-
);
|
|
44
|
-
const donors = sizes.flatMap((size, index) =>
|
|
45
|
-
size > targets[index] ? [{ index, amount: size - targets[index] }] : [],
|
|
46
|
-
);
|
|
47
|
-
const receivers = sizes.flatMap((size, index) =>
|
|
48
|
-
size < targets[index] ? [{ index, amount: targets[index] - size }] : [],
|
|
49
|
-
);
|
|
50
|
-
const transfers: Transfer[] = [];
|
|
51
|
-
for (const donor of donors) {
|
|
52
|
-
for (const receiver of receivers) {
|
|
53
|
-
const moved = Math.min(donor.amount, receiver.amount);
|
|
54
|
-
if (moved === 0) continue;
|
|
55
|
-
transfers.push({ from: donor.index, to: receiver.index, count: moved });
|
|
56
|
-
donor.amount -= moved;
|
|
57
|
-
receiver.amount -= moved;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return transfers;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
39
|
const getParentPort = async () => {
|
|
64
40
|
if (typeof (globalThis as { postMessage?: unknown }).postMessage === "function") {
|
|
65
41
|
return globalThis as unknown as Port;
|
|
@@ -70,33 +46,6 @@ const getParentPort = async () => {
|
|
|
70
46
|
return threads.parentPort as unknown as Port;
|
|
71
47
|
};
|
|
72
48
|
|
|
73
|
-
const createInbox = () => {
|
|
74
|
-
const messages = new Map<string, PeerMessage[]>();
|
|
75
|
-
const waiters = new Map<string, { count: number; resolve: (messages: PeerMessage[]) => void }>();
|
|
76
|
-
const getKey = (phase: PeerMessage["phase"], level: number) => `${phase}:${level}`;
|
|
77
|
-
const settle = (key: string) => {
|
|
78
|
-
const waiter = waiters.get(key);
|
|
79
|
-
const received = messages.get(key) ?? [];
|
|
80
|
-
if (!waiter || received.length < waiter.count) return;
|
|
81
|
-
waiters.delete(key);
|
|
82
|
-
messages.delete(key);
|
|
83
|
-
waiter.resolve(received);
|
|
84
|
-
};
|
|
85
|
-
return {
|
|
86
|
-
deliver: (message: PeerMessage) => {
|
|
87
|
-
const key = getKey(message.phase, message.level);
|
|
88
|
-
messages.set(key, [...(messages.get(key) ?? []), message]);
|
|
89
|
-
settle(key);
|
|
90
|
-
},
|
|
91
|
-
receive: (phase: PeerMessage["phase"], level: number, count: number) =>
|
|
92
|
-
new Promise<PeerMessage[]>((resolve) => {
|
|
93
|
-
const key = getKey(phase, level);
|
|
94
|
-
waiters.set(key, { count, resolve });
|
|
95
|
-
settle(key);
|
|
96
|
-
}),
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
|
|
100
49
|
const run = (start: StartMessage, parent: Port) => {
|
|
101
50
|
const modules = new Map(start.modules);
|
|
102
51
|
const loadModule = (name: string) => modules.get(name);
|
|
@@ -117,165 +66,163 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
117
66
|
const nextAction = search?.spec.next;
|
|
118
67
|
if (!search || nextAction === undefined)
|
|
119
68
|
return parent.postMessage({ type: "abort" } satisfies WorkerMessage);
|
|
120
|
-
const count = start.count;
|
|
121
69
|
const self = start.index;
|
|
122
70
|
const canonicalize = createCanonicalizer();
|
|
123
71
|
const decode = createStateDecoder(environment.variables.length);
|
|
124
|
-
const
|
|
125
|
-
const
|
|
72
|
+
const shared = { fingerprints: start.fingerprints };
|
|
73
|
+
const requests = start.requests;
|
|
74
|
+
const remaining = start.remaining;
|
|
75
|
+
// Under symmetry, a successor the worker generated before has the representative it had then,
|
|
76
|
+
// so the worker skips the search for one.
|
|
77
|
+
const generatedStates = search.isSymmetric ? createFingerprintTable() : undefined;
|
|
126
78
|
const fingerprint = new Int32Array(2);
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
peer.port.start?.();
|
|
79
|
+
const waiters = { resume: () => {}, loot: (_states: EncodedStates) => {} };
|
|
80
|
+
start.ports.forEach((port) => {
|
|
81
|
+
port?.addEventListener("message", (event) => waiters.loot(event.data as EncodedStates));
|
|
82
|
+
port?.start?.();
|
|
132
83
|
});
|
|
133
|
-
for (let index = 0; index < start.seedFingerprints.length; index += 2) {
|
|
134
|
-
table.insert(start.seedFingerprints[index], start.seedFingerprints[index + 1]);
|
|
135
|
-
}
|
|
136
84
|
const frontier = { states: decode(start.seed).map(canonicalize) };
|
|
137
|
-
const send = (index: number, message: PeerMessage, transfer: unknown[]) =>
|
|
138
|
-
start.ports[index]?.postMessage(message, transfer);
|
|
139
85
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
86
|
+
// A worker that runs out of states asks a peer for some by writing its index + 1 to the peer's
|
|
87
|
+
// request word. The peer sends half of the states it hasn't started, keeps expanding, and wakes
|
|
88
|
+
// the workers waiting to ask it next.
|
|
89
|
+
const answer = (expansion: Expansion, request: number) => {
|
|
90
|
+
const thief = Atomics.load(requests, self) - 1;
|
|
91
|
+
const given = Math.floor((expansion.end - expansion.origin - 1) / 2);
|
|
92
|
+
const encoder = createStateEncoder();
|
|
93
|
+
const states = expansion.states.slice(expansion.end - given, expansion.end);
|
|
94
|
+
const isEncoded = given > 0 && states.every(encoder.encode);
|
|
95
|
+
if (isEncoded) expansion.end -= given;
|
|
96
|
+
const loot = isEncoded ? encoder.finish() : createStateEncoder().finish();
|
|
97
|
+
start.ports[thief]?.postMessage(loot, [loot.words.buffer]);
|
|
98
|
+
Atomics.store(requests, self, request);
|
|
99
|
+
Atomics.notify(requests, self);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// A worker answers any request before it pauses or runs out of states, and takes no more until
|
|
103
|
+
// it resumes.
|
|
104
|
+
const lock = (expansion: Expansion) => {
|
|
105
|
+
Atomics.store(remaining, self, 0);
|
|
106
|
+
if (Atomics.compareExchange(requests, self, 0, LOCKED) === 0) return;
|
|
107
|
+
answer(expansion, LOCKED);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const unlock = (expansion: Expansion) => {
|
|
111
|
+
Atomics.store(remaining, self, expansion.end - expansion.origin);
|
|
112
|
+
Atomics.store(requests, self, 0);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// The worker pauses before a successor when the shared table needs to grow, and resumes there.
|
|
116
|
+
const expand = (expansion: Expansion) => {
|
|
117
|
+
for (; expansion.origin < expansion.end; expansion.origin++) {
|
|
118
|
+
const state = expansion.states[expansion.origin];
|
|
119
|
+
if (!expansion.successors) {
|
|
120
|
+
if (Atomics.load(requests, self) > 0) answer(expansion, 0);
|
|
121
|
+
Atomics.store(remaining, self, expansion.end - expansion.origin - 1);
|
|
122
|
+
expansion.successors = getSuccessors(nextAction, state, environment, search.spec.scope);
|
|
123
|
+
expansion.generated += expansion.successors.length;
|
|
124
|
+
if (expansion.successors.length === 0 && start.config.checkDeadlock) return undefined;
|
|
125
|
+
}
|
|
126
|
+
const successors = expansion.successors;
|
|
127
|
+
for (; expansion.index < successors.length; expansion.index++) {
|
|
128
|
+
if (isSharedFingerprintsFull(shared.fingerprints)) return "paused";
|
|
129
|
+
const successor = successors[expansion.index];
|
|
154
130
|
if (search.findViolatedActionProperty(state, successor.state)) return undefined;
|
|
155
131
|
if (!search.satisfiesActionConstraints(state, successor.state)) continue;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (owner === self) {
|
|
160
|
-
if (!table.insert(fingerprint[0], fingerprint[1])) continue;
|
|
161
|
-
if (search.findViolatedInvariant(successor.state, state)) return undefined;
|
|
162
|
-
if (!search.satisfiesConstraints(successor.state, state)) {
|
|
163
|
-
table.remove(fingerprint[0], fingerprint[1]);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
next.push(successor.state);
|
|
167
|
-
newStates[origin]++;
|
|
168
|
-
continue;
|
|
132
|
+
if (generatedStates) {
|
|
133
|
+
writeStateFingerprint(successor.state, fingerprint);
|
|
134
|
+
if (!generatedStates.insert(fingerprint[0], fingerprint[1])) continue;
|
|
169
135
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
136
|
+
canonicalize(successor.state);
|
|
137
|
+
const fingerprintState = search.getFingerprintState(successor.state);
|
|
138
|
+
writeStateFingerprint(fingerprintState, fingerprint);
|
|
139
|
+
if (!insertSharedFingerprint(shared.fingerprints, fingerprint[0], fingerprint[1])) continue;
|
|
140
|
+
if (search.findViolatedInvariant(successor.state, state)) return undefined;
|
|
141
|
+
if (!search.satisfiesConstraints(successor.state, state)) continue;
|
|
142
|
+
expansion.next.push(successor.state);
|
|
143
|
+
expansion.fingerprintStates.push(fingerprintState);
|
|
144
|
+
expansion.added++;
|
|
174
145
|
}
|
|
146
|
+
expansion.outDegrees[Math.min(expansion.added, OUT_DEGREE_BUCKETS - 1)]++;
|
|
147
|
+
expansion.successors = undefined;
|
|
148
|
+
expansion.index = 0;
|
|
149
|
+
expansion.added = 0;
|
|
175
150
|
}
|
|
176
|
-
return
|
|
151
|
+
return "done";
|
|
177
152
|
};
|
|
178
153
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
154
|
+
// Each attempt asks the peer with the most states left. While another worker's request to that
|
|
155
|
+
// peer is open, the worker waits for the peer to answer it. The worker stops once no peer that
|
|
156
|
+
// takes requests has two states left.
|
|
157
|
+
const steal = async (expansion: Expansion) => {
|
|
158
|
+
const skipped = new Set<number>();
|
|
159
|
+
for (;;) {
|
|
160
|
+
const victim = start.ports.reduce<number | undefined>((best, port, index) => {
|
|
161
|
+
if (!port || skipped.has(index) || Atomics.load(remaining, index) < 2) return best;
|
|
162
|
+
return best === undefined || Atomics.load(remaining, index) > Atomics.load(remaining, best)
|
|
163
|
+
? index
|
|
164
|
+
: best;
|
|
165
|
+
}, undefined);
|
|
166
|
+
if (victim === undefined) return false;
|
|
167
|
+
const request = Atomics.compareExchange(requests, victim, 0, self + 1);
|
|
168
|
+
if (request === LOCKED) skipped.add(victim);
|
|
169
|
+
if (request > 0) Atomics.wait(requests, victim, request);
|
|
170
|
+
if (request !== 0) continue;
|
|
171
|
+
const loot = await new Promise<EncodedStates>((resolve) => (waiters.loot = resolve));
|
|
172
|
+
if (loot.count === 0) {
|
|
173
|
+
skipped.add(victim);
|
|
197
174
|
continue;
|
|
198
175
|
}
|
|
199
|
-
|
|
200
|
-
|
|
176
|
+
expansion.states = decode(loot).map(canonicalize);
|
|
177
|
+
expansion.origin = 0;
|
|
178
|
+
expansion.end = expansion.states.length;
|
|
179
|
+
unlock(expansion);
|
|
180
|
+
return true;
|
|
201
181
|
}
|
|
202
|
-
return Int32Array.from(unmarks);
|
|
203
182
|
};
|
|
204
183
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
table.remove(fingerprints[index], fingerprints[index + 1]);
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const balance = async (level: number, next: Value[][], sizes: number[]) => {
|
|
212
|
-
const transfers = getBalancePlan(sizes);
|
|
213
|
-
if (transfers.length === 0) return true;
|
|
214
|
-
const outgoing = new Map(peers.map((peer) => [peer.index, createStateEncoder()]));
|
|
215
|
-
for (const transfer of transfers.filter((candidate) => candidate.from === self)) {
|
|
216
|
-
const encoder = outgoing.get(transfer.to);
|
|
217
|
-
const states = next.splice(next.length - transfer.count, transfer.count);
|
|
218
|
-
if (!encoder || !states.every(encoder.encode)) return false;
|
|
219
|
-
}
|
|
220
|
-
outgoing.forEach((encoder, index) => {
|
|
221
|
-
const states = encoder.finish();
|
|
222
|
-
send(index, { phase: "balance", level, from: self, states }, [states.words.buffer]);
|
|
223
|
-
});
|
|
224
|
-
const incoming = await inbox.receive("balance", level, peers.length);
|
|
225
|
-
incoming.forEach((message) => {
|
|
226
|
-
if (message.states) next.push(...decode(message.states).map(canonicalize));
|
|
227
|
-
});
|
|
228
|
-
return true;
|
|
229
|
-
};
|
|
184
|
+
const report = (message: WorkerMessage) =>
|
|
185
|
+
parent.postMessage(message, message.type === "level" ? [message.fingerprints.buffer] : []);
|
|
230
186
|
|
|
231
187
|
const runLevel = async (level: number): Promise<LevelMessage | undefined> => {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
188
|
+
const expansion: Expansion = {
|
|
189
|
+
states: frontier.states,
|
|
190
|
+
origin: 0,
|
|
191
|
+
end: frontier.states.length,
|
|
192
|
+
successors: undefined,
|
|
193
|
+
index: 0,
|
|
194
|
+
added: 0,
|
|
195
|
+
generated: 0,
|
|
196
|
+
outDegrees: Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0),
|
|
197
|
+
next: [],
|
|
198
|
+
fingerprintStates: [],
|
|
199
|
+
};
|
|
200
|
+
for (;;) {
|
|
201
|
+
const status = expand(expansion);
|
|
202
|
+
if (status === undefined) return undefined;
|
|
203
|
+
lock(expansion);
|
|
204
|
+
if (status === "paused") {
|
|
205
|
+
const resumed = new Promise<void>((resolve) => (waiters.resume = resolve));
|
|
206
|
+
report({ type: "paused" });
|
|
207
|
+
await resumed;
|
|
208
|
+
unlock(expansion);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!(await steal(expansion))) break;
|
|
254
212
|
}
|
|
255
|
-
const added = next.length;
|
|
256
|
-
const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
|
|
257
|
-
expanded.newStates.forEach((degree) => outDegrees[Math.min(degree, OUT_DEGREE_BUCKETS - 1)]++);
|
|
258
|
-
const fingerprints = BigInt64Array.from(next, (state) =>
|
|
259
|
-
environment.runtime.getFingerprint(state),
|
|
260
|
-
);
|
|
261
|
-
const sizes = Array.from({ length: count }, () => 0);
|
|
262
|
-
sizes[self] = added;
|
|
263
|
-
peers.forEach((peer) =>
|
|
264
|
-
send(peer.index, { phase: "sizes", level, from: self, size: added }, []),
|
|
265
|
-
);
|
|
266
|
-
const received = await inbox.receive("sizes", level, peers.length * 2);
|
|
267
|
-
received.forEach((message) => {
|
|
268
|
-
if (message.unmarks) removeAll(message.unmarks);
|
|
269
|
-
else sizes[message.from] = message.size ?? 0;
|
|
270
|
-
});
|
|
271
213
|
if (getRankedStringCount() !== startupStrings) return undefined;
|
|
272
|
-
|
|
273
|
-
frontier.states
|
|
274
|
-
return {
|
|
214
|
+
frontier.states = expansion.next;
|
|
215
|
+
Atomics.store(remaining, self, frontier.states.length);
|
|
216
|
+
return {
|
|
217
|
+
type: "level",
|
|
218
|
+
level,
|
|
219
|
+
generated: expansion.generated,
|
|
220
|
+
added: expansion.next.length,
|
|
221
|
+
outDegrees: expansion.outDegrees,
|
|
222
|
+
fingerprints: search.getStoredFingerprints(expansion.next, expansion.fingerprintStates),
|
|
223
|
+
};
|
|
275
224
|
};
|
|
276
225
|
|
|
277
|
-
const report = (message: WorkerMessage) =>
|
|
278
|
-
parent.postMessage(message, message.type === "level" ? [message.fingerprints.buffer] : []);
|
|
279
226
|
const step = (level: number) => {
|
|
280
227
|
runLevel(level).then(
|
|
281
228
|
(message) => report(message ?? { type: "abort" }),
|
|
@@ -283,9 +230,14 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
283
230
|
);
|
|
284
231
|
};
|
|
285
232
|
parent.addEventListener("message", (event) => {
|
|
286
|
-
const message = event.data as
|
|
233
|
+
const message = event.data as CoordinatorMessage;
|
|
287
234
|
if (message.type === "level") step(message.level);
|
|
235
|
+
if (message.type === "resize") {
|
|
236
|
+
shared.fingerprints = message.fingerprints;
|
|
237
|
+
waiters.resume();
|
|
238
|
+
}
|
|
288
239
|
});
|
|
240
|
+
Atomics.store(remaining, self, frontier.states.length);
|
|
289
241
|
report({ type: "ready" });
|
|
290
242
|
};
|
|
291
243
|
|