@tscircuit/rectdiff 0.0.40 → 0.0.41

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/benchmark.sh ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ LIMIT=""
5
+
6
+ default_concurrency() {
7
+ getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 4
8
+ }
9
+
10
+ CONCURRENT="${BENCHMARK_CONCURRENCY:-$(default_concurrency)}"
11
+
12
+ print_help() {
13
+ cat <<'EOF'
14
+ Usage:
15
+ ./benchmark.sh [--concurrent N] [--limit N]
16
+
17
+ Options:
18
+ --concurrent N Number of Bun worker processes to run, or "auto"
19
+ --limit N Run only the first N dataset scenarios
20
+ -h, --help Show this help
21
+
22
+ Examples:
23
+ ./benchmark.sh
24
+ ./benchmark.sh --concurrent 8
25
+ ./benchmark.sh --limit 25
26
+ ./benchmark.sh --concurrent auto --limit 50
27
+ EOF
28
+ }
29
+
30
+ while [ "$#" -gt 0 ]; do
31
+ case "$1" in
32
+ -h|--help)
33
+ print_help
34
+ exit 0
35
+ ;;
36
+ --concurrent)
37
+ CONCURRENT="${2:-}"
38
+ if [ "$CONCURRENT" = "auto" ]; then
39
+ CONCURRENT="$(default_concurrency)"
40
+ fi
41
+ shift 2
42
+ ;;
43
+ --limit)
44
+ LIMIT="${2:-}"
45
+ shift 2
46
+ ;;
47
+ *)
48
+ echo "Unknown argument: $1"
49
+ echo "Run ./benchmark.sh --help for usage"
50
+ exit 1
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ CMD=(bun "scripts/benchmark/index.ts" "--concurrent" "$CONCURRENT")
56
+
57
+ if [ -n "$LIMIT" ]; then
58
+ CMD+=("--limit" "$LIMIT")
59
+ fi
60
+
61
+ "${CMD[@]}"
package/global.d.ts CHANGED
@@ -2,3 +2,10 @@ declare module "*.json" {
2
2
  const value: any
3
3
  export default value
4
4
  }
5
+
6
+ declare module "@tscircuit/autorouting-dataset-01" {
7
+ import type { SimpleRouteJson } from "./lib/types/srj-types"
8
+
9
+ const dataset: Record<string, SimpleRouteJson>
10
+ export = dataset
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "type": "module",
5
5
  "main": "lib/index.ts",
6
6
  "types": "lib/index.ts",
@@ -18,6 +18,8 @@
18
18
  "devDependencies": {
19
19
  "@biomejs/biome": "^2.3.5",
20
20
  "@react-hook/resize-observer": "^2.0.2",
21
+ "@tscircuit/autorouting-dataset-01": "^1.0.32",
22
+ "@tscircuit/capacity-autorouter": "https://npm-releases.tscircuit.com/npm-tarballs/25314012755-1/tscircuit-capacity-autorouter-0.0.489.tgz",
21
23
  "@tscircuit/math-utils": "^0.0.29",
22
24
  "@tscircuit/solver-utils": "^0.0.16",
23
25
  "@types/bun": "latest",
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Marks the first phase after port-point pathing completes.
3
+ * The benchmark uses this as the stop condition for partial solver runs.
4
+ */
5
+ export const NEXT_PHASE_AFTER_PORT_POINT_PATHING =
6
+ "uniformPortDistributionSolver"
@@ -0,0 +1,34 @@
1
+ import { RectDiffPipeline } from "../../../lib/RectDiffPipeline"
2
+ import type { WorkerTaskMessage } from "../benchmark-types"
3
+ import { getAutorouterPipeline4ModulePath } from "./getAutorouterPipeline4ModulePath"
4
+ import { importRuntimeModule } from "./importRuntimeModule"
5
+ import type { Pipeline4Constructor } from "./types"
6
+
7
+ /**
8
+ * Creates the benchmark solver with the repo-local RectDiff pipeline override.
9
+ * The benchmark imports Pipeline 4 from the published autorouter bundle while
10
+ * forcing it to instantiate this repo's RectDiff implementation by default.
11
+ */
12
+ export const createSolver = async (
13
+ scenario: WorkerTaskMessage["task"]["scenario"],
14
+ RectDiffPipelineClass: typeof RectDiffPipeline = RectDiffPipeline,
15
+ ) => {
16
+ // Load Pipeline 4 from the installed capacity-autorouter dist bundle, then
17
+ // inject the RectDiff class explicitly through its runtime override hook.
18
+ const solverModule = await importRuntimeModule(
19
+ getAutorouterPipeline4ModulePath(),
20
+ )
21
+
22
+ // TODO: Replace this cast once capacity-autorouter exposes a typed
23
+ // Pipeline 4 override interface. As of this repo state, the installed
24
+ // Pipeline 4 constructor only exposes CapacityMeshSolverOptions and does
25
+ // not type the benchmark's overrides.RectDiffPipelineClass injection.
26
+ const AutoroutingPipelineSolver4 = (solverModule as any)
27
+ .AutoroutingPipelineSolver4 as Pipeline4Constructor
28
+
29
+ return new AutoroutingPipelineSolver4(scenario, {
30
+ overrides: {
31
+ RectDiffPipelineClass,
32
+ },
33
+ })
34
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolves the published capacity-autorouter dist entry for benchmarking.
3
+ * The benchmark relies on the bundled runtime export instead of package-
4
+ * internal source paths.
5
+ */
6
+ export const getAutorouterPipeline4ModulePath = () =>
7
+ import.meta.resolve("@tscircuit/capacity-autorouter")
@@ -0,0 +1,64 @@
1
+ import type { WorkerResult, WorkerTaskMessage } from "../benchmark-types"
2
+ import { NEXT_PHASE_AFTER_PORT_POINT_PATHING } from "./constants"
3
+ import { createSolver } from "./createSolver"
4
+ import { writeWorkerResultMessage } from "./writeWorkerResultMessage"
5
+
6
+ /**
7
+ * Handles one benchmark request line from the parent process.
8
+ * Each line must contain a single JSON-encoded worker task message.
9
+ */
10
+ export const handleWorkerTaskLine = async (line: string) => {
11
+ if (!line.trim()) {
12
+ return
13
+ }
14
+
15
+ let message: WorkerTaskMessage
16
+ try {
17
+ message = JSON.parse(line) as WorkerTaskMessage
18
+ } catch (error) {
19
+ const result: WorkerResult = {
20
+ scenarioName: "unknown",
21
+ elapsedTimeMs: 0,
22
+ didSolve: false,
23
+ didTimeout: false,
24
+ error: `Invalid worker message: ${String(error)}`,
25
+ }
26
+ writeWorkerResultMessage(result)
27
+ return
28
+ }
29
+
30
+ const startedAt = performance.now()
31
+
32
+ try {
33
+ const solver = await createSolver(message.task.scenario)
34
+
35
+ while (
36
+ !solver.failed &&
37
+ solver.getCurrentPhase() !== NEXT_PHASE_AFTER_PORT_POINT_PATHING
38
+ ) {
39
+ solver.step()
40
+ }
41
+
42
+ const result: WorkerResult = {
43
+ scenarioName: message.task.scenarioName,
44
+ elapsedTimeMs: Math.max(0, Math.round(performance.now() - startedAt)),
45
+ didSolve:
46
+ !solver.failed &&
47
+ solver.getCurrentPhase() === NEXT_PHASE_AFTER_PORT_POINT_PATHING &&
48
+ solver.portPointPathingSolver !== undefined,
49
+ didTimeout: false,
50
+ error: solver.failed ? solver.error : null,
51
+ }
52
+
53
+ writeWorkerResultMessage(result)
54
+ } catch (error) {
55
+ const result: WorkerResult = {
56
+ scenarioName: message.task.scenarioName,
57
+ elapsedTimeMs: Math.max(0, Math.round(performance.now() - startedAt)),
58
+ didSolve: false,
59
+ didTimeout: false,
60
+ error: String(error),
61
+ }
62
+ writeWorkerResultMessage(result)
63
+ }
64
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Imports a runtime-resolved module path.
3
+ * This keeps the benchmark loader on a runtime-only import path.
4
+ */
5
+ export const importRuntimeModule = (modulePath: string) =>
6
+ new Function("modulePath", "return import(modulePath)")(
7
+ modulePath,
8
+ ) as Promise<unknown>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Describes the subset of solver state the benchmark worker needs.
3
+ * This keeps the child process decoupled from internal package typings.
4
+ */
5
+ export type BenchmarkWorkerSolver = {
6
+ failed: boolean
7
+ error: string | null
8
+ portPointPathingSolver?: unknown
9
+ getCurrentPhase(): string
10
+ step(): void
11
+ }
12
+
13
+ /**
14
+ * Represents the Pipeline 4 constructor shape used by the benchmark.
15
+ * It allows the worker to inject the local RectDiff override.
16
+ */
17
+ export type Pipeline4Constructor = new (
18
+ srj: unknown,
19
+ opts?: Record<string, unknown>,
20
+ ) => BenchmarkWorkerSolver
@@ -0,0 +1,10 @@
1
+ import type { WorkerResult, WorkerResultMessage } from "../benchmark-types"
2
+
3
+ /**
4
+ * Writes a worker result message as a single JSON line.
5
+ * The parent benchmark process reads these messages over stdout.
6
+ */
7
+ export const writeWorkerResultMessage = (result: WorkerResult) => {
8
+ const response: WorkerResultMessage = { result }
9
+ process.stdout.write(`${JSON.stringify(response)}\n`)
10
+ }
@@ -0,0 +1,34 @@
1
+ import type { SimpleRouteJson } from "../../lib/types/srj-types"
2
+
3
+ /**
4
+ * Defines one benchmark scenario loaded from the dataset package.
5
+ */
6
+ export type BenchmarkTask = {
7
+ scenarioName: string
8
+ scenario: SimpleRouteJson
9
+ }
10
+
11
+ /**
12
+ * Wraps a benchmark task for line-delimited worker communication.
13
+ */
14
+ export type WorkerTaskMessage = {
15
+ task: BenchmarkTask
16
+ }
17
+
18
+ /**
19
+ * Captures the outcome of a single benchmark worker run.
20
+ */
21
+ export type WorkerResult = {
22
+ scenarioName: string
23
+ elapsedTimeMs: number
24
+ didSolve: boolean
25
+ didTimeout: boolean
26
+ error: string | null
27
+ }
28
+
29
+ /**
30
+ * Wraps a worker result for line-delimited stdout transport.
31
+ */
32
+ export type WorkerResultMessage = {
33
+ result: WorkerResult
34
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import * as readline from "node:readline"
4
+ import { handleWorkerTaskLine } from "./benchmark-child/handleWorkerTaskLine"
5
+
6
+ const reader = readline.createInterface({
7
+ input: process.stdin,
8
+ crlfDelay: Infinity,
9
+ })
10
+
11
+ reader.on("line", handleWorkerTaskLine)
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"
4
+ import * as os from "node:os"
5
+ import * as path from "node:path"
6
+ import * as readline from "node:readline"
7
+ import * as dataset from "@tscircuit/autorouting-dataset-01"
8
+ import type { SimpleRouteJson } from "../../lib/types/srj-types"
9
+ import type {
10
+ BenchmarkTask,
11
+ WorkerResult,
12
+ WorkerResultMessage,
13
+ WorkerTaskMessage,
14
+ } from "./benchmark-types"
15
+
16
+ type BenchmarkOptions = {
17
+ concurrency: number
18
+ limit?: number
19
+ }
20
+
21
+ type WorkerSlot = {
22
+ id: number
23
+ child: ChildProcessWithoutNullStreams
24
+ stdoutReader: readline.Interface
25
+ stderrReader: readline.Interface
26
+ }
27
+
28
+ type WorkerExecutionResult = {
29
+ result: WorkerResult
30
+ restartWorker: boolean
31
+ }
32
+
33
+ const SAMPLE_TIMEOUT_MS = 120_000
34
+
35
+ const formatDuration = (timeMs: number | null) => {
36
+ if (timeMs === null) {
37
+ return "n/a"
38
+ }
39
+ if (timeMs < 1000) {
40
+ return `${timeMs}ms`
41
+ }
42
+ return `${(timeMs / 1000).toFixed(1)}s`
43
+ }
44
+
45
+ const getPercentileMs = (
46
+ values: number[],
47
+ percentile: number,
48
+ ): number | null => {
49
+ if (values.length === 0) {
50
+ return null
51
+ }
52
+
53
+ const sorted = [...values].sort((a, b) => a - b)
54
+ const index = (sorted.length - 1) * percentile
55
+ const lower = Math.floor(index)
56
+ const upper = Math.ceil(index)
57
+
58
+ if (lower === upper) {
59
+ return sorted[lower] ?? null
60
+ }
61
+
62
+ const weight = index - lower
63
+ const lowerValue = sorted[lower]
64
+ const upperValue = sorted[upper]
65
+ if (lowerValue === undefined || upperValue === undefined) {
66
+ return null
67
+ }
68
+ return lowerValue + (upperValue - lowerValue) * weight
69
+ }
70
+
71
+ const parseArgs = (): BenchmarkOptions => {
72
+ const args = process.argv.slice(2)
73
+ const defaultConcurrency =
74
+ typeof os.availableParallelism === "function"
75
+ ? os.availableParallelism()
76
+ : os.cpus().length
77
+
78
+ const options: BenchmarkOptions = {
79
+ concurrency: defaultConcurrency,
80
+ }
81
+
82
+ for (let i = 0; i < args.length; i += 1) {
83
+ const arg = args[i]
84
+ if (arg === "--concurrent") {
85
+ options.concurrency = Number.parseInt(args[i + 1] ?? "", 10)
86
+ i += 1
87
+ continue
88
+ }
89
+ if (arg === "--limit") {
90
+ options.limit = Number.parseInt(args[i + 1] ?? "", 10)
91
+ i += 1
92
+ continue
93
+ }
94
+ throw new Error(`Unknown argument: ${arg}`)
95
+ }
96
+
97
+ if (!Number.isFinite(options.concurrency) || options.concurrency < 1) {
98
+ throw new Error("--concurrent must be a positive integer")
99
+ }
100
+
101
+ if (
102
+ options.limit !== undefined &&
103
+ (!Number.isFinite(options.limit) || options.limit < 1)
104
+ ) {
105
+ throw new Error("--limit must be a positive integer")
106
+ }
107
+
108
+ return options
109
+ }
110
+
111
+ const loadScenarios = (limit?: number): BenchmarkTask[] => {
112
+ const scenarios = Object.entries(dataset)
113
+ .map(([scenarioName, scenario]) => ({
114
+ scenarioName,
115
+ scenario: scenario as SimpleRouteJson,
116
+ }))
117
+ .sort((a, b) => a.scenarioName.localeCompare(b.scenarioName))
118
+
119
+ if (limit === undefined) {
120
+ return scenarios
121
+ }
122
+
123
+ return scenarios.slice(0, limit)
124
+ }
125
+
126
+ const createChildProcess = () =>
127
+ spawn(
128
+ process.execPath,
129
+ [path.join("scripts", "benchmark", "benchmark.child.ts")],
130
+ {
131
+ cwd: process.cwd(),
132
+ stdio: ["pipe", "pipe", "pipe"],
133
+ env: process.env,
134
+ },
135
+ )
136
+
137
+ const createWorkerSlot = (id: number): WorkerSlot => {
138
+ const child = createChildProcess()
139
+ child.stdout.setEncoding("utf8")
140
+ child.stderr.setEncoding("utf8")
141
+
142
+ return {
143
+ id,
144
+ child,
145
+ stdoutReader: readline.createInterface({
146
+ input: child.stdout,
147
+ crlfDelay: Infinity,
148
+ }),
149
+ stderrReader: readline.createInterface({
150
+ input: child.stderr,
151
+ crlfDelay: Infinity,
152
+ }),
153
+ }
154
+ }
155
+
156
+ const terminateWorker = async (slot: WorkerSlot) => {
157
+ if (slot.child.killed || slot.child.exitCode !== null) {
158
+ slot.stdoutReader.close()
159
+ slot.stderrReader.close()
160
+ return
161
+ }
162
+
163
+ await new Promise<void>((resolve) => {
164
+ const finish = () => {
165
+ slot.stdoutReader.close()
166
+ slot.stderrReader.close()
167
+ resolve()
168
+ }
169
+
170
+ slot.child.once("close", finish)
171
+ try {
172
+ slot.child.kill("SIGKILL")
173
+ } catch {
174
+ finish()
175
+ }
176
+ })
177
+ }
178
+
179
+ const replaceWorker = async (slot: WorkerSlot) => {
180
+ const previousWorker: WorkerSlot = {
181
+ id: slot.id,
182
+ child: slot.child,
183
+ stdoutReader: slot.stdoutReader,
184
+ stderrReader: slot.stderrReader,
185
+ }
186
+
187
+ const nextWorker = createWorkerSlot(slot.id)
188
+ slot.child = nextWorker.child
189
+ slot.stdoutReader = nextWorker.stdoutReader
190
+ slot.stderrReader = nextWorker.stderrReader
191
+ await terminateWorker(previousWorker)
192
+ }
193
+
194
+ const executeTaskOnWorker = (
195
+ slot: WorkerSlot,
196
+ request: WorkerTaskMessage,
197
+ ): Promise<WorkerExecutionResult> =>
198
+ new Promise((resolve) => {
199
+ const startedAt = performance.now()
200
+ let settled = false
201
+ let stderrOutput = ""
202
+
203
+ const finish = (result: WorkerResult, restartWorker: boolean) => {
204
+ if (settled) {
205
+ return
206
+ }
207
+ settled = true
208
+ clearTimeout(timeout)
209
+ slot.stdoutReader.removeListener("line", onLine)
210
+ slot.stderrReader.removeListener("line", onStderrLine)
211
+ slot.child.removeListener("error", onError)
212
+ slot.child.removeListener("exit", onExit)
213
+ resolve({ result, restartWorker })
214
+ }
215
+
216
+ const fail = (
217
+ error: string,
218
+ didTimeout: boolean,
219
+ restartWorker: boolean,
220
+ ) => {
221
+ finish(
222
+ {
223
+ scenarioName: request.task.scenarioName,
224
+ elapsedTimeMs: Math.max(0, Math.round(performance.now() - startedAt)),
225
+ didSolve: false,
226
+ didTimeout,
227
+ error,
228
+ },
229
+ restartWorker,
230
+ )
231
+ }
232
+
233
+ const onLine = (line: string) => {
234
+ let message: WorkerResultMessage
235
+ try {
236
+ message = JSON.parse(line) as WorkerResultMessage
237
+ } catch (error) {
238
+ fail(`Failed to parse worker output: ${String(error)}`, false, true)
239
+ return
240
+ }
241
+
242
+ finish(message.result, false)
243
+ }
244
+
245
+ const onStderrLine = (line: string) => {
246
+ stderrOutput = stderrOutput ? `${stderrOutput}\n${line}` : line
247
+ }
248
+
249
+ const onError = (error: Error) => {
250
+ fail(`Worker error: ${error.message}`, false, true)
251
+ }
252
+
253
+ const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
254
+ fail(
255
+ stderrOutput ||
256
+ `Worker exited before responding (code=${code}, signal=${signal})`,
257
+ false,
258
+ true,
259
+ )
260
+ }
261
+
262
+ const timeout = setTimeout(() => {
263
+ fail(`Timed out after ${formatDuration(SAMPLE_TIMEOUT_MS)}`, true, true)
264
+ }, SAMPLE_TIMEOUT_MS)
265
+
266
+ slot.stdoutReader.on("line", onLine)
267
+ slot.stderrReader.on("line", onStderrLine)
268
+ slot.child.once("error", onError)
269
+ slot.child.once("exit", onExit)
270
+ slot.child.stdin.write(`${JSON.stringify(request)}\n`)
271
+ })
272
+
273
+ const formatTable = (
274
+ scenarioCount: number,
275
+ solvedCount: number,
276
+ failedCount: number,
277
+ timedOutCount: number,
278
+ avgTimeMs: number,
279
+ p50TimeMs: number | null,
280
+ p95TimeMs: number | null,
281
+ totalTimeMs: number,
282
+ ) => {
283
+ const headers = [
284
+ "Solver",
285
+ "Scenarios",
286
+ "Solved",
287
+ "Failed",
288
+ "Timed Out",
289
+ "Success %",
290
+ "Avg Time",
291
+ "P50 Time",
292
+ "P95 Time",
293
+ "Total Time",
294
+ ]
295
+
296
+ const rows = [
297
+ [
298
+ "Pipeline4->PortPointPathing",
299
+ String(scenarioCount),
300
+ String(solvedCount),
301
+ String(failedCount),
302
+ String(timedOutCount),
303
+ `${((solvedCount / scenarioCount) * 100).toFixed(1)}%`,
304
+ formatDuration(avgTimeMs),
305
+ formatDuration(p50TimeMs),
306
+ formatDuration(p95TimeMs),
307
+ formatDuration(totalTimeMs),
308
+ ],
309
+ ]
310
+
311
+ const widths = headers.map((header, columnIndex) =>
312
+ Math.max(
313
+ header.length,
314
+ ...rows.map((row) => row[columnIndex]?.length ?? 0),
315
+ ),
316
+ )
317
+ const paddedWidths = widths.map((width) => width ?? 0)
318
+
319
+ const separator = `+${paddedWidths.map((width) => "-".repeat(width + 2)).join("+")}+`
320
+ const headerLine = `| ${headers.map((header, index) => header.padEnd(paddedWidths[index] ?? header.length)).join(" | ")} |`
321
+ const bodyLines = rows.map(
322
+ (row) =>
323
+ `| ${row.map((cell, index) => cell.padEnd(paddedWidths[index] ?? cell.length)).join(" | ")} |`,
324
+ )
325
+
326
+ return [separator, headerLine, separator, ...bodyLines, separator].join("\n")
327
+ }
328
+
329
+ const main = async () => {
330
+ const options = parseArgs()
331
+ const tasks = loadScenarios(options.limit)
332
+
333
+ console.log(
334
+ `Running ${tasks.length} dataset scenarios with concurrency ${options.concurrency}`,
335
+ )
336
+
337
+ const startedAt = performance.now()
338
+ const results: WorkerResult[] = []
339
+ const workers = Array.from(
340
+ { length: Math.min(options.concurrency, tasks.length) },
341
+ (_, index) => createWorkerSlot(index + 1),
342
+ )
343
+
344
+ let nextTaskIndex = 0
345
+
346
+ const runWorkerLoop = async (slot: WorkerSlot) => {
347
+ while (nextTaskIndex < tasks.length) {
348
+ const task = tasks[nextTaskIndex]
349
+ if (!task) {
350
+ break
351
+ }
352
+ nextTaskIndex += 1
353
+
354
+ const execution = await executeTaskOnWorker(slot, { task })
355
+ results.push(execution.result)
356
+
357
+ const progressLabel = `[${results.length}/${tasks.length}]`
358
+ const status = execution.result.didSolve
359
+ ? "solved"
360
+ : execution.result.didTimeout
361
+ ? "timed out"
362
+ : "failed"
363
+ console.log(
364
+ `${progressLabel} ${execution.result.scenarioName} ${status} in ${formatDuration(execution.result.elapsedTimeMs)}`,
365
+ )
366
+
367
+ if (execution.restartWorker) {
368
+ await replaceWorker(slot)
369
+ }
370
+ }
371
+ }
372
+
373
+ await Promise.all(workers.map((slot) => runWorkerLoop(slot)))
374
+ await Promise.all(workers.map((slot) => terminateWorker(slot)))
375
+
376
+ const totalTimeMs = Math.max(0, Math.round(performance.now() - startedAt))
377
+ const solvedCount = results.filter((result) => result.didSolve).length
378
+ const failedCount = results.length - solvedCount
379
+ const timedOutCount = results.filter((result) => result.didTimeout).length
380
+ const elapsedTimes = results.map((result) => result.elapsedTimeMs)
381
+ const avgTimeMs =
382
+ elapsedTimes.reduce((sum, value) => sum + value, 0) / elapsedTimes.length
383
+
384
+ console.log("")
385
+ console.log(
386
+ formatTable(
387
+ results.length,
388
+ solvedCount,
389
+ failedCount,
390
+ timedOutCount,
391
+ Math.round(avgTimeMs),
392
+ getPercentileMs(elapsedTimes, 0.5),
393
+ getPercentileMs(elapsedTimes, 0.95),
394
+ totalTimeMs,
395
+ ),
396
+ )
397
+
398
+ const failures = results.filter((result) => !result.didSolve)
399
+ if (failures.length > 0) {
400
+ console.log("")
401
+ console.log("Failed scenarios:")
402
+ for (const failure of failures.slice(0, 20)) {
403
+ console.log(
404
+ `- ${failure.scenarioName}: ${failure.error ?? (failure.didTimeout ? "Timed out" : "Unknown failure")}`,
405
+ )
406
+ }
407
+ if (failures.length > 20) {
408
+ console.log(`- ... and ${failures.length - 20} more`)
409
+ }
410
+ }
411
+ }
412
+
413
+ await main()
@@ -0,0 +1,41 @@
1
+ import { expect, test } from "bun:test"
2
+ import { RectDiffPipeline } from "../lib/RectDiffPipeline"
3
+ import { createSolver } from "../scripts/benchmark/benchmark-child/createSolver"
4
+ import type { SimpleRouteJson } from "../lib/types/srj-types"
5
+
6
+ const SENTINEL_ERROR = "FAKE_RECTDIFF_PIPELINE_USED"
7
+
8
+ class FakeRectDiffPipeline extends RectDiffPipeline {
9
+ constructor(...args: ConstructorParameters<typeof RectDiffPipeline>) {
10
+ super(...args)
11
+ throw new Error(SENTINEL_ERROR)
12
+ }
13
+ }
14
+
15
+ test("benchmark createSolver uses an injected fake RectDiff override", async () => {
16
+ const simpleRouteJson: SimpleRouteJson = {
17
+ bounds: {
18
+ minX: 0,
19
+ maxX: 10,
20
+ minY: 0,
21
+ maxY: 10,
22
+ },
23
+ obstacles: [],
24
+ connections: [],
25
+ layerCount: 2,
26
+ minTraceWidth: 0.15,
27
+ }
28
+
29
+ const solver = await createSolver(simpleRouteJson, FakeRectDiffPipeline)
30
+
31
+ while (!solver.failed && solver.getCurrentPhase() !== "nodeSolver") {
32
+ solver.step()
33
+ }
34
+
35
+ expect(solver.getCurrentPhase()).toBe("nodeSolver")
36
+ expect(solver.failed).toBe(false)
37
+
38
+ expect(() => solver.step()).toThrow(SENTINEL_ERROR)
39
+ expect(solver.failed).toBe(true)
40
+ expect(solver.error).toContain(SENTINEL_ERROR)
41
+ }, 60_000)
@@ -0,0 +1,30 @@
1
+ import { expect, test } from "bun:test"
2
+
3
+ test("benchmark prints result output for one scenario", async () => {
4
+ const benchmarkProcess = Bun.spawn({
5
+ cmd: [
6
+ process.execPath,
7
+ "scripts/benchmark/index.ts",
8
+ "--limit",
9
+ "1",
10
+ "--concurrent",
11
+ "1",
12
+ ],
13
+ cwd: process.cwd(),
14
+ stdout: "pipe",
15
+ stderr: "pipe",
16
+ env: process.env,
17
+ })
18
+
19
+ const [exitCode, stdout, stderr] = await Promise.all([
20
+ benchmarkProcess.exited,
21
+ new Response(benchmarkProcess.stdout).text(),
22
+ new Response(benchmarkProcess.stderr).text(),
23
+ ])
24
+
25
+ expect(stderr).toBe("")
26
+ expect(exitCode).toBe(0)
27
+ expect(stdout).toContain("Running 1 dataset scenarios with concurrency 1")
28
+ expect(stdout).toContain("[1/1]")
29
+ expect(stdout).toContain("Pipeline4->PortPointPathing")
30
+ }, 180_000)