Skip to content

Commit 89e6216

Browse files
authored
Add Tests to measure activation times of extension (microsoft#1813)
* Tests to measure activation times of extension * Code review fixes * Run performance tests on master
1 parent c6b374e commit 89e6216

File tree

13 files changed

+819
-21
lines changed

13 files changed

+819
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ analysis/**
1919
bin/**
2020
obj/**
2121
.pytest_cache
22+
tmp/**

.travis.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ matrix:
2727
- os: linux
2828
python: "3.6-dev"
2929
env: MULTIROOT_WORKSPACE_TEST=true
30+
- os: linux
31+
python: "3.6-dev"
32+
env: PERFORMANCE_TEST=true
3033
allow_failures:
3134
- os: linux
3235
python: "2.7"
@@ -111,6 +114,11 @@ script:
111114
- if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then
112115
bash <(curl -s https://codecov.io/bash);
113116
fi
117+
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" && "$PERFORMANCE_TEST" == "true" ]]; then
118+
yarn run clean;
119+
yarn run vscode:prepublish;
120+
yarn run testPerformance --silent;
121+
fi
114122
- if [ "$TRAVIS_PYTHON_VERSION" != "2.7" ]; then
115123
python3 -m pip install --upgrade -r news/requirements.txt;
116124
python3 news/announce.py --dry_run;

.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ requirements.txt
4545
scripts/**
4646
src/**
4747
test/**
48+
tmp/**
4849
typings/**
4950
vsc-extension-quickstart.md

news/3 Code Health/932.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Create tests to measure activation times for the extension.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,7 @@
18531853
"testSingleWorkspace": "node ./out/test/standardTest.js",
18541854
"testMultiWorkspace": "node ./out/test/multiRootTest.js",
18551855
"testAnalysisEngine": "node ./out/test/analysisEngineTest.js",
1856+
"testPerformance": "node ./out/test/performanceTest.js",
18561857
"precommit": "node gulpfile.js",
18571858
"lint-staged": "node gulpfile.js",
18581859
"lint": "tslint src/**/*.ts -t verbose",
@@ -1903,6 +1904,7 @@
19031904
"@types/chai-arrays": "^1.0.2",
19041905
"@types/chai-as-promised": "^7.1.0",
19051906
"@types/del": "^3.0.0",
1907+
"@types/download": "^6.2.2",
19061908
"@types/dotenv": "^4.0.3",
19071909
"@types/event-stream": "^3.3.33",
19081910
"@types/fs-extra": "^5.0.1",
@@ -1914,6 +1916,7 @@
19141916
"@types/md5": "^2.1.32",
19151917
"@types/mocha": "^2.2.48",
19161918
"@types/node": "^9.4.7",
1919+
"@types/request": "^2.47.0",
19171920
"@types/semver": "^5.5.0",
19181921
"@types/shortid": "^0.0.29",
19191922
"@types/sinon": "^4.3.0",
@@ -1931,6 +1934,7 @@
19311934
"debounce": "^1.1.0",
19321935
"decache": "^4.4.0",
19331936
"del": "^3.0.0",
1937+
"download": "^7.0.0",
19341938
"event-stream": "^3.3.4",
19351939
"flat": "^4.0.0",
19361940
"gulp": "^3.9.1",

src/test/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ if ((Reflect as any).metadata === undefined) {
33
// tslint:disable-next-line:no-require-imports no-var-requires
44
require('reflect-metadata');
55
}
6-
import { MochaSetupOptions } from 'vscode/lib/testrunner';
76
import { IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER, IS_MULTI_ROOT_TEST } from './constants';
87
import * as testRunner from './testRunner';
98

@@ -15,15 +14,18 @@ process.env.IS_MULTI_ROOT_TEST = IS_MULTI_ROOT_TEST.toString();
1514
// So the solution is to run them separately and first on CI.
1615
const grep = IS_CI_SERVER && IS_CI_SERVER_TEST_DEBUGGER ? 'Debug' : undefined;
1716

17+
const testFilesSuffix = process.env.TEST_FILES_SUFFIX;
18+
1819
// You can directly control Mocha options by uncommenting the following lines.
1920
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info.
2021
// Hack, as retries is not supported as setting in tsd.
21-
const options: MochaSetupOptions & { retries: number } = {
22+
const options: testRunner.SetupOptions & { retries: number } = {
2223
ui: 'tdd',
2324
useColors: true,
2425
timeout: 25000,
2526
retries: 3,
26-
grep
27+
grep,
28+
testFilesSuffix
2729
};
2830
testRunner.configure(options, { coverageConfig: '../coverconfig.json' });
2931
module.exports = testRunner;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
// tslint:disable:no-invalid-this no-console
7+
8+
import { expect } from 'chai';
9+
import * as fs from 'fs-extra';
10+
import { EOL } from 'os';
11+
import * as path from 'path';
12+
import { commands, extensions } from 'vscode';
13+
import { StopWatch } from '../../client/common/stopWatch';
14+
15+
const AllowedIncreaseInActivationDelayInMS = 500;
16+
17+
suite('Activation Times', () => {
18+
if (process.env.ACTIVATION_TIMES_LOG_FILE_PATH) {
19+
const logFile = process.env.ACTIVATION_TIMES_LOG_FILE_PATH;
20+
const sampleCounter = fs.existsSync(logFile) ? fs.readFileSync(logFile, { encoding: 'utf8' }).toString().split(/\r?\n/g).length : 1;
21+
if (sampleCounter > 10) {
22+
return;
23+
}
24+
test(`Capture Extension Activation Times (Version: ${process.env.ACTIVATION_TIMES_EXT_VERSION}, sample: ${sampleCounter})`, async () => {
25+
const pythonExtension = extensions.getExtension('ms-python.python');
26+
if (pythonExtension) {
27+
throw new Error('Python Extension not found');
28+
}
29+
const stopWatch = new StopWatch();
30+
await pythonExtension!.activate();
31+
const elapsedTime = stopWatch.elapsedTime;
32+
if (elapsedTime > 10) {
33+
await fs.ensureDir(path.dirname(logFile));
34+
await fs.appendFile(logFile, `${elapsedTime}${EOL}`, { encoding: 'utf8' });
35+
console.log(`Loaded in ${elapsedTime}ms`);
36+
}
37+
commands.executeCommand('workbench.action.reloadWindow');
38+
});
39+
}
40+
41+
if (process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS &&
42+
process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS &&
43+
process.env.ACTIVATION_TIMES_DEV_ANALYSIS_LOG_FILE_PATHS) {
44+
45+
test('Test activation times of Dev vs Release Extension', async () => {
46+
function getActivationTimes(files: string[]) {
47+
const activationTimes: number[] = [];
48+
for (const file of files) {
49+
fs.readFileSync(file, { encoding: 'utf8' }).toString()
50+
.split(/\r?\n/g)
51+
.map(line => line.trim())
52+
.filter(line => line.length > 0)
53+
.map(line => parseInt(line, 10))
54+
.forEach(item => activationTimes.push(item));
55+
}
56+
return activationTimes;
57+
}
58+
const devActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS!));
59+
const releaseActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS!));
60+
const analysisEngineActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_DEV_ANALYSIS_LOG_FILE_PATHS!));
61+
const devActivationAvgTime = devActivationTimes.reduce((sum, item) => sum + item, 0) / devActivationTimes.length;
62+
const releaseActivationAvgTime = releaseActivationTimes.reduce((sum, item) => sum + item, 0) / releaseActivationTimes.length;
63+
const analysisEngineActivationAvgTime = analysisEngineActivationTimes.reduce((sum, item) => sum + item, 0) / analysisEngineActivationTimes.length;
64+
65+
console.log(`Dev version Loaded in ${devActivationAvgTime}ms`);
66+
console.log(`Release version Loaded in ${releaseActivationAvgTime}ms`);
67+
console.log(`Analysis Engine Loaded in ${analysisEngineActivationAvgTime}ms`);
68+
69+
expect(devActivationAvgTime - releaseActivationAvgTime).to.be.lessThan(AllowedIncreaseInActivationDelayInMS, 'Activation times have increased above allowed threshold.');
70+
});
71+
}
72+
});

src/test/performance/sample.py

Whitespace-only changes.

src/test/performance/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "python.jediEnabled": true }

src/test/performanceTest.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
/*
7+
Comparing performance metrics is not easy (the metrics can and always get skewed).
8+
One approach is to run the tests multile times and gather multiple sample data.
9+
For Extension activation times, we load both extensions x times, and re-load the window y times in each x load.
10+
I.e. capture averages by giving the extensions sufficient time to warm up.
11+
This block of code merely launches the tests by using either the dev or release version of the extension,
12+
and spawning the tests (mimic user starting tests from command line), this way we can run tests multiple times.
13+
*/
14+
15+
// tslint:disable:no-console no-require-imports no-var-requires
16+
17+
import { spawn } from 'child_process';
18+
import * as download from 'download';
19+
import * as fs from 'fs-extra';
20+
import * as path from 'path';
21+
import * as request from 'request';
22+
import { EXTENSION_ROOT_DIR } from '../client/common/constants';
23+
24+
const NamedRegexp = require('named-js-regexp');
25+
const StreamZip = require('node-stream-zip');
26+
const del = require('del');
27+
28+
const tmpFolder = path.join(EXTENSION_ROOT_DIR, 'tmp');
29+
const publishedExtensionPath = path.join(tmpFolder, 'ext', 'testReleaseExtensionsFolder');
30+
const logFilesPath = path.join(tmpFolder, 'test', 'logs');
31+
32+
enum Version {
33+
Dev, Release
34+
}
35+
36+
class TestRunner {
37+
public async start() {
38+
await del([path.join(tmpFolder, '**')]);
39+
await this.extractLatestExtension(publishedExtensionPath);
40+
41+
const timesToLoadEachVersion = 3;
42+
const devLogFiles: string[] = [];
43+
const releaseLogFiles: string[] = [];
44+
const newAnalysisEngineLogFiles: string[] = [];
45+
46+
for (let i = 0; i < timesToLoadEachVersion; i += 1) {
47+
await this.enableNewAnalysisEngine(false);
48+
49+
const devLogFile = path.join(logFilesPath, `dev_loadtimes${i}.txt`);
50+
await this.capturePerfTimes(Version.Dev, devLogFile);
51+
devLogFiles.push(devLogFile);
52+
53+
const releaseLogFile = path.join(logFilesPath, `release_loadtimes${i}.txt`);
54+
await this.capturePerfTimes(Version.Release, releaseLogFile);
55+
releaseLogFiles.push(releaseLogFile);
56+
57+
// New Analysis engine.
58+
await this.enableNewAnalysisEngine(true);
59+
const newAnalysisEngineLogFile = path.join(logFilesPath, `newAnalysisEngine_loadtimes${i}.txt`);
60+
await this.capturePerfTimes(Version.Release, newAnalysisEngineLogFile);
61+
newAnalysisEngineLogFiles.push(newAnalysisEngineLogFile);
62+
}
63+
64+
await this.runPerfTest(devLogFiles, releaseLogFiles, newAnalysisEngineLogFiles);
65+
}
66+
private async enableNewAnalysisEngine(enable: boolean) {
67+
const settings = `{ "python.jediEnabled": ${!enable} }`;
68+
await fs.writeFile(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance', 'settings.json'), settings);
69+
}
70+
71+
private async capturePerfTimes(version: Version, logFile: string) {
72+
const releaseVersion = await this.getReleaseVersion();
73+
const devVersion = await this.getDevVersion();
74+
await fs.ensureDir(path.dirname(logFile));
75+
const env: { [key: string]: {} } = {
76+
ACTIVATION_TIMES_LOG_FILE_PATH: logFile,
77+
ACTIVATION_TIMES_EXT_VERSION: version === Version.Release ? releaseVersion : devVersion,
78+
CODE_EXTENSIONS_PATH: version === Version.Release ? publishedExtensionPath : EXTENSION_ROOT_DIR
79+
};
80+
81+
await this.launchTest(env);
82+
}
83+
private async runPerfTest(devLogFiles: string[], releaseLogFiles: string[], newAnalysisEngineLogFiles: string[]) {
84+
const env: { [key: string]: {} } = {
85+
ACTIVATION_TIMES_DEV_LOG_FILE_PATHS: JSON.stringify(devLogFiles),
86+
ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS: JSON.stringify(releaseLogFiles),
87+
ACTIVATION_TIMES_DEV_ANALYSIS_LOG_FILE_PATHS: JSON.stringify(newAnalysisEngineLogFiles)
88+
};
89+
90+
await this.launchTest(env);
91+
}
92+
93+
private async launchTest(customEnvVars: { [key: string]: {} }) {
94+
await new Promise((resolve, reject) => {
95+
const env: { [key: string]: {} } = {
96+
TEST_FILES_SUFFIX: 'perf.test',
97+
CODE_TESTS_WORKSPACE: path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance'),
98+
...process.env,
99+
...customEnvVars
100+
};
101+
102+
const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { cwd: EXTENSION_ROOT_DIR, env });
103+
proc.stdout.pipe(process.stdout);
104+
proc.stderr.pipe(process.stderr);
105+
proc.on('error', reject);
106+
proc.on('close', code => {
107+
if (code === 0) {
108+
resolve();
109+
} else {
110+
reject(`Failed with code ${code}.`);
111+
}
112+
});
113+
});
114+
}
115+
116+
private async extractLatestExtension(targetDir: string): Promise<void> {
117+
const extensionFile = await this.downloadExtension();
118+
await this.unzip(extensionFile, targetDir);
119+
}
120+
121+
private async getReleaseVersion(): Promise<string> {
122+
const url = 'https://marketplace.visualstudio.com/items?itemName=ms-python.python';
123+
const content = await new Promise<string>((resolve, reject) => {
124+
request(url, (error, response, body) => {
125+
if (error) {
126+
return reject(error);
127+
}
128+
if (response.statusCode === 200) {
129+
return resolve(body);
130+
}
131+
reject(`Status code of ${response.statusCode} received.`);
132+
});
133+
});
134+
const re = NamedRegexp('"version"\S?:\S?"(:<version>\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g');
135+
const matches = re.exec(content);
136+
return matches.groups().version;
137+
}
138+
139+
private async getDevVersion(): Promise<string> {
140+
// tslint:disable-next-line:non-literal-require
141+
return require(path.join(EXTENSION_ROOT_DIR, 'package.json')).version;
142+
}
143+
144+
private async unzip(zipFile: string, targetFolder: string): Promise<void> {
145+
await fs.ensureDir(targetFolder);
146+
return new Promise<void>((resolve, reject) => {
147+
const zip = new StreamZip({
148+
file: zipFile,
149+
storeEntries: true
150+
});
151+
zip.on('ready', async () => {
152+
zip.extract('extension', targetFolder, err => {
153+
if (err) {
154+
reject(err);
155+
} else {
156+
resolve();
157+
}
158+
zip.close();
159+
});
160+
});
161+
});
162+
}
163+
164+
private async downloadExtension(): Promise<string> {
165+
const version = await this.getReleaseVersion();
166+
const url = `https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/${version}/vspackage`;
167+
const destination = path.join(__dirname, `extension${version}.zip`);
168+
if (await fs.pathExists(destination)) {
169+
return destination;
170+
}
171+
172+
await download(url, path.dirname(destination), { filename: path.basename(destination) });
173+
return destination;
174+
}
175+
}
176+
177+
new TestRunner().start().catch(ex => console.error('Error in running Performance Tests', ex));

0 commit comments

Comments
 (0)