knitting 0.1.70 → 0.1.73
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 +101 -10
- package/knitting.browser.d.ts +3 -1
- package/knitting.browser.js +1 -1
- package/knitting.d.ts +3 -1
- package/knitting.js +2 -1
- package/package.json +8 -3
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/src/api.js +58 -34
- package/src/connections/node-addons.js +11 -1
- package/src/debug/gate.js +1 -1
- package/src/debug/handle.d.ts +6 -1
- package/src/debug/handle.js +14 -6
- package/src/error.d.ts +9 -0
- package/src/error.js +16 -2
- package/src/memory/lock.d.ts +48 -6
- package/src/memory/lock.js +350 -142
- package/src/memory/payloadCodec.js +31 -11
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +8 -3
- package/src/runtime/dispatcher.d.ts +6 -1
- package/src/runtime/dispatcher.js +24 -8
- package/src/runtime/inline-executor.js +2 -1
- package/src/runtime/pool.d.ts +13 -4
- package/src/runtime/pool.js +107 -47
- package/src/runtime/process-worker.js +10 -1
- package/src/runtime/tx-queue.d.ts +2 -1
- package/src/runtime/tx-queue.js +11 -0
- package/src/runtime/worker-common.d.ts +7 -0
- package/src/runtime/worker-common.js +28 -2
- package/src/types.d.ts +35 -11
- package/src/worker/loop.js +17 -4
- package/src/worker/safety/index.d.ts +1 -1
- package/src/worker/safety/index.js +1 -1
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +8 -1
- package/src/worker/safety/startup.js +11 -6
- package/src/worker/timers.js +25 -3
|
@@ -6,24 +6,29 @@ const execFlagKey = (flag) => flag.split("=", 1)[0];
|
|
|
6
6
|
const NODE_PERMISSION_EXEC_FLAGS = new Set([
|
|
7
7
|
"--permission",
|
|
8
8
|
"--experimental-permission",
|
|
9
|
+
"--experimental-config-file",
|
|
10
|
+
"--experimental-default-config-file",
|
|
9
11
|
"--allow-fs-read",
|
|
10
12
|
"--allow-fs-write",
|
|
13
|
+
"--allow-fs-vfs",
|
|
11
14
|
"--allow-worker",
|
|
12
15
|
"--allow-child-process",
|
|
16
|
+
"--allow-env",
|
|
13
17
|
"--allow-net",
|
|
14
18
|
"--allow-addons",
|
|
15
19
|
"--allow-ffi",
|
|
16
20
|
"--allow-wasi",
|
|
21
|
+
"--allow-inspector",
|
|
22
|
+
"--allow-openssl-store",
|
|
17
23
|
]);
|
|
18
24
|
const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
|
|
19
25
|
"--experimental-ffi",
|
|
20
26
|
"--experimental-transform-types",
|
|
21
|
-
"--expose-gc",
|
|
22
27
|
"--no-warnings",
|
|
23
28
|
...NODE_PERMISSION_EXEC_FLAGS,
|
|
24
29
|
]);
|
|
25
30
|
const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
|
|
26
|
-
const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
|
|
31
|
+
export const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
|
|
27
32
|
export const toWorkerSafeExecArgv = (flags) => {
|
|
28
33
|
if (!flags || flags.length === 0)
|
|
29
34
|
return undefined;
|
|
@@ -40,6 +45,7 @@ export const toWorkerSafeExecArgv = (flags) => {
|
|
|
40
45
|
}
|
|
41
46
|
return deduped;
|
|
42
47
|
};
|
|
48
|
+
/** Preserve process-worker runtime flags while replacing inherited permissions. */
|
|
43
49
|
export const toWorkerCompatExecArgv = (flags) => {
|
|
44
50
|
const safe = toWorkerSafeExecArgv(flags);
|
|
45
51
|
if (!safe || safe.length === 0)
|
|
@@ -47,6 +53,26 @@ export const toWorkerCompatExecArgv = (flags) => {
|
|
|
47
53
|
const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
|
|
48
54
|
return compat.length > 0 ? compat : undefined;
|
|
49
55
|
};
|
|
56
|
+
const droppedExecArgvWarnings = new Set();
|
|
57
|
+
/**
|
|
58
|
+
* Warn once per flag set when a `workerExecArgv` flag the caller asked for did
|
|
59
|
+
* not reach the worker thread, instead of dropping it silently.
|
|
60
|
+
*/
|
|
61
|
+
export const warnDroppedWorkerExecArgv = (requested, applied) => {
|
|
62
|
+
if (!requested || requested.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
const kept = new Set(applied ?? []);
|
|
65
|
+
const dropped = requested.filter((flag) => !kept.has(flag));
|
|
66
|
+
if (dropped.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
const key = dropped.join(" ");
|
|
69
|
+
if (droppedExecArgvWarnings.has(key))
|
|
70
|
+
return;
|
|
71
|
+
droppedExecArgvWarnings.add(key);
|
|
72
|
+
console.warn(`knitting: workerExecArgv ${dropped.join(", ")} cannot be applied to a ` +
|
|
73
|
+
`worker thread and was dropped. V8 and process-wide flags must be ` +
|
|
74
|
+
`passed to the host process instead.`);
|
|
75
|
+
};
|
|
50
76
|
const isPlainRecord = (value) => {
|
|
51
77
|
if (value === null || typeof value !== "object")
|
|
52
78
|
return false;
|
package/src/types.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ type WorkerData = {
|
|
|
28
28
|
thread: number;
|
|
29
29
|
totalNumberOfThread: number;
|
|
30
30
|
debug?: DebugOptions;
|
|
31
|
+
/**
|
|
32
|
+
* Host's debug zero as `performance.timeOrigin + performance.now()` (Unix
|
|
33
|
+
* epoch ms), so worker debug clocks line up with the host's. Set only when
|
|
34
|
+
* debug is requested.
|
|
35
|
+
*/
|
|
36
|
+
debugEpoch?: number;
|
|
31
37
|
startAt: number;
|
|
32
38
|
workerOptions?: WorkerSettings;
|
|
33
39
|
at: number[];
|
|
@@ -50,8 +56,8 @@ type WorkerData = {
|
|
|
50
56
|
consumers: number;
|
|
51
57
|
consumerId: number;
|
|
52
58
|
regionLanes: number;
|
|
53
|
-
/**
|
|
54
|
-
claim?: "dekker" | "
|
|
59
|
+
/** Claim discipline; see `DispatcherSettings.stealClaim`. */
|
|
60
|
+
claim?: "dekker" | "ticket";
|
|
55
61
|
};
|
|
56
62
|
};
|
|
57
63
|
type UnsafeOptions = {
|
|
@@ -173,12 +179,16 @@ type SingleTaskPool<A extends TaskInput = Args, B extends Args = Args, AS extend
|
|
|
173
179
|
shutdown: (delayMs?: number) => Promise<void>;
|
|
174
180
|
/** Starts shutdown at scope exit. Use `shutdown()` when you must await it. */
|
|
175
181
|
[Symbol.dispose]: () => void;
|
|
182
|
+
/** `await using` awaits worker teardown at scope exit. */
|
|
183
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
176
184
|
};
|
|
177
185
|
type Pool<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
|
|
178
186
|
/** Await worker teardown now. `using` disposes at scope exit without awaiting. */
|
|
179
187
|
shutdown: (delayMs?: number) => Promise<void>;
|
|
180
188
|
/** Starts shutdown at scope exit. Use `shutdown()` when you must await it. */
|
|
181
189
|
[Symbol.dispose]: () => void;
|
|
190
|
+
/** `await using` awaits worker teardown at scope exit. */
|
|
191
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
182
192
|
/**
|
|
183
193
|
* Typed task callers. Each call accepts the task input or a native Promise.
|
|
184
194
|
* Thrown errors/rejections reject here as Error objects with cause chains.
|
|
@@ -214,8 +224,8 @@ type Balancer = BalancerStrategy | {
|
|
|
214
224
|
*/
|
|
215
225
|
strategy?: BalancerStrategy;
|
|
216
226
|
};
|
|
217
|
-
/** Debug namespaces for host setup, worker state, imports, globals, and
|
|
218
|
-
type DebugNamespace = "host" | "globals" | "signals" | "imports" | "lifecycle";
|
|
227
|
+
/** Debug namespaces for host setup, worker state, imports, globals, lifecycle, and steal claims. */
|
|
228
|
+
type DebugNamespace = "host" | "globals" | "signals" | "imports" | "lifecycle" | "steal";
|
|
219
229
|
type DebugFlags = {
|
|
220
230
|
[Namespace in DebugNamespace]?: boolean;
|
|
221
231
|
};
|
|
@@ -340,7 +350,10 @@ type WorkerTimers = {
|
|
|
340
350
|
*/
|
|
341
351
|
spinMicroseconds?: number;
|
|
342
352
|
/**
|
|
343
|
-
* Atomics.wait timeout when parked (milliseconds).
|
|
353
|
+
* Atomics.wait timeout when parked (milliseconds). Defaults to 1000 for
|
|
354
|
+
* thread workers, which the host wakes on every publish, so the timeout is
|
|
355
|
+
* only a safety net; 1 for process workers, most of which only rediscover
|
|
356
|
+
* work when it expires.
|
|
344
357
|
*/
|
|
345
358
|
parkMs?: number;
|
|
346
359
|
/**
|
|
@@ -383,8 +396,13 @@ type DispatcherSettings = {
|
|
|
383
396
|
* tasks; Dekker requires at least one spare region per live consumer.
|
|
384
397
|
*/
|
|
385
398
|
stealRegionLanes?: number;
|
|
386
|
-
/**
|
|
387
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Publication-ordered tickets (`"ticket"`, the default) or Dekker regions
|
|
401
|
+
* (`"dekker"`). Unrecognised values are rejected rather than defaulted, so a
|
|
402
|
+
* removed discipline such as `cas-mask` fails at pool creation. Ticket pools
|
|
403
|
+
* reject pending and future calls on worker failure.
|
|
404
|
+
*/
|
|
405
|
+
stealClaim?: "dekker" | "ticket";
|
|
388
406
|
};
|
|
389
407
|
type CreatePool = {
|
|
390
408
|
/** Number of workers. Default: 1. */
|
|
@@ -412,14 +430,20 @@ type CreatePool = {
|
|
|
412
430
|
*/
|
|
413
431
|
host?: DispatcherSettings;
|
|
414
432
|
/**
|
|
415
|
-
* Extra Node.js execArgv flags for worker threads (e.g. ["--
|
|
416
|
-
* Defaults to process.execArgv
|
|
433
|
+
* Extra Node.js execArgv flags for worker threads (e.g. ["--no-warnings"]).
|
|
434
|
+
* Defaults to compatible flags from process.execArgv. Node permission flags
|
|
435
|
+
* are replaced by the resolved `permission` policy.
|
|
436
|
+
*
|
|
437
|
+
* Node rejects V8 and process-wide flags (`--expose-gc`,
|
|
438
|
+
* `--max-old-space-size`, ...). Unsupported caller flags are dropped with a
|
|
439
|
+
* warning. Permission flags are never dropped to start an unpermissioned
|
|
440
|
+
* thread worker; pool creation fails if Node cannot apply them.
|
|
417
441
|
*/
|
|
418
442
|
workerExecArgv?: string[];
|
|
419
443
|
/**
|
|
420
444
|
* Runtime permission protocol.
|
|
421
|
-
* Omit to use strict defaults with `allowImport: true
|
|
422
|
-
*
|
|
445
|
+
* Omit to use strict defaults with `allowImport: true`. `console` is
|
|
446
|
+
* accepted but not enforced: worker console output is always forwarded.
|
|
423
447
|
*
|
|
424
448
|
* Task code cannot terminate the host: process/Deno exit APIs are blocked.
|
|
425
449
|
* Use `"strict"` (default for object mode) or `"unsafe"`.
|
package/src/worker/loop.js
CHANGED
|
@@ -13,12 +13,14 @@ import { getFunctions } from "./task-loader.js";
|
|
|
13
13
|
import { IS_BUN_WINDOWS, pauseGeneric, sleepUntilChanged, whilePausing, } from "./timers.js";
|
|
14
14
|
import { IS_ANDROMEDA, RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
|
|
15
15
|
import { getNodeProcess } from "../common/node-compat.js";
|
|
16
|
-
import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
|
|
16
|
+
import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
|
|
17
17
|
import { signalAbortFactory } from "../shared/abortSignal.js";
|
|
18
18
|
import { runWorkerBootstrap } from "./bootstrap.js";
|
|
19
19
|
import { getProcessWorkerNativeWaitU32, installProcessWorkerBootstrap, } from "./process-worker-bootstrap.js";
|
|
20
20
|
import { resolveDebugNamespaces } from "../debug/gate.js";
|
|
21
21
|
const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
|
|
22
|
+
// Park timeout while finished results wait for return slots (see the loop).
|
|
23
|
+
const BLOCKED_RETURN_PARK_MS = 1;
|
|
22
24
|
const reportWorkerStartupFatal = (error) => {
|
|
23
25
|
const message = String(error?.message ?? error);
|
|
24
26
|
const payload = {
|
|
@@ -59,17 +61,22 @@ export const workerMainLoop = async (startupData) => {
|
|
|
59
61
|
installTerminationGuard();
|
|
60
62
|
installUnhandledRejectionSilencer();
|
|
61
63
|
installPerformanceNowGuard();
|
|
62
|
-
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, sharedReturn, permission, notifyOnHostPublish, processCompletionDoorbell, denoCompletionDoorbell, nodeCompletionDoorbell, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
|
|
64
|
+
const { debug, debugEpoch, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, sharedReturn, permission, notifyOnHostPublish, processCompletionDoorbell, denoCompletionDoorbell, nodeCompletionDoorbell, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
|
|
63
65
|
scrubWorkerDataSensitiveBuffers(startupData);
|
|
64
66
|
assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
|
|
65
67
|
const debugNamespaces = resolveDebugNamespaces(debug);
|
|
66
68
|
const dbg = debugNamespaces.size > 0
|
|
67
|
-
? await import("../debug/handle.
|
|
69
|
+
? await import("../debug/handle.js").then((module) => module.initDebug({
|
|
68
70
|
name: `w${thread}`,
|
|
69
71
|
runtime: RUNTIME,
|
|
70
72
|
namespaces: debugNamespaces,
|
|
73
|
+
epoch: debugEpoch,
|
|
71
74
|
}))
|
|
72
75
|
: undefined;
|
|
76
|
+
if (dbg?.enabled("lifecycle") === true) {
|
|
77
|
+
// Detached task promises reject silently otherwise (see the silencer).
|
|
78
|
+
reportSilencedRejections((reason) => dbg.log("lifecycle", `unhandled rejection silenced: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`));
|
|
79
|
+
}
|
|
73
80
|
// Andromeda cannot parse enums.
|
|
74
81
|
const Comment = {
|
|
75
82
|
thisIsAHint: 0,
|
|
@@ -97,6 +104,10 @@ export const workerMainLoop = async (startupData) => {
|
|
|
97
104
|
consumerId: steal?.consumerId,
|
|
98
105
|
regionLanes: steal?.regionLanes,
|
|
99
106
|
stealClaim: steal?.claim,
|
|
107
|
+
// Undefined unless `steal` is traced, so the claim path stays untouched.
|
|
108
|
+
traceClaim: steal !== undefined && dbg?.enabled("steal") === true
|
|
109
|
+
? (message) => dbg.log("steal", message)
|
|
110
|
+
: undefined,
|
|
100
111
|
});
|
|
101
112
|
const notifyDenoHost = createDenoCompletionNotifier(denoCompletionDoorbell);
|
|
102
113
|
const notifyNodeHost = createNodeCompletionNotifier(nodeCompletionDoorbell);
|
|
@@ -342,7 +353,9 @@ export const workerMainLoop = async (startupData) => {
|
|
|
342
353
|
} while (txStatus[Comment.thisIsAHint] === 1);
|
|
343
354
|
continue;
|
|
344
355
|
}
|
|
345
|
-
|
|
356
|
+
// Results that could not be written wait on the host freeing return
|
|
357
|
+
// slots, which rings no worker in particular: keep that park short.
|
|
358
|
+
_pauseUntil(wakeToken, spinMicroseconds, _hasCompleted() ? Math.min(parkMs, BLOCKED_RETURN_PARK_MS) : parkMs);
|
|
346
359
|
wakeToken = a_load(opView, 0);
|
|
347
360
|
}
|
|
348
361
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { installTerminationGuard, installUnhandledRejectionSilencer, } from "./process.js";
|
|
1
|
+
export { installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, } from "./process.js";
|
|
2
2
|
export { installPerformanceNowGuard } from "./performance.js";
|
|
3
3
|
export { scrubWorkerDataSensitiveBuffers } from "./worker-data.js";
|
|
4
4
|
export { assertWorkerSharedMemoryBootData, assertWorkerImportsResolved, } from "./startup.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { installTerminationGuard, installUnhandledRejectionSilencer, } from "./process.js";
|
|
1
|
+
export { installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, } from "./process.js";
|
|
2
2
|
export { installPerformanceNowGuard } from "./performance.js";
|
|
3
3
|
export { scrubWorkerDataSensitiveBuffers } from "./worker-data.js";
|
|
4
4
|
export { assertWorkerSharedMemoryBootData, assertWorkerImportsResolved, } from "./startup.js";
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare const installTerminationGuard: () => void;
|
|
2
|
+
/** Debug hook: see what the silencer below swallows. Unset by default. */
|
|
3
|
+
export declare const reportSilencedRejections: (report: ((reason: unknown) => void) | undefined) => void;
|
|
2
4
|
export declare const installUnhandledRejectionSilencer: () => void;
|
|
@@ -66,6 +66,11 @@ export const installTerminationGuard = () => {
|
|
|
66
66
|
}
|
|
67
67
|
proc.__knittingTerminationGuard = true;
|
|
68
68
|
};
|
|
69
|
+
let reportSilencedRejection;
|
|
70
|
+
/** Debug hook: see what the silencer below swallows. Unset by default. */
|
|
71
|
+
export const reportSilencedRejections = (report) => {
|
|
72
|
+
reportSilencedRejection = report;
|
|
73
|
+
};
|
|
69
74
|
export const installUnhandledRejectionSilencer = () => {
|
|
70
75
|
const proc = getNodeProcess();
|
|
71
76
|
if (!proc || typeof proc.on !== "function") {
|
|
@@ -75,5 +80,7 @@ export const installUnhandledRejectionSilencer = () => {
|
|
|
75
80
|
return;
|
|
76
81
|
proc.__knittingUnhandledRejectionSilencer = true;
|
|
77
82
|
// Worker task code may create detached promises; keep workers alive.
|
|
78
|
-
proc.on("unhandledRejection", () => {
|
|
83
|
+
proc.on("unhandledRejection", (reason) => {
|
|
84
|
+
reportSilencedRejection?.(reason);
|
|
85
|
+
});
|
|
79
86
|
};
|
|
@@ -21,10 +21,15 @@ export const assertWorkerImportsResolved = ({ list, ids, names, listOfFunctions
|
|
|
21
21
|
if (listOfFunctions.length > 0 &&
|
|
22
22
|
(names === undefined || listOfFunctions.length === names.length))
|
|
23
23
|
return;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const found = new Set(listOfFunctions.map((entry) => entry?.name));
|
|
25
|
+
const missing = (names ?? []).filter((name) => !found.has(name));
|
|
26
|
+
const tasks = missing.length > 0
|
|
27
|
+
? ` Missing task export(s): ${missing.join(", ")}.`
|
|
28
|
+
: "";
|
|
29
|
+
const modules = list.length > 0 ? ` Looked in: ${list.join(", ")}.` : "";
|
|
30
|
+
throw new Error(`No imports were found.${tasks}${modules}` +
|
|
31
|
+
" Workers re-import each listed module and look tasks up by export" +
|
|
32
|
+
" name: a bare function must be exported from the module that calls" +
|
|
33
|
+
" createPool, and a bundle that inlines task modules drops their" +
|
|
34
|
+
" exports.");
|
|
30
35
|
};
|
package/src/worker/timers.js
CHANGED
|
@@ -2,10 +2,24 @@
|
|
|
2
2
|
const Comment = {
|
|
3
3
|
thisIsAHint: 0,
|
|
4
4
|
};
|
|
5
|
+
// A full collection costs milliseconds. Run on every idle entry, it made each
|
|
6
|
+
// sequential call ~1000x slower under --expose-gc; once a second keeps the
|
|
7
|
+
// idle collection without paying it per call.
|
|
8
|
+
const IDLE_GC_INTERVAL_MS = 1000;
|
|
5
9
|
const maybeGc = (() => {
|
|
6
10
|
const host = globalThis;
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
// Capture the function itself: the global is deleted below, so reading
|
|
12
|
+
// `host.gc` at call time would throw on the first idle pass.
|
|
13
|
+
const exposedGc = host.gc;
|
|
14
|
+
let lastGcAt = -Infinity;
|
|
15
|
+
const gc = typeof exposedGc === "function"
|
|
16
|
+
? (() => {
|
|
17
|
+
const now = performance.now();
|
|
18
|
+
if (now - lastGcAt < IDLE_GC_INTERVAL_MS)
|
|
19
|
+
return;
|
|
20
|
+
lastGcAt = now;
|
|
21
|
+
exposedGc();
|
|
22
|
+
})
|
|
9
23
|
: undefined;
|
|
10
24
|
if (gc) {
|
|
11
25
|
try {
|
|
@@ -82,7 +96,6 @@ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, tx
|
|
|
82
96
|
};
|
|
83
97
|
return (value, spinMicroseconds, parkMs) => {
|
|
84
98
|
const until = p_now() + (spinMicroseconds / 1000);
|
|
85
|
-
maybeGc();
|
|
86
99
|
let spinChecks = 0;
|
|
87
100
|
while (true) {
|
|
88
101
|
if (a_load(opView, at) !== value ||
|
|
@@ -94,9 +107,18 @@ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, tx
|
|
|
94
107
|
if ((spinChecks++ & 63) === 0 && p_now() >= until)
|
|
95
108
|
break;
|
|
96
109
|
}
|
|
110
|
+
// Collect only once the spin found nothing, then re-check before parking.
|
|
111
|
+
maybeGc();
|
|
97
112
|
if (tryProgress())
|
|
98
113
|
return;
|
|
99
114
|
a_store(rxStatus, 0, 0);
|
|
115
|
+
// The other half of the host's post-publish check: a publication that
|
|
116
|
+
// landed before rxStatus cleared is visible now, and one after it finds
|
|
117
|
+
// rxStatus clear and rings.
|
|
118
|
+
if (a_load(opView, at) !== value || tryProgress()) {
|
|
119
|
+
a_store(rxStatus, 0, 1);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
100
122
|
if (nativeWaitU32 !== undefined) {
|
|
101
123
|
nativeWaitU32(opView.buffer, opView.byteOffset + (at * Int32Array.BYTES_PER_ELEMENT), value >>> 0, nativeWaitTimeoutMs(parkMs));
|
|
102
124
|
}
|