Skip to content

Commit 833f8e0

Browse files
Add a tree viewer UI for the evaluator logs (#1433)
Co-authored-by: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
1 parent 747049e commit 833f8e0

File tree

14 files changed

+499
-91
lines changed

14 files changed

+499
-91
lines changed

extensions/ql-vscode/package.json

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"onView:codeQLDatabases",
3939
"onView:codeQLQueryHistory",
4040
"onView:codeQLAstViewer",
41+
"onView:codeQLEvalLogViewer",
4142
"onView:test-explorer",
4243
"onCommand:codeQL.checkForUpdatesToCLI",
4344
"onCommand:codeQL.authenticateToGitHub",
@@ -527,11 +528,15 @@
527528
},
528529
{
529530
"command": "codeQLQueryHistory.showEvalLog",
530-
"title": "Show Evaluator Log (Raw)"
531+
"title": "Show Evaluator Log (Raw JSON)"
531532
},
532533
{
533534
"command": "codeQLQueryHistory.showEvalLogSummary",
534-
"title": "Show Evaluator Log (Summary)"
535+
"title": "Show Evaluator Log (Summary Text)"
536+
},
537+
{
538+
"command": "codeQLQueryHistory.showEvalLogViewer",
539+
"title": "Show Evaluator Log (UI)"
535540
},
536541
{
537542
"command": "codeQLQueryHistory.cancel",
@@ -608,6 +613,14 @@
608613
"light": "media/light/clear-all.svg",
609614
"dark": "media/dark/clear-all.svg"
610615
}
616+
},
617+
{
618+
"command": "codeQLEvalLogViewer.clear",
619+
"title": "Clear Viewer",
620+
"icon": {
621+
"light": "media/light/clear-all.svg",
622+
"dark": "media/dark/clear-all.svg"
623+
}
611624
}
612625
],
613626
"menus": {
@@ -681,6 +694,11 @@
681694
"command": "codeQLAstViewer.clear",
682695
"when": "view == codeQLAstViewer",
683696
"group": "navigation"
697+
},
698+
{
699+
"command": "codeQLEvalLogViewer.clear",
700+
"when": "view == codeQLEvalLogViewer",
701+
"group": "navigation"
684702
}
685703
],
686704
"view/item/context": [
@@ -754,6 +772,11 @@
754772
"group": "9_qlCommands",
755773
"when": "codeql.supportsEvalLog && viewItem == rawResultsItem || codeql.supportsEvalLog && viewItem == interpretedResultsItem || codeql.supportsEvalLog && viewItem == cancelledResultsItem"
756774
},
775+
{
776+
"command": "codeQLQueryHistory.showEvalLogViewer",
777+
"group": "9_qlCommands",
778+
"when": "config.codeQL.canary && codeql.supportsEvalLog && viewItem == rawResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == interpretedResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == cancelledResultsItem"
779+
},
757780
{
758781
"command": "codeQLQueryHistory.showQueryText",
759782
"group": "9_qlCommands",
@@ -975,6 +998,10 @@
975998
"command": "codeQLQueryHistory.showEvalLogSummary",
976999
"when": "false"
9771000
},
1001+
{
1002+
"command": "codeQLQueryHistory.showEvalLogViewer",
1003+
"when": "false"
1004+
},
9781005
{
9791006
"command": "codeQLQueryHistory.openQueryDirectory",
9801007
"when": "false"
@@ -1043,6 +1070,10 @@
10431070
"command": "codeQLAstViewer.clear",
10441071
"when": "false"
10451072
},
1073+
{
1074+
"command": "codeQLEvalLogViewer.clear",
1075+
"when": "false"
1076+
},
10461077
{
10471078
"command": "codeQLTests.acceptOutput",
10481079
"when": "false"
@@ -1109,6 +1140,11 @@
11091140
{
11101141
"id": "codeQLAstViewer",
11111142
"name": "AST Viewer"
1143+
},
1144+
{
1145+
"id": "codeQLEvalLogViewer",
1146+
"name": "Evaluator Log Viewer",
1147+
"when": "config.codeQL.canary"
11121148
}
11131149
]
11141150
},
@@ -1124,6 +1160,10 @@
11241160
{
11251161
"view": "codeQLDatabases",
11261162
"contents": "Add a CodeQL database:\n[From a folder](command:codeQLDatabases.chooseDatabaseFolder)\n[From an archive](command:codeQLDatabases.chooseDatabaseArchive)\n[From a URL (as a zip file)](command:codeQLDatabases.chooseDatabaseInternet)\n[From LGTM](command:codeQLDatabases.chooseDatabaseLgtm)"
1163+
},
1164+
{
1165+
"view": "codeQLEvalLogViewer",
1166+
"contents": "Run the 'Show Evaluator Log (UI)' command on a CodeQL query run in the Query History view."
11271167
}
11281168
]
11291169
},
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ChildEvalLogTreeItem, EvalLogTreeItem } from './eval-log-viewer';
2+
import { EvalLogData as EvalLogData } from './pure/log-summary-parser';
3+
4+
/** Builds the tree data for the evaluator log viewer for a single query run. */
5+
export default class EvalLogTreeBuilder {
6+
private queryName: string;
7+
private evalLogDataItems: EvalLogData[];
8+
9+
constructor(queryName: string, evaluatorLogDataItems: EvalLogData[]) {
10+
this.queryName = queryName;
11+
this.evalLogDataItems = evaluatorLogDataItems;
12+
}
13+
14+
async getRoots(): Promise<EvalLogTreeItem[]> {
15+
return await this.parseRoots();
16+
}
17+
18+
private async parseRoots(): Promise<EvalLogTreeItem[]> {
19+
const roots: EvalLogTreeItem[] = [];
20+
21+
// Once the viewer can show logs for multiple queries, there will be more than 1 item at the root
22+
// level. For now, there will always be one root (the one query being shown).
23+
const queryItem: EvalLogTreeItem = {
24+
label: this.queryName,
25+
children: [] // Will assign predicate items as children shortly.
26+
};
27+
28+
// Display descriptive message when no data exists
29+
if (this.evalLogDataItems.length === 0) {
30+
const noResultsItem: ChildEvalLogTreeItem = {
31+
label: 'No predicates evaluated in this query run.',
32+
parent: queryItem,
33+
children: [],
34+
};
35+
queryItem.children.push(noResultsItem);
36+
}
37+
38+
// For each predicate, create a TreeItem object with appropriate parents/children
39+
this.evalLogDataItems.forEach(logDataItem => {
40+
const predicateLabel = `${logDataItem.predicateName} (${logDataItem.resultSize} tuples, ${logDataItem.millis} ms)`;
41+
const predicateItem: ChildEvalLogTreeItem = {
42+
label: predicateLabel,
43+
parent: queryItem,
44+
children: [] // Will assign pipeline items as children shortly.
45+
};
46+
for (const [pipelineName, steps] of Object.entries(logDataItem.ra)) {
47+
const pipelineLabel = `Pipeline: ${pipelineName}`;
48+
const pipelineItem: ChildEvalLogTreeItem = {
49+
label: pipelineLabel,
50+
parent: predicateItem,
51+
children: [] // Will assign step items as children shortly.
52+
};
53+
predicateItem.children.push(pipelineItem);
54+
55+
pipelineItem.children = steps.map((step: string) => ({
56+
label: step,
57+
parent: pipelineItem,
58+
children: []
59+
}));
60+
}
61+
queryItem.children.push(predicateItem);
62+
});
63+
64+
roots.push(queryItem);
65+
return roots;
66+
}
67+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { window, TreeDataProvider, TreeView, TreeItem, ProviderResult, Event, EventEmitter, TreeItemCollapsibleState } from 'vscode';
2+
import { commandRunner } from './commandRunner';
3+
import { DisposableObject } from './pure/disposable-object';
4+
import { showAndLogErrorMessage } from './helpers';
5+
6+
export interface EvalLogTreeItem {
7+
label?: string;
8+
children: ChildEvalLogTreeItem[];
9+
}
10+
11+
export interface ChildEvalLogTreeItem extends EvalLogTreeItem {
12+
parent: ChildEvalLogTreeItem | EvalLogTreeItem;
13+
}
14+
15+
/** Provides data from parsed CodeQL evaluator logs to be rendered in a tree view. */
16+
class EvalLogDataProvider extends DisposableObject implements TreeDataProvider<EvalLogTreeItem> {
17+
public roots: EvalLogTreeItem[] = [];
18+
19+
private _onDidChangeTreeData: EventEmitter<EvalLogTreeItem | undefined | null | void> = new EventEmitter<EvalLogTreeItem | undefined | null | void>();
20+
readonly onDidChangeTreeData: Event<EvalLogTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
21+
22+
refresh(): void {
23+
this._onDidChangeTreeData.fire();
24+
}
25+
26+
getTreeItem(element: EvalLogTreeItem): TreeItem | Thenable<TreeItem> {
27+
const state = element.children.length
28+
? TreeItemCollapsibleState.Collapsed
29+
: TreeItemCollapsibleState.None;
30+
const treeItem = new TreeItem(element.label || '', state);
31+
treeItem.tooltip = `${treeItem.label} || ''}`;
32+
return treeItem;
33+
}
34+
35+
getChildren(element?: EvalLogTreeItem): ProviderResult<EvalLogTreeItem[]> {
36+
// If no item is passed, return the root.
37+
if (!element) {
38+
return this.roots || [];
39+
}
40+
// Otherwise it is called with an existing item, to load its children.
41+
return element.children;
42+
}
43+
44+
getParent(element: ChildEvalLogTreeItem): ProviderResult<EvalLogTreeItem> {
45+
return element.parent;
46+
}
47+
}
48+
49+
/** Manages a tree viewer of structured evaluator logs. */
50+
export class EvalLogViewer extends DisposableObject {
51+
private treeView: TreeView<EvalLogTreeItem>;
52+
private treeDataProvider: EvalLogDataProvider;
53+
54+
constructor() {
55+
super();
56+
57+
this.treeDataProvider = new EvalLogDataProvider();
58+
this.treeView = window.createTreeView('codeQLEvalLogViewer', {
59+
treeDataProvider: this.treeDataProvider,
60+
showCollapseAll: true
61+
});
62+
63+
this.push(this.treeView);
64+
this.push(this.treeDataProvider);
65+
this.push(
66+
commandRunner('codeQLEvalLogViewer.clear', async () => {
67+
this.clear();
68+
})
69+
);
70+
}
71+
72+
private clear(): void {
73+
this.treeDataProvider.roots = [];
74+
this.treeDataProvider.refresh();
75+
this.treeView.message = undefined;
76+
}
77+
78+
// Called when the Show Evaluator Log (UI) command is run on a new query.
79+
updateRoots(roots: EvalLogTreeItem[]): void {
80+
this.treeDataProvider.roots = roots;
81+
this.treeDataProvider.refresh();
82+
83+
this.treeView.message = 'Viewer for query run:'; // Currently only one query supported at a time.
84+
85+
// Handle error on reveal. This could happen if
86+
// the tree view is disposed during the reveal.
87+
this.treeView.reveal(roots[0], { focus: false })?.then(
88+
() => { /**/ },
89+
err => showAndLogErrorMessage(err)
90+
);
91+
}
92+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import { handleDownloadPacks, handleInstallPackDependencies } from './packaging'
9898
import { HistoryItemLabelProvider } from './history-item-label-provider';
9999
import { exportRemoteQueryResults } from './remote-queries/export-results';
100100
import { RemoteQuery } from './remote-queries/remote-query';
101+
import { EvalLogViewer } from './eval-log-viewer';
101102

102103
/**
103104
* extension.ts
@@ -442,6 +443,10 @@ async function activateWithInstalledDistribution(
442443
databaseUI.init();
443444
ctx.subscriptions.push(databaseUI);
444445

446+
void logger.log('Initializing evaluator log viewer.');
447+
const evalLogViewer = new EvalLogViewer();
448+
ctx.subscriptions.push(evalLogViewer);
449+
445450
void logger.log('Initializing query history manager.');
446451
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
447452
ctx.subscriptions.push(queryHistoryConfigurationListener);
@@ -465,6 +470,7 @@ async function activateWithInstalledDistribution(
465470
dbm,
466471
intm,
467472
rqm,
473+
evalLogViewer,
468474
queryStorageDir,
469475
ctx,
470476
queryHistoryConfigurationListener,
Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,36 @@
11
// TODO(angelapwen): Only load in necessary information and
2-
// location in bytes for this log to save memory.
3-
export interface EvaluatorLogData {
4-
queryCausingWork: string;
5-
predicateName: string;
6-
millis: number;
7-
resultSize: number;
8-
ra: Pipelines;
9-
}
10-
11-
interface Pipelines {
12-
// Key: pipeline identifier; Value: array of pipeline steps
13-
pipelineNamesToSteps: Map<string, string[]>;
2+
// location in bytes for this log to save memory.
3+
export interface EvalLogData {
4+
predicateName: string;
5+
millis: number;
6+
resultSize: number;
7+
// Key: pipeline identifier; Value: array of pipeline steps
8+
ra: Record<string, string[]>;
149
}
1510

1611
/**
1712
* A pure method that parses a string of evaluator log summaries into
18-
* an array of EvaluatorLogData objects.
13+
* an array of EvalLogData objects.
1914
*
2015
*/
21-
export function parseVisualizerData(logSummary: string): EvaluatorLogData[] {
22-
// Remove newline delimiters because summary is in .jsonl format.
23-
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);
24-
const visualizerData: EvaluatorLogData[] = [];
16+
export function parseViewerData(logSummary: string): EvalLogData[] {
17+
// Remove newline delimiters because summary is in .jsonl format.
18+
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);
19+
const viewerData: EvalLogData[] = [];
2520

26-
for (const obj of jsonSummaryObjects) {
27-
const jsonObj = JSON.parse(obj);
21+
for (const obj of jsonSummaryObjects) {
22+
const jsonObj = JSON.parse(obj);
2823

29-
// Only convert log items that have an RA and millis field
30-
if (jsonObj.ra !== undefined && jsonObj.millis !== undefined) {
31-
const newLogData: EvaluatorLogData = {
32-
queryCausingWork: jsonObj.queryCausingWork,
33-
predicateName: jsonObj.predicateName,
34-
millis: jsonObj.millis,
35-
resultSize: jsonObj.resultSize,
36-
ra: jsonObj.ra
37-
};
38-
visualizerData.push(newLogData);
39-
}
24+
// Only convert log items that have an RA and millis field
25+
if (jsonObj.ra !== undefined && jsonObj.millis !== undefined) {
26+
const newLogData: EvalLogData = {
27+
predicateName: jsonObj.predicateName,
28+
millis: jsonObj.millis,
29+
resultSize: jsonObj.resultSize,
30+
ra: jsonObj.ra
31+
};
32+
viewerData.push(newLogData);
4033
}
41-
return visualizerData;
34+
}
35+
return viewerData;
4236
}

0 commit comments

Comments
 (0)