Skip to content

Commit 7319295

Browse files
committed
Add infrastructure to lazily parse CXXRTL packets.
At the moment this still forces each packet to a deserialized representation, but it opens the door to avoiding this for packets going through secondary links.
1 parent fce5805 commit 7319295

File tree

3 files changed

+111
-57
lines changed

3 files changed

+111
-57
lines changed

src/cxxrtl/client.ts

+33-22
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,39 @@ export enum ConnectionState {
1616
// Note that we trust that server returns well-formed JSON. It would take far too much time to
1717
// verify its adherence to the schema here, for little gain.
1818
export class Connection {
19+
private readonly link: link.ILink;
20+
1921
private _state = ConnectionState.Initializing;
2022

2123
private _commands: string[] = [];
2224
private _events: string[] = [];
2325
private _itemValuesEncodings: string[] = [];
2426

2527
private promises: {
26-
resolve: (response: proto.AnyResponse) => void;
28+
resolve: (response: link.Packet<proto.AnyResponse>) => void;
2729
reject: (error: Error) => void;
2830
}[] = [];
2931
private timestamps: Date[] = [];
3032

3133
private sendIndex: number = 0;
3234
private recvIndex: number = 0;
3335

34-
constructor(private readonly link: link.ILink) {
36+
constructor(link_: link.ILink) {
37+
this.link = link_;
3538
this.link.onRecv = this.onLinkRecv.bind(this);
3639
this.link.onDone = this.onLinkDone.bind(this);
37-
this.send({
40+
this.send(link.Packet.fromObject({
3841
type: 'greeting',
3942
version: 0,
40-
});
43+
}));
4144
}
4245

4346
dispose(): void {
4447
this.link.dispose();
4548
}
4649

47-
private traceSend(packet: proto.ClientPacket) {
50+
private traceSend(linkPacket: link.Packet<proto.ClientPacket>) {
51+
const packet = linkPacket.asObject();
4852
if (packet.type === 'greeting') {
4953
console.debug('[CXXRTL] C>S', packet);
5054
} else if (packet.type === 'command') {
@@ -53,7 +57,8 @@ export class Connection {
5357
}
5458
}
5559

56-
private traceRecv(packet: proto.ServerPacket) {
60+
private traceRecv(linkPacket: link.Packet<proto.ServerPacket>) {
61+
const packet = linkPacket.asObject();
5762
if (packet.type === 'greeting') {
5863
console.debug('[CXXRTL] S>C', packet);
5964
} else if (packet.type === 'response') {
@@ -67,17 +72,18 @@ export class Connection {
6772
}
6873
}
6974

70-
private async send(packet: proto.ClientPacket): Promise<void> {
71-
this.traceSend(packet);
75+
private async send(linkPacket: link.Packet<proto.ClientPacket>): Promise<void> {
76+
this.traceSend(linkPacket);
7277
if (this._state === ConnectionState.Disconnected) {
7378
throw new Error('unable to send packet after link is shutdown');
7479
} else {
75-
this.link.send(packet);
80+
this.link.send(linkPacket);
7681
}
7782
}
7883

79-
private async onLinkRecv(packet: proto.ServerPacket): Promise<void> {
80-
this.traceRecv(packet);
84+
private async onLinkRecv(linkPacket: link.Packet<proto.ServerPacket>): Promise<void> {
85+
this.traceRecv(linkPacket);
86+
const packet = linkPacket.asObject();
8187
if (this._state === ConnectionState.Initializing && packet.type === 'greeting') {
8288
if (packet.version === 0) {
8389
this._commands = packet.commands;
@@ -93,15 +99,15 @@ export class Connection {
9399
const nextPromise = this.promises.shift();
94100
if (nextPromise !== undefined) {
95101
if (packet.type === 'response') {
96-
nextPromise.resolve(packet);
102+
nextPromise.resolve(link.Packet.fromObject(packet));
97103
} else {
98104
nextPromise.reject(new CommandError(packet));
99105
}
100106
} else {
101107
this.rejectPromises(new Error(`unexpected '${packet.type}' reply with no commands queued`));
102108
}
103109
} else if (this._state === ConnectionState.Connected && packet.type === 'event') {
104-
await this.onEvent(packet);
110+
await this.onEvent(link.Packet.fromObject(packet));
105111
} else {
106112
this.rejectPromises(new Error(`unexpected ${packet.type} packet received for ${this._state} connection`));
107113
}
@@ -119,7 +125,7 @@ export class Connection {
119125
}
120126
}
121127

122-
async perform(command: proto.AnyCommand): Promise<proto.AnyResponse> {
128+
async exchange(command: link.Packet<proto.AnyCommand>): Promise<link.Packet<proto.AnyResponse>> {
123129
await this.send(command);
124130
return new Promise((resolve, reject) => {
125131
this.promises.push({ resolve, reject });
@@ -130,7 +136,7 @@ export class Connection {
130136

131137
async onDisconnected(): Promise<void> {}
132138

133-
async onEvent(_event: proto.AnyEvent): Promise<void> {}
139+
async onEvent(_event: link.Packet<proto.AnyEvent>): Promise<void> {}
134140

135141
get state(): ConnectionState {
136142
return this._state;
@@ -148,31 +154,36 @@ export class Connection {
148154
return this._itemValuesEncodings.slice();
149155
}
150156

157+
private async command<T extends proto.AnyResponse>(command: proto.AnyCommand): Promise<T> {
158+
const response = await this.exchange(link.Packet.fromObject(command));
159+
return response.cast<T>().asObject();
160+
}
161+
151162
async listScopes(command: proto.CommandListScopes): Promise<proto.ResponseListScopes> {
152-
return await this.perform(command) as proto.ResponseListScopes;
163+
return this.command<proto.ResponseListScopes>(command);
153164
}
154165

155166
async listItems(command: proto.CommandListItems): Promise<proto.ResponseListItems> {
156-
return await this.perform(command) as proto.ResponseListItems;
167+
return this.command<proto.ResponseListItems>(command);
157168
}
158169

159170
async referenceItems(command: proto.CommandReferenceItems): Promise<proto.ResponseReferenceItems> {
160-
return await this.perform(command) as proto.ResponseReferenceItems;
171+
return this.command<proto.ResponseReferenceItems>(command);
161172
}
162173

163174
async queryInterval(command: proto.CommandQueryInterval): Promise<proto.ResponseQueryInterval> {
164-
return await this.perform(command) as proto.ResponseQueryInterval;
175+
return this.command<proto.ResponseQueryInterval>(command);
165176
}
166177

167178
async getSimulationStatus(command: proto.CommandGetSimulationStatus): Promise<proto.ResponseGetSimulationStatus> {
168-
return await this.perform(command) as proto.ResponseGetSimulationStatus;
179+
return this.command<proto.ResponseGetSimulationStatus>(command);
169180
}
170181

171182
async runSimulation(command: proto.CommandRunSimulation): Promise<proto.ResponseRunSimulation> {
172-
return await this.perform(command) as proto.ResponseRunSimulation;
183+
return this.command<proto.ResponseRunSimulation>(command);
173184
}
174185

175186
async pauseSimulation(command: proto.CommandPauseSimulation): Promise<proto.ResponsePauseSimulation> {
176-
return await this.perform(command) as proto.ResponsePauseSimulation;
187+
return this.command<proto.ResponsePauseSimulation>(command);
177188
}
178189
}

src/cxxrtl/link.ts

+59-19
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,52 @@ import * as stream from 'node:stream';
22

33
import * as proto from './proto';
44

5+
// Lazily serialize/deserialize packets in case they only need to be passed along.
6+
export class Packet<T> {
7+
private constructor(
8+
private serialized: string | undefined,
9+
private deserialized: T | undefined,
10+
) { }
11+
12+
static fromString<T>(serialized: string) {
13+
return new Packet<T>(serialized, undefined);
14+
}
15+
16+
static fromObject<T>(deserialized: T) {
17+
return new Packet<T>(undefined, deserialized);
18+
}
19+
20+
asString(): string {
21+
if (this.serialized === undefined) {
22+
this.serialized = JSON.stringify(this.deserialized!);
23+
}
24+
return this.serialized;
25+
}
26+
27+
asObject(): T {
28+
if (this.deserialized === undefined) {
29+
this.deserialized = <T>JSON.parse(this.serialized!);
30+
}
31+
return this.deserialized;
32+
}
33+
34+
cast<U>(): Packet<U> {
35+
return <Packet<U>>(<unknown>(this));
36+
}
37+
38+
// Make sure we don't unintentionally negate the performance advantages of this wrapper.
39+
toJSON(): never {
40+
throw new Error('call Packet.asObject() instead of serializing with JSON.stringify()');
41+
}
42+
}
43+
544
export interface ILink {
645
dispose(): void;
746

8-
onRecv: (packet: proto.ServerPacket) => Promise<void>;
47+
onRecv: (packet: Packet<proto.ServerPacket>) => Promise<void>;
948
onDone: () => Promise<void>;
1049

11-
send(packet: proto.ClientPacket): Promise<void>;
50+
send(packet: Packet<proto.ClientPacket>): Promise<void>;
1251
}
1352

1453
export class MockLink implements ILink {
@@ -22,24 +61,24 @@ export class MockLink implements ILink {
2261
}
2362
}
2463

25-
async onRecv(_serverPacket: proto.ServerPacket): Promise<void> {}
64+
async onRecv(_serverPacket: Packet<proto.ServerPacket>): Promise<void> {}
2665

2766
async onDone(): Promise<void> {}
2867

29-
async send(clientPacket: proto.ClientPacket): Promise<void> {
68+
async send(clientPacket: Packet<proto.ClientPacket>): Promise<void> {
3069
if (this.conversation.length === 0) {
3170
throw new Error('premature end of conversation');
3271
}
3372

3473
const [[expectedClient, expectedServer], ...restOfConversation] = this.conversation;
3574

36-
if (JSON.stringify(clientPacket) === JSON.stringify(expectedClient)) {
75+
if (clientPacket.asString() === JSON.stringify(expectedClient)) {
3776
if (expectedServer instanceof Array) {
3877
for (const serverPacket of expectedServer) {
39-
await this.onRecv(serverPacket);
78+
await this.onRecv(Packet.fromObject(serverPacket));
4079
}
4180
} else {
42-
await this.onRecv(expectedServer);
81+
await this.onRecv(Packet.fromObject(expectedServer));
4382
}
4483
} else {
4584
console.error('unexpected client packet', clientPacket, '; expected:', expectedClient);
@@ -82,28 +121,28 @@ export class NodeStreamLink implements ILink {
82121
// Second, convert the packet text to JSON. This can throw errors e.g. if there is foreign
83122
// data injected between server replies, or the server is malfunctioning. In that case,
84123
// stop processing input.
85-
const packets: proto.ServerPacket[] = [];
124+
const packets: Packet<proto.ServerPacket>[] = [];
86125
for (const packetText of packetTexts) {
87-
try {
88-
packets.push(JSON.parse(packetText) as proto.ServerPacket);
89-
} catch (error) {
90-
console.error('malformed JSON: ', packetText);
91-
this.stream.pause();
92-
return;
93-
}
126+
packets.push(Packet.fromString<proto.ServerPacket>(packetText));
94127
}
95128

96129
// Finally, run the handler for each of the packets. If the handler blocks, don't wait for
97130
// its completion, but run the next handler anyway; this is because a handler can send
98131
// another client packet, causing `onStreamData` to be re-entered, anyway.
99132
for (const packet of packets) {
100-
(async (packet: proto.ServerPacket) => {
133+
const success = (async (packet) => {
101134
try {
102135
await this.onRecv(packet);
136+
return true;
103137
} catch (error) {
104138
console.error('uncaught error in onRecv', error);
139+
this.stream.pause();
140+
return false;
105141
}
106142
})(packet);
143+
if (!success) {
144+
break;
145+
}
107146
}
108147
}
109148

@@ -119,11 +158,12 @@ export class NodeStreamLink implements ILink {
119158
this.stream.destroy();
120159
}
121160

122-
async onRecv(_serverPacket: proto.ServerPacket): Promise<void> {}
161+
async onRecv(_serverPacket: Packet<proto.ServerPacket>): Promise<void> {}
123162

124163
async onDone(): Promise<void> {}
125164

126-
async send(clientPacket: proto.ClientPacket): Promise<void> {
127-
this.stream.write(JSON.stringify(clientPacket) + '\0');
165+
async send(clientPacket: Packet<proto.ClientPacket>): Promise<void> {
166+
this.stream.write(clientPacket.asString());
167+
this.stream.write('\0');
128168
}
129169
}

src/debug/session.ts

+19-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as vscode from 'vscode';
22

33
import * as proto from '../cxxrtl/proto';
4-
import { ILink } from '../cxxrtl/link';
4+
import * as link from '../cxxrtl/link';
55
import { Connection } from '../cxxrtl/client';
66
import { TimeInterval, TimePoint } from '../model/time';
77
import { Diagnostic, DiagnosticType, Reference, Sample, UnboundReference } from '../model/sample';
@@ -40,10 +40,10 @@ export enum SimulationPauseReason {
4040
export class Session {
4141
private connection: Connection;
4242

43-
private secondaryLinks: ILink[] = [];
43+
private secondaryLinks: link.ILink[] = [];
4444
private greetingPacketPromise: Promise<proto.ServerGreeting>;
4545

46-
constructor(link: ILink) {
46+
constructor(link: link.ILink) {
4747
this.connection = new Connection(link);
4848
this.greetingPacketPromise = new Promise((resolve, _reject) => {
4949
this.connection.onConnected = async (greetingPacket) => resolve(greetingPacket);
@@ -53,10 +53,11 @@ export class Session {
5353
secondaryLink.onDone();
5454
}
5555
};
56-
this.connection.onEvent = async (event) => {
56+
this.connection.onEvent = async (linkEvent) => {
5757
for (const secondaryLink of this.secondaryLinks) {
58-
secondaryLink.onRecv(event);
58+
secondaryLink.onRecv(linkEvent);
5959
}
60+
const event = linkEvent.asObject();
6061
if (event.event === 'simulation_paused') {
6162
await this.handleSimulationPausedEvent(event.cause);
6263
} else if (event.event === 'simulation_finished') {
@@ -70,33 +71,35 @@ export class Session {
7071
this.connection.dispose();
7172
}
7273

73-
createSecondaryLink(): ILink {
74-
const link: ILink = {
74+
createSecondaryLink(): link.ILink {
75+
const secondaryLink: link.ILink = {
7576
dispose: () => {
76-
this.secondaryLinks.splice(this.secondaryLinks.indexOf(link));
77+
this.secondaryLinks.splice(this.secondaryLinks.indexOf(secondaryLink));
7778
},
7879

79-
send: async (clientPacket) => {
80-
if (clientPacket.type === 'greeting') {
80+
send: async (linkCommandPacket) => {
81+
const packet = linkCommandPacket.asObject();
82+
if (packet.type === 'greeting') {
8183
const serverGreetingPacket = await this.greetingPacketPromise;
82-
if (clientPacket.version === serverGreetingPacket.version) {
83-
await link.onRecv(serverGreetingPacket);
84+
if (packet.version === serverGreetingPacket.version) {
85+
await secondaryLink.onRecv(link.Packet.fromObject(serverGreetingPacket));
8486
} else {
8587
throw new Error(
86-
`Secondary link requested greeting version ${clientPacket.version}, ` +
88+
`Secondary link requested greeting version ${packet.version}, ` +
8789
`but server greeting version is ${serverGreetingPacket.version}`
8890
);
8991
}
9092
} else {
91-
const serverPacket = await this.connection.perform(clientPacket);
92-
await link.onRecv(serverPacket);
93+
const linkResponsePacket = await this.connection.exchange(
94+
linkCommandPacket.cast<proto.AnyCommand>());
95+
await secondaryLink.onRecv(linkResponsePacket);
9396
}
9497
},
9598

9699
onRecv: async (serverPacket) => {},
97100
onDone: async () => {},
98101
};
99-
return link;
102+
return secondaryLink;
100103
}
101104

102105
// ======================================== Inspecting the design

0 commit comments

Comments
 (0)