Skip to content

Commit 454e847

Browse files
authored
Merge pull request #481 from jcreedcmu/jcreed/interpreted-pagination
Allow pagination for interpreted results
2 parents 6e34c03 + e2d125a commit 454e847

File tree

5 files changed

+177
-78
lines changed

5 files changed

+177
-78
lines changed

extensions/ql-vscode/src/adapt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DecodedBqrsChunk, ResultSetSchema, ColumnKind, Column, ColumnValue } from './bqrs-cli-types';
22
import { LocationValue, ResultSetSchema as AdaptedSchema, ColumnSchema, ColumnType, LocationStyle } from 'semmle-bqrs';
3+
import { ResultSet } from './interface-types';
34

45
// FIXME: This is a temporary bit of impedance matching to convert
56
// from the types provided by ./bqrs-cli-types, to the types used by
@@ -128,7 +129,8 @@ export interface ExtensionParsedResultSets {
128129
t: 'ExtensionParsed';
129130
pageNumber: number;
130131
numPages: number;
132+
numInterpretedPages: number;
131133
selectedTable?: string; // when undefined, means 'show default table'
132134
resultSetNames: string[];
133-
resultSet: RawResultSet;
135+
resultSet: ResultSet;
134136
}

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ export type PathTableResultSet = {
2323

2424
export type ResultSet = RawTableResultSet | PathTableResultSet;
2525

26-
/**
27-
* Only ever show this many results per run in interpreted results.
28-
*/
29-
export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
30-
3126
/**
3227
* Only ever show this many rows in a raw result table.
3328
*/
@@ -38,6 +33,11 @@ export const RAW_RESULTS_LIMIT = 10000;
3833
*/
3934
export const RAW_RESULTS_PAGE_SIZE = 100;
4035

36+
/**
37+
* Show this many rows in an interpreted results table at a time.
38+
*/
39+
export const INTERPRETED_RESULTS_PAGE_SIZE = 100;
40+
4141
export interface DatabaseInfo {
4242
name: string;
4343
databaseUri: string;
@@ -61,6 +61,7 @@ export interface PreviousExecution {
6161
export interface Interpretation {
6262
sourceLocationPrefix: string;
6363
numTruncatedResults: number;
64+
numTotalResults: number;
6465
/**
6566
* sortState being undefined means don't sort, just present results in the order
6667
* they appear in the sarif file.
@@ -113,6 +114,16 @@ export interface SetStateMsg {
113114
parsedResultSets: ParsedResultSets;
114115
}
115116

117+
export interface ShowInterpretedPageMsg {
118+
t: 'showInterpretedPage';
119+
interpretation: Interpretation;
120+
database: DatabaseInfo;
121+
metadata?: QueryMetadata;
122+
pageNumber: number;
123+
numPages: number;
124+
resultSetNames: string[];
125+
}
126+
116127
/** Advance to the next or previous path no in the path viewer */
117128
export interface NavigatePathMsg {
118129
t: 'navigatePath';
@@ -124,6 +135,7 @@ export interface NavigatePathMsg {
124135
export type IntoResultsViewMsg =
125136
| ResultsUpdatingMsg
126137
| SetStateMsg
138+
| ShowInterpretedPageMsg
127139
| NavigatePathMsg;
128140

129141
export type FromResultsViewMsg =

extensions/ql-vscode/src/interface.ts

Lines changed: 110 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { assertNever } from './helpers-pure';
1919
import {
2020
FromResultsViewMsg,
2121
Interpretation,
22-
INTERPRETED_RESULTS_PER_RUN_LIMIT,
2322
IntoResultsViewMsg,
2423
QueryMetadata,
2524
ResultsPaths,
@@ -28,6 +27,8 @@ import {
2827
InterpretedResultsSortState,
2928
SortDirection,
3029
RAW_RESULTS_PAGE_SIZE,
30+
INTERPRETED_RESULTS_PAGE_SIZE,
31+
ALERTS_TABLE_NAME,
3132
} from './interface-types';
3233
import { Logger } from './logging';
3334
import * as messages from './messages';
@@ -51,6 +52,7 @@ import {
5152
jumpToLocation,
5253
} from './interface-utils';
5354
import { getDefaultResultSetName } from './interface-types';
55+
import { ResultSetSchema } from './bqrs-cli-types';
5456

5557
/**
5658
* interface.ts
@@ -95,6 +97,10 @@ function numPagesOfResultSet(resultSet: RawResultSet): number {
9597
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
9698
}
9799

100+
function numInterpretedPages(interpretation: Interpretation | undefined): number {
101+
return Math.ceil((interpretation?.sarif.runs[0].results?.length || 0) / INTERPRETED_RESULTS_PAGE_SIZE);
102+
}
103+
98104
export class InterfaceManager extends DisposableObject {
99105
private _displayedQuery?: CompletedQuery;
100106
private _interpretation?: Interpretation;
@@ -244,7 +250,12 @@ export class InterfaceManager extends DisposableObject {
244250
);
245251
break;
246252
case 'changePage':
247-
await this.showPageOfResults(msg.selectedTable, msg.pageNumber);
253+
if (msg.selectedTable === ALERTS_TABLE_NAME) {
254+
await this.showPageOfInterpretedResults(msg.pageNumber);
255+
}
256+
else {
257+
await this.showPageOfRawResults(msg.selectedTable, msg.pageNumber);
258+
}
248259
break;
249260
default:
250261
assertNever(msg);
@@ -283,7 +294,8 @@ export class InterfaceManager extends DisposableObject {
283294
return;
284295
}
285296

286-
const interpretation = await this.interpretResultsInfo(
297+
this._interpretation = undefined;
298+
const interpretationPage = await this.interpretResultsInfo(
287299
results.query,
288300
results.interpretedResultsSortState
289301
);
@@ -295,7 +307,6 @@ export class InterfaceManager extends DisposableObject {
295307
);
296308

297309
this._displayedQuery = results;
298-
this._interpretation = interpretation;
299310

300311
const panel = this.getPanel();
301312
await this.waitForPanelLoaded();
@@ -325,19 +336,13 @@ export class InterfaceManager extends DisposableObject {
325336

326337
const getParsedResultSets = async (): Promise<ParsedResultSets> => {
327338
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
328-
const schemas = await this.cliServer.bqrsInfo(
329-
results.query.resultsPaths.resultsPath,
330-
RAW_RESULTS_PAGE_SIZE
331-
);
332-
333-
const resultSetNames = schemas['result-sets'].map(
334-
(resultSet) => resultSet.name
335-
);
339+
const resultSetSchemas = await this.getResultSetSchemas(results);
340+
const resultSetNames = resultSetSchemas.map(schema => schema.name);
336341

337342
// This may not wind up being the page we actually show, if there are interpreted results,
338343
// but speculatively send it anyway.
339344
const selectedTable = getDefaultResultSetName(resultSetNames);
340-
const schema = schemas['result-sets'].find(
345+
const schema = resultSetSchemas.find(
341346
(resultSet) => resultSet.name == selectedTable
342347
)!;
343348
if (schema === undefined) {
@@ -352,12 +357,12 @@ export class InterfaceManager extends DisposableObject {
352357
);
353358
const adaptedSchema = adaptSchema(schema);
354359
const resultSet = adaptBqrs(adaptedSchema, chunk);
355-
356360
return {
357361
t: 'ExtensionParsed',
358362
pageNumber: 0,
359363
numPages: numPagesOfResultSet(resultSet),
360-
resultSet,
364+
numInterpretedPages: numInterpretedPages(this._interpretation),
365+
resultSet: { t: 'RawResultSet', ...resultSet },
361366
selectedTable: undefined,
362367
resultSetNames,
363368
};
@@ -368,7 +373,7 @@ export class InterfaceManager extends DisposableObject {
368373

369374
await this.postMessage({
370375
t: 'setState',
371-
interpretation,
376+
interpretation: interpretationPage,
372377
origResultsPaths: results.query.resultsPaths,
373378
resultsPath: this.convertPathToWebviewUri(
374379
results.query.resultsPaths.resultsPath
@@ -381,10 +386,48 @@ export class InterfaceManager extends DisposableObject {
381386
});
382387
}
383388

389+
/**
390+
* Show a page of interpreted results
391+
*/
392+
public async showPageOfInterpretedResults(
393+
pageNumber: number
394+
): Promise<void> {
395+
if (this._displayedQuery === undefined) {
396+
throw new Error('Trying to show interpreted results but displayed query was undefined');
397+
}
398+
if (this._interpretation === undefined) {
399+
throw new Error('Trying to show interpreted results but interpretation was undefined');
400+
}
401+
if (this._interpretation.sarif.runs[0].results === undefined) {
402+
throw new Error('Trying to show interpreted results but results were undefined');
403+
}
404+
405+
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery);
406+
const resultSetNames = resultSetSchemas.map(schema => schema.name);
407+
408+
await this.postMessage({
409+
t: 'showInterpretedPage',
410+
interpretation: this.getPageOfInterpretedResults(pageNumber),
411+
database: this._displayedQuery.database,
412+
metadata: this._displayedQuery.query.metadata,
413+
pageNumber,
414+
resultSetNames,
415+
numPages: numInterpretedPages(this._interpretation),
416+
});
417+
}
418+
419+
private async getResultSetSchemas(results: CompletedQuery): Promise<ResultSetSchema[]> {
420+
const schemas = await this.cliServer.bqrsInfo(
421+
results.query.resultsPaths.resultsPath,
422+
RAW_RESULTS_PAGE_SIZE
423+
);
424+
return schemas['result-sets'];
425+
}
426+
384427
/**
385428
* Show a page of raw results from the chosen table.
386429
*/
387-
public async showPageOfResults(
430+
public async showPageOfRawResults(
388431
selectedTable: string,
389432
pageNumber: number
390433
): Promise<void> {
@@ -399,16 +442,10 @@ export class InterfaceManager extends DisposableObject {
399442
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
400443
);
401444

402-
const schemas = await this.cliServer.bqrsInfo(
403-
results.query.resultsPaths.resultsPath,
404-
RAW_RESULTS_PAGE_SIZE
405-
);
406-
407-
const resultSetNames = schemas['result-sets'].map(
408-
(resultSet) => resultSet.name
409-
);
445+
const resultSetSchemas = await this.getResultSetSchemas(results);
446+
const resultSetNames = resultSetSchemas.map(schema => schema.name);
410447

411-
const schema = schemas['result-sets'].find(
448+
const schema = resultSetSchemas.find(
412449
(resultSet) => resultSet.name == selectedTable
413450
)!;
414451
if (schema === undefined)
@@ -426,8 +463,9 @@ export class InterfaceManager extends DisposableObject {
426463
const parsedResultSets: ParsedResultSets = {
427464
t: 'ExtensionParsed',
428465
pageNumber,
429-
resultSet,
466+
resultSet: { t: 'RawResultSet', ...resultSet },
430467
numPages: numPagesOfResultSet(resultSet),
468+
numInterpretedPages: numInterpretedPages(this._interpretation),
431469
selectedTable: selectedTable,
432470
resultSetNames,
433471
};
@@ -447,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
447485
});
448486
}
449487

450-
private async getTruncatedResults(
488+
private async _getInterpretedResults(
451489
metadata: QueryMetadata | undefined,
452490
resultsPaths: ResultsPaths,
453491
sourceInfo: cli.SourceInfo | undefined,
@@ -460,37 +498,58 @@ export class InterfaceManager extends DisposableObject {
460498
resultsPaths,
461499
sourceInfo
462500
);
463-
// For performance reasons, limit the number of results we try
464-
// to serialize and send to the webview. TODO: possibly also
465-
// limit number of paths per result, number of steps per path,
466-
// or throw an error if we are in aggregate trying to send
467-
// massively too much data, as it can make the extension
468-
// unresponsive.
469-
470-
let numTruncatedResults = 0;
471-
sarif.runs.forEach((run) => {
472-
if (run.results !== undefined) {
501+
sarif.runs.forEach(run => {
502+
if (run.results !== undefined)
473503
sortInterpretedResults(run.results, sortState);
474-
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
475-
numTruncatedResults +=
476-
run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
477-
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
478-
}
479-
}
480504
});
481-
return {
505+
506+
const numTotalResults = (() => {
507+
if (sarif.runs.length === 0) return 0;
508+
if (sarif.runs[0].results === undefined) return 0;
509+
return sarif.runs[0].results.length;
510+
})();
511+
512+
const interpretation: Interpretation = {
482513
sarif,
483514
sourceLocationPrefix,
484-
numTruncatedResults,
515+
numTruncatedResults: 0,
516+
numTotalResults,
485517
sortState,
486518
};
519+
this._interpretation = interpretation;
520+
return interpretation;
521+
}
522+
523+
private getPageOfInterpretedResults(
524+
pageNumber: number
525+
): Interpretation {
526+
527+
function getPageOfRun(run: Sarif.Run): Sarif.Run {
528+
return {
529+
...run, results: run.results?.slice(
530+
INTERPRETED_RESULTS_PAGE_SIZE * pageNumber,
531+
INTERPRETED_RESULTS_PAGE_SIZE * (pageNumber + 1)
532+
)
533+
};
534+
}
535+
536+
if (this._interpretation === undefined) {
537+
throw new Error('Tried to get interpreted results before interpretation finished');
538+
}
539+
if (this._interpretation.sarif.runs.length !== 1) {
540+
this.logger.log(`Warning: SARIF file had ${this._interpretation.sarif.runs.length} runs, expected 1`);
541+
}
542+
const interp = this._interpretation;
543+
return {
544+
...interp,
545+
sarif: { ...interp.sarif, runs: [getPageOfRun(interp.sarif.runs[0])] },
546+
};
487547
}
488548

489549
private async interpretResultsInfo(
490550
query: QueryInfo,
491551
sortState: InterpretedResultsSortState | undefined
492552
): Promise<Interpretation | undefined> {
493-
let interpretation: Interpretation | undefined = undefined;
494553
if (
495554
(await query.canHaveInterpretedResults()) &&
496555
query.quickEvalPosition === undefined // never do results interpretation if quickEval
@@ -507,7 +566,7 @@ export class InterfaceManager extends DisposableObject {
507566
sourceArchive: sourceArchiveUri.fsPath,
508567
sourceLocationPrefix,
509568
};
510-
interpretation = await this.getTruncatedResults(
569+
await this._getInterpretedResults(
511570
query.metadata,
512571
query.resultsPaths,
513572
sourceInfo,
@@ -522,7 +581,7 @@ export class InterfaceManager extends DisposableObject {
522581
);
523582
}
524583
}
525-
return interpretation;
584+
return this._interpretation && this.getPageOfInterpretedResults(0);
526585
}
527586

528587
private async showResultsAsDiagnostics(
@@ -541,7 +600,8 @@ export class InterfaceManager extends DisposableObject {
541600
sourceArchive: sourceArchiveUri.fsPath,
542601
sourceLocationPrefix,
543602
};
544-
const interpretation = await this.getTruncatedResults(
603+
// TODO: Performance-testing to determine whether this truncation is necessary.
604+
const interpretation = await this._getInterpretedResults(
545605
metadata,
546606
resultsInfo,
547607
sourceInfo,

0 commit comments

Comments
 (0)