@tscircuit/rectdiff 0.0.39 → 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/.github/workflows/bun-test.yml +1 -1
- package/benchmark.sh +61 -0
- package/global.d.ts +7 -0
- package/lib/RectDiffPipeline.ts +0 -27
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +4 -108
- 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/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +3 -3
- package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +3 -3
- package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +1 -1
- package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
- package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +3 -3
- package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
- package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
- package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
- package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +3 -3
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +3 -3
- package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
- package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
- package/tests/solver/bugreport18-1b2d06/__snapshots__/bugreport18-1b2d06.snap.svg +3 -3
- package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +2 -2
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +3 -3
- package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +2 -2
- package/tests/solver/interaction/__snapshots__/interaction.snap.svg +3 -3
- package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
- package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
- package/tests/solver/offboardconnects01/__snapshots__/offboardconnects01.snap.svg +1 -1
- package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +3 -3
- package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +3 -3
- package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
- package/lib/math/layers/getUnionZ.ts +0 -6
- package/lib/math/layers/getZLayerName.ts +0 -6
- package/lib/math/layers/getZSpanMask.ts +0 -6
- package/lib/math/layers/hasContiguousZSpan.ts +0 -11
- package/lib/math/rects/intersectRects.ts +0 -28
- package/lib/math/rects/mergeRects.ts +0 -12
- package/lib/math/rects/rectArea.ts +0 -7
- package/lib/math/rects/rectContainsRect.ts +0 -18
- package/lib/math/rects/rectsTouchOrOverlap.ts +0 -12
- package/lib/math/rects/subtractRects.ts +0 -23
- package/lib/solvers/SparseMultilayerPromotionSolver/SparseMultilayerPromotionSolver.ts +0 -134
- package/lib/solvers/SparseMultilayerPromotionSolver/cloneNode.ts +0 -15
- package/lib/solvers/SparseMultilayerPromotionSolver/cloneNodeWithRect.ts +0 -34
- package/lib/solvers/SparseMultilayerPromotionSolver/createResidualNodes.ts +0 -42
- package/lib/solvers/SparseMultilayerPromotionSolver/findBestCoalesceCandidate.ts +0 -98
- package/lib/solvers/SparseMultilayerPromotionSolver/findBestPromotionCandidate.ts +0 -72
- package/lib/solvers/SparseMultilayerPromotionSolver/getUsableMultilayerVolumeShare.ts +0 -34
- package/lib/solvers/SparseMultilayerPromotionSolver/isFreeNode.ts +0 -8
- package/lib/solvers/SparseMultilayerPromotionSolver/nodeToRect.ts +0 -13
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/CoalesceMultilayerTilesSolver.ts +0 -104
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/PromoteSparseMultilayerCoverageSolver.ts +0 -148
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/TrimContainedSingleLayerCoverageSolver.ts +0 -137
- package/lib/solvers/SparseMultilayerPromotionSolver/types.ts +0 -23
|
@@ -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)
|