@atolljs/core 0.1.6 → 0.1.7

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.
@@ -2,6 +2,7 @@ import { MemoryManager } from './memory';
2
2
  import { PoolQueueFullError, TaskAbortedError, TaskTimeoutError, WorkerCrashedError } from './errors';
3
3
  import { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
4
4
  import { MemoryPersistence, PoolTasks, TaskContract, TaskMap, TaskResult, WorkerPoolConfig } from '../contract/types';
5
+ import { devtoolsEnabled, emitDevtools, forwardDevtools, nextDevtoolsId, type EmittedDevtoolsEvent } from '../devtools';
5
6
  import { fmtBytes, scoped } from '../log';
6
7
 
7
8
  const poolLog = scoped('pool');
@@ -59,6 +60,8 @@ export interface PoolStats {
59
60
  interface TaskEntry {
60
61
  contract: TaskContract<any[], any>;
61
62
  args: any[];
63
+ /** Per-pool sequence — correlates a call's devtools events across its lifecycle. */
64
+ callId: number;
62
65
  enqueuedAt: number;
63
66
  resolve: (v: any) => void;
64
67
  reject: (e: unknown) => void;
@@ -100,6 +103,10 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
100
103
  waitMs: agg(),
101
104
  runMs: agg(),
102
105
  };
106
+ /** Devtools correlation id ('pool-N') — stamped on every event this pool emits. */
107
+ readonly poolId = nextDevtoolsId('pool');
108
+ private readonly label?: string;
109
+ private nextCallId = 0;
103
110
  public readonly sharedMemory: (SharedMemory<S> & SharedAccess<S>) | undefined;
104
111
  /** The attached memory-persistence adapter, if the config supplied one. */
105
112
  public readonly persistence: MemoryPersistence | undefined;
@@ -121,6 +128,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
121
128
  this.maxQueue = config.maxQueue ?? Infinity;
122
129
  this.respawn = config.respawn ?? true;
123
130
  this.taskTimeout = config.taskTimeout;
131
+ this.label = config.name;
124
132
 
125
133
  if (config.sharedMemory) {
126
134
  this.sharedMemory = config.sharedMemory;
@@ -142,6 +150,15 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
142
150
  );
143
151
  poolLog.debug(`tip: setLogLevel('trace') for per-task detail`);
144
152
 
153
+ emitDevtools({
154
+ type: 'pool:init',
155
+ poolId: this.poolId,
156
+ label: this.label,
157
+ poolSize,
158
+ concurrency: this.concurrency,
159
+ memoryBytes: this.sharedMemory?.totalBytes,
160
+ });
161
+
145
162
  for (let i = 0; i < poolSize; i++) {
146
163
  this.addWorker(this.spawn());
147
164
  }
@@ -167,18 +184,33 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
167
184
  private addWorker(worker: Worker): WorkerSlot {
168
185
  const slot: WorkerSlot = { worker, inFlight: 0 };
169
186
  worker.addEventListener('message', (event: MessageEvent) => {
170
- this.handleReply(slot, event.data as TaskResult & { messageId?: number });
187
+ const data = event.data as TaskResult & { messageId?: number; type?: string; event?: EmittedDevtoolsEvent };
188
+ // Worker-side instrumentation forwarded on the task channel — re-emit
189
+ // into the local sink so one sink sees the whole app. Stamp which
190
+ // slot produced it so worker-side fetches/heap/memory writes
191
+ // attribute to a specific worker.
192
+ if (data.type === 'ATOLL_DEVTOOLS' && data.event) {
193
+ forwardDevtools({
194
+ ...data.event,
195
+ worker: { poolId: this.poolId, slot: this.slots.indexOf(slot) },
196
+ });
197
+ return;
198
+ }
199
+ this.handleReply(slot, data);
171
200
  });
172
201
  worker.addEventListener('error', (event: unknown) => {
173
202
  this.handleWorkerError(slot, event);
174
203
  });
204
+ // `devtools` tells the worker to forward its instrumentation events back
205
+ // on this channel (ATOLL_DEVTOOLS) — only when a sink is live at spawn.
175
206
  if (this.memoryManager) {
176
- worker.postMessage({ type: 'INIT_MEMORY', memory: this.memoryManager.memory });
207
+ worker.postMessage({ type: 'INIT_MEMORY', memory: this.memoryManager.memory, devtools: devtoolsEnabled() });
177
208
  } else {
178
- worker.postMessage({ type: 'INIT' });
209
+ worker.postMessage({ type: 'INIT', devtools: devtoolsEnabled() });
179
210
  }
180
211
  this.slots.push(slot);
181
212
  poolLog.debug(`worker spawned, ${this.memoryManager ? 'INIT_MEMORY' : 'INIT'} sent`);
213
+ emitDevtools({ type: 'worker:spawn', poolId: this.poolId, slot: this.slots.indexOf(slot) });
182
214
  return slot;
183
215
  }
184
216
 
@@ -192,14 +224,17 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
192
224
  // stats and settlement — the caller was already rejected.
193
225
  if (!entry.done) {
194
226
  entry.done = true;
195
- this.statsAgg.runMs.add(performance.now() - (entry.sentAt ?? entry.enqueuedAt));
227
+ const runMs = performance.now() - (entry.sentAt ?? entry.enqueuedAt);
228
+ this.statsAgg.runMs.add(runMs);
196
229
  if (data.success) {
197
230
  this.statsAgg.completed++;
198
231
  taskLog.debug(`← ${entry.contract.taskId} ok`);
232
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'ok', runMs });
199
233
  entry.resolve(data.result);
200
234
  } else {
201
235
  this.statsAgg.failed++;
202
236
  taskLog.warn(`← ${entry.contract.taskId} failed: ${data.error}`);
237
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'error', runMs, error: data.error });
203
238
  entry.reject(new Error(data.error));
204
239
  }
205
240
  }
@@ -212,6 +247,8 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
212
247
  const e = event as { message?: string; error?: Error } | undefined;
213
248
  const message = e?.message ?? (e?.error ? String(e.error) : 'worker error');
214
249
  poolLog.warn(`worker error: ${message}`);
250
+ const idx = this.slots.indexOf(slot);
251
+ emitDevtools({ type: 'worker:error', poolId: this.poolId, slot: idx, message });
215
252
 
216
253
  // Reject the dead worker's in-flight tasks. Orphaned (already
217
254
  // aborted/timed-out) entries are dropped without re-counting.
@@ -222,23 +259,25 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
222
259
  if (entry.done) continue;
223
260
  entry.done = true;
224
261
  this.statsAgg.failed++;
262
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: message });
225
263
  entry.reject(new WorkerCrashedError(message));
226
264
  }
227
265
  }
228
266
 
229
- const idx = this.slots.indexOf(slot);
230
267
  if (idx >= 0) this.slots.splice(idx, 1);
231
268
  slot.worker.terminate();
232
269
 
233
270
  if (this.respawn) {
234
271
  poolLog.info('respawning crashed worker');
235
272
  this.addWorker(this.spawn());
273
+ emitDevtools({ type: 'worker:respawn', poolId: this.poolId, slot: this.slots.length - 1 });
236
274
  } else if (this.slots.length === 0) {
237
275
  // No workers left to ever serve the queue.
238
276
  for (const entry of this.queue.splice(0)) {
239
277
  entry.done = true;
240
278
  this.detach(entry);
241
279
  this.statsAgg.failed++;
280
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: message });
242
281
  entry.reject(new WorkerCrashedError(message));
243
282
  }
244
283
  }
@@ -270,10 +309,13 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
270
309
  entry.messageId = messageId;
271
310
  entry.slot = slot;
272
311
  entry.sentAt = performance.now();
273
- this.statsAgg.waitMs.add(performance.now() - entry.enqueuedAt);
312
+ const waitMs = entry.sentAt - entry.enqueuedAt;
313
+ this.statsAgg.waitMs.add(waitMs);
274
314
  slot.inFlight++;
275
315
  this.pending.set(messageId, entry);
276
- taskLog.debug(`→ ${entry.contract.taskId}`, { worker: this.slots.indexOf(slot), args: entry.args });
316
+ const slotIndex = this.slots.indexOf(slot);
317
+ taskLog.debug(`→ ${entry.contract.taskId}`, { worker: slotIndex, args: entry.args });
318
+ emitDevtools({ type: 'task:dispatch', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, slot: slotIndex, waitMs });
277
319
  slot.worker.postMessage({
278
320
  type: 'EXECUTE_TASK',
279
321
  messageId,
@@ -302,6 +344,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
302
344
  entry.done = true;
303
345
  this.detach(entry);
304
346
  this.statsAgg.aborted++;
347
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: err instanceof TaskTimeoutError ? 'timeout' : 'aborted' });
305
348
  entry.reject(err);
306
349
  this.drainQueue();
307
350
  this.checkDrained();
@@ -333,6 +376,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
333
376
  const entry: TaskEntry = {
334
377
  contract,
335
378
  args: args as any[],
379
+ callId: ++this.nextCallId,
336
380
  enqueuedAt: performance.now(),
337
381
  resolve,
338
382
  reject,
@@ -348,12 +392,14 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
348
392
  entry.timer = setTimeout(() => this.cancel(entry, new TaskTimeoutError()), timeout);
349
393
  }
350
394
 
395
+ emitDevtools({ type: 'task:enqueue', poolId: this.poolId, callId: entry.callId, taskId: contract.taskId });
351
396
  const slot = this.pickWorker();
352
397
  if (slot) {
353
398
  this.send(slot, entry);
354
399
  } else if (this.queue.length >= this.maxQueue) {
355
400
  entry.done = true;
356
401
  this.detach(entry);
402
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: contract.taskId, outcome: 'queue-full' });
357
403
  reject(new PoolQueueFullError()); // never dispatched — stats untouched
358
404
  return;
359
405
  } else {
@@ -425,12 +471,14 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
425
471
  poolLog.warn('memory persistence stop failed', e),
426
472
  );
427
473
  poolLog.info(`terminating ${this.slots.length} worker(s)`);
474
+ emitDevtools({ type: 'pool:terminate', poolId: this.poolId });
428
475
  const crashed = new WorkerCrashedError('pool terminated');
429
476
  for (const [, entry] of this.pending) {
430
477
  this.detach(entry);
431
478
  if (entry.done) continue; // orphaned — already counted as aborted
432
479
  entry.done = true;
433
480
  this.statsAgg.failed++;
481
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: 'pool terminated' });
434
482
  entry.reject(crashed);
435
483
  }
436
484
  this.pending.clear();
@@ -438,6 +486,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
438
486
  entry.done = true;
439
487
  this.detach(entry);
440
488
  this.statsAgg.failed++;
489
+ emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: 'pool terminated' });
441
490
  entry.reject(crashed);
442
491
  }
443
492
  for (const slot of this.slots) {
@@ -1,5 +1,6 @@
1
1
  import { TaskRegistry } from './registry';
2
2
  import { bindSharedMemories, getDefinedSharedMemoryCount } from '../contract/sharedMemory';
3
+ import { enableWorkerDevtoolsForwarding } from '../devtools';
3
4
  import { fmtBytes, scoped } from '../log';
4
5
 
5
6
  const workerLog = scoped('worker');
@@ -30,6 +31,7 @@ export function installWorkerListener(): void {
30
31
 
31
32
  if (data.type === 'INIT') {
32
33
  memoryInitialized = true;
34
+ if (data.devtools) enableWorkerDevtoolsForwarding();
33
35
  if (getDefinedSharedMemoryCount() > 0) {
34
36
  workerLog.warn(
35
37
  'pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound',
@@ -43,6 +45,7 @@ export function installWorkerListener(): void {
43
45
  const memory = data.memory as WebAssembly.Memory;
44
46
  bindSharedMemories(memory.buffer as unknown as SharedArrayBuffer);
45
47
  memoryInitialized = true;
48
+ if (data.devtools) enableWorkerDevtoolsForwarding();
46
49
  workerLog.info(`shared memory initialized (${fmtBytes(memory.buffer.byteLength)})`);
47
50
  return;
48
51
  }