@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 +61 -0
- package/global.d.ts +7 -0
- package/package.json +3 -1
- package/scripts/benchmark/benchmark-child/constants.ts +6 -0
- package/scripts/benchmark/benchmark-child/createSolver.ts +34 -0
- package/scripts/benchmark/benchmark-child/getAutorouterPipeline4ModulePath.ts +7 -0
- package/scripts/benchmark/benchmark-child/handleWorkerTaskLine.ts +64 -0
- package/scripts/benchmark/benchmark-child/importRuntimeModule.ts +8 -0
- package/scripts/benchmark/benchmark-child/types.ts +20 -0
- package/scripts/benchmark/benchmark-child/writeWorkerResultMessage.ts +10 -0
- package/scripts/benchmark/benchmark-types.ts +34 -0
- package/scripts/benchmark/benchmark.child.ts +11 -0
- package/scripts/benchmark/index.ts +413 -0
- package/tests/benchmark-rectdiff-override.test.ts +41 -0
- package/tests/benchmark-results.test.ts +30 -0
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.
|
|
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,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)
|