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.
Files changed (94) hide show
  1. package/BACKPORT_PLAN.md +20 -7
  2. package/WORKLOG.md +119 -0
  3. package/dist/benchmark/bench-report.d.ts +5 -0
  4. package/dist/benchmark/bench-report.js +91 -0
  5. package/dist/benchmark/bench-report.js.map +1 -0
  6. package/dist/benchmark/bench-runner.d.ts +12 -0
  7. package/dist/benchmark/bench-runner.js +193 -0
  8. package/dist/benchmark/bench-runner.js.map +1 -0
  9. package/dist/benchmark/bench-stats.d.ts +10 -0
  10. package/dist/benchmark/bench-stats.js +56 -0
  11. package/dist/benchmark/bench-stats.js.map +1 -0
  12. package/dist/benchmark/bench-types.d.ts +74 -0
  13. package/dist/benchmark/bench-types.js +3 -0
  14. package/dist/benchmark/bench-types.js.map +1 -0
  15. package/dist/benchmark/gc-probe.d.ts +18 -0
  16. package/dist/benchmark/gc-probe.js +103 -0
  17. package/dist/benchmark/gc-probe.js.map +1 -0
  18. package/dist/benchmark/index.d.ts +6 -0
  19. package/dist/benchmark/index.js +23 -0
  20. package/dist/benchmark/index.js.map +1 -0
  21. package/dist/benchmark/parse-bench.d.ts +28 -0
  22. package/dist/benchmark/parse-bench.js +85 -0
  23. package/dist/benchmark/parse-bench.js.map +1 -0
  24. package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
  25. package/dist/buffer/ascii/ascii-parser.js +4 -1
  26. package/dist/buffer/ascii/ascii-parser.js.map +1 -1
  27. package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
  28. package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
  29. package/dist/buffer/ascii/tag-index.js.map +1 -1
  30. package/dist/buffer/msg-view.d.ts +1 -0
  31. package/dist/buffer/msg-view.js +25 -19
  32. package/dist/buffer/msg-view.js.map +1 -1
  33. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
  34. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
  35. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
  36. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
  37. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
  38. package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
  39. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
  40. package/dist/jsfix-bench.d.ts +1 -0
  41. package/dist/jsfix-bench.js +213 -0
  42. package/dist/jsfix-bench.js.map +1 -0
  43. package/dist/store/file-session-store.js +1 -1
  44. package/dist/store/file-session-store.js.map +1 -1
  45. package/dist/store/file-session-stream-provider.d.ts +1 -0
  46. package/dist/store/file-session-stream-provider.js +13 -13
  47. package/dist/store/file-session-stream-provider.js.map +1 -1
  48. package/dist/store/fix-session-store-factory.d.ts +1 -1
  49. package/dist/store/fix-session-store-factory.js.map +1 -1
  50. package/dist/store/fix-session-store.d.ts +11 -11
  51. package/dist/store/fix-session-store.js.map +1 -1
  52. package/dist/store/memory-session-store.js.map +1 -1
  53. package/dist/store/session-stream-provider.d.ts +13 -13
  54. package/dist/store/session-stream-provider.js.map +1 -1
  55. package/dist/transport/session/fix-clock.d.ts +1 -1
  56. package/dist/transport/session/fix-clock.js.map +1 -1
  57. package/dist/transport/session/resend-request-manager.d.ts +2 -2
  58. package/dist/transport/session/resend-request-manager.js.map +1 -1
  59. package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
  60. package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
  61. package/dist/transport/session/session-sequence-store.d.ts +3 -3
  62. package/dist/transport/session/session-sequence-store.js.map +1 -1
  63. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  64. package/docs/benchmarking.md +117 -0
  65. package/eslint.config.mjs +15 -1
  66. package/jsfix.test_client.txt +62 -62
  67. package/jsfix.test_server.txt +72 -72
  68. package/package.json +9 -1
  69. package/src/benchmark/bench-report.ts +101 -0
  70. package/src/benchmark/bench-runner.ts +196 -0
  71. package/src/benchmark/bench-stats.ts +86 -0
  72. package/src/benchmark/bench-types.ts +110 -0
  73. package/src/benchmark/gc-probe.ts +145 -0
  74. package/src/benchmark/index.ts +6 -0
  75. package/src/benchmark/parse-bench.ts +144 -0
  76. package/src/buffer/ascii/ascii-parser.ts +16 -1
  77. package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
  78. package/src/buffer/ascii/tag-index.ts +3 -3
  79. package/src/buffer/msg-view.ts +35 -18
  80. package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
  81. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
  82. package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
  83. package/src/jsfix-bench.ts +204 -0
  84. package/src/store/file-session-store.ts +2 -2
  85. package/src/store/file-session-stream-provider.ts +17 -11
  86. package/src/store/fix-session-store-factory.ts +1 -1
  87. package/src/store/fix-session-store.ts +11 -11
  88. package/src/store/memory-session-store.ts +1 -1
  89. package/src/store/session-stream-provider.ts +13 -13
  90. package/src/transport/session/fix-clock.ts +1 -1
  91. package/src/transport/session/resend-request-manager.ts +4 -4
  92. package/src/transport/session/session-sequence-coordinator.ts +1 -1
  93. package/src/transport/session/session-sequence-store.ts +3 -3
  94. 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
- The demo's `server-bounce` scenario does not pass, on this branch or on published
441
- 5.8.5. The acceptor's persisted sender sequence lags what it actually sent, because
442
- `AsciiSession.txOnEncoded` writes to the store fire-and-forget; if the process exits
443
- first the store is behind, and on restart the client sees a sequence below what it
444
- has already recorded and drops the session. Needs its own phase.
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"]}