eyeleng 1.0.7 → 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 +6 -2
- package/playground.html +16 -4
- package/reports/w3c-shacl12-rules-earl.ttl +1337 -1056
- 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/browser-bundle.test.js +8 -0
- package/test/perf-baseline.json +39 -0
- package/test/shacl12-rules.test.js +0 -8
- package/test/w3c-rdf.test.js +0 -3
- package/tools/bundle.js +0 -17
- package/tools/perf-bench.js +234 -0
|
@@ -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
|
+
}
|