Skip to content

Commit b09a421

Browse files
authored
Speed up interpreter locators (#676)
performance improvements (fixes #666)
1 parent d596beb commit b09a421

21 files changed

+192
-89
lines changed

src/client/banner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export class BannerService {
1717
if (!this.shouldShowBanner.value) {
1818
return;
1919
}
20-
this.shouldShowBanner.value = false;
20+
this.shouldShowBanner.updateValue(false)
21+
.catch(ex => console.error('Python Extension: Failed to update banner value', ex));
2122

2223
const message = 'The Python extension is now published by Microsoft!';
2324
const yesButton = 'Read more';

src/client/common/featureDeprecationManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class FeatureDeprecationManager implements IFeatureDeprecationManager {
112112
break;
113113
}
114114
case doNotShowAgain: {
115-
notificationPromptEnabled.value = false;
115+
await notificationPromptEnabled.updateValue(false);
116116
break;
117117
}
118118
default: {

src/client/common/persistentState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class PersistentState<T> implements IPersistentState<T>{
1414
return this.storage.get<T>(this.key, this.defaultValue);
1515
}
1616

17-
public set value(newValue: T) {
18-
this.storage.update(this.key, newValue);
17+
public async updateValue(newValue: T): Promise<void> {
18+
await this.storage.update(this.key, newValue);
1919
}
2020
}
2121

src/client/common/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export const GLOBAL_MEMENTO = Symbol('IGlobalMemento');
1414
export const WORKSPACE_MEMENTO = Symbol('IWorkspaceMemento');
1515

1616
export interface IPersistentState<T> {
17-
value: T;
17+
readonly value: T;
18+
updateValue(value: T): Promise<void>;
1819
}
1920

2021
export const IPersistentStateFactory = Symbol('IPersistentStateFactory');

src/client/interpreter/configuration/interpreterSelector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ export class InterpreterSelector implements Disposable {
4343
if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) {
4444
detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`;
4545
}
46+
const cachedPrefix = suggestion.cachedEntry ? '(cached) ' : '';
4647
return {
4748
// tslint:disable-next-line:no-non-null-assertion
4849
label: suggestion.displayName!,
4950
description: suggestion.companyDisplayName || '',
50-
detail: detail,
51+
detail: `${cachedPrefix}${detail}`,
5152
path: suggestion.path
5253
};
5354
}

src/client/interpreter/contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export type PythonInterpreter = {
6262
type: InterpreterType;
6363
envName?: string;
6464
envPath?: string;
65+
cachedEntry?: boolean;
6566
};
6667

6768
export type WorkspacePythonPath = {

src/client/interpreter/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class InterpreterManager implements Disposable, IInterpreterService {
5353
}
5454
const virtualEnvMgr = this.serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager);
5555
const versionService = this.serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
56-
const virtualEnvInterpreterProvider = new VirtualEnvService([activeWorkspace.folderUri.fsPath], virtualEnvMgr, versionService);
56+
const virtualEnvInterpreterProvider = new VirtualEnvService([activeWorkspace.folderUri.fsPath], virtualEnvMgr, versionService, this.serviceContainer);
5757
const interpreters = await virtualEnvInterpreterProvider.getInterpreters(activeWorkspace.folderUri);
5858
const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase();
5959

src/client/interpreter/locators/index.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { inject, injectable } from 'inversify';
22
import * as _ from 'lodash';
33
import * as path from 'path';
4-
import { Disposable, Uri, workspace } from 'vscode';
4+
import { Disposable, Uri } from 'vscode';
55
import { IPlatformService } from '../../common/platform/types';
66
import { IDisposableRegistry } from '../../common/types';
77
import { arePathsSame } from '../../common/utils';
@@ -21,35 +21,21 @@ import { fixInterpreterDisplayName } from './helpers';
2121

2222
@injectable()
2323
export class PythonInterpreterLocatorService implements IInterpreterLocatorService {
24-
private interpretersPerResource: Map<string, Promise<PythonInterpreter[]>>;
2524
private disposables: Disposable[] = [];
2625
private platform: IPlatformService;
2726

2827
constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) {
29-
this.interpretersPerResource = new Map<string, Promise<PythonInterpreter[]>>();
3028
serviceContainer.get<Disposable[]>(IDisposableRegistry).push(this);
3129
this.platform = serviceContainer.get<IPlatformService>(IPlatformService);
3230
}
3331
public async getInterpreters(resource?: Uri) {
34-
const resourceKey = this.getResourceKey(resource);
35-
if (!this.interpretersPerResource.has(resourceKey)) {
36-
this.interpretersPerResource.set(resourceKey, this.getInterpretersPerResource(resource));
37-
}
38-
39-
return await this.interpretersPerResource.get(resourceKey)!;
32+
return this.getInterpretersPerResource(resource);
4033
}
4134
public dispose() {
4235
this.disposables.forEach(disposable => disposable.dispose());
4336
}
44-
private getResourceKey(resource?: Uri) {
45-
if (!resource) {
46-
return '';
47-
}
48-
const workspaceFolder = workspace.getWorkspaceFolder(resource);
49-
return workspaceFolder ? workspaceFolder.uri.fsPath : '';
50-
}
5137
private async getInterpretersPerResource(resource?: Uri) {
52-
const locators = this.getLocators(resource);
38+
const locators = this.getLocators();
5339
const promises = locators.map(async provider => provider.getInterpreters(resource));
5440
const listOfInterpreters = await Promise.all(promises);
5541

@@ -73,7 +59,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi
7359
return accumulator;
7460
}, []);
7561
}
76-
private getLocators(resource?: Uri) {
62+
private getLocators() {
7763
const locators: IInterpreterLocatorService[] = [];
7864
// The order of the services is important.
7965
if (this.platform.isWindows) {

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@ import * as _ from 'lodash';
33
import * as path from 'path';
44
import { Uri } from 'vscode';
55
import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils';
6-
import { IInterpreterLocatorService, IInterpreterVersionService, IKnownSearchPathsForInterpreters, InterpreterType } from '../../contracts';
6+
import { IServiceContainer } from '../../../ioc/types';
7+
import { IInterpreterVersionService, IKnownSearchPathsForInterpreters, InterpreterType, PythonInterpreter } from '../../contracts';
78
import { lookForInterpretersInDirectory } from '../helpers';
9+
import { CacheableLocatorService } from './cacheableLocatorService';
810

911
// tslint:disable-next-line:no-require-imports no-var-requires
1012
const untildify = require('untildify');
1113

1214
@injectable()
13-
export class KnownPathsService implements IInterpreterLocatorService {
15+
export class KnownPathsService extends CacheableLocatorService {
1416
public constructor( @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: string[],
15-
@inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService) { }
16-
// tslint:disable-next-line:no-shadowed-variable
17-
public getInterpreters(_?: Uri) {
18-
return this.suggestionsFromKnownPaths();
17+
@inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService,
18+
@inject(IServiceContainer) serviceContainer: IServiceContainer) {
19+
super('KnownPathsService', serviceContainer);
1920
}
2021
// tslint:disable-next-line:no-empty
2122
public dispose() { }
23+
protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> {
24+
return this.suggestionsFromKnownPaths();
25+
}
2226
private suggestionsFromKnownPaths() {
2327
const promises = this.knownSearchPaths.map(dir => this.getInterpretersInDirectory(dir));
2428
return Promise.all<string[]>(promises)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { injectable } from 'inversify';
5+
import { Uri } from 'vscode';
6+
import { createDeferred, Deferred } from '../../../common/helpers';
7+
import { IPersistentStateFactory } from '../../../common/types';
8+
import { IServiceContainer } from '../../../ioc/types';
9+
import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts';
10+
11+
@injectable()
12+
export abstract class CacheableLocatorService implements IInterpreterLocatorService {
13+
private getInterpretersPromise: Deferred<PythonInterpreter[]>;
14+
private readonly cacheKey: string;
15+
constructor(name: string,
16+
protected readonly serviceContainer: IServiceContainer) {
17+
this.cacheKey = `INTERPRETERS_CACHE_${name}`;
18+
}
19+
public abstract dispose();
20+
public async getInterpreters(resource?: Uri): Promise<PythonInterpreter[]> {
21+
if (!this.getInterpretersPromise) {
22+
this.getInterpretersPromise = createDeferred<PythonInterpreter[]>();
23+
this.getInterpretersImplementation(resource)
24+
.then(async items => {
25+
await this.cacheInterpreters(items);
26+
this.getInterpretersPromise.resolve(items);
27+
})
28+
.catch(ex => this.getInterpretersPromise.reject(ex));
29+
}
30+
if (this.getInterpretersPromise.completed) {
31+
return this.getInterpretersPromise.promise;
32+
}
33+
34+
const cachedInterpreters = this.getCachedInterpreters();
35+
return Array.isArray(cachedInterpreters) ? cachedInterpreters : this.getInterpretersPromise.promise;
36+
}
37+
38+
protected abstract getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]>;
39+
40+
private getCachedInterpreters() {
41+
const persistentFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
42+
// tslint:disable-next-line:no-any
43+
const globalPersistence = persistentFactory.createGlobalPersistentState<PythonInterpreter[]>(this.cacheKey, undefined as any);
44+
if (!Array.isArray(globalPersistence.value)) {
45+
return;
46+
}
47+
return globalPersistence.value.map(item => {
48+
return {
49+
...item,
50+
cachedEntry: true
51+
};
52+
});
53+
}
54+
private async cacheInterpreters(interpreters: PythonInterpreter[]) {
55+
const persistentFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
56+
const globalPersistence = persistentFactory.createGlobalPersistentState<PythonInterpreter[]>(this.cacheKey, []);
57+
await globalPersistence.updateValue(interpreters);
58+
}
59+
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@ import * as path from 'path';
33
import { Uri } from 'vscode';
44
import { IFileSystem } from '../../../common/platform/types';
55
import { ILogger } from '../../../common/types';
6+
import { IServiceContainer } from '../../../ioc/types';
67
import {
78
ICondaService,
8-
IInterpreterLocatorService,
99
IInterpreterVersionService,
1010
InterpreterType,
1111
PythonInterpreter
1212
} from '../../contracts';
13+
import { CacheableLocatorService } from './cacheableLocatorService';
1314
import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from './conda';
1415

1516
@injectable()
16-
export class CondaEnvFileService implements IInterpreterLocatorService {
17+
export class CondaEnvFileService extends CacheableLocatorService {
1718
constructor( @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService,
1819
@inject(ICondaService) private condaService: ICondaService,
1920
@inject(IFileSystem) private fileSystem: IFileSystem,
21+
@inject(IServiceContainer) serviceContainer: IServiceContainer,
2022
@inject(ILogger) private logger: ILogger) {
21-
}
22-
public async getInterpreters(_?: Uri) {
23-
return this.getSuggestionsFromConda();
23+
super('CondaEnvFileService', serviceContainer);
2424
}
2525
// tslint:disable-next-line:no-empty
2626
public dispose() { }
27+
protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> {
28+
return this.getSuggestionsFromConda();
29+
}
2730
private async getSuggestionsFromConda(): Promise<PythonInterpreter[]> {
2831
if (!this.condaService.condaEnvironmentsFile) {
2932
return [];

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ import { inject, injectable } from 'inversify';
55
import { Uri } from 'vscode';
66
import { IFileSystem } from '../../../common/platform/types';
77
import { ILogger } from '../../../common/types';
8-
import { CondaInfo, ICondaService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts';
8+
import { IServiceContainer } from '../../../ioc/types';
9+
import { CondaInfo, ICondaService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts';
10+
import { CacheableLocatorService } from './cacheableLocatorService';
911
import { AnacondaCompanyName, AnacondaCompanyNames } from './conda';
1012
import { CondaHelper } from './condaHelper';
1113

1214
@injectable()
13-
export class CondaEnvService implements IInterpreterLocatorService {
15+
export class CondaEnvService extends CacheableLocatorService {
1416
private readonly condaHelper = new CondaHelper();
1517
constructor( @inject(ICondaService) private condaService: ICondaService,
1618
@inject(IInterpreterVersionService) private versionService: IInterpreterVersionService,
1719
@inject(ILogger) private logger: ILogger,
20+
@inject(IServiceContainer) serviceContainer: IServiceContainer,
1821
@inject(IFileSystem) private fileSystem: IFileSystem) {
19-
}
20-
public async getInterpreters(resource?: Uri) {
21-
return this.getSuggestionsFromConda();
22+
super('CondaEnvService', serviceContainer);
2223
}
2324
// tslint:disable-next-line:no-empty
2425
public dispose() { }
@@ -62,6 +63,9 @@ export class CondaEnvService implements IInterpreterLocatorService {
6263
// tslint:disable-next-line:no-non-null-assertion
6364
.then(interpreters => interpreters.map(interpreter => interpreter!));
6465
}
66+
protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> {
67+
return this.getSuggestionsFromConda();
68+
}
6569
private stripCompanyName(content: string) {
6670
// Strip company name from version.
6771
const startOfCompanyName = AnacondaCompanyNames.reduce((index, companyName) => {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ export class CondaService implements ICondaService {
132132
const condaFile = await this.getCondaFile();
133133
const envInfo = await this.processService.exec(condaFile, ['env', 'list']).then(output => output.stdout);
134134
const environments = this.condaHelper.parseCondaEnvironmentNames(envInfo);
135-
globalPersistence.value = { data: environments };
135+
await globalPersistence.updateValue({ data: environments });
136136
return environments;
137137
} catch (ex) {
138-
globalPersistence.value = { data: undefined };
138+
await globalPersistence.updateValue({ data: undefined });
139139
// Failed because either:
140140
// 1. conda is not installed.
141141
// 2. `conda env list has changed signature.

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,24 @@ import * as path from 'path';
44
import { Uri } from 'vscode';
55
import { PythonSettings } from '../../../common/configSettings';
66
import { IProcessService } from '../../../common/process/types';
7-
import { IInterpreterLocatorService, IInterpreterVersionService, InterpreterType } from '../../contracts';
7+
import { IServiceContainer } from '../../../ioc/types';
8+
import { IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts';
89
import { IVirtualEnvironmentManager } from '../../virtualEnvs/types';
10+
import { CacheableLocatorService } from './cacheableLocatorService';
911

1012
@injectable()
11-
export class CurrentPathService implements IInterpreterLocatorService {
13+
export class CurrentPathService extends CacheableLocatorService {
1214
public constructor( @inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager,
1315
@inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService,
14-
@inject(IProcessService) private processService: IProcessService) { }
15-
public async getInterpreters(resource?: Uri) {
16-
return this.suggestionsFromKnownPaths();
16+
@inject(IProcessService) private processService: IProcessService,
17+
@inject(IServiceContainer) serviceContainer: IServiceContainer) {
18+
super('CurrentPathService', serviceContainer);
1719
}
1820
// tslint:disable-next-line:no-empty
1921
public dispose() { }
22+
protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> {
23+
return this.suggestionsFromKnownPaths();
24+
}
2025
private async suggestionsFromKnownPaths(resource?: Uri) {
2126
const currentPythonInterpreter = this.getInterpreter(PythonSettings.getInstance(resource).pythonPath, '').then(interpreter => [interpreter]);
2227
const python = this.getInterpreter('python', '').then(interpreter => [interpreter]);

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,29 @@ import * as _ from 'lodash';
33
import * as path from 'path';
44
import { Uri, workspace } from 'vscode';
55
import { fsReaddirAsync, IS_WINDOWS } from '../../../common/utils';
6-
import { IInterpreterLocatorService, IInterpreterVersionService, IKnownSearchPathsForVirtualEnvironments, InterpreterType, PythonInterpreter } from '../../contracts';
6+
import { IServiceContainer } from '../../../ioc/types';
7+
import { IInterpreterVersionService, IKnownSearchPathsForVirtualEnvironments, InterpreterType, PythonInterpreter } from '../../contracts';
78
import { IVirtualEnvironmentManager } from '../../virtualEnvs/types';
89
import { lookForInterpretersInDirectory } from '../helpers';
910
import * as settings from './../../../common/configSettings';
11+
import { CacheableLocatorService } from './cacheableLocatorService';
1012

1113
// tslint:disable-next-line:no-require-imports no-var-requires
1214
const untildify = require('untildify');
1315

1416
@injectable()
15-
export class VirtualEnvService implements IInterpreterLocatorService {
17+
export class VirtualEnvService extends CacheableLocatorService {
1618
public constructor( @inject(IKnownSearchPathsForVirtualEnvironments) private knownSearchPaths: string[],
1719
@inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager,
18-
@inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService) { }
19-
public async getInterpreters(resource?: Uri) {
20-
return this.suggestionsFromKnownVenvs();
20+
@inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService,
21+
@inject(IServiceContainer) serviceContainer: IServiceContainer) {
22+
super('KnownPathsService', serviceContainer);
2123
}
2224
// tslint:disable-next-line:no-empty
2325
public dispose() { }
26+
protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> {
27+
return this.suggestionsFromKnownVenvs();
28+
}
2429
private async suggestionsFromKnownVenvs() {
2530
return Promise.all(this.knownSearchPaths.map(dir => this.lookForInterpretersInVenvs(dir)))
2631
// tslint:disable-next-line:underscore-consistent-invocation

0 commit comments

Comments
 (0)