jspurefix 5.9.1 → 5.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/BACKPORT_PLAN.md +20 -7
  2. package/WORKLOG.md +119 -0
  3. package/dist/benchmark/bench-report.d.ts +5 -0
  4. package/dist/benchmark/bench-report.js +91 -0
  5. package/dist/benchmark/bench-report.js.map +1 -0
  6. package/dist/benchmark/bench-runner.d.ts +12 -0
  7. package/dist/benchmark/bench-runner.js +193 -0
  8. package/dist/benchmark/bench-runner.js.map +1 -0
  9. package/dist/benchmark/bench-stats.d.ts +10 -0
  10. package/dist/benchmark/bench-stats.js +56 -0
  11. package/dist/benchmark/bench-stats.js.map +1 -0
  12. package/dist/benchmark/bench-types.d.ts +74 -0
  13. package/dist/benchmark/bench-types.js +3 -0
  14. package/dist/benchmark/bench-types.js.map +1 -0
  15. package/dist/benchmark/gc-probe.d.ts +18 -0
  16. package/dist/benchmark/gc-probe.js +103 -0
  17. package/dist/benchmark/gc-probe.js.map +1 -0
  18. package/dist/benchmark/index.d.ts +6 -0
  19. package/dist/benchmark/index.js +23 -0
  20. package/dist/benchmark/index.js.map +1 -0
  21. package/dist/benchmark/parse-bench.d.ts +28 -0
  22. package/dist/benchmark/parse-bench.js +85 -0
  23. package/dist/benchmark/parse-bench.js.map +1 -0
  24. package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
  25. package/dist/buffer/ascii/ascii-parser.js +4 -1
  26. package/dist/buffer/ascii/ascii-parser.js.map +1 -1
  27. package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
  28. package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
  29. package/dist/buffer/ascii/tag-index.js.map +1 -1
  30. package/dist/buffer/msg-view.d.ts +1 -0
  31. package/dist/buffer/msg-view.js +25 -19
  32. package/dist/buffer/msg-view.js.map +1 -1
  33. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
  34. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
  35. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
  36. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
  37. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
  38. package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
  39. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
  40. package/dist/jsfix-bench.d.ts +1 -0
  41. package/dist/jsfix-bench.js +213 -0
  42. package/dist/jsfix-bench.js.map +1 -0
  43. package/dist/store/file-session-store.js +1 -1
  44. package/dist/store/file-session-store.js.map +1 -1
  45. package/dist/store/file-session-stream-provider.d.ts +1 -0
  46. package/dist/store/file-session-stream-provider.js +13 -13
  47. package/dist/store/file-session-stream-provider.js.map +1 -1
  48. package/dist/store/fix-session-store-factory.d.ts +1 -1
  49. package/dist/store/fix-session-store-factory.js.map +1 -1
  50. package/dist/store/fix-session-store.d.ts +11 -11
  51. package/dist/store/fix-session-store.js.map +1 -1
  52. package/dist/store/memory-session-store.js.map +1 -1
  53. package/dist/store/session-stream-provider.d.ts +13 -13
  54. package/dist/store/session-stream-provider.js.map +1 -1
  55. package/dist/transport/session/fix-clock.d.ts +1 -1
  56. package/dist/transport/session/fix-clock.js.map +1 -1
  57. package/dist/transport/session/resend-request-manager.d.ts +2 -2
  58. package/dist/transport/session/resend-request-manager.js.map +1 -1
  59. package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
  60. package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
  61. package/dist/transport/session/session-sequence-store.d.ts +3 -3
  62. package/dist/transport/session/session-sequence-store.js.map +1 -1
  63. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  64. package/docs/benchmarking.md +117 -0
  65. package/eslint.config.mjs +15 -1
  66. package/jsfix.test_client.txt +62 -62
  67. package/jsfix.test_server.txt +72 -72
  68. package/package.json +9 -1
  69. package/src/benchmark/bench-report.ts +101 -0
  70. package/src/benchmark/bench-runner.ts +196 -0
  71. package/src/benchmark/bench-stats.ts +86 -0
  72. package/src/benchmark/bench-types.ts +110 -0
  73. package/src/benchmark/gc-probe.ts +145 -0
  74. package/src/benchmark/index.ts +6 -0
  75. package/src/benchmark/parse-bench.ts +144 -0
  76. package/src/buffer/ascii/ascii-parser.ts +16 -1
  77. package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
  78. package/src/buffer/ascii/tag-index.ts +3 -3
  79. package/src/buffer/msg-view.ts +35 -18
  80. package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
  81. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
  82. package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
  83. package/src/jsfix-bench.ts +204 -0
  84. package/src/store/file-session-store.ts +2 -2
  85. package/src/store/file-session-stream-provider.ts +17 -11
  86. package/src/store/fix-session-store-factory.ts +1 -1
  87. package/src/store/fix-session-store.ts +11 -11
  88. package/src/store/memory-session-store.ts +1 -1
  89. package/src/store/session-stream-provider.ts +13 -13
  90. package/src/transport/session/fix-clock.ts +1 -1
  91. package/src/transport/session/resend-request-manager.ts +4 -4
  92. package/src/transport/session/session-sequence-coordinator.ts +1 -1
  93. package/src/transport/session/session-sequence-store.ts +3 -3
  94. package/src/transport/tcp/tcp-initiator.ts +1 -1
@@ -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,6 @@
1
+ export * from './bench-types'
2
+ export * from './bench-stats'
3
+ export * from './bench-runner'
4
+ export * from './bench-report'
5
+ export * from './gc-probe'
6
+ export * from './parse-bench'
@@ -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
- this.state.locations = new Tags(this.receivingBuffer.size / 10, this.maxMessageLen)
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 msgDefinition: MessageDefinition | undefined = this.definitions.message.get(msgType)
33
- if (!msgDefinition) {
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: Set<string> = new Set()
45
- const fragmentedComponents: Set<string> = new Set()
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!, tags, last + 1)
193
- const seen: Set<string> = new Set()
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: Map<number, TagSpan> = new Map()
14
- private readonly componentGroupWrappers: Set<string> = new Set()
15
- private readonly cache: Map<string, SegmentView | null> = new Map()
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,
@@ -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
- const parts: string[] = name.split('.')
268
- const reducer = (a: MsgView | null, current: string): MsgView | null => {
269
- if (!a) {
270
- return a
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
- const structure = a.structure
273
- const singleton: SegmentDescription | null = structure?.firstContainedWithin(current, a.segment) ?? null
274
- if (singleton) {
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
- if (component) {
281
- const abbreviated: SegmentDescription | null = structure?.firstContainedWithin(component.name, a.segment) ?? null
282
- if (abbreviated) {
283
- return a.create(abbreviated)
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 parts.reduce<MsgView | null>(reducer, this)
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 (): ReadonlyArray<ValidationError> {
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 (): ReadonlyArray<Edge> {
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
- while (this.queue.length > 0) {
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: ReadonlyArray<ValidationError>) {
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):`