55user opens VS Code
66python environments extension begins activation
77
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
8+ ** SYNC (` activate ` in extension.ts):**
9+ 1 . create StatusBar, ProjectManager, EnvVarManager, EnvironmentManagers, ManagerReady
10+ 2 . create TerminalActivation, shell providers, TerminalManager
11+ 3 . create ProjectCreators
12+ 4 . ` setPythonApi() ` — API object created, deferred resolved (API is now available to consumers)
13+ 5 . create views (EnvManagerView, ProjectView)
14+ 6 . register all commands
15+ 7 . activate() returns — extension is "active" from VS Code's perspective
1416
1517 📊 TELEMETRY: EXTENSION.ACTIVATION_DURATION { duration }
1618
17- ASYNC (setImmediate callback, still in extension.ts):
19+ ** ASYNC (setImmediate callback, still in extension.ts):**
18201 . spawn PET process (` createNativePythonFinder ` )
1921 1 . sets up a JSON-RPC connection to it over stdin/stdout
20- 2 . register all built-in managers in parallel (Promise.all):
22+ 2 . register all built-in managers + shell env init in parallel (Promise.all):
23+ - ` shellStartupVarsMgr.initialize() `
2124 - for each manager (system, conda, pyenv, pipenv, poetry):
2225 1 . check if tool exists (e.g. ` getConda(nativeFinder) ` asks PET for the conda binary)
2326 2 . if tool not found → log, return early (manager not registered)
@@ -26,17 +29,19 @@ ASYNC (setImmediate callback, still in extension.ts):
2629 - fires ` onDidChangeEnvironmentManager ` → ` ManagerReady ` deferred resolves for this manager
27303 . all registrations complete (Promise.all resolves)
2831
29- --- gate point: ` applyInitialEnvironmentSelection ` ---
32+ ** --- gate point: ` applyInitialEnvironmentSelection ` ---**
33+
3034 📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount }
3135
32361 . for each workspace folder + global scope (no workspace case), run ` resolvePriorityChainCore ` to find manager:
3337 - P1: pythonProjects[ ] setting → specific manager for this project
3438 - P2: user-configured defaultEnvManager setting
3539 - 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+ - P4: auto-discovery → try venv manager, fall back to system python
41+ - for workspace scope: call ` venvManager.get(scope) `
42+ - if venv found (local .venv/venv) → use venv manager with that env
43+ - if no local venv → venv manager may still return its ` globalEnv ` (system Python)
44+ - if venvManager.get returns undefined → fall back to system python manager
4045 - for global scope: use system python manager directly
4146
42472 . get the environment from the winning priority level:
@@ -54,30 +59,55 @@ ASYNC (setImmediate callback, still in extension.ts):
5459
5560 managerDiscovery — P1, P2, or P4 won (manager → interpreter):
5661 ` 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
62+ → falls through to ` await result.manager.get(scope) `
63+
64+ **--- inner fork: fast path vs slow path (tryFastPathGet in fastPath.ts) ---**
65+ Conditions checked before entering fast path:
66+ a. ` _initialized ` deferred is undefined (never created) OR has not yet completed
67+ b. scope is a ` Uri ` (not global/undefined)
68+
69+ FAST PATH (background init kickoff + optional early return):
70+ **Race-condition safety (runs before any await):**
71+ 1. if `_initialized` doesn't exist yet:
72+ - create deferred and **register immediately** via `setInitialized()` callback
73+ - this blocks concurrent callers from spawning duplicate background inits
74+ - kick off `startBackgroundInit()` as fire-and-forget
75+ - this happens as soon as (a) and (b) are true, **even if** no persisted path exists
76+ 2. get project fsPath: `getProjectFsPathForScope(api, scope)`
77+ - prefers resolved project path if available, falls back to scope.fsPath
78+ - shared across all managers to avoid lambda duplication
79+ 3. read persisted path (only if scope is a `Uri`; may return undefined)
80+ 4. if a persisted path exists:
81+ - attempt `resolve(persistedPath)`
82+ - failure (no env, mismatched manager, etc.) → fall through to SLOW PATH
83+ - success → return env immediately (background init continues in parallel)
84+ **Failure recovery (in startBackgroundInit error handler):**
85+ - if background init throws: `setInitialized(undefined)` — clear deferred so next `get()` call retries init
86+
87+ SLOW PATH — fast path conditions not met, or fast path failed:
88+ 4. `initialize()` — lazy, once-only per manager (guarded by `_initialized` deferred)
89+ **Once-only guarantee:**
90+ - first caller creates `_initialized` deferred (if not already created by fast path)
91+ - concurrent callers see the existing deferred and await it instead of re-running init
92+ - deferred is **not cleared on failure** here (unlike in fast-path background handler)
93+ so only one init attempt runs, but subsequent calls still await the same failed init
94+ **Note:** In the fast path, if background init fails, the deferred is cleared to allow retry
95+ a. `nativeFinder.refresh(hardRefresh=false)`:
96+ → internally calls `handleSoftRefresh()` → computes cache key from options
97+ - on reload: cache is empty (Map was destroyed) → cache miss
98+ - falls through to `handleHardRefresh()`
99+ → `handleHardRefresh()` adds request to WorkerPool queue (concurrency 1):
100+ 1. run `configure()` to setup PET search paths
101+ 2. run `refresh` — PET scans filesystem
102+ - PET may use its own on-disk cache
103+ 3. returns NativeInfo[] (all envs of all types)
104+ - result stored in in-memory cache so subsequent managers get instant cache hit
105+ b. filter results to this manager's env type (e.g. conda filters to kind=conda)
106+ c. convert NativeEnvInfo → PythonEnvironment objects → populate collection
107+ d. `loadEnvMap()` — reads persisted env path from workspace state
108+ → matches path against PET discovery results
109+ → populates `fsPathToEnv` map
110+ 5. look up scope in `fsPathToEnv` → return the matched env
81111
82112 📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration (priority chain + manager.get), scope, prioritySource, managerId, path, hasPersistedSelection }
83113
@@ -86,8 +116,8 @@ ASYNC (setImmediate callback, still in extension.ts):
86116
87117 📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration (activation→here), result, failureStage?, errorType? }
88118
89- POST-INIT:
119+ ** POST-INIT:**
901201 . register terminal package watcher
911212 . register settings change listener (` registerInterpreterSettingsChangeListener ` ) — re-runs priority chain if settings change
921223 . initialize terminal manager
93- 4 . send telemetry (manager selection, project structure, discovery summary)
123+ 4 . send telemetry (manager selection, project structure, discovery summary)
0 commit comments