jspurefix 5.9.1 → 5.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKPORT_PLAN.md +20 -7
- package/WORKLOG.md +119 -0
- package/dist/benchmark/bench-report.d.ts +5 -0
- package/dist/benchmark/bench-report.js +91 -0
- package/dist/benchmark/bench-report.js.map +1 -0
- package/dist/benchmark/bench-runner.d.ts +12 -0
- package/dist/benchmark/bench-runner.js +193 -0
- package/dist/benchmark/bench-runner.js.map +1 -0
- package/dist/benchmark/bench-stats.d.ts +10 -0
- package/dist/benchmark/bench-stats.js +56 -0
- package/dist/benchmark/bench-stats.js.map +1 -0
- package/dist/benchmark/bench-types.d.ts +74 -0
- package/dist/benchmark/bench-types.js +3 -0
- package/dist/benchmark/bench-types.js.map +1 -0
- package/dist/benchmark/gc-probe.d.ts +18 -0
- package/dist/benchmark/gc-probe.js +103 -0
- package/dist/benchmark/gc-probe.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.js +23 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/parse-bench.d.ts +28 -0
- package/dist/benchmark/parse-bench.js +85 -0
- package/dist/benchmark/parse-bench.js.map +1 -0
- package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
- package/dist/buffer/ascii/ascii-parser.js +4 -1
- package/dist/buffer/ascii/ascii-parser.js.map +1 -1
- package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
- package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
- package/dist/buffer/ascii/tag-index.js.map +1 -1
- package/dist/buffer/msg-view.d.ts +1 -0
- package/dist/buffer/msg-view.js +25 -19
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
- package/dist/jsfix-bench.d.ts +1 -0
- package/dist/jsfix-bench.js +213 -0
- package/dist/jsfix-bench.js.map +1 -0
- package/dist/store/file-session-store.js +1 -1
- package/dist/store/file-session-store.js.map +1 -1
- package/dist/store/file-session-stream-provider.d.ts +1 -0
- package/dist/store/file-session-stream-provider.js +13 -13
- package/dist/store/file-session-stream-provider.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +1 -1
- package/dist/store/fix-session-store-factory.js.map +1 -1
- package/dist/store/fix-session-store.d.ts +11 -11
- package/dist/store/fix-session-store.js.map +1 -1
- package/dist/store/memory-session-store.js.map +1 -1
- package/dist/store/session-stream-provider.d.ts +13 -13
- package/dist/store/session-stream-provider.js.map +1 -1
- package/dist/transport/session/fix-clock.d.ts +1 -1
- package/dist/transport/session/fix-clock.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +2 -2
- package/dist/transport/session/resend-request-manager.js.map +1 -1
- package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
- package/dist/transport/session/session-sequence-store.d.ts +3 -3
- package/dist/transport/session/session-sequence-store.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/docs/benchmarking.md +117 -0
- package/eslint.config.mjs +15 -1
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +9 -1
- package/src/benchmark/bench-report.ts +101 -0
- package/src/benchmark/bench-runner.ts +196 -0
- package/src/benchmark/bench-stats.ts +86 -0
- package/src/benchmark/bench-types.ts +110 -0
- package/src/benchmark/gc-probe.ts +145 -0
- package/src/benchmark/index.ts +6 -0
- package/src/benchmark/parse-bench.ts +144 -0
- package/src/buffer/ascii/ascii-parser.ts +16 -1
- package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
- package/src/buffer/ascii/tag-index.ts +3 -3
- package/src/buffer/msg-view.ts +35 -18
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
- package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
- package/src/jsfix-bench.ts +204 -0
- package/src/store/file-session-store.ts +2 -2
- package/src/store/file-session-stream-provider.ts +17 -11
- package/src/store/fix-session-store-factory.ts +1 -1
- package/src/store/fix-session-store.ts +11 -11
- package/src/store/memory-session-store.ts +1 -1
- package/src/store/session-stream-provider.ts +13 -13
- package/src/transport/session/fix-clock.ts +1 -1
- package/src/transport/session/resend-request-manager.ts +4 -4
- package/src/transport/session/session-sequence-coordinator.ts +1 -1
- package/src/transport/session/session-sequence-store.ts +3 -3
- package/src/transport/tcp/tcp-initiator.ts +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unit of work the runner can drive. run is handed the number of operations
|
|
3
|
+
* to perform and returns how many it actually completed, which the runner
|
|
4
|
+
* checks - a case that silently does less work than asked would otherwise look
|
|
5
|
+
* like a large improvement.
|
|
6
|
+
*/
|
|
7
|
+
export interface IBenchCase {
|
|
8
|
+
readonly name: string
|
|
9
|
+
readonly description?: string
|
|
10
|
+
/** called once before warmup, outside all timing */
|
|
11
|
+
setup?: () => void
|
|
12
|
+
run: (operations: number) => number
|
|
13
|
+
teardown?: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IBenchOptions {
|
|
17
|
+
/** operations run and discarded before measuring, to let v8 tier the code up */
|
|
18
|
+
readonly warmupOperations: number
|
|
19
|
+
/** measured rounds - each contributes one sample */
|
|
20
|
+
readonly rounds: number
|
|
21
|
+
readonly operationsPerRound: number
|
|
22
|
+
/** collect between rounds so one round's garbage is not charged to the next */
|
|
23
|
+
readonly gcBetweenRounds: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ISampleStats {
|
|
27
|
+
readonly samples: number
|
|
28
|
+
readonly min: number
|
|
29
|
+
readonly max: number
|
|
30
|
+
readonly mean: number
|
|
31
|
+
readonly median: number
|
|
32
|
+
readonly p95: number
|
|
33
|
+
readonly p99: number
|
|
34
|
+
readonly stdDev: number
|
|
35
|
+
/** stdDev as a fraction of the mean - the run to run noise floor */
|
|
36
|
+
readonly coefficientOfVariation: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IGcTotals {
|
|
40
|
+
readonly minor: number
|
|
41
|
+
readonly major: number
|
|
42
|
+
readonly incremental: number
|
|
43
|
+
readonly weakCallback: number
|
|
44
|
+
readonly total: number
|
|
45
|
+
readonly totalPauseMs: number
|
|
46
|
+
readonly longestPauseMs: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface IMemoryResult {
|
|
50
|
+
/** operations in the diagnostics pass these figures were taken over */
|
|
51
|
+
readonly operations: number
|
|
52
|
+
/**
|
|
53
|
+
* heapUsed after a forced collection, less the same reading taken before the
|
|
54
|
+
* measured rounds - what the work left behind rather than what it churned
|
|
55
|
+
*/
|
|
56
|
+
readonly retainedHeapBytes: number
|
|
57
|
+
readonly retainedBytesPerOperation: number
|
|
58
|
+
/**
|
|
59
|
+
* heapUsed sampled the instant the pass finished. Not a high water mark -
|
|
60
|
+
* it is wherever the sawtooth happened to be - so read it as a rough sense of
|
|
61
|
+
* the working set, not as a peak.
|
|
62
|
+
*/
|
|
63
|
+
readonly heapAtEndOfPassBytes: number
|
|
64
|
+
readonly gc: IGcTotals
|
|
65
|
+
/**
|
|
66
|
+
* collections per million operations. allocation volume is not directly
|
|
67
|
+
* observable from node, so this is the comparable proxy for it - the same
|
|
68
|
+
* work triggering fewer scavenges is the same work allocating less.
|
|
69
|
+
*/
|
|
70
|
+
readonly minorGcPerMillionOperations: number
|
|
71
|
+
readonly majorGcPerMillionOperations: number
|
|
72
|
+
readonly gcPauseMsPerMillionOperations: number
|
|
73
|
+
/** false when the process was started without --expose-gc */
|
|
74
|
+
readonly forcedGcAvailable: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface IRoundResult {
|
|
78
|
+
readonly round: number
|
|
79
|
+
readonly operations: number
|
|
80
|
+
readonly elapsedNanos: number
|
|
81
|
+
readonly nanosPerOperation: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface IEnvironment {
|
|
85
|
+
readonly node: string
|
|
86
|
+
readonly v8: string
|
|
87
|
+
readonly platform: string
|
|
88
|
+
readonly arch: string
|
|
89
|
+
readonly cpu: string
|
|
90
|
+
readonly cpuCount: number
|
|
91
|
+
readonly forcedGcAvailable: boolean
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface IBenchResult {
|
|
95
|
+
readonly name: string
|
|
96
|
+
readonly description?: string
|
|
97
|
+
readonly options: IBenchOptions
|
|
98
|
+
readonly rounds: IRoundResult[]
|
|
99
|
+
/** statistics over the per round nanosPerOperation samples */
|
|
100
|
+
readonly timing: ISampleStats
|
|
101
|
+
readonly operationsPerSecond: number
|
|
102
|
+
readonly memory: IMemoryResult
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface IBenchSuiteResult {
|
|
106
|
+
readonly label: string
|
|
107
|
+
readonly capturedAt: string
|
|
108
|
+
readonly environment: IEnvironment
|
|
109
|
+
readonly results: IBenchResult[]
|
|
110
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { PerformanceObserver, constants } from 'perf_hooks'
|
|
2
|
+
import { IGcTotals } from './bench-types'
|
|
3
|
+
|
|
4
|
+
interface IGcDetail {
|
|
5
|
+
kind: number
|
|
6
|
+
flags: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* a gc performance entry. detail is not on the PerformanceEntry the node types
|
|
11
|
+
* describe, it is only present on entries of this type
|
|
12
|
+
*/
|
|
13
|
+
interface IGcEntry {
|
|
14
|
+
duration: number
|
|
15
|
+
detail?: IGcDetail
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const emptyTotals: IGcTotals = {
|
|
19
|
+
minor: 0,
|
|
20
|
+
major: 0,
|
|
21
|
+
incremental: 0,
|
|
22
|
+
weakCallback: 0,
|
|
23
|
+
total: 0,
|
|
24
|
+
totalPauseMs: 0,
|
|
25
|
+
longestPauseMs: 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Counts collections and the pause they cost while a benchmark runs.
|
|
30
|
+
*
|
|
31
|
+
* Node does not expose how many bytes a stretch of code allocated, so the
|
|
32
|
+
* number of scavenges is the practical stand in - the same work provoking
|
|
33
|
+
* fewer minor collections is the same work producing less garbage, and that
|
|
34
|
+
* is the number worth moving for a drop copy engine where the cost of
|
|
35
|
+
* allocation is felt as pause rather than as throughput.
|
|
36
|
+
*/
|
|
37
|
+
export class GcProbe {
|
|
38
|
+
private observer: PerformanceObserver | null = null
|
|
39
|
+
private minor: number = 0
|
|
40
|
+
private major: number = 0
|
|
41
|
+
private incremental: number = 0
|
|
42
|
+
private weakCallback: number = 0
|
|
43
|
+
private totalPauseMs: number = 0
|
|
44
|
+
private longestPauseMs: number = 0
|
|
45
|
+
|
|
46
|
+
/** whether the process was started with --expose-gc */
|
|
47
|
+
public static forcedGcAvailable (): boolean {
|
|
48
|
+
return typeof global.gc === 'function'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Collect until the heap settles, so a reading taken afterwards reflects what
|
|
53
|
+
* is genuinely retained. Two passes because the first can promote survivors
|
|
54
|
+
* into the old space that the second is then able to reclaim. A no-op without
|
|
55
|
+
* --expose-gc, in which case retained figures are indicative only.
|
|
56
|
+
*/
|
|
57
|
+
public static forceGc (): void {
|
|
58
|
+
const gc = global.gc
|
|
59
|
+
if (typeof gc !== 'function') return
|
|
60
|
+
gc()
|
|
61
|
+
gc()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** entries recorded so far - lets a caller wait for delivery to go quiet */
|
|
65
|
+
public seen (): number {
|
|
66
|
+
return this.minor + this.major + this.incremental + this.weakCallback
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public start (): void {
|
|
70
|
+
this.reset()
|
|
71
|
+
const observer = new PerformanceObserver((list) => {
|
|
72
|
+
for (const entry of list.getEntries()) {
|
|
73
|
+
const gcEntry = entry as unknown as IGcEntry
|
|
74
|
+
this.record(gcEntry.duration, gcEntry.detail?.kind ?? 0)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
// buffered so collections are not missed while the loop is busy - the
|
|
78
|
+
// callback cannot run until the measured work yields
|
|
79
|
+
observer.observe({ entryTypes: ['gc'], buffered: true })
|
|
80
|
+
this.observer = observer
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stop observing and return what was seen. Any entries still queued are
|
|
85
|
+
* drained first - the observer callback is a macrotask and the benchmark
|
|
86
|
+
* loop never yields, so without this every collection would be lost.
|
|
87
|
+
*/
|
|
88
|
+
public stop (): IGcTotals {
|
|
89
|
+
const observer = this.observer
|
|
90
|
+
if (!observer) return emptyTotals
|
|
91
|
+
for (const entry of observer.takeRecords()) {
|
|
92
|
+
const gcEntry = entry as unknown as IGcEntry
|
|
93
|
+
this.record(gcEntry.duration, gcEntry.detail?.kind ?? 0)
|
|
94
|
+
}
|
|
95
|
+
observer.disconnect()
|
|
96
|
+
this.observer = null
|
|
97
|
+
return this.totals()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private reset (): void {
|
|
101
|
+
this.minor = 0
|
|
102
|
+
this.major = 0
|
|
103
|
+
this.incremental = 0
|
|
104
|
+
this.weakCallback = 0
|
|
105
|
+
this.totalPauseMs = 0
|
|
106
|
+
this.longestPauseMs = 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private record (durationMs: number, kind: number): void {
|
|
110
|
+
this.totalPauseMs += durationMs
|
|
111
|
+
this.longestPauseMs = Math.max(this.longestPauseMs, durationMs)
|
|
112
|
+
switch (kind) {
|
|
113
|
+
case constants.NODE_PERFORMANCE_GC_MINOR:
|
|
114
|
+
++this.minor
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
case constants.NODE_PERFORMANCE_GC_MAJOR:
|
|
118
|
+
++this.major
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
case constants.NODE_PERFORMANCE_GC_INCREMENTAL:
|
|
122
|
+
++this.incremental
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
case constants.NODE_PERFORMANCE_GC_WEAKCB:
|
|
126
|
+
++this.weakCallback
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private totals (): IGcTotals {
|
|
135
|
+
return {
|
|
136
|
+
minor: this.minor,
|
|
137
|
+
major: this.major,
|
|
138
|
+
incremental: this.incremental,
|
|
139
|
+
weakCallback: this.weakCallback,
|
|
140
|
+
total: this.minor + this.major + this.incremental + this.weakCallback,
|
|
141
|
+
totalPauseMs: this.totalPauseMs,
|
|
142
|
+
longestPauseMs: this.longestPauseMs
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AsciiParser } from '../buffer/ascii/ascii-parser'
|
|
2
|
+
import { ElasticBuffer } from '../buffer/elastic-buffer'
|
|
3
|
+
import { MsgView } from '../buffer/msg-view'
|
|
4
|
+
import { IJsFixConfig } from '../config/js-fix-config'
|
|
5
|
+
import { DITokens } from '../runtime/di-tokens'
|
|
6
|
+
import { makeSessionScope } from '../runtime/session-scope'
|
|
7
|
+
import { IBenchCase } from './bench-types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* what a decoded message is taken through once the parser has produced a view
|
|
11
|
+
*/
|
|
12
|
+
export enum ParseDepth {
|
|
13
|
+
/** stop at the MsgView - tokenise, discover the structure, build the view */
|
|
14
|
+
View = 'view',
|
|
15
|
+
/** materialise the whole object graph, which is what most consumers do */
|
|
16
|
+
Object = 'object'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IParseCaseOptions {
|
|
20
|
+
readonly config: IJsFixConfig
|
|
21
|
+
/** the raw text of a fix log, one or more complete messages */
|
|
22
|
+
readonly contents: string
|
|
23
|
+
readonly depth: ParseDepth
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Drives fix bytes through the real parser and counts messages out of it.
|
|
28
|
+
*
|
|
29
|
+
* The bytes are fed as one buffer per invocation, the same way a socket read
|
|
30
|
+
* reaches parse, rather than by concatenating the file thousands of times into
|
|
31
|
+
* one enormous string. That concatenation is itself a large allocation, and
|
|
32
|
+
* doing it before the timer starts leaves a heap that no memory reading taken
|
|
33
|
+
* afterwards can be trusted through.
|
|
34
|
+
*/
|
|
35
|
+
export class ParseCase implements IBenchCase {
|
|
36
|
+
public readonly name: string
|
|
37
|
+
public readonly description: string
|
|
38
|
+
/** messages produced by one pass over the input */
|
|
39
|
+
public readonly operationsPerInvocation: number
|
|
40
|
+
|
|
41
|
+
private readonly source: Buffer
|
|
42
|
+
private readonly depth: ParseDepth
|
|
43
|
+
private readonly options: IParseCaseOptions
|
|
44
|
+
private parser: AsciiParser | null = null
|
|
45
|
+
private decoded: number = 0
|
|
46
|
+
|
|
47
|
+
constructor (name: string, options: IParseCaseOptions) {
|
|
48
|
+
this.name = name
|
|
49
|
+
this.options = options
|
|
50
|
+
this.depth = options.depth
|
|
51
|
+
this.source = Buffer.from(options.contents)
|
|
52
|
+
this.description = options.depth === ParseDepth.Object
|
|
53
|
+
? 'tokenise, discover structure, build view, materialise object'
|
|
54
|
+
: 'tokenise, discover structure, build view'
|
|
55
|
+
this.operationsPerInvocation = this.countMessages()
|
|
56
|
+
if (this.operationsPerInvocation === 0) {
|
|
57
|
+
throw new Error(`${name}: input yielded no messages, check the dictionary and the delimiter`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Round a requested round size up to a whole number of passes over the input.
|
|
63
|
+
* A pass cannot be stopped part way, so asking for a count that is not a
|
|
64
|
+
* multiple of it would silently measure more work than it reported.
|
|
65
|
+
*/
|
|
66
|
+
public alignOperations (requested: number): number {
|
|
67
|
+
const per = this.operationsPerInvocation
|
|
68
|
+
return Math.max(per, Math.ceil(requested / per) * per)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public setup (): void {
|
|
72
|
+
this.parser = this.makeParser(true)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public teardown (): void {
|
|
76
|
+
this.parser?.removeAllListeners()
|
|
77
|
+
this.parser = null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public run (operations: number): number {
|
|
81
|
+
const parser = this.parser
|
|
82
|
+
if (!parser) {
|
|
83
|
+
throw new Error(`${this.name}: run called before setup`)
|
|
84
|
+
}
|
|
85
|
+
const source = this.source
|
|
86
|
+
const passes = operations / this.operationsPerInvocation
|
|
87
|
+
if (!Number.isInteger(passes)) {
|
|
88
|
+
throw new Error(`${this.name}: ${operations} operations is not a whole number of passes over the input`)
|
|
89
|
+
}
|
|
90
|
+
this.decoded = 0
|
|
91
|
+
for (let i = 0; i < passes; ++i) {
|
|
92
|
+
parser.parseBuffer(source)
|
|
93
|
+
}
|
|
94
|
+
return this.decoded
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A parser on a session scope of its own.
|
|
99
|
+
*
|
|
100
|
+
* It has to be a scope rather than a hand built AsciiParser: AsciiParserState
|
|
101
|
+
* takes the ParseBuffer from the container, so a parser handed some other
|
|
102
|
+
* buffer writes bytes the state never reads and the parse walks off into
|
|
103
|
+
* nonsense. The scope re-registers the buffer and hands the same instance to
|
|
104
|
+
* both, which is the same thing an accepted connection gets.
|
|
105
|
+
*/
|
|
106
|
+
private makeParser (subscribe: boolean): AsciiParser {
|
|
107
|
+
const scoped = makeSessionScope(this.options.config)
|
|
108
|
+
const container = scoped.sessionContainer
|
|
109
|
+
// constructed rather than resolved: DITokens.readStream has no registration
|
|
110
|
+
// outside a live transport, and there is no stream here - the harness hands
|
|
111
|
+
// the parser its bytes. Everything else comes from the scope, so the buffer
|
|
112
|
+
// below is the same instance AsciiParserState will be injected with.
|
|
113
|
+
const buffer = container.resolve<ElasticBuffer>(DITokens.ParseBuffer)
|
|
114
|
+
const maxMessageLen = container.resolve<number>(DITokens.maxMessageLen)
|
|
115
|
+
const parser = new AsciiParser(scoped, null, buffer, maxMessageLen)
|
|
116
|
+
const object = subscribe && this.depth === ParseDepth.Object
|
|
117
|
+
parser.on('msg', (_: string, view: MsgView) => {
|
|
118
|
+
++this.decoded
|
|
119
|
+
if (object) {
|
|
120
|
+
// read the result back so the materialisation cannot be treated as
|
|
121
|
+
// dead and folded away
|
|
122
|
+
const materialised = view.toObject()
|
|
123
|
+
if (materialised === null) {
|
|
124
|
+
throw new Error('view produced no object')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
return parser
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* How many messages one pass over the input yields. Counted on a throwaway
|
|
133
|
+
* parser during construction so the measured run never has to ask.
|
|
134
|
+
*/
|
|
135
|
+
private countMessages (): number {
|
|
136
|
+
const parser = this.makeParser(false)
|
|
137
|
+
this.decoded = 0
|
|
138
|
+
parser.parseBuffer(this.source)
|
|
139
|
+
parser.removeAllListeners()
|
|
140
|
+
const count = this.decoded
|
|
141
|
+
this.decoded = 0
|
|
142
|
+
return count
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -39,7 +39,9 @@ export class AsciiParser extends MsgParser {
|
|
|
39
39
|
this.id = AsciiParser.nextId++
|
|
40
40
|
this.segmentParser = config.sessionContainer.resolve<AsciiSegmentParser>(AsciiSegmentParser)
|
|
41
41
|
this.state = config.sessionContainer.resolve<AsciiParserState>(AsciiParserState)
|
|
42
|
-
|
|
42
|
+
// floored - Tags hands the starting length to new Array, which rejects a
|
|
43
|
+
// fraction, so any buffer size not divisible by 10 threw from here
|
|
44
|
+
this.state.locations = new Tags(Math.floor(this.receivingBuffer.size / 10), this.maxMessageLen)
|
|
43
45
|
this.state.definitions = this.config.definitions
|
|
44
46
|
this.state.beginMessage()
|
|
45
47
|
if (readStream !== null) {
|
|
@@ -101,6 +103,19 @@ export class AsciiParser extends MsgParser {
|
|
|
101
103
|
this.parse(buff, buff.length)
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/**
|
|
107
|
+
* feed bytes as though they had arrived from the transport. This is the path
|
|
108
|
+
* the socket takes - subscribe hands each chunk it receives straight to parse -
|
|
109
|
+
* so a caller driving the parser directly, a benchmark or a replay harness,
|
|
110
|
+
* measures the real thing rather than a copy of it.
|
|
111
|
+
*
|
|
112
|
+
* @param buff bytes to parse
|
|
113
|
+
* @param end how much of buff to consume, all of it by default
|
|
114
|
+
*/
|
|
115
|
+
public parseBuffer (buff: Buffer, end: number = buff.length): void {
|
|
116
|
+
this.parse(buff, end)
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
private parse (readBuffer: Buffer, end: number): void {
|
|
105
120
|
const state = this.state
|
|
106
121
|
const eq: number = AsciiChars.Eq
|
|
@@ -29,10 +29,13 @@ export class AsciiSegmentParser {
|
|
|
29
29
|
public parse (msgType: string, tags: Tags, last: number): Structure | null {
|
|
30
30
|
// completed segments in that they are fully parsed
|
|
31
31
|
const segments: SegmentDescription[] = []
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
32
|
+
const resolved: MessageDefinition | undefined = this.definitions.message.get(msgType)
|
|
33
|
+
if (!resolved) {
|
|
34
34
|
return null
|
|
35
35
|
}
|
|
36
|
+
// narrowing of resolved is not visible inside the hoisted helpers below, so
|
|
37
|
+
// carry the definition on a const the compiler already sees as non-optional
|
|
38
|
+
const msgDefinition: MessageDefinition = resolved
|
|
36
39
|
// in process of being discovered and may have any amount of depth
|
|
37
40
|
// i.e. a component containing a repeated group of components
|
|
38
41
|
// with sub-groups of components
|
|
@@ -41,8 +44,8 @@ export class AsciiSegmentParser {
|
|
|
41
44
|
let peek: SegmentDescription
|
|
42
45
|
|
|
43
46
|
// track depth-1 components that have been exited for fragmentation detection
|
|
44
|
-
const exitedDepth1Components
|
|
45
|
-
const fragmentedComponents
|
|
47
|
+
const exitedDepth1Components = new Set<string>()
|
|
48
|
+
const fragmentedComponents = new Set<string>()
|
|
46
49
|
|
|
47
50
|
// having finished one segments keep unwinding until tag matches further up stack
|
|
48
51
|
function unwind (tag: number): void {
|
|
@@ -189,8 +192,8 @@ export class AsciiSegmentParser {
|
|
|
189
192
|
// only build SegmentViews for components detected as fragmented during discover
|
|
190
193
|
// non-fragmented components use their position ranges directly (zero overhead)
|
|
191
194
|
if (fragmentedComponents.size === 0) return
|
|
192
|
-
const ti = new TagIndex(msgDefinition
|
|
193
|
-
const seen
|
|
195
|
+
const ti = new TagIndex(msgDefinition, tags, last + 1)
|
|
196
|
+
const seen = new Set<string>()
|
|
194
197
|
for (let i = 1; i < segments.length - 1; i++) {
|
|
195
198
|
const seg = segments[i]
|
|
196
199
|
if (seg.depth !== 1) continue
|
|
@@ -10,9 +10,9 @@ export interface TagSpan {
|
|
|
10
10
|
|
|
11
11
|
export class TagIndex {
|
|
12
12
|
private readonly sortedTagPos: TagPos[]
|
|
13
|
-
private readonly tagSpans
|
|
14
|
-
private readonly componentGroupWrappers
|
|
15
|
-
private readonly cache
|
|
13
|
+
private readonly tagSpans = new Map<number, TagSpan>()
|
|
14
|
+
private readonly componentGroupWrappers = new Set<string>()
|
|
15
|
+
private readonly cache = new Map<string, SegmentView | null>()
|
|
16
16
|
|
|
17
17
|
constructor (
|
|
18
18
|
public readonly set: IContainedSet,
|
package/src/buffer/msg-view.ts
CHANGED
|
@@ -263,29 +263,46 @@ export abstract class MsgView {
|
|
|
263
263
|
return JSON.stringify(this.toObject(), null, 4)
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* fetch a nested view, either by a single name - getView('Instrument') - or by a
|
|
268
|
+
* dotted path walked a step at a time - getView('Parties.PtysSubGrp')
|
|
269
|
+
*/
|
|
266
270
|
public getView (name: string): MsgView | null {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
+
// the dotted form is the exception. resolve a plain name without paying for
|
|
272
|
+
// the parts array and the closure a split and reduce would allocate - the
|
|
273
|
+
// component walk in toObject and validate comes through here per component.
|
|
274
|
+
if (!name.includes('.')) {
|
|
275
|
+
return this.resolveChild(name)
|
|
276
|
+
}
|
|
277
|
+
let view: MsgView | null = this
|
|
278
|
+
for (const part of name.split('.')) {
|
|
279
|
+
view = view.resolveChild(part)
|
|
280
|
+
if (!view) {
|
|
281
|
+
return null
|
|
271
282
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return a.create(singleton)
|
|
276
|
-
}
|
|
277
|
-
// is this a full name where abbreviation exists
|
|
278
|
-
const component: ContainedField | null = a.segment.set?.localNameToField.get(current) ?? null
|
|
283
|
+
}
|
|
284
|
+
return view
|
|
285
|
+
}
|
|
279
286
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
/**
|
|
288
|
+
* resolve one step of a view path against this view
|
|
289
|
+
*/
|
|
290
|
+
private resolveChild (name: string): MsgView | null {
|
|
291
|
+
const structure = this.structure
|
|
292
|
+
const singleton: SegmentDescription | null = structure?.firstContainedWithin(name, this.segment) ?? null
|
|
293
|
+
if (singleton) {
|
|
294
|
+
return this.create(singleton)
|
|
295
|
+
}
|
|
296
|
+
// is this a full name where abbreviation exists
|
|
297
|
+
const component: ContainedField | null = this.segment.set?.localNameToField.get(name) ?? null
|
|
298
|
+
|
|
299
|
+
if (component) {
|
|
300
|
+
const abbreviated: SegmentDescription | null = structure?.firstContainedWithin(component.name, this.segment) ?? null
|
|
301
|
+
if (abbreviated) {
|
|
302
|
+
return this.create(abbreviated)
|
|
285
303
|
}
|
|
286
|
-
return null
|
|
287
304
|
}
|
|
288
|
-
return
|
|
305
|
+
return null
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
public abstract checksum (): number
|
|
@@ -49,7 +49,7 @@ export class DictionaryValidator {
|
|
|
49
49
|
private readonly allFieldNames: string[] = []
|
|
50
50
|
private readonly allComponentNames: string[] = []
|
|
51
51
|
|
|
52
|
-
get errors ():
|
|
52
|
+
get errors (): readonly ValidationError[] {
|
|
53
53
|
return this._errors
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -51,7 +51,7 @@ export class GraphNode {
|
|
|
51
51
|
public readonly element: XNode
|
|
52
52
|
) {}
|
|
53
53
|
|
|
54
|
-
get edges ():
|
|
54
|
+
get edges (): readonly Edge[] {
|
|
55
55
|
return this._edges
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -118,8 +118,7 @@ export class QuickFixGraphParser {
|
|
|
118
118
|
this.parseTrailer(doc)
|
|
119
119
|
this.parseMessages(doc)
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
const node = this.queue.shift()!
|
|
121
|
+
for (let node = this.queue.shift(); node !== undefined; node = this.queue.shift()) {
|
|
123
122
|
this.work(node)
|
|
124
123
|
}
|
|
125
124
|
|
|
@@ -14,7 +14,7 @@ export interface ValidationError {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export class DictionaryValidationException extends Error {
|
|
17
|
-
constructor (public readonly errors:
|
|
17
|
+
constructor (public readonly errors: readonly ValidationError[]) {
|
|
18
18
|
const errorCount = errors.filter(e => e.severity === ValidationSeverity.Error).length
|
|
19
19
|
const warningCount = errors.filter(e => e.severity === ValidationSeverity.Warning).length
|
|
20
20
|
const header = `FIX dictionary validation failed with ${errorCount} error(s) and ${warningCount} warning(s):`
|