Skip to content

Commit d084c47

Browse files
authored
Improve the perf of functional tests running with real jupyter (#10242)
* Improve the perf of subsequent tests by caching interpreters * Add back the nightly flake * Remove coverage * Add news entry * Use a static map to allow promise to be cleared on new interpreters (as it was before)
1 parent 71ba3bb commit d084c47

File tree

7 files changed

+255
-5
lines changed

7 files changed

+255
-5
lines changed

build/.mocha.functional.perf.opts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
./out/test/**/*.functional.test.js
2+
--require=out/test/unittests.js
3+
--exclude=out/**/*.jsx
4+
--ui=tdd
5+
--recursive
6+
--colors
7+
--exit
8+
--timeout=180000
9+
--reporter spec
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Nightly build
2+
3+
name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly-flake'
4+
5+
# Not the CI build, see `vscode-python-nightly-flake-ci.yaml`.
6+
trigger: none
7+
8+
# Not the PR build for merges to master and release.
9+
pr: none
10+
11+
schedules:
12+
- cron: "0 8 * * 1-5"
13+
# Daily midnight PST build, runs Monday - Friday always
14+
displayName: Nightly Flake build
15+
branches:
16+
include:
17+
- master
18+
- release*
19+
always: true
20+
21+
# Variables that are available for the entire pipeline.
22+
variables:
23+
- template: templates/globals.yml
24+
25+
stages:
26+
- stage: Build
27+
jobs:
28+
- template: templates/jobs/build_compile.yml
29+
30+
# Each item in each matrix has a number of possible values it may
31+
# define. They are detailed in templates/test_phases.yml. The only
32+
# required value is "TestsToRun".
33+
34+
- stage: Linux
35+
dependsOn:
36+
- Build
37+
jobs:
38+
- job: 'Py3x'
39+
dependsOn: []
40+
timeoutInMinutes: 120
41+
strategy:
42+
matrix:
43+
'Functional':
44+
TestsToRun: 'testfunctional'
45+
NeedsPythonTestReqs: true
46+
NeedsPythonFunctionalReqs: true
47+
VSCODE_PYTHON_ROLLING: true
48+
pool:
49+
vmImage: 'ubuntu-16.04'
50+
steps:
51+
- template: templates/test_phases.yml
52+
53+
- job: 'Py36'
54+
dependsOn: []
55+
timeoutInMinutes: 120
56+
strategy:
57+
matrix:
58+
'Functional':
59+
PythonVersion: '3.6'
60+
TestsToRun: 'testfunctional'
61+
NeedsPythonTestReqs: true
62+
NeedsPythonFunctionalReqs: true
63+
VSCODE_PYTHON_ROLLING: true
64+
pool:
65+
vmImage: 'ubuntu-16.04'
66+
steps:
67+
- template: templates/test_phases.yml
68+
69+
- stage: Mac
70+
dependsOn:
71+
- Build
72+
jobs:
73+
- job: 'Py3x'
74+
dependsOn: []
75+
timeoutInMinutes: 120
76+
strategy:
77+
matrix:
78+
'Functional':
79+
TestsToRun: 'testfunctional'
80+
NeedsPythonTestReqs: true
81+
NeedsPythonFunctionalReqs: true
82+
VSCODE_PYTHON_ROLLING: true
83+
pool:
84+
vmImage: 'macos-10.13'
85+
steps:
86+
- template: templates/test_phases.yml
87+
88+
- job: 'Py36'
89+
dependsOn: []
90+
timeoutInMinutes: 120
91+
strategy:
92+
matrix:
93+
'Functional':
94+
PythonVersion: '3.6'
95+
TestsToRun: 'testfunctional'
96+
NeedsPythonTestReqs: true
97+
NeedsPythonFunctionalReqs: true
98+
VSCODE_PYTHON_ROLLING: true
99+
pool:
100+
vmImage: 'macos-10.13'
101+
steps:
102+
- template: templates/test_phases.yml
103+
104+
- stage: Windows
105+
dependsOn:
106+
- Build
107+
jobs:
108+
- job: 'Py3x'
109+
dependsOn: []
110+
timeoutInMinutes: 120
111+
strategy:
112+
matrix:
113+
'Functional':
114+
TestsToRun: 'testfunctional'
115+
NeedsPythonTestReqs: true
116+
NeedsPythonFunctionalReqs: true
117+
VSCODE_PYTHON_ROLLING: true
118+
pool:
119+
vmImage: 'vs2017-win2016'
120+
steps:
121+
- template: templates/test_phases.yml
122+
123+
- job: 'Py36'
124+
dependsOn: []
125+
timeoutInMinutes: 120
126+
strategy:
127+
matrix:
128+
'Functional':
129+
PythonVersion: '3.6'
130+
TestsToRun: 'testfunctional'
131+
NeedsPythonTestReqs: true
132+
NeedsPythonFunctionalReqs: true
133+
VSCODE_PYTHON_ROLLING: true
134+
pool:
135+
vmImage: 'vs2017-win2016'
136+
steps:
137+
- template: templates/test_phases.yml

news/3 Code Health/7997.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Functional tests using real jupyter can take 30-90 seconds each. Most of this time is searching for interpreters. Cache the interpreter search.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2844,6 +2844,7 @@
28442844
"test:unittests": "mocha --opts ./build/.mocha.unittests.js.opts",
28452845
"test:unittests:cover": "nyc --no-clean --nycrc-path build/.nycrc mocha --opts ./build/.mocha.unittests.ts.opts",
28462846
"test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts",
2847+
"test:functional:perf": "node --inspect-brk ./node_modules/mocha/bin/_mocha --require source-map-support/register --opts ./build/.mocha.functional.perf.opts",
28472848
"test:functional:cover": "npm run test:functional",
28482849
"test:cover:report": "nyc --nycrc-path build/.nycrc report --reporter=text --reporter=html --reporter=text-summary --reporter=cobertura",
28492850
"testDebugger": "node ./out/test/testBootstrap.js ./out/test/debuggerTest.js",

src/client/interpreter/locators/services/cacheableLocatorService.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,42 @@ import { sendTelemetryEvent } from '../../../telemetry';
1717
import { EventName } from '../../../telemetry/constants';
1818
import { IInterpreterLocatorService, IInterpreterWatcher, PythonInterpreter } from '../../contracts';
1919

20+
export class CacheableLocatorPromiseCache {
21+
private static useStatic = false;
22+
private static staticMap = new Map<string, Deferred<PythonInterpreter[]>>();
23+
private normalMap = new Map<string, Deferred<PythonInterpreter[]>>();
24+
25+
public static forceUseStatic() {
26+
CacheableLocatorPromiseCache.useStatic = true;
27+
}
28+
public get(key: string): Deferred<PythonInterpreter[]> | undefined {
29+
if (CacheableLocatorPromiseCache.useStatic) {
30+
return CacheableLocatorPromiseCache.staticMap.get(key);
31+
}
32+
return this.normalMap.get(key);
33+
}
34+
35+
public set(key: string, value: Deferred<PythonInterpreter[]>) {
36+
if (CacheableLocatorPromiseCache.useStatic) {
37+
CacheableLocatorPromiseCache.staticMap.set(key, value);
38+
} else {
39+
this.normalMap.set(key, value);
40+
}
41+
}
42+
43+
public delete(key: string) {
44+
if (CacheableLocatorPromiseCache.useStatic) {
45+
CacheableLocatorPromiseCache.staticMap.delete(key);
46+
} else {
47+
this.normalMap.delete(key);
48+
}
49+
}
50+
}
51+
2052
@injectable()
2153
export abstract class CacheableLocatorService implements IInterpreterLocatorService {
2254
protected readonly _hasInterpreters: Deferred<boolean>;
23-
private readonly promisesPerResource = new Map<string, Deferred<PythonInterpreter[]>>();
55+
private readonly promisesPerResource = new CacheableLocatorPromiseCache();
2456
private readonly handlersAddedToResource = new Set<string>();
2557
private readonly cacheKeyPrefix: string;
2658
private readonly locating = new EventEmitter<Promise<PythonInterpreter[]>>();
@@ -32,6 +64,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ
3264
this._hasInterpreters = createDeferred<boolean>();
3365
this.cacheKeyPrefix = `INTERPRETERS_CACHE_v3_${name}`;
3466
}
67+
3568
public get onLocating(): Event<Promise<PythonInterpreter[]>> {
3669
return this.locating.event;
3770
}
@@ -43,7 +76,6 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ
4376
public async getInterpreters(resource?: Uri, ignoreCache?: boolean): Promise<PythonInterpreter[]> {
4477
const cacheKey = this.getCacheKey(resource);
4578
let deferred = this.promisesPerResource.get(cacheKey);
46-
4779
if (!deferred || ignoreCache) {
4880
deferred = createDeferred<PythonInterpreter[]>();
4981
this.promisesPerResource.set(cacheKey, deferred);

src/test/datascience/dataScienceIocContainer.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Event,
1717
EventEmitter,
1818
FileSystemWatcher,
19+
Memento,
1920
Uri,
2021
WorkspaceConfiguration,
2122
WorkspaceFolder,
@@ -99,7 +100,6 @@ import {
99100
} from '../../client/common/installer/productPath';
100101
import { ProductService } from '../../client/common/installer/productService';
101102
import { IInstallationChannelManager, IProductPathService, IProductService } from '../../client/common/installer/types';
102-
import { PersistentStateFactory } from '../../client/common/persistentState';
103103
import { IS_WINDOWS } from '../../client/common/platform/constants';
104104
import { PathUtils } from '../../client/common/platform/pathUtils';
105105
import { RegistryImplementation } from '../../client/common/platform/registry';
@@ -130,6 +130,7 @@ import {
130130
} from '../../client/common/terminal/types';
131131
import {
132132
BANNER_NAME_LS_SURVEY,
133+
GLOBAL_MEMENTO,
133134
IAsyncDisposableRegistry,
134135
IConfigurationService,
135136
ICryptoUtils,
@@ -139,12 +140,14 @@ import {
139140
IExtensionContext,
140141
IExtensions,
141142
IInstaller,
143+
IMemento,
142144
IOutputChannel,
143145
IPathUtils,
144146
IPersistentStateFactory,
145147
IPythonExtensionBanner,
146148
IsWindows,
147-
ProductType
149+
ProductType,
150+
WORKSPACE_MEMENTO
148151
} from '../../client/common/types';
149152
import { Deferred, sleep } from '../../client/common/utils/async';
150153
import { noop } from '../../client/common/utils/misc';
@@ -283,6 +286,7 @@ import { InterpreterService } from '../../client/interpreter/interpreterService'
283286
import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion';
284287
import { PythonInterpreterLocatorService } from '../../client/interpreter/locators';
285288
import { InterpreterLocatorHelper } from '../../client/interpreter/locators/helpers';
289+
import { CacheableLocatorPromiseCache } from '../../client/interpreter/locators/services/cacheableLocatorService';
286290
import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService';
287291
import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService';
288292
import {
@@ -333,6 +337,7 @@ import { MockWorkspaceConfiguration } from './mockWorkspaceConfig';
333337
import { blurWindow, createMessageEvent } from './reactHelpers';
334338
import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider';
335339
import { TestNativeEditorProvider } from './testNativeEditorProvider';
340+
import { TestPersistentStateFactory } from './testPersistentStateFactory';
336341

337342
export class DataScienceIocContainer extends UnitTestIocContainer {
338343
public webPanelListener: IWebPanelMessageListener | undefined;
@@ -914,7 +919,19 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
914919
IInterpreterVersionService,
915920
InterpreterVersionService
916921
);
917-
this.serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory);
922+
923+
const globalStorage = this.serviceManager.get<Memento>(IMemento, GLOBAL_MEMENTO);
924+
const localStorage = this.serviceManager.get<Memento>(IMemento, WORKSPACE_MEMENTO);
925+
926+
// Create a custom persistent state factory that remembers specific things between tests
927+
this.serviceManager.addSingletonInstance<IPersistentStateFactory>(
928+
IPersistentStateFactory,
929+
new TestPersistentStateFactory(globalStorage, localStorage)
930+
);
931+
932+
// Inform the cacheable locator service to use a static map so that it stays in memory in between tests
933+
CacheableLocatorPromiseCache.forceUseStatic();
934+
918935
this.serviceManager.addSingletonInstance<IInterpreterDisplay>(IInterpreterDisplay, interpreterDisplay.object);
919936

920937
this.serviceManager.addSingleton<IPythonPathUpdaterServiceFactory>(
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Memento } from 'vscode';
2+
import { PersistentStateFactory } from '../../client/common/persistentState';
3+
import { IPersistentState, IPersistentStateFactory } from '../../client/common/types';
4+
5+
const PrefixesToStore = ['INTERPRETERS_CACHE'];
6+
7+
// tslint:disable-next-line: no-any
8+
const persistedState = new Map<string, any>();
9+
10+
class TestPersistentState<T> implements IPersistentState<T> {
11+
constructor(private key: string, defaultValue?: T | undefined) {
12+
if (defaultValue) {
13+
persistedState.set(key, defaultValue);
14+
}
15+
}
16+
public get value(): T {
17+
return persistedState.get(this.key);
18+
}
19+
public async updateValue(value: T): Promise<void> {
20+
persistedState.set(this.key, value);
21+
}
22+
}
23+
24+
// This class is used to make certain values persist across tests.
25+
export class TestPersistentStateFactory implements IPersistentStateFactory {
26+
private realStateFactory: PersistentStateFactory;
27+
constructor(globalState: Memento, localState: Memento) {
28+
this.realStateFactory = new PersistentStateFactory(globalState, localState);
29+
}
30+
31+
public createGlobalPersistentState<T>(
32+
key: string,
33+
defaultValue?: T | undefined,
34+
expiryDurationMs?: number | undefined
35+
): IPersistentState<T> {
36+
if (PrefixesToStore.find(p => key.startsWith(p))) {
37+
return new TestPersistentState(key, defaultValue);
38+
}
39+
40+
return this.realStateFactory.createGlobalPersistentState(key, defaultValue, expiryDurationMs);
41+
}
42+
public createWorkspacePersistentState<T>(
43+
key: string,
44+
defaultValue?: T | undefined,
45+
expiryDurationMs?: number | undefined
46+
): IPersistentState<T> {
47+
if (PrefixesToStore.find(p => key.startsWith(p))) {
48+
return new TestPersistentState(key, defaultValue);
49+
}
50+
51+
return this.realStateFactory.createWorkspacePersistentState(key, defaultValue, expiryDurationMs);
52+
}
53+
}

0 commit comments

Comments
 (0)