Skip to content

Commit c5a1430

Browse files
eleanorjboydCopilot
andauthored
Don't block on global scope during ext startup (#1456)
When at least one workspace folder resolved successfully, don't await the global scope resolution. Run it as a background task instead of blocking startup on it. For more than 3/4ths of slow sessions where the workspace env resolves fast (via `envPreResolved`), this should make the status bar update near-instantly. Files opened outside the workspace folder wouldn't have a Python env immediately available. They would resolve moments later when the background task completes. --------- Co-authored-by: Copilot <copilot@github.com>
1 parent f415f07 commit c5a1430

3 files changed

Lines changed: 353 additions & 29 deletions

File tree

src/common/telemetry/constants.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ export enum EventNames {
101101
* - hasPersistedSelection: boolean (whether a persisted env path existed in workspace state)
102102
*/
103103
ENV_SELECTION_RESULT = 'ENV_SELECTION.RESULT',
104+
/**
105+
* Telemetry event fired when applyInitialEnvironmentSelection returns.
106+
* Duration measures the blocking time (excludes deferred global scope).
107+
* Properties:
108+
* - globalScopeDeferred: boolean (true = global scope fired in background, false = awaited)
109+
* - workspaceFolderCount: number (total workspace folders)
110+
* - resolvedFolderCount: number (folders that resolved with a non-undefined env)
111+
* - settingErrorCount: number (user-configured settings that could not be applied)
112+
*/
113+
ENV_SELECTION_COMPLETED = 'ENV_SELECTION.COMPLETED',
104114
/**
105115
* Telemetry event fired when a lazily-registered manager completes its first initialization.
106116
* Replaces MANAGER_REGISTRATION_SKIPPED and MANAGER_REGISTRATION_FAILED for managers
@@ -431,6 +441,21 @@ export interface IEventNamePropertyMapping {
431441
hasPersistedSelection: boolean;
432442
};
433443

444+
/* __GDPR__
445+
"env_selection.completed": {
446+
"globalScopeDeferred": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
447+
"workspaceFolderCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
448+
"resolvedFolderCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
449+
"settingErrorCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
450+
}
451+
*/
452+
[EventNames.ENV_SELECTION_COMPLETED]: {
453+
globalScopeDeferred: boolean;
454+
workspaceFolderCount: number;
455+
resolvedFolderCount: number;
456+
settingErrorCount: number;
457+
};
458+
434459
/* __GDPR__
435460
"manager.lazy_init": {
436461
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },

src/features/interpreterSelection.ts

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,16 @@ export async function applyInitialEnvironmentSelection(
290290
`[interpreterSelection] Applying initial environment selection for ${folders.length} workspace folder(s)`,
291291
);
292292

293-
// Checkpoint 1: env selection starting — managers are registered
294293
sendTelemetryEvent(EventNames.ENV_SELECTION_STARTED, activationToReadyDurationMs, {
295294
registeredManagerCount: envManagers.managers.length,
296295
registeredManagerIds: envManagers.managers.map((m) => m.id).join(','),
297296
workspaceFolderCount: folders.length,
298297
});
299298

300299
const allErrors: SettingResolutionError[] = [];
300+
let workspaceFolderResolved = false;
301+
let resolvedFolderCount = 0;
302+
const selectionStopWatch = new StopWatch();
301303

302304
for (const folder of folders) {
303305
try {
@@ -311,23 +313,24 @@ export async function applyInitialEnvironmentSelection(
311313
);
312314
allErrors.push(...errors);
313315

314-
// Checkpoint 2: priority chain resolved — which path?
315-
const isPathA = result.environment !== undefined;
316-
317-
// Get the specific environment if not already resolved
318316
const env = result.environment ?? (await result.manager.get(folder.uri));
319317

320318
sendTelemetryEvent(EventNames.ENV_SELECTION_RESULT, scopeStopWatch.elapsedTime, {
321319
scope: 'workspace',
322320
prioritySource: result.source,
323321
managerId: result.manager.id,
324-
resolutionPath: isPathA ? 'envPreResolved' : 'managerDiscovery',
322+
resolutionPath: result.environment ? 'envPreResolved' : 'managerDiscovery',
325323
hasPersistedSelection: env !== undefined,
326324
});
327325

328326
// Cache only — NO settings.json write (shouldPersistSettings = false)
329327
await envManagers.setEnvironment(folder.uri, env, false);
330328

329+
if (env) {
330+
workspaceFolderResolved = true;
331+
resolvedFolderCount++;
332+
}
333+
331334
traceInfo(
332335
`[interpreterSelection] ${folder.name}: ${env?.displayName ?? 'none'} (source: ${result.source})`,
333336
);
@@ -336,49 +339,94 @@ export async function applyInitialEnvironmentSelection(
336339
}
337340
}
338341

339-
// Also apply initial selection for global scope (no workspace folder)
340-
// This ensures defaultInterpreterPath is respected even without a workspace
341-
try {
342-
const globalStopWatch = new StopWatch();
343-
const { result, errors } = await resolvePriorityChainCore(undefined, envManagers, undefined, nativeFinder, api);
344-
allErrors.push(...errors);
342+
// Resolve global scope (fallback for files outside workspace folders).
343+
// Deferred to background when a workspace folder already resolved.
344+
const resolveGlobalScope = async (): Promise<SettingResolutionError[]> => {
345+
try {
346+
const globalStopWatch = new StopWatch();
347+
const { result, errors: globalErrors } = await resolvePriorityChainCore(
348+
undefined,
349+
envManagers,
350+
undefined,
351+
nativeFinder,
352+
api,
353+
);
345354

346-
const isPathA = result.environment !== undefined;
355+
const env = result.environment ?? (await result.manager.get(undefined));
347356

348-
// Get the specific environment if not already resolved
349-
const env = result.environment ?? (await result.manager.get(undefined));
357+
sendTelemetryEvent(EventNames.ENV_SELECTION_RESULT, globalStopWatch.elapsedTime, {
358+
scope: 'global',
359+
prioritySource: result.source,
360+
managerId: result.manager.id,
361+
resolutionPath: result.environment ? 'envPreResolved' : 'managerDiscovery',
362+
hasPersistedSelection: env !== undefined,
363+
});
350364

351-
sendTelemetryEvent(EventNames.ENV_SELECTION_RESULT, globalStopWatch.elapsedTime, {
352-
scope: 'global',
353-
prioritySource: result.source,
354-
managerId: result.manager.id,
355-
resolutionPath: isPathA ? 'envPreResolved' : 'managerDiscovery',
356-
hasPersistedSelection: env !== undefined,
357-
});
365+
// Cache only — NO settings.json write
366+
await envManagers.setEnvironments('global', env, false);
358367

359-
// Cache only — NO settings.json write (shouldPersistSettings = false)
360-
await envManagers.setEnvironments('global', env, false);
368+
traceInfo(`[interpreterSelection] global: ${env?.displayName ?? 'none'} (source: ${result.source})`);
361369

362-
traceInfo(`[interpreterSelection] global: ${env?.displayName ?? 'none'} (source: ${result.source})`);
363-
} catch (err) {
364-
traceError(`[interpreterSelection] Failed to set global environment: ${err}`);
370+
return globalErrors;
371+
} catch (err) {
372+
traceError(`[interpreterSelection] Failed to set global environment: ${err}`);
373+
return [];
374+
}
375+
};
376+
377+
if (workspaceFolderResolved) {
378+
// Defer global scope so it doesn't block post-selection startup.
379+
traceInfo('[interpreterSelection] Workspace env resolved, deferring global scope to background');
380+
resolveGlobalScope()
381+
.then(async (globalErrors) => {
382+
if (globalErrors.length > 0) {
383+
await notifyUserOfSettingErrors(globalErrors);
384+
}
385+
})
386+
.catch((err) => traceError(`[interpreterSelection] Background global scope resolution failed: ${err}`));
387+
} else {
388+
// No workspace folder resolved — global scope is the primary fallback, must await.
389+
const globalErrors = await resolveGlobalScope();
390+
allErrors.push(...globalErrors);
365391
}
366392

367-
// Notify user if any settings could not be applied
393+
// Notify user if any settings could not be applied (workspace + global when awaited)
368394
if (allErrors.length > 0) {
369395
await notifyUserOfSettingErrors(allErrors);
370396
}
397+
398+
// Duration measures blocking time only (excludes deferred global scope).
399+
sendTelemetryEvent(EventNames.ENV_SELECTION_COMPLETED, selectionStopWatch.elapsedTime, {
400+
globalScopeDeferred: workspaceFolderResolved,
401+
workspaceFolderCount: folders.length,
402+
resolvedFolderCount,
403+
settingErrorCount: allErrors.length,
404+
});
371405
}
372406

373407
/**
374408
* Notify the user when their configured settings could not be applied.
375409
* Shows a warning message with an option to open settings.
410+
* Tracks already-warned settings to avoid duplicate dialogs (e.g., when
411+
* the same user-level misconfiguration is hit by both workspace and
412+
* deferred global scope resolution).
376413
*/
414+
const warnedSettings = new Set<string>();
415+
416+
export function resetSettingWarnings(): void {
417+
warnedSettings.clear();
418+
}
419+
377420
async function notifyUserOfSettingErrors(errors: SettingResolutionError[]): Promise<void> {
378421
// Group errors by setting type to avoid spamming the user
379422
const uniqueSettings = [...new Set(errors.map((e) => e.setting))];
380423

381424
for (const setting of uniqueSettings) {
425+
if (warnedSettings.has(setting)) {
426+
continue;
427+
}
428+
warnedSettings.add(setting);
429+
382430
const settingErrors = errors.filter((e) => e.setting === setting);
383431
const firstError = settingErrors[0];
384432

0 commit comments

Comments
 (0)