@omni-oss/task-bench 0.0.0-beta.1
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 +328 -0
- package/dist/bench/index.d.ts +82 -0
- package/dist/bench/index.d.ts.map +1 -0
- package/dist/bench/install.d.ts +5 -0
- package/dist/bench/install.d.ts.map +1 -0
- package/dist/bench/report.d.ts +9 -0
- package/dist/bench/report.d.ts.map +1 -0
- package/dist/bench/stats.d.ts +12 -0
- package/dist/bench/stats.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/config.d.ts +91 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/generate/index.d.ts +19 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/templates.d.ts +20 -0
- package/dist/generate/templates.d.ts.map +1 -0
- package/dist/graph.d.ts +21 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/src-D3XyMXAu.mjs +1167 -0
- package/dist/suite/index.d.ts +49 -0
- package/dist/suite/index.d.ts.map +1 -0
- package/dist/suite/preset.d.ts +93 -0
- package/dist/suite/preset.d.ts.map +1 -0
- package/dist/suite/report.d.ts +7 -0
- package/dist/suite/report.d.ts.map +1 -0
- package/dist/task-bench-cli.mjs +107 -0
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/moon.d.ts +10 -0
- package/dist/tools/moon.d.ts.map +1 -0
- package/dist/tools/nx.d.ts +7 -0
- package/dist/tools/nx.d.ts.map +1 -0
- package/dist/tools/omni.d.ts +7 -0
- package/dist/tools/omni.d.ts.map +1 -0
- package/dist/tools/turbo.d.ts +5 -0
- package/dist/tools/turbo.d.ts.map +1 -0
- package/dist/tools/types.d.ts +77 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/package.json +41 -0
- package/project.omni.yaml +33 -0
- package/src/bench/index.ts +323 -0
- package/src/bench/install.ts +12 -0
- package/src/bench/report.ts +142 -0
- package/src/bench/stats.spec.ts +35 -0
- package/src/bench/stats.ts +38 -0
- package/src/cli/index.ts +410 -0
- package/src/config.ts +138 -0
- package/src/generate/index.ts +215 -0
- package/src/generate/templates.ts +87 -0
- package/src/graph.spec.ts +119 -0
- package/src/graph.ts +120 -0
- package/src/index.ts +31 -0
- package/src/suite/index.ts +113 -0
- package/src/suite/preset.spec.ts +95 -0
- package/src/suite/preset.ts +253 -0
- package/src/suite/report.ts +135 -0
- package/src/tools/adapters.spec.ts +95 -0
- package/src/tools/config.spec.ts +73 -0
- package/src/tools/index.ts +76 -0
- package/src/tools/moon.ts +106 -0
- package/src/tools/nx.ts +106 -0
- package/src/tools/omni.ts +96 -0
- package/src/tools/turbo.ts +78 -0
- package/src/tools/types.ts +116 -0
- package/tsconfig.json +4 -0
- package/tsconfig.project.json +6 -0
- package/tsconfig.types.json +4 -0
- package/vite.config.ts +29 -0
- package/vitest.config.unit.ts +13 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
Command,
|
|
7
|
+
Option,
|
|
8
|
+
type OptionValues,
|
|
9
|
+
} from "@commander-js/extra-typings";
|
|
10
|
+
import { description, name, version } from "../../package.json";
|
|
11
|
+
import {
|
|
12
|
+
type BenchEvent,
|
|
13
|
+
type BenchmarkResult,
|
|
14
|
+
DEPENDENCY_STRATEGIES,
|
|
15
|
+
formatMs,
|
|
16
|
+
formatReport,
|
|
17
|
+
formatSuiteMarkdown,
|
|
18
|
+
generateWorkspace,
|
|
19
|
+
getPreset,
|
|
20
|
+
type HarnessConfigInput,
|
|
21
|
+
installWorkspace,
|
|
22
|
+
listPresets,
|
|
23
|
+
parseSuite,
|
|
24
|
+
resolveConfig,
|
|
25
|
+
runBenchmark,
|
|
26
|
+
runSuite,
|
|
27
|
+
type SuiteEvent,
|
|
28
|
+
type SuiteResult,
|
|
29
|
+
TOOLS,
|
|
30
|
+
type Tool,
|
|
31
|
+
} from "..";
|
|
32
|
+
|
|
33
|
+
const program = new Command();
|
|
34
|
+
program.name(name).version(version).description(description);
|
|
35
|
+
|
|
36
|
+
const int = (raw: string): number => {
|
|
37
|
+
const n = Number.parseInt(raw, 10);
|
|
38
|
+
if (Number.isNaN(n)) throw new Error(`expected an integer, got "${raw}"`);
|
|
39
|
+
return n;
|
|
40
|
+
};
|
|
41
|
+
const float = (raw: string): number => {
|
|
42
|
+
const n = Number.parseFloat(raw);
|
|
43
|
+
if (Number.isNaN(n)) throw new Error(`expected a number, got "${raw}"`);
|
|
44
|
+
return n;
|
|
45
|
+
};
|
|
46
|
+
const toolList = (raw: string): Tool[] =>
|
|
47
|
+
raw.split(",").map((s) => {
|
|
48
|
+
const t = s.trim();
|
|
49
|
+
if (!(TOOLS as readonly string[]).includes(t)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`unknown tool "${t}" (allowed: ${TOOLS.join(", ")})`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return t as Tool;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
interface GenerateOpts {
|
|
58
|
+
projects?: number;
|
|
59
|
+
tasks?: number;
|
|
60
|
+
strategy?: string;
|
|
61
|
+
layers?: number;
|
|
62
|
+
fanout?: number;
|
|
63
|
+
edgeProbability?: number;
|
|
64
|
+
logLines?: number;
|
|
65
|
+
work?: number;
|
|
66
|
+
outputFiles?: number;
|
|
67
|
+
seed?: number;
|
|
68
|
+
tools?: Tool[];
|
|
69
|
+
chain?: boolean;
|
|
70
|
+
fanUpstream?: boolean;
|
|
71
|
+
turboVersion?: string;
|
|
72
|
+
nxVersion?: string;
|
|
73
|
+
moonVersion?: string;
|
|
74
|
+
bunVersion?: string;
|
|
75
|
+
config?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Merge a base config file (if any) with CLI overrides into a config input. */
|
|
79
|
+
function buildInput(opts: GenerateOpts): HarnessConfigInput {
|
|
80
|
+
const base: HarnessConfigInput = opts.config
|
|
81
|
+
? JSON.parse(readFileSync(opts.config, "utf8"))
|
|
82
|
+
: {};
|
|
83
|
+
|
|
84
|
+
const input: HarnessConfigInput = { ...base };
|
|
85
|
+
if (opts.seed !== undefined) input.seed = opts.seed;
|
|
86
|
+
if (opts.projects !== undefined) input.projects = opts.projects;
|
|
87
|
+
if (opts.tasks !== undefined) input.tasksPerProject = opts.tasks;
|
|
88
|
+
if (opts.tools !== undefined) input.tools = opts.tools;
|
|
89
|
+
|
|
90
|
+
const dependency: NonNullable<HarnessConfigInput["dependency"]> = {
|
|
91
|
+
...(base.dependency ?? {}),
|
|
92
|
+
};
|
|
93
|
+
if (opts.strategy !== undefined)
|
|
94
|
+
dependency.strategy = opts.strategy as NonNullable<
|
|
95
|
+
typeof dependency.strategy
|
|
96
|
+
>;
|
|
97
|
+
if (opts.layers !== undefined) dependency.layers = opts.layers;
|
|
98
|
+
if (opts.fanout !== undefined) dependency.fanout = opts.fanout;
|
|
99
|
+
if (opts.edgeProbability !== undefined)
|
|
100
|
+
dependency.edgeProbability = opts.edgeProbability;
|
|
101
|
+
if (Object.keys(dependency).length) input.dependency = dependency;
|
|
102
|
+
|
|
103
|
+
const task: NonNullable<HarnessConfigInput["task"]> = {
|
|
104
|
+
...(base.task ?? {}),
|
|
105
|
+
};
|
|
106
|
+
if (opts.logLines !== undefined) task.logLines = opts.logLines;
|
|
107
|
+
if (opts.work !== undefined) task.workIterations = opts.work;
|
|
108
|
+
if (opts.outputFiles !== undefined) task.outputFiles = opts.outputFiles;
|
|
109
|
+
if (opts.chain !== undefined) task.chainWithinProject = opts.chain;
|
|
110
|
+
if (opts.fanUpstream !== undefined) task.fanUpstream = opts.fanUpstream;
|
|
111
|
+
if (Object.keys(task).length) input.task = task;
|
|
112
|
+
|
|
113
|
+
const versions: NonNullable<HarnessConfigInput["versions"]> = {
|
|
114
|
+
...(base.versions ?? {}),
|
|
115
|
+
};
|
|
116
|
+
if (opts.turboVersion !== undefined) versions.turbo = opts.turboVersion;
|
|
117
|
+
if (opts.nxVersion !== undefined) versions.nx = opts.nxVersion;
|
|
118
|
+
if (opts.moonVersion !== undefined) versions.moon = opts.moonVersion;
|
|
119
|
+
if (opts.bunVersion !== undefined) versions.bun = opts.bunVersion;
|
|
120
|
+
if (Object.keys(versions).length) input.versions = versions;
|
|
121
|
+
|
|
122
|
+
return input;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function addGenerateOptions<
|
|
126
|
+
Args extends unknown[],
|
|
127
|
+
Opts extends OptionValues,
|
|
128
|
+
Global extends OptionValues,
|
|
129
|
+
>(cmd: Command<Args, Opts, Global>) {
|
|
130
|
+
return cmd
|
|
131
|
+
.option("--config <file>", "Base config JSON file to extend.")
|
|
132
|
+
.option("--seed <n>", "Deterministic graph seed.", int)
|
|
133
|
+
.option("--projects <n>", "Number of projects.", int)
|
|
134
|
+
.option("--tasks <n>", "Tasks per project.", int)
|
|
135
|
+
.addOption(
|
|
136
|
+
new Option(
|
|
137
|
+
"--strategy <name>",
|
|
138
|
+
"Dependency graph strategy.",
|
|
139
|
+
).choices(DEPENDENCY_STRATEGIES),
|
|
140
|
+
)
|
|
141
|
+
.option("--layers <n>", "Layers for the `layered` strategy.", int)
|
|
142
|
+
.option("--fanout <n>", "Max upstream deps per project.", int)
|
|
143
|
+
.option(
|
|
144
|
+
"--edge-probability <p>",
|
|
145
|
+
"Edge probability for `random`.",
|
|
146
|
+
float,
|
|
147
|
+
)
|
|
148
|
+
.option("--log-lines <n>", "Log lines printed per task.", int)
|
|
149
|
+
.option("--work <n>", "CPU work iterations per task.", int)
|
|
150
|
+
.option("--output-files <n>", "Output files per task.", int)
|
|
151
|
+
.option(
|
|
152
|
+
"--tools <list>",
|
|
153
|
+
"Comma-separated tools (omni,turbo,nx,moon).",
|
|
154
|
+
toolList,
|
|
155
|
+
)
|
|
156
|
+
.option("--turbo-version <semver>", "Turbo version to install.")
|
|
157
|
+
.option("--nx-version <semver>", "Nx version to install.")
|
|
158
|
+
.option(
|
|
159
|
+
"--moon-version <semver>",
|
|
160
|
+
"moon (@moonrepo/cli) version to install.",
|
|
161
|
+
)
|
|
162
|
+
.option("--bun-version <semver>", "bun version for packageManager.")
|
|
163
|
+
.option("--no-chain", "Disable intra-project task chaining.")
|
|
164
|
+
.option("--no-fan-upstream", "Disable upstream (^) task dependencies.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function progressHandler(): (event: BenchEvent) => void {
|
|
168
|
+
return (event) => {
|
|
169
|
+
if (event.kind === "tool-start") {
|
|
170
|
+
process.stderr.write(`\n▶ ${event.tool}\n`);
|
|
171
|
+
} else if (event.kind === "tool-error") {
|
|
172
|
+
process.stderr.write(` ✖ ${event.tool}: ${event.error}\n`);
|
|
173
|
+
} else if (event.kind === "tool-unsuccessful") {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
` ✖ ${event.tool}: exit ${event.sample.exitCode}\n`,
|
|
176
|
+
);
|
|
177
|
+
if (event.sample.stdout) {
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
` === stdout ===\n${event.sample.stdout}\n`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (event.sample.stderr) {
|
|
183
|
+
process.stderr.write(
|
|
184
|
+
` === stderr ===\n${event.sample.stderr}\n`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const status = event.sample.ok
|
|
189
|
+
? "ok"
|
|
190
|
+
: `exit ${event.sample.exitCode}`;
|
|
191
|
+
process.stderr.write(
|
|
192
|
+
` ${event.scenario} ${event.run}/${event.total}: ` +
|
|
193
|
+
`${formatMs(event.sample.durationMs)} ` +
|
|
194
|
+
`(ran ${event.sample.executed} tasks, ${status})\n`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function writeJson(file: string | undefined, result: BenchmarkResult): void {
|
|
201
|
+
if (!file) return;
|
|
202
|
+
writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`);
|
|
203
|
+
process.stderr.write(`\nWrote results to ${file}\n`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- generate ---------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
addGenerateOptions(
|
|
209
|
+
program
|
|
210
|
+
.command("generate")
|
|
211
|
+
.alias("gen")
|
|
212
|
+
.description("Generate a benchmark workspace at the given root dir.")
|
|
213
|
+
.requiredOption(
|
|
214
|
+
"-o, --out <dir>",
|
|
215
|
+
"Root dir to generate the workspace into.",
|
|
216
|
+
),
|
|
217
|
+
).action(async (opts) => {
|
|
218
|
+
const out = resolve(opts.out);
|
|
219
|
+
const input = buildInput(opts);
|
|
220
|
+
const result = await generateWorkspace(out, input);
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`Generated ${result.projects.length} projects ` +
|
|
223
|
+
`(${result.files.length} files) at ${out}\n` +
|
|
224
|
+
`Tools: ${result.config.tools.join(", ")}\n`,
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// --- run ---------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
program
|
|
231
|
+
.command("run")
|
|
232
|
+
.description("Benchmark an already-generated workspace.")
|
|
233
|
+
.requiredOption("-d, --dir <dir>", "Root dir of a generated workspace.")
|
|
234
|
+
.option("--tools <list>", "Comma-separated tools to benchmark.", toolList)
|
|
235
|
+
.option("--task <name>", "Task to run (defaults to the last task).")
|
|
236
|
+
.option("--cold-runs <n>", "Cold (uncached) runs per tool.", int, 3)
|
|
237
|
+
.option("--warm-runs <n>", "Warm (cached) runs per tool.", int, 5)
|
|
238
|
+
.option(
|
|
239
|
+
"--concurrency <n>",
|
|
240
|
+
"Max parallel tasks, applied identically to all tools (default: CPU count).",
|
|
241
|
+
int,
|
|
242
|
+
)
|
|
243
|
+
.option("--no-daemon", "Disable each tool's persistent daemon (turbo, nx).")
|
|
244
|
+
.option("--json <file>", "Write full results as JSON to this file.")
|
|
245
|
+
.action(async (opts) => {
|
|
246
|
+
const dir = resolve(opts.dir);
|
|
247
|
+
const result = await runBenchmark(dir, {
|
|
248
|
+
tools: opts.tools,
|
|
249
|
+
task: opts.task,
|
|
250
|
+
concurrency: opts.concurrency,
|
|
251
|
+
daemon: opts.daemon,
|
|
252
|
+
coldRuns: opts.coldRuns,
|
|
253
|
+
warmRuns: opts.warmRuns,
|
|
254
|
+
onEvent: progressHandler(),
|
|
255
|
+
});
|
|
256
|
+
process.stdout.write(formatReport(result));
|
|
257
|
+
writeJson(opts.json, result);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// --- bench (generate + install + run) ---------------------------------------
|
|
261
|
+
|
|
262
|
+
addGenerateOptions(
|
|
263
|
+
program
|
|
264
|
+
.command("bench")
|
|
265
|
+
.description("Generate, install, and benchmark in one step.")
|
|
266
|
+
.requiredOption(
|
|
267
|
+
"-o, --out <dir>",
|
|
268
|
+
"Root dir to generate the workspace into.",
|
|
269
|
+
)
|
|
270
|
+
.option(
|
|
271
|
+
"--no-install",
|
|
272
|
+
"Skip `bun install` in the generated workspace.",
|
|
273
|
+
)
|
|
274
|
+
.option("--task <name>", "Task to run (defaults to the last task).")
|
|
275
|
+
.option("--cold-runs <n>", "Cold (uncached) runs per tool.", int, 3)
|
|
276
|
+
.option("--warm-runs <n>", "Warm (cached) runs per tool.", int, 5)
|
|
277
|
+
.option(
|
|
278
|
+
"--concurrency <n>",
|
|
279
|
+
"Max parallel tasks, applied identically to all tools (default: CPU count).",
|
|
280
|
+
int,
|
|
281
|
+
)
|
|
282
|
+
.option(
|
|
283
|
+
"--no-daemon",
|
|
284
|
+
"Disable each tool's persistent daemon (turbo, nx).",
|
|
285
|
+
)
|
|
286
|
+
.option("--json <file>", "Write full results as JSON to this file."),
|
|
287
|
+
).action(async (opts) => {
|
|
288
|
+
const out = resolve(opts.out);
|
|
289
|
+
const input = buildInput(opts);
|
|
290
|
+
const generated = await generateWorkspace(out, input);
|
|
291
|
+
process.stderr.write(
|
|
292
|
+
`Generated ${generated.projects.length} projects at ${out}\n`,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (opts.install !== false) {
|
|
296
|
+
process.stderr.write("Installing dependencies (bun install)...\n");
|
|
297
|
+
await installWorkspace(out);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const result = await runBenchmark(out, {
|
|
301
|
+
task: opts.task,
|
|
302
|
+
coldRuns: opts.coldRuns,
|
|
303
|
+
warmRuns: opts.warmRuns,
|
|
304
|
+
concurrency: opts.concurrency,
|
|
305
|
+
daemon: opts.daemon,
|
|
306
|
+
onEvent: progressHandler(),
|
|
307
|
+
});
|
|
308
|
+
process.stdout.write(formatReport(result));
|
|
309
|
+
writeJson(opts.json, result);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// --- inspect (no side effects) ----------------------------------------------
|
|
313
|
+
|
|
314
|
+
addGenerateOptions(
|
|
315
|
+
program
|
|
316
|
+
.command("inspect")
|
|
317
|
+
.description("Resolve a config and print the derived graph summary."),
|
|
318
|
+
).action((opts) => {
|
|
319
|
+
const config = resolveConfig(buildInput(opts));
|
|
320
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// --- suite (run a preset of scenarios) --------------------------------------
|
|
324
|
+
|
|
325
|
+
function suiteProgressHandler(): (event: SuiteEvent) => void {
|
|
326
|
+
const bench = progressHandler();
|
|
327
|
+
return (event) => {
|
|
328
|
+
if (event.kind === "scenario-start") {
|
|
329
|
+
process.stderr.write(
|
|
330
|
+
`\n=== [${event.index + 1}/${event.total}] ${event.name} ===\n`,
|
|
331
|
+
);
|
|
332
|
+
} else if (event.kind === "bench") {
|
|
333
|
+
bench(event.event);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
program
|
|
339
|
+
.command("suite")
|
|
340
|
+
.description(
|
|
341
|
+
"Run a preset (or JSON file) of benchmark scenarios and summarize them.",
|
|
342
|
+
)
|
|
343
|
+
.option("-p, --preset <name>", "Built-in preset name.")
|
|
344
|
+
.option("-f, --file <path>", "Path to a JSON suite preset file.")
|
|
345
|
+
.option("-o, --out <dir>", "Working dir for generated workspaces.")
|
|
346
|
+
.option("--json <file>", "Write aggregated results as JSON to this file.")
|
|
347
|
+
.option(
|
|
348
|
+
"--md <file>",
|
|
349
|
+
"Write a human-readable Markdown report to this file.",
|
|
350
|
+
)
|
|
351
|
+
.option("--tools <list>", "Override tools for every scenario.", toolList)
|
|
352
|
+
.option("--cold-runs <n>", "Override cold runs for every scenario.", int)
|
|
353
|
+
.option("--warm-runs <n>", "Override warm runs for every scenario.", int)
|
|
354
|
+
.option(
|
|
355
|
+
"--concurrency <n>",
|
|
356
|
+
"Override concurrency for every scenario.",
|
|
357
|
+
int,
|
|
358
|
+
)
|
|
359
|
+
.option("--no-install", "Skip `bun install` in generated workspaces.")
|
|
360
|
+
.option("--keep", "Keep generated workspaces instead of removing them.")
|
|
361
|
+
.option("--list", "List the built-in presets and exit.")
|
|
362
|
+
.action(async (opts) => {
|
|
363
|
+
if (opts.list) {
|
|
364
|
+
process.stdout.write(
|
|
365
|
+
`Available presets:\n${listPresets()
|
|
366
|
+
.map((p) => ` - ${p}`)
|
|
367
|
+
.join("\n")}\n`,
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const suite = opts.file
|
|
373
|
+
? parseSuite(JSON.parse(readFileSync(opts.file, "utf8")))
|
|
374
|
+
: getPreset(opts.preset ?? "quick");
|
|
375
|
+
|
|
376
|
+
const workdir = resolve(opts.out ?? join(tmpdir(), "task-bench-suite"));
|
|
377
|
+
process.stderr.write(
|
|
378
|
+
`Running suite "${suite.name}" (${suite.scenarios.length} scenarios) in ${workdir}\n`,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const overrides = {
|
|
382
|
+
...(opts.tools ? { tools: opts.tools } : {}),
|
|
383
|
+
...(opts.coldRuns !== undefined ? { coldRuns: opts.coldRuns } : {}),
|
|
384
|
+
...(opts.warmRuns !== undefined ? { warmRuns: opts.warmRuns } : {}),
|
|
385
|
+
...(opts.concurrency !== undefined
|
|
386
|
+
? { concurrency: opts.concurrency }
|
|
387
|
+
: {}),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const result: SuiteResult = await runSuite(suite, {
|
|
391
|
+
workdir,
|
|
392
|
+
install: opts.install,
|
|
393
|
+
keep: opts.keep,
|
|
394
|
+
overrides,
|
|
395
|
+
onEvent: suiteProgressHandler(),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const md = formatSuiteMarkdown(result);
|
|
399
|
+
process.stdout.write(`\n${md}\n`);
|
|
400
|
+
if (opts.md) {
|
|
401
|
+
writeFileSync(opts.md, `${md}\n`);
|
|
402
|
+
process.stderr.write(`\nWrote Markdown report to ${opts.md}\n`);
|
|
403
|
+
}
|
|
404
|
+
if (opts.json) {
|
|
405
|
+
writeFileSync(opts.json, `${JSON.stringify(result, null, 2)}\n`);
|
|
406
|
+
process.stderr.write(`Wrote JSON results to ${opts.json}\n`);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
program.parseAsync();
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The supported inter-project dependency graph shapes. These control how much
|
|
5
|
+
* of a task graph a runner has to walk during discovery / scheduling, which is
|
|
6
|
+
* what we are trying to measure.
|
|
7
|
+
*/
|
|
8
|
+
export const DEPENDENCY_STRATEGIES = [
|
|
9
|
+
"isolated",
|
|
10
|
+
"chain",
|
|
11
|
+
"fan-out",
|
|
12
|
+
"layered",
|
|
13
|
+
"random",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export const DependencyStrategySchema = z.enum(DEPENDENCY_STRATEGIES);
|
|
17
|
+
export type DependencyStrategy = z.infer<typeof DependencyStrategySchema>;
|
|
18
|
+
|
|
19
|
+
/** The task runners we know how to generate configuration for and benchmark. */
|
|
20
|
+
export const TOOLS = ["omni", "turbo", "nx", "moon"] as const;
|
|
21
|
+
export const ToolSchema = z.enum(TOOLS);
|
|
22
|
+
export type Tool = z.infer<typeof ToolSchema>;
|
|
23
|
+
|
|
24
|
+
export const DependencyConfigSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
strategy: DependencyStrategySchema.default("layered").describe(
|
|
27
|
+
"Shape of the inter-project dependency graph.",
|
|
28
|
+
),
|
|
29
|
+
layers: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.positive()
|
|
33
|
+
.default(5)
|
|
34
|
+
.describe("Number of layers for the `layered` strategy."),
|
|
35
|
+
fanout: z
|
|
36
|
+
.number()
|
|
37
|
+
.int()
|
|
38
|
+
.nonnegative()
|
|
39
|
+
.default(3)
|
|
40
|
+
.describe(
|
|
41
|
+
"Maximum number of upstream dependencies per project (cap for `layered`/`random`).",
|
|
42
|
+
),
|
|
43
|
+
edgeProbability: z
|
|
44
|
+
.number()
|
|
45
|
+
.min(0)
|
|
46
|
+
.max(1)
|
|
47
|
+
.default(0.35)
|
|
48
|
+
.describe("Edge inclusion probability for the `random` strategy."),
|
|
49
|
+
})
|
|
50
|
+
.prefault({});
|
|
51
|
+
|
|
52
|
+
export const TaskConfigSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
logLines: z
|
|
55
|
+
.number()
|
|
56
|
+
.int()
|
|
57
|
+
.nonnegative()
|
|
58
|
+
.default(25)
|
|
59
|
+
.describe("How many log lines each task prints to stdout."),
|
|
60
|
+
workIterations: z
|
|
61
|
+
.number()
|
|
62
|
+
.int()
|
|
63
|
+
.nonnegative()
|
|
64
|
+
.default(150_000)
|
|
65
|
+
.describe(
|
|
66
|
+
"Iterations of cheap CPU work per task. Keep small so caching dominates.",
|
|
67
|
+
),
|
|
68
|
+
outputFiles: z
|
|
69
|
+
.number()
|
|
70
|
+
.int()
|
|
71
|
+
.positive()
|
|
72
|
+
.default(1)
|
|
73
|
+
.describe("Number of output files each task writes into dist/."),
|
|
74
|
+
chainWithinProject: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.default(true)
|
|
77
|
+
.describe(
|
|
78
|
+
"Whether task `tN` depends on `t(N-1)` within a project.",
|
|
79
|
+
),
|
|
80
|
+
fanUpstream: z
|
|
81
|
+
.boolean()
|
|
82
|
+
.default(true)
|
|
83
|
+
.describe(
|
|
84
|
+
"Whether task `tN` depends on `tN` of upstream projects (^tN).",
|
|
85
|
+
),
|
|
86
|
+
})
|
|
87
|
+
.prefault({});
|
|
88
|
+
|
|
89
|
+
export const VersionsConfigSchema = z
|
|
90
|
+
.object({
|
|
91
|
+
turbo: z.string().default("2.10.3"),
|
|
92
|
+
nx: z.string().default("23.0.1"),
|
|
93
|
+
moon: z.string().default("2.3.5"),
|
|
94
|
+
bun: z.string().default("1.3.14"),
|
|
95
|
+
})
|
|
96
|
+
.prefault({});
|
|
97
|
+
|
|
98
|
+
export const HarnessConfigSchema = z.object({
|
|
99
|
+
seed: z
|
|
100
|
+
.number()
|
|
101
|
+
.int()
|
|
102
|
+
.nonnegative()
|
|
103
|
+
.default(1)
|
|
104
|
+
.describe("Seed for deterministic graph generation."),
|
|
105
|
+
projectPrefix: z
|
|
106
|
+
.string()
|
|
107
|
+
.regex(/^[a-z][a-z0-9-]*$/)
|
|
108
|
+
.default("bench-p")
|
|
109
|
+
.describe("Prefix used for generated package names."),
|
|
110
|
+
projects: z
|
|
111
|
+
.number()
|
|
112
|
+
.int()
|
|
113
|
+
.positive()
|
|
114
|
+
.default(50)
|
|
115
|
+
.describe("Number of projects to generate."),
|
|
116
|
+
tasksPerProject: z
|
|
117
|
+
.number()
|
|
118
|
+
.int()
|
|
119
|
+
.positive()
|
|
120
|
+
.default(3)
|
|
121
|
+
.describe("Number of tasks (t0..tN-1) per project."),
|
|
122
|
+
dependency: DependencyConfigSchema,
|
|
123
|
+
task: TaskConfigSchema,
|
|
124
|
+
tools: z
|
|
125
|
+
.array(ToolSchema)
|
|
126
|
+
.min(1)
|
|
127
|
+
.default([...TOOLS])
|
|
128
|
+
.describe("Which runners to configure and benchmark."),
|
|
129
|
+
versions: VersionsConfigSchema,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export type HarnessConfig = z.infer<typeof HarnessConfigSchema>;
|
|
133
|
+
export type HarnessConfigInput = z.input<typeof HarnessConfigSchema>;
|
|
134
|
+
|
|
135
|
+
/** Parse and fill defaults for a (possibly partial) harness config. */
|
|
136
|
+
export function resolveConfig(input?: HarnessConfigInput): HarnessConfig {
|
|
137
|
+
return HarnessConfigSchema.parse(input ?? {});
|
|
138
|
+
}
|