Skip to content

Commit 8488dd6

Browse files
authored
telemetry and comments to breakdown startup flow (#1400)
see `docs/startup-flow.md`, this is the first commit to set up for larger changes to this process
1 parent c29567b commit 8488dd6

File tree

4 files changed

+182
-2
lines changed

4 files changed

+182
-2
lines changed

docs/startup-flow.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
2+
# Startup Flow for Python Environments Extension
3+
4+
5+
user opens VS Code
6+
python environments extension begins activation
7+
8+
SYNC (`activate` in extension.ts):
9+
1. create core objects: ProjectManager, EnvironmentManagers, ManagerReady
10+
2. `setPythonApi()` — API object created, deferred resolved (API is now available to consumers)
11+
3. create views (EnvManagerView, ProjectView), status bar, terminal manager
12+
4. register all commands
13+
5. activate() returns — extension is "active" from VS Code's perspective
14+
15+
📊 TELEMETRY: EXTENSION.ACTIVATION_DURATION { duration }
16+
17+
ASYNC (setImmediate callback, still in extension.ts):
18+
1. spawn PET process (`createNativePythonFinder`)
19+
1. sets up a JSON-RPC connection to it over stdin/stdout
20+
2. register all built-in managers in parallel (Promise.all):
21+
- for each manager (system, conda, pyenv, pipenv, poetry):
22+
1. check if tool exists (e.g. `getConda(nativeFinder)` asks PET for the conda binary)
23+
2. if tool not found → log, return early (manager not registered)
24+
3. if tool found → create manager, call `api.registerEnvironmentManager(manager)`
25+
- this adds it to the `EnvironmentManagers` map
26+
- fires `onDidChangeEnvironmentManager``ManagerReady` deferred resolves for this manager
27+
3. all registrations complete (Promise.all resolves)
28+
29+
--- gate point: `applyInitialEnvironmentSelection` ---
30+
📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount }
31+
32+
1. for each workspace folder + global scope (no workspace case), run `resolvePriorityChainCore` to find manager:
33+
- P1: pythonProjects[] setting → specific manager for this project
34+
- P2: user-configured defaultEnvManager setting
35+
- P3: user-configured python.defaultInterpreterPath → nativeFinder.resolve(path)
36+
- P4: auto-discovery → try venv manager (local .venv), fall back to system python
37+
- for workspace scope: ask venv manager if there's a local env (.venv/venv in the folder)
38+
- if found → use venv manager with that env
39+
- if not found → fall back to system python manager
40+
- for global scope: use system python manager directly
41+
42+
2. get the environment from the winning priority level:
43+
44+
--- fork point: `result.environment ?? await result.manager.get(folder.uri)` ---
45+
left side truthy = envPreResolved | left side undefined = managerDiscovery
46+
47+
envPreResolved — P3 won (interpreter → manager):
48+
`resolvePriorityChainCore` calls `tryResolveInterpreterPath()`:
49+
1. `nativeFinder.resolve(path)` — single PET call, resolves just this one binary
50+
2. find which manager owns the resolved env (by managerId)
51+
3. return { manager, environment } — BOTH are known
52+
→ result.environment is set → the `??` short-circuits
53+
→ no `manager.get()` called, no `initialize()`, no full discovery
54+
55+
managerDiscovery — P1, P2, or P4 won (manager → interpreter):
56+
`resolvePriorityChainCore` returns { manager, environment: undefined }
57+
→ result.environment is undefined → falls through to `await result.manager.get(scope)`
58+
`manager.get(scope)` (e.g. `CondaEnvManager.get()`):
59+
4. `initialize()` — lazy, once-only per manager (guarded by deferred)
60+
a. `nativeFinder.refresh(hardRefresh=false)`:
61+
`handleSoftRefresh()` checks in-memory cache (Map) for key 'all' (bc one big scan, shared cache, all managers benefit)
62+
- on reload: cache is empty (Map was destroyed) → cache miss
63+
- falls through to `handleHardRefresh()`
64+
`handleHardRefresh()`:
65+
- adds request to WorkerPool queue (concurrency 1, so serialized)
66+
- when its turn comes, calls `doRefresh()`:
67+
1. `configure()` — JSON-RPC to PET with search paths, conda/poetry/pipenv paths, cache dir
68+
2. `refresh` — JSON-RPC to PET, PET scans filesystem
69+
- PET may use its own on-disk cache (cacheDirectory) to speed this up
70+
- PET streams back results as 'environment' and 'manager' notifications
71+
- envs missing version/prefix get an inline resolve() call
72+
3. returns NativeInfo[] (all envs of all types)
73+
- result stored in in-memory cache under key 'all'
74+
→ subsequent managers calling nativeFinder.refresh(false) get cache hit → instant
75+
b. filter results to this manager's env type (e.g. conda filters to kind=conda)
76+
c. convert NativeEnvInfo → PythonEnvironment objects → populate collection
77+
d. `loadEnvMap()` — reads persisted env path from workspace state
78+
→ matches path against freshly discovered collection via `findEnvironmentByPath()`
79+
→ populates `fsPathToEnv` map
80+
5. look up scope in `fsPathToEnv` → return the matched env
81+
82+
📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration (priority chain + manager.get), scope, prioritySource, managerId, path, hasPersistedSelection }
83+
84+
3. env is cached in memory (no settings.json write)
85+
4. Python extension / status bar can now get the selected env via `api.getEnvironment(scope)`
86+
87+
📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration (activation→here), result, failureStage?, errorType? }
88+
89+
POST-INIT:
90+
1. register terminal package watcher
91+
2. register settings change listener (`registerInterpreterSettingsChangeListener`) — re-runs priority chain if settings change
92+
3. initialize terminal manager
93+
4. send telemetry (manager selection, project structure, discovery summary)

src/common/telemetry/constants.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,24 @@ export enum EventNames {
8282
* - errorType: string (classified error category, on failure only)
8383
*/
8484
PET_INIT_DURATION = 'PET.INIT_DURATION',
85+
/**
86+
* Telemetry event fired when applyInitialEnvironmentSelection begins.
87+
* Signals that all managers are registered and env selection is starting.
88+
* Properties:
89+
* - registeredManagerCount: number (how many env managers registered)
90+
* - workspaceFolderCount: number (how many workspace folders to process)
91+
*/
92+
ENV_SELECTION_STARTED = 'ENV_SELECTION.STARTED',
93+
/**
94+
* Telemetry event fired per scope when the priority chain resolves.
95+
* Properties:
96+
* - scope: string ('workspace' or 'global')
97+
* - prioritySource: string (which priority won: 'pythonProjects', 'defaultEnvManager', 'defaultInterpreterPath', 'autoDiscovery')
98+
* - managerId: string (the winning manager's id)
99+
* - resolutionPath: string ('envPreResolved' = env already resolved, 'managerDiscovery' = needed full discovery)
100+
* - hasPersistedSelection: boolean (whether a persisted env path existed in workspace state)
101+
*/
102+
ENV_SELECTION_RESULT = 'ENV_SELECTION.RESULT',
85103
}
86104

87105
// Map all events to their properties
@@ -322,4 +340,34 @@ export interface IEventNamePropertyMapping {
322340
result: 'success' | 'error' | 'timeout';
323341
errorType?: string;
324342
};
343+
344+
/* __GDPR__
345+
"env_selection.started": {
346+
"registeredManagerCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
347+
"registeredManagerIds": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
348+
"workspaceFolderCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
349+
}
350+
*/
351+
[EventNames.ENV_SELECTION_STARTED]: {
352+
registeredManagerCount: number;
353+
registeredManagerIds: string;
354+
workspaceFolderCount: number;
355+
};
356+
357+
/* __GDPR__
358+
"env_selection.result": {
359+
"scope": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
360+
"prioritySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
361+
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
362+
"resolutionPath": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
363+
"hasPersistedSelection": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
364+
}
365+
*/
366+
[EventNames.ENV_SELECTION_RESULT]: {
367+
scope: string;
368+
prioritySource: string;
369+
managerId: string;
370+
resolutionPath: string;
371+
hasPersistedSelection: boolean;
372+
};
325373
}

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
611611
]);
612612

613613
failureStage = 'envSelection';
614-
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
614+
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api, start.elapsedTime);
615615

616616
// Register manager-agnostic terminal watcher for package-modifying commands
617617
failureStage = 'terminalWatcher';

src/features/interpreterSelection.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { commands, ConfigurationChangeEvent, Disposable, l10n, Uri } from 'vscod
66
import { PythonEnvironment, PythonEnvironmentApi } from '../api';
77
import { SYSTEM_MANAGER_ID, VENV_MANAGER_ID } from '../common/constants';
88
import { traceError, traceInfo, traceVerbose, traceWarn } from '../common/logging';
9+
import { StopWatch } from '../common/stopWatch';
10+
import { EventNames } from '../common/telemetry/constants';
11+
import { sendTelemetryEvent } from '../common/telemetry/sender';
912
import { resolveVariables } from '../common/utils/internalVariables';
1013
import { showWarningMessage } from '../common/window.apis';
1114
import {
@@ -140,7 +143,12 @@ async function resolvePriorityChainCore(
140143
};
141144
errors.push(error);
142145
} else {
143-
const resolved = await tryResolveInterpreterPath(nativeFinder, api, expandedInterpreterPath, envManagers);
146+
const resolved = await tryResolveInterpreterPath(
147+
nativeFinder,
148+
api,
149+
expandedInterpreterPath,
150+
envManagers,
151+
);
144152
if (resolved) {
145153
traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`);
146154
return { result: resolved, errors };
@@ -275,16 +283,25 @@ export async function applyInitialEnvironmentSelection(
275283
projectManager: PythonProjectManager,
276284
nativeFinder: NativePythonFinder,
277285
api: PythonEnvironmentApi,
286+
activationToReadyDurationMs?: number,
278287
): Promise<void> {
279288
const folders = getWorkspaceFolders() ?? [];
280289
traceInfo(
281290
`[interpreterSelection] Applying initial environment selection for ${folders.length} workspace folder(s)`,
282291
);
283292

293+
// Checkpoint 1: env selection starting — managers are registered
294+
sendTelemetryEvent(EventNames.ENV_SELECTION_STARTED, activationToReadyDurationMs, {
295+
registeredManagerCount: envManagers.managers.length,
296+
registeredManagerIds: envManagers.managers.map((m) => m.id).join(','),
297+
workspaceFolderCount: folders.length,
298+
});
299+
284300
const allErrors: SettingResolutionError[] = [];
285301

286302
for (const folder of folders) {
287303
try {
304+
const scopeStopWatch = new StopWatch();
288305
const { result, errors } = await resolvePriorityChainCore(
289306
folder.uri,
290307
envManagers,
@@ -294,9 +311,20 @@ export async function applyInitialEnvironmentSelection(
294311
);
295312
allErrors.push(...errors);
296313

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

320+
sendTelemetryEvent(EventNames.ENV_SELECTION_RESULT, scopeStopWatch.elapsedTime, {
321+
scope: 'workspace',
322+
prioritySource: result.source,
323+
managerId: result.manager.id,
324+
resolutionPath: isPathA ? 'envPreResolved' : 'managerDiscovery',
325+
hasPersistedSelection: env !== undefined,
326+
});
327+
300328
// Cache only — NO settings.json write (shouldPersistSettings = false)
301329
await envManagers.setEnvironment(folder.uri, env, false);
302330

@@ -311,12 +339,23 @@ export async function applyInitialEnvironmentSelection(
311339
// Also apply initial selection for global scope (no workspace folder)
312340
// This ensures defaultInterpreterPath is respected even without a workspace
313341
try {
342+
const globalStopWatch = new StopWatch();
314343
const { result, errors } = await resolvePriorityChainCore(undefined, envManagers, undefined, nativeFinder, api);
315344
allErrors.push(...errors);
316345

346+
const isPathA = result.environment !== undefined;
347+
317348
// Get the specific environment if not already resolved
318349
const env = result.environment ?? (await result.manager.get(undefined));
319350

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+
});
358+
320359
// Cache only — NO settings.json write (shouldPersistSettings = false)
321360
await envManagers.setEnvironments('global', env, false);
322361

0 commit comments

Comments
 (0)