knitting 0.1.60 → 0.1.62
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/BROWSER.md +134 -0
- package/README.md +94 -6
- package/knitting.browser.d.ts +6 -0
- package/knitting.browser.js +1 -0
- package/package.json +11 -2
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
- 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_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_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/src/api.js +136 -25
- package/src/common/runtime.d.ts +2 -1
- package/src/common/runtime.js +8 -1
- package/src/common/task-source.d.ts +9 -0
- package/src/common/task-source.js +15 -0
- package/src/memory/lock.d.ts +37 -1
- package/src/memory/lock.js +262 -5
- package/src/memory/payloadCodec.js +2 -6
- package/src/memory/regionRegistry.d.ts +1 -3
- package/src/memory/regionRegistry.js +16 -14
- package/src/permission/protocol.js +5 -1
- package/src/runtime/dispatcher.d.ts +11 -8
- package/src/runtime/dispatcher.js +163 -21
- package/src/runtime/inline-executor.js +6 -1
- package/src/runtime/pool.d.ts +127 -4
- package/src/runtime/pool.js +262 -73
- package/src/runtime/process-worker.d.ts +24 -12
- package/src/runtime/process-worker.js +185 -115
- package/src/runtime/tx-queue.d.ts +17 -1
- package/src/runtime/tx-queue.js +120 -15
- package/src/runtime/worker-common.d.ts +13 -0
- package/src/runtime/worker-common.js +103 -0
- package/src/types.d.ts +75 -4
- package/src/worker/loop.js +34 -5
- package/src/worker/rx-queue.d.ts +11 -1
- package/src/worker/rx-queue.js +4 -1
- package/src/worker/timers.d.ts +8 -1
- package/src/worker/timers.js +20 -17
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Worker helpers shared by every worker mode. Kept out of `process-worker.ts`
|
|
2
|
+
// so the thread and web-worker paths do not pull the process-worker module —
|
|
3
|
+
// and, through it, the whole native connections tree — into their bundles.
|
|
4
|
+
import { ProcessSharedBuffer } from "../connections/process-shared-buffer.js";
|
|
5
|
+
const execFlagKey = (flag) => flag.split("=", 1)[0];
|
|
6
|
+
const NODE_PERMISSION_EXEC_FLAGS = new Set([
|
|
7
|
+
"--permission",
|
|
8
|
+
"--experimental-permission",
|
|
9
|
+
"--allow-fs-read",
|
|
10
|
+
"--allow-fs-write",
|
|
11
|
+
"--allow-worker",
|
|
12
|
+
"--allow-child-process",
|
|
13
|
+
"--allow-net",
|
|
14
|
+
"--allow-addons",
|
|
15
|
+
"--allow-ffi",
|
|
16
|
+
"--allow-wasi",
|
|
17
|
+
]);
|
|
18
|
+
const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
|
|
19
|
+
"--experimental-ffi",
|
|
20
|
+
"--experimental-transform-types",
|
|
21
|
+
"--expose-gc",
|
|
22
|
+
"--no-warnings",
|
|
23
|
+
...NODE_PERMISSION_EXEC_FLAGS,
|
|
24
|
+
]);
|
|
25
|
+
const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
|
|
26
|
+
const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
|
|
27
|
+
export const toWorkerSafeExecArgv = (flags) => {
|
|
28
|
+
if (!flags || flags.length === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
const filtered = flags.filter(isNodeWorkerSafeExecFlag);
|
|
31
|
+
if (filtered.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
const deduped = [];
|
|
35
|
+
for (const flag of filtered) {
|
|
36
|
+
if (seen.has(flag))
|
|
37
|
+
continue;
|
|
38
|
+
seen.add(flag);
|
|
39
|
+
deduped.push(flag);
|
|
40
|
+
}
|
|
41
|
+
return deduped;
|
|
42
|
+
};
|
|
43
|
+
export const toWorkerCompatExecArgv = (flags) => {
|
|
44
|
+
const safe = toWorkerSafeExecArgv(flags);
|
|
45
|
+
if (!safe || safe.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
|
|
48
|
+
return compat.length > 0 ? compat : undefined;
|
|
49
|
+
};
|
|
50
|
+
const isPlainRecord = (value) => {
|
|
51
|
+
if (value === null || typeof value !== "object")
|
|
52
|
+
return false;
|
|
53
|
+
const prototype = Object.getPrototypeOf(value);
|
|
54
|
+
return prototype === Object.prototype || prototype === null;
|
|
55
|
+
};
|
|
56
|
+
const serializeWorkerBootstrapValue = (value, seen = new WeakMap()) => {
|
|
57
|
+
if (value instanceof ProcessSharedBuffer)
|
|
58
|
+
return value.toMetadata();
|
|
59
|
+
if (value === null || typeof value !== "object")
|
|
60
|
+
return value;
|
|
61
|
+
const existing = seen.get(value);
|
|
62
|
+
if (existing !== undefined)
|
|
63
|
+
return existing;
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
const out = [];
|
|
66
|
+
seen.set(value, out);
|
|
67
|
+
for (const item of value) {
|
|
68
|
+
out.push(serializeWorkerBootstrapValue(item, seen));
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
if (!isPlainRecord(value))
|
|
73
|
+
return value;
|
|
74
|
+
const out = {};
|
|
75
|
+
seen.set(value, out);
|
|
76
|
+
for (const [key, item] of Object.entries(value)) {
|
|
77
|
+
out[key] = serializeWorkerBootstrapValue(item, seen);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
};
|
|
81
|
+
export const serializeWorkerBootstrapData = (options) => {
|
|
82
|
+
const bootstrap = options.bootstrap;
|
|
83
|
+
if (bootstrap === undefined || bootstrap.data === undefined)
|
|
84
|
+
return options;
|
|
85
|
+
return {
|
|
86
|
+
...options,
|
|
87
|
+
bootstrap: {
|
|
88
|
+
...bootstrap,
|
|
89
|
+
data: serializeWorkerBootstrapValue(bootstrap.data),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
export const terminateWorkerQuietly = (worker) => {
|
|
94
|
+
try {
|
|
95
|
+
// Runaway worker termination can be slow or stuck on some runtimes; once the
|
|
96
|
+
// pool is closing it must not keep the host process alive.
|
|
97
|
+
worker.unref?.();
|
|
98
|
+
return Promise.resolve(worker.terminate()).then(() => { }, () => { });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return Promise.resolve();
|
|
102
|
+
}
|
|
103
|
+
};
|
package/src/types.d.ts
CHANGED
|
@@ -34,6 +34,19 @@ type WorkerData = {
|
|
|
34
34
|
payloadConfig?: PayloadBufferOptions;
|
|
35
35
|
bufferReferenceReturn?: "copy" | "borrow";
|
|
36
36
|
permission?: ResolvedPermissionProtocol;
|
|
37
|
+
/** Whether this host can arm an async completion waiter on the return lock. */
|
|
38
|
+
notifyOnHostPublish?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Work stealing. When present, `lock` is a submit region shared by every
|
|
41
|
+
* worker and this worker claims from it as consumer `consumerId` of
|
|
42
|
+
* `consumers`, rather than owning a private request lane. `returnLock` stays
|
|
43
|
+
* private — the endpoint that claims a task owns its response.
|
|
44
|
+
*/
|
|
45
|
+
steal?: {
|
|
46
|
+
consumers: number;
|
|
47
|
+
consumerId: number;
|
|
48
|
+
regionLanes: number;
|
|
49
|
+
};
|
|
37
50
|
};
|
|
38
51
|
type UnsafeOptions = {
|
|
39
52
|
/**
|
|
@@ -330,24 +343,82 @@ type WorkerTimers = {
|
|
|
330
343
|
};
|
|
331
344
|
type DispatcherSettings = {
|
|
332
345
|
/**
|
|
333
|
-
* How many immediate notify loops before
|
|
346
|
+
* How many immediate notify loops before the dispatcher stops re-arming the
|
|
347
|
+
* pump for free.
|
|
348
|
+
*
|
|
349
|
+
* The default depends on what the dispatcher escalates *to*. Polling
|
|
350
|
+
* escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
|
|
351
|
+
* defaults to 128 — escalating is expensive and the wide window also batches
|
|
352
|
+
* completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
|
|
353
|
+
* price of one hop, so pools that have one default to 1. Pools without a
|
|
354
|
+
* doorbell — Deno, or `doorbell: false` — keep 128.
|
|
355
|
+
*
|
|
356
|
+
* Setting this explicitly opts out of that coupling for every pool shape.
|
|
334
357
|
*/
|
|
335
358
|
stallFreeLoops?: number;
|
|
336
359
|
/**
|
|
337
360
|
* Max backoff delay (milliseconds).
|
|
338
361
|
*/
|
|
339
362
|
maxBackoffMs?: number;
|
|
363
|
+
/**
|
|
364
|
+
* Replace idle completion polling with an `Atomics.waitAsync` doorbell when
|
|
365
|
+
* the host runtime supports it.
|
|
366
|
+
*
|
|
367
|
+
* Defaults to enabled on Node and Bun at any worker count, where it costs
|
|
368
|
+
* 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
|
|
369
|
+
* has real work of its own, that converts to +13% to +36% throughput.
|
|
370
|
+
*
|
|
371
|
+
* Turn it off for a pool that oversubscribes its machine. The doorbell only
|
|
372
|
+
* progresses when the host gets scheduled, so once workers occupy every core
|
|
373
|
+
* a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
|
|
374
|
+
* within the cores, -22% to -32% once they do not. That is not gated
|
|
375
|
+
* automatically because the core count cannot be probed portably.
|
|
376
|
+
*
|
|
377
|
+
* Forced off, overriding an explicit `true`, where a doorbell cannot work:
|
|
378
|
+
* on Deno, whose `waitAsync` does not wake an idle event loop, and for
|
|
379
|
+
* process workers, which live in another process and so cannot ring a host
|
|
380
|
+
* waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
|
|
381
|
+
* they wake through a native futex addon instead. Compiled (Porffor) workers
|
|
382
|
+
* never reach this path: they reject `host` outright and use pipes rather
|
|
383
|
+
* than shared memory.
|
|
384
|
+
*/
|
|
385
|
+
doorbell?: boolean;
|
|
340
386
|
/**
|
|
341
387
|
* Host dispatcher topology.
|
|
342
388
|
* - `"per-thread"`: each worker owns its dispatcher and macro channel.
|
|
343
389
|
* - `"serial-channel"`: each worker keeps its own dispatcher check state, but
|
|
344
390
|
* one shared channel runs all lane checks from first to last.
|
|
345
391
|
*
|
|
346
|
-
*
|
|
347
|
-
* `"serial-channel"` for multi-worker
|
|
348
|
-
*
|
|
392
|
+
* Used by private-lane pools. Its experimental default is `"per-thread"` on
|
|
393
|
+
* Bun or with one worker, otherwise `"serial-channel"` for multi-worker
|
|
394
|
+
* Node/Deno pools. Selecting it explicitly keeps private lanes unless
|
|
395
|
+
* `steal: true` is also explicit. Can also be forced with the
|
|
396
|
+
* `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
|
|
349
397
|
*/
|
|
350
398
|
dispatcher?: "per-thread" | "serial-channel";
|
|
399
|
+
/**
|
|
400
|
+
* Work stealing: one shared submit region that any worker may
|
|
401
|
+
* claim from, private return lanes, and a pool-global pending registry. The
|
|
402
|
+
* endpoint that claims a task owns its response.
|
|
403
|
+
*
|
|
404
|
+
* Enabled by default for compatible multi-worker thread and process pools
|
|
405
|
+
* unless a balancer or private-lane dispatcher was explicitly selected.
|
|
406
|
+
* One-worker pools, inliners, compiled/Porffor workers, and pools above the
|
|
407
|
+
* current 31-claimant protocol limit retain their existing transport. Set
|
|
408
|
+
* `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
|
|
409
|
+
* opts in and overrides a balancer/dispatcher selection.
|
|
410
|
+
*/
|
|
411
|
+
steal?: boolean;
|
|
412
|
+
/**
|
|
413
|
+
* Lanes claimed per stealing handshake (a power of two, `slots / g >=
|
|
414
|
+
* workers + 1`). Defaults to the widest region the lane budget allows, which
|
|
415
|
+
* amortises arbitration best for cheap tasks.
|
|
416
|
+
*
|
|
417
|
+
* **A region is a batch.** For expensive tasks, a wide region lets one worker
|
|
418
|
+
* claim work the others could have run in parallel; set this to `1` (or a
|
|
419
|
+
* small value) when per-task cost dominates arbitration cost.
|
|
420
|
+
*/
|
|
421
|
+
stealRegionLanes?: number;
|
|
351
422
|
};
|
|
352
423
|
type CreatePool = {
|
|
353
424
|
/** Number of workers. Default: 1. */
|
package/src/worker/loop.js
CHANGED
|
@@ -73,7 +73,7 @@ export const workerMainLoop = async (startupData) => {
|
|
|
73
73
|
installTerminationGuard();
|
|
74
74
|
installUnhandledRejectionSilencer();
|
|
75
75
|
installPerformanceNowGuard();
|
|
76
|
-
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, totalNumberOfThread, list, ids, names, at, } = startupData;
|
|
76
|
+
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, notifyOnHostPublish, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
|
|
77
77
|
scrubWorkerDataSensitiveBuffers(startupData);
|
|
78
78
|
assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
|
|
79
79
|
const debugNamespaces = resolveDebugNamespaces(debug);
|
|
@@ -105,6 +105,11 @@ export const workerMainLoop = async (startupData) => {
|
|
|
105
105
|
payloadConfig,
|
|
106
106
|
textCompat: lock.textCompat,
|
|
107
107
|
processBoundary: RUNTIME_IS_PROCESS_WORKER,
|
|
108
|
+
// Under stealing the request region is shared, so decode() becomes a
|
|
109
|
+
// region-Dekker claim against the other endpoints.
|
|
110
|
+
consumers: steal?.consumers,
|
|
111
|
+
consumerId: steal?.consumerId,
|
|
112
|
+
regionLanes: steal?.regionLanes,
|
|
108
113
|
});
|
|
109
114
|
const returnLockState = lock2({
|
|
110
115
|
headers: returnLock.headers,
|
|
@@ -115,6 +120,9 @@ export const workerMainLoop = async (startupData) => {
|
|
|
115
120
|
payloadConfig,
|
|
116
121
|
textCompat: returnLock.textCompat,
|
|
117
122
|
processBoundary: RUNTIME_IS_PROCESS_WORKER,
|
|
123
|
+
// The host parks on this lock's publication word when it has no work to
|
|
124
|
+
// flush. Request locks are host-produced and must not wake that waiter.
|
|
125
|
+
notifyOnHostPublish,
|
|
118
126
|
});
|
|
119
127
|
const timers = workerOptions?.timers;
|
|
120
128
|
const spinMicroseconds = timers?.spinMicroseconds ??
|
|
@@ -163,6 +171,7 @@ export const workerMainLoop = async (startupData) => {
|
|
|
163
171
|
returnLock: returnLockState,
|
|
164
172
|
borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
|
|
165
173
|
hasAborted: abortSignals?.hasAborted,
|
|
174
|
+
stealing: steal !== undefined,
|
|
166
175
|
});
|
|
167
176
|
installBufferReferenceReleaseListener(releaseReturnedBufferReference);
|
|
168
177
|
a_store(rxStatus, 0, 1);
|
|
@@ -175,6 +184,7 @@ export const workerMainLoop = async (startupData) => {
|
|
|
175
184
|
pauseInNanoseconds: timers?.pauseNanoseconds,
|
|
176
185
|
enqueueLock,
|
|
177
186
|
write: () => hasCompleted() ? writeBatch(WRITE_MAX) : 0,
|
|
187
|
+
flushBeforeClaim: steal !== undefined,
|
|
178
188
|
nativeWaitU32,
|
|
179
189
|
useSharedMemoryWait: !(RUNTIME_IS_PROCESS_WORKER &&
|
|
180
190
|
RUNTIME === "node" &&
|
|
@@ -250,15 +260,34 @@ export const workerMainLoop = async (startupData) => {
|
|
|
250
260
|
pauseUntil(value, spinMicroseconds, parkMs);
|
|
251
261
|
}
|
|
252
262
|
: pauseUntil;
|
|
263
|
+
const flushBeforeClaim = steal !== undefined;
|
|
253
264
|
const loop = () => {
|
|
254
265
|
isInMacro = false;
|
|
255
266
|
let progressed = true;
|
|
256
267
|
let awaiting = 0;
|
|
257
268
|
while (true) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
269
|
+
if (flushBeforeClaim) {
|
|
270
|
+
// Stealing only. Flush finished work before taking more on: a computed
|
|
271
|
+
// response should not wait behind a claim, and that claim can block on
|
|
272
|
+
// a peer withdrawing its intent. Measured to hurt the per-lane path,
|
|
273
|
+
// where a claim is a cheap private decode and picking work up promptly
|
|
274
|
+
// matters more, so the classic order is kept below.
|
|
275
|
+
// Reordering must not make `progressed` sticky: it answers "did this
|
|
276
|
+
// iteration move anything", so it has to start false every pass or the
|
|
277
|
+
// park below is unreachable and the worker spins a core forever.
|
|
278
|
+
progressed = false;
|
|
279
|
+
if (_hasCompleted()) {
|
|
280
|
+
if (_writeBatch(WRITE_MAX) > 0)
|
|
281
|
+
progressed = true;
|
|
282
|
+
}
|
|
283
|
+
progressed = _enqueueLock() || progressed;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
progressed = _enqueueLock();
|
|
287
|
+
if (_hasCompleted()) {
|
|
288
|
+
if (_writeBatch(WRITE_MAX) > 0)
|
|
289
|
+
progressed = true;
|
|
290
|
+
}
|
|
262
291
|
}
|
|
263
292
|
_drainReturnReleases();
|
|
264
293
|
if (_hasPending()) {
|
package/src/worker/rx-queue.d.ts
CHANGED
|
@@ -9,9 +9,19 @@ type ArgumentsForCreateWorkerQueue = {
|
|
|
9
9
|
borrowReturnedBufferReferences?: boolean;
|
|
10
10
|
hasAborted?: (signal: number) => boolean;
|
|
11
11
|
now?: () => number;
|
|
12
|
+
/**
|
|
13
|
+
* Work-stealing discipline: only claim when this worker has nothing left to
|
|
14
|
+
* run. Without it a worker keeps claiming whole regions on top of a backlog
|
|
15
|
+
* it has not started, which is exactly the hoarding stealing exists to
|
|
16
|
+
* prevent — the queued tasks would be better taken by an idle peer.
|
|
17
|
+
*
|
|
18
|
+
* Off for the classic per-lane path, where the host has already assigned
|
|
19
|
+
* these tasks to this lane and batching them is a win.
|
|
20
|
+
*/
|
|
21
|
+
stealing?: boolean;
|
|
12
22
|
};
|
|
13
23
|
export type CreateWorkerRxQueue = ReturnType<typeof createWorkerRxQueue>;
|
|
14
|
-
export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, }: ArgumentsForCreateWorkerQueue) => {
|
|
24
|
+
export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, stealing, }: ArgumentsForCreateWorkerQueue) => {
|
|
15
25
|
hasCompleted: () => boolean;
|
|
16
26
|
hasPending: () => boolean;
|
|
17
27
|
writeBatch: (max: number) => number;
|
package/src/worker/rx-queue.js
CHANGED
|
@@ -2,7 +2,7 @@ import RingQueue from "../ipc/tools/ring-queue.js";
|
|
|
2
2
|
import { attachPayloadTransportFinalizer, runTaskFinalizers, TaskFlag, TaskIndex, } from "../memory/lock.js";
|
|
3
3
|
import { composeWorkerRunner } from "./composable-runners.js";
|
|
4
4
|
import { BUFFER_REFERENCE_RETURN_RELEASE_TOKEN } from "../connections/buffer-reference.js";
|
|
5
|
-
export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, }) => {
|
|
5
|
+
export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, stealing, }) => {
|
|
6
6
|
const PLACE_HOLDER = (_) => {
|
|
7
7
|
throw ("UNREACHABLE FROM PLACE HOLDER (thread)");
|
|
8
8
|
};
|
|
@@ -71,6 +71,9 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
|
|
|
71
71
|
const { decode, resolved } = lock;
|
|
72
72
|
const resolvedShift = () => resolved.shiftNoClear();
|
|
73
73
|
const enqueueLock = () => {
|
|
74
|
+
// Steal only when idle: leaving work unclaimed lets a free peer take it.
|
|
75
|
+
if (stealing && toWork.size !== 0)
|
|
76
|
+
return false;
|
|
74
77
|
if (!decode())
|
|
75
78
|
return false;
|
|
76
79
|
let task = resolvedShift();
|
package/src/worker/timers.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ type PauseOptions = {
|
|
|
4
4
|
export type NativeWaitU32 = (buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, expected: number, timeoutMs?: number) => unknown;
|
|
5
5
|
export declare const whilePausing: ({ pauseInNanoseconds }: PauseOptions) => () => void;
|
|
6
6
|
export declare const pauseGeneric: () => void;
|
|
7
|
-
export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait, }: {
|
|
7
|
+
export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait, flushBeforeClaim, }: {
|
|
8
8
|
opView: Int32Array;
|
|
9
9
|
rxStatus: Int32Array;
|
|
10
10
|
txStatus: Int32Array;
|
|
@@ -14,5 +14,12 @@ export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxSta
|
|
|
14
14
|
write?: () => number | boolean;
|
|
15
15
|
nativeWaitU32?: NativeWaitU32;
|
|
16
16
|
useSharedMemoryWait?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Stealing only: write results back before attempting to claim more work.
|
|
19
|
+
* A claim can spin waiting for a peer to withdraw its intent, so claiming
|
|
20
|
+
* first puts arbitration latency in front of a finished response. Measured
|
|
21
|
+
* to hurt the per-lane path, so it stays off by default.
|
|
22
|
+
*/
|
|
23
|
+
flushBeforeClaim?: boolean;
|
|
17
24
|
}) => (value: number, spinMicroseconds: number, parkMs?: number) => void;
|
|
18
25
|
export {};
|
package/src/worker/timers.js
CHANGED
|
@@ -30,7 +30,9 @@ const a_load = Atomics.load;
|
|
|
30
30
|
const a_store = Atomics.store;
|
|
31
31
|
const a_wait = typeof Atomics.wait === "function" ? Atomics.wait : undefined;
|
|
32
32
|
const p_now = performance.now.bind(performance);
|
|
33
|
-
|
|
33
|
+
// `SharedArrayBuffer` is undeclared on pages that are not cross-origin
|
|
34
|
+
// isolated, and this module has to load anyway so the pool can report why.
|
|
35
|
+
const waitFallbackView = a_wait === undefined || typeof SharedArrayBuffer !== "function"
|
|
34
36
|
? undefined
|
|
35
37
|
: new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
36
38
|
const a_pause = "pause" in Atomics
|
|
@@ -59,26 +61,27 @@ export const whilePausing = ({ pauseInNanoseconds }) => {
|
|
|
59
61
|
return () => a_pause(forNanoseconds);
|
|
60
62
|
};
|
|
61
63
|
export const pauseGeneric = whilePausing({});
|
|
62
|
-
export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait = true, }) => {
|
|
64
|
+
export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait = true, flushBeforeClaim = false, }) => {
|
|
63
65
|
const pause = pauseInNanoseconds === undefined
|
|
64
66
|
? pauseGeneric
|
|
65
67
|
: whilePausing({ pauseInNanoseconds });
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (wrote > 0)
|
|
74
|
-
progressed = true;
|
|
75
|
-
}
|
|
76
|
-
else if (wrote === true) {
|
|
77
|
-
progressed = true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return progressed;
|
|
68
|
+
const flushWrite = () => {
|
|
69
|
+
if (!write)
|
|
70
|
+
return false;
|
|
71
|
+
const wrote = write();
|
|
72
|
+
if (typeof wrote === "number")
|
|
73
|
+
return wrote > 0;
|
|
74
|
+
return wrote === true;
|
|
81
75
|
};
|
|
76
|
+
const tryProgress = flushBeforeClaim
|
|
77
|
+
? () => {
|
|
78
|
+
const wrote = flushWrite();
|
|
79
|
+
return enqueueLock() || wrote;
|
|
80
|
+
}
|
|
81
|
+
: () => {
|
|
82
|
+
const claimed = enqueueLock();
|
|
83
|
+
return flushWrite() || claimed;
|
|
84
|
+
};
|
|
82
85
|
return (value, spinMicroseconds, parkMs) => {
|
|
83
86
|
const until = p_now() + (spinMicroseconds / 1000);
|
|
84
87
|
maybeGc();
|