@bushscad/cli 0.0.0-stage → 0.3.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/LICENSE +202 -0
- package/README.md +4 -2
- package/dist/classic-benchmark.js +35250 -0
- package/dist/fonts/NotoSans-LICENSE.txt +94 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/NotoSansMono-LICENSE.txt +94 -0
- package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
- package/dist/fonts/NotoSerif-LICENSE.txt +94 -0
- package/dist/fonts/NotoSerif-Regular.ttf +0 -0
- package/dist/index.js +40765 -0
- package/dist/index.js.map +7 -0
- package/dist/occt-cancellation-probe.js +107 -0
- package/dist/occt-probe-runner.js +232 -0
- package/dist/occt-probe-worker.js +13936 -0
- package/dist/p7-benchmark.js +623 -0
- package/package.json +50 -6
- package/src/dto/bufferDto.ts +22 -0
- package/src/dto/index.ts +28 -0
- package/src/dto/json.ts +39 -0
- package/src/dto/meshDto.ts +26 -0
- package/src/dto/message-parser.ts +158 -0
- package/src/dto/request-fields.ts +174 -0
- package/src/dto/request-parser.ts +214 -0
- package/src/dto/types.ts +195 -0
- package/src/service/parallel/worker-protocol.ts +164 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
// scripts/parallel-compilation/p7-benchmark.ts
|
|
2
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname as dirname2, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
// scripts/parallel-compilation/report-output.ts
|
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
async function writeJsonReport(outPath, report, stdoutReport = report) {
|
|
9
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
10
|
+
await writeFile(outPath, `${JSON.stringify(report, null, 2)}
|
|
11
|
+
`, "utf8");
|
|
12
|
+
process.stdout.write(`${JSON.stringify(stdoutReport, null, 2)}
|
|
13
|
+
`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// scripts/parallel-compilation/p7-service-client.ts
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { createInterface } from "node:readline";
|
|
19
|
+
var P7ServiceClient = class _P7ServiceClient {
|
|
20
|
+
constructor(child, startupWallMs, readyStatus, messages) {
|
|
21
|
+
this.child = child;
|
|
22
|
+
this.startupWallMs = startupWallMs;
|
|
23
|
+
this.readyStatus = readyStatus;
|
|
24
|
+
this.messages = messages;
|
|
25
|
+
createInterface({ input: child.stderr }).on(
|
|
26
|
+
"line",
|
|
27
|
+
(line) => this.stderr.push(line)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
stderr = [];
|
|
31
|
+
nextRequestId = 1;
|
|
32
|
+
/** Starts the real compiled CLI and waits until every OCCT owner is ready. */
|
|
33
|
+
static async start(cliPath, workerCount) {
|
|
34
|
+
const startedAt = performance.now();
|
|
35
|
+
const child = spawn(process.execPath, [cliPath, "--service"], {
|
|
36
|
+
env: {
|
|
37
|
+
...process.env,
|
|
38
|
+
BUSHSCAD_EXPERIMENTAL_PARALLEL: "1",
|
|
39
|
+
BUSHSCAD_OCCT_WORKERS: String(workerCount)
|
|
40
|
+
},
|
|
41
|
+
windowsHide: true
|
|
42
|
+
});
|
|
43
|
+
const messages = [];
|
|
44
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
45
|
+
try {
|
|
46
|
+
messages.push(JSON.parse(line));
|
|
47
|
+
} catch {
|
|
48
|
+
messages.push({ kind: "invalid-json", line });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const status = await waitForReady(messages, child);
|
|
52
|
+
const client = new _P7ServiceClient(
|
|
53
|
+
child,
|
|
54
|
+
performance.now() - startedAt,
|
|
55
|
+
status,
|
|
56
|
+
messages
|
|
57
|
+
);
|
|
58
|
+
return client;
|
|
59
|
+
}
|
|
60
|
+
/** Sends one source request and captures coordinator wall time separately. */
|
|
61
|
+
async request(request) {
|
|
62
|
+
const id = this.nextRequestId++;
|
|
63
|
+
const startedAt = performance.now();
|
|
64
|
+
this.child.stdin.write(`${JSON.stringify({ id, ...request })}
|
|
65
|
+
`);
|
|
66
|
+
const response = await waitForResponse(
|
|
67
|
+
this.messages,
|
|
68
|
+
this.child,
|
|
69
|
+
id,
|
|
70
|
+
request.kind
|
|
71
|
+
);
|
|
72
|
+
return { response, wallMs: performance.now() - startedAt };
|
|
73
|
+
}
|
|
74
|
+
/** Returns the newest pool status emitted after task memory was sampled. */
|
|
75
|
+
latestStatus() {
|
|
76
|
+
const status = [...this.messages].reverse().find(isPoolStatusMessage);
|
|
77
|
+
if (!status) return this.readyStatus;
|
|
78
|
+
return status.status;
|
|
79
|
+
}
|
|
80
|
+
/** Gracefully shuts down the service and all child OCCT owners. */
|
|
81
|
+
async stop() {
|
|
82
|
+
if (this.child.exitCode !== null) return;
|
|
83
|
+
this.child.kill("SIGTERM");
|
|
84
|
+
await new Promise((resolve2) => {
|
|
85
|
+
this.child.once("exit", () => resolve2());
|
|
86
|
+
setTimeout(resolve2, 1e4);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
async function waitForReady(messages, child) {
|
|
91
|
+
return waitForValue(
|
|
92
|
+
messages,
|
|
93
|
+
child,
|
|
94
|
+
(message) => isPoolStatusMessage(message) && message.status.state === "ready" ? message.status : void 0
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
async function waitForResponse(messages, child, id, kind) {
|
|
98
|
+
return waitForValue(
|
|
99
|
+
messages,
|
|
100
|
+
child,
|
|
101
|
+
(message) => responseForRequest(message, id, kind)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function responseForRequest(value, id, kind) {
|
|
105
|
+
if (!isServiceResult(value)) return void 0;
|
|
106
|
+
return matchesRequest(value, id, kind) ? value : void 0;
|
|
107
|
+
}
|
|
108
|
+
function isServiceResult(value) {
|
|
109
|
+
return isRecord(value) && typeof value.ok === "boolean";
|
|
110
|
+
}
|
|
111
|
+
function matchesRequest(value, id, kind) {
|
|
112
|
+
return value.id === id && value.kind === kind;
|
|
113
|
+
}
|
|
114
|
+
async function waitForValue(messages, child, select) {
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
while (Date.now() - startedAt < 24e4) {
|
|
117
|
+
const value = selectNewest(messages, select);
|
|
118
|
+
if (value !== void 0) return value;
|
|
119
|
+
if (child.exitCode !== null)
|
|
120
|
+
throw new Error(`Compiler service exited with ${child.exitCode}.`);
|
|
121
|
+
await new Promise((resolve2) => setTimeout(resolve2, 10));
|
|
122
|
+
}
|
|
123
|
+
throw new Error("Compiler service response timed out.");
|
|
124
|
+
}
|
|
125
|
+
function selectNewest(messages, select) {
|
|
126
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
127
|
+
const value = select(messages[index]);
|
|
128
|
+
if (value !== void 0) return value;
|
|
129
|
+
}
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
function isPoolStatusMessage(value) {
|
|
133
|
+
return isRecord(value) && value.kind === "pool-status" && isRecord(value.status);
|
|
134
|
+
}
|
|
135
|
+
function isRecord(value) {
|
|
136
|
+
return typeof value === "object" && value !== null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// scripts/parallel-compilation/p7-response-contract.ts
|
|
140
|
+
import { createHash } from "node:crypto";
|
|
141
|
+
function responseContract(response) {
|
|
142
|
+
const value = response.ok ? successContract(response) : failureContract(response);
|
|
143
|
+
return {
|
|
144
|
+
hash: createHash("sha256").update(JSON.stringify(value)).digest("hex"),
|
|
145
|
+
value
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function successContract(response) {
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
program: response.program,
|
|
152
|
+
meshes: array(response.meshes).map(meshContract),
|
|
153
|
+
interaction: interactionContract(response.interaction),
|
|
154
|
+
drawings: response.drawings,
|
|
155
|
+
validations: response.validations,
|
|
156
|
+
joints: response.joints,
|
|
157
|
+
env: response.env,
|
|
158
|
+
dependents: response.dependents,
|
|
159
|
+
buffers: array(response.buffers).map(bufferContract),
|
|
160
|
+
traceErrors: array(response.occtTraceEvents).filter(isErrorTrace).map(traceContract)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function failureContract(response) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
errors: response.errors,
|
|
167
|
+
env: response.env,
|
|
168
|
+
traceErrors: array(response.occtTraceEvents).filter(isErrorTrace).map(traceContract)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function meshContract(value) {
|
|
172
|
+
if (!isRecord2(value)) return value;
|
|
173
|
+
const vertices = numberArray(value.vertices);
|
|
174
|
+
const indices = numberArray(value.indices);
|
|
175
|
+
const geometry = meshGeometry(vertices);
|
|
176
|
+
return {
|
|
177
|
+
partName: value.partName,
|
|
178
|
+
bodyName: value.bodyName,
|
|
179
|
+
color: value.color,
|
|
180
|
+
material: value.material,
|
|
181
|
+
vertexCount: vertices.length / 3,
|
|
182
|
+
triangleCount: indices.length / 3,
|
|
183
|
+
...geometry
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function meshGeometry(vertices) {
|
|
187
|
+
const bounds = [
|
|
188
|
+
[Infinity, Infinity, Infinity],
|
|
189
|
+
[-Infinity, -Infinity, -Infinity]
|
|
190
|
+
];
|
|
191
|
+
const sums = [0, 0, 0];
|
|
192
|
+
for (let index = 0; index < vertices.length; index += 3) {
|
|
193
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
194
|
+
const coordinate = vertices[index + axis] ?? 0;
|
|
195
|
+
bounds[0][axis] = Math.min(bounds[0][axis], coordinate);
|
|
196
|
+
bounds[1][axis] = Math.max(bounds[1][axis], coordinate);
|
|
197
|
+
sums[axis] += coordinate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { bounds, sums };
|
|
201
|
+
}
|
|
202
|
+
function interactionContract(value) {
|
|
203
|
+
if (!isRecord2(value)) return value;
|
|
204
|
+
return {
|
|
205
|
+
...value,
|
|
206
|
+
topology: array(value.topology).map((entry) => {
|
|
207
|
+
if (!isRecord2(entry)) return entry;
|
|
208
|
+
return {
|
|
209
|
+
meshIndex: entry.meshIndex,
|
|
210
|
+
partEntityId: entry.partEntityId,
|
|
211
|
+
objectEntityId: entry.objectEntityId,
|
|
212
|
+
faces: topologyContract(entry.faces, "triangleCount"),
|
|
213
|
+
edges: topologyContract(entry.edges, "points"),
|
|
214
|
+
corners: topologyContract(entry.corners)
|
|
215
|
+
};
|
|
216
|
+
})
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function topologyContract(value, aggregate) {
|
|
220
|
+
const entries = array(value).filter(isRecord2);
|
|
221
|
+
const total = entries.reduce(
|
|
222
|
+
(sum, entry) => sum + (aggregate === "triangleCount" ? number(entry.triangleCount) : aggregate === "points" ? array(entry.points).length : 0),
|
|
223
|
+
0
|
|
224
|
+
);
|
|
225
|
+
return {
|
|
226
|
+
count: entries.length,
|
|
227
|
+
...aggregate === "triangleCount" ? { triangles: total } : {},
|
|
228
|
+
...aggregate === "points" ? { points: total } : {},
|
|
229
|
+
owners: unique(entries.map((entry) => String(entry.ownerEntityId))),
|
|
230
|
+
targets: unique(
|
|
231
|
+
entries.flatMap((entry) => array(entry.semanticTargetIds).map(String))
|
|
232
|
+
)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function traceContract(value) {
|
|
236
|
+
if (!isRecord2(value)) return value;
|
|
237
|
+
const {
|
|
238
|
+
timestampMs: _timestamp,
|
|
239
|
+
elapsedMs: _elapsed,
|
|
240
|
+
cpuMs: _cpu,
|
|
241
|
+
...stable
|
|
242
|
+
} = value;
|
|
243
|
+
return stable;
|
|
244
|
+
}
|
|
245
|
+
function isErrorTrace(value) {
|
|
246
|
+
return isRecord2(value) && (value.kind === "op-error" || value.kind === "build-error");
|
|
247
|
+
}
|
|
248
|
+
function bufferContract(value) {
|
|
249
|
+
if (!isRecord2(value)) return value;
|
|
250
|
+
const data = String(value.dataBase64 ?? "");
|
|
251
|
+
return {
|
|
252
|
+
name: value.name,
|
|
253
|
+
byteLength: Buffer.from(data, "base64").byteLength,
|
|
254
|
+
sha256: createHash("sha256").update(data).digest("hex")
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function unique(values) {
|
|
258
|
+
return [...new Set(values)].sort();
|
|
259
|
+
}
|
|
260
|
+
function array(value) {
|
|
261
|
+
return Array.isArray(value) ? value : [];
|
|
262
|
+
}
|
|
263
|
+
function numberArray(value) {
|
|
264
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "number") : [];
|
|
265
|
+
}
|
|
266
|
+
function number(value) {
|
|
267
|
+
return typeof value === "number" ? value : 0;
|
|
268
|
+
}
|
|
269
|
+
function isRecord2(value) {
|
|
270
|
+
return typeof value === "object" && value !== null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// scripts/parallel-compilation/p7-benchmark-summary.ts
|
|
274
|
+
function summarizeSamples(samples) {
|
|
275
|
+
const groups = /* @__PURE__ */ new Map();
|
|
276
|
+
for (const sample of samples) {
|
|
277
|
+
const key = [
|
|
278
|
+
sample.workers,
|
|
279
|
+
sample.workload,
|
|
280
|
+
sample.engine,
|
|
281
|
+
sample.phase
|
|
282
|
+
].join("|");
|
|
283
|
+
groups.set(key, [...groups.get(key) ?? [], sample]);
|
|
284
|
+
}
|
|
285
|
+
return [...groups.values()].map((group) => {
|
|
286
|
+
const first = group[0];
|
|
287
|
+
return {
|
|
288
|
+
workers: first.workers,
|
|
289
|
+
workload: first.workload,
|
|
290
|
+
engine: first.engine,
|
|
291
|
+
phase: first.phase,
|
|
292
|
+
samples: group.length,
|
|
293
|
+
wallMs: statistics(group.map((sample) => sample.wallMs)),
|
|
294
|
+
compilerElapsedMs: statistics(
|
|
295
|
+
group.map((sample) => sample.compilerElapsedMs)
|
|
296
|
+
),
|
|
297
|
+
workerCpuMs: statistics(metricValues(group, "workerCpuMs")),
|
|
298
|
+
workerBusyMs: statistics(metricValues(group, "workerBusyMs")),
|
|
299
|
+
queueWaitMs: statistics(metricValues(group, "queueWaitMs")),
|
|
300
|
+
transportMs: statistics(metricValues(group, "transportMs")),
|
|
301
|
+
workerUtilization: statistics(metricValues(group, "workerUtilization")),
|
|
302
|
+
peakWorkerRssBytes: Math.max(
|
|
303
|
+
0,
|
|
304
|
+
...metricValues(group, "peakWorkerRssBytes")
|
|
305
|
+
),
|
|
306
|
+
schedulerTaskCount: statistics(metricValues(group, "schedulerTaskCount")),
|
|
307
|
+
longestWorkerTaskMs: statistics(
|
|
308
|
+
metricValues(group, "longestWorkerTaskMs")
|
|
309
|
+
),
|
|
310
|
+
nativeBooleanBatchCount: statistics(
|
|
311
|
+
metricValues(group, "nativeBooleanBatchCount")
|
|
312
|
+
),
|
|
313
|
+
operationTimings: operationTimings(group)
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function statistics(values) {
|
|
318
|
+
if (values.length === 0) return void 0;
|
|
319
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
320
|
+
return {
|
|
321
|
+
median: percentile(sorted, 0.5),
|
|
322
|
+
p95: percentile(sorted, 0.95),
|
|
323
|
+
min: sorted[0],
|
|
324
|
+
max: sorted.at(-1)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function percentile(sorted, fraction) {
|
|
328
|
+
return sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)] ?? 0;
|
|
329
|
+
}
|
|
330
|
+
function metricValues(samples, name) {
|
|
331
|
+
return samples.flatMap((sample) => {
|
|
332
|
+
const value = sample.metrics?.[name];
|
|
333
|
+
return typeof value === "number" ? [value] : [];
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function operationTimings(samples) {
|
|
337
|
+
const timings = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const event of samples.flatMap((sample) => sample.trace))
|
|
339
|
+
recordOperationTiming(timings, event);
|
|
340
|
+
return [...timings.entries()].map(([operation, values]) => ({
|
|
341
|
+
operation,
|
|
342
|
+
elapsedMs: statistics(values.elapsedMs),
|
|
343
|
+
cpuMs: statistics(values.cpuMs)
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
function recordOperationTiming(timings, event) {
|
|
347
|
+
if (!isTimedOperation(event.kind)) return;
|
|
348
|
+
const name = operationName(event);
|
|
349
|
+
const timing = timings.get(name) ?? { elapsedMs: [], cpuMs: [] };
|
|
350
|
+
appendNumber(timing.elapsedMs, event.elapsedMs);
|
|
351
|
+
appendNumber(timing.cpuMs, event.cpuMs);
|
|
352
|
+
timings.set(name, timing);
|
|
353
|
+
}
|
|
354
|
+
function isTimedOperation(kind) {
|
|
355
|
+
return kind === "op-success" || kind === "native-boolean-batch";
|
|
356
|
+
}
|
|
357
|
+
function operationName(event) {
|
|
358
|
+
return String(event.opKind ?? event.kind);
|
|
359
|
+
}
|
|
360
|
+
function appendNumber(values, value) {
|
|
361
|
+
if (typeof value === "number") values.push(value);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// scripts/parallel-compilation/p7-performance-runner.ts
|
|
365
|
+
import { readFile } from "node:fs/promises";
|
|
366
|
+
async function runPerformance(client, rootPath, workers, workloads, runs, samples, progress2) {
|
|
367
|
+
for (const workload of workloads)
|
|
368
|
+
await runWorkload(
|
|
369
|
+
client,
|
|
370
|
+
rootPath,
|
|
371
|
+
workers,
|
|
372
|
+
workload,
|
|
373
|
+
runs,
|
|
374
|
+
samples,
|
|
375
|
+
progress2
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
async function runWorkload(client, rootPath, workers, workload, runs, samples, progress2) {
|
|
379
|
+
const source = await readFile(workload.filePath, "utf8");
|
|
380
|
+
for (let round = 0; round < runs; round++)
|
|
381
|
+
await runRound(
|
|
382
|
+
client,
|
|
383
|
+
rootPath,
|
|
384
|
+
workers,
|
|
385
|
+
workload,
|
|
386
|
+
source,
|
|
387
|
+
round,
|
|
388
|
+
samples,
|
|
389
|
+
progress2
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
async function runRound(client, rootPath, workers, workload, source, round, samples, progress2) {
|
|
393
|
+
for (const engine of engineOrder(round))
|
|
394
|
+
await sampleEngine(
|
|
395
|
+
client,
|
|
396
|
+
rootPath,
|
|
397
|
+
workers,
|
|
398
|
+
workload,
|
|
399
|
+
source,
|
|
400
|
+
round,
|
|
401
|
+
engine,
|
|
402
|
+
samples,
|
|
403
|
+
progress2
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
async function sampleEngine(client, rootPath, workers, workload, source, round, engine, samples, progress2) {
|
|
407
|
+
progress2(`preview w${workers} ${workload.id} ${engine} r${round + 1}`);
|
|
408
|
+
samples.push(await takeSample(client, rootPath, workload, engine, "preview"));
|
|
409
|
+
samples.push(
|
|
410
|
+
await takeSample(client, rootPath, workload, engine, "incremental", {
|
|
411
|
+
[workload.filePath]: `${source}
|
|
412
|
+
// P7 incremental ${round}
|
|
413
|
+
`
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
if (!workload.export) return;
|
|
417
|
+
progress2(`export w${workers} ${workload.id} ${engine} r${round + 1}`);
|
|
418
|
+
samples.push(await takeSample(client, rootPath, workload, engine, "export"));
|
|
419
|
+
}
|
|
420
|
+
async function takeSample(client, rootPath, workload, engine, phase, overlays) {
|
|
421
|
+
const exporting = phase === "export";
|
|
422
|
+
const { response, wallMs } = await client.request({
|
|
423
|
+
kind: exporting ? "export" : "preview",
|
|
424
|
+
filePath: workload.filePath,
|
|
425
|
+
rootPath,
|
|
426
|
+
engine,
|
|
427
|
+
occtTrace: !exporting,
|
|
428
|
+
...overlays ? { overlays } : {},
|
|
429
|
+
...exporting ? { format: "step" } : {}
|
|
430
|
+
});
|
|
431
|
+
requireSuccess(response, workload, engine, phase);
|
|
432
|
+
return {
|
|
433
|
+
workers: client.readyStatus.desiredWorkers,
|
|
434
|
+
workload: workload.id,
|
|
435
|
+
engine,
|
|
436
|
+
phase,
|
|
437
|
+
wallMs,
|
|
438
|
+
compilerElapsedMs: response.elapsedMs,
|
|
439
|
+
metrics: response.compilerMetrics,
|
|
440
|
+
trace: records(response.occtTraceEvents)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function engineOrder(round) {
|
|
444
|
+
return round % 2 === 0 ? ["classic", "parallel"] : ["parallel", "classic"];
|
|
445
|
+
}
|
|
446
|
+
function requireSuccess(response, workload, engine, phase) {
|
|
447
|
+
if (response.ok) return;
|
|
448
|
+
throw new Error(
|
|
449
|
+
`${workload.id} ${engine} ${phase} failed: ${JSON.stringify(response)}`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
function records(value) {
|
|
453
|
+
if (!Array.isArray(value)) return [];
|
|
454
|
+
return value.filter(
|
|
455
|
+
(entry) => typeof entry === "object" && entry !== null
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// scripts/parallel-compilation/p7-benchmark.ts
|
|
460
|
+
async function run() {
|
|
461
|
+
const rootPath = resolve(readArg("--root", "."));
|
|
462
|
+
const cliPath = resolve(readArg("--cli", "packages/cli/dist/index.js"));
|
|
463
|
+
const outPath = resolve(
|
|
464
|
+
readArg(
|
|
465
|
+
"--out",
|
|
466
|
+
".bushscad-analysis/parallel-compilation/p7-benchmark.json"
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
const runs = positiveInteger(readArg("--runs", "3"));
|
|
470
|
+
const workers = readArg("--workers", "1,2,4").split(",").map(positiveInteger);
|
|
471
|
+
const scope = readArg("--scope", "full");
|
|
472
|
+
const correctnessCases = await loadSuiteCases(rootPath);
|
|
473
|
+
const performanceCases = performanceWorkloads(rootPath);
|
|
474
|
+
const equivalence = [];
|
|
475
|
+
const samples = [];
|
|
476
|
+
const lifecycle = [];
|
|
477
|
+
for (const workerCount of workers)
|
|
478
|
+
await runWorkerMatrix({
|
|
479
|
+
cliPath,
|
|
480
|
+
rootPath,
|
|
481
|
+
workerCount,
|
|
482
|
+
scope,
|
|
483
|
+
runs,
|
|
484
|
+
correctnessCases,
|
|
485
|
+
performanceCases,
|
|
486
|
+
equivalence,
|
|
487
|
+
samples,
|
|
488
|
+
lifecycle
|
|
489
|
+
});
|
|
490
|
+
const mismatches = equivalence.filter((result) => !result.match);
|
|
491
|
+
const report = {
|
|
492
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
493
|
+
node: process.version,
|
|
494
|
+
rootPath,
|
|
495
|
+
cliPath,
|
|
496
|
+
configuration: { runs, workers, scope },
|
|
497
|
+
lifecycle,
|
|
498
|
+
equivalence: {
|
|
499
|
+
comparisons: equivalence.length,
|
|
500
|
+
matches: equivalence.length - mismatches.length,
|
|
501
|
+
mismatches,
|
|
502
|
+
results: equivalence
|
|
503
|
+
},
|
|
504
|
+
summaries: summarizeSamples(samples),
|
|
505
|
+
samples
|
|
506
|
+
};
|
|
507
|
+
await writeJsonReport(outPath, report, {
|
|
508
|
+
outPath,
|
|
509
|
+
lifecycle,
|
|
510
|
+
equivalence: report.equivalence,
|
|
511
|
+
summaries: report.summaries
|
|
512
|
+
});
|
|
513
|
+
if (mismatches.length > 0) process.exitCode = 2;
|
|
514
|
+
}
|
|
515
|
+
async function runWorkerMatrix(input) {
|
|
516
|
+
progress(`starting ${input.workerCount}-worker pool`);
|
|
517
|
+
const client = await P7ServiceClient.start(input.cliPath, input.workerCount);
|
|
518
|
+
try {
|
|
519
|
+
input.lifecycle.push({
|
|
520
|
+
workers: input.workerCount,
|
|
521
|
+
startupWallMs: client.startupWallMs,
|
|
522
|
+
ready: client.readyStatus
|
|
523
|
+
});
|
|
524
|
+
if (input.scope !== "performance")
|
|
525
|
+
await runEquivalence(
|
|
526
|
+
client,
|
|
527
|
+
input.rootPath,
|
|
528
|
+
input.workerCount,
|
|
529
|
+
input.correctnessCases,
|
|
530
|
+
input.equivalence
|
|
531
|
+
);
|
|
532
|
+
if (input.scope !== "correctness")
|
|
533
|
+
await runPerformance(
|
|
534
|
+
client,
|
|
535
|
+
input.rootPath,
|
|
536
|
+
input.workerCount,
|
|
537
|
+
input.performanceCases,
|
|
538
|
+
input.runs,
|
|
539
|
+
input.samples,
|
|
540
|
+
progress
|
|
541
|
+
);
|
|
542
|
+
input.lifecycle.push({
|
|
543
|
+
workers: input.workerCount,
|
|
544
|
+
afterMatrix: client.latestStatus()
|
|
545
|
+
});
|
|
546
|
+
} finally {
|
|
547
|
+
await client.stop();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function runEquivalence(client, rootPath, workers, workloads, results) {
|
|
551
|
+
for (const workload of workloads) {
|
|
552
|
+
progress(`equivalence w${workers} ${workload.id}`);
|
|
553
|
+
const classic = await client.request({
|
|
554
|
+
kind: "preview",
|
|
555
|
+
filePath: workload.filePath,
|
|
556
|
+
rootPath,
|
|
557
|
+
engine: "classic",
|
|
558
|
+
occtTrace: true
|
|
559
|
+
});
|
|
560
|
+
const parallel = await client.request({
|
|
561
|
+
kind: "preview",
|
|
562
|
+
filePath: workload.filePath,
|
|
563
|
+
rootPath,
|
|
564
|
+
engine: "parallel",
|
|
565
|
+
occtTrace: true
|
|
566
|
+
});
|
|
567
|
+
const classicContract = responseContract(classic.response);
|
|
568
|
+
const parallelContract = responseContract(parallel.response);
|
|
569
|
+
results.push({
|
|
570
|
+
workers,
|
|
571
|
+
workload: workload.id,
|
|
572
|
+
classicHash: classicContract.hash,
|
|
573
|
+
parallelHash: parallelContract.hash,
|
|
574
|
+
match: classicContract.hash === parallelContract.hash
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function loadSuiteCases(rootPath) {
|
|
579
|
+
const suitePath = resolve(rootPath, "examples/test_suite/suite.json");
|
|
580
|
+
const suite = JSON.parse(await readFile2(suitePath, "utf8"));
|
|
581
|
+
return suite.cases.map((entry) => ({
|
|
582
|
+
id: entry.id,
|
|
583
|
+
filePath: resolve(dirname2(suitePath), entry.file),
|
|
584
|
+
export: false
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
function performanceWorkloads(rootPath) {
|
|
588
|
+
const paths = [
|
|
589
|
+
"examples/test_suite/13_module_library.bushscad",
|
|
590
|
+
"examples/test_suite/09_assembly_patterns.bushscad",
|
|
591
|
+
"examples/test_suite/04_solids_and_details.bushscad",
|
|
592
|
+
"examples/test_suite/12_imported_assets.bushscad",
|
|
593
|
+
"packages/cli/test/fixtures/parallel-components-p4-loft-benchmark.bushscad",
|
|
594
|
+
"packages/cli/test/fixtures/parallel-components-p5-reuse.bushscad",
|
|
595
|
+
"packages/cli/test/fixtures/parallel-p6-native-cut-benchmark.bushscad"
|
|
596
|
+
];
|
|
597
|
+
return paths.map((path) => ({
|
|
598
|
+
id: basename(path, ".bushscad"),
|
|
599
|
+
filePath: resolve(rootPath, path),
|
|
600
|
+
export: path.includes("09_assembly") || path.includes("p6-native-cut")
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
function positiveInteger(value) {
|
|
604
|
+
const parsed = Number(value);
|
|
605
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
606
|
+
throw new Error(`Expected a positive integer, received '${value}'.`);
|
|
607
|
+
return parsed;
|
|
608
|
+
}
|
|
609
|
+
function readArg(name, fallback) {
|
|
610
|
+
const index = process.argv.indexOf(name);
|
|
611
|
+
return index >= 0 ? process.argv[index + 1] ?? fallback : fallback;
|
|
612
|
+
}
|
|
613
|
+
function progress(message) {
|
|
614
|
+
process.stderr.write(`[P7] ${message}
|
|
615
|
+
`);
|
|
616
|
+
}
|
|
617
|
+
void run().catch((error) => {
|
|
618
|
+
process.stderr.write(
|
|
619
|
+
`${error instanceof Error ? error.stack : String(error)}
|
|
620
|
+
`
|
|
621
|
+
);
|
|
622
|
+
process.exitCode = 1;
|
|
623
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@bushscad/cli",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bushscad": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./protocol": "./src/dto/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "node scripts/build.mjs",
|
|
14
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"prepack": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@bushscad/compiler": "^0.3.0",
|
|
20
|
+
"@bushscad/ir": "^0.3.0",
|
|
21
|
+
"opencascade.js": ">=2.0.0-0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"esbuild": "^0.28.0",
|
|
25
|
+
"typescript": "^5.4.0",
|
|
26
|
+
"vitest": "^4.1.5"
|
|
27
|
+
},
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"description": "The command-line compiler and service interface for BushSCAD. The published package includes the bundled runtime and fonts; OpenCascade is installed as a package dependency.",
|
|
30
|
+
"homepage": "https://bushscad.byaidan.com/",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/AidanBoissonneault/BushSCAD-Language.git",
|
|
34
|
+
"directory": "packages/cli"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/AidanBoissonneault/BushSCAD-Language/issues"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist/**",
|
|
45
|
+
"src/dto/**",
|
|
46
|
+
"src/service/parallel/worker-protocol.ts",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"README.md"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author Aidan Boissonneault
|
|
3
|
+
* @file bufferDto.ts
|
|
4
|
+
* @purpose Provides buffer dto behavior for the dto in cli package.
|
|
5
|
+
* @last-edited October 6, 2026
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BufferDto } from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converts binary export buffers into base64 DTO format.
|
|
12
|
+
* Buffers must be serialized safely for IPC/service transport.
|
|
13
|
+
*/
|
|
14
|
+
export function bufferToDto(buffer: {
|
|
15
|
+
name: string;
|
|
16
|
+
data: ArrayBuffer;
|
|
17
|
+
}): BufferDto {
|
|
18
|
+
return {
|
|
19
|
+
name: buffer.name,
|
|
20
|
+
dataBase64: Buffer.from(buffer.data).toString('base64'),
|
|
21
|
+
};
|
|
22
|
+
}
|