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.
- package/.gitattributes +4 -0
- package/BACKPORT_PLAN.md +84 -0
- package/DEMO_PORT_PLAN.md +10 -0
- package/README.md +3 -0
- package/WORKLOG.md +119 -0
- package/dist/benchmark/bench-report.d.ts +5 -0
- package/dist/benchmark/bench-report.js +91 -0
- package/dist/benchmark/bench-report.js.map +1 -0
- package/dist/benchmark/bench-runner.d.ts +12 -0
- package/dist/benchmark/bench-runner.js +193 -0
- package/dist/benchmark/bench-runner.js.map +1 -0
- package/dist/benchmark/bench-stats.d.ts +10 -0
- package/dist/benchmark/bench-stats.js +56 -0
- package/dist/benchmark/bench-stats.js.map +1 -0
- package/dist/benchmark/bench-types.d.ts +74 -0
- package/dist/benchmark/bench-types.js +3 -0
- package/dist/benchmark/bench-types.js.map +1 -0
- package/dist/benchmark/gc-probe.d.ts +18 -0
- package/dist/benchmark/gc-probe.js +103 -0
- package/dist/benchmark/gc-probe.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.js +23 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/parse-bench.d.ts +28 -0
- package/dist/benchmark/parse-bench.js +85 -0
- package/dist/benchmark/parse-bench.js.map +1 -0
- package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
- package/dist/buffer/ascii/ascii-parser.js +4 -1
- package/dist/buffer/ascii/ascii-parser.js.map +1 -1
- package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
- package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
- package/dist/buffer/ascii/tag-index.js.map +1 -1
- package/dist/buffer/msg-view.d.ts +1 -0
- package/dist/buffer/msg-view.js +25 -19
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/config/js-fix-config.d.ts +4 -0
- package/dist/config/js-fix-config.js.map +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
- package/dist/jsfix-bench.d.ts +1 -0
- package/dist/jsfix-bench.js +213 -0
- package/dist/jsfix-bench.js.map +1 -0
- package/dist/runtime/session-launcher.js +1 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/runtime/session-scope.d.ts +3 -0
- package/dist/runtime/session-scope.js +47 -0
- package/dist/runtime/session-scope.js.map +1 -0
- package/dist/sample/http/oms/app.js +3 -3
- package/dist/sample/http/oms/app.js.map +1 -1
- package/dist/sample/tcp/qf-md/app.js +3 -3
- package/dist/sample/tcp/qf-md/app.js.map +1 -1
- package/dist/sample/tcp/skeleton/app.js +1 -1
- package/dist/sample/tcp/skeleton/app.js.map +1 -1
- package/dist/sample/tcp/trade-capture/app-launcher.js +3 -3
- package/dist/sample/tcp/trade-capture/app-launcher.js.map +1 -1
- package/dist/store/file-session-store.js +1 -1
- package/dist/store/file-session-store.js.map +1 -1
- package/dist/store/file-session-stream-provider.d.ts +1 -0
- package/dist/store/file-session-stream-provider.js +13 -13
- package/dist/store/file-session-stream-provider.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +1 -1
- package/dist/store/fix-session-store-factory.js.map +1 -1
- package/dist/store/fix-session-store.d.ts +11 -11
- package/dist/store/fix-session-store.js.map +1 -1
- package/dist/store/memory-session-store.js.map +1 -1
- package/dist/store/session-stream-provider.d.ts +13 -13
- package/dist/store/session-stream-provider.js.map +1 -1
- package/dist/transport/ascii/ascii-session.d.ts +17 -3
- package/dist/transport/ascii/ascii-session.js +121 -8
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/duplex/fix-duplex.d.ts +1 -0
- package/dist/transport/duplex/fix-duplex.js +3 -0
- package/dist/transport/duplex/fix-duplex.js.map +1 -1
- package/dist/transport/duplex/tcp-duplex.d.ts +1 -0
- package/dist/transport/duplex/tcp-duplex.js +5 -0
- package/dist/transport/duplex/tcp-duplex.js.map +1 -1
- package/dist/transport/factory/msg-transport.d.ts +2 -0
- package/dist/transport/factory/msg-transport.js +17 -2
- package/dist/transport/factory/msg-transport.js.map +1 -1
- package/dist/transport/session/a-session-msg-factory.d.ts +1 -0
- package/dist/transport/session/a-session-msg-factory.js +4 -0
- package/dist/transport/session/a-session-msg-factory.js.map +1 -1
- package/dist/transport/session/clone-session-description.d.ts +6 -0
- package/dist/transport/session/clone-session-description.js +11 -0
- package/dist/transport/session/clone-session-description.js.map +1 -0
- package/dist/transport/session/fix-clock.d.ts +1 -1
- package/dist/transport/session/fix-clock.js.map +1 -1
- package/dist/transport/session/fix-session.d.ts +5 -1
- package/dist/transport/session/fix-session.js +93 -24
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/index.d.ts +2 -0
- package/dist/transport/session/index.js +2 -0
- package/dist/transport/session/index.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +2 -2
- package/dist/transport/session/resend-request-manager.js.map +1 -1
- package/dist/transport/session/session-msg-factory.d.ts +1 -0
- package/dist/transport/session/session-msg-factory.js.map +1 -1
- package/dist/transport/session/session-registry.d.ts +23 -0
- package/dist/transport/session/session-registry.js +59 -0
- package/dist/transport/session/session-registry.js.map +1 -0
- package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
- package/dist/transport/session/session-sequence-store.d.ts +3 -3
- package/dist/transport/session/session-sequence-store.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor-listener.d.ts +3 -0
- package/dist/transport/tcp/tcp-acceptor-listener.js +26 -4
- package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor.d.ts +5 -0
- package/dist/transport/tcp/tcp-acceptor.js +74 -20
- package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js +15 -5
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/dist/transport/tcp/tcp-transport-description.d.ts +1 -0
- package/dist/transport/tcp/tcp-transport-description.js.map +1 -1
- package/dist/transport/tcp/tls-diagnostics.d.ts +5 -0
- package/dist/transport/tcp/tls-diagnostics.js +64 -0
- package/dist/transport/tcp/tls-diagnostics.js.map +1 -0
- package/dist/transport/tcp/tls-options-factory.d.ts +1 -0
- package/dist/transport/tcp/tls-options-factory.js +24 -2
- package/dist/transport/tcp/tls-options-factory.js.map +1 -1
- package/docs/acceptor.md +207 -0
- package/docs/benchmarking.md +117 -0
- package/eslint.config.mjs +15 -1
- package/jsfix.test_client.txt +62 -75
- package/jsfix.test_server.txt +72 -72
- package/package.json +19 -10
- package/script/pack-local.js +68 -0
- package/src/benchmark/bench-report.ts +101 -0
- package/src/benchmark/bench-runner.ts +196 -0
- package/src/benchmark/bench-stats.ts +86 -0
- package/src/benchmark/bench-types.ts +110 -0
- package/src/benchmark/gc-probe.ts +145 -0
- package/src/benchmark/index.ts +6 -0
- package/src/benchmark/parse-bench.ts +144 -0
- package/src/buffer/ascii/ascii-parser.ts +16 -1
- package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
- package/src/buffer/ascii/tag-index.ts +3 -3
- package/src/buffer/msg-view.ts +35 -18
- package/src/config/js-fix-config.ts +8 -0
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
- package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
- package/src/jsfix-bench.ts +204 -0
- package/src/runtime/session-launcher.ts +5 -1
- package/src/runtime/session-scope.ts +84 -0
- package/src/sample/http/oms/app.ts +3 -3
- package/src/sample/tcp/qf-md/app.ts +3 -3
- package/src/sample/tcp/skeleton/app.ts +1 -1
- package/src/sample/tcp/trade-capture/app-launcher.ts +3 -3
- package/src/store/file-session-store.ts +2 -2
- package/src/store/file-session-stream-provider.ts +17 -11
- package/src/store/fix-session-store-factory.ts +1 -1
- package/src/store/fix-session-store.ts +11 -11
- package/src/store/memory-session-store.ts +1 -1
- package/src/store/session-stream-provider.ts +13 -13
- package/src/transport/ascii/ascii-session.ts +186 -11
- package/src/transport/duplex/fix-duplex.ts +9 -0
- package/src/transport/duplex/tcp-duplex.ts +10 -0
- package/src/transport/factory/msg-transport.ts +20 -0
- package/src/transport/session/a-session-msg-factory.ts +18 -0
- package/src/transport/session/clone-session-description.ts +41 -0
- package/src/transport/session/fix-clock.ts +1 -1
- package/src/transport/session/fix-session.ts +136 -25
- package/src/transport/session/index.ts +2 -0
- package/src/transport/session/resend-request-manager.ts +4 -4
- package/src/transport/session/session-msg-factory.ts +13 -0
- package/src/transport/session/session-registry.ts +112 -0
- package/src/transport/session/session-sequence-coordinator.ts +1 -1
- package/src/transport/session/session-sequence-store.ts +3 -3
- package/src/transport/tcp/tcp-acceptor-listener.ts +43 -5
- package/src/transport/tcp/tcp-acceptor.ts +92 -18
- package/src/transport/tcp/tcp-initiator.ts +26 -5
- package/src/transport/tcp/tcp-transport-description.ts +7 -0
- package/src/transport/tcp/tls-diagnostics.ts +76 -0
- package/src/transport/tcp/tls-options-factory.ts +34 -2
package/.gitattributes
ADDED
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":"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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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,kCAWC;AApCD,uCAAwB;AACxB,yCAAoC;AACpC,+CAAyC;AAWzC,MAAM,cAAc,GAAG,GAAG,CAAA;AAC1B,MAAM,oBAAoB,GAAG,GAAG,CAAA;AAEnB,QAAA,mBAAmB,GAAkB;IAGhD,gBAAgB,EAAE,KAAK;IACvB,MAAM,EAAE,EAAE;IACV,kBAAkB,EAAE,KAAK;IACzB,eAAe,EAAE,IAAI;CACtB,CAAA;AAED,SAAgB,WAAW;IACzB,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,EAAE,CAAA;IACtB,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,OAAO;QACrB,EAAE,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE;QACvB,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE;QACxC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,GAAG,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;QACvD,QAAQ,EAAE,IAAI,CAAC,MAAM;QACrB,iBAAiB,EAAE,kBAAO,CAAC,iBAAiB,EAAE;KAC/C,CAAA;AACH,CAAC;AAGD,SAAe,IAAI;;QACjB,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;CAAA;AAQD,SAAe,KAAK;yDAAE,KAAc,EAAE,WAAmB,EAAE;QACzD,IAAI,UAAU,GAAG,CAAC,CAAA;QAClB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,QAAQ,IAAI,UAAU,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC;YAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;YAC3B,MAAM,IAAI,EAAE,CAAA;YACZ,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3D,CAAC;IACH,CAAC;CAAA;AAgBD,MAAa,WAAW;IACtB,YAA8B,UAAyB,2BAAmB;QAA5C,YAAO,GAAP,OAAO,CAAqC;IAC1E,CAAC;IAEY,GAAG,CAAE,SAAqB;;;YACrC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;YAC5B,MAAA,SAAS,CAAC,KAAK,yDAAI,CAAA;YACnB,IAAI,CAAC;gBACH,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;gBACtB,MAAM,MAAM,GAAmB,EAAE,CAAA;gBACjC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC;oBACpD,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;wBAG5B,kBAAO,CAAC,OAAO,EAAE,CAAA;oBACnB,CAAC;oBACD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAA;gBAClD,CAAC;gBACD,MAAM,MAAM,GAAG,IAAA,uBAAS,EAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAA;gBAC9D,OAAO;oBACL,IAAI,EAAE,SAAS,CAAC,IAAI;oBACpB,WAAW,EAAE,SAAS,CAAC,WAAW;oBAClC,OAAO;oBACP,MAAM;oBACN,MAAM;oBACN,mBAAmB,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,GAAG,MAAM,CAAC,MAAM;oBAC7E,MAAM,EAAE,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC;iBAC5C,CAAA;YACH,CAAC;oBAAS,CAAC;gBACT,MAAA,SAAS,CAAC,QAAQ,yDAAI,CAAA;YACxB,CAAC;QACH,CAAC;KAAA;IAEY,MAAM,CAAE,KAAa,EAAE,KAAmB;;YACrD,MAAM,OAAO,GAAmB,EAAE,CAAA;YAClC,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;gBAC9B,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;YACzC,CAAC;YACD,OAAO;gBACL,KAAK;gBAGL,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACpC,WAAW,EAAE,WAAW,EAAE;gBAC1B,OAAO;aACR,CAAA;QACH,CAAC;KAAA;IAUa,aAAa,CAAE,SAAqB;;YAIhD,kBAAO,CAAC,OAAO,EAAE,CAAA;YACjB,MAAM,IAAI,EAAE,CAAA;YAEZ,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAA;YACjD,MAAM,KAAK,GAAG,IAAI,kBAAO,EAAE,CAAA;YAC3B,KAAK,CAAC,KAAK,EAAE,CAAA;YACb,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;YACjE,MAAM,oBAAoB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAA;YAC3D,MAAM,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;YAEvB,kBAAO,CAAC,OAAO,EAAE,CAAA;YACjB,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAA;YAEhD,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,CAAA;YAChD,MAAM,UAAU,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,oBAAoB,GAAG,UAAU,CAAA;YAC3E,OAAO;gBACL,UAAU;gBACV,iBAAiB;gBACjB,yBAAyB,EAAE,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,GAAG,UAAU;gBAChF,oBAAoB;gBACpB,EAAE;gBACF,2BAA2B,EAAE,EAAE,CAAC,KAAK,GAAG,UAAU;gBAClD,2BAA2B,EAAE,EAAE,CAAC,KAAK,GAAG,UAAU;gBAClD,6BAA6B,EAAE,EAAE,CAAC,YAAY,GAAG,UAAU;gBAC3D,iBAAiB,EAAE,kBAAO,CAAC,iBAAiB,EAAE;aAC/C,CAAA;QACH,CAAC;KAAA;IAEO,MAAM,CAAE,SAAqB;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAA;QAC5C,IAAI,MAAM,IAAI,CAAC;YAAE,OAAM;QACvB,IAAI,IAAI,GAAG,CAAC,CAAA;QAGZ,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAA;QAC5E,OAAO,IAAI,GAAG,MAAM,EAAE,CAAC;YACrB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAA;YAChD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAG1C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,CAAC,IAAI,eAAe,SAAS,2CAA2C,SAAS,EAAE,CAAC,CAAA;YAClH,CAAC;YACD,IAAI,IAAI,SAAS,CAAA;QACnB,CAAC;IACH,CAAC;IAEO,YAAY,CAAE,SAAqB,EAAE,KAAa;QACxD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAA;QACjD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAA;QACzC,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC3C,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;QAChE,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,CAAC,IAAI,eAAe,SAAS,wBAAwB,KAAK,kBAAkB,UAAU,EAAE,CAAC,CAAA;QACvH,CAAC;QACD,OAAO;YACL,KAAK;YACL,UAAU;YACV,YAAY;YACZ,iBAAiB,EAAE,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,UAAU;SACpE,CAAA;IACH,CAAC;CACF;AA3HD,kCA2HC","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;
|