jspurefix 5.9.1 → 5.9.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/BACKPORT_PLAN.md +20 -7
- package/WORKLOG.md +119 -0
- package/dist/benchmark/bench-report.d.ts +5 -0
- package/dist/benchmark/bench-report.js +91 -0
- package/dist/benchmark/bench-report.js.map +1 -0
- package/dist/benchmark/bench-runner.d.ts +12 -0
- package/dist/benchmark/bench-runner.js +193 -0
- package/dist/benchmark/bench-runner.js.map +1 -0
- package/dist/benchmark/bench-stats.d.ts +10 -0
- package/dist/benchmark/bench-stats.js +56 -0
- package/dist/benchmark/bench-stats.js.map +1 -0
- package/dist/benchmark/bench-types.d.ts +74 -0
- package/dist/benchmark/bench-types.js +3 -0
- package/dist/benchmark/bench-types.js.map +1 -0
- package/dist/benchmark/gc-probe.d.ts +18 -0
- package/dist/benchmark/gc-probe.js +103 -0
- package/dist/benchmark/gc-probe.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.js +23 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/parse-bench.d.ts +28 -0
- package/dist/benchmark/parse-bench.js +85 -0
- package/dist/benchmark/parse-bench.js.map +1 -0
- package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
- package/dist/buffer/ascii/ascii-parser.js +4 -1
- package/dist/buffer/ascii/ascii-parser.js.map +1 -1
- package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
- package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
- package/dist/buffer/ascii/tag-index.js.map +1 -1
- package/dist/buffer/msg-view.d.ts +1 -0
- package/dist/buffer/msg-view.js +25 -19
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
- package/dist/jsfix-bench.d.ts +1 -0
- package/dist/jsfix-bench.js +213 -0
- package/dist/jsfix-bench.js.map +1 -0
- package/dist/store/file-session-store.js +1 -1
- package/dist/store/file-session-store.js.map +1 -1
- package/dist/store/file-session-stream-provider.d.ts +1 -0
- package/dist/store/file-session-stream-provider.js +13 -13
- package/dist/store/file-session-stream-provider.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +1 -1
- package/dist/store/fix-session-store-factory.js.map +1 -1
- package/dist/store/fix-session-store.d.ts +11 -11
- package/dist/store/fix-session-store.js.map +1 -1
- package/dist/store/memory-session-store.js.map +1 -1
- package/dist/store/session-stream-provider.d.ts +13 -13
- package/dist/store/session-stream-provider.js.map +1 -1
- package/dist/transport/session/fix-clock.d.ts +1 -1
- package/dist/transport/session/fix-clock.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +2 -2
- package/dist/transport/session/resend-request-manager.js.map +1 -1
- package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
- package/dist/transport/session/session-sequence-store.d.ts +3 -3
- package/dist/transport/session/session-sequence-store.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/docs/benchmarking.md +117 -0
- package/eslint.config.mjs +15 -1
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +9 -1
- package/src/benchmark/bench-report.ts +101 -0
- package/src/benchmark/bench-runner.ts +196 -0
- package/src/benchmark/bench-stats.ts +86 -0
- package/src/benchmark/bench-types.ts +110 -0
- package/src/benchmark/gc-probe.ts +145 -0
- package/src/benchmark/index.ts +6 -0
- package/src/benchmark/parse-bench.ts +144 -0
- package/src/buffer/ascii/ascii-parser.ts +16 -1
- package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
- package/src/buffer/ascii/tag-index.ts +3 -3
- package/src/buffer/msg-view.ts +35 -18
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
- package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
- package/src/jsfix-bench.ts +204 -0
- package/src/store/file-session-store.ts +2 -2
- package/src/store/file-session-stream-provider.ts +17 -11
- package/src/store/fix-session-store-factory.ts +1 -1
- package/src/store/fix-session-store.ts +11 -11
- package/src/store/memory-session-store.ts +1 -1
- package/src/store/session-stream-provider.ts +13 -13
- package/src/transport/session/fix-clock.ts +1 -1
- package/src/transport/session/resend-request-manager.ts +4 -4
- package/src/transport/session/session-sequence-coordinator.ts +1 -1
- package/src/transport/session/session-sequence-store.ts +3 -3
- package/src/transport/tcp/tcp-initiator.ts +1 -1
package/BACKPORT_PLAN.md
CHANGED
|
@@ -435,13 +435,26 @@ Note this supersedes phase D6 of `DEMO_PORT_PLAN.md`, which had placed the regis
|
|
|
435
435
|
and wildcard handling in the demo. Issue #153 showed they belong in the engine - the
|
|
436
436
|
reporter hit them without writing a demo.
|
|
437
437
|
|
|
438
|
-
### Follow-up found while testing
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
438
|
+
### Follow-up found while testing — resolved, and the first diagnosis was wrong
|
|
439
|
+
|
|
440
|
+
This originally recorded the demo's `server-bounce` scenario as failing because
|
|
441
|
+
`AsciiSession.txOnEncoded` writes to the session store fire-and-forget, leaving the
|
|
442
|
+
acceptor's persisted sender sequence behind what it had actually sent.
|
|
443
|
+
|
|
444
|
+
**That was wrong.** Reproducing the run by hand: after phase 1 the two stores agree
|
|
445
|
+
exactly (client `4 : 14`, server `14 : 4`), and a second session started against them
|
|
446
|
+
recovers and trades normally, including under the scenario's tight 5-second client
|
|
447
|
+
budget. There is no store flush bug.
|
|
448
|
+
|
|
449
|
+
The real cause was in the scenario harness. `run_quiet_bg` published its pid via
|
|
450
|
+
command substitution, so `$!` belonged to a subshell and `wait` on it failed
|
|
451
|
+
instantly with "not a child of this shell" rather than blocking. "Wait for the server
|
|
452
|
+
to time out" was therefore a no-op, and the first server was still bound to the port
|
|
453
|
+
when the next step started a second one. A `2>/dev/null || true` kept it silent.
|
|
454
|
+
|
|
455
|
+
Fixed in jspf-demo; all six scenarios pass. Kept here as a reminder that a failing
|
|
456
|
+
end-to-end scenario is not evidence of an engine bug until the engine has been
|
|
457
|
+
driven directly.
|
|
445
458
|
|
|
446
459
|
---
|
|
447
460
|
|
package/WORKLOG.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Worklog
|
|
2
|
+
|
|
3
|
+
Running notes on what has been done and what is worth doing next, so a session can
|
|
4
|
+
be resumed cold. The long-form plans live in `BACKPORT_PLAN.md` (engine, cspurefix →
|
|
5
|
+
jspurefix) and `DEMO_PORT_PLAN.md` (the jspf-demo reference app).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 2026-08-01 — acceptor hardening, released as 5.9.1
|
|
10
|
+
|
|
11
|
+
Started from [#153](https://github.com/TimelordUK/jspurefix/issues/153) and ended up
|
|
12
|
+
rewriting how the acceptor handles connections.
|
|
13
|
+
|
|
14
|
+
**Merged (PRs #154, #156, #157, #158):**
|
|
15
|
+
|
|
16
|
+
- transport subscribe/unsubscribe symmetry — `subscribe()` attached closures,
|
|
17
|
+
`unsubscribe()` removed method references, so it removed nothing. A dead transport
|
|
18
|
+
could drive a session that had moved to a newer connection.
|
|
19
|
+
- per-connection DI scopes — `ParseBuffer` / `TransmitBuffer` were process-wide
|
|
20
|
+
singletons and the description was shared, so concurrent clients interleaved bytes
|
|
21
|
+
in one buffer and computed one `SessionId`
|
|
22
|
+
- `SessionRegistry` — one live session per `SessionId`, stopping the stale one on
|
|
23
|
+
reconnect
|
|
24
|
+
- wildcard `TargetCompID` — bound after the Logon, so each counterparty gets its own
|
|
25
|
+
store (cspurefix binds in the constructor and keys a wildcard store on `*`)
|
|
26
|
+
- transport lifecycle — keep-alive, `destroy()`, linger, harvest on `close`, census
|
|
27
|
+
logging
|
|
28
|
+
- custom session message factories preserved into per-session scopes (`cloneFor`)
|
|
29
|
+
- `@jest/globals` moved out of `dependencies` — it was never imported, and shipped
|
|
30
|
+
the Babel toolchain to every consumer. Production tree 303 → 147 packages.
|
|
31
|
+
- `npm run pack:local` for consuming unpublished builds
|
|
32
|
+
|
|
33
|
+
**Issues resolved:** #153, #87, #140 (verified and closed), #77 (awaiting reporter
|
|
34
|
+
confirmation — see below).
|
|
35
|
+
|
|
36
|
+
**Docs:** `docs/acceptor.md` describes the resulting model.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Next up
|
|
41
|
+
|
|
42
|
+
### 1. Toolchain major bumps
|
|
43
|
+
|
|
44
|
+
Deliberately held back from 5.9.1 so a regression could not be confused with the
|
|
45
|
+
acceptor work:
|
|
46
|
+
|
|
47
|
+
- `typescript` 6 → 7
|
|
48
|
+
- `eslint` 9 → 10
|
|
49
|
+
- `eslint-config-love` 151 → 154
|
|
50
|
+
- `@types/node` 25 → 26
|
|
51
|
+
|
|
52
|
+
Expect lint churn. There are 17 pre-existing lint errors on master (`fix-clock`,
|
|
53
|
+
`resend-request-manager`, `session-sequence-*`, `tcp-initiator`) — worth clearing in
|
|
54
|
+
the same pass, since they make it hard to tell new problems from old. `npm audit` is
|
|
55
|
+
currently clean.
|
|
56
|
+
|
|
57
|
+
### 2. #77 — confirm or close
|
|
58
|
+
|
|
59
|
+
[#77](https://github.com/TimelordUK/jspurefix/issues/77) is very likely fixed by the
|
|
60
|
+
per-connection parse buffers in 5.9.1, not by the field-ordering work the issue is
|
|
61
|
+
named after. Both orderings of the reporter's message parse correctly today, and
|
|
62
|
+
they always did — a test on the `fix 52` branch in 2023 also parsed the swapped
|
|
63
|
+
message, which is why the issue stalled.
|
|
64
|
+
|
|
65
|
+
The better explanation: until 5.9.0 all accepted connections shared one `ParseBuffer`
|
|
66
|
+
while each parser kept its own state, and `beginMessage()` resets the shared buffer.
|
|
67
|
+
Sweeping every split point of the reporter's MarketDataRequest, a shared buffer
|
|
68
|
+
corrupts it at 170 of 170 — the trigger is one connection being mid-message, which
|
|
69
|
+
needs a message that straddles a TCP segment, which is why small session messages
|
|
70
|
+
never expose it. The reporter's deployment (per #87) was a multi-client acceptor
|
|
71
|
+
publishing market data.
|
|
72
|
+
|
|
73
|
+
Pinned in `src/test/ascii/interleaved-parse-isolation.test.ts`. Awaiting the
|
|
74
|
+
reporter running 5.9.1 before closing — the exact `unknown tag type 146` was never
|
|
75
|
+
reproduced.
|
|
76
|
+
|
|
77
|
+
### 3. Other open issues worth a look
|
|
78
|
+
|
|
79
|
+
Several are old and may already be fixed, in the way #140 turned out to be — worth
|
|
80
|
+
verifying against 5.9.1 rather than reading the thread:
|
|
81
|
+
|
|
82
|
+
- #96 / #69 — `DefaultApplVerID` / `SenderSubID` missing during login
|
|
83
|
+
- #93 — customising the Logon message
|
|
84
|
+
- #86 — make `encoderStream` protected
|
|
85
|
+
- #85 — `ASessionMsgFactory` types patch (may be superseded by `cloneFor`)
|
|
86
|
+
- #72 — `session-launcher.run()` never resolves when connection established
|
|
87
|
+
|
|
88
|
+
### 4. Small things noticed, none urgent
|
|
89
|
+
|
|
90
|
+
- on an acceptor, the parent container's `ParseBuffer` / `TransmitBuffer` are still
|
|
91
|
+
allocated and now serve only as a size template for the per-connection clones —
|
|
92
|
+
~170KB of dead allocation per listener
|
|
93
|
+
- `script/certs/` is untracked and contains private keys; worth a `.gitignore` entry
|
|
94
|
+
so it cannot be committed by accident
|
|
95
|
+
- per-connection buffers cost ~170KB per connection (160KB parse + 10KB transmit).
|
|
96
|
+
Verified released on disconnect via `WeakRef` — a tsyringe parent keeps no
|
|
97
|
+
reference to its children — but it is the number to know if someone runs hundreds
|
|
98
|
+
of counterparties
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Corrected earlier note
|
|
103
|
+
|
|
104
|
+
An earlier version of this work recorded the demo's `server-bounce` scenario as
|
|
105
|
+
failing because of a fire-and-forget store write in `AsciiSession.txOnEncoded`
|
|
106
|
+
leaving the acceptor's persisted sender sequence behind what it had sent.
|
|
107
|
+
|
|
108
|
+
**That was wrong.** Reproducing the run by hand: after phase 1 the two stores agree
|
|
109
|
+
exactly (client `4 : 14`, server `14 : 4`), and a second session started against them
|
|
110
|
+
recovers and trades normally, including with the scenario's tight 5-second client
|
|
111
|
+
budget.
|
|
112
|
+
|
|
113
|
+
The real cause was in the scenario harness — `run_quiet_bg` published its pid via
|
|
114
|
+
command substitution, so `$!` belonged to a subshell and `wait` failed instantly
|
|
115
|
+
instead of blocking. The first server was therefore still bound to the port when the
|
|
116
|
+
second started. Fixed in jspf-demo; all six scenarios now pass.
|
|
117
|
+
|
|
118
|
+
Worth remembering as a general lesson: a `|| true` on a `wait` hid this for months,
|
|
119
|
+
and the failure looked exactly like an engine sequencing bug.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IBenchResult, IBenchSuiteResult } from './bench-types';
|
|
2
|
+
export declare function formatEnvironment(suite: IBenchSuiteResult): string;
|
|
3
|
+
export declare function formatResult(result: IBenchResult): string;
|
|
4
|
+
export declare function formatSuite(suite: IBenchSuiteResult): string;
|
|
5
|
+
export declare function formatComparison(baseline: IBenchSuiteResult, candidate: IBenchSuiteResult): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatEnvironment = formatEnvironment;
|
|
4
|
+
exports.formatResult = formatResult;
|
|
5
|
+
exports.formatSuite = formatSuite;
|
|
6
|
+
exports.formatComparison = formatComparison;
|
|
7
|
+
const bench_stats_1 = require("./bench-stats");
|
|
8
|
+
const bytesPerKb = 1024;
|
|
9
|
+
const bytesPerMb = bytesPerKb * 1024;
|
|
10
|
+
const nanosPerMicro = 1e3;
|
|
11
|
+
function micros(nanos) {
|
|
12
|
+
return (nanos / nanosPerMicro).toFixed(3);
|
|
13
|
+
}
|
|
14
|
+
function mb(bytes) {
|
|
15
|
+
return (bytes / bytesPerMb).toFixed(2);
|
|
16
|
+
}
|
|
17
|
+
function thousands(n) {
|
|
18
|
+
return Math.round(n).toLocaleString('en-US');
|
|
19
|
+
}
|
|
20
|
+
function signed(n, digits = 2) {
|
|
21
|
+
return `${n >= 0 ? '+' : ''}${n.toFixed(digits)}`;
|
|
22
|
+
}
|
|
23
|
+
function formatEnvironment(suite) {
|
|
24
|
+
const env = suite.environment;
|
|
25
|
+
const lines = [
|
|
26
|
+
`${suite.label} ${suite.capturedAt}`,
|
|
27
|
+
`node ${env.node} v8 ${env.v8} ${env.platform} ${env.arch}`,
|
|
28
|
+
`${env.cpu} x${env.cpuCount}`
|
|
29
|
+
];
|
|
30
|
+
if (!env.forcedGcAvailable) {
|
|
31
|
+
lines.push('WARNING: no --expose-gc, rounds are not isolated and retained heap is indicative only');
|
|
32
|
+
}
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
function formatResult(result) {
|
|
36
|
+
const t = result.timing;
|
|
37
|
+
const m = result.memory;
|
|
38
|
+
const noise = t.coefficientOfVariation * 100;
|
|
39
|
+
const lines = [
|
|
40
|
+
`${result.name}`,
|
|
41
|
+
result.description ? ` ${result.description}` : '',
|
|
42
|
+
` rounds ${t.samples} x ${result.options.operationsPerRound} msgs` +
|
|
43
|
+
` (warmup ${result.options.warmupOperations})`,
|
|
44
|
+
` per msg median ${micros(t.median)}us min ${micros(t.min)}us` +
|
|
45
|
+
` p95 ${micros(t.p95)}us max ${micros(t.max)}us`,
|
|
46
|
+
` throughput ${thousands(result.operationsPerSecond)} msg/sec`,
|
|
47
|
+
` noise ${noise.toFixed(2)}% of median (a change smaller than this means nothing)`,
|
|
48
|
+
` gc pass ${m.operations} msgs uninterrupted: ${m.gc.minor} minor ${m.gc.major} major ` +
|
|
49
|
+
`${m.gc.totalPauseMs.toFixed(1)}ms paused longest ${m.gc.longestPauseMs.toFixed(2)}ms`,
|
|
50
|
+
` gc rate ${m.minorGcPerMillionOperations.toFixed(1)} minor/M msgs ` +
|
|
51
|
+
`${m.majorGcPerMillionOperations.toFixed(1)} major/M msgs ` +
|
|
52
|
+
`${m.gcPauseMsPerMillionOperations.toFixed(0)}ms paused/M msgs`,
|
|
53
|
+
` retained ${mb(m.retainedHeapBytes)}MB ` +
|
|
54
|
+
`${m.retainedBytesPerOperation.toFixed(1)} bytes/msg heap at end of pass ${mb(m.heapAtEndOfPassBytes)}MB`
|
|
55
|
+
];
|
|
56
|
+
return lines.filter(l => l.length > 0).join('\n');
|
|
57
|
+
}
|
|
58
|
+
function formatSuite(suite) {
|
|
59
|
+
const blocks = suite.results.map(formatResult);
|
|
60
|
+
return [formatEnvironment(suite), '', ...blocks].join('\n\n');
|
|
61
|
+
}
|
|
62
|
+
function formatComparison(baseline, candidate) {
|
|
63
|
+
const lines = [`baseline ${baseline.label} (${baseline.capturedAt})`, ''];
|
|
64
|
+
if (baseline.environment.cpu !== candidate.environment.cpu ||
|
|
65
|
+
baseline.environment.node !== candidate.environment.node) {
|
|
66
|
+
lines.push('WARNING: baseline was taken on a different machine or node build, timings are not comparable');
|
|
67
|
+
lines.push('');
|
|
68
|
+
}
|
|
69
|
+
for (const result of candidate.results) {
|
|
70
|
+
const before = baseline.results.find(r => r.name === result.name);
|
|
71
|
+
if (!before) {
|
|
72
|
+
lines.push(`${result.name} no baseline`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const timing = (0, bench_stats_1.compare)(before.timing, result.timing);
|
|
76
|
+
const verdict = timing.significant
|
|
77
|
+
? (timing.deltaPercent < 0 ? 'FASTER' : 'SLOWER')
|
|
78
|
+
: 'noise';
|
|
79
|
+
lines.push(`${result.name}`);
|
|
80
|
+
lines.push(` per msg ${micros(before.timing.median)}us -> ${micros(result.timing.median)}us` +
|
|
81
|
+
` ${signed(timing.deltaPercent)}% ${verdict}` +
|
|
82
|
+
` (noise floor +/-${timing.noiseThresholdPercent.toFixed(2)}%)`);
|
|
83
|
+
const gcBefore = before.memory.minorGcPerMillionOperations;
|
|
84
|
+
const gcAfter = result.memory.minorGcPerMillionOperations;
|
|
85
|
+
const gcDelta = gcBefore === 0 ? 0 : ((gcAfter - gcBefore) / gcBefore) * 100;
|
|
86
|
+
lines.push(` minor gc ${gcBefore.toFixed(1)} -> ${gcAfter.toFixed(1)} per M msgs ${signed(gcDelta)}%`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=bench-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bench-report.js","sourceRoot":"","sources":["../../src/benchmark/bench-report.ts"],"names":[],"mappings":"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","sourcesContent":["import { compare } from './bench-stats'\nimport { IBenchResult, IBenchSuiteResult } from './bench-types'\n\nconst bytesPerKb = 1024\nconst bytesPerMb = bytesPerKb * 1024\nconst nanosPerMicro = 1e3\n\nfunction micros (nanos: number): string {\n return (nanos / nanosPerMicro).toFixed(3)\n}\n\nfunction mb (bytes: number): string {\n return (bytes / bytesPerMb).toFixed(2)\n}\n\nfunction thousands (n: number): string {\n return Math.round(n).toLocaleString('en-US')\n}\n\nfunction signed (n: number, digits: number = 2): string {\n return `${n >= 0 ? '+' : ''}${n.toFixed(digits)}`\n}\n\nexport function formatEnvironment (suite: IBenchSuiteResult): string {\n const env = suite.environment\n const lines = [\n `${suite.label} ${suite.capturedAt}`,\n `node ${env.node} v8 ${env.v8} ${env.platform} ${env.arch}`,\n `${env.cpu} x${env.cpuCount}`\n ]\n if (!env.forcedGcAvailable) {\n lines.push('WARNING: no --expose-gc, rounds are not isolated and retained heap is indicative only')\n }\n return lines.join('\\n')\n}\n\nexport function formatResult (result: IBenchResult): string {\n const t = result.timing\n const m = result.memory\n const noise = t.coefficientOfVariation * 100\n const lines = [\n `${result.name}`,\n result.description ? ` ${result.description}` : '',\n ` rounds ${t.samples} x ${result.options.operationsPerRound} msgs` +\n ` (warmup ${result.options.warmupOperations})`,\n ` per msg median ${micros(t.median)}us min ${micros(t.min)}us` +\n ` p95 ${micros(t.p95)}us max ${micros(t.max)}us`,\n ` throughput ${thousands(result.operationsPerSecond)} msg/sec`,\n ` noise ${noise.toFixed(2)}% of median (a change smaller than this means nothing)`,\n ` gc pass ${m.operations} msgs uninterrupted: ${m.gc.minor} minor ${m.gc.major} major ` +\n `${m.gc.totalPauseMs.toFixed(1)}ms paused longest ${m.gc.longestPauseMs.toFixed(2)}ms`,\n ` gc rate ${m.minorGcPerMillionOperations.toFixed(1)} minor/M msgs ` +\n `${m.majorGcPerMillionOperations.toFixed(1)} major/M msgs ` +\n `${m.gcPauseMsPerMillionOperations.toFixed(0)}ms paused/M msgs`,\n ` retained ${mb(m.retainedHeapBytes)}MB ` +\n `${m.retainedBytesPerOperation.toFixed(1)} bytes/msg heap at end of pass ${mb(m.heapAtEndOfPassBytes)}MB`\n ]\n return lines.filter(l => l.length > 0).join('\\n')\n}\n\nexport function formatSuite (suite: IBenchSuiteResult): string {\n const blocks = suite.results.map(formatResult)\n return [formatEnvironment(suite), '', ...blocks].join('\\n\\n')\n}\n\n/**\n * Compare a suite against a stored baseline, matching results by name.\n *\n * A difference is only called out as real when it exceeds the run to run\n * spread both suites displayed. Everything else is reported as noise, because\n * that is what it is.\n */\nexport function formatComparison (baseline: IBenchSuiteResult, candidate: IBenchSuiteResult): string {\n const lines: string[] = [`baseline ${baseline.label} (${baseline.capturedAt})`, '']\n if (baseline.environment.cpu !== candidate.environment.cpu ||\n baseline.environment.node !== candidate.environment.node) {\n lines.push('WARNING: baseline was taken on a different machine or node build, timings are not comparable')\n lines.push('')\n }\n for (const result of candidate.results) {\n const before = baseline.results.find(r => r.name === result.name)\n if (!before) {\n lines.push(`${result.name} no baseline`)\n continue\n }\n const timing = compare(before.timing, result.timing)\n const verdict = timing.significant\n ? (timing.deltaPercent < 0 ? 'FASTER' : 'SLOWER')\n : 'noise'\n lines.push(`${result.name}`)\n lines.push(` per msg ${micros(before.timing.median)}us -> ${micros(result.timing.median)}us` +\n ` ${signed(timing.deltaPercent)}% ${verdict}` +\n ` (noise floor +/-${timing.noiseThresholdPercent.toFixed(2)}%)`)\n const gcBefore = before.memory.minorGcPerMillionOperations\n const gcAfter = result.memory.minorGcPerMillionOperations\n const gcDelta = gcBefore === 0 ? 0 : ((gcAfter - gcBefore) / gcBefore) * 100\n lines.push(` minor gc ${gcBefore.toFixed(1)} -> ${gcAfter.toFixed(1)} per M msgs ${signed(gcDelta)}%`)\n lines.push('')\n }\n return lines.join('\\n')\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IBenchCase, IBenchOptions, IBenchResult, IBenchSuiteResult, IEnvironment } from './bench-types';
|
|
2
|
+
export declare const defaultBenchOptions: IBenchOptions;
|
|
3
|
+
export declare function environment(): IEnvironment;
|
|
4
|
+
export declare class BenchRunner {
|
|
5
|
+
private readonly options;
|
|
6
|
+
constructor(options?: IBenchOptions);
|
|
7
|
+
run(benchCase: IBenchCase): Promise<IBenchResult>;
|
|
8
|
+
runAll(label: string, cases: IBenchCase[]): Promise<IBenchSuiteResult>;
|
|
9
|
+
private measureMemory;
|
|
10
|
+
private warmup;
|
|
11
|
+
private measureRound;
|
|
12
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.BenchRunner = exports.defaultBenchOptions = void 0;
|
|
46
|
+
exports.environment = environment;
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const gc_probe_1 = require("./gc-probe");
|
|
49
|
+
const bench_stats_1 = require("./bench-stats");
|
|
50
|
+
const nanosPerSecond = 1e9;
|
|
51
|
+
const operationsPerMillion = 1e6;
|
|
52
|
+
exports.defaultBenchOptions = {
|
|
53
|
+
warmupOperations: 20000,
|
|
54
|
+
rounds: 10,
|
|
55
|
+
operationsPerRound: 20000,
|
|
56
|
+
gcBetweenRounds: true
|
|
57
|
+
};
|
|
58
|
+
function environment() {
|
|
59
|
+
const cpus = os.cpus();
|
|
60
|
+
return {
|
|
61
|
+
node: process.version,
|
|
62
|
+
v8: process.versions.v8,
|
|
63
|
+
platform: `${os.type()} ${os.release()}`,
|
|
64
|
+
arch: process.arch,
|
|
65
|
+
cpu: cpus.length > 0 ? cpus[0].model.trim() : 'unknown',
|
|
66
|
+
cpuCount: cpus.length,
|
|
67
|
+
forcedGcAvailable: gc_probe_1.GcProbe.forcedGcAvailable()
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function tick() {
|
|
71
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
yield new Promise(resolve => { setImmediate(resolve); });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function drain(probe_1) {
|
|
76
|
+
return __awaiter(this, arguments, void 0, function* (probe, maxTurns = 50) {
|
|
77
|
+
let quietTurns = 0;
|
|
78
|
+
for (let turn = 0; turn < maxTurns && quietTurns < 3; ++turn) {
|
|
79
|
+
const before = probe.seen();
|
|
80
|
+
yield tick();
|
|
81
|
+
quietTurns = probe.seen() === before ? quietTurns + 1 : 0;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
class BenchRunner {
|
|
86
|
+
constructor(options = exports.defaultBenchOptions) {
|
|
87
|
+
this.options = options;
|
|
88
|
+
}
|
|
89
|
+
run(benchCase) {
|
|
90
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
91
|
+
var _a, _b;
|
|
92
|
+
const options = this.options;
|
|
93
|
+
(_a = benchCase.setup) === null || _a === void 0 ? void 0 : _a.call(benchCase);
|
|
94
|
+
try {
|
|
95
|
+
this.warmup(benchCase);
|
|
96
|
+
const rounds = [];
|
|
97
|
+
for (let round = 0; round < options.rounds; ++round) {
|
|
98
|
+
if (options.gcBetweenRounds) {
|
|
99
|
+
gc_probe_1.GcProbe.forceGc();
|
|
100
|
+
}
|
|
101
|
+
rounds.push(this.measureRound(benchCase, round));
|
|
102
|
+
}
|
|
103
|
+
const timing = (0, bench_stats_1.summarise)(rounds.map(r => r.nanosPerOperation));
|
|
104
|
+
return {
|
|
105
|
+
name: benchCase.name,
|
|
106
|
+
description: benchCase.description,
|
|
107
|
+
options,
|
|
108
|
+
rounds,
|
|
109
|
+
timing,
|
|
110
|
+
operationsPerSecond: timing.median === 0 ? 0 : nanosPerSecond / timing.median,
|
|
111
|
+
memory: yield this.measureMemory(benchCase)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
(_b = benchCase.teardown) === null || _b === void 0 ? void 0 : _b.call(benchCase);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
runAll(label, cases) {
|
|
120
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const benchCase of cases) {
|
|
123
|
+
results.push(yield this.run(benchCase));
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
label,
|
|
127
|
+
capturedAt: new Date().toISOString(),
|
|
128
|
+
environment: environment(),
|
|
129
|
+
results
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
measureMemory(benchCase) {
|
|
134
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
gc_probe_1.GcProbe.forceGc();
|
|
136
|
+
yield tick();
|
|
137
|
+
const heapBefore = process.memoryUsage().heapUsed;
|
|
138
|
+
const probe = new gc_probe_1.GcProbe();
|
|
139
|
+
probe.start();
|
|
140
|
+
const operations = benchCase.run(this.options.operationsPerRound);
|
|
141
|
+
const heapAtEndOfPassBytes = process.memoryUsage().heapUsed;
|
|
142
|
+
yield drain(probe);
|
|
143
|
+
const gc = probe.stop();
|
|
144
|
+
gc_probe_1.GcProbe.forceGc();
|
|
145
|
+
const heapAfter = process.memoryUsage().heapUsed;
|
|
146
|
+
const retainedHeapBytes = heapAfter - heapBefore;
|
|
147
|
+
const perMillion = operations === 0 ? 0 : operationsPerMillion / operations;
|
|
148
|
+
return {
|
|
149
|
+
operations,
|
|
150
|
+
retainedHeapBytes,
|
|
151
|
+
retainedBytesPerOperation: operations === 0 ? 0 : retainedHeapBytes / operations,
|
|
152
|
+
heapAtEndOfPassBytes,
|
|
153
|
+
gc,
|
|
154
|
+
minorGcPerMillionOperations: gc.minor * perMillion,
|
|
155
|
+
majorGcPerMillionOperations: gc.major * perMillion,
|
|
156
|
+
gcPauseMsPerMillionOperations: gc.totalPauseMs * perMillion,
|
|
157
|
+
forcedGcAvailable: gc_probe_1.GcProbe.forcedGcAvailable()
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
warmup(benchCase) {
|
|
162
|
+
const target = this.options.warmupOperations;
|
|
163
|
+
if (target <= 0)
|
|
164
|
+
return;
|
|
165
|
+
let done = 0;
|
|
166
|
+
const chunk = Math.max(1, Math.min(target, this.options.operationsPerRound));
|
|
167
|
+
while (done < target) {
|
|
168
|
+
const requested = Math.min(chunk, target - done);
|
|
169
|
+
const completed = benchCase.run(requested);
|
|
170
|
+
if (completed !== requested) {
|
|
171
|
+
throw new Error(`${benchCase.name}: asked for ${requested} operations during warmup but completed ${completed}`);
|
|
172
|
+
}
|
|
173
|
+
done += completed;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
measureRound(benchCase, round) {
|
|
177
|
+
const requested = this.options.operationsPerRound;
|
|
178
|
+
const startedAt = process.hrtime.bigint();
|
|
179
|
+
const operations = benchCase.run(requested);
|
|
180
|
+
const elapsedNanos = Number(process.hrtime.bigint() - startedAt);
|
|
181
|
+
if (operations !== requested) {
|
|
182
|
+
throw new Error(`${benchCase.name}: asked for ${requested} operations in round ${round} but completed ${operations}`);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
round,
|
|
186
|
+
operations,
|
|
187
|
+
elapsedNanos,
|
|
188
|
+
nanosPerOperation: operations === 0 ? 0 : elapsedNanos / operations
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
exports.BenchRunner = BenchRunner;
|
|
193
|
+
//# sourceMappingURL=bench-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bench-runner.js","sourceRoot":"","sources":["../../src/benchmark/bench-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as os from 'os'\nimport { GcProbe } from './gc-probe'\nimport { summarise } from './bench-stats'\nimport {\n IBenchCase,\n IBenchOptions,\n IBenchResult,\n IBenchSuiteResult,\n IEnvironment,\n IMemoryResult,\n IRoundResult\n} from './bench-types'\n\nconst nanosPerSecond = 1e9\nconst operationsPerMillion = 1e6\n\nexport const defaultBenchOptions: IBenchOptions = {\n // v8 needs to see a function run a few thousand times before it stops\n // interpreting it, so an unwarmed first round measures the wrong code\n warmupOperations: 20000,\n rounds: 10,\n operationsPerRound: 20000,\n gcBetweenRounds: true\n}\n\nexport function environment (): IEnvironment {\n const cpus = os.cpus()\n return {\n node: process.version,\n v8: process.versions.v8,\n platform: `${os.type()} ${os.release()}`,\n arch: process.arch,\n cpu: cpus.length > 0 ? cpus[0].model.trim() : 'unknown',\n cpuCount: cpus.length,\n forcedGcAvailable: GcProbe.forcedGcAvailable()\n }\n}\n\n/** hand the event loop one turn, so queued observer callbacks can run */\nasync function tick (): Promise<void> {\n await new Promise<void>(resolve => { setImmediate(resolve) })\n}\n\n/**\n * Yield until the probe stops seeing new entries. Node dispatches a gc entry on\n * the turn after the collection, and a burst takes several turns to come\n * through, so a single yield loses most of them. Bounded so a pathological case\n * cannot spin here.\n */\nasync function drain (probe: GcProbe, maxTurns: number = 50): Promise<void> {\n let quietTurns = 0\n for (let turn = 0; turn < maxTurns && quietTurns < 3; ++turn) {\n const before = probe.seen()\n await tick()\n quietTurns = probe.seen() === before ? quietTurns + 1 : 0\n }\n}\n\n/**\n * Drives a case through a warmup, a series of timed rounds and a separate\n * diagnostics pass.\n *\n * One round is one sample. A single long run cannot tell you whether a number\n * is repeatable, and a difference smaller than the spread between rounds is not\n * a difference at all.\n *\n * Timing and memory are deliberately not measured together. Isolating rounds\n * means forcing a collection between them, and a forced collection is itself a\n * gc event - counting those would report the harness rather than the work. So\n * the timed rounds collect between them and report no gc figures, and a final\n * pass runs uninterrupted with the probe attached and reports no timings.\n */\nexport class BenchRunner {\n constructor (private readonly options: IBenchOptions = defaultBenchOptions) {\n }\n\n public async run (benchCase: IBenchCase): Promise<IBenchResult> {\n const options = this.options\n benchCase.setup?.()\n try {\n this.warmup(benchCase)\n const rounds: IRoundResult[] = []\n for (let round = 0; round < options.rounds; ++round) {\n if (options.gcBetweenRounds) {\n // a collection provoked by the previous round must not be charged\n // to the next one\n GcProbe.forceGc()\n }\n rounds.push(this.measureRound(benchCase, round))\n }\n const timing = summarise(rounds.map(r => r.nanosPerOperation))\n return {\n name: benchCase.name,\n description: benchCase.description,\n options,\n rounds,\n timing,\n operationsPerSecond: timing.median === 0 ? 0 : nanosPerSecond / timing.median,\n memory: await this.measureMemory(benchCase)\n }\n } finally {\n benchCase.teardown?.()\n }\n }\n\n public async runAll (label: string, cases: IBenchCase[]): Promise<IBenchSuiteResult> {\n const results: IBenchResult[] = []\n for (const benchCase of cases) {\n results.push(await this.run(benchCase))\n }\n return {\n label,\n // stamped from the clock rather than passed in, a suite is a record of\n // when it was taken\n capturedAt: new Date().toISOString(),\n environment: environment(),\n results\n }\n }\n\n /**\n * One uninterrupted pass with the gc probe attached.\n *\n * Nothing is forced while it runs, so every collection counted is one the\n * work itself provoked. The heap readings either side are taken after a\n * forced collection, making them what the pass retained rather than what it\n * churned - churn is what the collection counts are for.\n */\n private async measureMemory (benchCase: IBenchCase): Promise<IMemoryResult> {\n // settle, then let any delivery from that settling drain before the probe\n // exists - entries are dispatched a turn late and would otherwise land in\n // the middle of the measured pass\n GcProbe.forceGc()\n await tick()\n\n const heapBefore = process.memoryUsage().heapUsed\n const probe = new GcProbe()\n probe.start()\n const operations = benchCase.run(this.options.operationsPerRound)\n const heapAtEndOfPassBytes = process.memoryUsage().heapUsed\n await drain(probe)\n const gc = probe.stop()\n\n GcProbe.forceGc()\n const heapAfter = process.memoryUsage().heapUsed\n\n const retainedHeapBytes = heapAfter - heapBefore\n const perMillion = operations === 0 ? 0 : operationsPerMillion / operations\n return {\n operations,\n retainedHeapBytes,\n retainedBytesPerOperation: operations === 0 ? 0 : retainedHeapBytes / operations,\n heapAtEndOfPassBytes,\n gc,\n minorGcPerMillionOperations: gc.minor * perMillion,\n majorGcPerMillionOperations: gc.major * perMillion,\n gcPauseMsPerMillionOperations: gc.totalPauseMs * perMillion,\n forcedGcAvailable: GcProbe.forcedGcAvailable()\n }\n }\n\n private warmup (benchCase: IBenchCase): void {\n const target = this.options.warmupOperations\n if (target <= 0) return\n let done = 0\n // in chunks the size of a real round, so warmup exercises the same shape of\n // work rather than one long unbroken call\n const chunk = Math.max(1, Math.min(target, this.options.operationsPerRound))\n while (done < target) {\n const requested = Math.min(chunk, target - done)\n const completed = benchCase.run(requested)\n // a case that reports less than it was asked for would never let this\n // loop reach its target, so refuse it here rather than spin\n if (completed !== requested) {\n throw new Error(`${benchCase.name}: asked for ${requested} operations during warmup but completed ${completed}`)\n }\n done += completed\n }\n }\n\n private measureRound (benchCase: IBenchCase, round: number): IRoundResult {\n const requested = this.options.operationsPerRound\n const startedAt = process.hrtime.bigint()\n const operations = benchCase.run(requested)\n const elapsedNanos = Number(process.hrtime.bigint() - startedAt)\n if (operations !== requested) {\n throw new Error(`${benchCase.name}: asked for ${requested} operations in round ${round} but completed ${operations}`)\n }\n return {\n round,\n operations,\n elapsedNanos,\n nanosPerOperation: operations === 0 ? 0 : elapsedNanos / operations\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ISampleStats } from './bench-types';
|
|
2
|
+
export declare function summarise(values: number[]): ISampleStats;
|
|
3
|
+
export declare function median(sorted: number[]): number;
|
|
4
|
+
export declare function percentile(sorted: number[], p: number): number;
|
|
5
|
+
export interface IComparison {
|
|
6
|
+
readonly deltaPercent: number;
|
|
7
|
+
readonly significant: boolean;
|
|
8
|
+
readonly noiseThresholdPercent: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function compare(baseline: ISampleStats, candidate: ISampleStats): IComparison;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarise = summarise;
|
|
4
|
+
exports.median = median;
|
|
5
|
+
exports.percentile = percentile;
|
|
6
|
+
exports.compare = compare;
|
|
7
|
+
function summarise(values) {
|
|
8
|
+
if (values.length === 0) {
|
|
9
|
+
throw new Error('cannot summarise an empty sample set');
|
|
10
|
+
}
|
|
11
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
12
|
+
const n = sorted.length;
|
|
13
|
+
const mean = sorted.reduce((a, v) => a + v, 0) / n;
|
|
14
|
+
const variance = sorted.reduce((a, v) => a + (v - mean) * (v - mean), 0) / n;
|
|
15
|
+
const stdDev = Math.sqrt(variance);
|
|
16
|
+
return {
|
|
17
|
+
samples: n,
|
|
18
|
+
min: sorted[0],
|
|
19
|
+
max: sorted[n - 1],
|
|
20
|
+
mean,
|
|
21
|
+
median: median(sorted),
|
|
22
|
+
p95: percentile(sorted, 95),
|
|
23
|
+
p99: percentile(sorted, 99),
|
|
24
|
+
stdDev,
|
|
25
|
+
coefficientOfVariation: mean === 0 ? 0 : stdDev / mean
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function median(sorted) {
|
|
29
|
+
const n = sorted.length;
|
|
30
|
+
if (n === 0)
|
|
31
|
+
return 0;
|
|
32
|
+
const mid = n >> 1;
|
|
33
|
+
return n % 2 === 1
|
|
34
|
+
? sorted[mid]
|
|
35
|
+
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
36
|
+
}
|
|
37
|
+
function percentile(sorted, p) {
|
|
38
|
+
const n = sorted.length;
|
|
39
|
+
if (n === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
const rank = Math.ceil((p / 100) * n);
|
|
42
|
+
const index = Math.min(Math.max(rank - 1, 0), n - 1);
|
|
43
|
+
return sorted[index];
|
|
44
|
+
}
|
|
45
|
+
function compare(baseline, candidate) {
|
|
46
|
+
const deltaPercent = baseline.median === 0
|
|
47
|
+
? 0
|
|
48
|
+
: ((candidate.median - baseline.median) / baseline.median) * 100;
|
|
49
|
+
const noiseThresholdPercent = (baseline.coefficientOfVariation + candidate.coefficientOfVariation) * 100;
|
|
50
|
+
return {
|
|
51
|
+
deltaPercent,
|
|
52
|
+
significant: Math.abs(deltaPercent) > noiseThresholdPercent,
|
|
53
|
+
noiseThresholdPercent
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=bench-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bench-stats.js","sourceRoot":"","sources":["../../src/benchmark/bench-stats.ts"],"names":[],"mappings":"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","sourcesContent":["import { ISampleStats } from './bench-types'\n\n/**\n * Descriptive statistics over a set of measured rounds.\n *\n * min matters as much as median here. A round can only ever be made slower by\n * interference - a collection landing mid round, the scheduler taking the core\n * away - so the fastest round is the closest reading to the true cost of the\n * work, while the median tells you what to expect in practice.\n */\nexport function summarise (values: number[]): ISampleStats {\n if (values.length === 0) {\n throw new Error('cannot summarise an empty sample set')\n }\n const sorted = [...values].sort((a, b) => a - b)\n const n = sorted.length\n const mean = sorted.reduce((a, v) => a + v, 0) / n\n const variance = sorted.reduce((a, v) => a + (v - mean) * (v - mean), 0) / n\n const stdDev = Math.sqrt(variance)\n return {\n samples: n,\n min: sorted[0],\n max: sorted[n - 1],\n mean,\n median: median(sorted),\n p95: percentile(sorted, 95),\n p99: percentile(sorted, 99),\n stdDev,\n coefficientOfVariation: mean === 0 ? 0 : stdDev / mean\n }\n}\n\n/**\n * @param sorted values in ascending order\n */\nexport function median (sorted: number[]): number {\n const n = sorted.length\n if (n === 0) return 0\n const mid = n >> 1\n return n % 2 === 1\n ? sorted[mid]\n : (sorted[mid - 1] + sorted[mid]) / 2\n}\n\n/**\n * Nearest rank percentile - no interpolation, so every value reported is a\n * reading that actually occurred.\n *\n * @param sorted values in ascending order\n * @param p percentile in the range 0 to 100\n */\nexport function percentile (sorted: number[], p: number): number {\n const n = sorted.length\n if (n === 0) return 0\n const rank = Math.ceil((p / 100) * n)\n const index = Math.min(Math.max(rank - 1, 0), n - 1)\n return sorted[index]\n}\n\nexport interface IComparison {\n /** positive means the candidate took longer than the baseline */\n readonly deltaPercent: number\n /**\n * true when the change is larger than the noise both runs displayed. Below\n * this threshold a difference is not evidence of anything.\n */\n readonly significant: boolean\n readonly noiseThresholdPercent: number\n}\n\n/**\n * Compare a candidate timing against a baseline, using the run to run spread\n * each of them showed to decide whether the difference is real.\n */\nexport function compare (baseline: ISampleStats, candidate: ISampleStats): IComparison {\n const deltaPercent = baseline.median === 0\n ? 0\n : ((candidate.median - baseline.median) / baseline.median) * 100\n // both runs contribute their own noise to any difference between them\n const noiseThresholdPercent = (baseline.coefficientOfVariation + candidate.coefficientOfVariation) * 100\n return {\n deltaPercent,\n significant: Math.abs(deltaPercent) > noiseThresholdPercent,\n noiseThresholdPercent\n }\n}\n"]}
|