@atolljs/core 0.1.6 → 0.1.8
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 +1 -1
- package/dist/contract/memoryDevtools.d.ts +30 -0
- package/dist/contract/sharedMemory.d.ts +6 -2
- package/dist/contract/types.d.ts +13 -0
- package/dist/devtools.d.ts +346 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +19 -16
- package/dist/index10.js +28 -184
- package/dist/index11.js +18 -48
- package/dist/index12.js +29 -63
- package/dist/index13.js +49 -24
- package/dist/index14.js +57 -59
- package/dist/index15.js +335 -45
- package/dist/index16.js +242 -47
- package/dist/index17.js +57 -38
- package/dist/index18.js +9 -86
- package/dist/index19.js +58 -48
- package/dist/index20.js +24 -56
- package/dist/index21.js +61 -0
- package/dist/index22.js +48 -0
- package/dist/index23.js +68 -0
- package/dist/index24.js +41 -0
- package/dist/index25.js +46 -0
- package/dist/index26.js +113 -0
- package/dist/index27.js +60 -0
- package/dist/index28.js +57 -0
- package/dist/index5.js +227 -35
- package/dist/index6.js +143 -471
- package/dist/index7.js +51 -20
- package/dist/index8.js +507 -27
- package/dist/index9.js +19 -18
- package/dist/pool/dedicatedWorker.d.ts +63 -0
- package/dist/pool/devtoolsCommands.d.ts +54 -0
- package/dist/pool/subWorkerClient.d.ts +50 -0
- package/dist/pool/workerChannel.d.ts +44 -0
- package/dist/pool/workerClient.d.ts +38 -8
- package/dist/pool/workerPool.d.ts +13 -0
- package/dist/reactive.d.ts +15 -0
- package/dist/reactiveGraph.d.ts +9 -0
- package/dist/userTiming.d.ts +27 -0
- package/package.json +2 -1
- package/src/contract/memoryDevtools.ts +249 -0
- package/src/contract/sharedMemory.ts +39 -9
- package/src/contract/types.ts +13 -0
- package/src/devtools.ts +735 -0
- package/src/index.ts +27 -1
- package/src/log.ts +22 -0
- package/src/observable.ts +16 -3
- package/src/pool/dedicatedWorker.ts +332 -0
- package/src/pool/devtoolsCommands.ts +165 -0
- package/src/pool/subWorkerClient.ts +70 -0
- package/src/pool/workerChannel.ts +99 -0
- package/src/pool/workerClient.ts +64 -15
- package/src/pool/workerPool.ts +115 -36
- package/src/reactive.ts +87 -12
- package/src/reactiveGraph.ts +91 -0
- package/src/testing/inProcessWorker.ts +4 -3
- package/src/userTiming.ts +71 -0
- package/src/worker/workerBootstrap.ts +29 -3
package/src/reactive.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createEffect, createMemo, createRenderEffect, createRoot, createSignal } from 'solid-js';
|
|
2
2
|
import { Connector } from './contract/sharedMemory';
|
|
3
3
|
import { scoped } from './log';
|
|
4
|
+
import { disposeNodes, graphNode, graphSource } from './reactiveGraph';
|
|
4
5
|
|
|
5
6
|
const reactiveLog = scoped('reactive');
|
|
6
7
|
|
|
@@ -45,11 +46,39 @@ export interface ReactiveConnector<T> {
|
|
|
45
46
|
* write — local or remote, both bump it. waitAsync when available, 50ms poll
|
|
46
47
|
* otherwise. No solid-js involved: this is the whole remote-write mechanism.
|
|
47
48
|
*/
|
|
48
|
-
|
|
49
|
+
interface VersionWatch {
|
|
50
|
+
stop: () => void;
|
|
51
|
+
/** Devtools graph id of the bridge node (null while devtools is off). */
|
|
52
|
+
node: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const watchVersionNode = <T>(connector: Connector<T>, onBump: () => void): VersionWatch => {
|
|
49
56
|
const version = connector._version;
|
|
50
57
|
if (!version) {
|
|
51
58
|
throw new Error('This connector does not support remote observation.');
|
|
52
59
|
}
|
|
60
|
+
const waits = typeof Atomics.waitAsync === 'function';
|
|
61
|
+
const node = graphNode('bridge', `${waits ? 'waitAsync' : 'poll'} ${connector._path ?? `@${connector.byteOffset}`}`, [
|
|
62
|
+
graphSource(connector._path),
|
|
63
|
+
]);
|
|
64
|
+
const stop = watchVersionRaw(connector, version, onBump);
|
|
65
|
+
return {
|
|
66
|
+
node,
|
|
67
|
+
stop: () => {
|
|
68
|
+
stop();
|
|
69
|
+
disposeNodes(node);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const watchVersion = <T>(connector: Connector<T>, onBump: () => void): (() => void) =>
|
|
75
|
+
watchVersionNode(connector, onBump).stop;
|
|
76
|
+
|
|
77
|
+
const watchVersionRaw = <T>(
|
|
78
|
+
connector: Connector<T>,
|
|
79
|
+
version: { view: Int32Array; index: number },
|
|
80
|
+
onBump: () => void,
|
|
81
|
+
): (() => void) => {
|
|
53
82
|
let stopped = false;
|
|
54
83
|
let last = Atomics.load(version.view, version.index);
|
|
55
84
|
|
|
@@ -167,6 +196,39 @@ export function watch<T, S = T>(
|
|
|
167
196
|
): () => void {
|
|
168
197
|
const select = onChange ? (selOrCb as (value: T) => S) : ((v: T) => v as unknown as S);
|
|
169
198
|
const cb = (onChange ?? selOrCb) as (slice: S, previous: S | undefined) => void;
|
|
199
|
+
return watchNodes(connector, select, cb, options, { sliced: onChange !== undefined, label: 'watch' }).stop;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Graph labelling for {@link watchNodes} — `observe()` names its effect. */
|
|
203
|
+
export interface WatchMeta {
|
|
204
|
+
/** A real selector was given (draws a derived node), not the identity. */
|
|
205
|
+
sliced: boolean;
|
|
206
|
+
/** Effect node label prefix: 'watch', 'observe'. */
|
|
207
|
+
label: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* `watch()` plus its devtools graph wiring: bridge → [derived slice] →
|
|
212
|
+
* effect. Internal: `observe()` uses it to own the effect node's label.
|
|
213
|
+
*/
|
|
214
|
+
export function watchNodes<T, S>(
|
|
215
|
+
connector: Connector<T>,
|
|
216
|
+
select: (value: T) => S,
|
|
217
|
+
cb: (slice: S, previous: S | undefined) => void,
|
|
218
|
+
options: SliceOptions<S> | undefined,
|
|
219
|
+
meta: WatchMeta,
|
|
220
|
+
): { stop: () => void; effect: string | null } {
|
|
221
|
+
const path = connector._path ?? `@${connector.byteOffset}`;
|
|
222
|
+
// Nodes are created around the version watch below; ids are null while
|
|
223
|
+
// devtools is off, and every graph call no-ops on null.
|
|
224
|
+
let bridge: string | null = null;
|
|
225
|
+
let derived: string | null = null;
|
|
226
|
+
let effect: string | null = null;
|
|
227
|
+
const graph = (): void => {
|
|
228
|
+
derived = meta.sliced ? graphNode('derived', `select(${path})`, [bridge]) : null;
|
|
229
|
+
effect = graphNode('effect', `${meta.label}(${path})`, [derived ?? bridge]);
|
|
230
|
+
};
|
|
231
|
+
const ungraph = (): void => disposeNodes(effect, derived);
|
|
170
232
|
|
|
171
233
|
// SSR solid build (node/worker conditions): effects never re-run, so the
|
|
172
234
|
// signal pipeline below would silently never emit. Same semantics, driven
|
|
@@ -184,20 +246,29 @@ export function watch<T, S = T>(
|
|
|
184
246
|
cb(s, old);
|
|
185
247
|
};
|
|
186
248
|
emit();
|
|
187
|
-
const
|
|
249
|
+
const remote = watchVersionNode(connector, emit);
|
|
250
|
+
bridge = remote.node;
|
|
251
|
+
graph();
|
|
188
252
|
reactiveLog.debug(`watching connector at offset ${connector.byteOffset} (direct)`);
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
stop()
|
|
253
|
+
return {
|
|
254
|
+
effect,
|
|
255
|
+
stop: () => {
|
|
256
|
+
reactiveLog.debug(`watch stopped (offset ${connector.byteOffset})`);
|
|
257
|
+
remote.stop();
|
|
258
|
+
ungraph();
|
|
259
|
+
},
|
|
192
260
|
};
|
|
193
261
|
}
|
|
194
262
|
|
|
195
|
-
|
|
196
|
-
const
|
|
263
|
+
// reactive(connector) + observeRemote(), inlined to keep the bridge id.
|
|
264
|
+
const [value, setValue] = createSignal<T>(connector.read(), { equals: false });
|
|
265
|
+
const remote = watchVersionNode(connector, () => setValue(() => connector.read()));
|
|
266
|
+
bridge = remote.node;
|
|
267
|
+
graph();
|
|
197
268
|
const dispose = createRoot((d) => {
|
|
198
269
|
const slice = createMemo(
|
|
199
270
|
() => {
|
|
200
|
-
const v =
|
|
271
|
+
const v = value() as T | null | undefined;
|
|
201
272
|
return v == null ? undefined : select(v);
|
|
202
273
|
},
|
|
203
274
|
undefined,
|
|
@@ -218,9 +289,13 @@ export function watch<T, S = T>(
|
|
|
218
289
|
return d;
|
|
219
290
|
});
|
|
220
291
|
reactiveLog.debug(`watching connector at offset ${connector.byteOffset}`);
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
292
|
+
return {
|
|
293
|
+
effect,
|
|
294
|
+
stop: () => {
|
|
295
|
+
reactiveLog.debug(`watch stopped (offset ${connector.byteOffset})`);
|
|
296
|
+
remote.stop();
|
|
297
|
+
dispose();
|
|
298
|
+
ungraph();
|
|
299
|
+
},
|
|
225
300
|
};
|
|
226
301
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reactive:node` emission for the dashboard's reactivity graph. The SDK's
|
|
3
|
+
* reactivity is shallow, and the node kinds map onto it directly:
|
|
4
|
+
*
|
|
5
|
+
* - **source**: a shared-memory field, id `mem:<path>`, owner 'shared'.
|
|
6
|
+
* The same id on every thread, so a worker-side writer and a main-thread
|
|
7
|
+
* watcher meet at one node. Two contracts with the same field path
|
|
8
|
+
* share the node (ids carry no contract identity across threads).
|
|
9
|
+
* - **bridge**: a version-counter watcher (`Atomics.waitAsync`, or the
|
|
10
|
+
* 50ms poll fallback): the only thing that crosses threads.
|
|
11
|
+
* - **derived**: a `watch`/`observe` selector slice.
|
|
12
|
+
* - **effect**: a `watch` callback, or an `observe` observable's
|
|
13
|
+
* subscriber fan-out (label carries the subscriber count).
|
|
14
|
+
*
|
|
15
|
+
* Non-source ids are per-thread counters ('b3', 'e7'); the dashboard
|
|
16
|
+
* qualifies them by the event's thread/worker stamp. Everything here is
|
|
17
|
+
* a no-op returning null while devtools is off.
|
|
18
|
+
*/
|
|
19
|
+
import { devtoolsEnabled, emitDevtools, registerDevtoolsCommand } from './devtools';
|
|
20
|
+
|
|
21
|
+
export type ReactiveKind = 'source' | 'derived' | 'effect' | 'bridge';
|
|
22
|
+
|
|
23
|
+
interface NodeInfo {
|
|
24
|
+
id: string;
|
|
25
|
+
kind: ReactiveKind;
|
|
26
|
+
label?: string;
|
|
27
|
+
deps?: string[];
|
|
28
|
+
owner?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const OWNER =
|
|
32
|
+
typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope ? 'worker' : 'main';
|
|
33
|
+
|
|
34
|
+
let seq = 0;
|
|
35
|
+
const live = new Map<string, NodeInfo>();
|
|
36
|
+
let commandRegistered = false;
|
|
37
|
+
|
|
38
|
+
const emit = (node: NodeInfo, disposed?: boolean): void => {
|
|
39
|
+
emitDevtools({ type: 'reactive:node', ...node, ...(disposed ? { disposed: true } : {}) });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ensureCommand = (): void => {
|
|
43
|
+
if (commandRegistered) return;
|
|
44
|
+
commandRegistered = true;
|
|
45
|
+
// Snapshot for dashboards that joined after the creation events scrolled
|
|
46
|
+
// out of the replay tail. This thread's nodes only (commands run on the
|
|
47
|
+
// main thread; worker nodes arrive through the event stream).
|
|
48
|
+
registerDevtoolsCommand('reactive.nodes', () => [...live.values()]);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The source node for a shared-memory field — emitted once per thread. */
|
|
52
|
+
export const graphSource = (path: string | undefined): string | null => {
|
|
53
|
+
if (!devtoolsEnabled() || path === undefined) return null;
|
|
54
|
+
ensureCommand();
|
|
55
|
+
const id = `mem:${path}`;
|
|
56
|
+
if (!live.has(id)) {
|
|
57
|
+
const node: NodeInfo = { id, kind: 'source', label: path, owner: 'shared' };
|
|
58
|
+
live.set(id, node);
|
|
59
|
+
emit(node);
|
|
60
|
+
}
|
|
61
|
+
return id;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Create a node; returns its id, or null while devtools is off. */
|
|
65
|
+
export const graphNode = (kind: ReactiveKind, label: string, deps: (string | null)[]): string | null => {
|
|
66
|
+
if (!devtoolsEnabled()) return null;
|
|
67
|
+
ensureCommand();
|
|
68
|
+
const id = `${kind[0]}${++seq}`;
|
|
69
|
+
const node: NodeInfo = { id, kind, label, deps: deps.filter((d): d is string => d !== null), owner: OWNER };
|
|
70
|
+
live.set(id, node);
|
|
71
|
+
emit(node);
|
|
72
|
+
return id;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Re-emit a node with a new label (same id — the dashboard replaces it). */
|
|
76
|
+
export const relabelNode = (id: string | null, label: string): void => {
|
|
77
|
+
const node = id === null ? undefined : live.get(id);
|
|
78
|
+
if (!node || node.label === label || !devtoolsEnabled()) return;
|
|
79
|
+
node.label = label;
|
|
80
|
+
emit(node);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Mark nodes disposed (null ids are skipped). */
|
|
84
|
+
export const disposeNodes = (...ids: (string | null)[]): void => {
|
|
85
|
+
for (const id of ids) {
|
|
86
|
+
const node = id === null ? undefined : live.get(id);
|
|
87
|
+
if (!node) continue;
|
|
88
|
+
live.delete(id!);
|
|
89
|
+
if (devtoolsEnabled()) emit(node, true);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
@@ -72,9 +72,10 @@ export class InProcessWorker {
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
if (data.type === 'INIT_MEMORY') {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const buffer = (data.memory as WebAssembly.Memory | undefined)?.buffer ?? data.buffer;
|
|
76
|
+
this.ready ??= this.boot().then(() => {
|
|
77
|
+
if (buffer) bindSharedMemories(buffer as SharedArrayBuffer);
|
|
78
|
+
});
|
|
78
79
|
try {
|
|
79
80
|
await this.ready;
|
|
80
81
|
} catch (err) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Timing bridge: `performance.measure` entries for atoll work, so the
|
|
3
|
+
* browser's own Performance panel shows tasks next to the app's frames.
|
|
4
|
+
* Callers gate on `devtoolsEnabled()`; this module never checks it itself.
|
|
5
|
+
*
|
|
6
|
+
* `detail.devtools` is Chrome's extensibility payload: entries land on a
|
|
7
|
+
* custom track inside an 'atoll' track group. Other engines ignore the
|
|
8
|
+
* detail and record a plain measure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Chrome's extensibility palette names. */
|
|
12
|
+
export type TimingColor = 'primary' | 'primary-light' | 'secondary' | 'tertiary' | 'error' | 'warning';
|
|
13
|
+
|
|
14
|
+
export interface TimingTrack {
|
|
15
|
+
/** Track label inside the 'atoll' group, e.g. a pool id. */
|
|
16
|
+
track: string;
|
|
17
|
+
color?: TimingColor;
|
|
18
|
+
properties?: [string, string | number][];
|
|
19
|
+
tooltipText?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type MeasureFn = (name: string, options: { start: number; end: number; detail?: unknown }) => unknown;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Record `[start, end]` (performance.now() ms on this thread) as `name`.
|
|
26
|
+
* The entry is cleared from the timeline buffer right away: the Performance
|
|
27
|
+
* panel captures it at measure time, and per-task measures would otherwise
|
|
28
|
+
* grow the buffer without bound (and trip Node's buffer-size warning).
|
|
29
|
+
*/
|
|
30
|
+
export const measureAtoll = (name: string, start: number, end: number, t: TimingTrack): void => {
|
|
31
|
+
const perf = globalThis.performance as
|
|
32
|
+
| (Performance & { measure?: MeasureFn; clearMeasures?: (name?: string) => void })
|
|
33
|
+
| undefined;
|
|
34
|
+
if (typeof perf?.measure !== 'function') return;
|
|
35
|
+
try {
|
|
36
|
+
perf.measure(name, {
|
|
37
|
+
start,
|
|
38
|
+
end: Math.max(end, start),
|
|
39
|
+
detail: {
|
|
40
|
+
devtools: {
|
|
41
|
+
dataType: 'track-entry',
|
|
42
|
+
track: t.track,
|
|
43
|
+
trackGroup: 'atoll',
|
|
44
|
+
color: t.color ?? 'primary',
|
|
45
|
+
properties: t.properties,
|
|
46
|
+
tooltipText: t.tooltipText,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
perf.clearMeasures?.(name);
|
|
51
|
+
} catch {
|
|
52
|
+
/* engines without the options-object measure signature */
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Main-thread span of one call, dispatch → settle, on the runner's track. */
|
|
57
|
+
export const measureTaskCall = (
|
|
58
|
+
runnerId: string,
|
|
59
|
+
taskId: string,
|
|
60
|
+
callId: number,
|
|
61
|
+
slot: number,
|
|
62
|
+
start: number,
|
|
63
|
+
end: number,
|
|
64
|
+
ok: boolean,
|
|
65
|
+
): void =>
|
|
66
|
+
measureAtoll(`atoll task ${taskId}`, start, end, {
|
|
67
|
+
track: runnerId,
|
|
68
|
+
color: ok ? 'primary' : 'error',
|
|
69
|
+
properties: [['runner', runnerId], ['slot', slot], ['call', callId], ['outcome', ok ? 'ok' : 'error']],
|
|
70
|
+
tooltipText: `${taskId} on ${runnerId}#${slot}: ${(end - start).toFixed(1)}ms`,
|
|
71
|
+
});
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { TaskRegistry } from './registry';
|
|
2
2
|
import { bindSharedMemories, getDefinedSharedMemoryCount } from '../contract/sharedMemory';
|
|
3
|
+
import { devtoolsEnabled, enableWorkerDevtoolsForwarding } from '../devtools';
|
|
3
4
|
import { fmtBytes, scoped } from '../log';
|
|
5
|
+
import { measureAtoll } from '../userTiming';
|
|
4
6
|
|
|
5
7
|
const workerLog = scoped('worker');
|
|
6
8
|
|
|
9
|
+
/** Worker-side span of one task execution, on this worker thread's 'tasks' track. */
|
|
10
|
+
const measureRun = (taskId: string, messageId: number, t0: number, ok: boolean): void => {
|
|
11
|
+
const t1 = performance.now();
|
|
12
|
+
measureAtoll(`atoll run ${taskId}`, t0, t1, {
|
|
13
|
+
track: 'tasks',
|
|
14
|
+
color: ok ? 'secondary' : 'error',
|
|
15
|
+
properties: [['message', messageId], ['outcome', ok ? 'ok' : 'error']],
|
|
16
|
+
tooltipText: `${taskId}: ${(t1 - t0).toFixed(1)}ms in worker`,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
7
20
|
let memoryInitialized = false;
|
|
8
21
|
let wired = false;
|
|
9
22
|
|
|
@@ -30,6 +43,7 @@ export function installWorkerListener(): void {
|
|
|
30
43
|
|
|
31
44
|
if (data.type === 'INIT') {
|
|
32
45
|
memoryInitialized = true;
|
|
46
|
+
if (data.devtools) enableWorkerDevtoolsForwarding();
|
|
33
47
|
if (getDefinedSharedMemoryCount() > 0) {
|
|
34
48
|
workerLog.warn(
|
|
35
49
|
'pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound',
|
|
@@ -40,10 +54,20 @@ export function installWorkerListener(): void {
|
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
if (data.type === 'INIT_MEMORY') {
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
// `memory` is the pool-allocated WebAssembly.Memory; `buffer` is an
|
|
58
|
+
// existing SharedArrayBuffer the caller ships directly (sub-worker
|
|
59
|
+
// shell, second pool sharing a buffer).
|
|
60
|
+
const buffer =
|
|
61
|
+
(data.memory as WebAssembly.Memory | undefined)?.buffer ??
|
|
62
|
+
(data.buffer as SharedArrayBuffer | undefined);
|
|
63
|
+
if (!buffer) {
|
|
64
|
+
workerLog.warn('INIT_MEMORY arrived without a buffer — contracts stay unbound');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
bindSharedMemories(buffer as unknown as SharedArrayBuffer);
|
|
45
68
|
memoryInitialized = true;
|
|
46
|
-
|
|
69
|
+
if (data.devtools) enableWorkerDevtoolsForwarding();
|
|
70
|
+
workerLog.info(`shared memory initialized (${fmtBytes(buffer.byteLength)})`);
|
|
47
71
|
return;
|
|
48
72
|
}
|
|
49
73
|
|
|
@@ -59,9 +83,11 @@ export function installWorkerListener(): void {
|
|
|
59
83
|
const t0 = performance.now();
|
|
60
84
|
try {
|
|
61
85
|
const result = await TaskRegistry.execute(taskId, ...(args || []));
|
|
86
|
+
if (devtoolsEnabled()) measureRun(taskId, messageId, t0, true);
|
|
62
87
|
workerLog.debug(`task ${taskId} → ${(performance.now() - t0).toFixed(1)}ms`);
|
|
63
88
|
self.postMessage({ messageId, success: true, result });
|
|
64
89
|
} catch (err: any) {
|
|
90
|
+
if (devtoolsEnabled()) measureRun(taskId, messageId, t0, false);
|
|
65
91
|
workerLog.warn(`task ${taskId} failed: ${err.message || String(err)}`);
|
|
66
92
|
self.postMessage({ messageId, success: false, error: err.message || String(err) });
|
|
67
93
|
}
|