@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.
- package/README.md +414 -0
- package/package.json +81 -0
- package/src/agents/index.mjs +1402 -0
- package/src/artifact.mjs +405 -0
- package/src/cli.mjs +932 -0
- package/src/config.mjs +115 -0
- package/src/guards.mjs +1213 -0
- package/src/index.mjs +347 -0
- package/src/merge.mjs +196 -0
- package/src/observation.mjs +193 -0
- package/src/orchestrator.mjs +315 -0
- package/src/probe.mjs +58 -0
- package/src/probes/CONCURRENCY-PROBE.md +256 -0
- package/src/probes/README.md +275 -0
- package/src/probes/concurrency.mjs +1175 -0
- package/src/probes/filesystem.mjs +731 -0
- package/src/probes/filesystem.test.mjs +244 -0
- package/src/probes/network.mjs +503 -0
- package/src/probes/performance.mjs +816 -0
- package/src/probes/persistence.mjs +785 -0
- package/src/probes/runtime.mjs +589 -0
- package/src/probes/tooling.mjs +454 -0
- package/src/probes/tooling.test.mjs +372 -0
- package/src/probes/verify-execution.mjs +131 -0
- package/src/probes/verify-guards.mjs +73 -0
- package/src/probes/wasm.mjs +715 -0
- package/src/receipt.mjs +197 -0
- package/src/receipts/index.mjs +813 -0
- package/src/reporter.example.mjs +223 -0
- package/src/reporter.mjs +555 -0
- package/src/reporters/markdown.mjs +355 -0
- package/src/reporters/rdf.mjs +383 -0
- package/src/storage/index.mjs +827 -0
- package/src/types.mjs +1028 -0
- package/src/utils/errors.mjs +397 -0
- package/src/utils/index.mjs +32 -0
- package/src/utils/logger.mjs +236 -0
- package/src/vocabulary.ttl +169 -0
|
@@ -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 };
|