Skip to content

Commit 42e1cc9

Browse files
authored
feat: implement an benchmark script (#118)
* feat: implement an benchmark script * fix some import paths * switch to git url * format & and end to end test * update comment * switch to npm-release and add override test * udpate comments * remove packages * remove un needed * types
1 parent 5d5c2dd commit 42e1cc9

15 files changed

Lines changed: 748 additions & 0 deletions

benchmark.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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[@]}"

global.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@ declare module "*.json" {
22
const value: any
33
export default value
44
}
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.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"devDependencies": {
1919
"@biomejs/biome": "^2.3.5",
2020
"@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",
2123
"@tscircuit/math-utils": "^0.0.29",
2224
"@tscircuit/solver-utils": "^0.0.16",
2325
"@types/bun": "latest",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)