Skip to content

Commit fa4766f

Browse files
Merge pull request #1599 from github/elenatanasoiu/add-batching-to-download-take-two
Download variant analysis results in batches - take two
2 parents 28eb9ea + 25b71e8 commit fa4766f

File tree

7 files changed

+160
-21
lines changed

7 files changed

+160
-21
lines changed

extensions/ql-vscode/package-lock.json

Lines changed: 36 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,7 @@
12301230
"minimist": "~1.2.6",
12311231
"nanoid": "^3.2.0",
12321232
"node-fetch": "~2.6.7",
1233+
"p-queue": "^6.0.0",
12331234
"path-browserify": "^1.0.1",
12341235
"react": "^17.0.2",
12351236
"react-dom": "^17.0.2",

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,7 @@ async function activateWithInstalledDistribution(
951951
variantAnalysisSummary: VariantAnalysisApiResponse,
952952
token: CancellationToken
953953
) => {
954-
await variantAnalysisManager.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysisSummary, token);
954+
await variantAnalysisManager.enqueueDownload(scannedRepo, variantAnalysisSummary, token);
955955
})
956956
);
957957

extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { VariantAnalysisResultsManager } from './variant-analysis-results-manage
2222
import { CodeQLCliServer } from '../cli';
2323
import { getControllerRepo } from './run-remote-query';
2424
import { processUpdatedVariantAnalysis } from './variant-analysis-processor';
25+
import PQueue from 'p-queue';
2526

2627
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
2728
private readonly _onVariantAnalysisAdded = this.push(new EventEmitter<VariantAnalysis>());
@@ -31,6 +32,8 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
3132
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager;
3233
private readonly variantAnalyses = new Map<number, VariantAnalysis>();
3334
private readonly views = new Map<number, VariantAnalysisView>();
35+
private static readonly maxConcurrentDownloads = 3;
36+
private readonly queue = new PQueue({ concurrency: VariantAnalysisManager.maxConcurrentDownloads });
3437

3538
constructor(
3639
private readonly ctx: ExtensionContext,
@@ -161,6 +164,18 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
161164
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
162165
}
163166

167+
public async enqueueDownload(
168+
scannedRepo: ApiVariantAnalysisScannedRepository,
169+
variantAnalysisSummary: VariantAnalysisApiResponse,
170+
token: CancellationToken
171+
): Promise<void> {
172+
await this.queue.add(() => this.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysisSummary, token));
173+
}
174+
175+
public downloadsQueueSize(): number {
176+
return this.queue.pending;
177+
}
178+
164179
public async promptOpenVariantAnalysis() {
165180
const credentials = await Credentials.initialize(this.ctx);
166181
if (!credentials) { throw Error('Error authenticating with GitHub'); }

extensions/ql-vscode/src/remote-queries/variant-analysis-monitor.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import * as ghApiClient from './gh-api/gh-api-client';
44

55
import { VariantAnalysis, VariantAnalysisStatus } from './shared/variant-analysis';
66
import {
7-
VariantAnalysis as VariantAnalysisApiResponse
7+
VariantAnalysis as VariantAnalysisApiResponse,
8+
VariantAnalysisScannedRepository
89
} from './gh-api/variant-analysis';
910
import { VariantAnalysisMonitorResult } from './shared/variant-analysis-monitor-result';
1011
import { processFailureReason, processUpdatedVariantAnalysis } from './variant-analysis-processor';
@@ -71,14 +72,8 @@ export class VariantAnalysisMonitor extends DisposableObject {
7172

7273
this._onVariantAnalysisChange.fire(variantAnalysis);
7374

74-
if (variantAnalysisSummary.scanned_repositories) {
75-
variantAnalysisSummary.scanned_repositories.forEach(scannedRepo => {
76-
if (!scannedReposDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded') {
77-
void commands.executeCommand('codeQL.autoDownloadVariantAnalysisResult', scannedRepo, variantAnalysisSummary);
78-
scannedReposDownloaded.push(scannedRepo.repository.id);
79-
}
80-
});
81-
}
75+
const downloadedRepos = this.downloadVariantAnalysisResults(variantAnalysisSummary, scannedReposDownloaded);
76+
scannedReposDownloaded.push(...downloadedRepos);
8277

8378
if (variantAnalysisSummary.status === 'completed') {
8479
break;
@@ -90,6 +85,46 @@ export class VariantAnalysisMonitor extends DisposableObject {
9085
return { status: 'CompletedSuccessfully', scannedReposDownloaded: scannedReposDownloaded };
9186
}
9287

88+
private scheduleForDownload(
89+
scannedRepo: VariantAnalysisScannedRepository,
90+
variantAnalysisSummary: VariantAnalysisApiResponse
91+
) {
92+
void commands.executeCommand('codeQL.autoDownloadVariantAnalysisResult', scannedRepo, variantAnalysisSummary);
93+
}
94+
95+
private shouldDownload(
96+
scannedRepo: VariantAnalysisScannedRepository,
97+
alreadyDownloaded: number[]
98+
): boolean {
99+
return !alreadyDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded';
100+
}
101+
102+
private getReposToDownload(
103+
variantAnalysisSummary: VariantAnalysisApiResponse,
104+
alreadyDownloaded: number[]
105+
): VariantAnalysisScannedRepository[] {
106+
if (variantAnalysisSummary.scanned_repositories) {
107+
return variantAnalysisSummary.scanned_repositories.filter(scannedRepo => this.shouldDownload(scannedRepo, alreadyDownloaded));
108+
} else {
109+
return [];
110+
}
111+
}
112+
113+
private downloadVariantAnalysisResults(
114+
variantAnalysisSummary: VariantAnalysisApiResponse,
115+
scannedReposDownloaded: number[]
116+
): number[] {
117+
const repoResultsToDownload = this.getReposToDownload(variantAnalysisSummary, scannedReposDownloaded);
118+
const downloadedRepos: number[] = [];
119+
120+
repoResultsToDownload.forEach(scannedRepo => {
121+
downloadedRepos.push(scannedRepo.repository.id);
122+
this.scheduleForDownload(scannedRepo, variantAnalysisSummary);
123+
});
124+
125+
return downloadedRepos;
126+
}
127+
93128
private async sleep(ms: number) {
94129
return new Promise(resolve => setTimeout(resolve, ms));
95130
}

extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,17 @@ describe('Variant Analysis Manager', async function() {
149149

150150
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
151151
});
152+
153+
it('should pop download tasks off the queue', async () => {
154+
const getResultsSpy = sandbox.spy(variantAnalysisManager, 'autoDownloadVariantAnalysisResult');
155+
156+
await variantAnalysisManager.enqueueDownload(scannedRepos[0], variantAnalysis, cancellationTokenSource.token);
157+
await variantAnalysisManager.enqueueDownload(scannedRepos[1], variantAnalysis, cancellationTokenSource.token);
158+
await variantAnalysisManager.enqueueDownload(scannedRepos[2], variantAnalysis, cancellationTokenSource.token);
159+
160+
expect(variantAnalysisManager.downloadsQueueSize()).to.equal(0);
161+
expect(getResultsSpy).to.have.been.calledThrice;
162+
});
152163
});
153164
});
154165
});

extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-monitor.test.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,35 @@
11
import * as sinon from 'sinon';
22
import { expect } from 'chai';
3-
import { CancellationTokenSource, extensions } from 'vscode';
3+
import { CancellationTokenSource, commands, extensions } from 'vscode';
44
import { CodeQLExtensionInterface } from '../../../extension';
55
import * as config from '../../../config';
66

77
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
88
import { VariantAnalysisMonitor } from '../../../remote-queries/variant-analysis-monitor';
99
import {
1010
VariantAnalysis as VariantAnalysisApiResponse,
11+
VariantAnalysisFailureReason,
1112
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
12-
VariantAnalysisFailureReason
1313
} from '../../../remote-queries/gh-api/variant-analysis';
1414
import { createFailedMockApiResponse, createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
1515
import { VariantAnalysis, VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
1616
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
1717
import { processFailureReason } from '../../../remote-queries/variant-analysis-processor';
1818
import { Credentials } from '../../../authentication';
1919
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
20+
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
2021

2122
describe('Variant Analysis Monitor', async function() {
2223
this.timeout(60000);
2324

2425
let sandbox: sinon.SinonSandbox;
26+
let extension: CodeQLExtensionInterface | Record<string, never>;
2527
let mockGetVariantAnalysis: sinon.SinonStub;
2628
let cancellationTokenSource: CancellationTokenSource;
2729
let variantAnalysisMonitor: VariantAnalysisMonitor;
2830
let variantAnalysis: VariantAnalysis;
31+
let variantAnalysisManager: VariantAnalysisManager;
32+
let mockGetDownloadResult: sinon.SinonStub;
2933

3034
beforeEach(async () => {
3135
sandbox = sinon.createSandbox();
@@ -36,12 +40,15 @@ describe('Variant Analysis Monitor', async function() {
3640
variantAnalysis = createMockVariantAnalysis();
3741

3842
try {
39-
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
43+
extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
4044
variantAnalysisMonitor = new VariantAnalysisMonitor(extension.ctx);
4145
} catch (e) {
4246
fail(e as Error);
4347
}
4448

49+
variantAnalysisManager = extension.variantAnalysisManager;
50+
mockGetDownloadResult = sandbox.stub(variantAnalysisManager, 'autoDownloadVariantAnalysisResult');
51+
4552
limitNumberOfAttemptsToMonitor();
4653
});
4754

@@ -110,20 +117,47 @@ describe('Variant Analysis Monitor', async function() {
110117
describe('when the variant analysis is in progress', async () => {
111118
let mockApiResponse: VariantAnalysisApiResponse;
112119
let scannedRepos: ApiVariantAnalysisScannedRepository[];
120+
let succeededRepos: ApiVariantAnalysisScannedRepository[];
113121

114122
describe('when there are successfully scanned repos', async () => {
115123
beforeEach(async function() {
116-
scannedRepos = createMockScannedRepos(['pending', 'in_progress', 'succeeded']);
124+
scannedRepos = createMockScannedRepos(['pending', 'pending', 'in_progress', 'in_progress', 'succeeded', 'succeeded', 'succeeded']);
117125
mockApiResponse = createMockApiResponse('completed', scannedRepos);
118126
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
127+
succeededRepos = scannedRepos.filter(r => r.analysis_status === 'succeeded');
119128
});
120129

121130
it('should succeed and return a list of scanned repo ids', async () => {
122131
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
123-
const scannedRepoIds = scannedRepos.filter(r => r.analysis_status == 'succeeded').map(r => r.repository.id);
124132

125133
expect(result.status).to.equal('CompletedSuccessfully');
126-
expect(result.scannedReposDownloaded).to.eql(scannedRepoIds);
134+
expect(result.scannedReposDownloaded).to.eql(succeededRepos.map(r => r.repository.id));
135+
});
136+
137+
it('should trigger a download extension command for each repo', async () => {
138+
const succeededRepos = scannedRepos.filter(r => r.analysis_status === 'succeeded');
139+
const commandSpy = sandbox.spy(commands, 'executeCommand');
140+
141+
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
142+
143+
expect(commandSpy).to.have.callCount(succeededRepos.length);
144+
145+
succeededRepos.forEach((succeededRepo, index) => {
146+
expect(commandSpy.getCall(index).args[0]).to.eq('codeQL.autoDownloadVariantAnalysisResult');
147+
expect(commandSpy.getCall(index).args[1]).to.eq(succeededRepo);
148+
expect(commandSpy.getCall(index).args[2]).to.eq(mockApiResponse);
149+
});
150+
});
151+
152+
it('should download all available results', async () => {
153+
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
154+
155+
expect(mockGetDownloadResult).to.have.callCount(succeededRepos.length);
156+
157+
succeededRepos.forEach((succeededRepo, index) => {
158+
expect(mockGetDownloadResult.getCall(index).args[0]).to.eq(succeededRepo);
159+
expect(mockGetDownloadResult.getCall(index).args[1]).to.eq(mockApiResponse);
160+
});
127161
});
128162
});
129163

@@ -142,6 +176,12 @@ describe('Variant Analysis Monitor', async function() {
142176
expect(result.status).to.equal('CompletedSuccessfully');
143177
expect(result.scannedReposDownloaded).to.eql([]);
144178
});
179+
180+
it('should not try to download any repos', async () => {
181+
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
182+
183+
expect(mockGetDownloadResult).to.not.have.been.called;
184+
});
145185
});
146186

147187
describe('when there are no repos to scan', async () => {
@@ -157,6 +197,12 @@ describe('Variant Analysis Monitor', async function() {
157197
expect(result.status).to.equal('CompletedSuccessfully');
158198
expect(result.scannedReposDownloaded).to.eql([]);
159199
});
200+
201+
it('should not try to download any repos', async () => {
202+
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
203+
204+
expect(mockGetDownloadResult).to.not.have.been.called;
205+
});
160206
});
161207
});
162208
});

0 commit comments

Comments
 (0)