eyeleng 1.0.8 → 1.0.9
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/dist/browser/eyeleng.browser.js +112 -78
- package/eyeleng.js +112 -78
- package/package.json +5 -2
- package/src/analyze.js +8 -10
- package/src/builtins.js +3 -1
- package/src/engine.js +36 -33
- package/src/parser.js +1 -1
- package/src/store.js +2 -2
- package/src/term.js +4 -2
- package/src/tokenizer.js +58 -29
- package/test/perf-baseline.json +39 -0
- package/tools/perf-bench.js +234 -0
package/src/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { tripleKey, termKey, termEquals
|
|
3
|
+
const { tripleKey, termKey, termEquals } = require('./term.js');
|
|
4
4
|
|
|
5
5
|
class TripleStore {
|
|
6
6
|
constructor(triples = []) {
|
|
@@ -109,7 +109,7 @@ function smallerValues(left, right) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function normalizeTriple(triple) {
|
|
112
|
-
return { s:
|
|
112
|
+
return { s: triple.s, p: triple.p, o: triple.o };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function bindingKey(binding) {
|
package/src/term.js
CHANGED
|
@@ -17,11 +17,13 @@ function iri(value) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function variable(name) {
|
|
20
|
-
|
|
20
|
+
const value = String(name);
|
|
21
|
+
return { type: 'var', value: value[0] === '?' || value[0] === '$' ? value.slice(1) : value };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function blankNode(value) {
|
|
24
|
-
|
|
25
|
+
const label = String(value);
|
|
26
|
+
return { type: 'blank', value: label.startsWith('_:') ? label.slice(2) : label };
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
function literal(value, datatype = null, lang = null, langDir = null) {
|
package/src/tokenizer.js
CHANGED
|
@@ -20,7 +20,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
20
20
|
|
|
21
21
|
function current() { return source[i]; }
|
|
22
22
|
function peek(n = 1) { return source[i + n]; }
|
|
23
|
-
function startsWith(text) { return source.
|
|
23
|
+
function startsWith(text) { return source.startsWith(text, i); }
|
|
24
24
|
function advance() {
|
|
25
25
|
const ch = source[i++];
|
|
26
26
|
if (ch === '\n') { line += 1; column = 1; }
|
|
@@ -28,7 +28,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
28
28
|
return ch;
|
|
29
29
|
}
|
|
30
30
|
function token(type, value, startLine, startColumn, extra = {}) {
|
|
31
|
-
tokens.push({ type, value, line: startLine, column: startColumn,
|
|
31
|
+
tokens.push({ type, value, line: startLine, column: startColumn, ...extra });
|
|
32
32
|
}
|
|
33
33
|
function syntax(message, startLine, startColumn) {
|
|
34
34
|
throw new SyntaxErrorWithLocation(message, { line: startLine, column: startColumn, filename });
|
|
@@ -36,19 +36,21 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
36
36
|
|
|
37
37
|
function readNumericLiteral() {
|
|
38
38
|
let value = '';
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const start = i;
|
|
40
|
+
while (i < source.length && isDigitCode(source.charCodeAt(i))) { i += 1; column += 1; }
|
|
41
|
+
if (source[i] === '.' && isDigitCode(source.charCodeAt(i + 1))) {
|
|
42
|
+
i += 1; column += 1;
|
|
43
|
+
while (i < source.length && isDigitCode(source.charCodeAt(i))) { i += 1; column += 1; }
|
|
43
44
|
}
|
|
45
|
+
value = source.slice(start, i);
|
|
44
46
|
if (current() === 'e' || current() === 'E') {
|
|
45
47
|
const saveI = i;
|
|
46
48
|
const saveLine = line;
|
|
47
49
|
const saveColumn = column;
|
|
48
50
|
let exponent = advance();
|
|
49
51
|
if (current() === '+' || current() === '-') exponent += advance();
|
|
50
|
-
if (
|
|
51
|
-
while (i < source.length &&
|
|
52
|
+
if (isDigitCode(source.charCodeAt(i))) {
|
|
53
|
+
while (i < source.length && isDigitCode(source.charCodeAt(i))) exponent += advance();
|
|
52
54
|
value += exponent;
|
|
53
55
|
} else {
|
|
54
56
|
i = saveI;
|
|
@@ -66,7 +68,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
66
68
|
const length = esc === 'u' ? 4 : 8;
|
|
67
69
|
let hex = '';
|
|
68
70
|
for (let j = 0; j < length; j += 1) {
|
|
69
|
-
if (
|
|
71
|
+
if (!isHexCode(source.charCodeAt(i))) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
|
|
70
72
|
hex += advance();
|
|
71
73
|
}
|
|
72
74
|
const codePoint = Number.parseInt(hex, 16);
|
|
@@ -85,7 +87,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
85
87
|
const length = esc === 'u' ? 4 : 8;
|
|
86
88
|
let hex = '';
|
|
87
89
|
for (let j = 0; j < length; j += 1) {
|
|
88
|
-
if (
|
|
90
|
+
if (!isHexCode(source.charCodeAt(i))) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
|
|
89
91
|
hex += advance();
|
|
90
92
|
}
|
|
91
93
|
const codePoint = Number.parseInt(hex, 16);
|
|
@@ -99,7 +101,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
99
101
|
|
|
100
102
|
while (i < source.length) {
|
|
101
103
|
const ch = current();
|
|
102
|
-
if (
|
|
104
|
+
if (isWhitespaceCode(source.charCodeAt(i))) { advance(); continue; }
|
|
103
105
|
if (ch === '#') {
|
|
104
106
|
while (i < source.length && current() !== '\n') advance();
|
|
105
107
|
continue;
|
|
@@ -185,18 +187,21 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
if (ch === '@') {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
const wordStart = i;
|
|
191
|
+
advance();
|
|
192
|
+
while (i < source.length && isLangTagCode(source.charCodeAt(i))) { i += 1; column += 1; }
|
|
193
|
+
const value = source.slice(wordStart, i);
|
|
190
194
|
if (!/^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(value)) syntax(`Invalid language tag ${value}`, startLine, startColumn);
|
|
191
195
|
token('word', value, startLine, startColumn);
|
|
192
196
|
continue;
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
if (ch === '?' || ch === '$') {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
const varStart = i;
|
|
201
|
+
advance();
|
|
202
|
+
while (i < source.length && isVarNameCode(source.charCodeAt(i))) { i += 1; column += 1; }
|
|
203
|
+
if (i - varStart === 1) syntax('Expected variable name', startLine, startColumn);
|
|
204
|
+
token('variable', source.slice(varStart + 1, i), startLine, startColumn);
|
|
200
205
|
continue;
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -223,24 +228,27 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
223
228
|
continue;
|
|
224
229
|
}
|
|
225
230
|
|
|
226
|
-
|
|
231
|
+
const wordStart = i;
|
|
227
232
|
while (i < source.length) {
|
|
228
|
-
const c =
|
|
229
|
-
if (c === '\\' &&
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
const c = source[i];
|
|
234
|
+
if (c === '\\' && source[i + 1] !== undefined) {
|
|
235
|
+
i += 2;
|
|
236
|
+
column += 2;
|
|
232
237
|
continue;
|
|
233
238
|
}
|
|
234
|
-
|
|
239
|
+
const code = source.charCodeAt(i);
|
|
240
|
+
if (isWhitespaceCode(code) || '{}()[],;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;
|
|
235
241
|
if (c === '.') {
|
|
236
|
-
const n =
|
|
237
|
-
if (n === undefined ||
|
|
242
|
+
const n = source[i + 1];
|
|
243
|
+
if (n === undefined || isWhitespaceCode(n.charCodeAt(0)) || '{}()[],;|'.includes(n) || '=<>+-*/!^~'.includes(n)) break;
|
|
238
244
|
}
|
|
239
245
|
if (c === '#') break;
|
|
240
|
-
|
|
246
|
+
i += 1;
|
|
247
|
+
column += 1;
|
|
241
248
|
}
|
|
242
|
-
if (
|
|
249
|
+
if (i === wordStart) syntax(`Unexpected character ${JSON.stringify(ch)}`, startLine, startColumn);
|
|
243
250
|
|
|
251
|
+
const value = source.slice(wordStart, i);
|
|
244
252
|
if (/^[+-]?(?:(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+|\d+)$/.test(value)) token('number', Number(value), startLine, startColumn);
|
|
245
253
|
else token('word', value, startLine, startColumn);
|
|
246
254
|
}
|
|
@@ -249,11 +257,32 @@ function tokenize(source, filenameOrOptions = '<input>') {
|
|
|
249
257
|
return tokens;
|
|
250
258
|
}
|
|
251
259
|
|
|
260
|
+
|
|
261
|
+
function isDigitCode(code) {
|
|
262
|
+
return code >= 48 && code <= 57;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isHexCode(code) {
|
|
266
|
+
return (code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isWhitespaceCode(code) {
|
|
270
|
+
return code === 32 || code === 9 || code === 10 || code === 13 || code === 12;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isLangTagCode(code) {
|
|
274
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 45;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isVarNameCode(code) {
|
|
278
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 95 || code === 45;
|
|
279
|
+
}
|
|
280
|
+
|
|
252
281
|
function startsNumericLiteral(source, i) {
|
|
253
282
|
const ch = source[i];
|
|
254
283
|
const next = source[i + 1];
|
|
255
|
-
if (
|
|
256
|
-
if (ch === '.' &&
|
|
284
|
+
if (isDigitCode(ch.charCodeAt(0))) return true;
|
|
285
|
+
if (ch === '.' && next !== undefined && isDigitCode(next.charCodeAt(0))) return true;
|
|
257
286
|
return false;
|
|
258
287
|
}
|
|
259
288
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"generatedBy": "npm run bench:update",
|
|
4
|
+
"note": "Machine-dependent performance baseline. Use npm run bench:update to refresh intentionally.",
|
|
5
|
+
"tolerance": {
|
|
6
|
+
"relative": 1.35,
|
|
7
|
+
"absoluteMs": 1000
|
|
8
|
+
},
|
|
9
|
+
"cases": [
|
|
10
|
+
{
|
|
11
|
+
"name": "deep-taxonomy-100000.srl",
|
|
12
|
+
"file": "examples/deep-taxonomy-100000.srl",
|
|
13
|
+
"repeat": 1,
|
|
14
|
+
"baselineMs": 5427.8,
|
|
15
|
+
"outputBytes": 5068161
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "fibonacci.srl",
|
|
19
|
+
"file": "examples/fibonacci.srl",
|
|
20
|
+
"repeat": 1,
|
|
21
|
+
"baselineMs": 1409.4,
|
|
22
|
+
"outputBytes": 10810713
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "fft32-numeric.srl",
|
|
26
|
+
"file": "examples/fft32-numeric.srl",
|
|
27
|
+
"repeat": 3,
|
|
28
|
+
"baselineMs": 182.2,
|
|
29
|
+
"outputBytes": 466351
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "path-discovery.srl",
|
|
33
|
+
"file": "examples/path-discovery.srl",
|
|
34
|
+
"repeat": 5,
|
|
35
|
+
"baselineMs": 1317.7,
|
|
36
|
+
"outputBytes": 736870
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { performance } = require('node:perf_hooks');
|
|
7
|
+
const { pathToFileURL, fileURLToPath } = require('node:url');
|
|
8
|
+
const { runToString } = require('../src/index.js');
|
|
9
|
+
|
|
10
|
+
const root = path.join(__dirname, '..');
|
|
11
|
+
const baselinePath = path.join(root, 'test', 'perf-baseline.json');
|
|
12
|
+
const DEFAULT_RELATIVE_TOLERANCE = 1.35;
|
|
13
|
+
const DEFAULT_ABSOLUTE_TOLERANCE_MS = 1000;
|
|
14
|
+
|
|
15
|
+
const defaultCases = [
|
|
16
|
+
{ name: 'deep-taxonomy-100000.srl', file: 'examples/deep-taxonomy-100000.srl', repeat: 1 },
|
|
17
|
+
{ name: 'fibonacci.srl', file: 'examples/fibonacci.srl', repeat: 1 },
|
|
18
|
+
{ name: 'fft32-numeric.srl', file: 'examples/fft32-numeric.srl', repeat: 3 },
|
|
19
|
+
{ name: 'path-discovery.srl', file: 'examples/path-discovery.srl', repeat: 5 },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const options = {
|
|
24
|
+
mode: 'check',
|
|
25
|
+
json: false,
|
|
26
|
+
cases: [],
|
|
27
|
+
repeat: null,
|
|
28
|
+
baseline: baselinePath,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
32
|
+
const arg = argv[i];
|
|
33
|
+
if (arg === '--check') options.mode = 'check';
|
|
34
|
+
else if (arg === '--update') options.mode = 'update';
|
|
35
|
+
else if (arg === '--report') options.mode = 'report';
|
|
36
|
+
else if (arg === '--json') options.json = true;
|
|
37
|
+
else if (arg === '--case') {
|
|
38
|
+
i += 1;
|
|
39
|
+
if (i >= argv.length) throw new Error('--case requires a benchmark name or file');
|
|
40
|
+
options.cases.push(argv[i]);
|
|
41
|
+
} else if (arg === '--repeat') {
|
|
42
|
+
i += 1;
|
|
43
|
+
if (i >= argv.length) throw new Error('--repeat requires a positive integer');
|
|
44
|
+
options.repeat = Number(argv[i]);
|
|
45
|
+
if (!Number.isInteger(options.repeat) || options.repeat < 1) throw new Error('--repeat requires a positive integer');
|
|
46
|
+
} else if (arg === '--baseline') {
|
|
47
|
+
i += 1;
|
|
48
|
+
if (i >= argv.length) throw new Error('--baseline requires a path');
|
|
49
|
+
options.baseline = path.resolve(argv[i]);
|
|
50
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
51
|
+
options.help = true;
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Unknown option ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return options;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function help() {
|
|
61
|
+
return `Usage: node tools/perf-bench.js [--check|--update|--report] [options]\n\nRuns selected large examples and checks them against test/perf-baseline.json.\nThis is intentionally separate from npm test so normal correctness tests do not\nfail because of machine-dependent timings.\n\nOptions:\n --check Fail if a benchmark exceeds its budget (default)\n --update Rewrite the baseline with the current timings\n --report Print timings without checking or updating\n --json Print JSON output\n --case NAME Run only a case by name or file; may be repeated\n --repeat N Override per-case repeat count\n --baseline FILE Use a different baseline file\n -h, --help Print this help\n\nEnvironment overrides:\n EYELENG_BENCH_RELATIVE_TOLERANCE default ${DEFAULT_RELATIVE_TOLERANCE}\n EYELENG_BENCH_ABSOLUTE_TOLERANCE_MS default ${DEFAULT_ABSOLUTE_TOLERANCE_MS}\n EYELENG_BENCH_REPEAT override repeat count\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadBaseline(filename) {
|
|
65
|
+
if (!fs.existsSync(filename)) return null;
|
|
66
|
+
return JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function baselineCases(baseline) {
|
|
70
|
+
if (!baseline || !Array.isArray(baseline.cases)) return null;
|
|
71
|
+
return baseline.cases.map((item) => ({
|
|
72
|
+
name: item.name,
|
|
73
|
+
file: item.file,
|
|
74
|
+
repeat: item.repeat || 1,
|
|
75
|
+
baselineMs: item.baselineMs,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function selectCases(allCases, requested) {
|
|
80
|
+
if (requested.length === 0) return allCases;
|
|
81
|
+
return requested.map((name) => {
|
|
82
|
+
const found = allCases.find((item) => item.name === name || item.file === name || path.basename(item.file) === name);
|
|
83
|
+
if (!found) throw new Error(`Unknown benchmark case ${name}`);
|
|
84
|
+
return found;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function importResolver(target) {
|
|
89
|
+
if (!target.startsWith('file:')) throw new Error(`benchmark import resolver only supports file: imports, got ${target}`);
|
|
90
|
+
const filename = fileURLToPath(target);
|
|
91
|
+
return {
|
|
92
|
+
source: fs.readFileSync(filename, 'utf8'),
|
|
93
|
+
options: { filename, baseIRI: pathToFileURL(filename).href },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function runOptions(filename) {
|
|
98
|
+
return {
|
|
99
|
+
filename,
|
|
100
|
+
baseIRI: pathToFileURL(filename).href,
|
|
101
|
+
importResolver,
|
|
102
|
+
syntax: filename.endsWith('.ttl') ? 'rdf' : undefined,
|
|
103
|
+
now: new Date('2026-05-15T12:34:56Z'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function median(values) {
|
|
108
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
109
|
+
const middle = Math.floor(sorted.length / 2);
|
|
110
|
+
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function roundMs(ms) {
|
|
114
|
+
return Math.round(ms * 10) / 10;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function runCase(testCase, repeatOverride) {
|
|
118
|
+
const filename = path.join(root, testCase.file);
|
|
119
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
120
|
+
const repeat = repeatOverride || Number(process.env.EYELENG_BENCH_REPEAT) || testCase.repeat || 1;
|
|
121
|
+
const samples = [];
|
|
122
|
+
let outputBytes = 0;
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < repeat; i += 1) {
|
|
125
|
+
if (global.gc) global.gc();
|
|
126
|
+
const start = performance.now();
|
|
127
|
+
const output = runToString(source, runOptions(filename));
|
|
128
|
+
const elapsed = performance.now() - start;
|
|
129
|
+
outputBytes = Buffer.byteLength(output, 'utf8');
|
|
130
|
+
samples.push(elapsed);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
name: testCase.name,
|
|
135
|
+
file: testCase.file,
|
|
136
|
+
repeat,
|
|
137
|
+
samplesMs: samples.map(roundMs),
|
|
138
|
+
ms: roundMs(median(samples)),
|
|
139
|
+
outputBytes,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toleranceFromEnv(baseline = {}) {
|
|
144
|
+
const fromBaseline = baseline.tolerance || {};
|
|
145
|
+
const relative = Number(process.env.EYELENG_BENCH_RELATIVE_TOLERANCE || fromBaseline.relative || DEFAULT_RELATIVE_TOLERANCE);
|
|
146
|
+
const absoluteMs = Number(process.env.EYELENG_BENCH_ABSOLUTE_TOLERANCE_MS || fromBaseline.absoluteMs || DEFAULT_ABSOLUTE_TOLERANCE_MS);
|
|
147
|
+
if (!Number.isFinite(relative) || relative < 1) throw new Error('relative tolerance must be >= 1');
|
|
148
|
+
if (!Number.isFinite(absoluteMs) || absoluteMs < 0) throw new Error('absolute tolerance must be >= 0');
|
|
149
|
+
return { relative, absoluteMs };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function budgetFor(item, tolerance) {
|
|
153
|
+
if (!Number.isFinite(item.baselineMs)) return null;
|
|
154
|
+
return roundMs(item.baselineMs * tolerance.relative + tolerance.absoluteMs);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function printTable(results, checks) {
|
|
158
|
+
const widths = {
|
|
159
|
+
name: Math.max('case'.length, ...results.map((item) => item.name.length)),
|
|
160
|
+
ms: Math.max('ms'.length, ...results.map((item) => String(item.ms).length)),
|
|
161
|
+
budget: Math.max('budget'.length, ...checks.map((item) => item.budgetMs == null ? 1 : String(item.budgetMs).length)),
|
|
162
|
+
};
|
|
163
|
+
console.log(`${'case'.padEnd(widths.name)} ${'ms'.padStart(widths.ms)} ${'budget'.padStart(widths.budget)} status`);
|
|
164
|
+
console.log(`${'-'.repeat(widths.name)} ${'-'.repeat(widths.ms)} ${'-'.repeat(widths.budget)} ------`);
|
|
165
|
+
for (const item of results) {
|
|
166
|
+
const check = checks.find((entry) => entry.name === item.name) || {};
|
|
167
|
+
const budget = check.budgetMs == null ? '-' : String(check.budgetMs);
|
|
168
|
+
const status = check.status || 'report';
|
|
169
|
+
console.log(`${item.name.padEnd(widths.name)} ${String(item.ms).padStart(widths.ms)} ${budget.padStart(widths.budget)} ${status}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function main() {
|
|
174
|
+
const options = parseArgs(process.argv.slice(2));
|
|
175
|
+
if (options.help) {
|
|
176
|
+
console.log(help());
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const baseline = loadBaseline(options.baseline);
|
|
181
|
+
const allCases = baselineCases(baseline) || defaultCases;
|
|
182
|
+
const selected = selectCases(allCases, options.cases);
|
|
183
|
+
const repeatOverride = options.repeat || null;
|
|
184
|
+
const results = selected.map((testCase) => runCase(testCase, repeatOverride));
|
|
185
|
+
const tolerance = toleranceFromEnv(baseline || {});
|
|
186
|
+
|
|
187
|
+
const byBaseline = new Map((baselineCases(baseline) || []).map((item) => [item.name, item]));
|
|
188
|
+
const checks = results.map((result) => {
|
|
189
|
+
const baselineCase = byBaseline.get(result.name);
|
|
190
|
+
const budgetMs = baselineCase ? budgetFor(baselineCase, tolerance) : null;
|
|
191
|
+
const status = budgetMs == null ? 'no-baseline' : result.ms <= budgetMs ? 'ok' : 'regressed';
|
|
192
|
+
return { name: result.name, baselineMs: baselineCase ? baselineCase.baselineMs : null, budgetMs, status };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (options.mode === 'update') {
|
|
196
|
+
const updated = {
|
|
197
|
+
version: 1,
|
|
198
|
+
generatedBy: 'npm run bench:update',
|
|
199
|
+
note: 'Machine-dependent performance baseline. Use npm run bench:update to refresh intentionally.',
|
|
200
|
+
tolerance,
|
|
201
|
+
cases: results.map((result) => ({
|
|
202
|
+
name: result.name,
|
|
203
|
+
file: result.file,
|
|
204
|
+
repeat: result.repeat,
|
|
205
|
+
baselineMs: result.ms,
|
|
206
|
+
outputBytes: result.outputBytes,
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
fs.mkdirSync(path.dirname(options.baseline), { recursive: true });
|
|
210
|
+
fs.writeFileSync(options.baseline, `${JSON.stringify(updated, null, 2)}\n`, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.json) {
|
|
214
|
+
console.log(JSON.stringify({ mode: options.mode, tolerance, results, checks }, null, 2));
|
|
215
|
+
} else {
|
|
216
|
+
printTable(results, checks);
|
|
217
|
+
if (options.mode === 'update') console.log(`\nUpdated ${path.relative(root, options.baseline)}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (options.mode === 'check') {
|
|
221
|
+
const regressions = checks.filter((item) => item.status === 'regressed');
|
|
222
|
+
if (regressions.length > 0) {
|
|
223
|
+
console.error('\nPerformance regression detected. Run npm run bench:update only after intentionally accepting a new baseline.');
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
main();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(err.stack || err.message || String(err));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|