@fraczak/k 6.3.1 → 6.3.2
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/a.klib +1 -1
- package/a.kvm +142676 -0
- package/a.wasm +0 -0
- package/benchmark-results.html +532 -0
- package/benchmark-results.json +480 -0
- package/kvm.mjs +78 -11
- package/package.json +4 -1
- package/scripts/benchmark.mjs +393 -0
- package/scripts/git-hooks/pre-commit +10 -0
- package/scripts/render-benchmark.mjs +343 -0
- package/scripts/run-tests.mjs +1 -0
- package/tests/integration.sh +7 -0
- package/tests/test-arithmetics.mjs +95 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { performance } from "node:perf_hooks";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
codes,
|
|
13
|
+
createState,
|
|
14
|
+
evaluateInput,
|
|
15
|
+
executeKVM,
|
|
16
|
+
lowerToKVM,
|
|
17
|
+
parseFloat64,
|
|
18
|
+
run,
|
|
19
|
+
run_converged,
|
|
20
|
+
valueForCode,
|
|
21
|
+
Value
|
|
22
|
+
} from "../backend-api.mjs";
|
|
23
|
+
import { parse as parseIntValue } from "../codecs/int.mjs";
|
|
24
|
+
|
|
25
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
26
|
+
const root = path.resolve(path.dirname(scriptPath), "..");
|
|
27
|
+
const resultsPath = path.join(root, "benchmark-results.json");
|
|
28
|
+
|
|
29
|
+
const bigA = "1234567890".repeat(22);
|
|
30
|
+
const bigB = "9876543210".repeat(22);
|
|
31
|
+
const gcdFactor = 1234567890123456789012345678901234567890n;
|
|
32
|
+
const gcdA = (gcdFactor * 123456789012345678901234567890123456789012345678901n).toString();
|
|
33
|
+
const gcdB = (gcdFactor * 987654321098765432109876543210987654321098765432109n).toString();
|
|
34
|
+
|
|
35
|
+
const cases = [
|
|
36
|
+
{ name: "ieee.add", suite: "ieee", rel: "add", input: { x: "0.5", y: "0.25" } },
|
|
37
|
+
{ name: "ieee.sub", suite: "ieee", rel: "sub", input: { x: "2", y: "4" } },
|
|
38
|
+
{ name: "ieee.mul", suite: "ieee", rel: "mul", input: { x: "-2", y: "4" } },
|
|
39
|
+
{ name: "ieee.div", suite: "ieee", rel: "div", input: { x: "1", y: "2" } },
|
|
40
|
+
{ name: "int.times.220d", suite: "arithmetics", rel: "times", input: { shape: "int-pair", x: bigA, y: bigB } },
|
|
41
|
+
{ name: "int.gcd.90d", suite: "arithmetics", rel: "gcd", input: { shape: "bits-pair", x: gcdA, y: gcdB } },
|
|
42
|
+
{ name: "int.factorial.101", suite: "arithmetics", rel: "factorial", input: { shape: "int", x: "101" } },
|
|
43
|
+
{ name: "int.factorial.242", suite: "arithmetics", rel: "factorial", input: { shape: "int", x: "242" } },
|
|
44
|
+
{ name: "int.factorial.1174", suite: "arithmetics", rel: "factorial", input: { shape: "int", x: "1174" } }
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const modes = [
|
|
48
|
+
{ name: "Native JS (Envelope-Aware)", kind: "native-aware" },
|
|
49
|
+
{ name: "Native JS (Envelope-Free)", kind: "native-free" },
|
|
50
|
+
{ name: "kVM Interpreter (Env-Free)", kind: "kvm-free" }
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function csvEnv(name, fallback) {
|
|
54
|
+
return (process.env[name] || fallback)
|
|
55
|
+
.split(",")
|
|
56
|
+
.map(item => item.trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function selectedCases() {
|
|
61
|
+
const selected = csvEnv("BENCHMARK_CASES", cases.map(testCase => testCase.name).join(","));
|
|
62
|
+
const byName = new Map(cases.map(testCase => [testCase.name, testCase]));
|
|
63
|
+
return selected.map((name) => {
|
|
64
|
+
const testCase = byName.get(name);
|
|
65
|
+
if (!testCase) throw new Error(`Unknown benchmark case '${name}'`);
|
|
66
|
+
return testCase;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function selectedModes() {
|
|
71
|
+
const selected = csvEnv("BENCHMARK_MODES", modes.map(mode => mode.name).join(","));
|
|
72
|
+
const byName = new Map(modes.map(mode => [mode.name, mode]));
|
|
73
|
+
return selected.map((name) => {
|
|
74
|
+
const mode = byName.get(name);
|
|
75
|
+
if (!mode) throw new Error(`Unknown benchmark mode '${name}'`);
|
|
76
|
+
return mode;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mean(values) {
|
|
81
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function maxRssMiB(maxRSS) {
|
|
85
|
+
return process.platform === "darwin" ? maxRSS / 1024 / 1024 : maxRSS / 1024;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function digestValue(value) {
|
|
89
|
+
const json = JSON.stringify(value);
|
|
90
|
+
return {
|
|
91
|
+
digest: crypto.createHash("sha256").update(json).digest("hex").slice(0, 16),
|
|
92
|
+
bytes: Buffer.byteLength(json)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatMs(value) {
|
|
97
|
+
return value == null ? "-" : value.toFixed(2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatMiB(value) {
|
|
101
|
+
return value == null ? "-" : value.toFixed(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pad(value, width) {
|
|
105
|
+
const text = String(value);
|
|
106
|
+
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function prepareState(suite) {
|
|
110
|
+
const state = createState();
|
|
111
|
+
const source = suite === "ieee" ? "Examples/ieee.k" : "Examples/arithmetics.k";
|
|
112
|
+
await evaluateInput(`:load ${path.join(root, source)}`, state);
|
|
113
|
+
return state;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ieeeInput(testCase, state) {
|
|
117
|
+
const float64Hash = state.typeAliases.float64;
|
|
118
|
+
const floatPairHash = state.typeAliases.float_pair;
|
|
119
|
+
const float64 = text => valueForCode(parseFloat64(text), float64Hash, codes.find);
|
|
120
|
+
return valueForCode(Value.product({
|
|
121
|
+
x: float64(testCase.input.x),
|
|
122
|
+
y: float64(testCase.input.y)
|
|
123
|
+
}), floatPairHash, codes.find);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function arithmeticInput(testCase) {
|
|
127
|
+
const { shape } = testCase.input;
|
|
128
|
+
if (shape === "int") {
|
|
129
|
+
return parseIntValue(testCase.input.x);
|
|
130
|
+
}
|
|
131
|
+
if (shape === "int-pair") {
|
|
132
|
+
return Value.product({
|
|
133
|
+
x: parseIntValue(testCase.input.x),
|
|
134
|
+
y: parseIntValue(testCase.input.y)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (shape === "bits-pair") {
|
|
138
|
+
return Value.product({
|
|
139
|
+
x: parseIntValue(testCase.input.x).value,
|
|
140
|
+
y: parseIntValue(testCase.input.y).value
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`Unknown arithmetic input shape '${shape}'`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function prepareRelation(testCase, state) {
|
|
147
|
+
const hash = state.relAliases[testCase.rel];
|
|
148
|
+
if (!hash) throw new Error(`Relation '${testCase.rel}' not found`);
|
|
149
|
+
const relDef = state.rels[hash];
|
|
150
|
+
if (!relDef) throw new Error(`Relation hash '${hash}' not found`);
|
|
151
|
+
return {
|
|
152
|
+
relDef,
|
|
153
|
+
kvmFunc: lowerToKVM(relDef, testCase.rel)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function runOnce(job, prepared) {
|
|
158
|
+
const { relDef, kvmFunc, inputVal, state } = prepared;
|
|
159
|
+
run.defs = state;
|
|
160
|
+
run_converged.defs = state;
|
|
161
|
+
|
|
162
|
+
if (job.mode.kind === "native-aware") {
|
|
163
|
+
return run(codes.find, relDef.def, inputVal, relDef.typePatternGraph);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (job.mode.kind === "native-free") {
|
|
167
|
+
return run_converged(codes.find, relDef.def, inputVal, relDef.typePatternGraph);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (job.mode.kind === "kvm-free") {
|
|
171
|
+
return executeKVM(kvmFunc, inputVal, {
|
|
172
|
+
rels: state.rels,
|
|
173
|
+
findCode: codes.find,
|
|
174
|
+
options: { envelopeFree: true }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error(`Unknown benchmark mode '${job.mode.name}'`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function workerMain() {
|
|
182
|
+
const job = JSON.parse(process.env.K_BENCHMARK_JOB);
|
|
183
|
+
const setupStartedAt = performance.now();
|
|
184
|
+
const state = await prepareState(job.case.suite);
|
|
185
|
+
const relation = prepareRelation(job.case, state);
|
|
186
|
+
const inputVal = job.case.suite === "ieee" ? ieeeInput(job.case, state) : arithmeticInput(job.case);
|
|
187
|
+
const prepared = { ...relation, inputVal, state };
|
|
188
|
+
const setupMs = performance.now() - setupStartedAt;
|
|
189
|
+
|
|
190
|
+
const samples = [];
|
|
191
|
+
let lastResult;
|
|
192
|
+
const runStartedHeap = process.memoryUsage().heapUsed;
|
|
193
|
+
for (let i = 0; i < job.samples; i++) {
|
|
194
|
+
const startedAt = performance.now();
|
|
195
|
+
lastResult = runOnce(job, prepared);
|
|
196
|
+
samples.push(performance.now() - startedAt);
|
|
197
|
+
if (lastResult === undefined) {
|
|
198
|
+
throw new Error("benchmark operation returned undefined");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const resultInfo = digestValue(lastResult);
|
|
203
|
+
const memory = process.memoryUsage();
|
|
204
|
+
const usage = process.resourceUsage();
|
|
205
|
+
process.stdout.write(JSON.stringify({
|
|
206
|
+
status: "ok",
|
|
207
|
+
mode: job.mode.name,
|
|
208
|
+
case: job.case.name,
|
|
209
|
+
samples,
|
|
210
|
+
avgMs: mean(samples),
|
|
211
|
+
minMs: Math.min(...samples),
|
|
212
|
+
maxMs: Math.max(...samples),
|
|
213
|
+
setupMs,
|
|
214
|
+
maxRssMiB: maxRssMiB(usage.maxRSS),
|
|
215
|
+
heapUsedMiB: memory.heapUsed / 1024 / 1024,
|
|
216
|
+
heapDeltaMiB: (memory.heapUsed - runStartedHeap) / 1024 / 1024,
|
|
217
|
+
resultDigest: resultInfo.digest,
|
|
218
|
+
resultBytes: resultInfo.bytes
|
|
219
|
+
}) + "\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function spawnJob(job, timeoutMs) {
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
const child = spawn(process.execPath, [scriptPath, "--worker"], {
|
|
225
|
+
cwd: root,
|
|
226
|
+
env: {
|
|
227
|
+
...process.env,
|
|
228
|
+
K_BENCHMARK_WORKER: "1",
|
|
229
|
+
K_BENCHMARK_JOB: JSON.stringify(job)
|
|
230
|
+
},
|
|
231
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let stdout = "";
|
|
235
|
+
let stderr = "";
|
|
236
|
+
let timedOut = false;
|
|
237
|
+
const timer = setTimeout(() => {
|
|
238
|
+
timedOut = true;
|
|
239
|
+
child.kill("SIGTERM");
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
242
|
+
}, 1000).unref();
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
|
|
245
|
+
child.stdout.on("data", chunk => { stdout += chunk.toString("utf8"); });
|
|
246
|
+
child.stderr.on("data", chunk => { stderr += chunk.toString("utf8"); });
|
|
247
|
+
child.on("close", (code, signal) => {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
if (timedOut) {
|
|
250
|
+
resolve({ status: "timeout", mode: job.mode.name, case: job.case.name, timeoutMs });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (code !== 0) {
|
|
254
|
+
resolve({
|
|
255
|
+
status: "failed",
|
|
256
|
+
mode: job.mode.name,
|
|
257
|
+
case: job.case.name,
|
|
258
|
+
code,
|
|
259
|
+
signal,
|
|
260
|
+
stderr: stderr.trim() || stdout.trim()
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
resolve(JSON.parse(stdout.trim().split("\n").at(-1)));
|
|
266
|
+
} catch (error) {
|
|
267
|
+
resolve({
|
|
268
|
+
status: "failed",
|
|
269
|
+
mode: job.mode.name,
|
|
270
|
+
case: job.case.name,
|
|
271
|
+
stderr: `Could not parse worker output: ${error.message}\n${stdout}\n${stderr}`
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function runPool(jobs, concurrency, timeoutMs) {
|
|
279
|
+
const results = [];
|
|
280
|
+
let next = 0;
|
|
281
|
+
|
|
282
|
+
async function worker() {
|
|
283
|
+
while (next < jobs.length) {
|
|
284
|
+
const job = jobs[next++];
|
|
285
|
+
const result = await spawnJob(job, timeoutMs);
|
|
286
|
+
results.push(result);
|
|
287
|
+
const label = `${result.mode}/${result.case}`;
|
|
288
|
+
if (result.status === "ok") {
|
|
289
|
+
console.log(`[ok] ${label} ${formatMs(result.avgMs)} ms avg, ${formatMiB(result.maxRssMiB)} MiB max RSS`);
|
|
290
|
+
} else {
|
|
291
|
+
console.log(`[${result.status}] ${label}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function resultDocument(results, metadata) {
|
|
301
|
+
const sorted = [...results].sort((a, b) => a.case.localeCompare(b.case) || a.mode.localeCompare(b.mode));
|
|
302
|
+
return {
|
|
303
|
+
generatedAt: new Date().toISOString(),
|
|
304
|
+
node: process.version,
|
|
305
|
+
platform: `${process.platform} ${process.arch}`,
|
|
306
|
+
cpuCount: os.availableParallelism(),
|
|
307
|
+
samples: metadata.samples,
|
|
308
|
+
timeoutMs: metadata.timeoutMs,
|
|
309
|
+
parallelJobs: metadata.concurrency,
|
|
310
|
+
results: sorted
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function writeResultsFile(document) {
|
|
315
|
+
fs.writeFileSync(resultsPath, JSON.stringify(document, null, 2) + "\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function printSummary(document) {
|
|
319
|
+
console.log("\n==================== K BENCHMARK SUMMARY ====================");
|
|
320
|
+
console.log(`samples/job: ${document.samples}`);
|
|
321
|
+
console.log(`timeout/job: ${(document.timeoutMs / 1000 / 60).toFixed(1)} min`);
|
|
322
|
+
console.log(`parallel jobs: ${document.parallelJobs}`);
|
|
323
|
+
console.log(`results file: ${path.relative(root, resultsPath)}`);
|
|
324
|
+
console.log("-------------------------------------------------------------");
|
|
325
|
+
console.log(`${pad("case", 20)} ${pad("mode", 24)} ${pad("status", 8)} ${pad("avg ms", 10)} ${pad("min", 9)} ${pad("max", 9)} ${pad("rss MiB", 9)} ${pad("heap MiB", 9)} digest`);
|
|
326
|
+
for (const result of document.results) {
|
|
327
|
+
console.log([
|
|
328
|
+
pad(result.case, 20),
|
|
329
|
+
pad(result.mode, 24),
|
|
330
|
+
pad(result.status, 8),
|
|
331
|
+
pad(formatMs(result.avgMs), 10),
|
|
332
|
+
pad(formatMs(result.minMs), 9),
|
|
333
|
+
pad(formatMs(result.maxMs), 9),
|
|
334
|
+
pad(formatMiB(result.maxRssMiB), 9),
|
|
335
|
+
pad(formatMiB(result.heapUsedMiB), 9),
|
|
336
|
+
result.resultDigest || ""
|
|
337
|
+
].join(" "));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const failures = document.results.filter(result => result.status !== "ok");
|
|
341
|
+
if (failures.length > 0) {
|
|
342
|
+
console.log("-------------------------------------------------------------");
|
|
343
|
+
console.log("Failures:");
|
|
344
|
+
for (const failure of failures) {
|
|
345
|
+
console.log(`- ${failure.mode}/${failure.case}: ${failure.status}`);
|
|
346
|
+
if (failure.stderr) console.log(failure.stderr.split("\n").slice(0, 12).map(line => ` ${line}`).join("\n"));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log("=============================================================\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function main() {
|
|
353
|
+
if (process.env.K_BENCHMARK_WORKER === "1" || process.argv.includes("--worker")) {
|
|
354
|
+
await workerMain();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const selected = selectedCases();
|
|
359
|
+
const selectedBenchmarkModes = selectedModes();
|
|
360
|
+
const samples = process.env.BENCHMARK_SAMPLES ? Number(process.env.BENCHMARK_SAMPLES) : 3;
|
|
361
|
+
const timeoutMs = process.env.BENCHMARK_TIMEOUT_MS ? Number(process.env.BENCHMARK_TIMEOUT_MS) : 2 * 60 * 1000;
|
|
362
|
+
const requestedJobs = process.env.BENCHMARK_JOBS ? Number(process.env.BENCHMARK_JOBS) : os.availableParallelism();
|
|
363
|
+
const concurrency = Math.max(1, Math.min(requestedJobs, os.availableParallelism(), selected.length * selectedBenchmarkModes.length));
|
|
364
|
+
|
|
365
|
+
if (!Number.isInteger(samples) || samples <= 0) throw new Error("BENCHMARK_SAMPLES must be a positive integer");
|
|
366
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) throw new Error("BENCHMARK_TIMEOUT_MS must be a positive integer");
|
|
367
|
+
|
|
368
|
+
const jobs = [];
|
|
369
|
+
for (const testCase of selected) {
|
|
370
|
+
for (const mode of selectedBenchmarkModes) {
|
|
371
|
+
jobs.push({ case: testCase, mode, samples });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log("==> k benchmark");
|
|
376
|
+
console.log(`cases: ${selected.map(testCase => testCase.name).join(", ")}`);
|
|
377
|
+
console.log(`modes: ${selectedBenchmarkModes.map(mode => mode.name).join(", ")}`);
|
|
378
|
+
console.log(`jobs: ${jobs.length}; parallelism: ${concurrency}; timeout/job: ${timeoutMs} ms`);
|
|
379
|
+
|
|
380
|
+
const results = await runPool(jobs, concurrency, timeoutMs);
|
|
381
|
+
const document = resultDocument(results, { samples, timeoutMs, concurrency });
|
|
382
|
+
writeResultsFile(document);
|
|
383
|
+
printSummary(document);
|
|
384
|
+
|
|
385
|
+
if (process.env.BENCHMARK_STRICT === "1" && results.some(result => result.status !== "ok")) {
|
|
386
|
+
process.exitCode = 1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
main().catch((error) => {
|
|
391
|
+
console.error(error.stack || error.message || String(error));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
});
|