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
|
@@ -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
|
|
@@ -6,6 +6,7 @@ import { EmptyLogFactory } from './empty-log-factory'
|
|
|
6
6
|
import { AsciiChars } from '../buffer/ascii/ascii-chars'
|
|
7
7
|
import { DependencyContainer } from 'tsyringe'
|
|
8
8
|
import { IFixSessionStoreFactory } from '../store/fix-session-store-factory'
|
|
9
|
+
import { ISessionRegistry } from '../transport/session/session-registry'
|
|
9
10
|
|
|
10
11
|
export interface IJsFixConfig {
|
|
11
12
|
factory: ISessionMsgFactory | null
|
|
@@ -16,11 +17,18 @@ export interface IJsFixConfig {
|
|
|
16
17
|
logFactory: JsFixLoggerFactory
|
|
17
18
|
sessionContainer: DependencyContainer
|
|
18
19
|
sessionStoreFactory?: IFixSessionStoreFactory
|
|
20
|
+
/**
|
|
21
|
+
* Tracks the live session per SessionId. Set by the acceptor listener so a
|
|
22
|
+
* reconnecting counterparty replaces - rather than duplicates - its session.
|
|
23
|
+
*/
|
|
24
|
+
sessionRegistry?: ISessionRegistry
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export class JsFixConfig implements IJsFixConfig {
|
|
22
28
|
public logDelimiter: number = AsciiChars.Pipe
|
|
23
29
|
public sessionContainer: DependencyContainer
|
|
30
|
+
public sessionStoreFactory?: IFixSessionStoreFactory
|
|
31
|
+
public sessionRegistry?: ISessionRegistry
|
|
24
32
|
constructor (
|
|
25
33
|
public readonly factory: ISessionMsgFactory | null,
|
|
26
34
|
public readonly definitions: FixDefinitions,
|
|
@@ -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):`
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import * as fs from 'fs'
|
|
5
|
+
import minimist = require('minimist')
|
|
6
|
+
import buildOptions from 'minimist-options'
|
|
7
|
+
|
|
8
|
+
import { AsciiChars } from './buffer/ascii'
|
|
9
|
+
import { IJsFixConfig } from './config'
|
|
10
|
+
import { DefinitionFactory } from './util'
|
|
11
|
+
import { SessionContainer } from './runtime'
|
|
12
|
+
import { DITokens } from './runtime/di-tokens'
|
|
13
|
+
import {
|
|
14
|
+
BenchRunner,
|
|
15
|
+
IBenchCase,
|
|
16
|
+
IBenchOptions,
|
|
17
|
+
IBenchSuiteResult,
|
|
18
|
+
ParseCase,
|
|
19
|
+
ParseDepth,
|
|
20
|
+
defaultBenchOptions,
|
|
21
|
+
formatComparison,
|
|
22
|
+
formatSuite
|
|
23
|
+
} from './benchmark'
|
|
24
|
+
|
|
25
|
+
const options = buildOptions({
|
|
26
|
+
dict: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
alias: 'd',
|
|
29
|
+
default: 'qf44'
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
fix: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
alias: 'f',
|
|
35
|
+
default: 'data/examples/FIX.4.4/quickfix/execution-report/fix.txt'
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
session: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
alias: 's',
|
|
41
|
+
default: 'data/session/test-initiator.json'
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
delimiter: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
alias: 'l',
|
|
47
|
+
default: '|'
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
depth: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
default: 'view,object'
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
rounds: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
default: defaultBenchOptions.rounds
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
msgs: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
default: defaultBenchOptions.operationsPerRound
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
warmup: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
default: defaultBenchOptions.warmupOperations
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
label: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
default: 'jspurefix'
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
save: {
|
|
76
|
+
type: 'string'
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
baseline: {
|
|
80
|
+
type: 'string'
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
json: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
default: false
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
help: {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
alias: 'h',
|
|
91
|
+
default: false
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const argv: any = minimist(process.argv.slice(2), options)
|
|
96
|
+
|
|
97
|
+
const root: string = path.join(__dirname, '../')
|
|
98
|
+
|
|
99
|
+
function norm (p: string): string {
|
|
100
|
+
return path.isAbsolute(p) ? p : path.join(root, p)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function usage (): void {
|
|
104
|
+
console.log('measure the parse path and what it costs in time and in garbage')
|
|
105
|
+
console.log()
|
|
106
|
+
console.log(' --fix=<path> fix log to parse, defaults to a 4.4 execution report')
|
|
107
|
+
console.log(' --dict=<name> dictionary, e.g. qf44, repo44, qf50sp2')
|
|
108
|
+
console.log(' --delimiter=<char> field delimiter in the log, defaults to |')
|
|
109
|
+
console.log(' --depth=view,object which stages to measure, comma separated')
|
|
110
|
+
console.log(' --rounds=<n> measured rounds, one sample each')
|
|
111
|
+
console.log(' --msgs=<n> messages per round')
|
|
112
|
+
console.log(' --warmup=<n> messages discarded before measuring')
|
|
113
|
+
console.log(' --save=<path> write the suite to json')
|
|
114
|
+
console.log(' --baseline=<path> compare against a previously saved suite')
|
|
115
|
+
console.log(' --json print the suite as json instead of a report')
|
|
116
|
+
console.log()
|
|
117
|
+
console.log('run under --expose-gc so rounds can be isolated and heap read cleanly:')
|
|
118
|
+
console.log(' node --expose-gc dist/jsfix-bench --fix=data/examples/FIX.4.4/quickfix/execution-report/fix.txt')
|
|
119
|
+
console.log(' npm run bench')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function makeConfig (): Promise<IJsFixConfig> {
|
|
123
|
+
const sys = new SessionContainer()
|
|
124
|
+
sys.registerGlobal('error')
|
|
125
|
+
const sessionDescription = require(norm(argv.session))
|
|
126
|
+
const container = await sys.makeSystem(sessionDescription)
|
|
127
|
+
const config = container.resolve<IJsFixConfig>(DITokens.IJsFixConfig)
|
|
128
|
+
config.definitions = await new DefinitionFactory().getDefinitions(argv.dict)
|
|
129
|
+
// the parser reads the delimiter when it is constructed, so this has to be
|
|
130
|
+
// settled before any case is built
|
|
131
|
+
config.delimiter = AsciiChars.firstChar(argv.delimiter)
|
|
132
|
+
return config
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function requestedDepths (): ParseDepth[] {
|
|
136
|
+
const known = new Map<string, ParseDepth>([
|
|
137
|
+
['view', ParseDepth.View],
|
|
138
|
+
['object', ParseDepth.Object]
|
|
139
|
+
])
|
|
140
|
+
const asked = String(argv.depth).split(',').map(d => d.trim().toLowerCase()).filter(d => d.length > 0)
|
|
141
|
+
return asked.map(d => {
|
|
142
|
+
const depth = known.get(d)
|
|
143
|
+
if (!depth) {
|
|
144
|
+
throw new Error(`unknown depth ${d}, expected view or object`)
|
|
145
|
+
}
|
|
146
|
+
return depth
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readSuite (p: string): IBenchSuiteResult {
|
|
151
|
+
return JSON.parse(fs.readFileSync(norm(p), 'utf8'))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function main (): Promise<void> {
|
|
155
|
+
if (argv.help) {
|
|
156
|
+
usage()
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const fixPath = norm(argv.fix)
|
|
160
|
+
if (!fs.existsSync(fixPath)) {
|
|
161
|
+
throw new Error(`no such fix log ${fixPath}`)
|
|
162
|
+
}
|
|
163
|
+
const contents = fs.readFileSync(fixPath, 'utf8')
|
|
164
|
+
const config = await makeConfig()
|
|
165
|
+
|
|
166
|
+
const cases: ParseCase[] = requestedDepths().map(depth =>
|
|
167
|
+
new ParseCase(`parse:${depth}`, { config, contents, depth }))
|
|
168
|
+
|
|
169
|
+
// every case walks the same input, so one alignment covers the suite and all
|
|
170
|
+
// of them stay directly comparable
|
|
171
|
+
const perInvocation = cases[0].operationsPerInvocation
|
|
172
|
+
const operationsPerRound = cases[0].alignOperations(argv.msgs)
|
|
173
|
+
const benchOptions: IBenchOptions = {
|
|
174
|
+
warmupOperations: cases[0].alignOperations(argv.warmup),
|
|
175
|
+
rounds: argv.rounds,
|
|
176
|
+
operationsPerRound,
|
|
177
|
+
gcBetweenRounds: true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`${path.relative(root, fixPath)} ${perInvocation} msg/pass dict ${argv.dict}`)
|
|
181
|
+
const suite = await new BenchRunner(benchOptions).runAll(argv.label, cases as IBenchCase[])
|
|
182
|
+
|
|
183
|
+
if (argv.json) {
|
|
184
|
+
console.log(JSON.stringify(suite, null, 2))
|
|
185
|
+
} else {
|
|
186
|
+
console.log()
|
|
187
|
+
console.log(formatSuite(suite))
|
|
188
|
+
}
|
|
189
|
+
if (argv.baseline) {
|
|
190
|
+
console.log()
|
|
191
|
+
console.log(formatComparison(readSuite(argv.baseline), suite))
|
|
192
|
+
}
|
|
193
|
+
if (argv.save) {
|
|
194
|
+
const target = norm(argv.save)
|
|
195
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
196
|
+
fs.writeFileSync(target, JSON.stringify(suite, null, 2), 'utf8')
|
|
197
|
+
console.log(`saved ${path.relative(root, target)}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch(e => {
|
|
202
|
+
console.error(e instanceof Error ? e.message : e)
|
|
203
|
+
process.exit(1)
|
|
204
|
+
})
|
|
@@ -126,8 +126,12 @@ export abstract class SessionLauncher {
|
|
|
126
126
|
this.registerApplication(container)
|
|
127
127
|
} else {
|
|
128
128
|
if (factory.makeSession) {
|
|
129
|
+
// resolve the config from the container doing the resolving, not the one
|
|
130
|
+
// captured here. An acceptor resolves each session from a per-connection
|
|
131
|
+
// child scope which re-registers IJsFixConfig, so this is how an application
|
|
132
|
+
// receives that session's own description, store and message factory.
|
|
129
133
|
container.register<FixSession>(DITokens.FixSession, {
|
|
130
|
-
useFactory: () => factory.makeSession(
|
|
134
|
+
useFactory: (c) => factory.makeSession(c.resolve<IJsFixConfig>(DITokens.IJsFixConfig))
|
|
131
135
|
})
|
|
132
136
|
}
|
|
133
137
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DependencyContainer } from 'tsyringe'
|
|
2
|
+
import { IJsFixConfig, JsFixConfig } from '../config'
|
|
3
|
+
import { DITokens } from './di-tokens'
|
|
4
|
+
import { ElasticBuffer } from '../buffer/elastic-buffer'
|
|
5
|
+
import { ISessionDescription } from '../transport/session/session-description'
|
|
6
|
+
import { ISessionMsgFactory } from '../transport/session/session-msg-factory'
|
|
7
|
+
import { cloneSessionDescription } from '../transport/session/clone-session-description'
|
|
8
|
+
import { AsciiSessionMsgFactory } from '../transport/ascii/ascii-session-msg-factory'
|
|
9
|
+
import { FixmlSessionMsgFactory } from '../transport/fixml/fixml-session-msg-factory'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A FIX acceptor serves many connections from one listener configuration. Until
|
|
13
|
+
* this existed every accepted connection shared the *same* parse buffer, transmit
|
|
14
|
+
* buffer, session description and message factory, because those are registered as
|
|
15
|
+
* instances on the single session container built at start up. Two clients talking
|
|
16
|
+
* to one acceptor would therefore interleave bytes in a shared ElasticBuffer and
|
|
17
|
+
* compute the same SessionId - so the same message store file.
|
|
18
|
+
*
|
|
19
|
+
* A session scope is a tsyringe child container which re-registers exactly the
|
|
20
|
+
* per-connection state. Everything else (dictionary, transient parser/transmitter
|
|
21
|
+
* classes, logger factory) is inherited from the parent, and resolving a parent
|
|
22
|
+
* registration from the child still injects the child's instances, so an
|
|
23
|
+
* AsciiParser resolved here receives this scope's ParseBuffer.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors cspurefix TradeCaptureSessionFactory.CreatePerSessionConfigAndEncoder,
|
|
26
|
+
* which had to do the same thing for the same reason.
|
|
27
|
+
*/
|
|
28
|
+
export function makeSessionScope (
|
|
29
|
+
config: IJsFixConfig,
|
|
30
|
+
overrides?: Partial<ISessionDescription>): IJsFixConfig {
|
|
31
|
+
const parent: DependencyContainer = config.sessionContainer
|
|
32
|
+
const scope = parent.createChildContainer()
|
|
33
|
+
|
|
34
|
+
const description = cloneSessionDescription(config.description, overrides)
|
|
35
|
+
const factory = makeSessionMsgFactory(description, config.factory)
|
|
36
|
+
|
|
37
|
+
scope.registerInstance(DITokens.ISessionDescription, description)
|
|
38
|
+
scope.registerInstance(DITokens.ISessionMsgFactory, factory)
|
|
39
|
+
scope.registerInstance<ElasticBuffer>(DITokens.TransmitBuffer, cloneBuffer(parent, DITokens.TransmitBuffer))
|
|
40
|
+
scope.registerInstance<ElasticBuffer>(DITokens.ParseBuffer, cloneBuffer(parent, DITokens.ParseBuffer))
|
|
41
|
+
|
|
42
|
+
const scoped = new JsFixConfig(factory, config.definitions, description, config.delimiter, config.logFactory)
|
|
43
|
+
scoped.logDelimiter = config.logDelimiter ?? scoped.logDelimiter
|
|
44
|
+
scoped.sessionStoreFactory = config.sessionStoreFactory
|
|
45
|
+
scoped.sessionRegistry = config.sessionRegistry
|
|
46
|
+
scoped.sessionContainer = scope
|
|
47
|
+
scope.registerInstance(DITokens.IJsFixConfig, scoped)
|
|
48
|
+
scope.registerInstance(DITokens.Definitions, config.definitions)
|
|
49
|
+
|
|
50
|
+
return scoped
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A fresh buffer of the same capacity as the one the parent container holds, so the
|
|
55
|
+
* sizing decisions made in SessionContainer.asAscii are honoured without repeating
|
|
56
|
+
* them here.
|
|
57
|
+
*/
|
|
58
|
+
function cloneBuffer (parent: DependencyContainer, token: DITokens): ElasticBuffer {
|
|
59
|
+
if (!parent.isRegistered(token, true)) {
|
|
60
|
+
return new ElasticBuffer()
|
|
61
|
+
}
|
|
62
|
+
const existing = parent.resolve<ElasticBuffer>(token)
|
|
63
|
+
return new ElasticBuffer(existing.size)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The scope needs a factory bound to *its* description, because the outbound header
|
|
68
|
+
* carries this session's comp ids. Applications may supply their own factory (by
|
|
69
|
+
* overriding SessionContainer.makeSessionFactory), and that must survive into the
|
|
70
|
+
* per-session scope - building a stock one here would silently drop custom header
|
|
71
|
+
* handling on the acceptor path. See issue #87.
|
|
72
|
+
*/
|
|
73
|
+
function makeSessionMsgFactory (
|
|
74
|
+
description: ISessionDescription,
|
|
75
|
+
current: ISessionMsgFactory | null): ISessionMsgFactory {
|
|
76
|
+
if (current?.cloneFor) {
|
|
77
|
+
return current.cloneFor(description)
|
|
78
|
+
}
|
|
79
|
+
const ascii = description.application?.protocol === 'ascii'
|
|
80
|
+
const mutator = (current as any)?.mutator ?? null
|
|
81
|
+
return ascii
|
|
82
|
+
? new AsciiSessionMsgFactory(description, mutator)
|
|
83
|
+
: new FixmlSessionMsgFactory(description, mutator)
|
|
84
|
+
}
|
|
@@ -19,9 +19,9 @@ class AppLauncher extends SessionLauncher {
|
|
|
19
19
|
protected override makeFactory (config: IJsFixConfig): EngineFactory {
|
|
20
20
|
const isInitiator = this.isInitiator(config.description)
|
|
21
21
|
return {
|
|
22
|
-
makeSession: () => isInitiator
|
|
23
|
-
? new HttpClient(
|
|
24
|
-
: new HttpServer(
|
|
22
|
+
makeSession: (sessionConfig: IJsFixConfig) => isInitiator
|
|
23
|
+
? new HttpClient(sessionConfig)
|
|
24
|
+
: new HttpServer(sessionConfig)
|
|
25
25
|
} as EngineFactory
|
|
26
26
|
}
|
|
27
27
|
|