Add real-time progress feedback to multi-run simulations
Switch from exec() to spawn() for streaming stderr, add onProgress callback to runner, and emit per-run progress lines from workers. CI now shows live percentage, tick count, and era during long runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { writeFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
import { resolve as pathResolve, dirname } from 'node:path';
|
import { resolve as pathResolve, dirname } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -74,20 +74,30 @@ interface WorkerResult {
|
|||||||
function spawnWorker(runId: number, seed: number): Promise<WorkerResult> {
|
function spawnWorker(runId: number, seed: number): Promise<WorkerResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const workerPath = new URL('./worker.ts', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
|
const workerPath = new URL('./worker.ts', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
|
||||||
const cmd = `npx tsx "${workerPath}" --strategy ${strategyName} --ticks ${totalTicks} --seed ${seed} --run-id ${runId}`;
|
const tsxBin = pathResolve(__dirname, '..', 'node_modules', '.bin', 'tsx');
|
||||||
exec(cmd, { maxBuffer: 200 * 1024 * 1024 }, (error, stdout, stderr) => {
|
const child = spawn(tsxBin, [workerPath, '--strategy', strategyName, '--ticks', String(totalTicks), '--seed', String(seed), '--run-id', String(runId)], {
|
||||||
if (stderr) process.stderr.write(stderr);
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
if (error) {
|
});
|
||||||
reject(new Error(`Run #${runId} (seed ${seed}) failed: ${error.message}`));
|
|
||||||
|
let stdout = '';
|
||||||
|
child.stdout.on('data', (chunk: Buffer) => { stdout += chunk; });
|
||||||
|
child.stderr.on('data', (chunk: Buffer) => { process.stderr.write(chunk); });
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`Run #${runId} (seed ${seed}) exited with code ${code}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(stdout) as WorkerResult;
|
resolve(JSON.parse(stdout) as WorkerResult);
|
||||||
resolve(result);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(new Error(`Run #${runId} (seed ${seed}) produced invalid JSON: ${(e as Error).message}`));
|
reject(new Error(`Run #${runId} (seed ${seed}) produced invalid JSON: ${(e as Error).message}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
reject(new Error(`Run #${runId} (seed ${seed}) failed to spawn: ${err.message}`));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface SimulationConfig {
|
|||||||
seed?: number;
|
seed?: number;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
|
onProgress?: (tick: number, totalTicks: number, era: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EraTransition {
|
export interface EraTransition {
|
||||||
@@ -139,9 +140,12 @@ export function runSimulation(config: SimulationConfig): SimulationResult {
|
|||||||
allMetrics.push(collectMetrics(state));
|
allMetrics.push(collectMetrics(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.silent && tick % progressInterval === 0) {
|
if (tick % progressInterval === 0) {
|
||||||
|
if (!config.silent) {
|
||||||
printProgress(tick, config.totalTicks, state, startTime);
|
printProgress(tick, config.totalTicks, state, startTime);
|
||||||
}
|
}
|
||||||
|
config.onProgress?.(tick, config.totalTicks, state.meta.currentEra);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.silent) {
|
if (!config.silent) {
|
||||||
|
|||||||
@@ -23,7 +23,18 @@ const strategy = strategyName === 'random' ? new RandomStrategy()
|
|||||||
|
|
||||||
process.stderr.write(`[Run #${runId}] Starting (seed ${seed}, ${totalTicks} ticks, ${strategyName})...\n`);
|
process.stderr.write(`[Run #${runId}] Starting (seed ${seed}, ${totalTicks} ticks, ${strategyName})...\n`);
|
||||||
|
|
||||||
const result = runSimulation({ totalTicks, decisionInterval, strategy, seed, verbose: false, silent: true });
|
const result = runSimulation({
|
||||||
|
totalTicks,
|
||||||
|
decisionInterval,
|
||||||
|
strategy,
|
||||||
|
seed,
|
||||||
|
verbose: false,
|
||||||
|
silent: true,
|
||||||
|
onProgress: (tick, total, era) => {
|
||||||
|
const pct = ((tick / total) * 100).toFixed(0);
|
||||||
|
process.stderr.write(`[Run #${runId}] ${pct}% (tick ${tick.toLocaleString()}/${total.toLocaleString()}) — ${era}\n`);
|
||||||
|
},
|
||||||
|
});
|
||||||
const report = generateJsonReport(result, { totalTicks, decisionInterval, strategy, seed });
|
const report = generateJsonReport(result, { totalTicks, decisionInterval, strategy, seed });
|
||||||
|
|
||||||
const output = {
|
const output = {
|
||||||
|
|||||||
Reference in New Issue
Block a user