Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve time taken to trigger language server startup once extension activation is triggered #22514

Merged
merged 20 commits into from
Feb 5, 2024
9 changes: 5 additions & 4 deletions src/client/activation/activationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PYTHON_LANGUAGE } from '../common/constants';
import { IFileSystem } from '../common/platform/types';
import { IDisposable, IInterpreterPathService, Resource } from '../common/types';
import { Deferred } from '../common/utils/async';
import { StopWatch } from '../common/utils/stopWatch';
import { IInterpreterAutoSelectionService } from '../interpreter/autoSelection/types';
import { traceDecoratorError } from '../logging';
import { sendActivationTelemetry } from '../telemetry/envFileTelemetry';
Expand Down Expand Up @@ -69,20 +70,20 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
}
}

public async activate(): Promise<void> {
public async activate(startupStopWatch: StopWatch): Promise<void> {
this.filterServices();
await this.initialize();

// Activate all activation services together.

await Promise.all([
...this.singleActivationServices.map((item) => item.activate()),
this.activateWorkspace(this.activeResourceService.getActiveResource()),
this.activateWorkspace(this.activeResourceService.getActiveResource(), startupStopWatch),
]);
}

@traceDecoratorError('Failed to activate a workspace')
public async activateWorkspace(resource: Resource): Promise<void> {
public async activateWorkspace(resource: Resource, startupStopWatch?: StopWatch): Promise<void> {
const folder = this.workspaceService.getWorkspaceFolder(resource);
resource = folder ? folder.uri : undefined;
const key = this.getWorkspaceKey(resource);
Expand All @@ -97,7 +98,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource);
}
await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource);
await Promise.all(this.activationServices.map((item) => item.activate(resource)));
await Promise.all(this.activationServices.map((item) => item.activate(resource, startupStopWatch)));
await this.appDiagnostics.performPreStartupHealthCheck(resource);
}

Expand Down
5 changes: 3 additions & 2 deletions src/client/activation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Event } from 'vscode';
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node';
import type { IDisposable, ILogOutputChannel, Resource } from '../common/types';
import { StopWatch } from '../common/utils/stopWatch';
import { PythonEnvironment } from '../pythonEnvironments/info';

export const IExtensionActivationManager = Symbol('IExtensionActivationManager');
Expand All @@ -23,7 +24,7 @@ export interface IExtensionActivationManager extends IDisposable {
* @returns {Promise<void>}
* @memberof IExtensionActivationManager
*/
activate(): Promise<void>;
activate(startupStopWatch: StopWatch): Promise<void>;
/**
* Method invoked when a workspace is loaded.
* This is where we place initialization scripts for each workspace.
Expand All @@ -47,7 +48,7 @@ export const IExtensionActivationService = Symbol('IExtensionActivationService')
*/
export interface IExtensionActivationService {
supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean };
activate(resource: Resource): Promise<void>;
activate(resource: Resource, startupStopWatch?: StopWatch): Promise<void>;
}

export enum LanguageServerType {
Expand Down
3 changes: 2 additions & 1 deletion src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ async function activateUnsafe(
const activationDeferred = createDeferred<void>();
displayProgress(activationDeferred.promise);
startupDurations.startActivateTime = startupStopWatch.elapsedTime;
const activationStopWatch = new StopWatch();

//===============================================
// activation starts here
Expand All @@ -127,7 +128,7 @@ async function activateUnsafe(
const components = await initializeComponents(ext);

// Then we finish activating.
const componentsActivated = await activateComponents(ext, components);
const componentsActivated = await activateComponents(ext, components, activationStopWatch);
activateFeatures(ext, components);

const nonBlocking = componentsActivated.map((r) => r.fullyReady);
Expand Down
8 changes: 5 additions & 3 deletions src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation
import { initializePersistentStateForTriggers } from './common/persistentState';
import { logAndNotifyOnLegacySettings } from './logging/settingLogs';
import { DebuggerTypeName } from './debugger/constants';
import { StopWatch } from './common/utils/stopWatch';

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

async function activateLegacy(ext: ExtensionState): Promise<ActivationResult> {
async function activateLegacy(ext: ExtensionState, startupStopWatch: StopWatch): Promise<ActivationResult> {
const { legacyIOC } = ext;
const { serviceManager, serviceContainer } = legacyIOC;

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

const activationPromise = manager.activate();
const activationPromise = manager.activate(startupStopWatch);

return { fullyReady: activationPromise };
}
47 changes: 36 additions & 11 deletions src/client/interpreter/autoSelection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import { inject, injectable } from 'inversify';
import { Event, EventEmitter, Uri } from 'vscode';
import { IWorkspaceService } from '../../common/application/types';
import { DiscoveryUsingWorkers } from '../../common/experiments/groups';
import '../../common/extensions';
import { IFileSystem } from '../../common/platform/types';
import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types';
import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types';
import { createDeferred, Deferred } from '../../common/utils/async';
import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion';
import { ProgressReportStage } from '../../pythonEnvironments/base/locator';
import { PythonEnvironment } from '../../pythonEnvironments/info';
import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
Expand Down Expand Up @@ -44,6 +46,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
@inject(IInterpreterComparer) private readonly envTypeComparer: IInterpreterComparer,
@inject(IInterpreterAutoSelectionProxyService) proxy: IInterpreterAutoSelectionProxyService,
@inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper,
@inject(IExperimentService) private readonly experimentService: IExperimentService,
) {
proxy.registerInstance!(this);
}
Expand Down Expand Up @@ -183,7 +186,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio

private getAutoSelectionQueriedOnceState(): IPersistentState<boolean | undefined> {
const key = `autoSelectionInterpretersQueriedOnce`;
return this.stateFactory.createWorkspacePersistentState(key, undefined);
return this.stateFactory.createGlobalPersistentState(key, undefined);
}

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

const globalQueriedState = this.getAutoSelectionQueriedOnceState();
if (!globalQueriedState.value) {
// Global interpreters are loaded the first time an extension loads, after which we don't need to
// wait on global interpreter promise refresh.
await this.interpreterService.refreshPromise;
}
const interpreters = this.interpreterService.getInterpreters(resource);
const inExperiment = this.experimentService.inExperimentSync(DiscoveryUsingWorkers.experiment);
const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource);
let recommendedInterpreter: PythonEnvironment | undefined;
if (inExperiment) {
if (!globalQueriedState.value) {
// Global interpreters are loaded the first time an extension loads, after which we don't need to
// wait on global interpreter promise refresh.
// Do not wait for validation of all interpreters to finish, we only need to validate the recommended interpreter.
await this.interpreterService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered });
}
let interpreters = this.interpreterService.getInterpreters(resource);

recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
const details = recommendedInterpreter
? await this.interpreterService.getInterpreterDetails(recommendedInterpreter.path)
: undefined;
if (!details || !recommendedInterpreter) {
await this.interpreterService.refreshPromise; // Interpreter is invalid, wait for all of validation to finish.
interpreters = this.interpreterService.getInterpreters(resource);
recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
}
} else {
if (!globalQueriedState.value) {
// Global interpreters are loaded the first time an extension loads, after which we don't need to
// wait on global interpreter promise refresh.
await this.interpreterService.refreshPromise;
}
const interpreters = this.interpreterService.getInterpreters(resource);

const recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);
}
if (!recommendedInterpreter) {
return;
}
Expand Down
4 changes: 3 additions & 1 deletion src/client/interpreter/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FileChangeType } from '../common/platform/fileSystemWatcher';
import { Resource } from '../common/types';
import { PythonEnvSource } from '../pythonEnvironments/base/info';
import {
GetRefreshEnvironmentsOptions,
ProgressNotificationEvent,
PythonLocatorQuery,
TriggerRefreshOptions,
Expand All @@ -22,7 +23,7 @@ export const IComponentAdapter = Symbol('IComponentAdapter');
export interface IComponentAdapter {
readonly onProgress: Event<ProgressNotificationEvent>;
triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>;
getRefreshPromise(): Promise<void> | undefined;
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
readonly onChanged: Event<PythonEnvironmentsChangedEvent>;
// VirtualEnvPrompt
onDidCreate(resource: Resource, callback: () => void): Disposable;
Expand Down Expand Up @@ -74,6 +75,7 @@ export const IInterpreterService = Symbol('IInterpreterService');
export interface IInterpreterService {
triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>;
readonly refreshPromise: Promise<void> | undefined;
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
readonly onDidChangeInterpreters: Event<PythonEnvironmentsChangedEvent>;
onDidChangeInterpreterConfiguration: Event<Uri | undefined>;
onDidChangeInterpreter: Event<Uri | undefined>;
Expand Down
10 changes: 9 additions & 1 deletion src/client/interpreter/interpreterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ import { Interpreters } from '../common/utils/localize';
import { sendTelemetryEvent } from '../telemetry';
import { EventName } from '../telemetry/constants';
import { cache } from '../common/utils/decorators';
import { PythonLocatorQuery, TriggerRefreshOptions } from '../pythonEnvironments/base/locator';
import {
GetRefreshEnvironmentsOptions,
PythonLocatorQuery,
TriggerRefreshOptions,
} from '../pythonEnvironments/base/locator';
import { sleep } from '../common/utils/async';

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

public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined {
return this.pyenvs.getRefreshPromise(options);
}

public get onDidChangeInterpreter(): Event<Uri | undefined> {
return this.didChangeInterpreterEmitter.event;
}
Expand Down
18 changes: 14 additions & 4 deletions src/client/languageServer/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { PylanceLSExtensionManager } from './pylanceLSExtensionManager';
import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types';
import { sendTelemetryEvent } from '../telemetry';
import { EventName } from '../telemetry/constants';
import { StopWatch } from '../common/utils/stopWatch';

@injectable()
/**
Expand Down Expand Up @@ -73,14 +74,18 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang

// IExtensionActivationService

public async activate(resource?: Resource): Promise<void> {
public async activate(resource?: Resource, startupStopWatch?: StopWatch): Promise<void> {
this.register();
await this.startLanguageServer(this.languageServerType, resource);
await this.startLanguageServer(this.languageServerType, resource, startupStopWatch);
}

// ILanguageServerWatcher
public async startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise<void> {
await this.startAndGetLanguageServer(languageServerType, resource);
public async startLanguageServer(
languageServerType: LanguageServerType,
resource?: Resource,
startupStopWatch?: StopWatch,
): Promise<void> {
await this.startAndGetLanguageServer(languageServerType, resource, startupStopWatch);
}

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

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

logStartup(languageServerType, lsResource);
Expand Down
2 changes: 1 addition & 1 deletion src/client/pythonEnvironments/base/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type PythonEnvUpdatedEvent<I = PythonEnvInfo> = {
/**
* The iteration index of The env info that was previously provided.
*/
index: number;
index?: number;
/**
* The env info that was previously provided.
*/
Expand Down
4 changes: 3 additions & 1 deletion src/client/pythonEnvironments/base/locatorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export async function getEnvs<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I
}
updatesDone.resolve();
listener.dispose();
} else {
} else if (event.index !== undefined) {
const { index, update } = event;
if (envs[index] === undefined) {
const json = JSON.stringify(update);
Expand All @@ -95,6 +95,8 @@ export async function getEnvs<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I
}
// We don't worry about if envs[index] is set already.
envs[index] = update;
} else if (event.update) {
envs.push(event.update);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ export abstract class LazyResourceBasedLocator extends Locator<BasicEnvInfo> imp
await this.disposables.dispose();
}

public async *iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<BasicEnvInfo> {
await this.activate();
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<BasicEnvInfo> {
const iterator = this.doIterEnvs(query);
const it = this._iterEnvs(iterator, query);
it.onUpdated = iterator.onUpdated;
return it;
}

private async *_iterEnvs(
iterator: IPythonEnvsIterator<BasicEnvInfo>,
query?: PythonLocatorQuery,
): IPythonEnvsIterator<BasicEnvInfo> {
await this.activate();
if (query?.envPath) {
let result = await iterator.next();
while (!result.done) {
Expand Down
Loading
Loading