Skip to content

Commit a69ef15

Browse files
committed
Implement streaming for compare view
1 parent 4ed6167 commit a69ef15

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed

extensions/ql-vscode/src/common/interface-types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ interface ChangeCompareMessage {
360360
export type ToCompareViewMessage =
361361
| SetComparisonQueryInfoMessage
362362
| SetComparisonsMessage
363+
| StreamingComparisonSetupMessage
364+
| StreamingComparisonAddResultsMessage
365+
| StreamingComparisonCompleteMessage
363366
| SetUserSettingsMsg;
364367

365368
/**
@@ -419,6 +422,24 @@ export type InterpretedQueryCompareResult = {
419422
to: Result[];
420423
};
421424

425+
export interface StreamingComparisonSetupMessage {
426+
readonly t: "streamingComparisonSetup";
427+
readonly currentResultSetName: string;
428+
readonly message: string | undefined;
429+
// The from and to fields will only contain a chunk of the results
430+
readonly result: QueryCompareResult;
431+
}
432+
433+
interface StreamingComparisonAddResultsMessage {
434+
readonly t: "streamingComparisonAddResults";
435+
// The from and to fields will only contain a chunk of the results
436+
readonly result: QueryCompareResult;
437+
}
438+
439+
interface StreamingComparisonCompleteMessage {
440+
readonly t: "streamingComparisonComplete";
441+
}
442+
422443
/**
423444
* Extract the name of the default result. Prefer returning
424445
* 'alerts', or '#select'. Otherwise return the first in the list.

extensions/ql-vscode/src/compare/compare-view.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,92 @@ export class CompareView extends AbstractWebview<
183183
message = getErrorMessage(e);
184184
}
185185

186+
await this.streamResults(result, currentResultSetDisplayName, message);
187+
}
188+
}
189+
190+
private async streamResults(
191+
result: QueryCompareResult | undefined,
192+
currentResultSetName: string,
193+
message: string | undefined,
194+
) {
195+
// Since there is a string limit of 1GB in Node.js, the comparison is send as a JSON.stringified string to the webview
196+
// and some comparisons may be larger than that, we sometimes need to stream results. This uses a heuristic of 2,000 results
197+
// to determine if we should stream results.
198+
199+
if (!this.shouldStreamResults(result)) {
186200
await this.postMessage({
187201
t: "setComparisons",
188202
result,
189-
currentResultSetName: currentResultSetDisplayName,
203+
currentResultSetName,
190204
message,
191205
});
206+
return;
207+
}
208+
209+
// Streaming itself is implemented like this:
210+
// - 1 setup message which contains the first 1,000 results
211+
// - n "add results" messages which contain 1,000 results each
212+
// - 1 complete message which just tells the webview that we're done
213+
214+
await this.postMessage({
215+
t: "streamingComparisonSetup",
216+
result: this.chunkResults(result, 0, 1000),
217+
currentResultSetName,
218+
message,
219+
});
220+
221+
const { from, to } = result;
222+
223+
const maxResults = Math.max(from.length, to.length);
224+
for (let i = 1000; i < maxResults; i += 1000) {
225+
const chunk = this.chunkResults(result, i, i + 1000);
226+
227+
await this.postMessage({
228+
t: "streamingComparisonAddResults",
229+
result: chunk,
230+
});
231+
}
232+
233+
await this.postMessage({
234+
t: "streamingComparisonComplete",
235+
});
236+
}
237+
238+
private shouldStreamResults(
239+
result: QueryCompareResult | undefined,
240+
): result is QueryCompareResult {
241+
if (result === undefined) {
242+
return false;
192243
}
244+
245+
// We probably won't run into limits if we have less than 2,000 total results
246+
const totalResults = result.from.length + result.to.length;
247+
return totalResults > 2000;
248+
}
249+
250+
private chunkResults(
251+
result: QueryCompareResult,
252+
start: number,
253+
end: number,
254+
): QueryCompareResult {
255+
if (result.kind === "raw") {
256+
return {
257+
...result,
258+
from: result.from.slice(start, end),
259+
to: result.to.slice(start, end),
260+
};
261+
}
262+
263+
if (result.kind === "interpreted") {
264+
return {
265+
...result,
266+
from: result.from.slice(start, end),
267+
to: result.to.slice(start, end),
268+
};
269+
}
270+
271+
assertNever(result);
193272
}
194273

195274
protected getPanelConfig(): WebviewPanelConfig {

extensions/ql-vscode/src/view/compare/Compare.tsx

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { useState, useEffect } from "react";
1+
import { useState, useEffect, useRef } from "react";
22
import { styled } from "styled-components";
33

44
import type {
55
ToCompareViewMessage,
66
SetComparisonsMessage,
77
SetComparisonQueryInfoMessage,
88
UserSettings,
9+
StreamingComparisonSetupMessage,
10+
QueryCompareResult,
911
} from "../../common/interface-types";
1012
import { DEFAULT_USER_SETTINGS } from "../../common/interface-types";
1113
import CompareSelector from "./CompareSelector";
@@ -37,6 +39,12 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
3739
DEFAULT_USER_SETTINGS,
3840
);
3941

42+
// This is a ref because we don't need to re-render when we get a new streaming comparison message
43+
// and we don't want to change the listener every time we get a new message
44+
const streamingComparisonRef = useRef<StreamingComparisonSetupMessage | null>(
45+
null,
46+
);
47+
4048
const message = comparison?.message || "Empty comparison";
4149
const hasRows =
4250
comparison?.result &&
@@ -53,6 +61,72 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
5361
case "setComparisons":
5462
setComparison(msg);
5563
break;
64+
case "streamingComparisonSetup":
65+
setComparison(null);
66+
streamingComparisonRef.current = msg;
67+
break;
68+
case "streamingComparisonAddResults": {
69+
const prev = streamingComparisonRef.current;
70+
if (prev === null) {
71+
console.warn(
72+
'Received "streamingComparisonAddResults" before "streamingComparisonSetup"',
73+
);
74+
break;
75+
}
76+
77+
let result: QueryCompareResult;
78+
switch (prev.result.kind) {
79+
case "raw":
80+
if (msg.result.kind !== "raw") {
81+
throw new Error(
82+
"Streaming comparison: expected raw results, got interpreted results",
83+
);
84+
}
85+
86+
result = {
87+
...prev.result,
88+
from: [...prev.result.from, ...msg.result.from],
89+
to: [...prev.result.to, ...msg.result.to],
90+
};
91+
break;
92+
case "interpreted":
93+
if (msg.result.kind !== "interpreted") {
94+
throw new Error(
95+
"Streaming comparison: expected interpreted results, got raw results",
96+
);
97+
}
98+
99+
result = {
100+
...prev.result,
101+
from: [...prev.result.from, ...msg.result.from],
102+
to: [...prev.result.to, ...msg.result.to],
103+
};
104+
break;
105+
default:
106+
throw new Error("Unexpected comparison result kind");
107+
}
108+
109+
streamingComparisonRef.current = {
110+
...prev,
111+
result,
112+
};
113+
114+
break;
115+
}
116+
case "streamingComparisonComplete":
117+
if (streamingComparisonRef.current === null) {
118+
console.warn(
119+
'Received "streamingComparisonComplete" before "streamingComparisonSetup"',
120+
);
121+
setComparison(null);
122+
break;
123+
}
124+
setComparison({
125+
...streamingComparisonRef.current,
126+
t: "setComparisons",
127+
});
128+
streamingComparisonRef.current = null;
129+
break;
56130
case "setUserSettings":
57131
setUserSettings(msg.userSettings);
58132
break;

0 commit comments

Comments
 (0)