Skip to content

Commit 625c8a7

Browse files
authored
Merge pull request #3771 from github/koesie10/streaming-comparison
Implement streaming for compare view
2 parents 79aafeb + 510a269 commit 625c8a7

File tree

3 files changed

+201
-2
lines changed

3 files changed

+201
-2
lines changed

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

Lines changed: 25 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,28 @@ export type InterpretedQueryCompareResult = {
419422
to: Result[];
420423
};
421424

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

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

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from "./result-set-names";
3535
import { compareInterpretedResults } from "./interpreted-results";
3636
import { isCanary } from "../config";
37+
import { nanoid } from "nanoid";
3738

3839
interface ComparePair {
3940
from: CompletedLocalQueryInfo;
@@ -183,13 +184,97 @@ export class CompareView extends AbstractWebview<
183184
message = getErrorMessage(e);
184185
}
185186

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

195280
protected getPanelConfig(): WebviewPanelConfig {

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

Lines changed: 90 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,87 @@ 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+
if (prev.id !== msg.id) {
78+
console.warn(
79+
'Received "streamingComparisonAddResults" with different id, ignoring',
80+
);
81+
break;
82+
}
83+
84+
let result: QueryCompareResult;
85+
switch (prev.result.kind) {
86+
case "raw":
87+
if (msg.result.kind !== "raw") {
88+
throw new Error(
89+
"Streaming comparison: expected raw results, got interpreted results",
90+
);
91+
}
92+
93+
result = {
94+
...prev.result,
95+
from: [...prev.result.from, ...msg.result.from],
96+
to: [...prev.result.to, ...msg.result.to],
97+
};
98+
break;
99+
case "interpreted":
100+
if (msg.result.kind !== "interpreted") {
101+
throw new Error(
102+
"Streaming comparison: expected interpreted results, got raw results",
103+
);
104+
}
105+
106+
result = {
107+
...prev.result,
108+
from: [...prev.result.from, ...msg.result.from],
109+
to: [...prev.result.to, ...msg.result.to],
110+
};
111+
break;
112+
default:
113+
throw new Error("Unexpected comparison result kind");
114+
}
115+
116+
streamingComparisonRef.current = {
117+
...prev,
118+
result,
119+
};
120+
121+
break;
122+
}
123+
case "streamingComparisonComplete":
124+
if (streamingComparisonRef.current === null) {
125+
console.warn(
126+
'Received "streamingComparisonComplete" before "streamingComparisonSetup"',
127+
);
128+
setComparison(null);
129+
break;
130+
}
131+
132+
if (streamingComparisonRef.current.id !== msg.id) {
133+
console.warn(
134+
'Received "streamingComparisonComplete" with different id, ignoring',
135+
);
136+
break;
137+
}
138+
139+
setComparison({
140+
...streamingComparisonRef.current,
141+
t: "setComparisons",
142+
});
143+
streamingComparisonRef.current = null;
144+
break;
56145
case "setUserSettings":
57146
setUserSettings(msg.userSettings);
58147
break;

0 commit comments

Comments
 (0)