jspurefix 5.8.5 → 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 (180) hide show
  1. package/.gitattributes +4 -0
  2. package/BACKPORT_PLAN.md +84 -0
  3. package/DEMO_PORT_PLAN.md +10 -0
  4. package/README.md +3 -0
  5. package/WORKLOG.md +119 -0
  6. package/dist/benchmark/bench-report.d.ts +5 -0
  7. package/dist/benchmark/bench-report.js +91 -0
  8. package/dist/benchmark/bench-report.js.map +1 -0
  9. package/dist/benchmark/bench-runner.d.ts +12 -0
  10. package/dist/benchmark/bench-runner.js +193 -0
  11. package/dist/benchmark/bench-runner.js.map +1 -0
  12. package/dist/benchmark/bench-stats.d.ts +10 -0
  13. package/dist/benchmark/bench-stats.js +56 -0
  14. package/dist/benchmark/bench-stats.js.map +1 -0
  15. package/dist/benchmark/bench-types.d.ts +74 -0
  16. package/dist/benchmark/bench-types.js +3 -0
  17. package/dist/benchmark/bench-types.js.map +1 -0
  18. package/dist/benchmark/gc-probe.d.ts +18 -0
  19. package/dist/benchmark/gc-probe.js +103 -0
  20. package/dist/benchmark/gc-probe.js.map +1 -0
  21. package/dist/benchmark/index.d.ts +6 -0
  22. package/dist/benchmark/index.js +23 -0
  23. package/dist/benchmark/index.js.map +1 -0
  24. package/dist/benchmark/parse-bench.d.ts +28 -0
  25. package/dist/benchmark/parse-bench.js +85 -0
  26. package/dist/benchmark/parse-bench.js.map +1 -0
  27. package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
  28. package/dist/buffer/ascii/ascii-parser.js +4 -1
  29. package/dist/buffer/ascii/ascii-parser.js.map +1 -1
  30. package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
  31. package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
  32. package/dist/buffer/ascii/tag-index.js.map +1 -1
  33. package/dist/buffer/msg-view.d.ts +1 -0
  34. package/dist/buffer/msg-view.js +25 -19
  35. package/dist/buffer/msg-view.js.map +1 -1
  36. package/dist/config/js-fix-config.d.ts +4 -0
  37. package/dist/config/js-fix-config.js.map +1 -1
  38. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
  39. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
  40. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
  41. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
  42. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
  43. package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
  44. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
  45. package/dist/jsfix-bench.d.ts +1 -0
  46. package/dist/jsfix-bench.js +213 -0
  47. package/dist/jsfix-bench.js.map +1 -0
  48. package/dist/runtime/session-launcher.js +1 -1
  49. package/dist/runtime/session-launcher.js.map +1 -1
  50. package/dist/runtime/session-scope.d.ts +3 -0
  51. package/dist/runtime/session-scope.js +47 -0
  52. package/dist/runtime/session-scope.js.map +1 -0
  53. package/dist/sample/http/oms/app.js +3 -3
  54. package/dist/sample/http/oms/app.js.map +1 -1
  55. package/dist/sample/tcp/qf-md/app.js +3 -3
  56. package/dist/sample/tcp/qf-md/app.js.map +1 -1
  57. package/dist/sample/tcp/skeleton/app.js +1 -1
  58. package/dist/sample/tcp/skeleton/app.js.map +1 -1
  59. package/dist/sample/tcp/trade-capture/app-launcher.js +3 -3
  60. package/dist/sample/tcp/trade-capture/app-launcher.js.map +1 -1
  61. package/dist/store/file-session-store.js +1 -1
  62. package/dist/store/file-session-store.js.map +1 -1
  63. package/dist/store/file-session-stream-provider.d.ts +1 -0
  64. package/dist/store/file-session-stream-provider.js +13 -13
  65. package/dist/store/file-session-stream-provider.js.map +1 -1
  66. package/dist/store/fix-session-store-factory.d.ts +1 -1
  67. package/dist/store/fix-session-store-factory.js.map +1 -1
  68. package/dist/store/fix-session-store.d.ts +11 -11
  69. package/dist/store/fix-session-store.js.map +1 -1
  70. package/dist/store/memory-session-store.js.map +1 -1
  71. package/dist/store/session-stream-provider.d.ts +13 -13
  72. package/dist/store/session-stream-provider.js.map +1 -1
  73. package/dist/transport/ascii/ascii-session.d.ts +17 -3
  74. package/dist/transport/ascii/ascii-session.js +121 -8
  75. package/dist/transport/ascii/ascii-session.js.map +1 -1
  76. package/dist/transport/duplex/fix-duplex.d.ts +1 -0
  77. package/dist/transport/duplex/fix-duplex.js +3 -0
  78. package/dist/transport/duplex/fix-duplex.js.map +1 -1
  79. package/dist/transport/duplex/tcp-duplex.d.ts +1 -0
  80. package/dist/transport/duplex/tcp-duplex.js +5 -0
  81. package/dist/transport/duplex/tcp-duplex.js.map +1 -1
  82. package/dist/transport/factory/msg-transport.d.ts +2 -0
  83. package/dist/transport/factory/msg-transport.js +17 -2
  84. package/dist/transport/factory/msg-transport.js.map +1 -1
  85. package/dist/transport/session/a-session-msg-factory.d.ts +1 -0
  86. package/dist/transport/session/a-session-msg-factory.js +4 -0
  87. package/dist/transport/session/a-session-msg-factory.js.map +1 -1
  88. package/dist/transport/session/clone-session-description.d.ts +6 -0
  89. package/dist/transport/session/clone-session-description.js +11 -0
  90. package/dist/transport/session/clone-session-description.js.map +1 -0
  91. package/dist/transport/session/fix-clock.d.ts +1 -1
  92. package/dist/transport/session/fix-clock.js.map +1 -1
  93. package/dist/transport/session/fix-session.d.ts +5 -1
  94. package/dist/transport/session/fix-session.js +93 -24
  95. package/dist/transport/session/fix-session.js.map +1 -1
  96. package/dist/transport/session/index.d.ts +2 -0
  97. package/dist/transport/session/index.js +2 -0
  98. package/dist/transport/session/index.js.map +1 -1
  99. package/dist/transport/session/resend-request-manager.d.ts +2 -2
  100. package/dist/transport/session/resend-request-manager.js.map +1 -1
  101. package/dist/transport/session/session-msg-factory.d.ts +1 -0
  102. package/dist/transport/session/session-msg-factory.js.map +1 -1
  103. package/dist/transport/session/session-registry.d.ts +23 -0
  104. package/dist/transport/session/session-registry.js +59 -0
  105. package/dist/transport/session/session-registry.js.map +1 -0
  106. package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
  107. package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
  108. package/dist/transport/session/session-sequence-store.d.ts +3 -3
  109. package/dist/transport/session/session-sequence-store.js.map +1 -1
  110. package/dist/transport/tcp/tcp-acceptor-listener.d.ts +3 -0
  111. package/dist/transport/tcp/tcp-acceptor-listener.js +26 -4
  112. package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
  113. package/dist/transport/tcp/tcp-acceptor.d.ts +5 -0
  114. package/dist/transport/tcp/tcp-acceptor.js +74 -20
  115. package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
  116. package/dist/transport/tcp/tcp-initiator.js +15 -5
  117. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  118. package/dist/transport/tcp/tcp-transport-description.d.ts +1 -0
  119. package/dist/transport/tcp/tcp-transport-description.js.map +1 -1
  120. package/dist/transport/tcp/tls-diagnostics.d.ts +5 -0
  121. package/dist/transport/tcp/tls-diagnostics.js +64 -0
  122. package/dist/transport/tcp/tls-diagnostics.js.map +1 -0
  123. package/dist/transport/tcp/tls-options-factory.d.ts +1 -0
  124. package/dist/transport/tcp/tls-options-factory.js +24 -2
  125. package/dist/transport/tcp/tls-options-factory.js.map +1 -1
  126. package/docs/acceptor.md +207 -0
  127. package/docs/benchmarking.md +117 -0
  128. package/eslint.config.mjs +15 -1
  129. package/jsfix.test_client.txt +62 -75
  130. package/jsfix.test_server.txt +72 -72
  131. package/package.json +19 -10
  132. package/script/pack-local.js +68 -0
  133. package/src/benchmark/bench-report.ts +101 -0
  134. package/src/benchmark/bench-runner.ts +196 -0
  135. package/src/benchmark/bench-stats.ts +86 -0
  136. package/src/benchmark/bench-types.ts +110 -0
  137. package/src/benchmark/gc-probe.ts +145 -0
  138. package/src/benchmark/index.ts +6 -0
  139. package/src/benchmark/parse-bench.ts +144 -0
  140. package/src/buffer/ascii/ascii-parser.ts +16 -1
  141. package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
  142. package/src/buffer/ascii/tag-index.ts +3 -3
  143. package/src/buffer/msg-view.ts +35 -18
  144. package/src/config/js-fix-config.ts +8 -0
  145. package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
  146. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
  147. package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
  148. package/src/jsfix-bench.ts +204 -0
  149. package/src/runtime/session-launcher.ts +5 -1
  150. package/src/runtime/session-scope.ts +84 -0
  151. package/src/sample/http/oms/app.ts +3 -3
  152. package/src/sample/tcp/qf-md/app.ts +3 -3
  153. package/src/sample/tcp/skeleton/app.ts +1 -1
  154. package/src/sample/tcp/trade-capture/app-launcher.ts +3 -3
  155. package/src/store/file-session-store.ts +2 -2
  156. package/src/store/file-session-stream-provider.ts +17 -11
  157. package/src/store/fix-session-store-factory.ts +1 -1
  158. package/src/store/fix-session-store.ts +11 -11
  159. package/src/store/memory-session-store.ts +1 -1
  160. package/src/store/session-stream-provider.ts +13 -13
  161. package/src/transport/ascii/ascii-session.ts +186 -11
  162. package/src/transport/duplex/fix-duplex.ts +9 -0
  163. package/src/transport/duplex/tcp-duplex.ts +10 -0
  164. package/src/transport/factory/msg-transport.ts +20 -0
  165. package/src/transport/session/a-session-msg-factory.ts +18 -0
  166. package/src/transport/session/clone-session-description.ts +41 -0
  167. package/src/transport/session/fix-clock.ts +1 -1
  168. package/src/transport/session/fix-session.ts +136 -25
  169. package/src/transport/session/index.ts +2 -0
  170. package/src/transport/session/resend-request-manager.ts +4 -4
  171. package/src/transport/session/session-msg-factory.ts +13 -0
  172. package/src/transport/session/session-registry.ts +112 -0
  173. package/src/transport/session/session-sequence-coordinator.ts +1 -1
  174. package/src/transport/session/session-sequence-store.ts +3 -3
  175. package/src/transport/tcp/tcp-acceptor-listener.ts +43 -5
  176. package/src/transport/tcp/tcp-acceptor.ts +92 -18
  177. package/src/transport/tcp/tcp-initiator.ts +26 -5
  178. package/src/transport/tcp/tcp-transport-description.ts +7 -0
  179. package/src/transport/tcp/tls-diagnostics.ts +76 -0
  180. package/src/transport/tcp/tls-options-factory.ts +34 -2
package/.gitattributes ADDED
@@ -0,0 +1,4 @@
1
+ # pem encoded test fixtures - keep byte for byte identical on every platform rather than
2
+ # letting eol normalisation rewrite them on checkout.
3
+ *.crt -text
4
+ *.key -text
package/BACKPORT_PLAN.md CHANGED
@@ -372,6 +372,90 @@ The parser is the foundation of the entire system. Test strategy must be compreh
372
372
  3. **Micro-dictionary tests** (future hardening): use Trim to create single-message dictionaries, then mutate them (remove fields, duplicate tags, etc.) to test validator edge cases
373
373
  4. **Regression anchor**: snapshot the `FixDefinitions` output for each FIX version before any changes — tests assert against snapshots
374
374
 
375
+
376
+ ---
377
+
378
+ ## Phase 7: Acceptor Hardening (issue #153)
379
+
380
+ **Priority: High | Risk: Medium | Scope: `src/transport/`, `src/runtime/`**
381
+
382
+ ### Problem
383
+
384
+ The acceptor was written as a smoke-test harness for the initiator and was never
385
+ multi-client safe. [Issue #153](https://github.com/TimelordUK/jspurefix/issues/153)
386
+ reported the visible symptom - a counterparty's socket went half open (no FIN, no
387
+ RST), the counterparty reconnected, and the library had no coherent answer for what
388
+ should happen to the first session - and correctly identified a concrete bug behind
389
+ it: `FixSession.subscribe()` attached closures while `unsubscribe()` removed method
390
+ references, so unsubscribe removed nothing.
391
+
392
+ Underneath that sat four more:
393
+
394
+ | Defect | Consequence |
395
+ |--------|-------------|
396
+ | `ParseBuffer` / `TransmitBuffer` are registerInstance singletons | concurrent clients interleave bytes in one buffer |
397
+ | one shared `ISessionDescription` per listener | every accepted session computes the same `SessionId`, so one store |
398
+ | no session registry | a reconnect runs alongside its own stale session |
399
+ | no wildcard `TargetCompID` | an acceptor can only serve statically configured counterparties |
400
+
401
+ ### C# Solution
402
+
403
+ | File | Purpose |
404
+ |------|---------|
405
+ | `PureFix.Transport/Session/SessionRegistry.cs` | one live session per SessionId, stop the old on reconnect |
406
+ | `PureFix.Transport/Session/FixSession.cs` | `RequestStop`, `OnSessionStopping` |
407
+ | `PureFix.Transport/Ascii/AsciiSession.cs` | wildcard TargetCompID adopted from the peer Logon |
408
+ | `purefix-standalone-demo/Infrastructure/TradeCaptureSessionFactory.cs` | per-session config, parser and encoder |
409
+
410
+ ### TypeScript Solution
411
+
412
+ 1. **Transport subscription** — bound handlers held in an `ITransportSubscription`
413
+ alongside the transport they attached to; events from a superseded transport are
414
+ ignored and logged. `terminate()` also informs the application via `onStopped`,
415
+ which it never did, so application timers leaked on every broken connection.
416
+ 2. **Session scopes** — `makeSessionScope` builds a tsyringe child container per
417
+ connection with fresh buffers, a cloned description and a matching message
418
+ factory. `SessionLauncher` passes the resolving container's config to
419
+ `EngineFactory.makeSession`.
420
+ 3. **SessionRegistry** — ported, with identity-checked unregister so a displaced
421
+ session cannot evict its successor.
422
+ 4. **Wildcard TargetCompID** — bound *after* the Logon rather than at construction,
423
+ so each counterparty gets its own store file. cspurefix binds in the constructor
424
+ and therefore keys a wildcard store on the literal `*`; this is the one place the
425
+ TS version deliberately improves on the C#.
426
+ 5. **Transport lifecycle** — TCP keep-alive, `FixDuplex.destroy()`, linger-then-destroy
427
+ on `end()`, harvest on socket `close`, and a census line whenever the connected
428
+ population changes.
429
+
430
+ See [`docs/acceptor.md`](docs/acceptor.md) for the resulting model.
431
+
432
+ ### Status: **DONE**
433
+
434
+ Note this supersedes phase D6 of `DEMO_PORT_PLAN.md`, which had placed the registry
435
+ and wildcard handling in the demo. Issue #153 showed they belong in the engine - the
436
+ reporter hit them without writing a demo.
437
+
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.
458
+
375
459
  ---
376
460
 
377
461
  ## General Principles
package/DEMO_PORT_PLAN.md CHANGED
@@ -154,6 +154,16 @@ The good news: many of the underlying engine bugs are already fixed in jspurefix
154
154
 
155
155
  ### PR D6: Multi-client + session registry (medium-high risk)
156
156
 
157
+ > **Superseded, and re-scoped.** Everything below assumed the session registry, the
158
+ > wildcard `TargetCompID` handling and the per-session parser were application-level
159
+ > concerns. Issue [#153](https://github.com/TimelordUK/jspurefix/issues/153) showed
160
+ > otherwise: the reporter hit all of them without writing a demo at all. They now
161
+ > live in jspurefix itself - see Phase 7 of `BACKPORT_PLAN.md` and
162
+ > `docs/acceptor.md`. What remains for the demo is configuration and demonstration:
163
+ > a `multi-client` mode with a `TargetCompID: "*"` acceptor config, `--clients <n>`
164
+ > to spawn suffixed initiators, and the `multi-client` and `stale-transport`
165
+ > scenarios. **Done** in jspf-demo.
166
+
157
167
  **Goal**: Support multiple concurrent clients to the same acceptor without state corruption. This is where the hardest soak-test bugs lived in C#.
158
168
 
159
169
  **Note**: the C# `SessionRegistry` is itself work-in-progress (lost when leaving the hedge fund where the original was written). The broader vision is loading ~20 broker dictionaries and using first-message metadata to dispatch — that's a future expansion. For D6 we're building the **minimum viable registry** to fix the stale-transport bug.
package/README.md CHANGED
@@ -34,6 +34,7 @@ A fast, fully native TypeScript [FIX protocol](https://www.fixtrading.org/) engi
34
34
  - Repeating groups, components, nested structures and raw data fields
35
35
  - Dictionary-driven: load QuickFIX XML or FIX repository definitions, compile typed interfaces
36
36
  - Full session lifecycle: logon, heartbeats, test requests, resend requests, logout
37
+ - Multi-client acceptor: per-connection isolation, one session per SessionId, wildcard `TargetCompID` — see [docs/acceptor.md](docs/acceptor.md)
37
38
  - TLS-encrypted sessions over TCP
38
39
  - Pluggable persistent message store (in-memory or file) with sequence recovery
39
40
  - HTTP initiator/acceptor for FIXML
@@ -51,6 +52,8 @@ cd node_modules/jspurefix && npm run unzip-repo
51
52
 
52
53
  A standalone demo project lives at [TimelordUK/jspf-demo](https://github.com/TimelordUK/jspf-demo) — the fastest way to see a working initiator/acceptor.
53
54
 
55
+ To try an unpublished build of this library in your own project, `npm run pack:local` builds and packs a tarball you can `npm install` directly.
56
+
54
57
  ## Examples
55
58
 
56
59
  | description | link |
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":";;AAuBA,8CAWC;AAED,oCAsBC;AAED,kCAGC;AASD,4CA4BC;AApGD,+CAAuC;AAGvC,MAAM,UAAU,GAAG,IAAI,CAAA;AACvB,MAAM,UAAU,GAAG,UAAU,GAAG,IAAI,CAAA;AACpC,MAAM,aAAa,GAAG,GAAG,CAAA;AAEzB,SAAS,MAAM,CAAE,KAAa;IAC5B,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,EAAE,CAAE,KAAa;IACxB,OAAO,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AACxC,CAAC;AAED,SAAS,SAAS,CAAE,CAAS;IAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAA;AAC9C,CAAC;AAED,SAAS,MAAM,CAAE,CAAS,EAAE,SAAiB,CAAC;IAC5C,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAA;AACnD,CAAC;AAED,SAAgB,iBAAiB,CAAE,KAAwB;IACzD,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAA;IAC7B,MAAM,KAAK,GAAG;QACZ,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,UAAU,EAAE;QACrC,QAAQ,GAAG,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE;QAC7D,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,QAAQ,EAAE;KAC9B,CAAA;IACD,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,uFAAuF,CAAC,CAAA;IACrG,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,SAAgB,YAAY,CAAE,MAAoB;IAChD,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAA;IACvB,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAA;IACvB,MAAM,KAAK,GAAG,CAAC,CAAC,sBAAsB,GAAG,GAAG,CAAA;IAC5C,MAAM,KAAK,GAAG;QACZ,GAAG,MAAM,CAAC,IAAI,EAAE;QAChB,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE;QACnD,mBAAmB,CAAC,CAAC,OAAO,MAAM,MAAM,CAAC,OAAO,CAAC,kBAAkB,OAAO;YACxE,YAAY,MAAM,CAAC,OAAO,CAAC,gBAAgB,GAAG;QAChD,0BAA0B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;YACrE,UAAU,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;QACtD,mBAAmB,SAAS,CAAC,MAAM,CAAC,mBAAmB,CAAC,UAAU;QAClE,mBAAmB,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wDAAwD;QAC3F,mBAAmB,CAAC,CAAC,UAAU,wBAAwB,CAAC,CAAC,EAAE,CAAC,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,KAAK,UAAU;YAC9F,GAAG,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;QACzF,mBAAmB,CAAC,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB;YAC3E,GAAG,CAAC,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB;YAC7D,GAAG,CAAC,CAAC,6BAA6B,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB;QACjE,mBAAmB,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,OAAO;YAC/C,GAAG,CAAC,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC,oCAAoC,EAAE,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI;KAC9G,CAAA;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACnD,CAAC;AAED,SAAgB,WAAW,CAAE,KAAwB;IACnD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAC9C,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AAC/D,CAAC;AASD,SAAgB,gBAAgB,CAAE,QAA2B,EAAE,SAA4B;IACzF,MAAM,KAAK,GAAa,CAAC,YAAY,QAAQ,CAAC,KAAK,KAAK,QAAQ,CAAC,UAAU,GAAG,EAAE,EAAE,CAAC,CAAA;IACnF,IAAI,QAAQ,CAAC,WAAW,CAAC,GAAG,KAAK,SAAS,CAAC,WAAW,CAAC,GAAG;QACtD,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC7D,KAAK,CAAC,IAAI,CAAC,8FAA8F,CAAC,CAAA;QAC1G,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChB,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAA;QACjE,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,eAAe,CAAC,CAAA;YACzC,SAAQ;QACV,CAAC;QACD,MAAM,MAAM,GAAG,IAAA,qBAAO,EAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;QACpD,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW;YAChC,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;YACjD,CAAC,CAAC,OAAO,CAAA;QACX,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QAC5B,KAAK,CAAC,IAAI,CAAC,eAAe,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI;YAC7F,MAAM,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,OAAO,EAAE;YACjD,sBAAsB,MAAM,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACpE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,2BAA2B,CAAA;QAC1D,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,2BAA2B,CAAA;QACzD,MAAM,OAAO,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAA;QAC5E,KAAK,CAAC,IAAI,CAAC,eAAe,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC1G,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC","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;