Skip to content

Commit c0bf1b7

Browse files
author
Kartik Raj
authored
Improve time taken to trigger language server startup once extension activation is triggered (#22514)
For #22146 Improves time taken to trigger language server startup once extension activation is triggered - Do not block discovery on windows registry - Do not blocking auto-selection on validation of all interpreters - Make Windows Path locator faster
1 parent 20c1a10 commit c0bf1b7

File tree

25 files changed

+283
-128
lines changed

25 files changed

+283
-128
lines changed

src/client/activation/activationManager.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { PYTHON_LANGUAGE } from '../common/constants';
1111
import { IFileSystem } from '../common/platform/types';
1212
import { IDisposable, IInterpreterPathService, Resource } from '../common/types';
1313
import { Deferred } from '../common/utils/async';
14+
import { StopWatch } from '../common/utils/stopWatch';
1415
import { IInterpreterAutoSelectionService } from '../interpreter/autoSelection/types';
1516
import { traceDecoratorError } from '../logging';
1617
import { sendActivationTelemetry } from '../telemetry/envFileTelemetry';
@@ -69,20 +70,20 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
6970
}
7071
}
7172

72-
public async activate(): Promise<void> {
73+
public async activate(startupStopWatch: StopWatch): Promise<void> {
7374
this.filterServices();
7475
await this.initialize();
7576

7677
// Activate all activation services together.
7778

7879
await Promise.all([
7980
...this.singleActivationServices.map((item) => item.activate()),
80-
this.activateWorkspace(this.activeResourceService.getActiveResource()),
81+
this.activateWorkspace(this.activeResourceService.getActiveResource(), startupStopWatch),
8182
]);
8283
}
8384

8485
@traceDecoratorError('Failed to activate a workspace')
85-
public async activateWorkspace(resource: Resource): Promise<void> {
86+
public async activateWorkspace(resource: Resource, startupStopWatch?: StopWatch): Promise<void> {
8687
const folder = this.workspaceService.getWorkspaceFolder(resource);
8788
resource = folder ? folder.uri : undefined;
8889
const key = this.getWorkspaceKey(resource);
@@ -97,7 +98,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
9798
await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource);
9899
}
99100
await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource);
100-
await Promise.all(this.activationServices.map((item) => item.activate(resource)));
101+
await Promise.all(this.activationServices.map((item) => item.activate(resource, startupStopWatch)));
101102
await this.appDiagnostics.performPreStartupHealthCheck(resource);
102103
}
103104

src/client/activation/types.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { Event } from 'vscode';
77
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node';
88
import type { IDisposable, ILogOutputChannel, Resource } from '../common/types';
9+
import { StopWatch } from '../common/utils/stopWatch';
910
import { PythonEnvironment } from '../pythonEnvironments/info';
1011

1112
export const IExtensionActivationManager = Symbol('IExtensionActivationManager');
@@ -23,7 +24,7 @@ export interface IExtensionActivationManager extends IDisposable {
2324
* @returns {Promise<void>}
2425
* @memberof IExtensionActivationManager
2526
*/
26-
activate(): Promise<void>;
27+
activate(startupStopWatch: StopWatch): Promise<void>;
2728
/**
2829
* Method invoked when a workspace is loaded.
2930
* This is where we place initialization scripts for each workspace.
@@ -47,7 +48,7 @@ export const IExtensionActivationService = Symbol('IExtensionActivationService')
4748
*/
4849
export interface IExtensionActivationService {
4950
supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean };
50-
activate(resource: Resource): Promise<void>;
51+
activate(resource: Resource, startupStopWatch?: StopWatch): Promise<void>;
5152
}
5253

5354
export enum LanguageServerType {

src/client/extension.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ async function activateUnsafe(
110110
const activationDeferred = createDeferred<void>();
111111
displayProgress(activationDeferred.promise);
112112
startupDurations.startActivateTime = startupStopWatch.elapsedTime;
113+
const activationStopWatch = new StopWatch();
113114

114115
//===============================================
115116
// activation starts here
@@ -127,7 +128,7 @@ async function activateUnsafe(
127128
const components = await initializeComponents(ext);
128129

129130
// Then we finish activating.
130-
const componentsActivated = await activateComponents(ext, components);
131+
const componentsActivated = await activateComponents(ext, components, activationStopWatch);
131132
activateFeatures(ext, components);
132133

133134
const nonBlocking = componentsActivated.map((r) => r.fullyReady);

src/client/extensionActivation.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation
5151
import { initializePersistentStateForTriggers } from './common/persistentState';
5252
import { logAndNotifyOnLegacySettings } from './logging/settingLogs';
5353
import { DebuggerTypeName } from './debugger/constants';
54+
import { StopWatch } from './common/utils/stopWatch';
5455

5556
export async function activateComponents(
5657
// `ext` is passed to any extra activation funcs.
5758
ext: ExtensionState,
5859
components: Components,
60+
startupStopWatch: StopWatch,
5961
): Promise<ActivationResult[]> {
6062
// Note that each activation returns a promise that resolves
6163
// when that activation completes. However, it might have started
@@ -73,7 +75,7 @@ export async function activateComponents(
7375
// activate them in parallel with the other components.
7476
// https://github.com/microsoft/vscode-python/issues/15380
7577
// These will go away eventually once everything is refactored into components.
76-
const legacyActivationResult = await activateLegacy(ext);
78+
const legacyActivationResult = await activateLegacy(ext, startupStopWatch);
7779
const workspaceService = new WorkspaceService();
7880
if (!workspaceService.isTrusted) {
7981
return [legacyActivationResult];
@@ -105,7 +107,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
105107
// init and activation: move them to activateComponents().
106108
// See https://github.com/microsoft/vscode-python/issues/10454.
107109

108-
async function activateLegacy(ext: ExtensionState): Promise<ActivationResult> {
110+
async function activateLegacy(ext: ExtensionState, startupStopWatch: StopWatch): Promise<ActivationResult> {
109111
const { legacyIOC } = ext;
110112
const { serviceManager, serviceContainer } = legacyIOC;
111113

@@ -183,7 +185,7 @@ async function activateLegacy(ext: ExtensionState): Promise<ActivationResult> {
183185
const manager = serviceContainer.get<IExtensionActivationManager>(IExtensionActivationManager);
184186
disposables.push(manager);
185187

186-
const activationPromise = manager.activate();
188+
const activationPromise = manager.activate(startupStopWatch);
187189

188190
return { fullyReady: activationPromise };
189191
}

src/client/interpreter/autoSelection/index.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import { inject, injectable } from 'inversify';
77
import { Event, EventEmitter, Uri } from 'vscode';
88
import { IWorkspaceService } from '../../common/application/types';
9+
import { DiscoveryUsingWorkers } from '../../common/experiments/groups';
910
import '../../common/extensions';
1011
import { IFileSystem } from '../../common/platform/types';
11-
import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types';
12+
import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types';
1213
import { createDeferred, Deferred } from '../../common/utils/async';
1314
import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion';
15+
import { ProgressReportStage } from '../../pythonEnvironments/base/locator';
1416
import { PythonEnvironment } from '../../pythonEnvironments/info';
1517
import { sendTelemetryEvent } from '../../telemetry';
1618
import { EventName } from '../../telemetry/constants';
@@ -44,6 +46,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
4446
@inject(IInterpreterComparer) private readonly envTypeComparer: IInterpreterComparer,
4547
@inject(IInterpreterAutoSelectionProxyService) proxy: IInterpreterAutoSelectionProxyService,
4648
@inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper,
49+
@inject(IExperimentService) private readonly experimentService: IExperimentService,
4750
) {
4851
proxy.registerInstance!(this);
4952
}
@@ -183,7 +186,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
183186

184187
private getAutoSelectionQueriedOnceState(): IPersistentState<boolean | undefined> {
185188
const key = `autoSelectionInterpretersQueriedOnce`;
186-
return this.stateFactory.createWorkspacePersistentState(key, undefined);
189+
return this.stateFactory.createGlobalPersistentState(key, undefined);
187190
}
188191

189192
/**
@@ -199,22 +202,44 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
199202
private async autoselectInterpreterWithLocators(resource: Resource): Promise<void> {
200203
// Do not perform a full interpreter search if we already have cached interpreters for this workspace.
201204
const queriedState = this.getAutoSelectionInterpretersQueryState(resource);
202-
if (queriedState.value !== true && resource) {
205+
const globalQueriedState = this.getAutoSelectionQueriedOnceState();
206+
if (globalQueriedState.value && queriedState.value !== true && resource) {
203207
await this.interpreterService.triggerRefresh({
204208
searchLocations: { roots: [resource], doNotIncludeNonRooted: true },
205209
});
206210
}
207211

208-
const globalQueriedState = this.getAutoSelectionQueriedOnceState();
209-
if (!globalQueriedState.value) {
210-
// Global interpreters are loaded the first time an extension loads, after which we don't need to
211-
// wait on global interpreter promise refresh.
212-
await this.interpreterService.refreshPromise;
213-
}
214-
const interpreters = this.interpreterService.getInterpreters(resource);
212+
const inExperiment = this.experimentService.inExperimentSync(DiscoveryUsingWorkers.experiment);
215213
const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource);
214+
let recommendedInterpreter: PythonEnvironment | undefined;
215+
if (inExperiment) {
216+
if (!globalQueriedState.value) {
217+
// Global interpreters are loaded the first time an extension loads, after which we don't need to
218+
// wait on global interpreter promise refresh.
219+
// Do not wait for validation of all interpreters to finish, we only need to validate the recommended interpreter.
220+
await this.interpreterService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered });
221+
}
222+
let interpreters = this.interpreterService.getInterpreters(resource);
223+
224+
recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
225+
const details = recommendedInterpreter
226+
? await this.interpreterService.getInterpreterDetails(recommendedInterpreter.path)
227+
: undefined;
228+
if (!details || !recommendedInterpreter) {
229+
await this.interpreterService.refreshPromise; // Interpreter is invalid, wait for all of validation to finish.
230+
interpreters = this.interpreterService.getInterpreters(resource);
231+
recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
232+
}
233+
} else {
234+
if (!globalQueriedState.value) {
235+
// Global interpreters are loaded the first time an extension loads, after which we don't need to
236+
// wait on global interpreter promise refresh.
237+
await this.interpreterService.refreshPromise;
238+
}
239+
const interpreters = this.interpreterService.getInterpreters(resource);
216240

217-
const recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
241+
recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
242+
}
218243
if (!recommendedInterpreter) {
219244
return;
220245
}

src/client/interpreter/contracts.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FileChangeType } from '../common/platform/fileSystemWatcher';
44
import { Resource } from '../common/types';
55
import { PythonEnvSource } from '../pythonEnvironments/base/info';
66
import {
7+
GetRefreshEnvironmentsOptions,
78
ProgressNotificationEvent,
89
PythonLocatorQuery,
910
TriggerRefreshOptions,
@@ -22,7 +23,7 @@ export const IComponentAdapter = Symbol('IComponentAdapter');
2223
export interface IComponentAdapter {
2324
readonly onProgress: Event<ProgressNotificationEvent>;
2425
triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>;
25-
getRefreshPromise(): Promise<void> | undefined;
26+
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
2627
readonly onChanged: Event<PythonEnvironmentsChangedEvent>;
2728
// VirtualEnvPrompt
2829
onDidCreate(resource: Resource, callback: () => void): Disposable;
@@ -74,6 +75,7 @@ export const IInterpreterService = Symbol('IInterpreterService');
7475
export interface IInterpreterService {
7576
triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>;
7677
readonly refreshPromise: Promise<void> | undefined;
78+
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
7779
readonly onDidChangeInterpreters: Event<PythonEnvironmentsChangedEvent>;
7880
onDidChangeInterpreterConfiguration: Event<Uri | undefined>;
7981
onDidChangeInterpreter: Event<Uri | undefined>;

src/client/interpreter/interpreterService.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ import { Interpreters } from '../common/utils/localize';
3838
import { sendTelemetryEvent } from '../telemetry';
3939
import { EventName } from '../telemetry/constants';
4040
import { cache } from '../common/utils/decorators';
41-
import { PythonLocatorQuery, TriggerRefreshOptions } from '../pythonEnvironments/base/locator';
41+
import {
42+
GetRefreshEnvironmentsOptions,
43+
PythonLocatorQuery,
44+
TriggerRefreshOptions,
45+
} from '../pythonEnvironments/base/locator';
4246
import { sleep } from '../common/utils/async';
4347

4448
type StoredPythonEnvironment = PythonEnvironment & { store?: boolean };
@@ -59,6 +63,10 @@ export class InterpreterService implements Disposable, IInterpreterService {
5963
return this.pyenvs.getRefreshPromise();
6064
}
6165

66+
public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined {
67+
return this.pyenvs.getRefreshPromise(options);
68+
}
69+
6270
public get onDidChangeInterpreter(): Event<Uri | undefined> {
6371
return this.didChangeInterpreterEmitter.event;
6472
}

src/client/languageServer/watcher.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { PylanceLSExtensionManager } from './pylanceLSExtensionManager';
2929
import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types';
3030
import { sendTelemetryEvent } from '../telemetry';
3131
import { EventName } from '../telemetry/constants';
32+
import { StopWatch } from '../common/utils/stopWatch';
3233

3334
@injectable()
3435
/**
@@ -73,14 +74,18 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang
7374

7475
// IExtensionActivationService
7576

76-
public async activate(resource?: Resource): Promise<void> {
77+
public async activate(resource?: Resource, startupStopWatch?: StopWatch): Promise<void> {
7778
this.register();
78-
await this.startLanguageServer(this.languageServerType, resource);
79+
await this.startLanguageServer(this.languageServerType, resource, startupStopWatch);
7980
}
8081

8182
// ILanguageServerWatcher
82-
public async startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise<void> {
83-
await this.startAndGetLanguageServer(languageServerType, resource);
83+
public async startLanguageServer(
84+
languageServerType: LanguageServerType,
85+
resource?: Resource,
86+
startupStopWatch?: StopWatch,
87+
): Promise<void> {
88+
await this.startAndGetLanguageServer(languageServerType, resource, startupStopWatch);
8489
}
8590

8691
public register(): void {
@@ -124,6 +129,7 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang
124129
private async startAndGetLanguageServer(
125130
languageServerType: LanguageServerType,
126131
resource?: Resource,
132+
startupStopWatch?: StopWatch,
127133
): Promise<ILanguageServerExtensionManager> {
128134
const lsResource = this.getWorkspaceUri(resource);
129135
const currentInterpreter = this.workspaceInterpreters.get(lsResource.fsPath);
@@ -170,6 +176,10 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang
170176

171177
if (languageServerExtensionManager.canStartLanguageServer(interpreter)) {
172178
// Start the language server.
179+
if (startupStopWatch) {
180+
// It means that startup is triggering this code, track time it takes since startup to activate this code.
181+
sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRIGGER_DURATION, startupStopWatch.elapsedTime);
182+
}
173183
await languageServerExtensionManager.startLanguageServer(lsResource, interpreter);
174184

175185
logStartup(languageServerType, lsResource);

src/client/pythonEnvironments/base/locator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type PythonEnvUpdatedEvent<I = PythonEnvInfo> = {
2020
/**
2121
* The iteration index of The env info that was previously provided.
2222
*/
23-
index: number;
23+
index?: number;
2424
/**
2525
* The env info that was previously provided.
2626
*/

src/client/pythonEnvironments/base/locatorUtils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export async function getEnvs<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I
8585
}
8686
updatesDone.resolve();
8787
listener.dispose();
88-
} else {
88+
} else if (event.index !== undefined) {
8989
const { index, update } = event;
9090
if (envs[index] === undefined) {
9191
const json = JSON.stringify(update);
@@ -95,6 +95,8 @@ export async function getEnvs<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I
9595
}
9696
// We don't worry about if envs[index] is set already.
9797
envs[index] = update;
98+
} else if (event.update) {
99+
envs.push(event.update);
98100
}
99101
});
100102
}

src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,18 @@ export abstract class LazyResourceBasedLocator extends Locator<BasicEnvInfo> imp
4343
await this.disposables.dispose();
4444
}
4545

46-
public async *iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<BasicEnvInfo> {
47-
await this.activate();
46+
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<BasicEnvInfo> {
4847
const iterator = this.doIterEnvs(query);
48+
const it = this._iterEnvs(iterator, query);
49+
it.onUpdated = iterator.onUpdated;
50+
return it;
51+
}
52+
53+
private async *_iterEnvs(
54+
iterator: IPythonEnvsIterator<BasicEnvInfo>,
55+
query?: PythonLocatorQuery,
56+
): IPythonEnvsIterator<BasicEnvInfo> {
57+
await this.activate();
4958
if (query?.envPath) {
5059
let result = await iterator.next();
5160
while (!result.done) {

0 commit comments

Comments
 (0)