user opens VS Code python environments extension begins activation
SYNC (activate in extension.ts):
-
create StatusBar, ProjectManager, EnvVarManager, EnvironmentManagers, ManagerReady
-
create TerminalActivation, shell providers, TerminalManager
-
create ProjectCreators
-
setPythonApi()— API object created, deferred resolved (API is now available to consumers) -
create views (EnvManagerView, ProjectView)
-
register all commands
-
activate() returns — extension is "active" from VS Code's perspective
📊 TELEMETRY: EXTENSION.ACTIVATION_DURATION { duration }
ASYNC (setImmediate callback, still in extension.ts):
- spawn PET process (
createNativePythonFinder)- sets up a JSON-RPC connection to it over stdin/stdout
- register all built-in managers + shell env init in parallel (Promise.all):
shellStartupVarsMgr.initialize()- for each manager (system, conda, pyenv, pipenv, poetry):
- check if tool exists (e.g.
getConda(nativeFinder)asks PET for the conda binary) - if tool not found → log, return early (manager not registered)
- if tool found → create manager, call
api.registerEnvironmentManager(manager)- this adds it to the
EnvironmentManagersmap - fires
onDidChangeEnvironmentManager→ManagerReadydeferred resolves for this manager
- this adds it to the
- check if tool exists (e.g.
- all registrations complete (Promise.all resolves)
--- gate point: applyInitialEnvironmentSelection ---
📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount }
-
for each workspace folder + global scope (no workspace case), run
resolvePriorityChainCoreto find manager:- P1: pythonProjects[] setting → specific manager for this project
- P2: user-configured defaultEnvManager setting
- P3: user-configured python.defaultInterpreterPath → nativeFinder.resolve(path)
- P4: auto-discovery → try venv manager, fall back to system python
- for workspace scope: call
venvManager.get(scope)- if venv found (local .venv/venv) → use venv manager with that env
- if no local venv → venv manager may still return its
globalEnv(system Python) - if venvManager.get returns undefined → fall back to system python manager
- for global scope: use system python manager directly
- for workspace scope: call
-
get the environment from the winning priority level:
--- fork point:
result.environment ?? await result.manager.get(folder.uri)--- left side truthy = envPreResolved | left side undefined = managerDiscoveryenvPreResolved — P3 won (interpreter → manager):
resolvePriorityChainCorecallstryResolveInterpreterPath(): 1.nativeFinder.resolve(path)— single PET call, resolves just this one binary 2. find which manager owns the resolved env (by managerId) 3. return { manager, environment } — BOTH are known → result.environment is set → the??short-circuits → nomanager.get()called, noinitialize(), no full discoverymanagerDiscovery — P1, P2, or P4 won (manager → interpreter):
resolvePriorityChainCorereturns { manager, environment: undefined } → falls through toawait result.manager.get(scope)**--- inner fork: fast path vs slow path (tryFastPathGet in fastPath.ts) ---** three conditions checked: a. `_initialized` deferred is undefined (never created) OR has not yet completed b. scope is a `Uri` (not global/undefined) c. a persisted env path exists in workspace state for this scope (folder Uri) FAST PATH (run if above three conditions are true): **Race-condition safety (runs before any await):** 1. if `_initialized` doesn't exist yet: - create deferred and **register immediately** via `setInitialized()` callback - this blocks concurrent callers from spawning duplicate background inits - kick off `startBackgroundInit()` as fire-and-forget 2. get project fsPath: `getProjectFsPathForScope(api, scope)` - prefers resolved project path if available, falls back to scope.fsPath - shared across all managers to avoid lambda duplication 3. read persisted path 4. `resolve(persistedPath)` 1. failure → see SLOW PATH 2. successful → return env immediately (background init continues in parallel) **Failure recovery (in startBackgroundInit error handler):** - if background init throws: `setInitialized(undefined)` — clear deferred so next `get()` call retries init SLOW PATH — fast path conditions not met, or fast path failed: 4. `initialize()` — lazy, once-only per manager (guarded by `_initialized` deferred) **Once-only guarantee:** - first caller creates `_initialized` deferred (if not already created by fast path) - concurrent callers see the existing deferred and await it instead of re-running init - deferred is **not cleared on failure** here (unlike in fast-path background handler) so only one init attempt runs, but subsequent calls still await the same failed init **Note:** In the fast path, if background init fails, the deferred is cleared to allow retry a. `nativeFinder.refresh(hardRefresh=false)`: → internally calls `handleSoftRefresh()` → computes cache key from options - on reload: cache is empty (Map was destroyed) → cache miss - falls through to `handleHardRefresh()` → `handleHardRefresh()` adds request to WorkerPool queue (concurrency 1): 1. run `configure()` to setup PET search paths 2. run `refresh` — PET scans filesystem - PET may use its own on-disk cache 3. returns NativeInfo[] (all envs of all types) - result stored in in-memory cache so subsequent managers get instant cache hit b. filter results to this manager's env type (e.g. conda filters to kind=conda) c. convert NativeEnvInfo → PythonEnvironment objects → populate collection d. `loadEnvMap()` — reads persisted env path from workspace state → matches path against PET discovery results → populates `fsPathToEnv` map 5. look up scope in `fsPathToEnv` → return the matched env📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration (priority chain + manager.get), scope, prioritySource, managerId, path, hasPersistedSelection }
-
env is cached in memory (no settings.json write)
-
Python extension / status bar can now get the selected env via
api.getEnvironment(scope)📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration (activation→here), result, failureStage?, errorType? }
POST-INIT:
- register terminal package watcher
- register settings change listener (
registerInterpreterSettingsChangeListener) — re-runs priority chain if settings change - initialize terminal manager
- send telemetry (manager selection, project structure, discovery summary)