Skip to content

Commit a5828a2

Browse files
rlagentflowbeta[bot]Runloop Agentclaude
authored
perf(cli): parallelize scenario log downloads with max concurrency of 50 (#176)
## Summary - Replace the serial download loop in `rli bmj logs` with a concurrent executor capped at 50 simultaneous downloads - Adds a small `runWithConcurrency` helper that distributes tasks across N workers using a shared index - Output lines are printed atomically per-download (one full line with ✓ on success) since the old "write prefix / complete line" pattern doesn't work in parallel ## Test plan - [ ] Run `rli bmj logs <job-id>` on a job with many scenarios and confirm downloads complete much faster - [ ] Verify the output directory structure matches the previous behaviour (`<output-dir>/<agent>/<scenario>/`) - [ ] Verify `results.json` is written correctly for each scenario - [ ] Confirm failed downloads still produce warning messages and the final summary counts correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Runloop Agent <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]>
1 parent 23f8a28 commit a5828a2

File tree

1 file changed

+36
-8
lines changed

1 file changed

+36
-8
lines changed

src/commands/benchmark-job/logs.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@ async function downloadScenarioLogs(
253253
}
254254
}
255255

256+
/** Run tasks in parallel with a maximum concurrency limit */
257+
async function runWithConcurrency<T>(
258+
tasks: (() => Promise<T>)[],
259+
maxConcurrency: number,
260+
): Promise<T[]> {
261+
const results: T[] = new Array(tasks.length);
262+
let nextIndex = 0;
263+
264+
async function worker() {
265+
while (nextIndex < tasks.length) {
266+
const i = nextIndex++;
267+
results[i] = await tasks[i]();
268+
}
269+
}
270+
271+
const workers = Array.from(
272+
{ length: Math.min(maxConcurrency, tasks.length) },
273+
() => worker(),
274+
);
275+
await Promise.all(workers);
276+
return results;
277+
}
278+
256279
export async function downloadBenchmarkJobLogs(
257280
jobId: string,
258281
options: LogsOptions = {},
@@ -326,18 +349,23 @@ export async function downloadBenchmarkJobLogs(
326349
`\nDownloading logs for ${targets.length} scenario run(s) to ${chalk.bold(outputDir)}\n`,
327350
);
328351

329-
// Download logs one at a time to avoid overwhelming the API
352+
// Download logs in parallel with a max concurrency of 50
353+
const MAX_CONCURRENCY = 50;
330354
let succeeded = 0;
331-
for (const target of targets) {
332-
process.stdout.write(
333-
` ${target.agentName} / ${target.scenarioName}... `,
334-
);
355+
356+
const tasks = targets.map((target) => async () => {
335357
const ok = await downloadScenarioLogs(target);
336358
if (ok) {
337-
console.log(chalk.green("done"));
338-
succeeded++;
359+
console.log(
360+
chalk.green(` ✓ ${target.agentName} / ${target.scenarioName}`),
361+
);
362+
return true;
339363
}
340-
}
364+
return false;
365+
});
366+
367+
const results = await runWithConcurrency(tasks, MAX_CONCURRENCY);
368+
succeeded = results.filter(Boolean).length;
341369

342370
console.log(
343371
`\n${chalk.green(`Downloaded logs for ${succeeded}/${targets.length} scenario run(s)`)} to ${chalk.bold(outputDir)}`,

0 commit comments

Comments
 (0)