@fraczak/k 6.2.4 → 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/kvm.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import { argv, stdin, exit, stdout } from "node:process";
4
- import { Product, Variant, composePattern, withPattern } from "./Value.mjs";
4
+ import { Value, composePattern, withPattern, isProduct, isVariant } from "./Value.mjs";
5
5
  import { exportPatternGraph } from "./codecs/runtime/codec.mjs";
6
6
  import { patternToPropertyList } from "./codecs/runtime/pattern-json.mjs";
7
7
  import {
@@ -17,6 +17,42 @@ import { decodeObject, loadLibrary } from "./object.mjs";
17
17
  import codes from "./codes.mjs";
18
18
  import { isMainEntrypoint } from "./codecs/runtime/cli-entry.mjs";
19
19
 
20
+ function isFile(filePath) {
21
+ try {
22
+ return fs.statSync(filePath).isFile();
23
+ } catch (error) {
24
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return false;
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ function buildExportPreamble(exports, libraries) {
30
+ if (exports.length === 0) return "";
31
+ const aliasMap = {};
32
+ for (const lib of libraries) {
33
+ for (const [name, hash] of Object.entries(lib.relAlias || {})) {
34
+ if (name !== "__main__") aliasMap[name] = hash;
35
+ }
36
+ for (const [hash, entry] of Object.entries(lib.meta || {})) {
37
+ if (entry?.type !== "rel") continue;
38
+ for (const origin of entry?.origins || []) {
39
+ if (origin?.name && origin.name !== "__main__") {
40
+ aliasMap[origin.name] = hash;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ const lines = [];
46
+ for (const spec of exports) {
47
+ const [libName, localName] = spec.includes(":") ? spec.split(":", 2) : [spec, spec];
48
+ const hash = aliasMap[libName];
49
+ if (!hash) throw new Error(`--export: '${libName}' not found in loaded libraries`);
50
+ const body = hash.startsWith("@") ? hash.slice(1) : hash;
51
+ lines.push(`${localName} = @${body};`);
52
+ }
53
+ return lines.join("\n") + "\n";
54
+ }
55
+
20
56
  class KVMBuilder {
21
57
  constructor(typePatternGraph) {
22
58
  this.typePatternGraph = typePatternGraph;
@@ -281,7 +317,7 @@ function executeInstruction(inst, registers, context) {
281
317
  }
282
318
  case "project_field": {
283
319
  const val = registers.get(inst.src);
284
- if (!(val instanceof Product)) return undefined;
320
+ if (!isProduct(val)) return undefined;
285
321
  const fieldVal = val.product[inst.label];
286
322
  if (fieldVal === undefined) return undefined;
287
323
 
@@ -295,7 +331,7 @@ function executeInstruction(inst, registers, context) {
295
331
  }
296
332
  case "project_variant": {
297
333
  const val = registers.get(inst.src);
298
- if (!(val instanceof Variant) || val.tag !== inst.tag) return undefined;
334
+ if (!isVariant(val) || val.tag !== inst.tag) return undefined;
299
335
 
300
336
  if (options.envelopeFree) {
301
337
  registers.set(inst.dest, val.value);
@@ -308,11 +344,11 @@ function executeInstruction(inst, registers, context) {
308
344
  case "make_variant": {
309
345
  const val = registers.get(inst.src);
310
346
  if (options.envelopeFree) {
311
- registers.set(inst.dest, new Variant(inst.tag, val));
347
+ registers.set(inst.dest, Value.variant(inst.tag, val));
312
348
  } else {
313
349
  registers.set(
314
350
  inst.dest,
315
- new Variant(inst.tag, val, composePattern("open-union", [[inst.tag, val.pattern]]))
351
+ Value.variant(inst.tag, val, composePattern("open-union", [[inst.tag, val.pattern]]))
316
352
  );
317
353
  }
318
354
  return { type: "continue" };
@@ -375,13 +411,13 @@ function executeInstruction(inst, registers, context) {
375
411
  }
376
412
  let productValue;
377
413
  if (options.envelopeFree) {
378
- productValue = new Product(result);
414
+ productValue = Value.product(result);
379
415
  } else {
380
416
  const staticOutputPattern = inst.pattern;
381
417
  if (staticOutputPattern) {
382
- productValue = new Product(result, staticOutputPattern);
418
+ productValue = Value.product(result, staticOutputPattern);
383
419
  } else {
384
- productValue = new Product(result, composePattern("closed-product", patternEntries));
420
+ productValue = Value.product(result, composePattern("closed-product", patternEntries));
385
421
  }
386
422
  }
387
423
  registers.set(inst.dest, productValue);
@@ -428,9 +464,10 @@ export default {
428
464
 
429
465
  function usage() {
430
466
  const prog = argv[1] || "kvm.mjs";
431
- console.error(`Usage: node ${prog} [ options ] ( k-expr | -k file ) [ input-file ]`);
467
+ console.error(`Usage: node ${prog} [ options ] ( k-expr | input-file ) [ input-file ]`);
432
468
  console.error("Options:");
433
469
  console.error(" --lib file Load one .klib dependency before compiling.");
470
+ console.error(" --export spec Export a library alias into scope. 'name' or 'libname:localname'. May be repeated.");
434
471
  console.error(" --envelope-free Run the interpreter in envelope-free mode.");
435
472
  console.error(" -h, --help Show this help.");
436
473
  }
@@ -444,6 +481,7 @@ async function main() {
444
481
 
445
482
  let envelopeFree = false;
446
483
  const libraries = [];
484
+ const exports = [];
447
485
 
448
486
  // Parse options
449
487
  while (args.length > 0) {
@@ -457,22 +495,44 @@ async function main() {
457
495
  if (libraries.length > 0) throw new Error("--lib may be specified at most once");
458
496
  const libBuffer = fs.readFileSync(libPath);
459
497
  libraries.push(loadLibrary(decodeObject(libBuffer)));
498
+ } else if (args[0] === "--export") {
499
+ args.shift();
500
+ const spec = args.shift();
501
+ if (!spec) throw new Error("--export requires a spec argument");
502
+ exports.push(spec);
503
+ } else if (args[0].startsWith("--")) {
504
+ throw new Error(`Unknown option: ${args[0]}`);
460
505
  } else {
461
506
  break;
462
507
  }
463
508
  }
464
509
 
465
- let kScriptStr = (function (arg) {
510
+ const programInput = (function (arg) {
466
511
  if (arg == null) {
467
512
  throw new Error("Missing script argument");
468
513
  }
469
514
  if (arg === "-k") {
470
- const fileArg = args.shift();
471
- if (!fileArg) throw new Error("-k requires a file argument");
472
- return fs.readFileSync(fileArg, "utf8");
473
- } else {
474
- return arg;
515
+ throw new Error("-k is no longer supported; pass the source/object file path directly");
516
+ }
517
+ if (isFile(arg)) {
518
+ const buffer = fs.readFileSync(arg);
519
+ try {
520
+ const object = decodeObject(buffer);
521
+ if (object.main == null) {
522
+ throw new Error("Cannot run a .klib library without a main relation; load it with --lib.");
523
+ }
524
+ return { kind: "object", defs: { rels: object.rels }, main: object.main };
525
+ } catch (error) {
526
+ if (error.message === "Cannot run a .klib library without a main relation; load it with --lib.") {
527
+ throw error;
528
+ }
529
+ return {
530
+ kind: "source",
531
+ source: buildExportPreamble(exports, libraries) + buffer.toString("utf8")
532
+ };
533
+ }
475
534
  }
535
+ return { kind: "source", source: buildExportPreamble(exports, libraries) + arg };
476
536
  })(args.shift());
477
537
 
478
538
  const inputStream = (function (arg) {
@@ -482,13 +542,20 @@ async function main() {
482
542
  return fs.createReadStream(arg);
483
543
  })(args.shift());
484
544
 
485
- const defs = annotate(kScriptStr, { libraries });
486
- const mainRel = defs.rels.__main__;
545
+ if (args.length > 0) {
546
+ throw new Error(`Unexpected argument: ${args[0]}`);
547
+ }
548
+
549
+ const defs = programInput.kind === "object"
550
+ ? programInput.defs
551
+ : annotate(programInput.source, { libraries });
552
+ const mainRelName = programInput.kind === "object" ? programInput.main : "__main__";
553
+ const mainRel = defs.rels[mainRelName];
487
554
  if (!mainRel) {
488
- throw new Error("No main relation (__main__) defined in script");
555
+ throw new Error(`No main relation (${mainRelName}) defined in script`);
489
556
  }
490
557
 
491
- const kvmFunc = lowerToKVM(mainRel, "__main__");
558
+ const kvmFunc = lowerToKVM(mainRel, mainRelName);
492
559
 
493
560
  const buffer = [];
494
561
  inputStream.on("data", (data) => buffer.push(Buffer.isBuffer(data) ? data : Buffer.from(data)));
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@fraczak/k",
3
- "version": "6.2.4",
3
+ "version": "6.3.2",
4
4
  "description": "k — a language for composing first-order partial functions over algebraic data types (products and tagged unions); includes runtime, type derivation, REPL, and codec toolchain",
5
5
  "main": "index.mjs",
6
6
  "scripts": {
7
7
  "prepare": "jiwson -m es6 parser.jison -o parser.mjs; jiwson -m es6 valueParser.jison -o valueParser.mjs; ",
8
8
  "test": "node scripts/run-tests.mjs",
9
+ "benchmark": "node scripts/benchmark.mjs",
10
+ "benchmark:html": "node scripts/render-benchmark.mjs",
11
+ "hooks:install": "git config core.hooksPath scripts/git-hooks",
9
12
  "perf:idempotency": "node scripts/benchmark-idempotency.mjs",
10
13
  "deps:graph": "node scripts/generate-dependency-graph.mjs"
11
14
  },
package/repl.mjs CHANGED
@@ -10,7 +10,7 @@ import { argv, exit, stdin, stdout } from "node:process";
10
10
  import { annotate, parse } from "./index.mjs";
11
11
  import run from "./run.mjs";
12
12
  import codes from "./codes.mjs";
13
- import { Product } from "./Value.mjs";
13
+ import { Value } from "./Value.mjs";
14
14
  import { patterns2filters, prettyCode, prettyRel } from "./pretty.mjs";
15
15
  import {
16
16
  compileObject,
@@ -50,7 +50,7 @@ function cloneJSON(value) {
50
50
  }
51
51
 
52
52
  function emptyValue() {
53
- return new Product({}, [["closed-product", []]]);
53
+ return Value.product({}, [["closed-product", []]]);
54
54
  }
55
55
 
56
56
  function createState() {
package/run.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import assert from "assert";
2
- import { Product, Variant, edgeSubpattern, composePattern, withPattern } from "./Value.mjs"
2
+ import { Value, edgeSubpattern, composePattern, withPattern, isProduct, isVariant } from "./Value.mjs"
3
3
  import { exportPatternGraph, intersectPropertyListPatterns } from "./codecs/runtime/codec.mjs";
4
4
  import { patternToPropertyList } from "./codecs/runtime/pattern-json.mjs";
5
5
 
@@ -261,14 +261,14 @@ function compiledExp(findCode, exp, typePatternGraph) {
261
261
  ` - product envelope: ${patternPreview(dynamicOutputPattern)}`
262
262
  );
263
263
  }
264
- return new Product(result, pattern);
264
+ return Value.product(result, pattern);
265
265
  };
266
266
  break;
267
267
  }
268
268
  case "vid":
269
269
  fn = (value) => {
270
270
  if (value === undefined) return undefined;
271
- return new Variant(exp.vid, value, composePattern("open-union", [[exp.vid, value.pattern]]));
271
+ return Value.variant(exp.vid, value, composePattern("open-union", [[exp.vid, value.pattern]]));
272
272
  };
273
273
  break;
274
274
  default:
@@ -375,13 +375,13 @@ function compiledConvergedExp(findCode, exp, typePatternGraph, options = {}) {
375
375
  break;
376
376
  case "dot":
377
377
  fn = (value) => {
378
- if (value === undefined || !(value instanceof Product)) return undefined;
378
+ if (value === undefined || !isProduct(value)) return undefined;
379
379
  return Object.hasOwn(value.product, exp.dot) ? value.product[exp.dot] : undefined;
380
380
  };
381
381
  break;
382
382
  case "div":
383
383
  fn = (value) => {
384
- if (value === undefined || !(value instanceof Variant) || value.tag !== exp.div) return undefined;
384
+ if (value === undefined || !isVariant(value) || value.tag !== exp.div) return undefined;
385
385
  return value.value;
386
386
  };
387
387
  break;
@@ -419,12 +419,12 @@ function compiledConvergedExp(findCode, exp, typePatternGraph, options = {}) {
419
419
  if (r === undefined) return undefined;
420
420
  result[labels[i]] = r;
421
421
  }
422
- return new Product(result);
422
+ return Value.product(result);
423
423
  };
424
424
  break;
425
425
  }
426
426
  case "vid":
427
- fn = (value) => value === undefined ? undefined : new Variant(exp.vid, value);
427
+ fn = (value) => value === undefined ? undefined : Value.variant(exp.vid, value);
428
428
  break;
429
429
  default:
430
430
  assert(false,`Unknown operation: '${exp.op}'`);
@@ -440,7 +440,7 @@ function verify(findCode, code, value) {
440
440
  code = findCode(code);
441
441
  switch (code.code) {
442
442
  case "product":
443
- if (! (value instanceof Product)) return false;
443
+ if (!isProduct(value)) return false;
444
444
  else {
445
445
  const fields = Object.keys(value.product);
446
446
  if (fields.length !== Object.keys(code.product).length) return false;
@@ -449,7 +449,7 @@ function verify(findCode, code, value) {
449
449
  );
450
450
  }
451
451
  case "union":
452
- if (! (value instanceof Product)) return false;
452
+ if (!isProduct(value)) return false;
453
453
  else {
454
454
  const fields = Object.keys(value.product);
455
455
  if (fields.length !== 1) return false;
@@ -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
+ });