@unrdf/kgc-probe 26.4.2

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.
@@ -0,0 +1,1175 @@
1
+ /**
2
+ * @fileoverview Concurrency Surface Probe - Parallel execution capabilities
3
+ * @module @unrdf/kgc-probe/probes/concurrency
4
+ *
5
+ * CRITICAL: This probe measures concurrency primitives with strict guard constraints.
6
+ * NO unbounded spawning. All workers cleaned up after probing.
7
+ * Each operation: timeout 5s max.
8
+ *
9
+ * Probes (15 total):
10
+ * 1. worker_threads availability and limits
11
+ * 2. SharedArrayBuffer availability
12
+ * 3. Atomics support
13
+ * 4. Thread pool size detection (UV_THREADPOOL_SIZE)
14
+ * 5. Event loop latency under load
15
+ * 6. Worker spawn time (mean, p95, stddev)
16
+ * 7. Message passing overhead (postMessage latency)
17
+ * 8. Maximum concurrent workers (test by spawning)
18
+ * 9. Parallel file I/O contention (measure with multiple reads)
19
+ * 10. Event loop ordering (nextTick vs queueMicrotask vs setImmediate vs promise)
20
+ * 11. Stack depth (recursion limit detection)
21
+ * 12. AsyncLocalStorage availability and functionality
22
+ * 13. Microtask queue depth handling
23
+ * 14. Max concurrent promises stress test (1000 pending)
24
+ * 15. Stream backpressure behavior
25
+ *
26
+ * @agent Agent 4 - Concurrency Surface Probe (KGC Probe Swarm)
27
+ */
28
+
29
+ import { Worker } from 'node:worker_threads';
30
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
31
+ import { join } from 'node:path';
32
+ import { tmpdir } from 'node:os';
33
+ import { z } from 'zod';
34
+
35
+ // ============================================================================
36
+ // SCHEMAS
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Observation schema - represents a single probe measurement
41
+ * @typedef {Object} Observation
42
+ * @property {string} method - Probe method identifier (e.g., "concurrency.worker_threads")
43
+ * @property {Record<string, any>} inputs - Input parameters used for probing
44
+ * @property {Record<string, any>} outputs - Observed outputs/measurements
45
+ * @property {number} timestamp - Unix epoch timestamp (ms)
46
+ * @property {string} [hash] - Hash of observation for verification
47
+ * @property {string} [guardDecision] - Guard decision: "allowed", "denied", "unknown"
48
+ * @property {Record<string, any>} [metadata] - Additional metadata
49
+ */
50
+ const ObservationSchema = z.object({
51
+ method: z.string().min(1),
52
+ inputs: z.record(z.string(), z.any()),
53
+ outputs: z.record(z.string(), z.any()),
54
+ timestamp: z.number().int().positive(),
55
+ hash: z.string().optional(),
56
+ guardDecision: z.enum(['allowed', 'denied', 'unknown']).optional(),
57
+ metadata: z.record(z.string(), z.any()).optional(),
58
+ });
59
+
60
+ /**
61
+ * Probe configuration schema
62
+ * @typedef {Object} ProbeConfig
63
+ * @property {number} [timeout] - Operation timeout in milliseconds (default: 5000, max: 5000)
64
+ * @property {number} [maxWorkers] - Maximum workers to spawn (default: 16, max: 16)
65
+ * @property {number} [samples] - Number of benchmark samples (default: 10, max: 100)
66
+ * @property {number} [budgetMs] - Global timeout budget in milliseconds (default: 30000)
67
+ * @property {string} [testDir] - Directory for I/O tests (default: tmpdir)
68
+ */
69
+ const ProbeConfigSchema = z.object({
70
+ timeout: z.number().int().positive().max(5000).default(5000),
71
+ maxWorkers: z.number().int().positive().max(16).default(16),
72
+ samples: z.number().int().positive().max(100).default(10),
73
+ budgetMs: z.number().int().positive().max(60000).default(30000),
74
+ testDir: z.string().optional(),
75
+ }).default({});
76
+
77
+ // ============================================================================
78
+ // GUARD: WORKER CLEANUP (POKA-YOKE)
79
+ // ============================================================================
80
+
81
+ /**
82
+ * CRITICAL: Track all spawned workers for cleanup
83
+ * Ensures no workers are left running after probe completes
84
+ */
85
+ const activeWorkers = new Set();
86
+
87
+ /**
88
+ * Cleanup all active workers
89
+ * MUST be called after probing completes or on error
90
+ */
91
+ async function cleanupWorkers() {
92
+ const workers = Array.from(activeWorkers);
93
+ activeWorkers.clear();
94
+
95
+ await Promise.all(
96
+ workers.map(worker =>
97
+ worker.terminate().catch(() => {
98
+ /* ignore termination errors */
99
+ })
100
+ )
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Register worker for cleanup tracking
106
+ * @param {Worker} worker - Worker to track
107
+ */
108
+ function registerWorker(worker) {
109
+ activeWorkers.add(worker);
110
+ worker.on('exit', () => activeWorkers.delete(worker));
111
+ }
112
+
113
+ // ============================================================================
114
+ // UTILITY: STATISTICS
115
+ // ============================================================================
116
+
117
+ /**
118
+ * Calculate statistics from array of measurements
119
+ * @param {number[]} values - Array of numeric measurements
120
+ * @returns {{mean: number, median: number, p95: number, min: number, max: number, stddev: number}}
121
+ */
122
+ function calculateStats(values) {
123
+ if (values.length === 0) {
124
+ return { mean: 0, median: 0, p95: 0, min: 0, max: 0, stddev: 0 };
125
+ }
126
+
127
+ const sorted = values.slice().sort((a, b) => a - b);
128
+ const sum = sorted.reduce((a, b) => a + b, 0);
129
+ const mean = sum / sorted.length;
130
+
131
+ const median = sorted[Math.floor(sorted.length / 2)];
132
+ const p95Index = Math.floor(sorted.length * 0.95);
133
+ const p95 = sorted[p95Index] || sorted[sorted.length - 1];
134
+
135
+ const min = sorted[0];
136
+ const max = sorted[sorted.length - 1];
137
+
138
+ // Calculate standard deviation
139
+ const variance = sorted.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / sorted.length;
140
+ const stddev = Math.sqrt(variance);
141
+
142
+ return { mean, median, p95, min, max, stddev };
143
+ }
144
+
145
+ // ============================================================================
146
+ // PROBE: WORKER THREADS AVAILABILITY
147
+ // ============================================================================
148
+
149
+ /**
150
+ * Probe worker_threads module availability
151
+ * @returns {Promise<Observation>}
152
+ */
153
+ async function probeWorkerThreadsAvailability() {
154
+ const timestamp = Date.now();
155
+ const outputs = {};
156
+ const metadata = {};
157
+
158
+ try {
159
+ // Check if Worker constructor exists
160
+ outputs.available = typeof Worker === 'function';
161
+ outputs.module = 'worker_threads';
162
+
163
+ // Check Node.js version
164
+ const nodeVersion = process.version;
165
+ outputs.nodeVersion = nodeVersion;
166
+ metadata.minVersion = 'v10.5.0'; // worker_threads introduced in Node 10.5.0
167
+
168
+ } catch (error) {
169
+ outputs.available = false;
170
+ metadata.error = error.message;
171
+ }
172
+
173
+ return {
174
+ method: 'concurrency.worker_threads_available',
175
+ inputs: {},
176
+ outputs,
177
+ timestamp,
178
+ guardDecision: 'allowed',
179
+ metadata,
180
+ };
181
+ }
182
+
183
+ // ============================================================================
184
+ // PROBE: SHAREDARRAYBUFFER AVAILABILITY
185
+ // ============================================================================
186
+
187
+ /**
188
+ * Probe SharedArrayBuffer availability
189
+ * @returns {Promise<Observation>}
190
+ */
191
+ async function probeSharedArrayBuffer() {
192
+ const timestamp = Date.now();
193
+ const outputs = {};
194
+ const metadata = {};
195
+
196
+ try {
197
+ // Check if SharedArrayBuffer constructor exists
198
+ outputs.available = typeof SharedArrayBuffer === 'function';
199
+
200
+ if (outputs.available) {
201
+ // Test creation of SharedArrayBuffer
202
+ const sab = new SharedArrayBuffer(8);
203
+ outputs.testSize = sab.byteLength;
204
+ outputs.functional = sab.byteLength === 8;
205
+ } else {
206
+ outputs.functional = false;
207
+ metadata.reason = 'Constructor not available';
208
+ }
209
+ } catch (error) {
210
+ outputs.available = false;
211
+ outputs.functional = false;
212
+ metadata.error = error.message;
213
+ }
214
+
215
+ return {
216
+ method: 'concurrency.shared_array_buffer',
217
+ inputs: {},
218
+ outputs,
219
+ timestamp,
220
+ guardDecision: 'allowed',
221
+ metadata,
222
+ };
223
+ }
224
+
225
+ // ============================================================================
226
+ // PROBE: ATOMICS SUPPORT
227
+ // ============================================================================
228
+
229
+ /**
230
+ * Probe Atomics support
231
+ * @returns {Promise<Observation>}
232
+ */
233
+ async function probeAtomics() {
234
+ const timestamp = Date.now();
235
+ const outputs = {};
236
+ const metadata = {};
237
+
238
+ try {
239
+ // Check if Atomics object exists
240
+ outputs.available = typeof Atomics === 'object';
241
+
242
+ if (outputs.available && typeof SharedArrayBuffer === 'function') {
243
+ // Test basic Atomics operations
244
+ const sab = new SharedArrayBuffer(4);
245
+ const view = new Int32Array(sab);
246
+
247
+ // Test add operation
248
+ Atomics.store(view, 0, 10);
249
+ const addResult = Atomics.add(view, 0, 5);
250
+ outputs.functional = addResult === 10 && Atomics.load(view, 0) === 15;
251
+
252
+ // List available operations
253
+ outputs.operations = [
254
+ 'add', 'and', 'compareExchange', 'exchange', 'load',
255
+ 'or', 'store', 'sub', 'xor', 'wait', 'notify'
256
+ ].filter(op => typeof Atomics[op] === 'function');
257
+
258
+ } else {
259
+ outputs.functional = false;
260
+ metadata.reason = outputs.available ? 'SharedArrayBuffer unavailable' : 'Atomics unavailable';
261
+ }
262
+ } catch (error) {
263
+ outputs.available = false;
264
+ outputs.functional = false;
265
+ metadata.error = error.message;
266
+ }
267
+
268
+ return {
269
+ method: 'concurrency.atomics',
270
+ inputs: {},
271
+ outputs,
272
+ timestamp,
273
+ guardDecision: 'allowed',
274
+ metadata,
275
+ };
276
+ }
277
+
278
+ // ============================================================================
279
+ // PROBE: EVENT LOOP LATENCY
280
+ // ============================================================================
281
+
282
+ /**
283
+ * Measure event loop latency using setImmediate chains
284
+ * @param {number} samples - Number of samples to measure
285
+ * @returns {Promise<Observation>}
286
+ */
287
+ async function probeEventLoopLatency(samples) {
288
+ const timestamp = Date.now();
289
+ const measurements = [];
290
+ const metadata = {};
291
+
292
+ try {
293
+ for (let i = 0; i < samples; i++) {
294
+ const start = performance.now();
295
+ await new Promise(resolve => setImmediate(resolve));
296
+ const end = performance.now();
297
+ measurements.push(end - start);
298
+ }
299
+
300
+ const stats = calculateStats(measurements);
301
+
302
+ return {
303
+ method: 'concurrency.event_loop_latency',
304
+ inputs: { samples },
305
+ outputs: {
306
+ ...stats,
307
+ unit: 'ms',
308
+ samples: measurements.length,
309
+ },
310
+ timestamp,
311
+ guardDecision: 'allowed',
312
+ metadata,
313
+ };
314
+ } catch (error) {
315
+ return {
316
+ method: 'concurrency.event_loop_latency',
317
+ inputs: { samples },
318
+ outputs: { error: true },
319
+ timestamp,
320
+ guardDecision: 'allowed',
321
+ metadata: { error: error.message },
322
+ };
323
+ }
324
+ }
325
+
326
+ // ============================================================================
327
+ // PROBE: WORKER SPAWN TIME
328
+ // ============================================================================
329
+
330
+ /**
331
+ * Measure worker spawn time
332
+ * Creates simple worker that exits immediately
333
+ * @param {number} samples - Number of workers to spawn
334
+ * @param {number} timeout - Timeout per worker (ms)
335
+ * @returns {Promise<Observation>}
336
+ */
337
+ async function probeWorkerSpawnTime(samples, timeout) {
338
+ const timestamp = Date.now();
339
+ const measurements = [];
340
+ const metadata = {};
341
+
342
+ // Create simple worker script inline
343
+ const workerCode = `
344
+ const { parentPort } = require('worker_threads');
345
+ parentPort.postMessage('ready');
346
+ `;
347
+
348
+ try {
349
+ for (let i = 0; i < samples; i++) {
350
+ const start = performance.now();
351
+
352
+ const worker = new Worker(workerCode, { eval: true });
353
+ registerWorker(worker);
354
+
355
+ // Wait for worker to send ready message
356
+ await new Promise((resolve, reject) => {
357
+ const timer = setTimeout(() => {
358
+ worker.terminate();
359
+ reject(new Error('Worker timeout'));
360
+ }, timeout);
361
+
362
+ worker.once('message', () => {
363
+ clearTimeout(timer);
364
+ resolve();
365
+ });
366
+
367
+ worker.once('error', err => {
368
+ clearTimeout(timer);
369
+ reject(err);
370
+ });
371
+ });
372
+
373
+ const end = performance.now();
374
+ measurements.push(end - start);
375
+
376
+ await worker.terminate();
377
+ activeWorkers.delete(worker);
378
+ }
379
+
380
+ const stats = calculateStats(measurements);
381
+
382
+ return {
383
+ method: 'concurrency.worker_spawn_time',
384
+ inputs: { samples, timeout },
385
+ outputs: {
386
+ ...stats,
387
+ unit: 'ms',
388
+ samples: measurements.length,
389
+ },
390
+ timestamp,
391
+ guardDecision: 'allowed',
392
+ metadata,
393
+ };
394
+ } catch (error) {
395
+ return {
396
+ method: 'concurrency.worker_spawn_time',
397
+ inputs: { samples, timeout },
398
+ outputs: { error: true },
399
+ timestamp,
400
+ guardDecision: 'allowed',
401
+ metadata: { error: error.message },
402
+ };
403
+ } finally {
404
+ await cleanupWorkers();
405
+ }
406
+ }
407
+
408
+ // ============================================================================
409
+ // PROBE: MESSAGE PASSING OVERHEAD
410
+ // ============================================================================
411
+
412
+ /**
413
+ * Measure message passing overhead (postMessage latency)
414
+ * @param {number} samples - Number of messages to send
415
+ * @param {number} timeout - Timeout for worker (ms)
416
+ * @returns {Promise<Observation>}
417
+ */
418
+ async function probeMessagePassingOverhead(samples, timeout) {
419
+ const timestamp = Date.now();
420
+ const measurements = [];
421
+ const metadata = {};
422
+
423
+ // Echo worker - receives message and sends it back
424
+ const workerCode = `
425
+ const { parentPort } = require('worker_threads');
426
+ parentPort.on('message', msg => {
427
+ parentPort.postMessage(msg);
428
+ });
429
+ `;
430
+
431
+ try {
432
+ const worker = new Worker(workerCode, { eval: true });
433
+ registerWorker(worker);
434
+
435
+ for (let i = 0; i < samples; i++) {
436
+ const start = performance.now();
437
+
438
+ await new Promise((resolve, reject) => {
439
+ const timer = setTimeout(() => reject(new Error('Message timeout')), timeout);
440
+
441
+ worker.once('message', () => {
442
+ clearTimeout(timer);
443
+ resolve();
444
+ });
445
+
446
+ worker.postMessage({ id: i, timestamp: start });
447
+ });
448
+
449
+ const end = performance.now();
450
+ measurements.push(end - start);
451
+ }
452
+
453
+ await worker.terminate();
454
+ activeWorkers.delete(worker);
455
+
456
+ const stats = calculateStats(measurements);
457
+
458
+ return {
459
+ method: 'concurrency.message_passing_overhead',
460
+ inputs: { samples, timeout },
461
+ outputs: {
462
+ ...stats,
463
+ unit: 'ms',
464
+ samples: measurements.length,
465
+ },
466
+ timestamp,
467
+ guardDecision: 'allowed',
468
+ metadata,
469
+ };
470
+ } catch (error) {
471
+ return {
472
+ method: 'concurrency.message_passing_overhead',
473
+ inputs: { samples, timeout },
474
+ outputs: { error: true },
475
+ timestamp,
476
+ guardDecision: 'allowed',
477
+ metadata: { error: error.message },
478
+ };
479
+ } finally {
480
+ await cleanupWorkers();
481
+ }
482
+ }
483
+
484
+ // ============================================================================
485
+ // PROBE: MAX CONCURRENT WORKERS
486
+ // ============================================================================
487
+
488
+ /**
489
+ * Test maximum concurrent workers by spawning workers until failure
490
+ * @param {number} maxWorkers - Maximum workers to attempt (hard limit)
491
+ * @param {number} timeout - Timeout per worker (ms)
492
+ * @returns {Promise<Observation>}
493
+ */
494
+ async function probeMaxConcurrentWorkers(maxWorkers, timeout) {
495
+ const timestamp = Date.now();
496
+ const metadata = {};
497
+ let successfulWorkers = 0;
498
+
499
+ // Worker that sleeps briefly to hold resources
500
+ const workerCode = `
501
+ const { parentPort } = require('worker_threads');
502
+ setTimeout(() => {
503
+ parentPort.postMessage('done');
504
+ }, 100);
505
+ `;
506
+
507
+ const workers = [];
508
+
509
+ try {
510
+ // Spawn workers up to maxWorkers limit
511
+ for (let i = 0; i < maxWorkers; i++) {
512
+ try {
513
+ const worker = new Worker(workerCode, { eval: true });
514
+ registerWorker(worker);
515
+ workers.push(worker);
516
+
517
+ // Wait for worker to signal ready
518
+ await Promise.race([
519
+ new Promise((resolve, reject) => {
520
+ worker.once('message', resolve);
521
+ worker.once('error', reject);
522
+ }),
523
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout)),
524
+ ]);
525
+
526
+ successfulWorkers++;
527
+ } catch (error) {
528
+ metadata.failureReason = error.message;
529
+ break;
530
+ }
531
+ }
532
+
533
+ return {
534
+ method: 'concurrency.max_concurrent_workers',
535
+ inputs: { maxWorkers, timeout },
536
+ outputs: {
537
+ maxAchieved: successfulWorkers,
538
+ limitReached: successfulWorkers < maxWorkers,
539
+ guardLimit: maxWorkers,
540
+ },
541
+ timestamp,
542
+ guardDecision: 'allowed',
543
+ metadata,
544
+ };
545
+ } catch (error) {
546
+ return {
547
+ method: 'concurrency.max_concurrent_workers',
548
+ inputs: { maxWorkers, timeout },
549
+ outputs: {
550
+ maxAchieved: successfulWorkers,
551
+ error: true,
552
+ },
553
+ timestamp,
554
+ guardDecision: 'allowed',
555
+ metadata: { error: error.message },
556
+ };
557
+ } finally {
558
+ // Cleanup all workers
559
+ await Promise.all(
560
+ workers.map(w =>
561
+ w.terminate().catch(() => {
562
+ /* ignore */
563
+ })
564
+ )
565
+ );
566
+ workers.forEach(w => activeWorkers.delete(w));
567
+ }
568
+ }
569
+
570
+ // ============================================================================
571
+ // PROBE: PARALLEL FILE I/O CONTENTION
572
+ // ============================================================================
573
+
574
+ /**
575
+ * Measure parallel file I/O contention
576
+ * Spawn N readers reading same file simultaneously
577
+ * @param {number} numReaders - Number of parallel readers
578
+ * @param {number} timeout - Timeout per operation (ms)
579
+ * @param {string} testDir - Directory for test files
580
+ * @returns {Promise<Observation>}
581
+ */
582
+ async function probeParallelIOContention(numReaders, timeout, testDir) {
583
+ const timestamp = Date.now();
584
+ const metadata = {};
585
+ const measurements = [];
586
+
587
+ try {
588
+ // Create test directory
589
+ const dir = testDir || join(tmpdir(), `kgc-probe-${Date.now()}`);
590
+ await mkdir(dir, { recursive: true });
591
+ metadata.testDir = dir;
592
+
593
+ // Create test file (1MB)
594
+ const testFile = join(dir, 'test-file.bin');
595
+ const testData = Buffer.alloc(1024 * 1024, 'x');
596
+ await writeFile(testFile, testData);
597
+ metadata.fileSize = testData.length;
598
+
599
+ // Spawn N parallel readers
600
+ const start = performance.now();
601
+
602
+ const readPromises = Array.from({ length: numReaders }, async (_, i) => {
603
+ const readStart = performance.now();
604
+ await readFile(testFile);
605
+ const readEnd = performance.now();
606
+ return readEnd - readStart;
607
+ });
608
+
609
+ const results = await Promise.race([
610
+ Promise.all(readPromises),
611
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout)),
612
+ ]);
613
+
614
+ const end = performance.now();
615
+ const totalTime = end - start;
616
+
617
+ measurements.push(...results);
618
+ const stats = calculateStats(measurements);
619
+
620
+ // Calculate throughput
621
+ const throughputMBps = (testData.length * numReaders) / (totalTime / 1000) / (1024 * 1024);
622
+
623
+ return {
624
+ method: 'concurrency.parallel_io_contention',
625
+ inputs: { numReaders, timeout, fileSize: testData.length },
626
+ outputs: {
627
+ totalTime,
628
+ throughputMBps: Math.round(throughputMBps * 100) / 100,
629
+ perReaderStats: stats,
630
+ unit: 'ms',
631
+ samples: measurements.length,
632
+ },
633
+ timestamp,
634
+ guardDecision: 'allowed',
635
+ metadata,
636
+ };
637
+ } catch (error) {
638
+ return {
639
+ method: 'concurrency.parallel_io_contention',
640
+ inputs: { numReaders, timeout },
641
+ outputs: { error: true },
642
+ timestamp,
643
+ guardDecision: 'allowed',
644
+ metadata: { error: error.message },
645
+ };
646
+ }
647
+ }
648
+
649
+ // ============================================================================
650
+ // PROBE: THREAD POOL SIZE DETECTION
651
+ // ============================================================================
652
+
653
+ /**
654
+ * Detect thread pool size using UV_THREADPOOL_SIZE
655
+ * @returns {Promise<Observation>}
656
+ */
657
+ async function probeThreadPoolSize() {
658
+ const timestamp = Date.now();
659
+ const outputs = {};
660
+ const metadata = {};
661
+
662
+ try {
663
+ // Check UV_THREADPOOL_SIZE environment variable
664
+ const envValue = process.env.UV_THREADPOOL_SIZE;
665
+ outputs.uvThreadpoolSize = envValue ? parseInt(envValue, 10) : null;
666
+ outputs.default = 4; // Default libuv thread pool size
667
+
668
+ // Effective size
669
+ outputs.effective = outputs.uvThreadpoolSize || outputs.default;
670
+
671
+ metadata.note = 'UV_THREADPOOL_SIZE can be set to customize libuv thread pool';
672
+ } catch (error) {
673
+ outputs.error = true;
674
+ metadata.error = error.message;
675
+ }
676
+
677
+ return {
678
+ method: 'concurrency.thread_pool_size',
679
+ inputs: {},
680
+ outputs,
681
+ timestamp,
682
+ guardDecision: 'allowed',
683
+ metadata,
684
+ };
685
+ }
686
+
687
+ // ============================================================================
688
+ // PROBE: EVENT LOOP ORDERING
689
+ // ============================================================================
690
+
691
+ /**
692
+ * Probe event loop ordering: nextTick, queueMicrotask, setImmediate
693
+ * Tests the execution order of different async scheduling primitives
694
+ * @returns {Promise<Observation>}
695
+ */
696
+ async function probeEventLoopOrdering() {
697
+ const timestamp = Date.now();
698
+ const outputs = {};
699
+ const metadata = {};
700
+
701
+ try {
702
+ const order = [];
703
+
704
+ // Schedule all three types
705
+ setImmediate(() => order.push('setImmediate'));
706
+ process.nextTick(() => order.push('nextTick'));
707
+ queueMicrotask(() => order.push('queueMicrotask'));
708
+ Promise.resolve().then(() => order.push('promise'));
709
+
710
+ // Wait for event loop tick
711
+ await new Promise(resolve => setTimeout(resolve, 10));
712
+
713
+ outputs.executionOrder = order;
714
+ outputs.expectedOrder = ['nextTick', 'queueMicrotask', 'promise', 'setImmediate'];
715
+ outputs.matchesExpected = JSON.stringify(order) === JSON.stringify(outputs.expectedOrder);
716
+
717
+ metadata.note = 'nextTick > microtasks > setImmediate in Node.js event loop';
718
+ } catch (error) {
719
+ outputs.error = true;
720
+ metadata.error = error.message;
721
+ }
722
+
723
+ return {
724
+ method: 'concurrency.event_loop_ordering',
725
+ inputs: {},
726
+ outputs,
727
+ timestamp,
728
+ guardDecision: 'allowed',
729
+ metadata,
730
+ };
731
+ }
732
+
733
+ // ============================================================================
734
+ // PROBE: STACK DEPTH (RECURSION LIMIT)
735
+ // ============================================================================
736
+
737
+ /**
738
+ * Probe maximum stack depth via recursion
739
+ * BOUNDED: Stops at reasonable limit to avoid crash
740
+ * @param {number} maxAttempts - Maximum recursion depth to try (default: 10000)
741
+ * @returns {Promise<Observation>}
742
+ */
743
+ async function probeStackDepth(maxAttempts = 10000) {
744
+ const timestamp = Date.now();
745
+ const outputs = {};
746
+ const metadata = {};
747
+
748
+ let depth = 0;
749
+
750
+ function recurse(n) {
751
+ depth = n;
752
+ if (n >= maxAttempts) {
753
+ return n; // Hit guard limit
754
+ }
755
+ try {
756
+ return recurse(n + 1);
757
+ } catch (error) {
758
+ // Stack overflow caught
759
+ metadata.errorType = error.name;
760
+ return n;
761
+ }
762
+ }
763
+
764
+ try {
765
+ const maxDepth = recurse(0);
766
+ outputs.maxStackDepth = maxDepth;
767
+ outputs.hitGuardLimit = maxDepth >= maxAttempts;
768
+ outputs.guardLimit = maxAttempts;
769
+
770
+ metadata.note = 'Recursion bounded to prevent VM crash';
771
+ } catch (error) {
772
+ outputs.maxStackDepth = depth;
773
+ outputs.error = true;
774
+ metadata.error = error.message;
775
+ }
776
+
777
+ return {
778
+ method: 'concurrency.stack_depth',
779
+ inputs: { maxAttempts },
780
+ outputs,
781
+ timestamp,
782
+ guardDecision: 'allowed',
783
+ metadata,
784
+ };
785
+ }
786
+
787
+ // ============================================================================
788
+ // PROBE: ASYNCLOCALSTORAGE AVAILABILITY
789
+ // ============================================================================
790
+
791
+ /**
792
+ * Probe AsyncLocalStorage availability and functionality
793
+ * @returns {Promise<Observation>}
794
+ */
795
+ async function probeAsyncLocalStorage() {
796
+ const timestamp = Date.now();
797
+ const outputs = {};
798
+ const metadata = {};
799
+
800
+ try {
801
+ // Dynamic import to avoid error if not available
802
+ const { AsyncLocalStorage } = await import('node:async_hooks');
803
+
804
+ outputs.available = typeof AsyncLocalStorage === 'function';
805
+
806
+ if (outputs.available) {
807
+ // Test functionality
808
+ const asyncLocalStorage = new AsyncLocalStorage();
809
+ let capturedValue = null;
810
+
811
+ await asyncLocalStorage.run('test-value', async () => {
812
+ capturedValue = asyncLocalStorage.getStore();
813
+ });
814
+
815
+ outputs.functional = capturedValue === 'test-value';
816
+ outputs.testValue = capturedValue;
817
+ } else {
818
+ outputs.functional = false;
819
+ }
820
+
821
+ metadata.nodeVersion = process.version;
822
+ metadata.note = 'AsyncLocalStorage available since Node.js v13.10.0';
823
+ } catch (error) {
824
+ outputs.available = false;
825
+ outputs.functional = false;
826
+ metadata.error = error.message;
827
+ }
828
+
829
+ return {
830
+ method: 'concurrency.async_local_storage',
831
+ inputs: {},
832
+ outputs,
833
+ timestamp,
834
+ guardDecision: 'allowed',
835
+ metadata,
836
+ };
837
+ }
838
+
839
+ // ============================================================================
840
+ // PROBE: MICROTASK QUEUE DEPTH
841
+ // ============================================================================
842
+
843
+ /**
844
+ * Probe microtask queue depth handling
845
+ * Tests how many microtasks can be queued before event loop stalls
846
+ * BOUNDED: Limited to prevent infinite loop
847
+ * @param {number} maxMicrotasks - Maximum microtasks to queue (default: 1000)
848
+ * @returns {Promise<Observation>}
849
+ */
850
+ async function probeMicrotaskQueueDepth(maxMicrotasks = 1000) {
851
+ const timestamp = Date.now();
852
+ const outputs = {};
853
+ const metadata = {};
854
+
855
+ try {
856
+ let executed = 0;
857
+ const promises = [];
858
+
859
+ // Queue N microtasks
860
+ for (let i = 0; i < maxMicrotasks; i++) {
861
+ promises.push(
862
+ queueMicrotask(() => {
863
+ executed++;
864
+ })
865
+ );
866
+ }
867
+
868
+ // Wait for all to execute
869
+ await new Promise(resolve => setImmediate(resolve));
870
+
871
+ outputs.queued = maxMicrotasks;
872
+ outputs.executed = executed;
873
+ outputs.allExecuted = executed === maxMicrotasks;
874
+
875
+ metadata.note = 'Microtasks execute before next event loop phase';
876
+ } catch (error) {
877
+ outputs.queued = maxMicrotasks;
878
+ outputs.executed = executed;
879
+ outputs.error = true;
880
+ metadata.error = error.message;
881
+ }
882
+
883
+ return {
884
+ method: 'concurrency.microtask_queue_depth',
885
+ inputs: { maxMicrotasks },
886
+ outputs,
887
+ timestamp,
888
+ guardDecision: 'allowed',
889
+ metadata,
890
+ };
891
+ }
892
+
893
+ // ============================================================================
894
+ // PROBE: MAX CONCURRENT PROMISES
895
+ // ============================================================================
896
+
897
+ /**
898
+ * Stress test with many concurrent pending promises
899
+ * BOUNDED: Limited to prevent memory exhaustion
900
+ * @param {number} maxPromises - Maximum concurrent promises (default: 1000, max: 10000)
901
+ * @param {number} timeout - Timeout for operation (ms)
902
+ * @returns {Promise<Observation>}
903
+ */
904
+ async function probeMaxConcurrentPromises(maxPromises = 1000, timeout = 5000) {
905
+ const timestamp = Date.now();
906
+ const outputs = {};
907
+ const metadata = {};
908
+
909
+ try {
910
+ const start = performance.now();
911
+ let resolved = 0;
912
+ let rejected = 0;
913
+
914
+ // Create N promises that resolve after random delay
915
+ const promises = Array.from({ length: maxPromises }, (_, i) =>
916
+ new Promise(resolve => {
917
+ setImmediate(() => {
918
+ resolved++;
919
+ resolve(i);
920
+ });
921
+ }).catch(() => {
922
+ rejected++;
923
+ })
924
+ );
925
+
926
+ // Wait for all with timeout
927
+ await Promise.race([
928
+ Promise.all(promises),
929
+ new Promise((_, reject) =>
930
+ setTimeout(() => reject(new Error('Timeout')), timeout)
931
+ ),
932
+ ]);
933
+
934
+ const end = performance.now();
935
+
936
+ outputs.created = maxPromises;
937
+ outputs.resolved = resolved;
938
+ outputs.rejected = rejected;
939
+ outputs.timeMs = Math.round(end - start);
940
+ outputs.success = resolved === maxPromises;
941
+
942
+ metadata.note = 'Tests VM ability to handle many concurrent promises';
943
+ } catch (error) {
944
+ outputs.created = maxPromises;
945
+ outputs.resolved = resolved;
946
+ outputs.rejected = rejected;
947
+ outputs.error = true;
948
+ metadata.error = error.message;
949
+ }
950
+
951
+ return {
952
+ method: 'concurrency.max_concurrent_promises',
953
+ inputs: { maxPromises, timeout },
954
+ outputs,
955
+ timestamp,
956
+ guardDecision: 'allowed',
957
+ metadata,
958
+ };
959
+ }
960
+
961
+ // ============================================================================
962
+ // PROBE: STREAM BACKPRESSURE BEHAVIOR
963
+ // ============================================================================
964
+
965
+ /**
966
+ * Probe stream backpressure behavior
967
+ * Tests how streams handle flow control when consumer is slower than producer
968
+ * @param {number} chunkCount - Number of chunks to write (default: 100)
969
+ * @returns {Promise<Observation>}
970
+ */
971
+ async function probeStreamBackpressure(chunkCount = 100) {
972
+ const timestamp = Date.now();
973
+ const outputs = {};
974
+ const metadata = {};
975
+
976
+ try {
977
+ const { Readable, Writable } = await import('node:stream');
978
+
979
+ let backpressureCount = 0;
980
+ let chunksWritten = 0;
981
+ let chunksRead = 0;
982
+
983
+ // Create readable stream
984
+ const readable = new Readable({
985
+ read() {
986
+ // Producer writes faster than consumer
987
+ for (let i = 0; i < chunkCount; i++) {
988
+ const canContinue = this.push(`chunk-${i}`);
989
+ if (!canContinue) {
990
+ backpressureCount++;
991
+ }
992
+ chunksWritten++;
993
+ }
994
+ this.push(null); // End stream
995
+ },
996
+ });
997
+
998
+ // Create writable stream (slow consumer)
999
+ const writable = new Writable({
1000
+ write(chunk, encoding, callback) {
1001
+ chunksRead++;
1002
+ // Simulate slow processing
1003
+ setImmediate(callback);
1004
+ },
1005
+ });
1006
+
1007
+ // Pipe and wait for finish
1008
+ await new Promise((resolve, reject) => {
1009
+ readable.pipe(writable);
1010
+ writable.on('finish', resolve);
1011
+ writable.on('error', reject);
1012
+ readable.on('error', reject);
1013
+ });
1014
+
1015
+ outputs.chunksWritten = chunksWritten;
1016
+ outputs.chunksRead = chunksRead;
1017
+ outputs.backpressureDetected = backpressureCount > 0;
1018
+ outputs.backpressureCount = backpressureCount;
1019
+ outputs.allChunksDelivered = chunksRead === chunkCount;
1020
+
1021
+ metadata.note = 'Backpressure prevents memory overflow in streams';
1022
+ } catch (error) {
1023
+ outputs.error = true;
1024
+ metadata.error = error.message;
1025
+ }
1026
+
1027
+ return {
1028
+ method: 'concurrency.stream_backpressure',
1029
+ inputs: { chunkCount },
1030
+ outputs,
1031
+ timestamp,
1032
+ guardDecision: 'allowed',
1033
+ metadata,
1034
+ };
1035
+ }
1036
+
1037
+ // ============================================================================
1038
+ // MAIN PROBE FUNCTION
1039
+ // ============================================================================
1040
+
1041
+ /**
1042
+ * Probes concurrency primitives and parallel execution capabilities
1043
+ *
1044
+ * Returns observations for:
1045
+ * - worker_threads availability and limits
1046
+ * - Maximum concurrent workers (test by spawning)
1047
+ * - Parallel file I/O contention (measure with multiple reads)
1048
+ * - Event loop latency under load
1049
+ * - SharedArrayBuffer availability
1050
+ * - Atomics support
1051
+ * - Message passing overhead (postMessage latency)
1052
+ * - Thread pool size detection
1053
+ *
1054
+ * GUARD CONSTRAINTS:
1055
+ * - No unbounded spawning (limit to config.maxWorkers or 16, whichever smaller)
1056
+ * - Clean up all workers after probing
1057
+ * - Timeout each worker operation (5s max)
1058
+ *
1059
+ * BENCHMARKING:
1060
+ * - Measure event loop latency with setImmediate chains
1061
+ * - Test parallel I/O: spawn N readers, measure throughput
1062
+ * - Sample worker spawn time (mean, p95)
1063
+ * - Respect --samples and --budget-ms
1064
+ *
1065
+ * @param {ProbeConfig} [config] - Probe configuration
1066
+ * @returns {Promise<Observation[]>} Array of observations
1067
+ *
1068
+ * @example
1069
+ * const observations = await probeConcurrency({
1070
+ * timeout: 5000,
1071
+ * maxWorkers: 8,
1072
+ * samples: 10
1073
+ * });
1074
+ * observations.forEach(obs => {
1075
+ * console.log(`${obs.method}: ${JSON.stringify(obs.outputs)}`);
1076
+ * });
1077
+ */
1078
+ export async function probeConcurrency(config = {}) {
1079
+ // Validate config
1080
+ const validatedConfig = ProbeConfigSchema.parse(config);
1081
+ const { timeout, maxWorkers, samples, testDir } = validatedConfig;
1082
+
1083
+ const observations = [];
1084
+
1085
+ try {
1086
+ // Run all probes (15 total: 9 original + 6 new)
1087
+ const [
1088
+ workerAvailObs,
1089
+ sabObs,
1090
+ atomicsObs,
1091
+ threadPoolObs,
1092
+ eventLoopObs,
1093
+ spawnTimeObs,
1094
+ messagingObs,
1095
+ maxWorkersObs,
1096
+ ioContentionObs,
1097
+ // New probes (6)
1098
+ eventLoopOrderObs,
1099
+ stackDepthObs,
1100
+ asyncLocalStorageObs,
1101
+ microtaskQueueObs,
1102
+ maxPromisesObs,
1103
+ streamBackpressureObs,
1104
+ ] = await Promise.all([
1105
+ probeWorkerThreadsAvailability(),
1106
+ probeSharedArrayBuffer(),
1107
+ probeAtomics(),
1108
+ probeThreadPoolSize(),
1109
+ probeEventLoopLatency(samples),
1110
+ probeWorkerSpawnTime(Math.min(samples, 5), timeout), // Limit spawn tests
1111
+ probeMessagePassingOverhead(samples, timeout),
1112
+ probeMaxConcurrentWorkers(maxWorkers, timeout),
1113
+ probeParallelIOContention(Math.min(maxWorkers, 4), timeout, testDir), // Limit I/O readers
1114
+ // New probes
1115
+ probeEventLoopOrdering(),
1116
+ probeStackDepth(10000), // Bounded recursion limit
1117
+ probeAsyncLocalStorage(),
1118
+ probeMicrotaskQueueDepth(1000), // Bounded microtask count
1119
+ probeMaxConcurrentPromises(1000, timeout), // Bounded promise count
1120
+ probeStreamBackpressure(100), // Bounded chunk count
1121
+ ]);
1122
+
1123
+ observations.push(
1124
+ workerAvailObs,
1125
+ sabObs,
1126
+ atomicsObs,
1127
+ threadPoolObs,
1128
+ eventLoopObs,
1129
+ spawnTimeObs,
1130
+ messagingObs,
1131
+ maxWorkersObs,
1132
+ ioContentionObs,
1133
+ eventLoopOrderObs,
1134
+ stackDepthObs,
1135
+ asyncLocalStorageObs,
1136
+ microtaskQueueObs,
1137
+ maxPromisesObs,
1138
+ streamBackpressureObs
1139
+ );
1140
+
1141
+ } catch (error) {
1142
+ // Catastrophic failure
1143
+ observations.push({
1144
+ method: 'concurrency.execution_error',
1145
+ inputs: {},
1146
+ outputs: {},
1147
+ timestamp: Date.now(),
1148
+ guardDecision: 'unknown',
1149
+ metadata: {
1150
+ reason: 'probe_execution_failed',
1151
+ error: error.message,
1152
+ },
1153
+ });
1154
+ } finally {
1155
+ // CRITICAL: Always cleanup workers
1156
+ await cleanupWorkers();
1157
+ }
1158
+
1159
+ // Validate all observations
1160
+ return observations.map(obs => ObservationSchema.parse(obs));
1161
+ }
1162
+
1163
+ // ============================================================================
1164
+ // EXPORTS
1165
+ // ============================================================================
1166
+
1167
+ /**
1168
+ * Re-export schemas for external validation
1169
+ */
1170
+ export { ObservationSchema, ProbeConfigSchema };
1171
+
1172
+ /**
1173
+ * Re-export cleanup for testing
1174
+ */
1175
+ export { cleanupWorkers };