Compare commits
243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edc2fe8454 | ||
|
|
427c6031fe | ||
|
|
6687669aad | ||
|
|
2d3b62a021 | ||
|
|
f8d010ad10 | ||
|
|
c35f927436 | ||
|
|
ffed4b634f | ||
|
|
13389358ac | ||
|
|
60f392cceb | ||
|
|
6f8d6f2541 | ||
|
|
3fbbf4045d | ||
|
|
666c26e6a1 | ||
|
|
bba31c030a | ||
|
|
8b8d174781 | ||
|
|
370b17c0f5 | ||
|
|
37dcd0822b | ||
|
|
f09210b033 | ||
|
|
44d33d6d31 | ||
|
|
08bffab05f | ||
|
|
2293cc3537 | ||
|
|
6f461e75a7 | ||
|
|
8b0a16ea14 | ||
|
|
c086a80384 | ||
|
|
e947756a5a | ||
|
|
6cafa5d905 | ||
|
|
4607a452bd | ||
|
|
276405f743 | ||
|
|
056e45ad1e | ||
|
|
575628990e | ||
|
|
9f0a5f0daa | ||
|
|
92e5181dd6 | ||
|
|
88924f1556 | ||
|
|
26edfa5c43 | ||
|
|
17bae27c34 | ||
|
|
49839a1a52 | ||
|
|
60754a81d6 | ||
|
|
bda74b83c0 | ||
|
|
055c53aba1 | ||
|
|
8e5f331cb5 | ||
|
|
b4d925bbb2 | ||
|
|
a9879d2da3 | ||
|
|
3dcfefa0ae | ||
|
|
e81dda377a | ||
|
|
1faaaff59e | ||
|
|
75f77bcfca | ||
|
|
94c576b255 | ||
|
|
acf7ccdf6a | ||
|
|
b24aedea99 | ||
|
|
29b0269a40 | ||
|
|
00e27195d9 | ||
|
|
e54805fb06 | ||
|
|
6fc8b726f4 | ||
|
|
daf1096389 | ||
|
|
e35bd1be8c | ||
|
|
995a311d5f | ||
|
|
a86775f840 | ||
|
|
64e60f986c | ||
|
|
a47dc5555d | ||
|
|
8c81ab73f9 | ||
|
|
99f082fb27 | ||
|
|
05926431da | ||
|
|
52d20df1ae | ||
|
|
370e192dfc | ||
|
|
db12100969 | ||
|
|
bb435a9a28 | ||
|
|
7babc6421e | ||
|
|
8c37403a1c | ||
|
|
92a0fb1e34 | ||
|
|
162b255899 | ||
|
|
2a8aede154 | ||
|
|
a3b8fea2b6 | ||
|
|
07e9e44310 | ||
|
|
2c7021be0e | ||
|
|
99535003e1 | ||
|
|
8ab6990407 | ||
|
|
36a3ee311a | ||
|
|
be48acdf0b | ||
|
|
91beca7d85 | ||
|
|
793748c2f4 | ||
|
|
bf62444403 | ||
|
|
de6bd05a29 | ||
|
|
40fa5aaed8 | ||
|
|
24bb6bbf32 | ||
|
|
25f5dbfdcf | ||
|
|
e98313a893 | ||
|
|
7610de24c8 | ||
|
|
a6da43b9e9 | ||
|
|
ffe8248040 | ||
|
|
54e531d175 | ||
|
|
ff95caffd0 | ||
|
|
da108fb115 | ||
|
|
0d4850c811 | ||
|
|
2b958743e7 | ||
|
|
a6859aba47 | ||
|
|
bae16bb8d9 | ||
|
|
85953fb235 | ||
|
|
de9fd276d1 | ||
|
|
9506c17a79 | ||
|
|
036c60b494 | ||
|
|
52beaeded2 | ||
|
|
b1a15d8340 | ||
|
|
815af5cc48 | ||
|
|
b331cb019c | ||
|
|
d03b3486ee | ||
|
|
17542d1041 | ||
|
|
6fd3d205a5 | ||
|
|
aca005a777 | ||
|
|
0f88b63392 | ||
|
|
52fe22a0be | ||
|
|
89729d1831 | ||
|
|
b212ee6a4b | ||
|
|
580ed9f54f | ||
|
|
52683302f2 | ||
|
|
cc4513c927 | ||
|
|
f256c00ced | ||
|
|
15be2d14b3 | ||
|
|
ff501552a3 | ||
|
|
aa528c6037 | ||
|
|
c99bf5bb9f | ||
|
|
afa3d558c6 | ||
|
|
b9d15511cb | ||
|
|
8a58279e67 | ||
|
|
d37469fc94 | ||
|
|
2cae71c657 | ||
|
|
568f0827b2 | ||
|
|
4a835b8711 | ||
|
|
ab00152ce2 | ||
|
|
48954c7d22 | ||
|
|
9a0699f50a | ||
|
|
eec42c5532 | ||
|
|
d008963602 | ||
|
|
9800fa1333 | ||
|
|
62f3b4f696 | ||
|
|
6f7eb74496 | ||
|
|
6568b569a1 | ||
|
|
558d957eb7 | ||
|
|
20f6e3d45c | ||
|
|
b05ec33ba3 | ||
|
|
1d2c2cfcf9 | ||
|
|
e039f6bc52 | ||
|
|
6d4427e59c | ||
|
|
412338c717 | ||
|
|
ccf2dc64ac | ||
|
|
453aa833f2 | ||
|
|
260bf0e8d1 | ||
|
|
876c5b6091 | ||
|
|
317e52c0e7 | ||
|
|
03ca407713 | ||
|
|
58afeba1ac | ||
|
|
8268d6812f | ||
|
|
70ec5704c8 | ||
|
|
aaf23eae72 | ||
|
|
96aa770e85 | ||
|
|
3b0697771d | ||
|
|
a5b64d6459 | ||
|
|
60c15a0eb2 | ||
|
|
f5cd48d9d9 | ||
|
|
94434f4397 | ||
|
|
f77ae4cd69 | ||
|
|
5a3aeb6332 | ||
|
|
86f37f408f | ||
|
|
5d7db66902 | ||
|
|
87a470dde6 | ||
|
|
dba6718649 | ||
|
|
2bcdf93996 | ||
|
|
1468b2830b | ||
|
|
5a078d35e0 | ||
|
|
5ebc76a005 | ||
|
|
badaedd1fe | ||
|
|
06fb1bed5a | ||
|
|
15d2e4ee6b | ||
|
|
4c2e0ccdda | ||
|
|
b840c38886 | ||
|
|
99175e78b0 | ||
|
|
46c284d2ed | ||
|
|
1ac725b1c4 | ||
|
|
eee593973d | ||
|
|
57e2b51b43 | ||
|
|
b90cfb670b | ||
|
|
d05cdf49ec | ||
|
|
38849f70f5 | ||
|
|
6d665ea5c8 | ||
|
|
025737a18b | ||
|
|
e7e95e2511 | ||
|
|
8b3add82b1 | ||
|
|
5b854bc1cd | ||
|
|
9f1fd2c8af | ||
|
|
17a6076732 | ||
|
|
1b007c2586 | ||
|
|
1f6a7afffa | ||
|
|
5d4f75b72e | ||
|
|
8170c46042 | ||
|
|
a93bf1469b | ||
|
|
775e6dc354 | ||
|
|
c84331e1a3 | ||
|
|
955f8c8ab4 | ||
|
|
1e749ec793 | ||
|
|
fb6fac8803 | ||
|
|
1ec341a744 | ||
|
|
736dc46b63 | ||
|
|
bb1da9c6ff | ||
|
|
2cde3b9c2f | ||
|
|
fac7961e2d | ||
|
|
4e32a108a6 | ||
|
|
9a0bff6ebb | ||
|
|
d5f3c77690 | ||
|
|
ea45e389c4 | ||
|
|
873d4e3e7e | ||
|
|
eca125c24e | ||
|
|
6162d268ba | ||
|
|
6ba2a19c5e | ||
|
|
d97db8c02a | ||
|
|
acc7025555 | ||
|
|
84a31a940e | ||
|
|
24cee731fe | ||
|
|
471bf28bb3 | ||
|
|
8ce5b920eb | ||
|
|
fa3e3ff669 | ||
|
|
e146e7a314 | ||
|
|
bd38355591 | ||
|
|
06b45394dd | ||
|
|
c9fbafb919 | ||
|
|
5969c6f5d3 | ||
|
|
687aceca8f | ||
|
|
811972e87b | ||
|
|
71cd892afa | ||
|
|
11bc465fca | ||
|
|
eeeeadd06d | ||
|
|
b44c6024f4 | ||
|
|
67ae86067e | ||
|
|
7c1cb87647 | ||
|
|
7b2901568b | ||
|
|
deaaeb82b5 | ||
|
|
fdc925bad7 | ||
|
|
86b5a8bbee | ||
|
|
563ad5b09d | ||
|
|
e3c79e48b4 | ||
|
|
8e99bc93c0 | ||
|
|
7373919843 | ||
|
|
1f8996dbe7 | ||
|
|
20a8976d8f | ||
|
|
e7d76f7605 | ||
|
|
80a6116bef |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -38,7 +38,7 @@ updates:
|
||||
labels:
|
||||
- "Update dependencies"
|
||||
- package-ecosystem: docker
|
||||
directory: "extensions/ql-vscode/test/e2e"
|
||||
directory: "extensions/ql-vscode/test/e2e/docker"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "thursday" # Thursday is arbitrary
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -124,8 +124,9 @@ jobs:
|
||||
needs: build
|
||||
environment: publish-vscode-marketplace
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -139,9 +140,16 @@ jobs:
|
||||
with:
|
||||
name: vscode-codeql-extension
|
||||
|
||||
- name: Azure User-assigned managed identity login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
allow-no-subscriptions: true
|
||||
enable-AzPSSession: true
|
||||
|
||||
- name: Publish to Registry
|
||||
run: |
|
||||
npx @vscode/vsce publish -p $VSCE_TOKEN --packagePath *.vsix
|
||||
run: npx @vscode/vsce publish --azure-credential --packagePath *.vsix
|
||||
|
||||
open-vsx-publish:
|
||||
name: Publish to Open VSX Registry
|
||||
|
||||
2
.github/workflows/update-node-version.yml
vendored
2
.github/workflows/update-node-version.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Update Node version
|
||||
working-directory: extensions/ql-vscode
|
||||
run: |
|
||||
npx ts-node scripts/update-node-version.ts
|
||||
npx vite-node scripts/update-node-version.ts
|
||||
shell: bash
|
||||
- name: Get current Node version
|
||||
working-directory: extensions/ql-vscode
|
||||
|
||||
@@ -78,7 +78,7 @@ $ vscode/scripts/code-cli.sh --install-extension dist/vscode-codeql-*.vsix # if
|
||||
|
||||
### Debugging
|
||||
|
||||
You can use VS Code to debug the extension without explicitly installing it. Just open this directory as a workspace in VS Code, and hit `F5` to start a debugging session.
|
||||
You can use VS Code to debug the extension without explicitly installing it. Just open this repository's root directory as a workspace in VS Code, and hit `F5` to start a debugging session.
|
||||
|
||||
### Storybook
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ The extension is released. You can download it from the [Visual Studio Marketpla
|
||||
|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||
[](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amain)
|
||||
[](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amain)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Releasing (write access required)
|
||||
|
||||
1. Make sure the needed authentication keys are valid. Most likely the Azure DevOps PAT needs to be regenerated. See below.
|
||||
1. Determine the new version number. We default to increasing the patch version number, but make our own judgement about whether a change is big enough to warrant a minor version bump. Common reasons for a minor bump could include:
|
||||
- Making substantial new features available to all users. This can include lifting a feature flag.
|
||||
- Breakage in compatibility with recent versions of the CLI.
|
||||
@@ -61,7 +60,7 @@
|
||||
|
||||
## Secrets and authentication for publishing
|
||||
|
||||
Repository administrators, will need to manage the authentication keys for publishing to the VS Code marketplace and Open VSX. Each requires an authentication token.
|
||||
Repository administrators will need to manage the authentication keys for publishing to the VS Code marketplace and Open VSX. Each requires an authentication token.
|
||||
|
||||
To regenerate the Open VSX token:
|
||||
|
||||
@@ -70,4 +69,4 @@ To regenerate the Open VSX token:
|
||||
1. Go to the [Access Tokens](https://open-vsx.org/user-settings/tokens) page and generate a new token.
|
||||
1. Update the secret in the `publish-open-vsx` environment in the project settings.
|
||||
|
||||
To regenerate the VSCode Marketplace token, please see our internal documentation. Note that Azure DevOps PATs expire every 7 days and must be regenerated.
|
||||
Publishing to the VS Code Marketplace is done using a user-assigned managed identity and should not require the token to be manually updated.
|
||||
|
||||
@@ -1 +1 @@
|
||||
v20.16.0
|
||||
v20.18.1
|
||||
|
||||
@@ -1,17 +1 @@
|
||||
.vs/**
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
typings/**
|
||||
out/test/**
|
||||
out/vscode-tests/**
|
||||
**/@types/**
|
||||
**/*.ts
|
||||
test/**
|
||||
src/**
|
||||
**/*.map
|
||||
.gitignore
|
||||
gulpfile.js/**
|
||||
tsconfig.json
|
||||
.prettierrc
|
||||
vsc-extension-quickstart.md
|
||||
node_modules/**
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.17.1 - 23 January 2025
|
||||
|
||||
- Remove support for CodeQL CLI versions older than 2.18.4. [#3895](https://github.com/github/vscode-codeql/pull/3895)
|
||||
|
||||
## 1.17.0 - 20 December 2024
|
||||
|
||||
- Add a palette command that allows importing all databases directly inside of a parent folder. [#3797](https://github.com/github/vscode-codeql/pull/3797)
|
||||
- Only use VS Code telemetry settings instead of using `codeQL.telemetry.enableTelemetry` [#3853](https://github.com/github/vscode-codeql/pull/3853)
|
||||
- Improve the performance of the results view with large numbers of results. [#3862](https://github.com/github/vscode-codeql/pull/3862)
|
||||
|
||||
## 1.16.1 - 6 November 2024
|
||||
|
||||
- Support result columns of type `QlBuiltins::BigInt` in quick evaluations. [#3647](https://github.com/github/vscode-codeql/pull/3647)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolve } from "path";
|
||||
import { deployPackage } from "./deploy";
|
||||
import { spawn } from "child-process-promise";
|
||||
import { spawn } from "cross-spawn";
|
||||
|
||||
export async function packageExtension(): Promise<void> {
|
||||
const deployedPackage = await deployPackage();
|
||||
@@ -16,16 +16,22 @@ export async function packageExtension(): Promise<void> {
|
||||
`${deployedPackage.name}-${deployedPackage.version}.vsix`,
|
||||
),
|
||||
"--no-dependencies",
|
||||
"--skip-license",
|
||||
];
|
||||
const proc = spawn(resolve(__dirname, "../node_modules/.bin/vsce"), args, {
|
||||
cwd: deployedPackage.distPath,
|
||||
});
|
||||
proc.childProcess.stdout!.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
proc.childProcess.stderr!.on("data", (data) => {
|
||||
console.error(data.toString());
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
});
|
||||
|
||||
await proc;
|
||||
await new Promise((resolve, reject) => {
|
||||
proc.on("error", reject);
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(undefined);
|
||||
} else {
|
||||
reject(new Error(`Failed to package extension with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
4631
extensions/ql-vscode/package-lock.json
generated
4631
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.16.1",
|
||||
"version": "1.17.1",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.90.0",
|
||||
"node": "^20.16.0",
|
||||
"node": "^20.18.1",
|
||||
"npm": ">=7.20.6"
|
||||
},
|
||||
"categories": [
|
||||
@@ -42,13 +42,6 @@
|
||||
"workspaceContains:.git"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
"files": [
|
||||
"gen/*.js",
|
||||
"media/**",
|
||||
"out/**",
|
||||
"package.json",
|
||||
"language-configuration.json"
|
||||
],
|
||||
"contributes": {
|
||||
"configurationDefaults": {
|
||||
"[ql]": {
|
||||
@@ -302,8 +295,8 @@
|
||||
"properties": {
|
||||
"codeQL.queryHistory.format": {
|
||||
"type": "string",
|
||||
"default": "%q on %d - %s %r [%t]",
|
||||
"markdownDescription": "Default string for how to label query history items.\n* %t is the time of the query\n* %q is the human-readable query name\n* %f is the query file name\n* %d is the database name\n* %r is the number of results\n* %s is a status string"
|
||||
"default": "${queryName} on ${databaseName} - ${status} ${resultCount} [${startTime}]",
|
||||
"markdownDescription": "Default string for how to label query history items.\n\nThe following variables are supported:\n* **${startTime}** - the time of the query\n* **${queryName}** - the human-readable query name\n* **${queryFileBasename}** - the query file's base name\n* **${queryLanguage}** - the query language\n* **${databaseName}** - the database name\n* **${resultCount}** - the number of results\n* **${status}** - a status string"
|
||||
},
|
||||
"codeQL.queryHistory.ttl": {
|
||||
"type": "number",
|
||||
@@ -497,16 +490,6 @@
|
||||
"title": "Telemetry",
|
||||
"order": 11,
|
||||
"properties": {
|
||||
"codeQL.telemetry.enableTelemetry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"scope": "application",
|
||||
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the one of the global telemetry settings (`#telemetry.enableTelemetry#` or `#telemetry.telemetryLevel#`) must be enabled for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)",
|
||||
"tags": [
|
||||
"telemetry",
|
||||
"usesOnlineServices"
|
||||
]
|
||||
},
|
||||
"codeQL.telemetry.logTelemetry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@@ -839,6 +822,10 @@
|
||||
"command": "codeQL.chooseDatabaseFolder",
|
||||
"title": "CodeQL: Choose Database from Folder"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseFoldersParent",
|
||||
"title": "CodeQL: Import All Databases Directly Contained in a Parent Folder"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseArchive",
|
||||
"title": "CodeQL: Choose Database from Archive"
|
||||
@@ -955,6 +942,10 @@
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"title": "Compare Results"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.comparePerformanceWith",
|
||||
"title": "Compare Performance"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openOnGithub",
|
||||
"title": "View Logs"
|
||||
@@ -1226,6 +1217,11 @@
|
||||
"group": "3_queryHistory@0",
|
||||
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.comparePerformanceWith",
|
||||
"group": "3_queryHistory@1",
|
||||
"when": "viewItem == rawResultsItem && config.codeQL.canary || viewItem == interpretedResultsItem && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"group": "4_queryHistory@4",
|
||||
@@ -1729,6 +1725,10 @@
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.comparePerformanceWith",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.sortByName",
|
||||
"when": "false"
|
||||
@@ -1968,7 +1968,7 @@
|
||||
"prepare": "cd ../.. && husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.24",
|
||||
"@floating-ui/react": "^0.27.0",
|
||||
"@octokit/plugin-retry": "^7.1.2",
|
||||
"@octokit/plugin-throttling": "^9.3.2",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
@@ -1977,13 +1977,12 @@
|
||||
"@vscode/debugprotocol": "^1.68.0",
|
||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||
"ajv": "^8.11.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"d3": "^7.9.0",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"msw": "^2.2.13",
|
||||
"msw": "^2.6.8",
|
||||
"nanoid": "^5.0.7",
|
||||
"p-queue": "^8.0.1",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
@@ -1993,7 +1992,7 @@
|
||||
"source-map": "^0.7.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"stream-json": "^1.7.3",
|
||||
"styled-components": "^6.1.9",
|
||||
"styled-components": "^6.1.13",
|
||||
"tmp": "^0.2.1",
|
||||
"tmp-promise": "^3.0.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -2005,32 +2004,32 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.6",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.24.7",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/preset-react": "^7.25.9",
|
||||
"@babel/preset-typescript": "^7.21.4",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@faker-js/faker": "^9.0.3",
|
||||
"@github/markdownlint-github": "^0.6.3",
|
||||
"@microsoft/eslint-formatter-sarif": "^3.1.0",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@storybook/addon-a11y": "^8.4.0",
|
||||
"@storybook/addon-actions": "^8.4.0",
|
||||
"@storybook/addon-essentials": "^8.4.0",
|
||||
"@storybook/addon-interactions": "^8.4.0",
|
||||
"@storybook/addon-links": "^8.4.0",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@storybook/addon-a11y": "^8.5.0",
|
||||
"@storybook/addon-actions": "^8.5.0",
|
||||
"@storybook/addon-essentials": "^8.5.0",
|
||||
"@storybook/addon-interactions": "^8.5.0",
|
||||
"@storybook/addon-links": "^8.5.0",
|
||||
"@storybook/blocks": "^8.0.2",
|
||||
"@storybook/components": "^8.4.0",
|
||||
"@storybook/csf": "^0.1.11",
|
||||
"@storybook/icons": "^1.2.12",
|
||||
"@storybook/manager-api": "^8.4.0",
|
||||
"@storybook/react": "^8.4.0",
|
||||
"@storybook/react-vite": "^8.4.0",
|
||||
"@storybook/components": "^8.5.0",
|
||||
"@storybook/csf": "^0.1.13",
|
||||
"@storybook/icons": "^1.3.0",
|
||||
"@storybook/manager-api": "^8.5.0",
|
||||
"@storybook/react": "^8.5.0",
|
||||
"@storybook/react-vite": "^8.5.0",
|
||||
"@storybook/theming": "^8.2.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.2",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/child-process-promise": "^2.2.1",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-graphviz": "^2.6.6",
|
||||
"@types/del": "^4.0.0",
|
||||
@@ -2039,10 +2038,10 @@
|
||||
"@types/gulp-replace": "^1.1.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/js-yaml": "^4.0.6",
|
||||
"@types/node": "20.16.*",
|
||||
"@types/node": "20.17.*",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/sarif": "^2.1.2",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/stream-json": "^1.7.1",
|
||||
@@ -2052,43 +2051,44 @@
|
||||
"@types/tmp": "^0.2.6",
|
||||
"@types/vscode": "1.90.0",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.12.2",
|
||||
"@typescript-eslint/parser": "^8.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"@vscode/test-electron": "^2.3.9",
|
||||
"@vscode/vsce": "^2.24.0",
|
||||
"@vscode/vsce": "^3.2.1",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^2.9.5",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-deprecation": "^3.0.0",
|
||||
"eslint-plugin-etc": "^2.0.2",
|
||||
"eslint-plugin-github": "^5.0.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest-dom": "^5.4.0",
|
||||
"eslint-plugin-jest-dom": "^5.5.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"glob": "^11.0.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-esbuild": "^0.12.1",
|
||||
"gulp-esbuild": "^0.14.0",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "^9.1.5",
|
||||
"jest": "^29.0.3",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"jest-runner-vscode": "^3.0.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"markdownlint-cli2": "^0.13.0",
|
||||
"markdownlint-cli2-formatter-pretty": "^0.0.6",
|
||||
"lint-staged": "^15.3.0",
|
||||
"markdownlint-cli2": "^0.17.0",
|
||||
"markdownlint-cli2-formatter-pretty": "^0.0.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"storybook": "^8.4.0",
|
||||
"storybook": "^8.5.0",
|
||||
"tar-stream": "^3.1.7",
|
||||
"through2": "^4.0.2",
|
||||
"ts-jest": "^29.2.5",
|
||||
@@ -2096,7 +2096,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-unused-exports": "^10.1.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.6",
|
||||
"vite": "^6.0.1",
|
||||
"vite-node": "^2.0.5"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { join, resolve } from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { outputFile, readFile, readJSON } from "fs-extra";
|
||||
import { outputFile, readJSON } from "fs-extra";
|
||||
import { getVersionInformation } from "./util/vscode-versions";
|
||||
import { fetchJson } from "./util/fetch";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
const extensionDirectory = resolve(__dirname, "..");
|
||||
|
||||
@@ -10,6 +11,29 @@ interface Release {
|
||||
tag_name: string;
|
||||
}
|
||||
|
||||
interface NpmViewError {
|
||||
error: {
|
||||
code: string;
|
||||
summary: string;
|
||||
detail: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ExecError extends Error {
|
||||
status: number;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
function isExecError(e: unknown): e is ExecError {
|
||||
return (
|
||||
e instanceof Error &&
|
||||
"status" in e &&
|
||||
typeof e.status === "number" &&
|
||||
"stdout" in e &&
|
||||
typeof e.stdout === "string"
|
||||
);
|
||||
}
|
||||
|
||||
async function updateNodeVersion() {
|
||||
const latestVsCodeRelease = await fetchJson<Release>(
|
||||
"https://api.github.com/repos/microsoft/vscode/releases/latest",
|
||||
@@ -23,19 +47,7 @@ async function updateNodeVersion() {
|
||||
`VS Code ${versionInformation.vscodeVersion} uses Electron ${versionInformation.electronVersion} and Node ${versionInformation.nodeVersion}`,
|
||||
);
|
||||
|
||||
let currentNodeVersion = (
|
||||
await readFile(join(extensionDirectory, ".nvmrc"), "utf8")
|
||||
).trim();
|
||||
if (currentNodeVersion.startsWith("v")) {
|
||||
currentNodeVersion = currentNodeVersion.slice(1);
|
||||
}
|
||||
|
||||
if (currentNodeVersion === versionInformation.nodeVersion) {
|
||||
console.log("Node version is already up to date");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Node version needs to be updated, updating now");
|
||||
console.log("Updating files related to the Node version");
|
||||
|
||||
await outputFile(
|
||||
join(extensionDirectory, ".nvmrc"),
|
||||
@@ -49,6 +61,8 @@ async function updateNodeVersion() {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const nodeVersion = new SemVer(versionInformation.nodeVersion);
|
||||
|
||||
// The @types/node version needs to match the first two parts of the Node
|
||||
// version, e.g. if the Node version is 18.17.3, the @types/node version
|
||||
// should be 18.17.*. This corresponds with the documentation at
|
||||
@@ -56,13 +70,56 @@ async function updateNodeVersion() {
|
||||
// "The patch version of the type declaration package is unrelated to the library patch version. This allows
|
||||
// Definitely Typed to safely update type declarations for the same major/minor version of a library."
|
||||
// 18.17.* is equivalent to >=18.17.0 <18.18.0
|
||||
const typesNodeVersion = versionInformation.nodeVersion
|
||||
.split(".")
|
||||
.slice(0, 2)
|
||||
.join(".");
|
||||
// In some cases, the @types/node version matching the exact Node version may not exist, in which case we'll try
|
||||
// the next lower minor version, and so on, until we find a version that exists.
|
||||
const typesNodeSemver = new SemVer(nodeVersion);
|
||||
typesNodeSemver.patch = 0;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const typesNodeVersion = `${typesNodeSemver.major}.${typesNodeSemver.minor}.*`;
|
||||
|
||||
try {
|
||||
// Check that this version actually exists
|
||||
console.log(`Checking if @types/node@${typesNodeVersion} exists`);
|
||||
|
||||
execSync(`npm view --json "@types/node@${typesNodeVersion}"`, {
|
||||
encoding: "utf-8",
|
||||
stdio: "pipe",
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
console.log(`@types/node@${typesNodeVersion} exists`);
|
||||
|
||||
// If it exists, we can break out of this loop
|
||||
break;
|
||||
} catch (e: unknown) {
|
||||
if (!isExecError(e)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
const error = JSON.parse(e.stdout) as NpmViewError;
|
||||
if (error.error.code !== "E404") {
|
||||
throw new Error(error.error.detail);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`@types/node package doesn't exist for ${typesNodeVersion}, trying a lower version (${error.error.summary})`,
|
||||
);
|
||||
|
||||
// This means the version doesn't exist, so we'll try decrementing the minor version
|
||||
typesNodeSemver.minor -= 1;
|
||||
if (typesNodeSemver.minor < 0) {
|
||||
throw new Error(
|
||||
`Could not find a suitable @types/node version for Node ${nodeVersion.format()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packageJson.engines.node = `^${versionInformation.nodeVersion}`;
|
||||
packageJson.devDependencies["@types/node"] = `${typesNodeVersion}.*`;
|
||||
packageJson.devDependencies["@types/node"] =
|
||||
`${typesNodeSemver.major}.${typesNodeSemver.minor}.*`;
|
||||
|
||||
await outputFile(
|
||||
join(extensionDirectory, "package.json"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EOL } from "os";
|
||||
import { spawn } from "child-process-promise";
|
||||
import { spawn } from "cross-spawn";
|
||||
import type { ChildProcessWithoutNullStreams } from "child_process";
|
||||
import { spawn as spawnChildProcess } from "child_process";
|
||||
import { readFile } from "fs-extra";
|
||||
@@ -37,6 +37,7 @@ import { LOGGING_FLAGS } from "./cli-command";
|
||||
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
|
||||
import { ExitCodeError, getCliError } from "./cli-errors";
|
||||
import { UserCancellationException } from "../common/vscode/progress";
|
||||
import type { LanguageClient } from "vscode-languageclient/node";
|
||||
|
||||
/**
|
||||
* The version of the SARIF format that we are using.
|
||||
@@ -277,6 +278,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
private readonly languageClient: LanguageClient,
|
||||
private distributionProvider: DistributionProvider,
|
||||
private cliConfig: CliConfig,
|
||||
public readonly logger: Logger,
|
||||
@@ -716,13 +718,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
|
||||
// Spawn the CodeQL process
|
||||
const codeqlPath = await this.getCodeQlPath();
|
||||
const childPromise = spawn(codeqlPath, args);
|
||||
// Avoid a runtime message about unhandled rejection.
|
||||
childPromise.catch(() => {
|
||||
/**/
|
||||
});
|
||||
|
||||
const child = childPromise.childProcess;
|
||||
const child = spawn(codeqlPath, args);
|
||||
|
||||
let cancellationRegistration: Disposable | undefined = undefined;
|
||||
try {
|
||||
@@ -735,16 +731,28 @@ export class CodeQLCliServer implements Disposable {
|
||||
}
|
||||
if (logger !== undefined) {
|
||||
// The human-readable output goes to stderr.
|
||||
void logStream(child.stderr!, logger);
|
||||
void logStream(child.stderr, logger);
|
||||
}
|
||||
|
||||
for await (const event of splitStreamAtSeparators(child.stdout!, [
|
||||
"\0",
|
||||
])) {
|
||||
for await (const event of splitStreamAtSeparators(child.stdout, ["\0"])) {
|
||||
yield event;
|
||||
}
|
||||
|
||||
await childPromise;
|
||||
await new Promise((resolve, reject) => {
|
||||
child.on("error", reject);
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(undefined);
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`${command} ${commandArgs.join(" ")} failed with code ${code}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
if (cancellationRegistration !== undefined) {
|
||||
cancellationRegistration.dispose();
|
||||
@@ -1578,11 +1586,13 @@ export class CodeQLCliServer implements Disposable {
|
||||
async packAdd(dir: string, queryLanguage: QueryLanguage) {
|
||||
const args = ["--dir", dir];
|
||||
args.push(`codeql/${queryLanguage}-all`);
|
||||
return this.runCodeQlCliCommand(
|
||||
const ret = await this.runCodeQlCliCommand(
|
||||
["pack", "add"],
|
||||
args,
|
||||
`Adding and installing ${queryLanguage} pack dependency.`,
|
||||
);
|
||||
await this.notifyPackInstalled();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1617,16 +1627,18 @@ export class CodeQLCliServer implements Disposable {
|
||||
args.push(
|
||||
// Allow prerelease packs from the ql submodule.
|
||||
"--allow-prerelease",
|
||||
// Allow the use of --additional-packs argument without issueing a warning
|
||||
// Allow the use of --additional-packs argument without issuing a warning
|
||||
"--no-strict-mode",
|
||||
...this.getAdditionalPacksArg(workspaceFolders),
|
||||
);
|
||||
}
|
||||
return this.runJsonCodeQlCliCommandWithAuthentication(
|
||||
const ret = await this.runJsonCodeQlCliCommandWithAuthentication(
|
||||
["pack", "install"],
|
||||
args,
|
||||
"Installing pack dependencies",
|
||||
);
|
||||
await this.notifyPackInstalled();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1744,6 +1756,17 @@ export class CodeQLCliServer implements Disposable {
|
||||
this._versionChangedListeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be called after a pack has been installed.
|
||||
*
|
||||
* This restarts the language client. Restarting the language client has the
|
||||
* effect of removing compilation errors in open ql/qll files that are caused
|
||||
* by the pack not having been installed previously.
|
||||
*/
|
||||
private async notifyPackInstalled() {
|
||||
await this.languageClient.restart();
|
||||
}
|
||||
|
||||
private async refreshVersion(): Promise<VersionAndFeatures> {
|
||||
const distribution = await this.distributionProvider.getDistribution();
|
||||
switch (distribution.kind) {
|
||||
@@ -1881,7 +1904,7 @@ function shouldDebugCliServer() {
|
||||
export class CliVersionConstraint {
|
||||
// The oldest version of the CLI that we support. This is used to determine
|
||||
// whether to show a warning about the CLI being too old on startup.
|
||||
public static OLDEST_SUPPORTED_CLI_VERSION = new SemVer("2.16.6");
|
||||
public static OLDEST_SUPPORTED_CLI_VERSION = new SemVer("2.18.4");
|
||||
|
||||
constructor(private readonly cli: CodeQLCliServer) {
|
||||
/**/
|
||||
|
||||
@@ -180,6 +180,7 @@ export type QueryHistoryCommands = {
|
||||
"codeQLQueryHistory.removeHistoryItemContextInline": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.renameItem": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.compareWith": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.comparePerformanceWith": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLog": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLogSummary": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLogViewer": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
|
||||
@@ -211,6 +212,7 @@ export type LanguageSelectionCommands = {
|
||||
export type LocalDatabasesCommands = {
|
||||
// Command palette commands
|
||||
"codeQL.chooseDatabaseFolder": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseFoldersParent": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseArchive": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseInternet": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseGithub": () => Promise<void>;
|
||||
@@ -348,7 +350,9 @@ export type MockGitHubApiServerCommands = {
|
||||
"codeQL.mockGitHubApiServer.startRecording": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.saveScenario": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.cancelRecording": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.loadScenario": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.loadScenario": (
|
||||
scenario?: string,
|
||||
) => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.unloadScenario": () => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
} from "./raw-result-types";
|
||||
import type { AccessPathSuggestionOptions } from "../model-editor/suggestions";
|
||||
import type { ModelEvaluationRunState } from "../model-editor/shared/model-evaluation-run-state";
|
||||
import type { PerformanceComparisonDataFromLog } from "../log-insights/performance-comparison";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -396,6 +397,17 @@ export interface SetComparisonsMessage {
|
||||
readonly message: string | undefined;
|
||||
}
|
||||
|
||||
export type ToComparePerformanceViewMessage = SetPerformanceComparisonQueries;
|
||||
|
||||
export interface SetPerformanceComparisonQueries {
|
||||
readonly t: "setPerformanceComparison";
|
||||
readonly from: PerformanceComparisonDataFromLog;
|
||||
readonly to: PerformanceComparisonDataFromLog;
|
||||
readonly comparison: boolean;
|
||||
}
|
||||
|
||||
export type FromComparePerformanceViewMessage = CommonFromViewMessages;
|
||||
|
||||
export type QueryCompareResult =
|
||||
| RawQueryCompareResult
|
||||
| InterpretedQueryCompareResult;
|
||||
|
||||
@@ -1,26 +1,61 @@
|
||||
import { readFile } from "fs-extra";
|
||||
import { stat } from "fs/promises";
|
||||
import { createReadStream } from "fs-extra";
|
||||
import type { BaseLogger } from "./logging";
|
||||
|
||||
const doubleLineBreakRegexp = /\n\r?\n/;
|
||||
|
||||
/**
|
||||
* Read a file consisting of multiple JSON objects. Each object is separated from the previous one
|
||||
* by a double newline sequence. This is basically a more human-readable form of JSONL.
|
||||
*
|
||||
* The current implementation reads the entire text of the document into memory, but in the future
|
||||
* it will stream the document to improve the performance with large documents.
|
||||
*
|
||||
* @param path The path to the file.
|
||||
* @param handler Callback to be invoked for each top-level JSON object in order.
|
||||
*/
|
||||
export async function readJsonlFile<T>(
|
||||
path: string,
|
||||
handler: (value: T) => Promise<void>,
|
||||
logger?: BaseLogger,
|
||||
): Promise<void> {
|
||||
const logSummary = await readFile(path, "utf-8");
|
||||
// Stream the data as large evaluator logs won't fit in memory.
|
||||
// Also avoid using 'readline' as it is slower than our manual line splitting.
|
||||
void logger?.log(
|
||||
`Parsing ${path} (${(await stat(path)).size / 1024 / 1024} MB)...`,
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = createReadStream(path, { encoding: "utf8" });
|
||||
let buffer = "";
|
||||
stream.on("data", async (chunk: string | Buffer) => {
|
||||
if (typeof chunk !== "string") {
|
||||
// This should never happen because we specify the encoding as "utf8".
|
||||
throw new Error("Invalid chunk");
|
||||
}
|
||||
|
||||
// Remove newline delimiters because summary is in .jsonl format.
|
||||
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);
|
||||
|
||||
for (const obj of jsonSummaryObjects) {
|
||||
const jsonObj = JSON.parse(obj) as T;
|
||||
await handler(jsonObj);
|
||||
}
|
||||
const parts = (buffer + chunk).split(doubleLineBreakRegexp);
|
||||
buffer = parts.pop()!;
|
||||
if (parts.length > 0) {
|
||||
try {
|
||||
stream.pause();
|
||||
for (const part of parts) {
|
||||
await handler(JSON.parse(part));
|
||||
}
|
||||
stream.resume();
|
||||
} catch (e) {
|
||||
stream.destroy();
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
stream.on("end", async () => {
|
||||
try {
|
||||
if (buffer.trim().length > 0) {
|
||||
await handler(JSON.parse(buffer));
|
||||
}
|
||||
void logger?.log(`Finished parsing ${path}`);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,26 +63,33 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
public async loadScenario(): Promise<void> {
|
||||
public async loadScenario(scenario?: string): Promise<void> {
|
||||
const scenariosPath = await this.getScenariosPath();
|
||||
if (!scenariosPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scenarioNames = await this.server.getScenarioNames(scenariosPath);
|
||||
const scenarioQuickPickItems = scenarioNames.map((s) => ({ label: s }));
|
||||
const quickPickOptions = {
|
||||
placeHolder: "Select a scenario to load",
|
||||
};
|
||||
const selectedScenario = await window.showQuickPick<QuickPickItem>(
|
||||
scenarioQuickPickItems,
|
||||
quickPickOptions,
|
||||
);
|
||||
if (!selectedScenario) {
|
||||
return;
|
||||
let scenarioName = scenario;
|
||||
if (!scenarioName) {
|
||||
const scenarioNames = await this.server.getScenarioNames(scenariosPath);
|
||||
const scenarioQuickPickItems = scenarioNames.map((s) => ({ label: s }));
|
||||
const quickPickOptions = {
|
||||
placeHolder: "Select a scenario to load",
|
||||
};
|
||||
const selectedScenario = await window.showQuickPick<QuickPickItem>(
|
||||
scenarioQuickPickItems,
|
||||
quickPickOptions,
|
||||
);
|
||||
if (!selectedScenario) {
|
||||
return;
|
||||
}
|
||||
|
||||
scenarioName = selectedScenario.label;
|
||||
}
|
||||
|
||||
const scenarioName = selectedScenario.label;
|
||||
if (!this.server.isListening && this.app.mode === AppMode.Test) {
|
||||
await this.startServer();
|
||||
}
|
||||
|
||||
await this.server.loadScenario(scenarioName, scenariosPath);
|
||||
|
||||
@@ -94,12 +101,12 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
|
||||
true,
|
||||
);
|
||||
|
||||
await window.showInformationMessage(`Loaded scenario '${scenarioName}'`);
|
||||
void window.showInformationMessage(`Loaded scenario '${scenarioName}'`);
|
||||
}
|
||||
|
||||
public async unloadScenario(): Promise<void> {
|
||||
if (!this.server.isScenarioLoaded) {
|
||||
await window.showInformationMessage("No scenario currently loaded");
|
||||
void window.showInformationMessage("No scenario currently loaded");
|
||||
} else {
|
||||
await this.server.unloadScenario();
|
||||
await this.app.commands.execute(
|
||||
@@ -107,7 +114,11 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
|
||||
"codeQL.mockGitHubApiServer.scenarioLoaded",
|
||||
false,
|
||||
);
|
||||
await window.showInformationMessage("Unloaded scenario");
|
||||
void window.showInformationMessage("Unloaded scenario");
|
||||
}
|
||||
|
||||
if (this.server.isListening && this.app.mode === AppMode.Test) {
|
||||
await this.stopServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +150,7 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
|
||||
true,
|
||||
);
|
||||
|
||||
await window.showInformationMessage(
|
||||
void window.showInformationMessage(
|
||||
'Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.',
|
||||
);
|
||||
}
|
||||
@@ -221,7 +232,10 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
|
||||
return scenariosPath;
|
||||
}
|
||||
|
||||
if (this.app.mode === AppMode.Development) {
|
||||
if (
|
||||
this.app.mode === AppMode.Development ||
|
||||
this.app.mode === AppMode.Test
|
||||
) {
|
||||
const developmentScenariosPath = path.join(
|
||||
this.app.extensionPath,
|
||||
"src/common/mock-gh-api/scenarios",
|
||||
|
||||
@@ -41,6 +41,13 @@ export abstract class AbstractWebview<
|
||||
|
||||
constructor(protected readonly app: App) {}
|
||||
|
||||
public hidePanel() {
|
||||
if (this.panel !== undefined) {
|
||||
this.panel.dispose();
|
||||
this.panel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async restoreView(panel: WebviewPanel): Promise<void> {
|
||||
this.panel = panel;
|
||||
const config = await this.getPanelConfig();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { env, Uri, window } from "vscode";
|
||||
import { window } from "vscode";
|
||||
|
||||
/**
|
||||
* Opens a modal dialog for the user to make a yes/no choice.
|
||||
@@ -34,50 +34,6 @@ export async function showBinaryChoiceDialog(
|
||||
return chosenItem?.title === yesItem.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a modal dialog for the user to make a yes/no choice.
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
|
||||
* be closed even if the user does not make a choice.
|
||||
*
|
||||
* @return
|
||||
* `true` if the user clicks 'Yes',
|
||||
* `false` if the user clicks 'No' or cancels the dialog,
|
||||
* `undefined` if the dialog is closed without the user making a choice.
|
||||
*/
|
||||
export async function showBinaryChoiceWithUrlDialog(
|
||||
message: string,
|
||||
url: string,
|
||||
): Promise<boolean | undefined> {
|
||||
const urlItem = { title: "More Information", isCloseAffordance: false };
|
||||
const yesItem = { title: "Yes", isCloseAffordance: false };
|
||||
const noItem = { title: "No", isCloseAffordance: true };
|
||||
let chosenItem;
|
||||
|
||||
// Keep the dialog open as long as the user is clicking the 'more information' option.
|
||||
// To prevent an infinite loop, if the user clicks 'more information' 5 times, close the dialog and return cancelled
|
||||
let count = 0;
|
||||
do {
|
||||
chosenItem = await window.showInformationMessage(
|
||||
message,
|
||||
{ modal: true },
|
||||
urlItem,
|
||||
yesItem,
|
||||
noItem,
|
||||
);
|
||||
if (chosenItem === urlItem) {
|
||||
await env.openExternal(Uri.parse(url, true));
|
||||
}
|
||||
count++;
|
||||
} while (chosenItem === urlItem && count < 5);
|
||||
|
||||
if (!chosenItem || chosenItem.title === urlItem.title) {
|
||||
return undefined;
|
||||
}
|
||||
return chosenItem.title === yesItem.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an information message with a customisable action.
|
||||
* @param message The message to show.
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionContext,
|
||||
ConfigurationChangeEvent,
|
||||
} from "vscode";
|
||||
import { ConfigurationTarget, env } from "vscode";
|
||||
import type { Extension, ExtensionContext } from "vscode";
|
||||
import { ConfigurationTarget, env, Uri, window } from "vscode";
|
||||
import TelemetryReporter from "vscode-extension-telemetry";
|
||||
import {
|
||||
ConfigListener,
|
||||
CANARY_FEATURES,
|
||||
ENABLE_TELEMETRY,
|
||||
LOG_TELEMETRY,
|
||||
isIntegrationTestMode,
|
||||
isCanary,
|
||||
} from "../../config";
|
||||
import { ENABLE_TELEMETRY, isCanary, LOG_TELEMETRY } from "../../config";
|
||||
import type { TelemetryClient } from "applicationinsights";
|
||||
import { extLogger } from "../logging/vscode";
|
||||
import { UserCancellationException } from "./progress";
|
||||
import { showBinaryChoiceWithUrlDialog } from "./dialog";
|
||||
import type { RedactableError } from "../errors";
|
||||
import type { SemVer } from "semver";
|
||||
import type { AppTelemetry } from "../telemetry";
|
||||
import type { EnvelopeTelemetry } from "applicationinsights/out/Declarations/Contracts";
|
||||
import type { Disposable } from "../disposable-object";
|
||||
|
||||
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
|
||||
const key = "REPLACE-APP-INSIGHTS-KEY";
|
||||
@@ -55,80 +44,25 @@ const baseDataPropertiesToRemove = [
|
||||
|
||||
const NOT_SET_CLI_VERSION = "not-set";
|
||||
|
||||
export class ExtensionTelemetryListener
|
||||
extends ConfigListener
|
||||
implements AppTelemetry
|
||||
{
|
||||
private reporter?: TelemetryReporter;
|
||||
export class ExtensionTelemetryListener implements AppTelemetry, Disposable {
|
||||
private readonly reporter: TelemetryReporter;
|
||||
|
||||
private cliVersionStr = NOT_SET_CLI_VERSION;
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
private readonly version: string,
|
||||
private readonly key: string,
|
||||
private readonly ctx: ExtensionContext,
|
||||
) {
|
||||
super();
|
||||
|
||||
env.onDidChangeTelemetryEnabled(async () => {
|
||||
await this.initialize();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handles changes to relevant configuration elements. There are 2 configuration
|
||||
* ids that this function cares about:
|
||||
*
|
||||
* * `codeQL.telemetry.enableTelemetry`: If this one has changed, then we need to re-initialize
|
||||
* the reporter and the reporter may wind up being removed.
|
||||
* * `codeQL.canary`: A change here could possibly re-trigger a dialog popup.
|
||||
*
|
||||
* Note that the global telemetry setting also gate-keeps whether or not to send telemetry events
|
||||
* to Application Insights. However, this gatekeeping happens inside of the vscode-extension-telemetry
|
||||
* package. So, this does not need to be handled here.
|
||||
*
|
||||
* @param e the configuration change event
|
||||
*/
|
||||
async handleDidChangeConfiguration(
|
||||
e: ConfigurationChangeEvent,
|
||||
): Promise<void> {
|
||||
if (e.affectsConfiguration(ENABLE_TELEMETRY.qualifiedName)) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
// Re-request telemetry so that users can see the dialog again.
|
||||
// Re-request if codeQL.canary is being set to `true` and telemetry
|
||||
// is not currently enabled.
|
||||
if (
|
||||
e.affectsConfiguration(CANARY_FEATURES.qualifiedName) &&
|
||||
CANARY_FEATURES.getValue() &&
|
||||
!ENABLE_TELEMETRY.getValue()
|
||||
) {
|
||||
await this.setTelemetryRequested(false);
|
||||
await this.requestTelemetryPermission();
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.requestTelemetryPermission();
|
||||
|
||||
this.disposeReporter();
|
||||
|
||||
if (ENABLE_TELEMETRY.getValue<boolean>()) {
|
||||
this.createReporter();
|
||||
}
|
||||
}
|
||||
|
||||
private createReporter() {
|
||||
constructor(id: string, version: string, key: string) {
|
||||
// We can always initialize this and send events using it because the vscode-extension-telemetry will check
|
||||
// whether the `telemetry.telemetryLevel` setting is enabled.
|
||||
this.reporter = new TelemetryReporter(
|
||||
this.id,
|
||||
this.version,
|
||||
this.key,
|
||||
id,
|
||||
version,
|
||||
key,
|
||||
/* anonymize stack traces */ true,
|
||||
);
|
||||
this.push(this.reporter);
|
||||
|
||||
this.addTelemetryProcessor();
|
||||
}
|
||||
|
||||
private addTelemetryProcessor() {
|
||||
// The appInsightsClient field is private but we want to access it anyway
|
||||
const client = this.reporter["appInsightsClient"] as TelemetryClient;
|
||||
if (client) {
|
||||
@@ -151,14 +85,10 @@ export class ExtensionTelemetryListener
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
void this.reporter?.dispose();
|
||||
void this.reporter.dispose();
|
||||
}
|
||||
|
||||
sendCommandUsage(name: string, executionTime: number, error?: Error): void {
|
||||
if (!this.reporter) {
|
||||
return;
|
||||
}
|
||||
const status = !error
|
||||
? CommandCompletion.Success
|
||||
: error instanceof UserCancellationException
|
||||
@@ -178,10 +108,6 @@ export class ExtensionTelemetryListener
|
||||
}
|
||||
|
||||
sendUIInteraction(name: string): void {
|
||||
if (!this.reporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reporter.sendTelemetryEvent(
|
||||
"ui-interaction",
|
||||
{
|
||||
@@ -197,10 +123,6 @@ export class ExtensionTelemetryListener
|
||||
error: RedactableError,
|
||||
extraProperties?: { [key: string]: string },
|
||||
): void {
|
||||
if (!this.reporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const properties: { [key: string]: string } = {
|
||||
isCanary: isCanary().toString(),
|
||||
cliVersion: this.cliVersionStr,
|
||||
@@ -215,10 +137,6 @@ export class ExtensionTelemetryListener
|
||||
}
|
||||
|
||||
sendConfigInformation(config: Record<string, string>): void {
|
||||
if (!this.reporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reporter.sendTelemetryEvent(
|
||||
"config",
|
||||
{
|
||||
@@ -230,37 +148,6 @@ export class ExtensionTelemetryListener
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a popup asking the user if they want to enable telemetry
|
||||
* for this extension.
|
||||
*/
|
||||
async requestTelemetryPermission() {
|
||||
if (!this.wasTelemetryRequested()) {
|
||||
// if global telemetry is disabled, avoid showing the dialog or making any changes
|
||||
let result = undefined;
|
||||
if (
|
||||
env.isTelemetryEnabled &&
|
||||
// Avoid showing the dialog if we are in integration test mode.
|
||||
!isIntegrationTestMode()
|
||||
) {
|
||||
// Extension won't start until this completes.
|
||||
result = await showBinaryChoiceWithUrlDialog(
|
||||
"Does the CodeQL Extension by GitHub have your permission to collect usage data and metrics to help us improve CodeQL for VSCode?",
|
||||
"https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code",
|
||||
);
|
||||
}
|
||||
if (result !== undefined) {
|
||||
await Promise.all([
|
||||
this.setTelemetryRequested(true),
|
||||
ENABLE_TELEMETRY.updateValue<boolean>(
|
||||
result,
|
||||
ConfigurationTarget.Global,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing
|
||||
*/
|
||||
@@ -271,21 +158,45 @@ export class ExtensionTelemetryListener
|
||||
set cliVersion(version: SemVer | undefined) {
|
||||
this.cliVersionStr = version ? version.toString() : NOT_SET_CLI_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
private disposeReporter() {
|
||||
if (this.reporter) {
|
||||
void this.reporter.dispose();
|
||||
this.reporter = undefined;
|
||||
async function notifyTelemetryChange() {
|
||||
const continueItem = { title: "Continue", isCloseAffordance: false };
|
||||
const vsCodeTelemetryItem = {
|
||||
title: "More Information about VS Code Telemetry",
|
||||
isCloseAffordance: false,
|
||||
};
|
||||
const codeqlTelemetryItem = {
|
||||
title: "More Information about CodeQL Telemetry",
|
||||
isCloseAffordance: false,
|
||||
};
|
||||
let chosenItem;
|
||||
|
||||
do {
|
||||
chosenItem = await window.showInformationMessage(
|
||||
"The CodeQL extension now follows VS Code's telemetry settings. VS Code telemetry is currently enabled. Learn how to update your telemetry settings by clicking the links below.",
|
||||
{ modal: true },
|
||||
continueItem,
|
||||
vsCodeTelemetryItem,
|
||||
codeqlTelemetryItem,
|
||||
);
|
||||
if (chosenItem === vsCodeTelemetryItem) {
|
||||
await env.openExternal(
|
||||
Uri.parse(
|
||||
"https://code.visualstudio.com/docs/getstarted/telemetry",
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private wasTelemetryRequested(): boolean {
|
||||
return !!this.ctx.globalState.get<boolean>("telemetry-request-viewed");
|
||||
}
|
||||
|
||||
private async setTelemetryRequested(newValue: boolean): Promise<void> {
|
||||
await this.ctx.globalState.update("telemetry-request-viewed", newValue);
|
||||
}
|
||||
if (chosenItem === codeqlTelemetryItem) {
|
||||
await env.openExternal(
|
||||
Uri.parse(
|
||||
"https://docs.github.com/en/code-security/codeql-for-vs-code/using-the-advanced-functionality-of-the-codeql-for-vs-code-extension/telemetry-in-codeql-for-visual-studio-code",
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
} while (chosenItem !== continueItem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,15 +212,28 @@ export async function initializeTelemetry(
|
||||
if (telemetryListener !== undefined) {
|
||||
throw new Error("Telemetry is already initialized");
|
||||
}
|
||||
|
||||
if (ENABLE_TELEMETRY.getValue<boolean | undefined>() === false) {
|
||||
if (env.isTelemetryEnabled) {
|
||||
// Await this so that the user is notified before any telemetry is sent
|
||||
await notifyTelemetryChange();
|
||||
}
|
||||
|
||||
// Remove the deprecated telemetry setting
|
||||
ENABLE_TELEMETRY.updateValue(undefined, ConfigurationTarget.Global);
|
||||
ENABLE_TELEMETRY.updateValue(undefined, ConfigurationTarget.Workspace);
|
||||
ENABLE_TELEMETRY.updateValue(
|
||||
undefined,
|
||||
ConfigurationTarget.WorkspaceFolder,
|
||||
);
|
||||
}
|
||||
|
||||
telemetryListener = new ExtensionTelemetryListener(
|
||||
extension.id,
|
||||
extension.packageJSON.version,
|
||||
key,
|
||||
ctx,
|
||||
);
|
||||
// do not await initialization, since doing so will sometimes cause a modal popup.
|
||||
// this is a particular problem during integration tests, which will hang if a modal popup is displayed.
|
||||
void telemetryListener.initialize();
|
||||
ctx.subscriptions.push(telemetryListener);
|
||||
|
||||
return telemetryListener;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { App } from "../app";
|
||||
export type WebviewKind =
|
||||
| "results"
|
||||
| "compare"
|
||||
| "compare-performance"
|
||||
| "variant-analysis"
|
||||
| "data-flow-paths"
|
||||
| "model-editor"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { statSync } from "fs";
|
||||
import { ViewColumn } from "vscode";
|
||||
|
||||
import type { App } from "../common/app";
|
||||
import { redactableError } from "../common/errors";
|
||||
import type {
|
||||
FromComparePerformanceViewMessage,
|
||||
ToComparePerformanceViewMessage,
|
||||
} from "../common/interface-types";
|
||||
import type { Logger } from "../common/logging";
|
||||
import { showAndLogExceptionWithTelemetry } from "../common/logging";
|
||||
import { extLogger } from "../common/logging/vscode";
|
||||
import type { WebviewPanelConfig } from "../common/vscode/abstract-webview";
|
||||
import { AbstractWebview } from "../common/vscode/abstract-webview";
|
||||
import { withProgress } from "../common/vscode/progress";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import type { HistoryItemLabelProvider } from "../query-history/history-item-label-provider";
|
||||
import { PerformanceOverviewScanner } from "../log-insights/performance-comparison";
|
||||
import { scanLog } from "../log-insights/log-scanner";
|
||||
import type { ResultsView } from "../local-queries";
|
||||
|
||||
export class ComparePerformanceView extends AbstractWebview<
|
||||
ToComparePerformanceViewMessage,
|
||||
FromComparePerformanceViewMessage
|
||||
> {
|
||||
constructor(
|
||||
app: App,
|
||||
public logger: Logger,
|
||||
public labelProvider: HistoryItemLabelProvider,
|
||||
private resultsView: ResultsView,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async showResults(fromJsonLog: string, toJsonLog: string) {
|
||||
const panel = await this.getPanel();
|
||||
panel.reveal(undefined, false);
|
||||
|
||||
// Close the results viewer as it will have opened when the user clicked the query in the history view
|
||||
// (which they must do as part of the UI interaction for opening the performance view).
|
||||
// The performance view generally needs a lot of width so it's annoying to have the result viewer open.
|
||||
this.resultsView.hidePanel();
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
|
||||
function scanLogWithProgress(log: string, logDescription: string) {
|
||||
const bytes = statSync(log).size;
|
||||
return withProgress(
|
||||
async (progress) =>
|
||||
scanLog(log, new PerformanceOverviewScanner(), progress),
|
||||
|
||||
{
|
||||
title: `Scanning evaluator log ${logDescription} (${(bytes / 1024 / 1024).toFixed(1)} MB)`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const [fromPerf, toPerf] = await Promise.all([
|
||||
fromJsonLog === ""
|
||||
? new PerformanceOverviewScanner()
|
||||
: scanLogWithProgress(fromJsonLog, "1/2"),
|
||||
scanLogWithProgress(toJsonLog, fromJsonLog === "" ? "1/1" : "2/2"),
|
||||
]);
|
||||
|
||||
await this.postMessage({
|
||||
t: "setPerformanceComparison",
|
||||
from: fromPerf.getData(),
|
||||
to: toPerf.getData(),
|
||||
comparison: fromJsonLog !== "",
|
||||
});
|
||||
}
|
||||
|
||||
protected getPanelConfig(): WebviewPanelConfig {
|
||||
return {
|
||||
viewId: "comparePerformanceView",
|
||||
title: "Compare CodeQL Performance",
|
||||
viewColumn: ViewColumn.Active,
|
||||
preserveFocus: true,
|
||||
view: "compare-performance",
|
||||
};
|
||||
}
|
||||
|
||||
protected onPanelDispose(): void {}
|
||||
|
||||
protected async onMessage(
|
||||
msg: FromComparePerformanceViewMessage,
|
||||
): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "viewLoaded":
|
||||
this.onWebViewLoaded();
|
||||
break;
|
||||
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
extLogger,
|
||||
telemetryListener,
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in performance comparison view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,8 @@ const ROOT_SETTING = new Setting("codeQL");
|
||||
const TELEMETRY_SETTING = new Setting("telemetry", ROOT_SETTING);
|
||||
|
||||
export const LOG_TELEMETRY = new Setting("logTelemetry", TELEMETRY_SETTING);
|
||||
|
||||
// Legacy setting that is no longer used, but is used for showing a message when the user upgrades.
|
||||
export const ENABLE_TELEMETRY = new Setting(
|
||||
"enableTelemetry",
|
||||
TELEMETRY_SETTING,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ThemeIcon,
|
||||
ThemeColor,
|
||||
workspace,
|
||||
FileType,
|
||||
} from "vscode";
|
||||
import { pathExists, stat, readdir, remove } from "fs-extra";
|
||||
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogInformationMessage,
|
||||
} from "../common/logging";
|
||||
import type { DatabaseFetcher } from "./database-fetcher";
|
||||
import { asError, asyncFilter, getErrorMessage } from "../common/helpers-pure";
|
||||
@@ -267,6 +269,8 @@ export class DatabaseUI extends DisposableObject {
|
||||
"codeQL.getCurrentDatabase": this.handleGetCurrentDatabase.bind(this),
|
||||
"codeQL.chooseDatabaseFolder":
|
||||
this.handleChooseDatabaseFolderFromPalette.bind(this),
|
||||
"codeQL.chooseDatabaseFoldersParent":
|
||||
this.handleChooseDatabaseFoldersParentFromPalette.bind(this),
|
||||
"codeQL.chooseDatabaseArchive":
|
||||
this.handleChooseDatabaseArchiveFromPalette.bind(this),
|
||||
"codeQL.chooseDatabaseInternet":
|
||||
@@ -359,6 +363,12 @@ export class DatabaseUI extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChooseDatabaseFoldersParentFromPalette(): Promise<void> {
|
||||
return withProgress(async (progress) => {
|
||||
await this.chooseDatabasesParentFolder(progress);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleSetDefaultTourDatabase(): Promise<void> {
|
||||
return withProgress(
|
||||
async () => {
|
||||
@@ -956,6 +966,32 @@ export class DatabaseUI extends DisposableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import database from uri. Returns the imported database, or `undefined` if the
|
||||
* operation was unsuccessful or canceled.
|
||||
*/
|
||||
private async importDatabase(
|
||||
uri: Uri,
|
||||
byFolder: boolean,
|
||||
progress: ProgressCallback,
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
if (byFolder && !uri.fsPath.endsWith(".testproj")) {
|
||||
const fixedUri = await this.fixDbUri(uri);
|
||||
// we are selecting a database folder
|
||||
return await this.databaseManager.openDatabase(fixedUri, {
|
||||
type: "folder",
|
||||
});
|
||||
} else {
|
||||
// we are selecting a database archive or a .testproj.
|
||||
// Unzip archives (if an archive) and copy into a workspace-controlled area
|
||||
// before importing.
|
||||
return await this.databaseFetcher.importLocalDatabase(
|
||||
uri.toString(true),
|
||||
progress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user for a database directory. Returns the chosen database, or `undefined` if the
|
||||
* operation was canceled.
|
||||
@@ -969,21 +1005,89 @@ export class DatabaseUI extends DisposableObject {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (byFolder && !uri.fsPath.endsWith("testproj")) {
|
||||
const fixedUri = await this.fixDbUri(uri);
|
||||
// we are selecting a database folder
|
||||
return await this.databaseManager.openDatabase(fixedUri, {
|
||||
type: "folder",
|
||||
return await this.importDatabase(uri, byFolder, progress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user for a parent directory that contains all databases.
|
||||
* Returns all valid databases, or `undefined` if the operation was canceled.
|
||||
*/
|
||||
private async chooseDatabasesParentFolder(
|
||||
progress: ProgressCallback,
|
||||
): Promise<DatabaseItem[] | undefined> {
|
||||
const uri = await chooseDatabaseDir(true);
|
||||
if (!uri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const databases: DatabaseItem[] = [];
|
||||
const failures: string[] = [];
|
||||
const entries = await workspace.fs.readDirectory(uri);
|
||||
const validFileTypes = [FileType.File, FileType.Directory];
|
||||
|
||||
for (const [index, entry] of entries.entries()) {
|
||||
progress({
|
||||
step: index + 1,
|
||||
maxStep: entries.length,
|
||||
message: `Importing '${entry[0]}'`,
|
||||
});
|
||||
|
||||
const subProgress: ProgressCallback = (p) => {
|
||||
progress({
|
||||
step: index + 1,
|
||||
maxStep: entries.length,
|
||||
message: `Importing '${entry[0]}': (${p.step}/${p.maxStep}) ${p.message}`,
|
||||
});
|
||||
};
|
||||
|
||||
if (!validFileTypes.includes(entry[1])) {
|
||||
void this.app.logger.log(
|
||||
`Skipping import for '${entry}', invalid file type: ${entry[1]}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const databaseUri = Uri.joinPath(uri, entry[0]);
|
||||
void this.app.logger.log(`Importing from ${databaseUri}`);
|
||||
|
||||
const database = await this.importDatabase(
|
||||
databaseUri,
|
||||
entry[1] === FileType.Directory,
|
||||
subProgress,
|
||||
);
|
||||
if (database) {
|
||||
databases.push(database);
|
||||
} else {
|
||||
failures.push(entry[0]);
|
||||
}
|
||||
} catch (e) {
|
||||
failures.push(`${entry[0]}: ${getErrorMessage(e)}`.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`Failed to import ${failures.length} database(s), successfully imported ${databases.length} database(s).`,
|
||||
{
|
||||
fullMessage: `Failed to import ${failures.length} database(s), successfully imported ${databases.length} database(s).\nFailed databases:\n - ${failures.join("\n - ")}`,
|
||||
},
|
||||
);
|
||||
} else if (databases.length === 0) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`No database folder to import.`,
|
||||
);
|
||||
return undefined;
|
||||
} else {
|
||||
// we are selecting a database archive or a testproj.
|
||||
// Unzip archives (if an archive) and copy into a workspace-controlled area
|
||||
// before importing.
|
||||
return await this.databaseFetcher.importLocalDatabase(
|
||||
uri.toString(true),
|
||||
progress,
|
||||
void showAndLogInformationMessage(
|
||||
this.app.logger,
|
||||
`Successfully imported ${databases.length} database(s).`,
|
||||
);
|
||||
}
|
||||
|
||||
return databases;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -135,6 +135,7 @@ import { LanguageContextStore } from "./language-context-store";
|
||||
import { LanguageSelectionPanel } from "./language-selection-panel/language-selection-panel";
|
||||
import { GitHubDatabasesModule } from "./databases/github-databases";
|
||||
import { DatabaseFetcher } from "./databases/database-fetcher";
|
||||
import { ComparePerformanceView } from "./compare-performance/compare-performance-view";
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -748,9 +749,13 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
ctx.subscriptions.push(qlConfigurationListener);
|
||||
|
||||
void extLogger.log("Initializing CodeQL language server.");
|
||||
const languageClient = createLanguageClient(qlConfigurationListener);
|
||||
|
||||
void extLogger.log("Initializing CodeQL cli server...");
|
||||
const cliServer = new CodeQLCliServer(
|
||||
app,
|
||||
languageClient,
|
||||
distributionManager,
|
||||
new CliConfigListener(),
|
||||
extLogger,
|
||||
@@ -924,6 +929,11 @@ async function activateWithInstalledDistribution(
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo,
|
||||
): Promise<void> => showResultsForComparison(compareView, from, to),
|
||||
async (
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo | undefined,
|
||||
): Promise<void> =>
|
||||
showPerformanceComparison(comparePerformanceView, from, to),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(qhm);
|
||||
@@ -949,6 +959,15 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
ctx.subscriptions.push(compareView);
|
||||
|
||||
void extLogger.log("Initializing performance comparison view.");
|
||||
const comparePerformanceView = new ComparePerformanceView(
|
||||
app,
|
||||
queryServerLogger,
|
||||
labelProvider,
|
||||
localQueryResultsView,
|
||||
);
|
||||
ctx.subscriptions.push(comparePerformanceView);
|
||||
|
||||
void extLogger.log("Initializing source archive filesystem provider.");
|
||||
archiveFilesystemProvider_activate(ctx, dbm);
|
||||
|
||||
@@ -961,9 +980,6 @@ async function activateWithInstalledDistribution(
|
||||
|
||||
ctx.subscriptions.push(tmpDirDisposal);
|
||||
|
||||
void extLogger.log("Initializing CodeQL language server.");
|
||||
const languageClient = createLanguageClient(qlConfigurationListener);
|
||||
|
||||
const localQueries = new LocalQueries(
|
||||
app,
|
||||
qs,
|
||||
@@ -1190,6 +1206,30 @@ async function showResultsForComparison(
|
||||
}
|
||||
}
|
||||
|
||||
async function showPerformanceComparison(
|
||||
view: ComparePerformanceView,
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo | undefined,
|
||||
): Promise<void> {
|
||||
let fromLog = from.evaluatorLogPaths?.jsonSummary;
|
||||
let toLog = to?.evaluatorLogPaths?.jsonSummary;
|
||||
|
||||
if (to === undefined) {
|
||||
toLog = fromLog;
|
||||
fromLog = "";
|
||||
}
|
||||
if (fromLog === undefined || toLog === undefined) {
|
||||
return extLogger.showWarningMessage(
|
||||
`Cannot compare performance as the structured logs are missing. Did they queries complete normally?`,
|
||||
);
|
||||
}
|
||||
await extLogger.log(
|
||||
`Comparing performance of ${from.getQueryName()} and ${to?.getQueryName() ?? "baseline"}`,
|
||||
);
|
||||
|
||||
await view.showResults(fromLog, toLog);
|
||||
}
|
||||
|
||||
function addUnhandledRejectionListener() {
|
||||
const handler = (error: unknown) => {
|
||||
// This listener will be triggered for errors from other extensions as
|
||||
|
||||
@@ -7,6 +7,7 @@ export class CachedOperation<S extends unknown[], U> {
|
||||
private readonly operation: (t: string, ...args: S) => Promise<U>;
|
||||
private readonly cached: Map<string, U>;
|
||||
private readonly lru: string[];
|
||||
private generation: number;
|
||||
private readonly inProgressCallbacks: Map<
|
||||
string,
|
||||
Array<[(u: U) => void, (reason?: Error) => void]>
|
||||
@@ -17,6 +18,7 @@ export class CachedOperation<S extends unknown[], U> {
|
||||
private cacheSize = 100,
|
||||
) {
|
||||
this.operation = operation;
|
||||
this.generation = 0;
|
||||
this.lru = [];
|
||||
this.inProgressCallbacks = new Map<
|
||||
string,
|
||||
@@ -46,7 +48,7 @@ export class CachedOperation<S extends unknown[], U> {
|
||||
inProgressCallback.push([resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
const origGeneration = this.generation;
|
||||
// Otherwise compute the new value, but leave a callback to allow sharing work
|
||||
const callbacks: Array<[(u: U) => void, (reason?: Error) => void]> = [];
|
||||
this.inProgressCallbacks.set(t, callbacks);
|
||||
@@ -54,6 +56,11 @@ export class CachedOperation<S extends unknown[], U> {
|
||||
const result = await this.operation(t, ...args);
|
||||
callbacks.forEach((f) => f[0](result));
|
||||
this.inProgressCallbacks.delete(t);
|
||||
if (this.generation !== origGeneration) {
|
||||
// Cache was reset in the meantime so don't trust this
|
||||
// result enough to cache it.
|
||||
return result;
|
||||
}
|
||||
if (this.lru.length > this.cacheSize) {
|
||||
const toRemove = this.lru.shift()!;
|
||||
this.cached.delete(toRemove);
|
||||
@@ -69,4 +76,11 @@ export class CachedOperation<S extends unknown[], U> {
|
||||
this.inProgressCallbacks.delete(t);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.cached.clear();
|
||||
this.lru.length = 0;
|
||||
this.generation++;
|
||||
this.inProgressCallbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ import type { App } from "../common/app";
|
||||
import type { Disposable } from "../common/disposable-object";
|
||||
import type { RawResultSet } from "../common/raw-result-types";
|
||||
import type { BqrsResultSetSchema } from "../common/bqrs-cli-types";
|
||||
import { CachedOperation } from "../language-support/contextual/cached-operation";
|
||||
|
||||
/**
|
||||
* results-view.ts
|
||||
@@ -177,6 +178,8 @@ export class ResultsView extends AbstractWebview<
|
||||
// Event listeners that should be disposed of when the view is disposed.
|
||||
private disposableEventListeners: Disposable[] = [];
|
||||
|
||||
private schemaCache: CachedOperation<[], BqrsResultSetSchema[]>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private databaseManager: DatabaseManager,
|
||||
@@ -206,6 +209,10 @@ export class ResultsView extends AbstractWebview<
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.schemaCache = new CachedOperation(
|
||||
this.getResultSetSchemasImpl.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
public getCommands(): ResultsViewCommands {
|
||||
@@ -420,6 +427,7 @@ export class ResultsView extends AbstractWebview<
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.schemaCache.reset();
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: "resultsUpdating" });
|
||||
await this._displayedQuery.completedQuery.updateSortState(
|
||||
@@ -610,6 +618,12 @@ export class ResultsView extends AbstractWebview<
|
||||
selectedTable = "",
|
||||
): Promise<BqrsResultSetSchema[]> {
|
||||
const resultsPath = completedQuery.getResultsPath(selectedTable);
|
||||
return this.schemaCache.get(resultsPath);
|
||||
}
|
||||
|
||||
private async getResultSetSchemasImpl(
|
||||
resultsPath: string,
|
||||
): Promise<BqrsResultSetSchema[]> {
|
||||
const schemas = await this.cliServer.bqrsInfo(
|
||||
resultsPath,
|
||||
PAGE_SIZE.getValue(),
|
||||
|
||||
@@ -94,19 +94,19 @@ export class LogScannerService extends DisposableObject {
|
||||
public async scanEvalLog(query: QueryHistoryInfo | undefined): Promise<void> {
|
||||
this.diagnosticCollection.clear();
|
||||
|
||||
if (
|
||||
query?.t !== "local" ||
|
||||
query.evalLogSummaryLocation === undefined ||
|
||||
query.jsonEvalLogSummaryLocation === undefined
|
||||
) {
|
||||
if (query?.t !== "local" || query.evaluatorLogPaths === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diagnostics = await this.scanLog(
|
||||
query.jsonEvalLogSummaryLocation,
|
||||
query.evalLogSummarySymbolsLocation,
|
||||
);
|
||||
const uri = Uri.file(query.evalLogSummaryLocation);
|
||||
const { summarySymbols, jsonSummary, humanReadableSummary } =
|
||||
query.evaluatorLogPaths;
|
||||
|
||||
if (jsonSummary === undefined || humanReadableSummary === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diagnostics = await this.scanLog(jsonSummary, summarySymbols);
|
||||
const uri = Uri.file(humanReadableSummary);
|
||||
this.diagnosticCollection.set(uri, diagnostics);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SummaryEvent } from "./log-summary";
|
||||
import { readJsonlFile } from "../common/jsonl-reader";
|
||||
import type { Disposable } from "../common/disposable-object";
|
||||
import { readJsonlFile } from "../common/jsonl-reader";
|
||||
import type { ProgressCallback } from "../common/vscode/progress";
|
||||
import type { SummaryEvent } from "./log-summary";
|
||||
|
||||
/**
|
||||
* Callback interface used to report diagnostics from a log scanner.
|
||||
@@ -112,3 +113,27 @@ export class EvaluationLogScannerSet {
|
||||
scanners.forEach((scanner) => scanner.onDone());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the evaluator summary log using the given scanner. For convenience, returns the scanner.
|
||||
*
|
||||
* @param jsonSummaryLocation The file path of the JSON summary log.
|
||||
* @param scanner The scanner to process events from the log
|
||||
*/
|
||||
export async function scanLog<T extends EvaluationLogScanner>(
|
||||
jsonSummaryLocation: string,
|
||||
scanner: T,
|
||||
progress?: ProgressCallback,
|
||||
): Promise<T> {
|
||||
progress?.({
|
||||
// all scans have step 1 - the backing progress tracker allows increments instead of steps - but for now we are happy with a tiny UI that says what is happening
|
||||
message: `Scanning ...`,
|
||||
step: 1,
|
||||
maxStep: 2,
|
||||
});
|
||||
await readJsonlFile<SummaryEvent>(jsonSummaryLocation, async (obj) => {
|
||||
scanner.onEvent(obj);
|
||||
});
|
||||
scanner.onDone();
|
||||
return scanner;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ResultEventBase extends SummaryEventBase {
|
||||
export interface ComputeSimple extends ResultEventBase {
|
||||
evaluationStrategy: "COMPUTE_SIMPLE";
|
||||
ra: Ra;
|
||||
millis: number;
|
||||
pipelineRuns?: [PipelineRun];
|
||||
queryCausingWork?: string;
|
||||
dependencies: { [key: string]: string };
|
||||
@@ -42,6 +43,7 @@ export interface ComputeRecursive extends ResultEventBase {
|
||||
evaluationStrategy: "COMPUTE_RECURSIVE";
|
||||
deltaSizes: number[];
|
||||
ra: Ra;
|
||||
millis: number;
|
||||
pipelineRuns: PipelineRun[];
|
||||
queryCausingWork?: string;
|
||||
dependencies: { [key: string]: string };
|
||||
|
||||
183
extensions/ql-vscode/src/log-insights/performance-comparison.ts
Normal file
183
extensions/ql-vscode/src/log-insights/performance-comparison.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import type { EvaluationLogScanner } from "./log-scanner";
|
||||
import type { SummaryEvent } from "./log-summary";
|
||||
|
||||
export interface PipelineSummary {
|
||||
steps: string[];
|
||||
/** Total counts for each step in the RA array, across all iterations */
|
||||
counts: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data extracted from a log for the purpose of doing a performance comparison.
|
||||
*
|
||||
* Memory compactness is important since we keep this data in memory; once for
|
||||
* each side of the comparison.
|
||||
*
|
||||
* This object must be able to survive a `postMessage` transfer from the extension host
|
||||
* to a web view (which rules out `Map` values, for example).
|
||||
*/
|
||||
export interface PerformanceComparisonDataFromLog {
|
||||
/**
|
||||
* Names of predicates mentioned in the log.
|
||||
*
|
||||
* For compactness, details of these predicates are stored in a "struct of arrays" style.
|
||||
*
|
||||
* All fields (except those ending with `Indices`) should contain an array of the same length as `names`;
|
||||
* details of a given predicate should be stored at the same index in each of those arrays.
|
||||
*/
|
||||
names: string[];
|
||||
|
||||
/** Number of milliseconds spent evaluating the `i`th predicate from the `names` array. */
|
||||
timeCosts: number[];
|
||||
|
||||
/** Number of tuples seen in pipelines evaluating the `i`th predicate from the `names` array. */
|
||||
tupleCosts: number[];
|
||||
|
||||
/** Number of iterations seen when evaluating the `i`th predicate from the `names` array. */
|
||||
iterationCounts: number[];
|
||||
|
||||
/** Number of executions of pipelines evaluating the `i`th predicate from the `names` array. */
|
||||
evaluationCounts: number[];
|
||||
|
||||
/**
|
||||
* List of indices into the `names` array for which we have seen a cache hit.
|
||||
*/
|
||||
cacheHitIndices: number[];
|
||||
|
||||
/**
|
||||
* List of indices into the `names` array where the predicate was deemed empty due to a sentinel check.
|
||||
*/
|
||||
sentinelEmptyIndices: number[];
|
||||
|
||||
/**
|
||||
* All the pipeline runs seen for the `i`th predicate from the `names` array.
|
||||
*/
|
||||
pipelineSummaryList: Array<Record<string, PipelineSummary>>;
|
||||
}
|
||||
|
||||
export class PerformanceOverviewScanner implements EvaluationLogScanner {
|
||||
private readonly nameToIndex = new Map<string, number>();
|
||||
private readonly data: PerformanceComparisonDataFromLog = {
|
||||
names: [],
|
||||
timeCosts: [],
|
||||
tupleCosts: [],
|
||||
cacheHitIndices: [],
|
||||
sentinelEmptyIndices: [],
|
||||
pipelineSummaryList: [],
|
||||
evaluationCounts: [],
|
||||
iterationCounts: [],
|
||||
};
|
||||
|
||||
private getPredicateIndex(name: string): number {
|
||||
const { nameToIndex } = this;
|
||||
let index = nameToIndex.get(name);
|
||||
if (index === undefined) {
|
||||
index = nameToIndex.size;
|
||||
nameToIndex.set(name, index);
|
||||
const {
|
||||
names,
|
||||
timeCosts,
|
||||
tupleCosts,
|
||||
iterationCounts,
|
||||
evaluationCounts,
|
||||
pipelineSummaryList,
|
||||
} = this.data;
|
||||
names.push(name);
|
||||
timeCosts.push(0);
|
||||
tupleCosts.push(0);
|
||||
iterationCounts.push(0);
|
||||
evaluationCounts.push(0);
|
||||
pipelineSummaryList.push({});
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
getData(): PerformanceComparisonDataFromLog {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
onEvent(event: SummaryEvent): void {
|
||||
if (
|
||||
event.completionType !== undefined &&
|
||||
event.completionType !== "SUCCESS"
|
||||
) {
|
||||
return; // Skip any evaluation that wasn't successful
|
||||
}
|
||||
|
||||
switch (event.evaluationStrategy) {
|
||||
case "EXTENSIONAL":
|
||||
case "COMPUTED_EXTENSIONAL": {
|
||||
break;
|
||||
}
|
||||
case "CACHE_HIT":
|
||||
case "CACHACA": {
|
||||
// Record a cache hit, but only if the predicate has not been seen before.
|
||||
// We're mainly interested in the reuse of caches from an earlier query run as they can distort comparisons.
|
||||
if (!this.nameToIndex.has(event.predicateName)) {
|
||||
this.data.cacheHitIndices.push(
|
||||
this.getPredicateIndex(event.predicateName),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "SENTINEL_EMPTY": {
|
||||
this.data.sentinelEmptyIndices.push(
|
||||
this.getPredicateIndex(event.predicateName),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "COMPUTE_RECURSIVE":
|
||||
case "COMPUTE_SIMPLE":
|
||||
case "IN_LAYER": {
|
||||
const index = this.getPredicateIndex(event.predicateName);
|
||||
let totalTime = 0;
|
||||
let totalTuples = 0;
|
||||
if (event.evaluationStrategy !== "IN_LAYER") {
|
||||
totalTime += event.millis;
|
||||
} else {
|
||||
// IN_LAYER events do no record of their total time.
|
||||
// Make a best-effort estimate by adding up the positive iteration times (they can be negative).
|
||||
for (const millis of event.predicateIterationMillis ?? []) {
|
||||
if (millis > 0) {
|
||||
totalTime += millis;
|
||||
}
|
||||
}
|
||||
}
|
||||
const {
|
||||
timeCosts,
|
||||
tupleCosts,
|
||||
iterationCounts,
|
||||
evaluationCounts,
|
||||
pipelineSummaryList,
|
||||
} = this.data;
|
||||
const pipelineSummaries = pipelineSummaryList[index];
|
||||
for (const { counts, raReference } of event.pipelineRuns ?? []) {
|
||||
// Get or create the pipeline summary for this RA
|
||||
const pipelineSummary = (pipelineSummaries[raReference] ??= {
|
||||
steps: event.ra[raReference],
|
||||
counts: counts.map(() => 0),
|
||||
});
|
||||
const { counts: totalTuplesPerStep } = pipelineSummary;
|
||||
for (let i = 0, length = counts.length; i < length; ++i) {
|
||||
const count = counts[i];
|
||||
if (count < 0) {
|
||||
// Empty RA lines have a tuple count of -1. Do not count them when aggregating.
|
||||
// But retain the fact that this step had a negative count for rendering purposes.
|
||||
totalTuplesPerStep[i] = count;
|
||||
continue;
|
||||
}
|
||||
totalTuples += count;
|
||||
totalTuplesPerStep[i] += count;
|
||||
}
|
||||
}
|
||||
timeCosts[index] += totalTime;
|
||||
tupleCosts[index] += totalTuples;
|
||||
iterationCounts[index] += event.pipelineRuns?.length ?? 0;
|
||||
evaluationCounts[index] += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDone(): void {}
|
||||
}
|
||||
@@ -12,17 +12,36 @@ import type { VariantAnalysisHistoryItem } from "./variant-analysis-history-item
|
||||
import { assertNever } from "../common/helpers-pure";
|
||||
import { pluralize } from "../common/word";
|
||||
import { humanizeQueryStatus } from "./query-status";
|
||||
import { substituteConfigVariables } from "../common/config-template";
|
||||
|
||||
interface InterpolateReplacements {
|
||||
t: string; // Start time
|
||||
q: string; // Query name
|
||||
d: string; // Database/repositories count
|
||||
r: string; // Result count/Empty
|
||||
s: string; // Status
|
||||
f: string; // Query file name
|
||||
l: string; // Query language
|
||||
"%": "%"; // Percent sign
|
||||
}
|
||||
type LabelVariables = {
|
||||
startTime: string;
|
||||
queryName: string;
|
||||
databaseName: string;
|
||||
resultCount: string;
|
||||
status: string;
|
||||
queryFileBasename: string;
|
||||
queryLanguage: string;
|
||||
};
|
||||
|
||||
const legacyVariableInterpolateReplacements: Record<
|
||||
keyof LabelVariables,
|
||||
string
|
||||
> = {
|
||||
startTime: "t",
|
||||
queryName: "q",
|
||||
databaseName: "d",
|
||||
resultCount: "r",
|
||||
status: "s",
|
||||
queryFileBasename: "f",
|
||||
queryLanguage: "l",
|
||||
};
|
||||
|
||||
// If any of the "legacy" variables are used, we need to use legacy interpolation.
|
||||
const legacyLabelRegex = new RegExp(
|
||||
`%([${Object.values(legacyVariableInterpolateReplacements).join("")}%])`,
|
||||
"g",
|
||||
);
|
||||
|
||||
export class HistoryItemLabelProvider {
|
||||
constructor(private config: QueryHistoryConfig) {
|
||||
@@ -30,21 +49,26 @@ export class HistoryItemLabelProvider {
|
||||
}
|
||||
|
||||
getLabel(item: QueryHistoryInfo) {
|
||||
let replacements: InterpolateReplacements;
|
||||
let variables: LabelVariables;
|
||||
switch (item.t) {
|
||||
case "local":
|
||||
replacements = this.getLocalInterpolateReplacements(item);
|
||||
variables = this.getLocalVariables(item);
|
||||
break;
|
||||
case "variant-analysis":
|
||||
replacements = this.getVariantAnalysisInterpolateReplacements(item);
|
||||
variables = this.getVariantAnalysisVariables(item);
|
||||
break;
|
||||
default:
|
||||
assertNever(item);
|
||||
}
|
||||
|
||||
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || "%q");
|
||||
const rawLabel =
|
||||
item.userSpecifiedLabel ?? (this.config.format || "${queryName}");
|
||||
|
||||
return this.interpolate(rawLabel, replacements);
|
||||
if (legacyLabelRegex.test(rawLabel)) {
|
||||
return this.legacyInterpolate(rawLabel, variables);
|
||||
}
|
||||
|
||||
return substituteConfigVariables(rawLabel, variables).replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,55 +83,60 @@ export class HistoryItemLabelProvider {
|
||||
: getRawQueryName(item);
|
||||
}
|
||||
|
||||
private interpolate(
|
||||
private legacyInterpolate(
|
||||
rawLabel: string,
|
||||
replacements: InterpolateReplacements,
|
||||
variables: LabelVariables,
|
||||
): string {
|
||||
const label = rawLabel.replace(
|
||||
/%(.)/g,
|
||||
(match, key: keyof InterpolateReplacements) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
const replacements = Object.entries(variables).reduce(
|
||||
(acc, [key, value]) => {
|
||||
acc[
|
||||
legacyVariableInterpolateReplacements[key as keyof LabelVariables]
|
||||
] = value;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
"%": "%",
|
||||
} as Record<string, string>,
|
||||
);
|
||||
|
||||
const label = rawLabel.replace(/%(.)/g, (match, key: string) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
|
||||
return label.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
private getLocalInterpolateReplacements(
|
||||
item: LocalQueryInfo,
|
||||
): InterpolateReplacements {
|
||||
private getLocalVariables(item: LocalQueryInfo): LabelVariables {
|
||||
const { resultCount = 0, message = "in progress" } =
|
||||
item.completedQuery || {};
|
||||
return {
|
||||
t: item.startTime,
|
||||
q: item.getQueryName(),
|
||||
d: item.databaseName,
|
||||
r: `(${resultCount} results)`,
|
||||
s: message,
|
||||
f: item.getQueryFileName(),
|
||||
l: this.getLanguageLabel(item),
|
||||
"%": "%",
|
||||
startTime: item.startTime,
|
||||
queryName: item.getQueryName(),
|
||||
databaseName: item.databaseName,
|
||||
resultCount: `(${resultCount} results)`,
|
||||
status: message,
|
||||
queryFileBasename: item.getQueryFileName(),
|
||||
queryLanguage: this.getLanguageLabel(item),
|
||||
};
|
||||
}
|
||||
|
||||
private getVariantAnalysisInterpolateReplacements(
|
||||
private getVariantAnalysisVariables(
|
||||
item: VariantAnalysisHistoryItem,
|
||||
): InterpolateReplacements {
|
||||
): LabelVariables {
|
||||
const resultCount = item.resultCount
|
||||
? `(${pluralize(item.resultCount, "result", "results")})`
|
||||
: "";
|
||||
return {
|
||||
t: new Date(item.variantAnalysis.executionStartTime).toLocaleString(
|
||||
env.language,
|
||||
),
|
||||
q: `${item.variantAnalysis.query.name} (${item.variantAnalysis.language})`,
|
||||
d: buildRepoLabel(item),
|
||||
r: resultCount,
|
||||
s: humanizeQueryStatus(item.status),
|
||||
f: basename(item.variantAnalysis.query.filePath),
|
||||
l: this.getLanguageLabel(item),
|
||||
"%": "%",
|
||||
startTime: new Date(
|
||||
item.variantAnalysis.executionStartTime,
|
||||
).toLocaleString(env.language),
|
||||
queryName: `${item.variantAnalysis.query.name} (${item.variantAnalysis.language})`,
|
||||
databaseName: buildRepoLabel(item),
|
||||
resultCount,
|
||||
status: humanizeQueryStatus(item.status),
|
||||
queryFileBasename: basename(item.variantAnalysis.query.filePath),
|
||||
queryLanguage: this.getLanguageLabel(item),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +149,10 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo,
|
||||
) => Promise<void>,
|
||||
private readonly doComparePerformanceCallback: (
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo | undefined,
|
||||
) => Promise<void>,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -263,6 +267,8 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
"query",
|
||||
),
|
||||
"codeQLQueryHistory.compareWith": this.handleCompareWith.bind(this),
|
||||
"codeQLQueryHistory.comparePerformanceWith":
|
||||
this.handleComparePerformanceWith.bind(this),
|
||||
"codeQLQueryHistory.showEvalLog": createSingleSelectionCommand(
|
||||
this.app.logger,
|
||||
this.handleShowEvalLog.bind(this),
|
||||
@@ -679,6 +685,39 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
}
|
||||
|
||||
async handleComparePerformanceWith(
|
||||
singleItem: QueryHistoryInfo,
|
||||
multiSelect: QueryHistoryInfo[] | undefined,
|
||||
) {
|
||||
multiSelect ||= [singleItem];
|
||||
|
||||
if (
|
||||
!this.isSuccessfulCompletedLocalQueryInfo(singleItem) ||
|
||||
!multiSelect.every(this.isSuccessfulCompletedLocalQueryInfo)
|
||||
) {
|
||||
throw new Error(
|
||||
"Please only select local queries that have completed successfully.",
|
||||
);
|
||||
}
|
||||
|
||||
const fromItem = this.getFromQueryToCompare(singleItem, multiSelect);
|
||||
|
||||
let toItem: CompletedLocalQueryInfo | undefined = undefined;
|
||||
try {
|
||||
toItem = await this.findOtherQueryToComparePerformance(
|
||||
fromItem,
|
||||
multiSelect,
|
||||
);
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`Failed to compare queries: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.doComparePerformanceCallback(fromItem, toItem);
|
||||
}
|
||||
|
||||
async handleItemClicked(item: QueryHistoryInfo) {
|
||||
this.treeDataProvider.setCurrentItem(item);
|
||||
|
||||
@@ -781,7 +820,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
|
||||
private async warnNoEvalLogSummary(item: LocalQueryInfo) {
|
||||
const evalLogLocation =
|
||||
item.evalLogLocation ?? item.initialInfo.outputDir?.evalLogPath;
|
||||
item.evaluatorLogPaths?.log ?? item.initialInfo.outputDir?.evalLogPath;
|
||||
|
||||
// Summary log file doesn't exist.
|
||||
if (evalLogLocation && (await pathExists(evalLogLocation))) {
|
||||
@@ -801,7 +840,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
const evalLogLocation =
|
||||
item.evalLogLocation ?? item.initialInfo.outputDir?.evalLogPath;
|
||||
item.evaluatorLogPaths?.log ?? item.initialInfo.outputDir?.evalLogPath;
|
||||
|
||||
if (evalLogLocation && (await pathExists(evalLogLocation))) {
|
||||
await tryOpenExternalFile(this.app.commands, evalLogLocation);
|
||||
@@ -816,12 +855,15 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
// If the summary file location wasn't saved, display error
|
||||
if (!item.evalLogSummaryLocation) {
|
||||
if (!item.evaluatorLogPaths?.humanReadableSummary) {
|
||||
await this.warnNoEvalLogSummary(item);
|
||||
return;
|
||||
}
|
||||
|
||||
await tryOpenExternalFile(this.app.commands, item.evalLogSummaryLocation);
|
||||
await tryOpenExternalFile(
|
||||
this.app.commands,
|
||||
item.evaluatorLogPaths.humanReadableSummary,
|
||||
);
|
||||
}
|
||||
|
||||
async handleShowEvalLogViewer(item: QueryHistoryInfo) {
|
||||
@@ -830,7 +872,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
// If the JSON summary file location wasn't saved, display error
|
||||
if (item.jsonEvalLogSummaryLocation === undefined) {
|
||||
if (item.evaluatorLogPaths?.jsonSummary === undefined) {
|
||||
await this.warnNoEvalLogSummary(item);
|
||||
return;
|
||||
}
|
||||
@@ -838,7 +880,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
// TODO(angelapwen): Stream the file in.
|
||||
try {
|
||||
const evalLogData: EvalLogData[] = await parseViewerData(
|
||||
item.jsonEvalLogSummaryLocation,
|
||||
item.evaluatorLogPaths.jsonSummary,
|
||||
);
|
||||
const evalLogTreeBuilder = new EvalLogTreeBuilder(
|
||||
item.getQueryName(),
|
||||
@@ -847,7 +889,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
this.evalLogViewer.updateRoots(await evalLogTreeBuilder.getRoots());
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Could not read evaluator log summary JSON file to generate viewer data at ${item.jsonEvalLogSummaryLocation}.`,
|
||||
`Could not read evaluator log summary JSON file to generate viewer data at ${item.evaluatorLogPaths.jsonSummary}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1073,6 +1115,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
detail: item.completedQuery.message,
|
||||
query: item,
|
||||
}));
|
||||
|
||||
if (comparableQueryLabels.length < 1) {
|
||||
throw new Error("No other queries available to compare with.");
|
||||
}
|
||||
@@ -1081,6 +1124,52 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
return choice?.query;
|
||||
}
|
||||
|
||||
private async findOtherQueryToComparePerformance(
|
||||
fromItem: CompletedLocalQueryInfo,
|
||||
allSelectedItems: CompletedLocalQueryInfo[],
|
||||
): Promise<CompletedLocalQueryInfo | undefined> {
|
||||
// If exactly 2 items are selected, return the one that
|
||||
// isn't being used as the "from" item.
|
||||
if (allSelectedItems.length === 2) {
|
||||
const otherItem =
|
||||
fromItem === allSelectedItems[0]
|
||||
? allSelectedItems[1]
|
||||
: allSelectedItems[0];
|
||||
return otherItem;
|
||||
}
|
||||
|
||||
if (allSelectedItems.length > 2) {
|
||||
throw new Error("Please select no more than 2 queries.");
|
||||
}
|
||||
|
||||
// Otherwise, present a dialog so the user can choose the item they want to use.
|
||||
const comparableQueryLabels = this.treeDataProvider.allHistory
|
||||
.filter(this.isSuccessfulCompletedLocalQueryInfo)
|
||||
.filter((otherItem) => otherItem !== fromItem)
|
||||
.map((item) => ({
|
||||
label: this.labelProvider.getLabel(item),
|
||||
description: item.databaseName,
|
||||
detail: item.completedQuery.message,
|
||||
query: item,
|
||||
}));
|
||||
const comparableQueryLabelsWithDefault = [
|
||||
{
|
||||
label: "Single run",
|
||||
description:
|
||||
"Look at the performance of this run, compared to a trivial baseline",
|
||||
detail: undefined,
|
||||
query: undefined,
|
||||
},
|
||||
...comparableQueryLabels,
|
||||
];
|
||||
if (comparableQueryLabelsWithDefault.length < 1) {
|
||||
throw new Error("No other queries available to compare with.");
|
||||
}
|
||||
const choice = await window.showQuickPick(comparableQueryLabelsWithDefault);
|
||||
|
||||
return choice?.query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the compare with source query. This ensures that all compare command invocations
|
||||
* when exactly 2 queries are selected always have the proper _from_ query. Always use
|
||||
|
||||
@@ -25,10 +25,10 @@ export function mapLocalQueryInfoToDto(
|
||||
return {
|
||||
initialInfo: mapInitialQueryInfoToDto(query.initialInfo),
|
||||
t: "local",
|
||||
evalLogLocation: query.evalLogLocation,
|
||||
evalLogSummaryLocation: query.evalLogSummaryLocation,
|
||||
jsonEvalLogSummaryLocation: query.jsonEvalLogSummaryLocation,
|
||||
evalLogSummarySymbolsLocation: query.evalLogSummarySymbolsLocation,
|
||||
evalLogLocation: query.evaluatorLogPaths?.log,
|
||||
evalLogSummaryLocation: query.evaluatorLogPaths?.humanReadableSummary,
|
||||
jsonEvalLogSummaryLocation: query.evaluatorLogPaths?.jsonSummary,
|
||||
evalLogSummarySymbolsLocation: query.evaluatorLogPaths?.summarySymbols,
|
||||
failureReason: query.failureReason,
|
||||
completedQuery:
|
||||
query.completedQuery && mapCompletedQueryToDto(query.completedQuery),
|
||||
|
||||
@@ -32,10 +32,15 @@ export function mapLocalQueryItemToDomainModel(
|
||||
localQuery.failureReason,
|
||||
localQuery.completedQuery &&
|
||||
mapCompletedQueryInfoToDomainModel(localQuery.completedQuery),
|
||||
localQuery.evalLogLocation,
|
||||
localQuery.evalLogSummaryLocation,
|
||||
localQuery.jsonEvalLogSummaryLocation,
|
||||
localQuery.evalLogSummarySymbolsLocation,
|
||||
localQuery.evalLogLocation
|
||||
? {
|
||||
log: localQuery.evalLogLocation,
|
||||
humanReadableSummary: localQuery.evalLogSummaryLocation,
|
||||
jsonSummary: localQuery.jsonEvalLogSummaryLocation,
|
||||
summarySymbols: localQuery.evalLogSummarySymbolsLocation,
|
||||
endSummary: undefined,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -200,10 +200,7 @@ export class LocalQueryInfo {
|
||||
private cancellationSource?: CancellationTokenSource, // used to cancel in progress queries
|
||||
public failureReason?: string,
|
||||
public completedQuery?: CompletedQueryInfo,
|
||||
public evalLogLocation?: string,
|
||||
public evalLogSummaryLocation?: string,
|
||||
public jsonEvalLogSummaryLocation?: string,
|
||||
public evalLogSummarySymbolsLocation?: string,
|
||||
public evaluatorLogPaths?: EvaluatorLogPaths,
|
||||
) {
|
||||
/**/
|
||||
}
|
||||
@@ -229,10 +226,7 @@ export class LocalQueryInfo {
|
||||
|
||||
/** Sets the paths to the various structured evaluator logs. */
|
||||
public setEvaluatorLogPaths(logPaths: EvaluatorLogPaths): void {
|
||||
this.evalLogLocation = logPaths.log;
|
||||
this.evalLogSummaryLocation = logPaths.humanReadableSummary;
|
||||
this.jsonEvalLogSummaryLocation = logPaths.jsonSummary;
|
||||
this.evalLogSummarySymbolsLocation = logPaths.summarySymbols;
|
||||
this.evaluatorLogPaths = logPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -244,7 +244,7 @@ export class QueryEvaluationInfo extends QueryOutputDir {
|
||||
*/
|
||||
async chooseResultSet(cliServer: CodeQLCliServer) {
|
||||
const resultSets = (
|
||||
await cliServer.bqrsInfo(this.resultsPaths.resultsPath, 0)
|
||||
await cliServer.bqrsInfo(this.resultsPaths.resultsPath)
|
||||
)["result-sets"];
|
||||
if (!resultSets.length) {
|
||||
return undefined;
|
||||
|
||||
31
extensions/ql-vscode/src/view/common/WarningBox.tsx
Normal file
31
extensions/ql-vscode/src/view/common/WarningBox.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { styled } from "styled-components";
|
||||
import { WarningIcon } from "./icon/WarningIcon";
|
||||
|
||||
const WarningBoxDiv = styled.div`
|
||||
max-width: 100em;
|
||||
padding: 0.5em 1em;
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
box-shadow: var(--vscode-widget-shadow) 0px 3px 8px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const IconPane = styled.p`
|
||||
width: 3em;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export interface WarningBoxProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function WarningBox(props: WarningBoxProps) {
|
||||
return (
|
||||
<WarningBoxDiv>
|
||||
<IconPane>
|
||||
<WarningIcon />
|
||||
</IconPane>
|
||||
<p>{props.children}</p>
|
||||
</WarningBoxDiv>
|
||||
);
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export * from "./HorizontalSpace";
|
||||
export * from "./SectionTitle";
|
||||
export * from "./VerticalSpace";
|
||||
export * from "./ViewTitle";
|
||||
export * from "./WarningBox";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Invokes the given callback when a message is received from the extension.
|
||||
*/
|
||||
export function useMessageFromExtension<T>(
|
||||
onEvent: (event: T) => void,
|
||||
onEventDependencies: unknown[],
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
onEvent(evt.data as T);
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, onEventDependencies);
|
||||
}
|
||||
@@ -0,0 +1,862 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useDeferredValue,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type {
|
||||
SetPerformanceComparisonQueries,
|
||||
ToComparePerformanceViewMessage,
|
||||
} from "../../common/interface-types";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
import type {
|
||||
PerformanceComparisonDataFromLog,
|
||||
PipelineSummary,
|
||||
} from "../../log-insights/performance-comparison";
|
||||
import { formatDecimal } from "../../common/number";
|
||||
import { styled } from "styled-components";
|
||||
import { Codicon, ViewTitle, WarningBox } from "../common";
|
||||
import { abbreviateRANames, abbreviateRASteps } from "./RAPrettyPrinter";
|
||||
import { Renaming, RenamingInput } from "./RenamingInput";
|
||||
|
||||
const enum AbsentReason {
|
||||
NotSeen = "NotSeen",
|
||||
CacheHit = "CacheHit",
|
||||
Sentinel = "Sentinel",
|
||||
}
|
||||
|
||||
type Optional<T> = AbsentReason | T;
|
||||
|
||||
function isPresent<T>(x: Optional<T>): x is T {
|
||||
return typeof x !== "string";
|
||||
}
|
||||
|
||||
interface PredicateInfo {
|
||||
tuples: number;
|
||||
evaluationCount: number;
|
||||
iterationCount: number;
|
||||
timeCost: number;
|
||||
pipelines: Record<string, PipelineSummary>;
|
||||
}
|
||||
|
||||
class ComparisonDataset {
|
||||
public nameToIndex = new Map<string, number>();
|
||||
public cacheHitIndices: Set<number>;
|
||||
public sentinelEmptyIndices: Set<number>;
|
||||
|
||||
constructor(public data: PerformanceComparisonDataFromLog) {
|
||||
const { names } = data;
|
||||
const { nameToIndex } = this;
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
nameToIndex.set(names[i], i);
|
||||
}
|
||||
this.cacheHitIndices = new Set(data.cacheHitIndices);
|
||||
this.sentinelEmptyIndices = new Set(data.sentinelEmptyIndices);
|
||||
}
|
||||
|
||||
getTupleCountInfo(name: string): Optional<PredicateInfo> {
|
||||
const { data, nameToIndex, cacheHitIndices, sentinelEmptyIndices } = this;
|
||||
const index = nameToIndex.get(name);
|
||||
if (index == null) {
|
||||
return AbsentReason.NotSeen;
|
||||
}
|
||||
const tupleCost = data.tupleCosts[index];
|
||||
if (tupleCost === 0) {
|
||||
if (sentinelEmptyIndices.has(index)) {
|
||||
return AbsentReason.Sentinel;
|
||||
} else if (cacheHitIndices.has(index)) {
|
||||
return AbsentReason.CacheHit;
|
||||
}
|
||||
}
|
||||
return {
|
||||
evaluationCount: data.evaluationCounts[index],
|
||||
iterationCount: data.iterationCounts[index],
|
||||
timeCost: data.timeCosts[index],
|
||||
tuples: tupleCost,
|
||||
pipelines: data.pipelineSummaryList[index],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function renderOptionalValue(x: Optional<number>, unit?: string) {
|
||||
switch (x) {
|
||||
case AbsentReason.NotSeen:
|
||||
return <AbsentNumberCell>n/a</AbsentNumberCell>;
|
||||
case AbsentReason.CacheHit:
|
||||
return <AbsentNumberCell>cache hit</AbsentNumberCell>;
|
||||
case AbsentReason.Sentinel:
|
||||
return <AbsentNumberCell>sentinel empty</AbsentNumberCell>;
|
||||
default:
|
||||
return (
|
||||
<NumberCell>
|
||||
{formatDecimal(x)}
|
||||
{renderUnit(unit)}
|
||||
</NumberCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPredicateMetric(
|
||||
x: Optional<PredicateInfo>,
|
||||
metric: Metric,
|
||||
isPerEvaluation: boolean,
|
||||
) {
|
||||
return renderOptionalValue(
|
||||
metricGetOptional(metric, x, isPerEvaluation),
|
||||
metric.unit,
|
||||
);
|
||||
}
|
||||
|
||||
function renderDelta(x: number, unit?: string) {
|
||||
const sign = x > 0 ? "+" : "";
|
||||
return (
|
||||
<NumberCell className={x > 0 ? "bad-value" : x < 0 ? "good-value" : ""}>
|
||||
{sign}
|
||||
{formatDecimal(x)}
|
||||
{renderUnit(unit)}
|
||||
</NumberCell>
|
||||
);
|
||||
}
|
||||
|
||||
function renderUnit(unit: string | undefined) {
|
||||
return unit == null ? "" : ` ${unit}`;
|
||||
}
|
||||
|
||||
function orderBy<T>(fn: (x: T) => number | string) {
|
||||
return (x: T, y: T) => {
|
||||
const fx = fn(x);
|
||||
const fy = fn(y);
|
||||
return fx === fy ? 0 : fx < fy ? -1 : 1;
|
||||
};
|
||||
}
|
||||
|
||||
const ChevronCell = styled.td`
|
||||
width: 1em !important;
|
||||
`;
|
||||
|
||||
const NameHeader = styled.th`
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
const NumberHeader = styled.th`
|
||||
text-align: right;
|
||||
width: 10em !important;
|
||||
`;
|
||||
|
||||
const NameCell = styled.td``;
|
||||
|
||||
const NumberCell = styled.td`
|
||||
text-align: right;
|
||||
width: 10em !important;
|
||||
|
||||
&.bad-value {
|
||||
color: var(--vscode-problemsErrorIcon-foreground);
|
||||
tr.expanded & {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
&.good-value {
|
||||
color: var(--vscode-problemsInfoIcon-foreground);
|
||||
tr.expanded & {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AbsentNumberCell = styled.td`
|
||||
text-align: right;
|
||||
color: var(--vscode-disabledForeground);
|
||||
|
||||
tr.expanded & {
|
||||
color: inherit;
|
||||
}
|
||||
width: 10em !important;
|
||||
`;
|
||||
|
||||
const Table = styled.table`
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
background-color: var(--vscode-background);
|
||||
color: var(--vscode-foreground);
|
||||
& td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
& th {
|
||||
padding: 0.5em;
|
||||
}
|
||||
&.expanded {
|
||||
border: 1px solid var(--vscode-list-activeSelectionBackground);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
word-break: break-all;
|
||||
`;
|
||||
|
||||
const PredicateTR = styled.tr`
|
||||
cursor: pointer;
|
||||
|
||||
&.expanded {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
& .codicon-chevron-right {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover:not(.expanded) {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
& .codicon-chevron-right {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const PipelineStepTR = styled.tr`
|
||||
& td {
|
||||
padding-top: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
`;
|
||||
|
||||
const Dropdown = styled.select``;
|
||||
|
||||
interface PipelineStepProps {
|
||||
before: number | undefined;
|
||||
after: number | undefined;
|
||||
comparison: boolean;
|
||||
step: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Row with details of a pipeline step, or one of the high-level stats appearing above the pipelines (evaluation/iteration counts).
|
||||
*/
|
||||
function PipelineStep(props: PipelineStepProps) {
|
||||
let { before, after, comparison, step } = props;
|
||||
if (before != null && before < 0) {
|
||||
before = undefined;
|
||||
}
|
||||
if (after != null && after < 0) {
|
||||
after = undefined;
|
||||
}
|
||||
const delta = before != null && after != null ? after - before : undefined;
|
||||
return (
|
||||
<PipelineStepTR>
|
||||
<ChevronCell />
|
||||
{comparison && (
|
||||
<NumberCell>{before != null ? formatDecimal(before) : ""}</NumberCell>
|
||||
)}
|
||||
<NumberCell>{after != null ? formatDecimal(after) : ""}</NumberCell>
|
||||
{comparison && (delta != null ? renderDelta(delta) : <td></td>)}
|
||||
<NameCell>{step}</NameCell>
|
||||
</PipelineStepTR>
|
||||
);
|
||||
}
|
||||
|
||||
const HeaderTR = styled.tr`
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
`;
|
||||
|
||||
interface HeaderRowProps {
|
||||
hasBefore?: boolean;
|
||||
hasAfter?: boolean;
|
||||
comparison: boolean;
|
||||
title: React.ReactNode;
|
||||
}
|
||||
|
||||
function HeaderRow(props: HeaderRowProps) {
|
||||
const { comparison, hasBefore, hasAfter, title } = props;
|
||||
return (
|
||||
<HeaderTR>
|
||||
<ChevronCell />
|
||||
{comparison ? (
|
||||
<>
|
||||
<NumberHeader>{hasBefore ? "Before" : ""}</NumberHeader>
|
||||
<NumberHeader>{hasAfter ? "After" : ""}</NumberHeader>
|
||||
<NumberHeader>{hasBefore && hasAfter ? "Delta" : ""}</NumberHeader>
|
||||
</>
|
||||
) : (
|
||||
<NumberHeader>Value</NumberHeader>
|
||||
)}
|
||||
<NameHeader>{title}</NameHeader>
|
||||
</HeaderTR>
|
||||
);
|
||||
}
|
||||
|
||||
interface HighLevelStatsProps {
|
||||
before: Optional<PredicateInfo>;
|
||||
after: Optional<PredicateInfo>;
|
||||
comparison: boolean;
|
||||
}
|
||||
|
||||
function HighLevelStats(props: HighLevelStatsProps) {
|
||||
const { before, after, comparison } = props;
|
||||
const hasBefore = isPresent(before);
|
||||
const hasAfter = isPresent(after);
|
||||
const showEvaluationCount =
|
||||
(hasBefore && before.evaluationCount > 1) ||
|
||||
(hasAfter && after.evaluationCount > 1);
|
||||
return (
|
||||
<>
|
||||
<HeaderRow
|
||||
hasBefore={hasBefore}
|
||||
hasAfter={hasAfter}
|
||||
title="Stats"
|
||||
comparison={comparison}
|
||||
/>
|
||||
{showEvaluationCount && (
|
||||
<PipelineStep
|
||||
before={hasBefore ? before.evaluationCount : undefined}
|
||||
after={hasAfter ? after.evaluationCount : undefined}
|
||||
comparison={comparison}
|
||||
step="Number of evaluations"
|
||||
/>
|
||||
)}
|
||||
<PipelineStep
|
||||
before={
|
||||
hasBefore ? before.iterationCount / before.evaluationCount : undefined
|
||||
}
|
||||
after={
|
||||
hasAfter ? after.iterationCount / after.evaluationCount : undefined
|
||||
}
|
||||
comparison={comparison}
|
||||
step={
|
||||
showEvaluationCount
|
||||
? "Number of iterations per evaluation"
|
||||
: "Number of iterations"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface Row {
|
||||
name: string;
|
||||
before: Optional<PredicateInfo>;
|
||||
after: Optional<PredicateInfo>;
|
||||
diff: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of predicates that have been grouped together because their names have the same fingerprint.
|
||||
*/
|
||||
interface RowGroup {
|
||||
name: string;
|
||||
rows: Row[];
|
||||
before: Optional<number>;
|
||||
after: Optional<number>;
|
||||
diff: number;
|
||||
}
|
||||
|
||||
function getSortOrder(sortOrder: "delta" | "absDelta") {
|
||||
if (sortOrder === "absDelta") {
|
||||
return orderBy((row: { diff: number }) => -Math.abs(row.diff));
|
||||
}
|
||||
return orderBy((row: { diff: number }) => row.diff);
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
title: string;
|
||||
get(info: PredicateInfo): number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const metrics: Record<string, Metric> = {
|
||||
tuples: {
|
||||
title: "Tuple count",
|
||||
get: (info) => info.tuples,
|
||||
},
|
||||
time: {
|
||||
title: "Time spent",
|
||||
get: (info) => info.timeCost,
|
||||
unit: "ms",
|
||||
},
|
||||
evaluations: {
|
||||
title: "Evaluations",
|
||||
get: (info) => info.evaluationCount,
|
||||
},
|
||||
iterationsTotal: {
|
||||
title: "Iterations",
|
||||
get: (info) => info.iterationCount,
|
||||
},
|
||||
};
|
||||
|
||||
function metricGetOptional(
|
||||
metric: Metric,
|
||||
info: Optional<PredicateInfo>,
|
||||
isPerEvaluation: boolean,
|
||||
): Optional<number> {
|
||||
if (!isPresent(info)) {
|
||||
return info;
|
||||
}
|
||||
const value = metric.get(info);
|
||||
return isPerEvaluation ? (value / info.evaluationCount) | 0 : value;
|
||||
}
|
||||
|
||||
function addOptionals(a: Optional<number>, b: Optional<number>) {
|
||||
if (isPresent(a) && isPresent(b)) {
|
||||
return a + b;
|
||||
}
|
||||
if (isPresent(a)) {
|
||||
return a;
|
||||
}
|
||||
if (isPresent(b)) {
|
||||
return b;
|
||||
}
|
||||
if (a === b) {
|
||||
return a; // If absent for the same reason, preserve that reason
|
||||
}
|
||||
return 0; // Otherwise collapse to zero
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "fingerprint" from the given name, which is used to group together similar names.
|
||||
*/
|
||||
function getNameFingerprint(name: string, renamings: Renaming[]) {
|
||||
for (const { patternRegexp, replacement } of renamings) {
|
||||
if (patternRegexp != null) {
|
||||
name = name.replace(patternRegexp, replacement);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function Chevron({ expanded }: { expanded: boolean }) {
|
||||
return <Codicon name={expanded ? "chevron-down" : "chevron-right"} />;
|
||||
}
|
||||
|
||||
function union<T>(a: Set<T> | T[], b: Set<T> | T[]) {
|
||||
const result = new Set(a);
|
||||
for (const x of b) {
|
||||
result.add(x);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function ComparePerformance(_: Record<string, never>) {
|
||||
const [data, setData] = useState<
|
||||
SetPerformanceComparisonQueries | undefined
|
||||
>();
|
||||
|
||||
useMessageFromExtension<ToComparePerformanceViewMessage>(
|
||||
(msg) => {
|
||||
setData(msg);
|
||||
},
|
||||
[setData],
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading performance comparison...</div>;
|
||||
}
|
||||
|
||||
return <ComparePerformanceWithData data={data} />;
|
||||
}
|
||||
|
||||
function ComparePerformanceWithData(props: {
|
||||
data: SetPerformanceComparisonQueries;
|
||||
}) {
|
||||
const { data } = props;
|
||||
|
||||
const { from, to } = useMemo(
|
||||
() => ({
|
||||
from: new ComparisonDataset(data.from),
|
||||
to: new ComparisonDataset(data.to),
|
||||
}),
|
||||
[data],
|
||||
);
|
||||
|
||||
const comparison = data?.comparison;
|
||||
|
||||
const [hideCacheHits, setHideCacheHits] = useState(false);
|
||||
|
||||
const [sortOrder, setSortOrder] = useState<"delta" | "absDelta">("absDelta");
|
||||
|
||||
const [metric, setMetric] = useState<Metric>(metrics.tuples);
|
||||
|
||||
const [isPerEvaluation, setPerEvaluation] = useState(false);
|
||||
|
||||
const nameSet = useMemo(
|
||||
() => union(from.data.names, to.data.names),
|
||||
[from, to],
|
||||
);
|
||||
|
||||
const hasCacheHitMismatch = useRef(false);
|
||||
|
||||
const rows: Row[] = useMemo(() => {
|
||||
hasCacheHitMismatch.current = false;
|
||||
return Array.from(nameSet)
|
||||
.map((name) => {
|
||||
const before = from.getTupleCountInfo(name);
|
||||
const after = to.getTupleCountInfo(name);
|
||||
const beforeValue = metricGetOptional(metric, before, isPerEvaluation);
|
||||
const afterValue = metricGetOptional(metric, after, isPerEvaluation);
|
||||
if (beforeValue === afterValue) {
|
||||
return undefined!;
|
||||
}
|
||||
if (
|
||||
before === AbsentReason.CacheHit ||
|
||||
after === AbsentReason.CacheHit
|
||||
) {
|
||||
hasCacheHitMismatch.current = true;
|
||||
if (hideCacheHits) {
|
||||
return undefined!;
|
||||
}
|
||||
}
|
||||
const diff =
|
||||
(isPresent(afterValue) ? afterValue : 0) -
|
||||
(isPresent(beforeValue) ? beforeValue : 0);
|
||||
return { name, before, after, diff } satisfies Row;
|
||||
})
|
||||
.filter((x) => !!x)
|
||||
.sort(getSortOrder(sortOrder));
|
||||
}, [nameSet, from, to, metric, hideCacheHits, sortOrder, isPerEvaluation]);
|
||||
|
||||
const { totalBefore, totalAfter, totalDiff } = useMemo(() => {
|
||||
let totalBefore = 0;
|
||||
let totalAfter = 0;
|
||||
let totalDiff = 0;
|
||||
for (const row of rows) {
|
||||
totalBefore += isPresent(row.before) ? metric.get(row.before) : 0;
|
||||
totalAfter += isPresent(row.after) ? metric.get(row.after) : 0;
|
||||
totalDiff += row.diff;
|
||||
}
|
||||
return { totalBefore, totalAfter, totalDiff };
|
||||
}, [rows, metric]);
|
||||
|
||||
const [renamings, setRenamings] = useState<Renaming[]>(() => [
|
||||
new Renaming("#[0-9a-f]{8}(?![0-9a-f])", "#"),
|
||||
]);
|
||||
|
||||
// Use deferred value to avoid expensive re-rendering for every keypress in the renaming editor
|
||||
const deferredRenamings = useDeferredValue(renamings);
|
||||
|
||||
const rowGroups = useMemo(() => {
|
||||
const groupedRows = new Map<string, Row[]>();
|
||||
for (const row of rows) {
|
||||
const fingerprint = getNameFingerprint(row.name, deferredRenamings);
|
||||
const rows = groupedRows.get(fingerprint);
|
||||
if (rows) {
|
||||
rows.push(row);
|
||||
} else {
|
||||
groupedRows.set(fingerprint, [row]);
|
||||
}
|
||||
}
|
||||
return Array.from(groupedRows.entries())
|
||||
.map(([fingerprint, rows]) => {
|
||||
const before = rows
|
||||
.map((row) => metricGetOptional(metric, row.before, isPerEvaluation))
|
||||
.reduce(addOptionals);
|
||||
const after = rows
|
||||
.map((row) => metricGetOptional(metric, row.after, isPerEvaluation))
|
||||
.reduce(addOptionals);
|
||||
return {
|
||||
name: rows.length === 1 ? rows[0].name : fingerprint,
|
||||
before,
|
||||
after,
|
||||
diff:
|
||||
(isPresent(after) ? after : 0) - (isPresent(before) ? before : 0),
|
||||
rows,
|
||||
} satisfies RowGroup;
|
||||
})
|
||||
.sort(getSortOrder(sortOrder));
|
||||
}, [rows, metric, sortOrder, deferredRenamings, isPerEvaluation]);
|
||||
|
||||
const rowGroupNames = useMemo(
|
||||
() => abbreviateRANames(rowGroups.map((group) => group.name)),
|
||||
[rowGroups],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewTitle>Performance comparison</ViewTitle>
|
||||
{comparison && hasCacheHitMismatch.current && (
|
||||
<WarningBox>
|
||||
<strong>Inconsistent cache hits</strong>
|
||||
<br />
|
||||
Some predicates had a cache hit on one side but not the other. For
|
||||
more accurate results, try running the{" "}
|
||||
<strong>CodeQL: Clear Cache</strong> command before each query.
|
||||
<br />
|
||||
<br />
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hideCacheHits}
|
||||
onChange={() => setHideCacheHits(!hideCacheHits)}
|
||||
/>
|
||||
Hide predicates with cache hits
|
||||
</label>
|
||||
</WarningBox>
|
||||
)}
|
||||
<RenamingInput renamings={renamings} setRenamings={setRenamings} />
|
||||
Compare{" "}
|
||||
<Dropdown
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
setMetric(metrics[e.target.value])
|
||||
}
|
||||
>
|
||||
{Object.entries(metrics).map(([key, value]) => (
|
||||
<option key={key} value={key}>
|
||||
{value.title}
|
||||
</option>
|
||||
))}
|
||||
</Dropdown>{" "}
|
||||
<Dropdown
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
setPerEvaluation(e.target.value === "per-evaluation")
|
||||
}
|
||||
>
|
||||
<option value="total">Overall</option>
|
||||
<option value="per-evaluation">Per evaluation</option>
|
||||
</Dropdown>{" "}
|
||||
{comparison && (
|
||||
<>
|
||||
sorted by{" "}
|
||||
<Dropdown
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
setSortOrder(e.target.value as "delta" | "absDelta")
|
||||
}
|
||||
value={sortOrder}
|
||||
>
|
||||
<option value="delta">Delta</option>
|
||||
<option value="absDelta">Absolute delta</option>
|
||||
</Dropdown>
|
||||
</>
|
||||
)}
|
||||
<Table>
|
||||
<thead>
|
||||
<HeaderRow comparison={comparison} title="Predicate" />
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr key="total">
|
||||
<ChevronCell />
|
||||
{comparison && renderOptionalValue(totalBefore, metric.unit)}
|
||||
{renderOptionalValue(totalAfter, metric.unit)}
|
||||
{comparison && renderDelta(totalDiff, metric.unit)}
|
||||
<NameCell>
|
||||
<strong>TOTAL</strong>
|
||||
</NameCell>
|
||||
</tr>
|
||||
<tr key="spacing">
|
||||
<td colSpan={5} style={{ height: "1em" }}></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<PredicateTable
|
||||
rowGroups={rowGroups}
|
||||
rowGroupNames={rowGroupNames}
|
||||
comparison={comparison}
|
||||
metric={metric}
|
||||
isPerEvaluation={isPerEvaluation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface PredicateTableProps {
|
||||
rowGroups: RowGroup[];
|
||||
rowGroupNames: React.ReactNode[];
|
||||
comparison: boolean;
|
||||
metric: Metric;
|
||||
isPerEvaluation: boolean;
|
||||
}
|
||||
|
||||
function PredicateTableRaw(props: PredicateTableProps) {
|
||||
const { comparison, metric, rowGroupNames, rowGroups, isPerEvaluation } =
|
||||
props;
|
||||
return rowGroups.map((rowGroup, rowGroupIndex) => (
|
||||
<PredicateRowGroup
|
||||
key={rowGroupIndex}
|
||||
renderedName={rowGroupNames[rowGroupIndex]}
|
||||
rowGroup={rowGroup}
|
||||
comparison={comparison}
|
||||
metric={metric}
|
||||
isPerEvaluation={isPerEvaluation}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
const PredicateTable = memo(PredicateTableRaw);
|
||||
|
||||
interface PredicateRowGroupProps {
|
||||
renderedName: React.ReactNode;
|
||||
rowGroup: RowGroup;
|
||||
comparison: boolean;
|
||||
metric: Metric;
|
||||
isPerEvaluation: boolean;
|
||||
}
|
||||
|
||||
function PredicateRowGroup(props: PredicateRowGroupProps) {
|
||||
const { renderedName, rowGroup, comparison, metric, isPerEvaluation } = props;
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
const rowNames = useMemo(
|
||||
() => abbreviateRANames(rowGroup.rows.map((row) => row.name)),
|
||||
[rowGroup],
|
||||
);
|
||||
if (rowGroup.rows.length === 1) {
|
||||
return <PredicateRow row={rowGroup.rows[0]} {...props} />;
|
||||
}
|
||||
return (
|
||||
<Table className={isExpanded ? "expanded" : ""}>
|
||||
<tbody>
|
||||
<PredicateTR
|
||||
className={isExpanded ? "expanded" : ""}
|
||||
key={"main"}
|
||||
onClick={() => setExpanded(!isExpanded)}
|
||||
>
|
||||
<ChevronCell>
|
||||
<Chevron expanded={isExpanded} />
|
||||
</ChevronCell>
|
||||
{comparison && renderOptionalValue(rowGroup.before)}
|
||||
{renderOptionalValue(rowGroup.after)}
|
||||
{comparison && renderDelta(rowGroup.diff, metric.unit)}
|
||||
<NameCell>
|
||||
{renderedName} ({rowGroup.rows.length} predicates)
|
||||
</NameCell>
|
||||
</PredicateTR>
|
||||
{isExpanded &&
|
||||
rowGroup.rows.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
<td colSpan={5}>
|
||||
<PredicateRow
|
||||
renderedName={rowNames[rowIndex]}
|
||||
row={row}
|
||||
comparison={comparison}
|
||||
metric={metric}
|
||||
isPerEvaluation={isPerEvaluation}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
interface PredicateRowProps {
|
||||
renderedName: React.ReactNode;
|
||||
row: Row;
|
||||
comparison: boolean;
|
||||
metric: Metric;
|
||||
isPerEvaluation: boolean;
|
||||
}
|
||||
|
||||
function PredicateRow(props: PredicateRowProps) {
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
const { renderedName, row, comparison, metric, isPerEvaluation } = props;
|
||||
const evaluationFactorBefore =
|
||||
isPerEvaluation && isPresent(row.before) ? row.before.evaluationCount : 1;
|
||||
const evaluationFactorAfter =
|
||||
isPerEvaluation && isPresent(row.after) ? row.after.evaluationCount : 1;
|
||||
return (
|
||||
<Table className={isExpanded ? "expanded" : ""}>
|
||||
<tbody>
|
||||
<PredicateTR
|
||||
className={isExpanded ? "expanded" : ""}
|
||||
key={"main"}
|
||||
onClick={() => setExpanded(!isExpanded)}
|
||||
>
|
||||
<ChevronCell>
|
||||
<Chevron expanded={isExpanded} />
|
||||
</ChevronCell>
|
||||
{comparison &&
|
||||
renderPredicateMetric(row.before, metric, isPerEvaluation)}
|
||||
{renderPredicateMetric(row.after, metric, isPerEvaluation)}
|
||||
{comparison && renderDelta(row.diff, metric.unit)}
|
||||
<NameCell>{renderedName}</NameCell>
|
||||
</PredicateTR>
|
||||
{isExpanded && (
|
||||
<>
|
||||
<HighLevelStats
|
||||
before={row.before}
|
||||
after={row.after}
|
||||
comparison={comparison}
|
||||
/>
|
||||
{collatePipelines(
|
||||
isPresent(row.before) ? row.before.pipelines : {},
|
||||
isPresent(row.after) ? row.after.pipelines : {},
|
||||
).map(({ name, first, second }, pipelineIndex) => (
|
||||
<Fragment key={pipelineIndex}>
|
||||
<HeaderRow
|
||||
hasBefore={first != null}
|
||||
hasAfter={second != null}
|
||||
comparison={comparison}
|
||||
title={
|
||||
<>
|
||||
Tuple counts for '{name}' pipeline
|
||||
{comparison &&
|
||||
(first == null
|
||||
? " (after)"
|
||||
: second == null
|
||||
? " (before)"
|
||||
: "")}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{abbreviateRASteps(first?.steps ?? second!.steps).map(
|
||||
(step, index) => (
|
||||
<PipelineStep
|
||||
key={index}
|
||||
before={
|
||||
first &&
|
||||
(first.counts[index] / evaluationFactorBefore) | 0
|
||||
}
|
||||
after={
|
||||
second &&
|
||||
(second.counts[index] / evaluationFactorAfter) | 0
|
||||
}
|
||||
comparison={comparison}
|
||||
step={step}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
interface PipelinePair {
|
||||
name: string;
|
||||
first: PipelineSummary | undefined;
|
||||
second: PipelineSummary | undefined;
|
||||
}
|
||||
|
||||
function collatePipelines(
|
||||
before: Record<string, PipelineSummary>,
|
||||
after: Record<string, PipelineSummary>,
|
||||
): PipelinePair[] {
|
||||
const result: PipelinePair[] = [];
|
||||
|
||||
for (const [name, first] of Object.entries(before)) {
|
||||
const second = after[name];
|
||||
if (second == null) {
|
||||
result.push({ name, first, second: undefined });
|
||||
} else if (samePipeline(first.steps, second.steps)) {
|
||||
result.push({ name, first, second });
|
||||
} else {
|
||||
result.push({ name, first, second: undefined });
|
||||
result.push({ name, first: undefined, second });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, second] of Object.entries(after)) {
|
||||
if (before[name] == null) {
|
||||
result.push({ name, first: undefined, second });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function samePipeline(a: string[], b: string[]) {
|
||||
return a.length === b.length && a.every((x, i) => x === b[i]);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { styled } from "styled-components";
|
||||
|
||||
/**
|
||||
* A set of names, for generating unambiguous abbreviations.
|
||||
*/
|
||||
class NameSet {
|
||||
private readonly abbreviations = new Map<string, React.ReactNode>();
|
||||
|
||||
constructor(readonly names: string[]) {
|
||||
const qnames = names.map(parseName);
|
||||
const builder = new TrieBuilder();
|
||||
qnames
|
||||
.map((qname) => builder.visitQName(qname))
|
||||
.forEach((r, index) => {
|
||||
this.abbreviations.set(names[index], r.abbreviate(true));
|
||||
});
|
||||
}
|
||||
|
||||
public getAbbreviation(name: string): React.ReactNode {
|
||||
return this.abbreviations.get(name) ?? name;
|
||||
}
|
||||
}
|
||||
|
||||
/** Name parsed into the form `prefix::name<args>` */
|
||||
interface QualifiedName {
|
||||
prefix?: QualifiedName;
|
||||
name: string;
|
||||
args?: QualifiedName[];
|
||||
}
|
||||
|
||||
function qnameToString(name: QualifiedName): string {
|
||||
const parts: string[] = [];
|
||||
if (name.prefix != null) {
|
||||
parts.push(qnameToString(name.prefix));
|
||||
parts.push("::");
|
||||
}
|
||||
parts.push(name.name);
|
||||
if (name.args != null && name.args.length > 0) {
|
||||
parts.push("<");
|
||||
parts.push(name.args.map(qnameToString).join(","));
|
||||
parts.push(">");
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function tokeniseName(text: string) {
|
||||
return Array.from(text.matchAll(/:+|<|>|,|"[^"]+"|`[^`]+`|[^:<>,"`]+/g));
|
||||
}
|
||||
|
||||
function parseName(text: string): QualifiedName {
|
||||
const tokens = tokeniseName(text);
|
||||
|
||||
function next() {
|
||||
return tokens.pop()![0];
|
||||
}
|
||||
function peek() {
|
||||
return tokens[tokens.length - 1][0];
|
||||
}
|
||||
function skipToken(token: string) {
|
||||
if (tokens.length > 0 && peek() === token) {
|
||||
tokens.pop();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function parseQName(): QualifiedName {
|
||||
// Note that the tokens stream is parsed in reverse order. This is simpler, but may look confusing initially.
|
||||
let args: QualifiedName[] | undefined;
|
||||
if (skipToken(">")) {
|
||||
args = [];
|
||||
while (tokens.length > 0 && peek() !== "<") {
|
||||
args.push(parseQName());
|
||||
skipToken(",");
|
||||
}
|
||||
args.reverse();
|
||||
skipToken("<");
|
||||
}
|
||||
const name = tokens.length === 0 ? "" : next();
|
||||
const prefix = skipToken("::") ? parseQName() : undefined;
|
||||
return {
|
||||
prefix,
|
||||
name,
|
||||
args,
|
||||
};
|
||||
}
|
||||
|
||||
const result = parseQName();
|
||||
if (tokens.length > 0) {
|
||||
// It's a parse error if we did not consume all tokens.
|
||||
// Just treat the whole text as the 'name'.
|
||||
return { prefix: undefined, name: text, args: undefined };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class TrieNode {
|
||||
children = new Map<string, TrieNode>();
|
||||
constructor(readonly index: number) {}
|
||||
}
|
||||
|
||||
interface VisitResult {
|
||||
node: TrieNode;
|
||||
abbreviate: (isRoot?: boolean) => React.ReactNode;
|
||||
}
|
||||
|
||||
class TrieBuilder {
|
||||
root = new TrieNode(0);
|
||||
nextId = 1;
|
||||
|
||||
getOrCreate(trieNode: TrieNode, child: string) {
|
||||
const { children } = trieNode;
|
||||
let node = children.get(child);
|
||||
if (node == null) {
|
||||
node = new TrieNode(this.nextId++);
|
||||
children.set(child, node);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
visitQName(qname: QualifiedName): VisitResult {
|
||||
const prefix =
|
||||
qname.prefix != null ? this.visitQName(qname.prefix) : undefined;
|
||||
const trieNodeBeforeArgs = this.getOrCreate(
|
||||
prefix?.node ?? this.root,
|
||||
qname.name,
|
||||
);
|
||||
let trieNode = trieNodeBeforeArgs;
|
||||
const args = qname.args?.map((arg) => this.visitQName(arg));
|
||||
if (args != null) {
|
||||
const argKey = args.map((arg) => arg.node.index).join(",");
|
||||
trieNode = this.getOrCreate(trieNodeBeforeArgs, argKey);
|
||||
}
|
||||
return {
|
||||
node: trieNode,
|
||||
abbreviate: (isRoot = false) => {
|
||||
const result: React.ReactNode[] = [];
|
||||
if (prefix != null) {
|
||||
result.push(prefix.abbreviate());
|
||||
result.push("::");
|
||||
}
|
||||
const { name } = qname;
|
||||
const hash = name.indexOf("#");
|
||||
if (hash !== -1 && isRoot) {
|
||||
const shortName = name.substring(0, hash);
|
||||
result.push(<IdentifierSpan>{shortName}</IdentifierSpan>);
|
||||
result.push(name.substring(hash));
|
||||
} else {
|
||||
result.push(isRoot ? <IdentifierSpan>{name}</IdentifierSpan> : name);
|
||||
}
|
||||
if (args != null) {
|
||||
result.push("<");
|
||||
if (trieNodeBeforeArgs.children.size === 1) {
|
||||
const argsText = qname
|
||||
.args!.map((arg) => qnameToString(arg))
|
||||
.join(",");
|
||||
result.push(<ExpandableNamePart>{argsText}</ExpandableNamePart>);
|
||||
} else {
|
||||
let first = true;
|
||||
for (const arg of args) {
|
||||
result.push(arg.abbreviate());
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
result.push(",");
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push(">");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ExpandableTextButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
&:hover {
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
interface ExpandableNamePartProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function ExpandableNamePart(props: ExpandableNamePartProps) {
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<ExpandableTextButton
|
||||
onClick={(event: Event) => {
|
||||
setExpanded(!isExpanded);
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{isExpanded ? props.children : "..."}
|
||||
</ExpandableTextButton>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Span enclosing an entire qualified name.
|
||||
*
|
||||
* Can be used to gray out uninteresting parts of the name, though this looks worse than expected.
|
||||
*/
|
||||
const QNameSpan = styled.span`
|
||||
/* color: var(--vscode-disabledForeground); */
|
||||
`;
|
||||
|
||||
/** Span enclosing the innermost identifier, e.g. the `foo` in `A::B<X>::foo#abc` */
|
||||
const IdentifierSpan = styled.span`
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
/** Span enclosing keywords such as `JOIN` and `WITH`. */
|
||||
const KeywordSpan = styled.span`
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const nameTokenRegex = /\b[^ (]+\b/g;
|
||||
|
||||
function traverseMatches(
|
||||
text: string,
|
||||
regex: RegExp,
|
||||
callbacks: {
|
||||
onMatch: (match: RegExpMatchArray) => void;
|
||||
onText: (text: string) => void;
|
||||
},
|
||||
) {
|
||||
const matches = Array.from(text.matchAll(regex));
|
||||
let lastIndex = 0;
|
||||
for (const match of matches) {
|
||||
const before = text.substring(lastIndex, match.index);
|
||||
if (before !== "") {
|
||||
callbacks.onText(before);
|
||||
}
|
||||
callbacks.onMatch(match);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
const after = text.substring(lastIndex);
|
||||
if (after !== "") {
|
||||
callbacks.onText(after);
|
||||
}
|
||||
}
|
||||
|
||||
export function abbreviateRASteps(steps: string[]): React.ReactNode[] {
|
||||
const nameTokens = steps.flatMap((step) =>
|
||||
Array.from(step.matchAll(nameTokenRegex)).map((tok) => tok[0]),
|
||||
);
|
||||
const nameSet = new NameSet(nameTokens.filter((name) => name.includes("::")));
|
||||
return steps.map((step, index) => {
|
||||
const result: React.ReactNode[] = [];
|
||||
traverseMatches(step, nameTokenRegex, {
|
||||
onMatch(match) {
|
||||
const text = match[0];
|
||||
if (text.includes("::")) {
|
||||
result.push(<QNameSpan>{nameSet.getAbbreviation(text)}</QNameSpan>);
|
||||
} else if (/[A-Z]+/.test(text)) {
|
||||
result.push(<KeywordSpan>{text}</KeywordSpan>);
|
||||
} else {
|
||||
result.push(match[0]);
|
||||
}
|
||||
},
|
||||
onText(text) {
|
||||
result.push(text);
|
||||
},
|
||||
});
|
||||
return <Fragment key={index}>{result}</Fragment>;
|
||||
});
|
||||
}
|
||||
|
||||
export function abbreviateRANames(names: string[]): React.ReactNode[] {
|
||||
const nameSet = new NameSet(names);
|
||||
return names.map((name) => nameSet.getAbbreviation(name));
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import { styled } from "styled-components";
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import { Codicon } from "../common";
|
||||
|
||||
export class Renaming {
|
||||
patternRegexp: RegExp | undefined;
|
||||
|
||||
constructor(
|
||||
public pattern: string,
|
||||
public replacement: string,
|
||||
) {
|
||||
this.patternRegexp = tryCompilePattern(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
function tryCompilePattern(pattern: string): RegExp | undefined {
|
||||
try {
|
||||
return new RegExp(pattern, "i");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const Input = styled(VSCodeTextField)`
|
||||
width: 20em;
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
padding-bottom: 0.25em;
|
||||
`;
|
||||
|
||||
const Details = styled.details`
|
||||
padding: 1em;
|
||||
`;
|
||||
|
||||
interface RenamingInputProps {
|
||||
renamings: Renaming[];
|
||||
setRenamings: (renamings: Renaming[]) => void;
|
||||
}
|
||||
|
||||
export function RenamingInput(props: RenamingInputProps) {
|
||||
const { renamings, setRenamings } = props;
|
||||
return (
|
||||
<Details>
|
||||
<summary>Predicate renaming</summary>
|
||||
<p>
|
||||
The following regexp replacements are applied to every predicate name on
|
||||
both sides. Predicates whose names clash after renaming are grouped
|
||||
together. Can be used to correlate predicates that were renamed between
|
||||
the two runs.
|
||||
<br />
|
||||
Can also be used to group related predicates, for example, renaming{" "}
|
||||
<code>.*ssa.*</code> to <code>SSA</code> will group all SSA-related
|
||||
predicates together.
|
||||
</p>
|
||||
{renamings.map((renaming, index) => (
|
||||
<Row key={index}>
|
||||
<Input
|
||||
value={renaming.pattern}
|
||||
placeholder="Pattern"
|
||||
onInput={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newRenamings = [...renamings];
|
||||
newRenamings[index] = new Renaming(
|
||||
e.target.value,
|
||||
renaming.replacement,
|
||||
);
|
||||
setRenamings(newRenamings);
|
||||
}}
|
||||
>
|
||||
<Codicon name="search" slot="start" />
|
||||
</Input>
|
||||
<Input
|
||||
value={renaming.replacement}
|
||||
placeholder="Replacement"
|
||||
onInput={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newRenamings = [...renamings];
|
||||
newRenamings[index] = new Renaming(
|
||||
renaming.pattern,
|
||||
e.target.value,
|
||||
);
|
||||
setRenamings(newRenamings);
|
||||
}}
|
||||
></Input>
|
||||
<VSCodeButton
|
||||
onClick={() =>
|
||||
setRenamings(renamings.filter((_, i) => i !== index))
|
||||
}
|
||||
>
|
||||
<Codicon name="trash" />
|
||||
</VSCodeButton>
|
||||
<br />
|
||||
</Row>
|
||||
))}
|
||||
<VSCodeButton
|
||||
onClick={() => setRenamings([...renamings, new Renaming("", "")])}
|
||||
>
|
||||
Add renaming rule
|
||||
</VSCodeButton>
|
||||
</Details>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { WebviewDefinition } from "../webview-definition";
|
||||
import { ComparePerformance } from "./ComparePerformance";
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <ComparePerformance />,
|
||||
};
|
||||
|
||||
export default definition;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { styled } from "styled-components";
|
||||
|
||||
import type {
|
||||
@@ -16,6 +16,7 @@ import CompareTable from "./CompareTable";
|
||||
|
||||
import "../results/resultsView.css";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
@@ -50,115 +51,101 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
|
||||
comparison?.result &&
|
||||
(comparison.result.to.length || comparison.result.from.length);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToCompareViewMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setComparisonQueryInfo":
|
||||
setQueryInfo(msg);
|
||||
break;
|
||||
case "setComparisons":
|
||||
setComparison(msg);
|
||||
break;
|
||||
case "streamingComparisonSetup":
|
||||
setComparison(null);
|
||||
streamingComparisonRef.current = msg;
|
||||
break;
|
||||
case "streamingComparisonAddResults": {
|
||||
const prev = streamingComparisonRef.current;
|
||||
if (prev === null) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonAddResults" before "streamingComparisonSetup"',
|
||||
useMessageFromExtension<ToCompareViewMessage>((msg) => {
|
||||
switch (msg.t) {
|
||||
case "setComparisonQueryInfo":
|
||||
setQueryInfo(msg);
|
||||
break;
|
||||
case "setComparisons":
|
||||
setComparison(msg);
|
||||
break;
|
||||
case "streamingComparisonSetup":
|
||||
setComparison(null);
|
||||
streamingComparisonRef.current = msg;
|
||||
break;
|
||||
case "streamingComparisonAddResults": {
|
||||
const prev = streamingComparisonRef.current;
|
||||
if (prev === null) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonAddResults" before "streamingComparisonSetup"',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev.id !== msg.id) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonAddResults" with different id, ignoring',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let result: QueryCompareResult;
|
||||
switch (prev.result.kind) {
|
||||
case "raw":
|
||||
if (msg.result.kind !== "raw") {
|
||||
throw new Error(
|
||||
"Streaming comparison: expected raw results, got interpreted results",
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev.id !== msg.id) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonAddResults" with different id, ignoring',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let result: QueryCompareResult;
|
||||
switch (prev.result.kind) {
|
||||
case "raw":
|
||||
if (msg.result.kind !== "raw") {
|
||||
throw new Error(
|
||||
"Streaming comparison: expected raw results, got interpreted results",
|
||||
);
|
||||
}
|
||||
|
||||
result = {
|
||||
...prev.result,
|
||||
from: [...prev.result.from, ...msg.result.from],
|
||||
to: [...prev.result.to, ...msg.result.to],
|
||||
};
|
||||
break;
|
||||
case "interpreted":
|
||||
if (msg.result.kind !== "interpreted") {
|
||||
throw new Error(
|
||||
"Streaming comparison: expected interpreted results, got raw results",
|
||||
);
|
||||
}
|
||||
|
||||
result = {
|
||||
...prev.result,
|
||||
from: [...prev.result.from, ...msg.result.from],
|
||||
to: [...prev.result.to, ...msg.result.to],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected comparison result kind");
|
||||
}
|
||||
|
||||
streamingComparisonRef.current = {
|
||||
...prev,
|
||||
result,
|
||||
result = {
|
||||
...prev.result,
|
||||
from: [...prev.result.from, ...msg.result.from],
|
||||
to: [...prev.result.to, ...msg.result.to],
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
case "streamingComparisonComplete":
|
||||
if (streamingComparisonRef.current === null) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonComplete" before "streamingComparisonSetup"',
|
||||
case "interpreted":
|
||||
if (msg.result.kind !== "interpreted") {
|
||||
throw new Error(
|
||||
"Streaming comparison: expected interpreted results, got raw results",
|
||||
);
|
||||
setComparison(null);
|
||||
break;
|
||||
}
|
||||
|
||||
if (streamingComparisonRef.current.id !== msg.id) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonComplete" with different id, ignoring',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
setComparison({
|
||||
...streamingComparisonRef.current,
|
||||
t: "setComparisons",
|
||||
});
|
||||
streamingComparisonRef.current = null;
|
||||
break;
|
||||
case "setUserSettings":
|
||||
setUserSettings(msg.userSettings);
|
||||
result = {
|
||||
...prev.result,
|
||||
from: [...prev.result.from, ...msg.result.from],
|
||||
to: [...prev.result.to, ...msg.result.to],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
throw new Error("Unexpected comparison result kind");
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
streamingComparisonRef.current = {
|
||||
...prev,
|
||||
result,
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
case "streamingComparisonComplete":
|
||||
if (streamingComparisonRef.current === null) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonComplete" before "streamingComparisonSetup"',
|
||||
);
|
||||
setComparison(null);
|
||||
break;
|
||||
}
|
||||
|
||||
if (streamingComparisonRef.current.id !== msg.id) {
|
||||
console.warn(
|
||||
'Received "streamingComparisonComplete" with different id, ignoring',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
setComparison({
|
||||
...streamingComparisonRef.current,
|
||||
t: "setComparisons",
|
||||
});
|
||||
streamingComparisonRef.current = null;
|
||||
break;
|
||||
case "setUserSettings":
|
||||
setUserSettings(msg.userSettings);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!queryInfo || !comparison) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import type { ToDataFlowPathsMessage } from "../../common/interface-types";
|
||||
import type { DataFlowPaths as DataFlowPathsDomainModel } from "../../variant-analysis/shared/data-flow-paths";
|
||||
import { DataFlowPaths } from "./DataFlowPaths";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
|
||||
export type DataFlowPathsViewProps = {
|
||||
dataFlowPaths?: DataFlowPathsDomainModel;
|
||||
@@ -14,28 +15,12 @@ export function DataFlowPathsView({
|
||||
DataFlowPathsDomainModel | undefined
|
||||
>(initialDataFlowPaths);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToDataFlowPathsMessage = evt.data;
|
||||
if (msg.t === "setDataFlowPaths") {
|
||||
setDataFlowPaths(msg.dataFlowPaths);
|
||||
useMessageFromExtension<ToDataFlowPathsMessage>((msg) => {
|
||||
setDataFlowPaths(msg.dataFlowPaths);
|
||||
|
||||
// Scroll to the top of the page when we're rendering
|
||||
// new data flow paths.
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
// Scroll to the top of the page when we're rendering
|
||||
// new data flow paths.
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
|
||||
if (!dataFlowPaths) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { MethodModeling } from "./MethodModeling";
|
||||
import { getModelingStatus } from "../../model-editor/shared/modeling-status";
|
||||
import type { Method } from "../../model-editor/method";
|
||||
@@ -12,6 +12,7 @@ import { NoMethodSelected } from "./NoMethodSelected";
|
||||
import type { MethodModelingPanelViewState } from "../../model-editor/shared/view-state";
|
||||
import { MethodAlreadyModeled } from "./MethodAlreadyModeled";
|
||||
import { defaultModelConfig } from "../../model-editor/languages";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
|
||||
type Props = {
|
||||
initialViewState?: MethodModelingPanelViewState;
|
||||
@@ -36,47 +37,33 @@ export function MethodModelingView({
|
||||
[modeledMethods, isMethodModified],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToMethodModelingMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setMethodModelingPanelViewState":
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
case "setInModelingMode":
|
||||
setInModelingMode(msg.inModelingMode);
|
||||
break;
|
||||
case "setMultipleModeledMethods":
|
||||
setModeledMethods(msg.modeledMethods);
|
||||
break;
|
||||
case "setMethodModified":
|
||||
setIsMethodModified(msg.isModified);
|
||||
break;
|
||||
case "setNoMethodSelected":
|
||||
setMethod(undefined);
|
||||
setModeledMethods([]);
|
||||
setIsMethodModified(false);
|
||||
break;
|
||||
case "setSelectedMethod":
|
||||
setMethod(msg.method);
|
||||
setModeledMethods(msg.modeledMethods);
|
||||
setIsMethodModified(msg.isModified);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
useMessageFromExtension<ToMethodModelingMessage>((msg) => {
|
||||
switch (msg.t) {
|
||||
case "setMethodModelingPanelViewState":
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
case "setInModelingMode":
|
||||
setInModelingMode(msg.inModelingMode);
|
||||
break;
|
||||
case "setMultipleModeledMethods":
|
||||
setModeledMethods(msg.modeledMethods);
|
||||
break;
|
||||
case "setMethodModified":
|
||||
setIsMethodModified(msg.isModified);
|
||||
break;
|
||||
case "setNoMethodSelected":
|
||||
setMethod(undefined);
|
||||
setModeledMethods([]);
|
||||
setIsMethodModified(false);
|
||||
break;
|
||||
case "setSelectedMethod":
|
||||
setMethod(msg.method);
|
||||
setModeledMethods(msg.modeledMethods);
|
||||
setIsMethodModified(msg.isModified);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!inModelingMode || !viewState?.language) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { styled } from "styled-components";
|
||||
import { ModelAlertsHeader } from "./ModelAlertsHeader";
|
||||
import type { ModelAlertsViewState } from "../../model-editor/shared/view-state";
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "../../model-editor/shared/model-alerts-filter-sort";
|
||||
import type { ModelAlertsFilterSortState } from "../../model-editor/shared/model-alerts-filter-sort";
|
||||
import type { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
|
||||
type Props = {
|
||||
initialViewState?: ModelAlertsViewState;
|
||||
@@ -67,47 +68,33 @@ export function ModelAlerts({
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToModelAlertsMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setModelAlertsViewState": {
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
}
|
||||
case "setVariantAnalysis": {
|
||||
setVariantAnalysis(msg.variantAnalysis);
|
||||
break;
|
||||
}
|
||||
case "setRepoResults": {
|
||||
setRepoResults((oldRepoResults) => {
|
||||
const newRepoIds = msg.repoResults.map((r) => r.repositoryId);
|
||||
return [
|
||||
...oldRepoResults.filter(
|
||||
(v) => !newRepoIds.includes(v.repositoryId),
|
||||
),
|
||||
...msg.repoResults,
|
||||
];
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "revealModel": {
|
||||
setRevealedModel(msg.modeledMethod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
useMessageFromExtension<ToModelAlertsMessage>((msg) => {
|
||||
switch (msg.t) {
|
||||
case "setModelAlertsViewState": {
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
case "setVariantAnalysis": {
|
||||
setVariantAnalysis(msg.variantAnalysis);
|
||||
break;
|
||||
}
|
||||
case "setRepoResults": {
|
||||
setRepoResults((oldRepoResults) => {
|
||||
const newRepoIds = msg.repoResults.map((r) => r.repositoryId);
|
||||
return [
|
||||
...oldRepoResults.filter(
|
||||
(v) => !newRepoIds.includes(v.repositoryId),
|
||||
),
|
||||
...msg.repoResults,
|
||||
];
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "revealModel": {
|
||||
setRevealedModel(msg.modeledMethod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const modelAlerts = useMemo(() => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../../model-editor/shared/hi
|
||||
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
|
||||
import type { ModelEvaluationRunState } from "../../model-editor/shared/model-evaluation-run-state";
|
||||
import { ModelEvaluation } from "./ModelEvaluation";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
text-align: center;
|
||||
@@ -129,47 +130,33 @@ export function ModelEditor({
|
||||
AccessPathSuggestionOptions | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToModelEditorMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setModelEditorViewState":
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
case "setMethods":
|
||||
setMethods(msg.methods);
|
||||
break;
|
||||
case "setModeledAndModifiedMethods":
|
||||
setModeledMethods(msg.methods);
|
||||
setModifiedSignatures(new Set(msg.modifiedMethodSignatures));
|
||||
break;
|
||||
case "setModifiedMethods":
|
||||
setModifiedSignatures(new Set(msg.methodSignatures));
|
||||
break;
|
||||
case "revealMethod":
|
||||
setRevealedMethodSignature(msg.methodSignature);
|
||||
break;
|
||||
case "setAccessPathSuggestions":
|
||||
setAccessPathSuggestions(msg.accessPathSuggestions);
|
||||
break;
|
||||
case "setModelEvaluationRun":
|
||||
setEvaluationRun(msg.run);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
useMessageFromExtension<ToModelEditorMessage>((msg) => {
|
||||
switch (msg.t) {
|
||||
case "setModelEditorViewState":
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
case "setMethods":
|
||||
setMethods(msg.methods);
|
||||
break;
|
||||
case "setModeledAndModifiedMethods":
|
||||
setModeledMethods(msg.methods);
|
||||
setModifiedSignatures(new Set(msg.modifiedMethodSignatures));
|
||||
break;
|
||||
case "setModifiedMethods":
|
||||
setModifiedSignatures(new Set(msg.methodSignatures));
|
||||
break;
|
||||
case "revealMethod":
|
||||
setRevealedMethodSignature(msg.methodSignature);
|
||||
break;
|
||||
case "setAccessPathSuggestions":
|
||||
setAccessPathSuggestions(msg.accessPathSuggestions);
|
||||
break;
|
||||
case "setModelEvaluationRun":
|
||||
setEvaluationRun(msg.run);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -48,7 +48,10 @@ export function Graph({ graphData, databaseUri }: GraphProps) {
|
||||
d.attributes["xlink:href"] = "#";
|
||||
d.attributes["href"] = "#";
|
||||
loc.uri = `file://${loc.uri}`;
|
||||
select(this).on("click", () => jumpToLocation(loc, databaseUri));
|
||||
select(this).on("click", (event: Event) => {
|
||||
jumpToLocation(loc, databaseUri);
|
||||
event.preventDefault(); // Avoid resetting scroll position
|
||||
});
|
||||
}
|
||||
}
|
||||
if ("fill" in d.attributes) {
|
||||
|
||||
@@ -16,11 +16,12 @@ import {
|
||||
DEFAULT_USER_SETTINGS,
|
||||
GRAPH_TABLE_NAME,
|
||||
} from "../../common/interface-types";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
import { ResultTables } from "./ResultTables";
|
||||
import { onNavigation } from "./navigation";
|
||||
|
||||
import "./resultsView.css";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
/**
|
||||
* ResultsApp.tsx
|
||||
@@ -113,8 +114,8 @@ export function ResultsApp() {
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(msg: IntoResultsViewMsg): void => {
|
||||
useMessageFromExtension<IntoResultsViewMsg>(
|
||||
(msg) => {
|
||||
switch (msg.t) {
|
||||
case "setUserSettings":
|
||||
setUserSettings(msg.userSettings);
|
||||
@@ -189,26 +190,6 @@ export function ResultsApp() {
|
||||
[updateStateWithNewResultsInfo],
|
||||
);
|
||||
|
||||
const vscodeMessageHandler = useCallback(
|
||||
(evt: MessageEvent) => {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
if (evt.origin === window.origin) {
|
||||
handleMessage(evt.data as IntoResultsViewMsg);
|
||||
} else {
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
},
|
||||
[handleMessage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", vscodeMessageHandler);
|
||||
return () => {
|
||||
window.removeEventListener("message", vscodeMessageHandler);
|
||||
};
|
||||
}, [vscodeMessageHandler]);
|
||||
|
||||
const { displayedResults, nextResultsInfo, isExpectingResultsUpdate } = state;
|
||||
if (
|
||||
displayedResults.results !== null &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import type {
|
||||
VariantAnalysis as VariantAnalysisDomainModel,
|
||||
@@ -13,6 +13,7 @@ import type { ToVariantAnalysisMessage } from "../../common/interface-types";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { defaultFilterSortState } from "../../variant-analysis/shared/variant-analysis-filter-sort";
|
||||
import { sendTelemetry, useTelemetryOnChange } from "../common/telemetry";
|
||||
import { useMessageFromExtension } from "../common/useMessageFromExtension";
|
||||
|
||||
export type VariantAnalysisProps = {
|
||||
variantAnalysis?: VariantAnalysisDomainModel;
|
||||
@@ -77,49 +78,31 @@ export function VariantAnalysis({
|
||||
debounceTimeoutMillis: 1000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToVariantAnalysisMessage = evt.data;
|
||||
if (msg.t === "setVariantAnalysis") {
|
||||
setVariantAnalysis(msg.variantAnalysis);
|
||||
vscode.setState({
|
||||
variantAnalysisId: msg.variantAnalysis.id,
|
||||
});
|
||||
} else if (msg.t === "setFilterSortState") {
|
||||
setFilterSortState(msg.filterSortState);
|
||||
} else if (msg.t === "setRepoResults") {
|
||||
setRepoResults((oldRepoResults) => {
|
||||
const newRepoIds = msg.repoResults.map((r) => r.repositoryId);
|
||||
return [
|
||||
...oldRepoResults.filter(
|
||||
(v) => !newRepoIds.includes(v.repositoryId),
|
||||
),
|
||||
...msg.repoResults,
|
||||
];
|
||||
});
|
||||
} else if (msg.t === "setRepoStates") {
|
||||
setRepoStates((oldRepoStates) => {
|
||||
const newRepoIds = msg.repoStates.map((r) => r.repositoryId);
|
||||
return [
|
||||
...oldRepoStates.filter(
|
||||
(v) => !newRepoIds.includes(v.repositoryId),
|
||||
),
|
||||
...msg.repoStates,
|
||||
];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
useMessageFromExtension<ToVariantAnalysisMessage>((msg) => {
|
||||
if (msg.t === "setVariantAnalysis") {
|
||||
setVariantAnalysis(msg.variantAnalysis);
|
||||
vscode.setState({
|
||||
variantAnalysisId: msg.variantAnalysis.id,
|
||||
});
|
||||
} else if (msg.t === "setFilterSortState") {
|
||||
setFilterSortState(msg.filterSortState);
|
||||
} else if (msg.t === "setRepoResults") {
|
||||
setRepoResults((oldRepoResults) => {
|
||||
const newRepoIds = msg.repoResults.map((r) => r.repositoryId);
|
||||
return [
|
||||
...oldRepoResults.filter((v) => !newRepoIds.includes(v.repositoryId)),
|
||||
...msg.repoResults,
|
||||
];
|
||||
});
|
||||
} else if (msg.t === "setRepoStates") {
|
||||
setRepoStates((oldRepoStates) => {
|
||||
const newRepoIds = msg.repoStates.map((r) => r.repositoryId);
|
||||
return [
|
||||
...oldRepoStates.filter((v) => !newRepoIds.includes(v.repositoryId)),
|
||||
...msg.repoStates,
|
||||
];
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const copyRepositoryList = useCallback(() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { registerUnhandledErrorListener } from "./common/errors";
|
||||
import type { WebviewDefinition } from "./webview-definition";
|
||||
|
||||
import compareView from "./compare";
|
||||
import comparePerformance from "./compare-performance";
|
||||
import dataFlowPathsView from "./data-flow-paths";
|
||||
import methodModelingView from "./method-modeling";
|
||||
import modelEditorView from "./model-editor";
|
||||
@@ -18,6 +19,7 @@ import "@vscode/codicons/dist/codicon.css";
|
||||
|
||||
const views: Record<string, WebviewDefinition> = {
|
||||
compare: compareView,
|
||||
"compare-performance": comparePerformance,
|
||||
"data-flow-paths": dataFlowPathsView,
|
||||
"method-modeling": methodModelingView,
|
||||
"model-editor": modelEditorView,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[
|
||||
"v2.19.2",
|
||||
"v2.20.2",
|
||||
"v2.19.4",
|
||||
"v2.18.4",
|
||||
"v2.17.6",
|
||||
"v2.16.6",
|
||||
"nightly"
|
||||
]
|
||||
|
||||
87
extensions/ql-vscode/test/benchmarks/jsonl-reader.bench.ts
Normal file
87
extensions/ql-vscode/test/benchmarks/jsonl-reader.bench.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Benchmarks the jsonl-parser against a reference implementation and checks that it generates
|
||||
* the same output.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ts-node json-reader.bench.ts [evaluator-log.summary.jsonl] [count]
|
||||
*
|
||||
* The log file defaults to a small checked-in log and count defaults to 100
|
||||
* (and should be lowered significantly for large files).
|
||||
*
|
||||
* At the time of writing it is about as fast as the synchronous reference implementation,
|
||||
* but doesn't run out of memory for large files.
|
||||
*/
|
||||
import { readFile } from "fs-extra";
|
||||
import { readJsonlFile } from "../../src/common/jsonl-reader";
|
||||
import { performance } from "perf_hooks";
|
||||
import { join } from "path";
|
||||
|
||||
/** An "obviously correct" implementation to test against. */
|
||||
async function readJsonlReferenceImpl<T>(
|
||||
path: string,
|
||||
handler: (value: T) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const logSummary = await readFile(path, "utf-8");
|
||||
|
||||
// Remove newline delimiters because summary is in .jsonl format.
|
||||
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);
|
||||
|
||||
for (const obj of jsonSummaryObjects) {
|
||||
const jsonObj = JSON.parse(obj) as T;
|
||||
await handler(jsonObj);
|
||||
}
|
||||
}
|
||||
|
||||
type ParserFn = (
|
||||
text: string,
|
||||
callback: (v: unknown) => Promise<void>,
|
||||
) => Promise<void>;
|
||||
|
||||
const parsers: Record<string, ParserFn> = {
|
||||
readJsonlReferenceImpl,
|
||||
readJsonlFile,
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const file =
|
||||
args.length > 0
|
||||
? args[0]
|
||||
: join(
|
||||
__dirname,
|
||||
"../unit-tests/data/evaluator-log-summaries/bad-join-order.jsonl",
|
||||
);
|
||||
const numTrials = args.length > 1 ? Number(args[1]) : 100;
|
||||
const referenceValues: any[] = [];
|
||||
await readJsonlReferenceImpl(file, async (event) => {
|
||||
referenceValues.push(event);
|
||||
});
|
||||
const referenceValueString = JSON.stringify(referenceValues);
|
||||
// Do warm-up runs and check against reference implementation
|
||||
for (const [name, parser] of Object.entries(parsers)) {
|
||||
const values: unknown[] = [];
|
||||
await parser(file, async (event) => {
|
||||
values.push(event);
|
||||
});
|
||||
if (JSON.stringify(values) !== referenceValueString) {
|
||||
console.error(`${name}: failed to match reference implementation`);
|
||||
}
|
||||
}
|
||||
for (const [name, parser] of Object.entries(parsers)) {
|
||||
const startTime = performance.now();
|
||||
for (let i = 0; i < numTrials; ++i) {
|
||||
await Promise.all([
|
||||
parser(file, async () => {}),
|
||||
parser(file, async () => {}),
|
||||
]);
|
||||
}
|
||||
const duration = performance.now() - startTime;
|
||||
const durationPerTrial = duration / numTrials;
|
||||
console.log(`${name}: ${durationPerTrial.toFixed(1)} ms`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
console.error(err);
|
||||
});
|
||||
@@ -9,12 +9,12 @@ Setup
|
||||
- install playwright if you haven't yet (`npx playwright install`)
|
||||
- go to the e2e test folder on your terminal
|
||||
- make sure docker is running
|
||||
- run `docker-compose build`
|
||||
- run `docker-compose up`
|
||||
- run `docker compose build`
|
||||
- run `docker compose up`
|
||||
|
||||
Run tests
|
||||
|
||||
- run `npx playwright test --ui` from the e2e test folder to follow the test while it's running. This UI has a 'locator' tool with which elements on the test screen can be found
|
||||
- run `npx playwright test --ui` from the e2e test folder to follow the test while it's running. This UI has a 'locator' tool with which elements on the test screen can be found
|
||||
- use `npx playwright test --debug` to follow the test in real time and interact with the interface, e.g. press enter or input into fields, stop and start
|
||||
|
||||
During the test elements are created in the docker volume, e.g. the downloaded database or query data. This might interfer with other tests or when running a test twice. If that happens restart your docker volume by using `docker-compose down -v` and `docker-compose up`. Sometimes already existing queries from former runs change the input the extension needs.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
code-server:
|
||||
build:
|
||||
@@ -38,7 +36,7 @@ services:
|
||||
depends_on:
|
||||
- files-init
|
||||
files-init:
|
||||
image: alpine:3.19.1
|
||||
image: alpine:3.21.0
|
||||
restart: "no"
|
||||
# Since we're not running the code-server container using the same user as our host user,
|
||||
# we need to set the permissions on the mounted volumes to match the user inside the container.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM codercom/code-server:4.23.1
|
||||
FROM codercom/code-server:4.96.2
|
||||
|
||||
USER root
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"workbench.startupEditor": "none",
|
||||
"security.workspace.trust.enabled": false,
|
||||
"codeQL.cli.executablePath": "/opt/codeql/codeql",
|
||||
"codeQL.telemetry.enableTelemetry": false
|
||||
"codeQL.cli.executablePath": "/opt/codeql/codeql"
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@ test("run query and open it from history", async ({ page }) => {
|
||||
|
||||
await page.getByRole("tab", { name: "CodeQL" }).locator("a").click();
|
||||
|
||||
// decline extension telemetry
|
||||
await page.getByRole("button", { name: "No", exact: true }).click({
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
await page.keyboard.press("Control+Shift+P");
|
||||
await page.keyboard.type("Create Query");
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
@@ -3,18 +3,12 @@ import { resolve } from "path";
|
||||
import type { TextDocument } from "vscode";
|
||||
import { authentication, commands, window, workspace } from "vscode";
|
||||
|
||||
import { MockGitHubApiServer } from "../../../../src/common/mock-gh-api/mock-gh-api-server";
|
||||
import { mockedQuickPickItem } from "../../utils/mocking.helpers";
|
||||
import { setRemoteControllerRepo } from "../../../../src/config";
|
||||
import { getActivatedExtension } from "../../global.helper";
|
||||
import { createVSCodeCommandManager } from "../../../../src/common/vscode/commands";
|
||||
import type { AllCommands } from "../../../../src/common/commands";
|
||||
|
||||
const mockServer = new MockGitHubApiServer();
|
||||
beforeAll(() => mockServer.startServer("bypass"));
|
||||
afterEach(() => mockServer.unloadScenario());
|
||||
afterAll(() => mockServer.stopServer());
|
||||
|
||||
async function showQlDocument(name: string): Promise<TextDocument> {
|
||||
const folderPath = workspace.workspaceFolders![0].uri.fsPath;
|
||||
const documentPath = resolve(folderPath, name);
|
||||
@@ -24,7 +18,7 @@ async function showQlDocument(name: string): Promise<TextDocument> {
|
||||
}
|
||||
|
||||
// MSW can't intercept fetch requests made in VS Code, so we are skipping these tests for now
|
||||
describe.skip("Variant Analysis Submission Integration", () => {
|
||||
describe("Variant Analysis Submission Integration", () => {
|
||||
const commandManager = createVSCodeCommandManager<AllCommands>();
|
||||
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
|
||||
let executeCommandSpy: jest.SpiedFunction<typeof commands.executeCommand>;
|
||||
@@ -54,9 +48,16 @@ describe.skip("Variant Analysis Submission Integration", () => {
|
||||
await getActivatedExtension();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await commandManager.execute("codeQL.mockGitHubApiServer.unloadScenario");
|
||||
});
|
||||
|
||||
describe("Successful scenario", () => {
|
||||
beforeEach(async () => {
|
||||
await mockServer.loadScenario("mrva-problem-query-success");
|
||||
await commandManager.execute(
|
||||
"codeQL.mockGitHubApiServer.loadScenario",
|
||||
"mrva-problem-query-success",
|
||||
);
|
||||
});
|
||||
|
||||
it("opens the variant analysis view", async () => {
|
||||
@@ -81,7 +82,10 @@ describe.skip("Variant Analysis Submission Integration", () => {
|
||||
|
||||
describe("Missing controller repo", () => {
|
||||
beforeEach(async () => {
|
||||
await mockServer.loadScenario("mrva-missing-controller-repo");
|
||||
await commandManager.execute(
|
||||
"codeQL.mockGitHubApiServer.loadScenario",
|
||||
"mrva-missing-controller-repo",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the error message", async () => {
|
||||
@@ -108,7 +112,10 @@ describe.skip("Variant Analysis Submission Integration", () => {
|
||||
|
||||
describe("Submission failure", () => {
|
||||
beforeEach(async () => {
|
||||
await mockServer.loadScenario("mrva-submission-failure");
|
||||
await commandManager.execute(
|
||||
"codeQL.mockGitHubApiServer.loadScenario",
|
||||
"mrva-submission-failure",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the error message", async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Uri } from "vscode";
|
||||
import { remove } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
import { isIOError } from "../../../../src/common/files";
|
||||
import { QLTestDiscovery } from "../../../../src/query-testing/qltest-discovery";
|
||||
import type { DirectoryResult } from "tmp-promise";
|
||||
import { dir } from "tmp-promise";
|
||||
@@ -49,7 +50,15 @@ describe("qltest-discovery", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await directory.cleanup();
|
||||
try {
|
||||
await directory.cleanup();
|
||||
} catch (e) {
|
||||
if (isIOError(e) && e.code === "ENOENT") {
|
||||
// This is fine, the directory was already removed
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should run discovery", async () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { window } from "vscode";
|
||||
import {
|
||||
showBinaryChoiceDialog,
|
||||
showBinaryChoiceWithUrlDialog,
|
||||
showInformationMessageWithAction,
|
||||
showNeverAskAgainDialog,
|
||||
} from "../../../../../src/common/vscode/dialog";
|
||||
@@ -68,57 +67,6 @@ describe("showInformationMessageWithAction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("showBinaryChoiceWithUrlDialog", () => {
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
showInformationMessageSpy = jest
|
||||
.spyOn(window, "showInformationMessage")
|
||||
.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
const resolveArg =
|
||||
(index: number) =>
|
||||
(...args: any[]) =>
|
||||
Promise.resolve(args[index]);
|
||||
|
||||
it("should show a binary choice dialog with a url and return `yes`", async () => {
|
||||
// pretend user clicks on the url twice and then clicks 'yes'
|
||||
showInformationMessageSpy
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(3));
|
||||
const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url");
|
||||
expect(val).toBe(true);
|
||||
});
|
||||
|
||||
it("should show a binary choice dialog with a url and return `no`", async () => {
|
||||
// pretend user clicks on the url twice and then clicks 'no'
|
||||
showInformationMessageSpy
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(4));
|
||||
const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url");
|
||||
expect(val).toBe(false);
|
||||
});
|
||||
|
||||
it("should show a binary choice dialog and exit after clcking `more info` 5 times", async () => {
|
||||
// pretend user clicks on the url twice and then clicks 'no'
|
||||
showInformationMessageSpy
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(2))
|
||||
.mockImplementation(resolveArg(2));
|
||||
const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url");
|
||||
// No choice was made
|
||||
expect(val).toBeUndefined();
|
||||
expect(showInformationMessageSpy).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showNeverAskAgainDialog", () => {
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { ExtensionContext } from "vscode";
|
||||
export function createMockExtensionContext(): ExtensionContext {
|
||||
return {
|
||||
globalState: {
|
||||
_state: {
|
||||
"telemetry-request-viewed": true,
|
||||
} as Record<string, any>,
|
||||
_state: {} as Record<string, any>,
|
||||
get(key: string) {
|
||||
return this._state[key];
|
||||
},
|
||||
|
||||
@@ -21,241 +21,489 @@ describe("HistoryItemLabelProvider", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
format: "xxx %q xxx",
|
||||
format: "xxx ${queryName} xxx",
|
||||
ttlInMillis: 0,
|
||||
onDidChangeConfiguration: jest.fn(),
|
||||
};
|
||||
labelProvider = new HistoryItemLabelProvider(config);
|
||||
});
|
||||
|
||||
describe("local queries", () => {
|
||||
it("should interpolate query when user specified", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
userSpecifiedLabel,
|
||||
resultCount: 456,
|
||||
hasMetadata: true,
|
||||
describe("modern format", () => {
|
||||
describe("local queries", () => {
|
||||
it("should interpolate query when user specified", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
userSpecifiedLabel,
|
||||
resultCount: 456,
|
||||
hasMetadata: true,
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe("user-specified-name");
|
||||
|
||||
fqi.userSpecifiedLabel =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %f %r %%::%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %::${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe("user-specified-name");
|
||||
it("should interpolate query when not user specified", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
resultCount: 456,
|
||||
hasMetadata: true,
|
||||
});
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
expect(labelProvider.getLabel(fqi)).toBe("xxx query-name xxx");
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %f %r %%::%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %::${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %::${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %::${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should get query short label", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
userSpecifiedLabel,
|
||||
hasMetadata: true,
|
||||
resultCount: 456,
|
||||
});
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("user-specified-name");
|
||||
|
||||
// use query name if no user-specified label exists
|
||||
fqi.userSpecifiedLabel = undefined;
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("query-name");
|
||||
|
||||
// use file name if no user-specified label exists and the query is not yet completed (meaning it has no results)
|
||||
const fqi2 = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
hasMetadata: true,
|
||||
});
|
||||
expect(labelProvider.getShortLabel(fqi2)).toBe("query-file.ql");
|
||||
});
|
||||
});
|
||||
|
||||
it("should interpolate query when not user specified", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
resultCount: 456,
|
||||
hasMetadata: true,
|
||||
describe("variant analyses", () => {
|
||||
it("should interpolate query when user specified", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
userSpecifiedLabel,
|
||||
executionStartTime,
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe(userSpecifiedLabel);
|
||||
|
||||
fqi.userSpecifiedLabel =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories in progress %`,
|
||||
);
|
||||
|
||||
fqi.userSpecifiedLabel =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} %::${startTime} ${queryName} ${databaseName} ${status} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories in progress %::${dateStr} a-query-name (javascript) 1/3 repositories in progress %`,
|
||||
);
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe("xxx query-name xxx");
|
||||
it("should interpolate query when not user-specified", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
resultCount: 16,
|
||||
});
|
||||
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
"xxx a-query-name (javascript) xxx",
|
||||
);
|
||||
|
||||
config.format = "%t %q %d %s %f %r %%::%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %::${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
});
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
|
||||
it("should get query short label", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
userSpecifiedLabel,
|
||||
hasMetadata: true,
|
||||
resultCount: 456,
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %::${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %::${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
});
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("user-specified-name");
|
||||
it("should get query short label", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
userSpecifiedLabel,
|
||||
});
|
||||
|
||||
// use query name if no user-specified label exists
|
||||
fqi.userSpecifiedLabel = undefined;
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("query-name");
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("user-specified-name");
|
||||
|
||||
// use file name if no user-specified label exists and the query is not yet completed (meaning it has no results)
|
||||
const fqi2 = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
hasMetadata: true,
|
||||
// use query name if no user-specified label exists
|
||||
const fqi2 = createMockVariantAnalysisHistoryItem({});
|
||||
|
||||
expect(labelProvider.getShortLabel(fqi2)).toBe("a-query-name");
|
||||
});
|
||||
|
||||
describe("when results are present", () => {
|
||||
it("should display results if there are any", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 16,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when results are not present", () => {
|
||||
it("should skip displaying them", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present in the middle of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present at the start of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format =
|
||||
" ${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} %";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
` ${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present at the end of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format =
|
||||
"${startTime} ${queryName} ${databaseName} ${status} ${queryFileBasename} ${resultCount} % ";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path % `,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(labelProvider.getShortLabel(fqi2)).toBe("query-file.ql");
|
||||
});
|
||||
});
|
||||
|
||||
describe("variant analyses", () => {
|
||||
it("should interpolate query when user specified", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
userSpecifiedLabel,
|
||||
executionStartTime,
|
||||
describe("legacy format", () => {
|
||||
describe("local queries", () => {
|
||||
it("should interpolate query when user specified", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
userSpecifiedLabel,
|
||||
resultCount: 456,
|
||||
hasMetadata: true,
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe("user-specified-name");
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %f %r %%::%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %::${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe(userSpecifiedLabel);
|
||||
it("should interpolate query when not user specified", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
resultCount: 456,
|
||||
hasMetadata: true,
|
||||
});
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories in progress %`,
|
||||
);
|
||||
expect(labelProvider.getLabel(fqi)).toBe("xxx query-name xxx");
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %%::%t %q %d %s %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories in progress %::${dateStr} a-query-name (javascript) 1/3 repositories in progress %`,
|
||||
);
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
|
||||
it("should interpolate query when not user-specified", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
resultCount: 16,
|
||||
config.format = "%t %q %d %s %f %r %%::%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %::${dateStr} query-name db-name finished in 0 seconds query-file.ql (456 results) %`,
|
||||
);
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
"xxx a-query-name (javascript) xxx",
|
||||
);
|
||||
it("should get query short label", () => {
|
||||
const fqi = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
userSpecifiedLabel,
|
||||
hasMetadata: true,
|
||||
resultCount: 456,
|
||||
});
|
||||
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("user-specified-name");
|
||||
|
||||
config.format = "%t %q %d %s %f %r %%::%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %::${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
// use query name if no user-specified label exists
|
||||
fqi.userSpecifiedLabel = undefined;
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("query-name");
|
||||
|
||||
// use file name if no user-specified label exists and the query is not yet completed (meaning it has no results)
|
||||
const fqi2 = createMockLocalQueryInfo({
|
||||
startTime: date,
|
||||
hasMetadata: true,
|
||||
});
|
||||
expect(labelProvider.getShortLabel(fqi2)).toBe("query-file.ql");
|
||||
});
|
||||
});
|
||||
|
||||
it("should get query short label", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
userSpecifiedLabel,
|
||||
describe("variant analyses", () => {
|
||||
it("should interpolate query when user specified", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
userSpecifiedLabel,
|
||||
executionStartTime,
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe(userSpecifiedLabel);
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories in progress %`,
|
||||
);
|
||||
|
||||
fqi.userSpecifiedLabel = "%t %q %d %s %%::%t %q %d %s %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories in progress %::${dateStr} a-query-name (javascript) 1/3 repositories in progress %`,
|
||||
);
|
||||
});
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("user-specified-name");
|
||||
|
||||
// use query name if no user-specified label exists
|
||||
const fqi2 = createMockVariantAnalysisHistoryItem({});
|
||||
|
||||
expect(labelProvider.getShortLabel(fqi2)).toBe("a-query-name");
|
||||
});
|
||||
|
||||
describe("when results are present", () => {
|
||||
it("should display results if there are any", () => {
|
||||
it("should interpolate query when not user-specified", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
resultCount: 16,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
"xxx a-query-name (javascript) xxx",
|
||||
);
|
||||
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path (16 results) %`,
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
|
||||
config.format = "%t %q %d %s %f %r %%::%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %::${dateStr} a-query-name (javascript) 1/3 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when results are not present", () => {
|
||||
it("should skip displaying them", () => {
|
||||
it("should get query short label", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
userSpecifiedLabel,
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).toBe("user-specified-name");
|
||||
|
||||
// use query name if no user-specified label exists
|
||||
const fqi2 = createMockVariantAnalysisHistoryItem({});
|
||||
|
||||
expect(labelProvider.getShortLabel(fqi2)).toBe("a-query-name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present in the middle of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
describe("when results are present", () => {
|
||||
it("should display results if there are any", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 16,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path (16 results) %`,
|
||||
);
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present at the start of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
describe("when results are not present", () => {
|
||||
it("should skip displaying them", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
config.format = " %t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
` ${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present at the end of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
describe("when extra whitespace is present in the middle of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present at the start of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format = " %t %q %d %s %f %r %%";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
` ${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path %`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when extra whitespace is present at the end of the label", () => {
|
||||
it("should squash it down to a single whitespace", () => {
|
||||
const fqi = createMockVariantAnalysisHistoryItem({
|
||||
historyItemStatus: QueryStatus.Completed,
|
||||
resultCount: 0,
|
||||
variantAnalysis: createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
executionStartTime,
|
||||
scannedRepos: createMockScannedRepos([
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
VariantAnalysisRepoStatus.Succeeded,
|
||||
]),
|
||||
}),
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %% ";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path % `,
|
||||
);
|
||||
});
|
||||
config.format = "%t %q %d %s %f %r %% ";
|
||||
expect(labelProvider.getLabel(fqi)).toBe(
|
||||
`${dateStr} a-query-name (javascript) 2/2 repositories completed a-query-file-path % `,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ describe("HistoryTreeDataProvider", () => {
|
||||
let app: App;
|
||||
let configListener: QueryHistoryConfigListener;
|
||||
const doCompareCallback = jest.fn();
|
||||
const doComparePerformanceCallback = jest.fn();
|
||||
|
||||
let queryHistoryManager: QueryHistoryManager;
|
||||
|
||||
@@ -506,6 +507,7 @@ describe("HistoryTreeDataProvider", () => {
|
||||
}),
|
||||
languageContext,
|
||||
doCompareCallback,
|
||||
doComparePerformanceCallback,
|
||||
);
|
||||
(qhm.treeDataProvider as any).history = [...allHistory];
|
||||
await workspace.saveAll();
|
||||
|
||||
@@ -40,6 +40,7 @@ describe("QueryHistoryManager", () => {
|
||||
typeof variantAnalysisManagerStub.cancelVariantAnalysis
|
||||
>;
|
||||
const doCompareCallback = jest.fn();
|
||||
const doComparePerformanceCallback = jest.fn();
|
||||
|
||||
let executeCommand: jest.MockedFn<
|
||||
(commandName: string, ...args: any[]) => Promise<any>
|
||||
@@ -939,6 +940,7 @@ describe("QueryHistoryManager", () => {
|
||||
}),
|
||||
new LanguageContextStore(mockApp),
|
||||
doCompareCallback,
|
||||
doComparePerformanceCallback,
|
||||
);
|
||||
(qhm.treeDataProvider as any).history = [...allHistory];
|
||||
await workspace.saveAll();
|
||||
|
||||
@@ -105,6 +105,7 @@ describe("Variant Analyses and QueryHistoryManager", () => {
|
||||
}),
|
||||
new LanguageContextStore(app),
|
||||
asyncNoop,
|
||||
asyncNoop,
|
||||
);
|
||||
disposables.push(qhm);
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ describe("query-results", () => {
|
||||
});
|
||||
|
||||
const finished = new Promise((res, rej) => {
|
||||
validSarifStream.addListener("close", res);
|
||||
validSarifStream.addListener("close", () => res(undefined));
|
||||
validSarifStream.addListener("error", rej);
|
||||
});
|
||||
|
||||
@@ -357,7 +357,7 @@ describe("query-results", () => {
|
||||
});
|
||||
|
||||
const finished = new Promise((res, rej) => {
|
||||
invalidSarifStream.addListener("close", res);
|
||||
invalidSarifStream.addListener("close", () => res(undefined));
|
||||
invalidSarifStream.addListener("error", rej);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import TelemetryReporter from "vscode-extension-telemetry";
|
||||
import type { ExtensionContext } from "vscode";
|
||||
import { workspace, ConfigurationTarget, window, env } from "vscode";
|
||||
import { workspace, env } from "vscode";
|
||||
import {
|
||||
ExtensionTelemetryListener,
|
||||
telemetryListener as globalTelemetryListener,
|
||||
} from "../../../src/common/vscode/telemetry";
|
||||
import { UserCancellationException } from "../../../src/common/vscode/progress";
|
||||
import { ENABLE_TELEMETRY } from "../../../src/config";
|
||||
import { createMockExtensionContext } from "./index";
|
||||
import { vscodeGetConfigurationMock } from "../test-config";
|
||||
import { redactableError } from "../../../src/common/errors";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
@@ -17,10 +13,7 @@ import { SemVer } from "semver";
|
||||
jest.setTimeout(10000);
|
||||
|
||||
describe("telemetry reporting", () => {
|
||||
let originalTelemetryExtension: boolean | undefined;
|
||||
let originalTelemetryGlobal: string | undefined;
|
||||
let isCanary: string;
|
||||
let ctx: ExtensionContext;
|
||||
let telemetryListener: ExtensionTelemetryListener;
|
||||
|
||||
let sendTelemetryEventSpy: jest.SpiedFunction<
|
||||
@@ -29,22 +22,8 @@ describe("telemetry reporting", () => {
|
||||
let sendTelemetryErrorEventSpy: jest.SpiedFunction<
|
||||
typeof TelemetryReporter.prototype.sendTelemetryErrorEvent
|
||||
>;
|
||||
let disposeSpy: jest.SpiedFunction<
|
||||
typeof TelemetryReporter.prototype.dispose
|
||||
>;
|
||||
|
||||
let isTelemetryEnabledSpy: jest.SpyInstance<
|
||||
typeof env.isTelemetryEnabled,
|
||||
[]
|
||||
>;
|
||||
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vscodeGetConfigurationMock.mockRestore();
|
||||
|
||||
try {
|
||||
// in case a previous test has accidentally activated this extension,
|
||||
// need to disable it first.
|
||||
@@ -52,44 +31,24 @@ describe("telemetry reporting", () => {
|
||||
// specified in the package.json.
|
||||
globalTelemetryListener?.dispose();
|
||||
|
||||
ctx = createMockExtensionContext();
|
||||
|
||||
sendTelemetryEventSpy = jest
|
||||
.spyOn(TelemetryReporter.prototype, "sendTelemetryEvent")
|
||||
.mockReturnValue(undefined);
|
||||
sendTelemetryErrorEventSpy = jest
|
||||
.spyOn(TelemetryReporter.prototype, "sendTelemetryErrorEvent")
|
||||
.mockReturnValue(undefined);
|
||||
disposeSpy = jest
|
||||
.spyOn(TelemetryReporter.prototype, "dispose")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
showInformationMessageSpy = jest
|
||||
.spyOn(window, "showInformationMessage")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
originalTelemetryExtension = workspace
|
||||
.getConfiguration()
|
||||
.get<boolean>("codeQL.telemetry.enableTelemetry");
|
||||
originalTelemetryGlobal = workspace
|
||||
.getConfiguration()
|
||||
.get<string>("telemetry.telemetryLevel");
|
||||
isCanary = (!!workspace
|
||||
.getConfiguration()
|
||||
.get<boolean>("codeQL.canary")).toString();
|
||||
|
||||
// each test will default to telemetry being enabled
|
||||
isTelemetryEnabledSpy = jest
|
||||
.spyOn(env, "isTelemetryEnabled", "get")
|
||||
.mockReturnValue(true);
|
||||
await setTelemetryLevel("telemetry", "all");
|
||||
await enableTelemetry("codeQL.telemetry", true);
|
||||
jest.spyOn(env, "isTelemetryEnabled", "get").mockReturnValue(true);
|
||||
|
||||
telemetryListener = new ExtensionTelemetryListener(
|
||||
"my-id",
|
||||
"1.2.3",
|
||||
"fake-key",
|
||||
ctx,
|
||||
);
|
||||
await wait(100);
|
||||
} catch (e) {
|
||||
@@ -99,18 +58,9 @@ describe("telemetry reporting", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
telemetryListener?.dispose();
|
||||
// await wait(100);
|
||||
try {
|
||||
await setTelemetryLevel("telemetry", originalTelemetryGlobal);
|
||||
await enableTelemetry("codeQL.telemetry", originalTelemetryExtension);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
it("should initialize telemetry when 'codeQL.telemetry.enableTelemetry' is enabled and global 'telemetry.telemetryLevel' is 'all'", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
it("should initialize telemetry", async () => {
|
||||
expect(telemetryListener._reporter).toBeDefined();
|
||||
const reporter: any = telemetryListener._reporter;
|
||||
expect(reporter.extensionId).toBe("my-id");
|
||||
@@ -118,80 +68,7 @@ describe("telemetry reporting", () => {
|
||||
expect(reporter.userOptIn).toBe(true); // enabled
|
||||
});
|
||||
|
||||
it("should initialize telemetry when global 'telemetry.telemetryLevel' is 'off'", async () => {
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await setTelemetryLevel("telemetry", "off");
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).toBeDefined();
|
||||
|
||||
const reporter: any = telemetryListener._reporter;
|
||||
expect(reporter.userOptIn).toBe(false); // disabled
|
||||
});
|
||||
|
||||
it("should not initialize telemetry when extension option disabled", async () => {
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
await telemetryListener.initialize();
|
||||
|
||||
expect(telemetryListener._reporter).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not initialize telemetry when both options disabled", async () => {
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await setTelemetryLevel("telemetry", "off");
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should dispose telemetry object when re-initializing and should not add multiple", async () => {
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).toBeDefined();
|
||||
const firstReporter = telemetryListener._reporter;
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).toBeDefined();
|
||||
expect(telemetryListener._reporter).not.toBe(firstReporter);
|
||||
|
||||
expect(disposeSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// initializing a third time continues to dispose
|
||||
await telemetryListener.initialize();
|
||||
expect(disposeSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should reinitialize reporter when extension setting changes", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
expect(disposeSpy).not.toHaveBeenCalled();
|
||||
expect(telemetryListener._reporter).toBeDefined();
|
||||
|
||||
// this disables the reporter
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
|
||||
expect(telemetryListener._reporter).toBeUndefined();
|
||||
|
||||
expect(disposeSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// creates a new reporter, but does not dispose again
|
||||
await enableTelemetry("codeQL.telemetry", true);
|
||||
|
||||
expect(telemetryListener._reporter).toBeDefined();
|
||||
expect(disposeSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should set userOptIn to false when global setting changes", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
const reporter: any = telemetryListener._reporter;
|
||||
expect(reporter.userOptIn).toBe(true); // enabled
|
||||
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await setTelemetryLevel("telemetry", "off");
|
||||
expect(reporter.userOptIn).toBe(false); // disabled
|
||||
});
|
||||
|
||||
it("should send an event", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendCommandUsage("command-id", 1234, undefined);
|
||||
|
||||
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
|
||||
@@ -208,8 +85,6 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should send a command usage event with an error", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendCommandUsage(
|
||||
"command-id",
|
||||
1234,
|
||||
@@ -230,7 +105,6 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should send a command usage event with a cli version", async () => {
|
||||
await telemetryListener.initialize();
|
||||
telemetryListener.cliVersion = new SemVer("1.2.3");
|
||||
|
||||
telemetryListener.sendCommandUsage(
|
||||
@@ -274,39 +148,7 @@ describe("telemetry reporting", () => {
|
||||
expect(sendTelemetryErrorEventSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should avoid sending an event when telemetry is disabled", async () => {
|
||||
await telemetryListener.initialize();
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
|
||||
telemetryListener.sendCommandUsage("command-id", 1234, undefined);
|
||||
telemetryListener.sendCommandUsage("command-id", 1234, new Error());
|
||||
|
||||
expect(sendTelemetryEventSpy).not.toHaveBeenCalled();
|
||||
expect(sendTelemetryErrorEventSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send an event when telemetry is re-enabled", async () => {
|
||||
await telemetryListener.initialize();
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
await enableTelemetry("codeQL.telemetry", true);
|
||||
|
||||
telemetryListener.sendCommandUsage("command-id", 1234, undefined);
|
||||
|
||||
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
|
||||
"command-usage",
|
||||
{
|
||||
name: "command-id",
|
||||
status: "Success",
|
||||
isCanary,
|
||||
cliVersion: "not-set",
|
||||
},
|
||||
{ executionTime: 1234 },
|
||||
);
|
||||
expect(sendTelemetryErrorEventSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should filter undesired properties from telemetry payload", async () => {
|
||||
await telemetryListener.initialize();
|
||||
// Reach into the internal appInsights client to grab our telemetry processor.
|
||||
const telemetryProcessor: Function = (telemetryListener._reporter as any)
|
||||
.appInsightsClient._telemetryProcessors[0];
|
||||
@@ -340,118 +182,7 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const resolveArg =
|
||||
(index: number) =>
|
||||
(...args: any[]) =>
|
||||
Promise.resolve(args[index]);
|
||||
|
||||
it("should request permission if popup has never been seen before", async () => {
|
||||
showInformationMessageSpy.mockImplementation(
|
||||
resolveArg(3 /* "yes" item */),
|
||||
);
|
||||
await ctx.globalState.update("telemetry-request-viewed", false);
|
||||
expect(env.isTelemetryEnabled).toBe(true);
|
||||
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
|
||||
await telemetryListener.initialize();
|
||||
|
||||
// Wait for user's selection to propagate in settings.
|
||||
await wait(500);
|
||||
|
||||
// Dialog opened, user clicks "yes" and telemetry enabled
|
||||
expect(showInformationMessageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(ENABLE_TELEMETRY.getValue()).toBe(true);
|
||||
expect(ctx.globalState.get("telemetry-request-viewed")).toBe(true);
|
||||
});
|
||||
|
||||
it("should prevent telemetry if permission is denied", async () => {
|
||||
showInformationMessageSpy.mockImplementation(resolveArg(4 /* "no" item */));
|
||||
await ctx.globalState.update("telemetry-request-viewed", false);
|
||||
await enableTelemetry("codeQL.telemetry", true);
|
||||
|
||||
await telemetryListener.initialize();
|
||||
|
||||
// Dialog opened, user clicks "no" and telemetry disabled
|
||||
expect(showInformationMessageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(ENABLE_TELEMETRY.getValue()).toBe(false);
|
||||
expect(ctx.globalState.get("telemetry-request-viewed")).toBe(true);
|
||||
});
|
||||
|
||||
it("should unchange telemetry if permission dialog is dismissed", async () => {
|
||||
showInformationMessageSpy.mockResolvedValue(undefined /* cancelled */);
|
||||
await ctx.globalState.update("telemetry-request-viewed", false);
|
||||
|
||||
// this causes requestTelemetryPermission to be called
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
|
||||
// Dialog opened, and user closes without interacting with it
|
||||
expect(showInformationMessageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(ENABLE_TELEMETRY.getValue()).toBe(false);
|
||||
// dialog was canceled, so should not have marked as viewed
|
||||
expect(ctx.globalState.get("telemetry-request-viewed")).toBe(false);
|
||||
});
|
||||
|
||||
it("should unchange telemetry if permission dialog is cancelled if starting as true", async () => {
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
|
||||
// as before, except start with telemetry enabled. It should _stay_ enabled if the
|
||||
// dialog is canceled.
|
||||
showInformationMessageSpy.mockResolvedValue(undefined /* cancelled */);
|
||||
await ctx.globalState.update("telemetry-request-viewed", false);
|
||||
|
||||
// this causes requestTelemetryPermission to be called
|
||||
await enableTelemetry("codeQL.telemetry", true);
|
||||
|
||||
// Dialog opened, and user closes without interacting with it
|
||||
// Telemetry state should not have changed
|
||||
expect(showInformationMessageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(ENABLE_TELEMETRY.getValue()).toBe(true);
|
||||
// dialog was canceled, so should not have marked as viewed
|
||||
expect(ctx.globalState.get("telemetry-request-viewed")).toBe(false);
|
||||
});
|
||||
|
||||
it("should avoid showing dialog if global telemetry is disabled", async () => {
|
||||
// when telemetry is disabled globally, we never want to show the
|
||||
// opt in/out dialog. We just assume that codeql telemetry should
|
||||
// remain disabled as well.
|
||||
// If the user ever turns global telemetry back on, then we can
|
||||
// show the dialog.
|
||||
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await setTelemetryLevel("telemetry", "off");
|
||||
await ctx.globalState.update("telemetry-request-viewed", false);
|
||||
|
||||
await telemetryListener.initialize();
|
||||
|
||||
// popup should not be shown even though we have initialized telemetry
|
||||
expect(showInformationMessageSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// This test is failing because codeQL.canary is not a registered configuration.
|
||||
// We do not want to have it registered because we don't want this item
|
||||
// appearing in the settings page. It needs to only be set by users we tell
|
||||
// about it to.
|
||||
// At this point, I see no other way of testing re-requesting permission.
|
||||
xit("should request permission again when user changes canary setting", async () => {
|
||||
// initially, both canary and telemetry are false
|
||||
await workspace.getConfiguration().update("codeQL.canary", false);
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
await ctx.globalState.update("telemetry-request-viewed", true);
|
||||
await telemetryListener.initialize();
|
||||
showInformationMessageSpy.mockResolvedValue(undefined /* cancelled */);
|
||||
|
||||
// set canary to true
|
||||
await workspace.getConfiguration().update("codeQL.canary", true);
|
||||
|
||||
// now, we should have to click through the telemetry requestor again
|
||||
expect(ctx.globalState.get("telemetry-request-viewed")).toBe(false);
|
||||
expect(showInformationMessageSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should send a ui-interaction telemetry event", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendUIInteraction("test");
|
||||
|
||||
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
|
||||
@@ -467,8 +198,6 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should send a ui-interaction telemetry event with a cli version", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.cliVersion = new SemVer("1.2.3");
|
||||
telemetryListener.sendUIInteraction("test");
|
||||
|
||||
@@ -485,8 +214,6 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should send an error telemetry event", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendError(redactableError`test`);
|
||||
|
||||
expect(sendTelemetryEventSpy).not.toHaveBeenCalled();
|
||||
@@ -503,7 +230,6 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should send an error telemetry event with a cli version", async () => {
|
||||
await telemetryListener.initialize();
|
||||
telemetryListener.cliVersion = new SemVer("1.2.3");
|
||||
|
||||
telemetryListener.sendError(redactableError`test`);
|
||||
@@ -522,8 +248,6 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should redact error message contents", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendError(
|
||||
redactableError`test message with secret information: ${42} and more ${"secret"} parts`,
|
||||
);
|
||||
@@ -543,8 +267,6 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should send config telemetry event", async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendConfigInformation({
|
||||
testKey: "testValue",
|
||||
testKey2: "42",
|
||||
@@ -563,26 +285,6 @@ describe("telemetry reporting", () => {
|
||||
expect(sendTelemetryErrorEventSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
async function enableTelemetry(section: string, value: boolean | undefined) {
|
||||
await workspace
|
||||
.getConfiguration(section)
|
||||
.update("enableTelemetry", value, ConfigurationTarget.Global);
|
||||
|
||||
// Need to wait some time since the onDidChangeConfiguration listeners fire
|
||||
// asynchronously. Must ensure they to complete in order to have a successful test.
|
||||
await wait(100);
|
||||
}
|
||||
|
||||
async function setTelemetryLevel(section: string, value: string | undefined) {
|
||||
await workspace
|
||||
.getConfiguration(section)
|
||||
.update("telemetryLevel", value, ConfigurationTarget.Global);
|
||||
|
||||
// Need to wait some time since the onDidChangeConfiguration listeners fire
|
||||
// asynchronously. Must ensure they to complete in order to have a successful test.
|
||||
await wait(100);
|
||||
}
|
||||
|
||||
async function wait(ms = 0) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user