Skip to content

Commit 16d19c1

Browse files
authored
test(performance): make tests more deterministic by relying more on system counts (#5786)
## Problem Performance tests are currently flaky, making them a poor signal for performance regressions within the code base. Additionally, false alarms slow down development of unrelated features as test failures keep CI from being "green". Therefore, rather than relying only on system usage thresholds for performance regressions, we can count the number of high-risk / potentially slow operations made by the code as a deterministic measure of its performance. However, directly counting each individual potentially slow operation used within the performance tests highly couples the test to the implementation details, making the tests less effective if the implementation changes. Therefore, the goal of this PR is the following: 1. decrease performance test flakiness by increasing thresholds. 2. increase performance test effectiveness by relying on deterministic measures. 3. avoid coupling the tests to the implementation details. ## Solution To meet goal (1), we increase the thresholds of the tests to decrease the changes of a false alarm. To meet goal (2), we count expensive operations. But, to avoid tying it to the implementation details, we count the expensive operations using somewhat-loose upper bounds. Thus, implementation changes modifying the exact number of expensive operations by a small constant do not set a false alarm. However, if they increase the number of expensive operations by a multiplicative factor, the upper bound will alert us. As an example, we don't want the test to fail if it makes 5-10 more file system calls when working with a few hundred files, but we do want the test to fail if it makes 2x the number of files system calls. Therefore, in the code the bounds are often described as "operations per file", since it is the multiplicative increase we are concerned about. This allows us to achieve goal (3). ### Implementation Details - The most common "expensive operation" we count is file system calls (through our `fs` module). Some other examples include the use of `zip` libraries or loading large files into memory. - We separate the upper bounds for the file system into `read` and `write` bounds. This granularity allows us to assert that specific code paths do not modify any files. ## Notes - `AdmZip` removed in #4769 , so we don't address spying on it here. Once `zip.js` is implemented, we can spy on it in a follow up. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 1ca1501 commit 16d19c1

File tree

15 files changed

+333
-244
lines changed

15 files changed

+333
-244
lines changed

packages/core/src/amazonqFeatureDev/util/files.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { AmazonqCreateUpload, Span, telemetry as amznTelemetry } from '../../sha
1818
import { TelemetryHelper } from './telemetryHelper'
1919
import { maxRepoSizeBytes } from '../constants'
2020
import { isCodeFile } from '../../shared/filetypes'
21+
import { fs } from '../../shared'
2122

2223
const getSha256 = (file: Buffer) => createHash('sha256').update(file).digest('base64')
2324

@@ -28,17 +29,17 @@ export async function prepareRepoData(
2829
repoRootPaths: string[],
2930
workspaceFolders: CurrentWsFolders,
3031
telemetry: TelemetryHelper,
31-
span: Span<AmazonqCreateUpload>
32+
span: Span<AmazonqCreateUpload>,
33+
zip: AdmZip = new AdmZip()
3234
) {
3335
try {
3436
const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes)
35-
const zip = new AdmZip()
3637

3738
let totalBytes = 0
3839
const ignoredExtensionMap = new Map<string, number>()
3940

4041
for (const file of files) {
41-
const fileSize = (await vscode.workspace.fs.stat(file.fileUri)).size
42+
const fileSize = (await fs.stat(file.fileUri)).size
4243
const isCodeFile_ = isCodeFile(file.relativeFilePath)
4344

4445
if (fileSize >= maxFileSizeBytes || !isCodeFile_) {

packages/core/src/codewhisperer/commands/startSecurityScan.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ export async function startSecurityScan(
9393
editor: vscode.TextEditor | undefined,
9494
client: DefaultCodeWhispererClient,
9595
context: vscode.ExtensionContext,
96-
scope: CodeWhispererConstants.CodeAnalysisScope
96+
scope: CodeWhispererConstants.CodeAnalysisScope,
97+
zipUtil: ZipUtil = new ZipUtil()
9798
) {
9899
const logger = getLoggerForScope(scope)
99100
/**
@@ -130,7 +131,6 @@ export async function startSecurityScan(
130131
* Step 1: Generate zip
131132
*/
132133
throwIfCancelled(scope, codeScanStartTime)
133-
const zipUtil = new ZipUtil()
134134
const zipMetadata = await zipUtil.generateZip(editor?.document.uri, scope)
135135
const projectPaths = zipUtil.getProjectPaths()
136136

packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,15 @@ interface ZipCodeResult {
287287
fileSize: number
288288
}
289289

290-
export async function zipCode({ dependenciesFolder, humanInTheLoopFlag, modulePath, zipManifest }: IZipCodeParams) {
290+
export async function zipCode(
291+
{ dependenciesFolder, humanInTheLoopFlag, modulePath, zipManifest }: IZipCodeParams,
292+
zip: AdmZip = new AdmZip()
293+
) {
291294
let tempFilePath = undefined
292295
let logFilePath = undefined
293296
let dependenciesCopied = false
294297
try {
295298
throwIfCancelled()
296-
const zip = new AdmZip()
297299

298300
// If no modulePath is passed in, we are not uploaded the source folder
299301
// NOTE: We only upload dependencies for human in the loop work

packages/core/src/shared/performance/performance.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,11 @@ function assertPerformanceMetrics(
206206
`Expected total duration for ${name} to be less than ${expectedDuration}. Actual duration was ${foundDuration}`
207207
)
208208
}
209+
210+
export function getEqualOSTestOptions(testOptions: Partial<PerformanceMetrics>): Partial<TestOptions> {
211+
return {
212+
linux: testOptions,
213+
darwin: testOptions,
214+
win32: testOptions,
215+
}
216+
}

packages/core/src/shared/utilities/workspaceUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ export async function collectFilesForIndex(
582582
continue
583583
}
584584

585-
const fileStat = await vscode.workspace.fs.stat(file)
585+
const fileStat = await fs.stat(file)
586586
// ignore single file over 10 MB
587587
if (fileStat.size > 10 * 1024 * 1024) {
588588
continue

packages/core/src/testInteg/perf/buildIndex.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,36 @@ import { LspClient, LspController } from '../../amazonq'
1111
import { LanguageClient, ServerOptions } from 'vscode-languageclient'
1212
import { createTestWorkspace } from '../../test/testUtil'
1313
import { GetUsageRequestType, IndexRequestType } from '../../amazonq/lsp/types'
14-
import { getRandomString } from '../../shared'
14+
import { fs, getRandomString } from '../../shared'
15+
import { FileSystem } from '../../shared/fs/fs'
16+
import { getFsCallsUpperBound } from './utilities'
1517

1618
interface SetupResult {
1719
clientReqStub: sinon.SinonStub
20+
fsSpy: sinon.SinonSpiedInstance<FileSystem>
21+
findFilesSpy: sinon.SinonSpy
1822
}
1923

2024
async function verifyResult(setup: SetupResult) {
2125
assert.ok(setup.clientReqStub.calledTwice)
2226
assert.ok(setup.clientReqStub.firstCall.calledWith(IndexRequestType))
2327
assert.ok(setup.clientReqStub.secondCall.calledWith(GetUsageRequestType))
28+
29+
assert.strictEqual(getFsCallsUpperBound(setup.fsSpy), 0, 'should not make any fs calls')
30+
assert.ok(setup.findFilesSpy.callCount <= 2, 'findFiles should not be called more than twice')
2431
}
2532

2633
async function setupWithWorkspace(numFiles: number, options: { fileContent: string }): Promise<SetupResult> {
2734
// Force VSCode to find my test workspace only to keep test contained and controlled.
2835
const testWorksapce = await createTestWorkspace(numFiles, options)
2936
sinon.stub(vscode.workspace, 'workspaceFolders').value([testWorksapce])
37+
3038
// Avoid sending real request to lsp.
3139
const clientReqStub = sinon.stub(LanguageClient.prototype, 'sendRequest').resolves(true)
40+
const fsSpy = sinon.spy(fs)
41+
const findFilesSpy = sinon.spy(vscode.workspace, 'findFiles')
3242
LspClient.instance.client = new LanguageClient('amazonq', 'test-client', {} as ServerOptions, {})
33-
return { clientReqStub }
43+
return { clientReqStub, fsSpy, findFilesSpy }
3444
}
3545

3646
describe('buildIndex', function () {

packages/core/src/testInteg/perf/collectFiles.test.ts

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,80 @@
55
import assert from 'assert'
66
import * as vscode from 'vscode'
77
import * as sinon from 'sinon'
8-
import { performanceTest } from '../../shared/performance/performance'
8+
import { getEqualOSTestOptions, performanceTest } from '../../shared/performance/performance'
99
import { createTestWorkspaceFolder, toFile } from '../../test/testUtil'
1010
import path from 'path'
11-
import { randomUUID } from '../../shared'
11+
import { fs, randomUUID } from '../../shared'
1212
import { collectFiles } from '../../shared/utilities/workspaceUtils'
13+
import { getFsCallsUpperBound } from './utilities'
14+
import { FileSystem } from '../../shared/fs/fs'
1315

14-
performanceTest(
15-
// collecting all files in the workspace and zipping them is pretty resource intensive
16-
{
17-
linux: {
18-
userCpuUsage: 85,
16+
function performanceTestWrapper(totalFiles: number) {
17+
return performanceTest(
18+
getEqualOSTestOptions({
19+
userCpuUsage: 100,
20+
systemCpuUsage: 35,
1921
heapTotal: 2,
20-
duration: 0.8,
21-
},
22-
},
23-
'calculate cpu and memory usage',
24-
function () {
25-
const totalFiles = 100
26-
return {
27-
setup: async () => {
28-
const workspace = await createTestWorkspaceFolder()
22+
}),
23+
'calculate cpu and memory usage',
24+
function () {
25+
return {
26+
setup: async () => {
27+
const workspace = await createTestWorkspaceFolder()
2928

30-
sinon.stub(vscode.workspace, 'workspaceFolders').value([workspace])
29+
sinon.stub(vscode.workspace, 'workspaceFolders').value([workspace])
30+
const fsSpy = sinon.spy(fs)
31+
const findFilesSpy = sinon.spy(vscode.workspace, 'findFiles')
32+
const fileContent = randomUUID()
33+
for (let x = 0; x < totalFiles; x++) {
34+
await toFile(fileContent, path.join(workspace.uri.fsPath, `file.${x}`))
35+
}
3136

32-
const fileContent = randomUUID()
33-
for (let x = 0; x < totalFiles; x++) {
34-
await toFile(fileContent, path.join(workspace.uri.fsPath, `file.${x}`))
35-
}
37+
return {
38+
workspace,
39+
fsSpy,
40+
findFilesSpy,
41+
}
42+
},
43+
execute: async ({ workspace }: { workspace: vscode.WorkspaceFolder }) => {
44+
return {
45+
result: await collectFiles([workspace.uri.fsPath], [workspace], true),
46+
}
47+
},
48+
verify: (
49+
setup: {
50+
workspace: vscode.WorkspaceFolder
51+
fsSpy: sinon.SinonSpiedInstance<FileSystem>
52+
findFilesSpy: sinon.SinonSpy
53+
},
54+
{ result }: { result: Awaited<ReturnType<typeof collectFiles>> }
55+
) => {
56+
assert.deepStrictEqual(result.length, totalFiles)
57+
const sortedFiles = [...result].sort((a, b) => {
58+
const numA = parseInt(a.relativeFilePath.split('.')[1])
59+
const numB = parseInt(b.relativeFilePath.split('.')[1])
60+
return numA - numB
61+
})
62+
for (let x = 0; x < totalFiles; x++) {
63+
assert.deepStrictEqual(sortedFiles[x].relativeFilePath, `file.${x}`)
64+
}
3665

37-
return {
38-
workspace,
39-
}
40-
},
41-
execute: async ({ workspace }: { workspace: vscode.WorkspaceFolder }) => {
42-
return {
43-
result: await collectFiles([workspace.uri.fsPath], [workspace], true),
44-
}
45-
},
46-
verify: (
47-
_: { workspace: vscode.WorkspaceFolder },
48-
{ result }: { result: Awaited<ReturnType<typeof collectFiles>> }
49-
) => {
50-
assert.deepStrictEqual(result.length, totalFiles)
51-
const sortedFiles = [...result].sort((a, b) => {
52-
const numA = parseInt(a.relativeFilePath.split('.')[1])
53-
const numB = parseInt(b.relativeFilePath.split('.')[1])
54-
return numA - numB
55-
})
56-
for (let x = 0; x < totalFiles; x++) {
57-
assert.deepStrictEqual(sortedFiles[x].relativeFilePath, `file.${x}`)
58-
}
59-
},
66+
assert.ok(
67+
getFsCallsUpperBound(setup.fsSpy) <= totalFiles * 5,
68+
'total system calls below 5 per file'
69+
)
70+
assert.ok(setup.findFilesSpy.callCount <= 2, 'findFiles not called more than twice')
71+
},
72+
}
6073
}
61-
}
62-
)
74+
)
75+
}
76+
77+
describe('collectFiles', function () {
78+
afterEach(function () {
79+
sinon.restore()
80+
})
81+
performanceTestWrapper(10)
82+
performanceTestWrapper(100)
83+
performanceTestWrapper(250)
84+
})

packages/core/src/testInteg/perf/downloadExportResultArchive.test.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as sinon from 'sinon'
99
import path from 'path'
1010
import { fs, getRandomString } from '../../shared'
1111
import { createTestWorkspace } from '../../test/testUtil'
12-
import { performanceTest } from '../../shared/performance/performance'
12+
import { getEqualOSTestOptions, performanceTest } from '../../shared/performance/performance'
1313
import { downloadExportResultArchive } from '../../shared/utilities/download'
1414

1515
interface SetupResult {
@@ -47,24 +47,11 @@ async function setup(pieces: number, pieceSize: number): Promise<SetupResult> {
4747

4848
function perfTest(pieces: number, pieceSize: number, label: string) {
4949
return performanceTest(
50-
{
51-
testRuns: 10,
52-
linux: {
53-
userCpuUsage: 200,
54-
systemCpuUsage: 35,
55-
heapTotal: 4,
56-
},
57-
darwin: {
58-
userCpuUsage: 200,
59-
systemCpuUsage: 35,
60-
heapTotal: 4,
61-
},
62-
win32: {
63-
userCpuUsage: 200,
64-
systemCpuUsage: 35,
65-
heapTotal: 4,
66-
},
67-
},
50+
getEqualOSTestOptions({
51+
userCpuUsage: 200,
52+
systemCpuUsage: 35,
53+
heapTotal: 4,
54+
}),
6855
label,
6956
function () {
7057
return {

packages/core/src/testInteg/perf/getFileSha384.test.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,26 @@
44
*/
55
import assert from 'assert'
66
import path from 'path'
7+
import sinon from 'sinon'
78
import { getTestWorkspaceFolder } from '../integrationTestsUtilities'
89
import { fs, getRandomString } from '../../shared'
910
import { LspController } from '../../amazonq'
10-
import { performanceTest } from '../../shared/performance/performance'
11+
import { getEqualOSTestOptions, performanceTest } from '../../shared/performance/performance'
12+
import { FileSystem } from '../../shared/fs/fs'
13+
import { getFsCallsUpperBound } from './utilities'
14+
15+
interface SetupResult {
16+
testFile: string
17+
fsSpy: sinon.SinonSpiedInstance<FileSystem>
18+
}
1119

1220
function performanceTestWrapper(label: string, fileSize: number) {
1321
return performanceTest(
14-
{
15-
testRuns: 1,
16-
linux: {
17-
userCpuUsage: 400,
18-
systemCpuUsage: 35,
19-
heapTotal: 4,
20-
},
21-
darwin: {
22-
userCpuUsage: 400,
23-
systemCpuUsage: 35,
24-
heapTotal: 4,
25-
},
26-
win32: {
27-
userCpuUsage: 400,
28-
systemCpuUsage: 35,
29-
heapTotal: 4,
30-
},
31-
},
22+
getEqualOSTestOptions({
23+
userCpuUsage: 400,
24+
systemCpuUsage: 35,
25+
heapTotal: 4,
26+
}),
3227
label,
3328
function () {
3429
return {
@@ -37,14 +32,15 @@ function performanceTestWrapper(label: string, fileSize: number) {
3732
const fileContent = getRandomString(fileSize)
3833
const testFile = path.join(workspace, 'test-file')
3934
await fs.writeFile(testFile, fileContent)
40-
41-
return testFile
35+
const fsSpy = sinon.spy(fs)
36+
return { testFile, fsSpy }
4237
},
43-
execute: async (testFile: string) => {
44-
return await LspController.instance.getFileSha384(testFile)
38+
execute: async (setup: SetupResult) => {
39+
return await LspController.instance.getFileSha384(setup.testFile)
4540
},
46-
verify: async (_testFile: string, result: string) => {
41+
verify: async (setup: SetupResult, result: string) => {
4742
assert.strictEqual(result.length, 96)
43+
assert.ok(getFsCallsUpperBound(setup.fsSpy) <= 1, 'makes a single call to fs')
4844
},
4945
}
5046
}
@@ -53,6 +49,9 @@ function performanceTestWrapper(label: string, fileSize: number) {
5349

5450
describe('getFileSha384', function () {
5551
describe('performance tests', function () {
52+
afterEach(function () {
53+
sinon.restore()
54+
})
5655
performanceTestWrapper('1MB', 1000)
5756
performanceTestWrapper('2MB', 2000)
5857
performanceTestWrapper('4MB', 4000)

0 commit comments

Comments
 (0)