eyeprolog 1.5.94 → 1.5.95
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/README.md +2 -3
- package/package.json +1 -2
- package/src/ARCHITECTURE.md +4 -7
- package/test/README.md +2 -3
- package/test/regression/cases-documentation-sync.mjs +1 -1
- package/the-art-of-eyeprolog.md +6 -27
- package/test/bench/benchmarks.json +0 -193
- package/test/bench/eyelet-forward.pl +0 -58
- package/test/benchmark-core.mjs +0 -94
- package/test/benchmark-worker.mjs +0 -28
- package/test/benchmark.mjs +0 -307
- package/test/run-benchmark-tests.mjs +0 -73
package/README.md
CHANGED
|
@@ -83,8 +83,8 @@ The checked [Symbiotic Knowledge Graphs example](examples/symbiotic-knowledge-gr
|
|
|
83
83
|
|
|
84
84
|
The same RDF → Prolog → RDF boundary is exercised by five additional checked scenarios: [cross-organization data sharing](https://eyereasoner.github.io/eyeprolog/examples/deck/cross-organization-data-sharing), [explainable EV-depot configuration](https://eyereasoner.github.io/eyeprolog/examples/deck/explainable-ev-depot-configuration), [operational incident response](https://eyereasoner.github.io/eyeprolog/examples/deck/operational-incident-response), [software supply-chain vulnerability response](https://eyereasoner.github.io/eyeprolog/examples/deck/sbom-vulnerability-response), and a [scientific evidence graph](https://eyereasoner.github.io/eyeprolog/examples/deck/scientific-evidence-graph). Together they cover policy decisions, reversible configuration reasoning, dependency-graph diagnosis, transitive SBOM exposure, and evidence aggregation with explicit disagreement.
|
|
85
85
|
|
|
86
|
-
##
|
|
87
|
-
EyeProlog
|
|
86
|
+
## Performance
|
|
87
|
+
EyeProlog does not carry a separate wall-clock benchmark harness. `npm test`'s own elapsed time, run across thousands of conformance, regression, and example programs, is the coarse performance indicator instead — a real slowdown shows up there. [OpenRuleBench](openrulebench/README.md) remains a dedicated, checked correctness-and-scale benchmark for the Datalog rule-engine profile specifically.
|
|
88
88
|
For the project policy on post-ISO-standard and WG17 compatibility features such as digit separators, see [ISO/WG17 compatibility extensions](test/conformance/ISO-WG17-EXTENSIONS.md).
|
|
89
89
|
## Development
|
|
90
90
|
```sh
|
|
@@ -97,7 +97,6 @@ The npm command list is deliberately small:
|
|
|
97
97
|
|
|
98
98
|
- `npm test` (or `npm run test`): run the release gate, including live upstream conformity checks (WG17 syntax among them).
|
|
99
99
|
- `npm run generate`: rebuild generated library and book files.
|
|
100
|
-
- `npm run benchmark`: run the wall-clock benchmarks.
|
|
101
100
|
|
|
102
101
|
Use `npm test -- --offline` for a network-free local pass (this also skips the live-discovered WG17 syntax check, since it has no offline snapshot). Focused checks remain available directly, for example `node test/run-regression.mjs docs`; see [test runners](test/README.md). The automatic version hooks still run the release gate, refresh and stage conformance reports, and push the release. Detailed upstream report maintenance is documented in the [conformance guide](test/conformance/README.md).
|
|
103
102
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.5.
|
|
6
|
+
"version": "1.5.95",
|
|
7
7
|
"description": "EyeProlog turns facts and rules into answers and proofs.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./index.js",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"test": "node test/run-all.mjs",
|
|
49
|
-
"benchmark": "node test/benchmark.mjs",
|
|
50
49
|
"generate": "node tools/generate-library-autoload-index.mjs && node tools/generate-predicate-reference.mjs && node tools/extract-book-examples.mjs",
|
|
51
50
|
"preversion": "npm test && node test/run-neumerkel.mjs --cached --update-report && node test/run-conformance-report.mjs conformance-report.md && git add test/conformance/NEUMERKEL-LATEST.md conformance-report.md",
|
|
52
51
|
"postversion": "git push origin HEAD --follow-tags"
|
package/src/ARCHITECTURE.md
CHANGED
|
@@ -92,15 +92,12 @@ as direct function calls. Candidate indexing is separated physically but
|
|
|
92
92
|
retains the same data structures and selection functions.
|
|
93
93
|
|
|
94
94
|
Large solver fast paths deliberately remain co-located in `solver.js` until a
|
|
95
|
-
split can demonstrate
|
|
95
|
+
split can demonstrate performance parity. A cleaner file layout is not worth a
|
|
96
96
|
runtime regression.
|
|
97
97
|
|
|
98
|
-
Performance claims use
|
|
99
|
-
inference, or host-call counts as a substitute for elapsed
|
|
100
|
-
|
|
101
|
-
runs, reports the median, and verifies a committed output SHA-256 before its
|
|
102
|
-
timing is accepted. Machine-specific timing baselines live under `.benchmarks/`
|
|
103
|
-
and are intentionally not versioned.
|
|
98
|
+
Performance claims use `npm test`'s own elapsed time across the full corpus,
|
|
99
|
+
not predicate, inference, or host-call counts as a substitute for elapsed
|
|
100
|
+
time, and not a separate wall-clock benchmark harness.
|
|
104
101
|
|
|
105
102
|
|
|
106
103
|
## HTTP and JSON library ownership
|
package/test/README.md
CHANGED
|
@@ -21,7 +21,6 @@ node test/run-properties.mjs # seeded random-term invariant checks (see b
|
|
|
21
21
|
node test/run-openrulebench.mjs
|
|
22
22
|
node test/run-http-json.mjs
|
|
23
23
|
node test/run-interop.mjs # requires the comparison engines
|
|
24
|
-
node test/run-benchmark-tests.mjs # benchmark harness
|
|
25
24
|
```
|
|
26
25
|
|
|
27
26
|
These runners retain their existing options; there is no separate npm alias for
|
|
@@ -52,7 +51,7 @@ still runs in about two seconds. The seed (`SEED` in that file) is fixed so a
|
|
|
52
51
|
failure is exactly reproducible by rerunning it -- change it only
|
|
53
52
|
deliberately, and say why, never to make a transient failure disappear.
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
There is no separate wall-clock benchmark harness: `npm test`'s own elapsed
|
|
55
|
+
time is the project's performance indicator.
|
|
57
56
|
|
|
58
57
|
See the [conformance guide](conformance/README.md) for report maintenance.
|
|
@@ -629,7 +629,7 @@ ${profile}`;
|
|
|
629
629
|
const publishIndex = publishWorkflow.indexOf('run: npm publish');
|
|
630
630
|
assertEqual(testIndex >= 0 && testIndex < publishIndex, true, 'publish workflow test gate');
|
|
631
631
|
assertEqual(packIndex >= 0 && packIndex < publishIndex, true, 'publish workflow package gate');
|
|
632
|
-
assertArrayEqual(Object.keys(pkg.scripts).sort(), ['
|
|
632
|
+
assertArrayEqual(Object.keys(pkg.scripts).sort(), ['generate', 'postversion', 'preversion', 'test'], 'small npm command surface');
|
|
633
633
|
assertEqual(pkg.scripts.test, 'node test/run-all.mjs', 'full release gate');
|
|
634
634
|
const runner = fs.readFileSync(path.join(packageRoot, 'test', 'run-all.mjs'), 'utf8');
|
|
635
635
|
assertIncludes(runner, 'runOpenRuleBenchChecks(reporter)', 'OpenRuleBench remains in release gate');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -2083,7 +2083,7 @@ while compact-clause representation and conservative candidate indexes live in
|
|
|
2083
2083
|
`src/program-indexing.js`. The solver consumes those same indexes directly;
|
|
2084
2084
|
large execution fast paths deliberately remain in `src/solver.js` rather than
|
|
2085
2085
|
being split through extra strategy objects or callbacks. Architectural cleanup
|
|
2086
|
-
is required to preserve
|
|
2086
|
+
is required to preserve performance as well as semantics.
|
|
2087
2087
|
|
|
2088
2088
|
Focused files under `src/lib/` contain the portable extensions, with
|
|
2089
2089
|
`src/lib/lists.pl` supplying common list relations. They are ordinary Prolog modules using EyeProlog's documented module compatibility
|
|
@@ -3221,9 +3221,8 @@ performance evidence comes after semantic evidence. A faster program that
|
|
|
3221
3221
|
silently drops a mode is a different program. Predicate and inference counts are
|
|
3222
3222
|
also not reliable substitutes for elapsed time: one expensive host call can cost
|
|
3223
3223
|
more than thousands of cheap Prolog calls. Repository-level performance work
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
longer matches the committed result.
|
|
3224
|
+
watches `npm test`'s own elapsed time across the whole corpus rather than a
|
|
3225
|
+
dedicated wall-clock benchmark harness.
|
|
3227
3226
|
|
|
3228
3227
|
**Exercises.**
|
|
3229
3228
|
|
|
@@ -10360,29 +10359,9 @@ with:
|
|
|
10360
10359
|
npm test
|
|
10361
10360
|
```
|
|
10362
10361
|
|
|
10363
|
-
|
|
10364
|
-
and
|
|
10365
|
-
|
|
10366
|
-
```sh
|
|
10367
|
-
npm run benchmark
|
|
10368
|
-
npm run benchmark -- --save .benchmarks/baseline.json
|
|
10369
|
-
```
|
|
10370
|
-
|
|
10371
|
-
The benchmark suite contains 21 representative workloads and stores their
|
|
10372
|
-
semantic output digests in the repository, while wall-clock baselines remain
|
|
10373
|
-
machine-local under `.benchmarks/` because absolute timings are machine-specific.
|
|
10374
|
-
Each benchmark runs in its own fresh Node worker. Inside that worker, one untimed
|
|
10375
|
-
execution primes parser, module, and JIT state; short workloads are then repeated
|
|
10376
|
-
with independent `run()` calls until a batch is roughly 400 ms long. After one
|
|
10377
|
-
warm-up batch, five measured batches are reported as milliseconds per workload
|
|
10378
|
-
execution. Naturally long workloads keep a batch size of one.
|
|
10379
|
-
|
|
10380
|
-
The report shows the median, per-operation range, chosen batch size, saved
|
|
10381
|
-
baseline median, and the percentage change between the current median and baseline
|
|
10382
|
-
median. The range remains visible as context, but it does not suppress or reinterpret
|
|
10383
|
-
the median-to-median comparison. Older unbatched baseline files are ignored with a
|
|
10384
|
-
request to regenerate them. This keeps the rule simple: unchanged answers first,
|
|
10385
|
-
sufficiently long wall-clock samples second, and direct median-versus-median change.
|
|
10362
|
+
There is no separate wall-clock benchmark harness. `npm test`'s own elapsed
|
|
10363
|
+
time, run across thousands of conformance, regression, and example programs,
|
|
10364
|
+
is the project's performance indicator: a real slowdown shows up there.
|
|
10386
10365
|
|
|
10387
10366
|
When adding an example:
|
|
10388
10367
|
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"name": "deep-taxonomy",
|
|
4
|
-
"group": "recursion/indexing",
|
|
5
|
-
"file": "examples/deep-taxonomy-100000.pl",
|
|
6
|
-
"goals": [
|
|
7
|
-
"result(report, success)"
|
|
8
|
-
],
|
|
9
|
-
"expectedSha256": "a0aa24fcbc87c394a4da789ac861b8e18cabad5e0d73b25ddc02df5dd4376c1b"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"name": "clpz-sudoku",
|
|
13
|
-
"group": "clpz",
|
|
14
|
-
"file": "examples/clpz-sudoku-9x9.pl",
|
|
15
|
-
"goals": [
|
|
16
|
-
"sudoku9_solution(X0)"
|
|
17
|
-
],
|
|
18
|
-
"expectedSha256": "0eed3bcf38498cb514143adb2e475d8e48b0326bcf329fbedb6a3b0e87646731"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"name": "clpb-weighted-planning",
|
|
22
|
-
"group": "clpb",
|
|
23
|
-
"file": "examples/clpb-weighted-planning.pl",
|
|
24
|
-
"goals": [
|
|
25
|
-
"best_release_plan(X0, X1)"
|
|
26
|
-
],
|
|
27
|
-
"expectedSha256": "52f2cc7d22873accef7968f89aea3584fb3304b2dceb555428e79477b41f0b5d"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"name": "clpb-cardinality",
|
|
31
|
-
"group": "clpb",
|
|
32
|
-
"file": "examples/clpb-cardinality.pl",
|
|
33
|
-
"goals": [
|
|
34
|
-
"review_quorum_count(X0)"
|
|
35
|
-
],
|
|
36
|
-
"expectedSha256": "a643a582baefc95da37b0ccf9609a2241aeb7cfe559e2636bb439c05c50e2177"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"name": "tabling-cycle",
|
|
40
|
-
"group": "tabling",
|
|
41
|
-
"file": "examples/cyclic-path.pl",
|
|
42
|
-
"goals": [
|
|
43
|
-
"path(X0, X1)"
|
|
44
|
-
],
|
|
45
|
-
"expectedSha256": "5580a2782c6896ffdf36bd6cc242931e96e1722b154976c1a20e7cad1206566f"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"name": "tabling-chart-parser",
|
|
49
|
-
"group": "tabling",
|
|
50
|
-
"file": "examples/chart-parser.pl",
|
|
51
|
-
"goals": [
|
|
52
|
-
"chart_parser_answer(X0, X1)"
|
|
53
|
-
],
|
|
54
|
-
"expectedSha256": "70707d3026aabc65982b82b9617a15d3de6cbb5fa6cdb2ee955bedd4f29f0e5a"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"name": "wfs-policy",
|
|
58
|
-
"group": "wfs",
|
|
59
|
-
"file": "examples/odrl-policy-reasoning.pl",
|
|
60
|
-
"goals": [
|
|
61
|
-
"wfsQuestion(X0, X1)"
|
|
62
|
-
],
|
|
63
|
-
"expectedSha256": "b4a153234f3daf1e2ba6c26843349a1bc1919ebd9f9e98559835eb5ef915716b"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"name": "dcg-expression",
|
|
67
|
-
"group": "dcg",
|
|
68
|
-
"file": "examples/dcg-expression-language.pl",
|
|
69
|
-
"goals": [
|
|
70
|
-
"dcg_expression_example(X0, X1)"
|
|
71
|
-
],
|
|
72
|
-
"expectedSha256": "562985450257f1db8c67682c6184102622b4e6946f2cb0abd3ac15bde493f5d5"
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"name": "eyelet-forward",
|
|
76
|
-
"group": "eyelet",
|
|
77
|
-
"file": "test/bench/eyelet-forward.pl",
|
|
78
|
-
"goals": [],
|
|
79
|
-
"expectedSha256": "127277f2ba5c02d41143b22ac0ae069596a4a7dd4dc71079f3dff1319ca019cc"
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
"name": "takeuchi",
|
|
83
|
-
"group": "recursive-control",
|
|
84
|
-
"file": "examples/takeuchi.pl",
|
|
85
|
-
"goals": [
|
|
86
|
-
"tak([34, 13, 8], X0)"
|
|
87
|
-
],
|
|
88
|
-
"expectedSha256": "a7c33872b86aa7877b9c38ec41c447fc96a328d40f512428993d662f28a097ad"
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
"name": "attributed-variables",
|
|
92
|
-
"group": "attributes",
|
|
93
|
-
"file": "examples/attributed-variables.pl",
|
|
94
|
-
"goals": [
|
|
95
|
-
"accepts_required_value(X0)",
|
|
96
|
-
"alias_preserves_attribute(X0)"
|
|
97
|
-
],
|
|
98
|
-
"expectedSha256": "3d60cd3464f2aae44ad01859e838fc2b8754e35916bafe3bba66ca745f77c236"
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
"name": "dif-constraints",
|
|
102
|
-
"group": "constraints",
|
|
103
|
-
"file": "examples/dif-constraints.pl",
|
|
104
|
-
"goals": [
|
|
105
|
-
"allowed_pair(X0, X1)",
|
|
106
|
-
"specialization(X0, X1)"
|
|
107
|
-
],
|
|
108
|
-
"expectedSha256": "b6268a6653d02f7312f73406612b451d030a3766931e093003c3642cc170009d"
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
"name": "dijkstra-findall-sort",
|
|
112
|
-
"group": "search/collections",
|
|
113
|
-
"file": "examples/dijkstra-findall-sort.pl",
|
|
114
|
-
"goals": [
|
|
115
|
-
"shortestPath(X0, X1)",
|
|
116
|
-
"cost(X0, X1)"
|
|
117
|
-
],
|
|
118
|
-
"expectedSha256": "1318aaaca44aa19f295f988d87b09165cd0fc279e94ae14c10e2d1f146d544f3"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
"name": "iso-term-io",
|
|
122
|
-
"group": "term-io",
|
|
123
|
-
"file": "examples/iso-term-io.pl",
|
|
124
|
-
"goals": [
|
|
125
|
-
"report(X0, X1)"
|
|
126
|
-
],
|
|
127
|
-
"expectedSha256": "0719932d697e7587285082b558f4c39c04ec1f2222131073e73e6b6fe37e0320"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"name": "symbolic-derivative",
|
|
131
|
-
"group": "term-rewriting",
|
|
132
|
-
"file": "examples/symbolic-derivative.pl",
|
|
133
|
-
"goals": [
|
|
134
|
-
"derivative_result(X0, X1)"
|
|
135
|
-
],
|
|
136
|
-
"expectedSha256": "49f9f33e89e8c8c63ce2eb64ea2639e416869d6ef5db185cab70452d1f77f342"
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
"name": "sat-dpll",
|
|
140
|
-
"group": "search/control",
|
|
141
|
-
"file": "examples/sat-solver-dpll.pl",
|
|
142
|
-
"goals": [
|
|
143
|
-
"satModel(X0)",
|
|
144
|
-
"satValue(X0, X1)"
|
|
145
|
-
],
|
|
146
|
-
"expectedSha256": "a8c5cdddac5efcb7d5b1031200d3bbc5b78c1668928a45e064377432f05b3c9f"
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
"name": "type-inference",
|
|
150
|
-
"group": "unification",
|
|
151
|
-
"file": "examples/type-inference.pl",
|
|
152
|
-
"goals": [
|
|
153
|
-
"type_answer(X0, X1)"
|
|
154
|
-
],
|
|
155
|
-
"expectedSha256": "acad9ddb995d9ddef091e17cc91659d36ed7f9014d6714fd40100d85a53ae203"
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
"name": "bulk-stream-write",
|
|
159
|
-
"group": "term-io",
|
|
160
|
-
"file": "examples/bulk-stream-write.pl",
|
|
161
|
-
"goals": [
|
|
162
|
-
"bulk_write_result(X0, X1)"
|
|
163
|
-
],
|
|
164
|
-
"expectedSha256": "8471da130a4a3a92dd31ee6cf285b1e129886177d7be367d51650a48b8920324"
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
"name": "quine-mccluskey",
|
|
168
|
-
"group": "dynamic-database",
|
|
169
|
-
"file": "examples/quine-mccluskey.pl",
|
|
170
|
-
"goals": [
|
|
171
|
-
"answer(X0)"
|
|
172
|
-
],
|
|
173
|
-
"expectedSha256": "9ebf4a55f6f15a78ad614da02b418650d77cae39eb0d49cbf9eb614865bec501"
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
"name": "fibonacci",
|
|
177
|
-
"group": "tabling",
|
|
178
|
-
"file": "examples/fibonacci.pl",
|
|
179
|
-
"goals": [
|
|
180
|
-
"fibonacci(X0, X1)"
|
|
181
|
-
],
|
|
182
|
-
"expectedSha256": "3adc819083c11d6bf4195e2718904d1712c21553d8fcad970c11a4a4e79e2a3f"
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
"name": "iso-atomic-conversion",
|
|
186
|
-
"group": "term-io",
|
|
187
|
-
"file": "examples/iso-atomic-conversion.pl",
|
|
188
|
-
"goals": [
|
|
189
|
-
"report(X0, X1)"
|
|
190
|
-
],
|
|
191
|
-
"expectedSha256": "68c82dac7dd666cae5af51786f4fbc75423aac12309cedda1e342aba08ea4253"
|
|
192
|
-
}
|
|
193
|
-
]
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
% Synthetic Eyelet fixed-point benchmark.
|
|
2
|
-
% The chain is intentionally moderate: large enough to exercise repeated
|
|
3
|
-
% forward-rule rounds without making the default benchmark suite unwieldy.
|
|
4
|
-
|
|
5
|
-
reach(0).
|
|
6
|
-
edge(0, 1).
|
|
7
|
-
edge(1, 2).
|
|
8
|
-
edge(2, 3).
|
|
9
|
-
edge(3, 4).
|
|
10
|
-
edge(4, 5).
|
|
11
|
-
edge(5, 6).
|
|
12
|
-
edge(6, 7).
|
|
13
|
-
edge(7, 8).
|
|
14
|
-
edge(8, 9).
|
|
15
|
-
edge(9, 10).
|
|
16
|
-
edge(10, 11).
|
|
17
|
-
edge(11, 12).
|
|
18
|
-
edge(12, 13).
|
|
19
|
-
edge(13, 14).
|
|
20
|
-
edge(14, 15).
|
|
21
|
-
edge(15, 16).
|
|
22
|
-
edge(16, 17).
|
|
23
|
-
edge(17, 18).
|
|
24
|
-
edge(18, 19).
|
|
25
|
-
edge(19, 20).
|
|
26
|
-
edge(20, 21).
|
|
27
|
-
edge(21, 22).
|
|
28
|
-
edge(22, 23).
|
|
29
|
-
edge(23, 24).
|
|
30
|
-
edge(24, 25).
|
|
31
|
-
edge(25, 26).
|
|
32
|
-
edge(26, 27).
|
|
33
|
-
edge(27, 28).
|
|
34
|
-
edge(28, 29).
|
|
35
|
-
edge(29, 30).
|
|
36
|
-
edge(30, 31).
|
|
37
|
-
edge(31, 32).
|
|
38
|
-
edge(32, 33).
|
|
39
|
-
edge(33, 34).
|
|
40
|
-
edge(34, 35).
|
|
41
|
-
edge(35, 36).
|
|
42
|
-
edge(36, 37).
|
|
43
|
-
edge(37, 38).
|
|
44
|
-
edge(38, 39).
|
|
45
|
-
edge(39, 40).
|
|
46
|
-
edge(40, 41).
|
|
47
|
-
edge(41, 42).
|
|
48
|
-
edge(42, 43).
|
|
49
|
-
edge(43, 44).
|
|
50
|
-
edge(44, 45).
|
|
51
|
-
edge(45, 46).
|
|
52
|
-
edge(46, 47).
|
|
53
|
-
edge(47, 48).
|
|
54
|
-
edge(48, 49).
|
|
55
|
-
edge(49, 50).
|
|
56
|
-
|
|
57
|
-
reach(Y) :+ reach(X), edge(X, Y).
|
|
58
|
-
true :+ reach(50).
|
package/test/benchmark-core.mjs
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import { performance } from 'node:perf_hooks';
|
|
3
|
-
import { run } from '../index.js';
|
|
4
|
-
|
|
5
|
-
function digest(text) {
|
|
6
|
-
return crypto.createHash('sha256').update(text).digest('hex');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function nextPowerOfTwo(value) {
|
|
10
|
-
let power = 1;
|
|
11
|
-
while (power < value && power < 1024) power *= 2;
|
|
12
|
-
return power;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function measureBenchmark(source, goals, { runs = 5, warmup = 1, targetMs = 400 } = {}) {
|
|
16
|
-
let referenceStdout = null;
|
|
17
|
-
let referenceDigest = null;
|
|
18
|
-
let answerLines = null;
|
|
19
|
-
let outputBytes = null;
|
|
20
|
-
|
|
21
|
-
function execute() {
|
|
22
|
-
return run(source, { goals });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function validateResult(result) {
|
|
26
|
-
if (result.haltCode != null) throw new Error(`benchmark halted with code ${result.haltCode}`);
|
|
27
|
-
if (referenceStdout == null) {
|
|
28
|
-
referenceStdout = result.stdout;
|
|
29
|
-
referenceDigest = digest(result.stdout);
|
|
30
|
-
answerLines = result.stdout.split('\n').filter((line) => line.length > 0).length;
|
|
31
|
-
outputBytes = Buffer.byteLength(result.stdout);
|
|
32
|
-
} else if (result.stdout !== referenceStdout) {
|
|
33
|
-
throw new Error(`non-deterministic output digest: ${referenceDigest} != ${digest(result.stdout)}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function runBatch(batchSize) {
|
|
38
|
-
const results = new Array(batchSize);
|
|
39
|
-
const started = performance.now();
|
|
40
|
-
for (let i = 0; i < batchSize; i++) results[i] = execute();
|
|
41
|
-
const elapsed = performance.now() - started;
|
|
42
|
-
for (const result of results) validateResult(result);
|
|
43
|
-
return elapsed;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let batchSize = 1;
|
|
47
|
-
let calibrationMs = 0;
|
|
48
|
-
if (targetMs > 0) {
|
|
49
|
-
runBatch(1); // prime parser/JIT/module state before sizing the measured batch
|
|
50
|
-
calibrationMs = runBatch(1);
|
|
51
|
-
if (calibrationMs < targetMs) {
|
|
52
|
-
batchSize = nextPowerOfTwo(Math.ceil(targetMs / Math.max(calibrationMs, 0.01)));
|
|
53
|
-
calibrationMs = runBatch(batchSize);
|
|
54
|
-
while (calibrationMs < targetMs * 0.8 && batchSize < 1024) {
|
|
55
|
-
batchSize = Math.min(batchSize * 2, 1024);
|
|
56
|
-
calibrationMs = runBatch(batchSize);
|
|
57
|
-
}
|
|
58
|
-
while (calibrationMs > targetMs * 2 && batchSize > 1) {
|
|
59
|
-
batchSize = Math.max(Math.floor(batchSize / 2), 1);
|
|
60
|
-
calibrationMs = runBatch(batchSize);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < warmup; i++) runBatch(batchSize);
|
|
66
|
-
|
|
67
|
-
// Warm-up can materially change short-workload cost. Re-check the chosen batch
|
|
68
|
-
// in the warmed state and grow it until samples are long enough to be useful.
|
|
69
|
-
if (targetMs > 0) {
|
|
70
|
-
calibrationMs = runBatch(batchSize);
|
|
71
|
-
while (calibrationMs < targetMs * 0.8 && batchSize < 1024) {
|
|
72
|
-
batchSize = Math.min(batchSize * 2, 1024);
|
|
73
|
-
calibrationMs = runBatch(batchSize);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const samplesMs = [];
|
|
78
|
-
const batchSamplesMs = [];
|
|
79
|
-
for (let i = 0; i < runs; i++) {
|
|
80
|
-
const elapsed = runBatch(batchSize);
|
|
81
|
-
batchSamplesMs.push(elapsed);
|
|
82
|
-
samplesMs.push(elapsed / batchSize);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
digest: referenceDigest,
|
|
87
|
-
answerLines,
|
|
88
|
-
outputBytes,
|
|
89
|
-
batchSize,
|
|
90
|
-
calibrationMs,
|
|
91
|
-
batchSamplesMs,
|
|
92
|
-
samplesMs,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import process from 'node:process';
|
|
4
|
-
import { measureBenchmark } from './benchmark-core.mjs';
|
|
5
|
-
|
|
6
|
-
function parseInteger(value, name, minimum) {
|
|
7
|
-
const number = Number(value);
|
|
8
|
-
if (!Number.isInteger(number) || number < minimum) throw new Error(`${name} must be an integer >= ${minimum}`);
|
|
9
|
-
return number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function parseNumber(value, name, minimum) {
|
|
13
|
-
const number = Number(value);
|
|
14
|
-
if (!Number.isFinite(number) || number < minimum) throw new Error(`${name} must be a number >= ${minimum}`);
|
|
15
|
-
return number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const [fileArg, goalsJson = '[]', runsArg = '5', warmupArg = '1', targetMsArg = '400'] = process.argv.slice(2);
|
|
19
|
-
if (!fileArg) throw new Error('benchmark worker requires a file');
|
|
20
|
-
const goals = JSON.parse(goalsJson);
|
|
21
|
-
if (!Array.isArray(goals) || !goals.every((goal) => typeof goal === 'string')) throw new Error('benchmark worker goals must be a JSON array of strings');
|
|
22
|
-
const source = await fs.readFile(path.resolve(fileArg), 'utf8');
|
|
23
|
-
const result = measureBenchmark(source, goals, {
|
|
24
|
-
runs: parseInteger(runsArg, 'runs', 1),
|
|
25
|
-
warmup: parseInteger(warmupArg, 'warmup', 0),
|
|
26
|
-
targetMs: parseNumber(targetMsArg, 'targetMs', 0),
|
|
27
|
-
});
|
|
28
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
package/test/benchmark.mjs
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import process from 'node:process';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
-
const manifestPath = path.join(root, 'test', 'bench', 'benchmarks.json');
|
|
9
|
-
const defaultBaselinePath = path.join(root, '.benchmarks', 'baseline.json');
|
|
10
|
-
|
|
11
|
-
function usage() {
|
|
12
|
-
process.stdout.write(`EyeProlog benchmark runner\n\nUsage:\n node test/benchmark.mjs [options]\n\nOptions:\n --runs N Measured batches per benchmark (default: 5)\n --warmup N Warm-up batches per benchmark (default: 1)\n --target-ms N Minimum target for short measured batches (default: 400)\n --filter TEXT Run benchmarks whose name or group contains TEXT\n --baseline FILE Compare against a saved timing baseline\n --save FILE Save current medians as a timing baseline\n --json Print machine-readable JSON\n --list List benchmark names and exit\n -h, --help Show this help\n\nAfter one untimed priming execution, short workloads are automatically repeated\nin independent run() executions until a measured batch lasts about --target-ms. The report divides batch time by the\niteration count, so all results remain milliseconds per workload execution.\nOutput digests are always checked against the committed semantic checksums in\ntest/bench/benchmarks.json.\n`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function parseInteger(value, name, minimum) {
|
|
16
|
-
const number = Number(value);
|
|
17
|
-
if (!Number.isInteger(number) || number < minimum) {
|
|
18
|
-
throw new Error(`${name} must be an integer >= ${minimum}`);
|
|
19
|
-
}
|
|
20
|
-
return number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseNumber(value, name, minimum) {
|
|
24
|
-
const number = Number(value);
|
|
25
|
-
if (!Number.isFinite(number) || number < minimum) {
|
|
26
|
-
throw new Error(`${name} must be a number >= ${minimum}`);
|
|
27
|
-
}
|
|
28
|
-
return number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const options = {
|
|
32
|
-
runs: 5,
|
|
33
|
-
warmup: 1,
|
|
34
|
-
targetMs: 400,
|
|
35
|
-
filter: null,
|
|
36
|
-
baseline: null,
|
|
37
|
-
save: null,
|
|
38
|
-
json: false,
|
|
39
|
-
list: false,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
for (let i = 2; i < process.argv.length; i++) {
|
|
43
|
-
const arg = process.argv[i];
|
|
44
|
-
if (arg === '--runs') options.runs = parseInteger(process.argv[++i], '--runs', 1);
|
|
45
|
-
else if (arg === '--warmup') options.warmup = parseInteger(process.argv[++i], '--warmup', 0);
|
|
46
|
-
else if (arg === '--target-ms') options.targetMs = parseNumber(process.argv[++i], '--target-ms', 0);
|
|
47
|
-
else if (arg === '--filter') options.filter = process.argv[++i] ?? '';
|
|
48
|
-
else if (arg === '--baseline') options.baseline = path.resolve(process.argv[++i] ?? '');
|
|
49
|
-
else if (arg === '--save') options.save = path.resolve(process.argv[++i] ?? '');
|
|
50
|
-
else if (arg === '--json') options.json = true;
|
|
51
|
-
else if (arg === '--list') options.list = true;
|
|
52
|
-
else if (arg === '--help' || arg === '-h') { usage(); process.exit(0); }
|
|
53
|
-
else throw new Error(`unknown option: ${arg}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
57
|
-
if (!Array.isArray(manifest) || manifest.length === 0) throw new Error('benchmark manifest is empty');
|
|
58
|
-
|
|
59
|
-
const names = new Set();
|
|
60
|
-
for (const item of manifest) {
|
|
61
|
-
if (!item || typeof item.name !== 'string' || typeof item.group !== 'string' || typeof item.file !== 'string') {
|
|
62
|
-
throw new Error('invalid benchmark manifest entry');
|
|
63
|
-
}
|
|
64
|
-
if (names.has(item.name)) throw new Error(`duplicate benchmark name: ${item.name}`);
|
|
65
|
-
names.add(item.name);
|
|
66
|
-
if (!Array.isArray(item.goals) || !item.goals.every((goal) => typeof goal === 'string')) {
|
|
67
|
-
throw new Error(`invalid goals for benchmark ${item.name}`);
|
|
68
|
-
}
|
|
69
|
-
if (!/^[0-9a-f]{64}$/.test(item.expectedSha256)) {
|
|
70
|
-
throw new Error(`invalid expectedSha256 for benchmark ${item.name}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const selected = manifest.filter((item) => {
|
|
75
|
-
if (options.filter == null) return true;
|
|
76
|
-
const needle = options.filter.toLowerCase();
|
|
77
|
-
return item.name.toLowerCase().includes(needle) || item.group.toLowerCase().includes(needle);
|
|
78
|
-
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
79
|
-
if (selected.length === 0) throw new Error(`no benchmarks match filter: ${options.filter}`);
|
|
80
|
-
|
|
81
|
-
if (options.list) {
|
|
82
|
-
for (const item of selected) process.stdout.write(`${item.name}\t${item.group}\t${item.file}\n`);
|
|
83
|
-
process.exit(0);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let baselinePath = options.baseline;
|
|
87
|
-
if (baselinePath == null) {
|
|
88
|
-
try {
|
|
89
|
-
await fs.access(defaultBaselinePath);
|
|
90
|
-
baselinePath = defaultBaselinePath;
|
|
91
|
-
} catch (_) {
|
|
92
|
-
baselinePath = null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
let baseline = null;
|
|
97
|
-
let baselineWarning = null;
|
|
98
|
-
if (baselinePath != null) {
|
|
99
|
-
const loaded = JSON.parse(await fs.readFile(baselinePath, 'utf8'));
|
|
100
|
-
if (loaded.format === 2) baseline = loaded;
|
|
101
|
-
else {
|
|
102
|
-
baselineWarning = `Ignoring legacy timing baseline format ${loaded.format ?? 'unknown'}; regenerate it with npm run benchmark -- --save .benchmarks/baseline.json.`;
|
|
103
|
-
baselinePath = null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function median(values) {
|
|
108
|
-
const sorted = [...values].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 formatMs(value) {
|
|
114
|
-
if (value == null) return '—';
|
|
115
|
-
return `${value.toFixed(1)} ms`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function changePercent(medianMs, baselineMs) {
|
|
119
|
-
if (baselineMs == null || baselineMs === 0) return null;
|
|
120
|
-
return ((medianMs - baselineMs) / baselineMs) * 100;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function changeText(item) {
|
|
124
|
-
if (item.changePercent == null) return '—';
|
|
125
|
-
const change = item.changePercent;
|
|
126
|
-
const value = `${change >= 0 ? '+' : ''}${change.toFixed(1)}%`;
|
|
127
|
-
if (Math.abs(change) < 5) return `≈ ${value}`;
|
|
128
|
-
return `${change < 0 ? '↓' : '↑'} ${value}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// A regression (slower) reads red, an improvement (faster) reads green, and a
|
|
132
|
-
// change too small to trust (< 5%) reads dim rather than either color, so the
|
|
133
|
-
// palette itself does not editorialize about noise-level swings.
|
|
134
|
-
function changeColor(item) {
|
|
135
|
-
if (item.changePercent == null) return null;
|
|
136
|
-
if (Math.abs(item.changePercent) < 5) return 'dim';
|
|
137
|
-
return item.changePercent < 0 ? 'green' : 'red';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const supportsColor = !options.json && process.stdout.isTTY &&
|
|
141
|
-
process.env.NO_COLOR == null && process.env.TERM !== 'dumb';
|
|
142
|
-
const ANSI = { bold: '1', dim: '2', green: '32', red: '31' };
|
|
143
|
-
function paint(name, text) {
|
|
144
|
-
return name && supportsColor ? `\x1b[${ANSI[name]}m${text}\x1b[0m` : text;
|
|
145
|
-
}
|
|
146
|
-
function visibleLength(text) {
|
|
147
|
-
return text.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
148
|
-
}
|
|
149
|
-
function padVisible(text, width) {
|
|
150
|
-
return text + ' '.repeat(Math.max(0, width - visibleLength(text)));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function summarizeResults(results) {
|
|
154
|
-
const comparable = results.filter((item) =>
|
|
155
|
-
Number.isFinite(item.medianMs) &&
|
|
156
|
-
item.medianMs > 0 &&
|
|
157
|
-
Number.isFinite(item.baselineMs) &&
|
|
158
|
-
item.baselineMs > 0);
|
|
159
|
-
|
|
160
|
-
if (comparable.length === 0) {
|
|
161
|
-
return {
|
|
162
|
-
comparable: 0,
|
|
163
|
-
total: results.length,
|
|
164
|
-
ratio: null,
|
|
165
|
-
changePercent: null,
|
|
166
|
-
currentTotalMs: null,
|
|
167
|
-
baselineTotalMs: null,
|
|
168
|
-
totalRatio: null,
|
|
169
|
-
totalChangePercent: null,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const ratio = Math.exp(
|
|
174
|
-
comparable.reduce(
|
|
175
|
-
(sum, item) => sum + Math.log(item.medianMs / item.baselineMs),
|
|
176
|
-
0,
|
|
177
|
-
) / comparable.length,
|
|
178
|
-
);
|
|
179
|
-
const currentTotalMs = comparable.reduce((sum, item) => sum + item.medianMs, 0);
|
|
180
|
-
const baselineTotalMs = comparable.reduce((sum, item) => sum + item.baselineMs, 0);
|
|
181
|
-
const totalRatio = currentTotalMs / baselineTotalMs;
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
comparable: comparable.length,
|
|
185
|
-
total: results.length,
|
|
186
|
-
ratio,
|
|
187
|
-
changePercent: (ratio - 1) * 100,
|
|
188
|
-
currentTotalMs,
|
|
189
|
-
baselineTotalMs,
|
|
190
|
-
totalRatio,
|
|
191
|
-
totalChangePercent: (totalRatio - 1) * 100,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function runWorker(item) {
|
|
196
|
-
const child = spawnSync(process.execPath, [
|
|
197
|
-
path.join(root, 'test', 'benchmark-worker.mjs'),
|
|
198
|
-
path.join(root, item.file),
|
|
199
|
-
JSON.stringify(item.goals),
|
|
200
|
-
String(options.runs),
|
|
201
|
-
String(options.warmup),
|
|
202
|
-
String(options.targetMs),
|
|
203
|
-
], { cwd: root, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 120_000 });
|
|
204
|
-
if (child.error) throw new Error(`${item.name} failed: ${child.error.message}`);
|
|
205
|
-
if (child.status !== 0) {
|
|
206
|
-
throw new Error(`${item.name} failed${child.stderr ? `: ${child.stderr.trim()}` : ''}`);
|
|
207
|
-
}
|
|
208
|
-
try { return JSON.parse(child.stdout); }
|
|
209
|
-
catch (error) { throw new Error(`${item.name} returned invalid worker output: ${error.message}`); }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const results = [];
|
|
213
|
-
for (const item of selected) {
|
|
214
|
-
if (!options.json) process.stderr.write(`benchmark ${item.name}...\n`);
|
|
215
|
-
const worker = runWorker(item);
|
|
216
|
-
if (worker.digest !== item.expectedSha256) {
|
|
217
|
-
throw new Error(`${item.name} semantic checksum changed: expected ${item.expectedSha256}, got ${worker.digest}`);
|
|
218
|
-
}
|
|
219
|
-
const medianMs = median(worker.samplesMs);
|
|
220
|
-
const minMs = Math.min(...worker.samplesMs);
|
|
221
|
-
const maxMs = Math.max(...worker.samplesMs);
|
|
222
|
-
const baselineItem = baseline?.benchmarks?.[item.name] ?? null;
|
|
223
|
-
results.push({
|
|
224
|
-
name: item.name,
|
|
225
|
-
group: item.group,
|
|
226
|
-
file: item.file,
|
|
227
|
-
goals: item.goals,
|
|
228
|
-
medianMs,
|
|
229
|
-
minMs,
|
|
230
|
-
maxMs,
|
|
231
|
-
batchSize: worker.batchSize,
|
|
232
|
-
calibrationMs: worker.calibrationMs,
|
|
233
|
-
batchSamplesMs: worker.batchSamplesMs,
|
|
234
|
-
samplesMs: worker.samplesMs,
|
|
235
|
-
answerLines: worker.answerLines,
|
|
236
|
-
outputBytes: worker.outputBytes,
|
|
237
|
-
sha256: worker.digest,
|
|
238
|
-
baselineMs: baselineItem?.medianMs ?? null,
|
|
239
|
-
changePercent: changePercent(medianMs, baselineItem?.medianMs ?? null),
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const summary = summarizeResults(results);
|
|
244
|
-
|
|
245
|
-
if (options.save != null) {
|
|
246
|
-
await fs.mkdir(path.dirname(options.save), { recursive: true });
|
|
247
|
-
const saved = {
|
|
248
|
-
format: 2,
|
|
249
|
-
generatedAt: new Date().toISOString(),
|
|
250
|
-
node: process.version,
|
|
251
|
-
platform: `${process.platform}-${process.arch}`,
|
|
252
|
-
runs: options.runs,
|
|
253
|
-
warmup: options.warmup,
|
|
254
|
-
targetMs: options.targetMs,
|
|
255
|
-
benchmarks: Object.fromEntries(results.map((item) => [item.name, {
|
|
256
|
-
medianMs: item.medianMs,
|
|
257
|
-
batchSize: item.batchSize,
|
|
258
|
-
sha256: item.sha256,
|
|
259
|
-
}])),
|
|
260
|
-
};
|
|
261
|
-
await fs.writeFile(options.save, `${JSON.stringify(saved, null, 2)}\n`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (options.json) {
|
|
265
|
-
process.stdout.write(`${JSON.stringify({
|
|
266
|
-
runs: options.runs,
|
|
267
|
-
warmup: options.warmup,
|
|
268
|
-
targetMs: options.targetMs,
|
|
269
|
-
baseline: baselinePath,
|
|
270
|
-
baselineWarning,
|
|
271
|
-
summary,
|
|
272
|
-
results,
|
|
273
|
-
}, null, 2)}\n`);
|
|
274
|
-
} else {
|
|
275
|
-
const headers = ['Benchmark', 'Median/op', 'Range/op', 'Batch', 'Baseline', 'Change', 'Answers'];
|
|
276
|
-
const rows = results.map((item) => [
|
|
277
|
-
item.name,
|
|
278
|
-
formatMs(item.medianMs),
|
|
279
|
-
`${formatMs(item.minMs)}–${formatMs(item.maxMs)}`,
|
|
280
|
-
String(item.batchSize),
|
|
281
|
-
formatMs(item.baselineMs),
|
|
282
|
-
paint(changeColor(item), changeText(item)),
|
|
283
|
-
String(item.answerLines),
|
|
284
|
-
]);
|
|
285
|
-
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => visibleLength(row[index]))));
|
|
286
|
-
const printRow = (row, style) => process.stdout.write(
|
|
287
|
-
`${row.map((cell, index) => paint(style, padVisible(cell, widths[index]))).join(' ')}\n`,
|
|
288
|
-
);
|
|
289
|
-
printRow(headers, 'bold');
|
|
290
|
-
printRow(widths.map((width) => '-'.repeat(width)), 'dim');
|
|
291
|
-
for (const row of rows) printRow(row);
|
|
292
|
-
|
|
293
|
-
if (summary.comparable > 0) {
|
|
294
|
-
process.stdout.write(
|
|
295
|
-
`\nSuite score: ${summary.ratio.toFixed(3)}x baseline ` +
|
|
296
|
-
`(${paint(changeColor({ changePercent: summary.changePercent }), changeText({ changePercent: summary.changePercent }))}), ` +
|
|
297
|
-
`${summary.comparable}/${summary.total} comparable. ` +
|
|
298
|
-
`Time-weighted: ${formatMs(summary.currentTotalMs)} vs ${formatMs(summary.baselineTotalMs)} ` +
|
|
299
|
-
`(${paint(changeColor({ changePercent: summary.totalChangePercent }), changeText({ changePercent: summary.totalChangePercent }))}).\n`,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
process.stdout.write(`\n${results.length} benchmarks, ${options.runs}× after ${options.warmup}× warm-up, ~${options.targetMs} ms/batch.\n`);
|
|
304
|
-
if (baselineWarning) process.stdout.write(paint('dim', `${baselineWarning}\n`));
|
|
305
|
-
else if (baselinePath == null) process.stdout.write(paint('dim', 'No baseline: npm run benchmark -- --save .benchmarks/baseline.json\n'));
|
|
306
|
-
if (options.save != null) process.stdout.write(`Saved baseline: ${path.relative(root, options.save)}\n`);
|
|
307
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import process from 'node:process';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
-
const manifest = JSON.parse(await fs.readFile(path.join(root, 'test', 'bench', 'benchmarks.json'), 'utf8'));
|
|
9
|
-
|
|
10
|
-
let passed = 0;
|
|
11
|
-
function ok(condition, message) {
|
|
12
|
-
if (!condition) throw new Error(message);
|
|
13
|
-
passed++;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function spawnJson(args) {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
const child = spawn(process.execPath, args, { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
19
|
-
let stdout = '';
|
|
20
|
-
let stderr = '';
|
|
21
|
-
child.stdout.setEncoding('utf8');
|
|
22
|
-
child.stderr.setEncoding('utf8');
|
|
23
|
-
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
24
|
-
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
25
|
-
child.on('error', reject);
|
|
26
|
-
child.on('close', (code) => {
|
|
27
|
-
if (code !== 0) return reject(new Error(stderr || `child exited ${code}`));
|
|
28
|
-
try { resolve(JSON.parse(stdout)); }
|
|
29
|
-
catch (error) { reject(new Error(`invalid child JSON: ${error.message}`)); }
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function runWorker(item) {
|
|
35
|
-
return spawnJson([
|
|
36
|
-
path.join(root, 'test', 'benchmark-worker.mjs'),
|
|
37
|
-
path.join(root, item.file),
|
|
38
|
-
JSON.stringify(item.goals),
|
|
39
|
-
'1',
|
|
40
|
-
'0',
|
|
41
|
-
'0',
|
|
42
|
-
]);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
ok(Array.isArray(manifest) && manifest.length === 21, 'benchmark manifest should contain exactly 21 representative workloads');
|
|
46
|
-
ok(new Set(manifest.map((item) => item.name)).size === manifest.length, 'benchmark names should be unique');
|
|
47
|
-
|
|
48
|
-
for (const item of manifest) {
|
|
49
|
-
ok(/^[0-9a-f]{64}$/.test(item.expectedSha256), `${item.name} should have a committed semantic checksum`);
|
|
50
|
-
await fs.access(path.join(root, item.file));
|
|
51
|
-
passed++;
|
|
52
|
-
|
|
53
|
-
const worker = await runWorker(item);
|
|
54
|
-
ok(worker.digest === item.expectedSha256, `${item.name} should preserve its committed semantic checksum`);
|
|
55
|
-
ok(worker.batchSize === 1, `${item.name} digest test should use one execution per batch`);
|
|
56
|
-
ok(Array.isArray(worker.samplesMs) && worker.samplesMs.length === 1, `${item.name} should return one requested sample`);
|
|
57
|
-
ok(worker.samplesMs[0] >= 0, `${item.name} should return a non-negative wall time`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const adaptive = await spawnJson([
|
|
61
|
-
path.join(root, 'test', 'benchmark.mjs'),
|
|
62
|
-
'--filter', 'dcg-expression',
|
|
63
|
-
'--runs', '1',
|
|
64
|
-
'--warmup', '0',
|
|
65
|
-
'--target-ms', '50',
|
|
66
|
-
'--json',
|
|
67
|
-
]);
|
|
68
|
-
ok(adaptive.results.length === 1, 'adaptive benchmark smoke test should select one workload');
|
|
69
|
-
ok(adaptive.results[0].batchSize > 1, 'adaptive benchmark smoke test should batch a short workload');
|
|
70
|
-
ok(adaptive.results[0].sha256 === manifest.find((item) => item.name === 'dcg-expression').expectedSha256,
|
|
71
|
-
'adaptive batching should preserve the semantic checksum');
|
|
72
|
-
|
|
73
|
-
process.stdout.write(`Benchmark harness tests: ${passed}/${passed} passed.\n`);
|