Skip to content

Commit dafa89c

Browse files
authored
Merge branch 'main' into zygotic-mosquito
2 parents 5900317 + 4e46444 commit dafa89c

15 files changed

+1299
-79
lines changed

docs/startup-flow.md

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
user opens VS Code
66
python 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):**
1820
1. spawn PET process (`createNativePythonFinder`)
1921
1. sets up a JSON-RPC connection to it over stdin/stdout
2022
2. register all built-in managers in parallel (Promise.all):
@@ -35,17 +37,20 @@ ASYNC (setImmediate callback, still in extension.ts):
3537
- all managers fire `onDidChangeEnvironmentManager` → ManagerReady resolves
3638
3. all registrations complete (Promise.all resolves) — fast, typically milliseconds
3739

38-
--- gate point: `applyInitialEnvironmentSelection` ---
40+
41+
**--- gate point: `applyInitialEnvironmentSelection` ---**
42+
3943
📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount }
4044

4145
1. for each workspace folder + global scope (no workspace case), run `resolvePriorityChainCore` to find manager:
4246
- P1: pythonProjects[] setting → specific manager for this project
4347
- P2: user-configured defaultEnvManager setting
4448
- P3: user-configured python.defaultInterpreterPath → nativeFinder.resolve(path)
45-
- P4: auto-discovery → try venv manager (local .venv), fall back to system python
46-
- for workspace scope: ask venv manager if there's a local env (.venv/venv in the folder)
47-
- if found → use venv manager with that env
48-
- if not found → fall back to system python manager
49+
- P4: auto-discovery → try venv manager, fall back to system python
50+
- for workspace scope: call `venvManager.get(scope)`
51+
- if venv found (local .venv/venv) → use venv manager with that env
52+
- if no local venv → venv manager may still return its `globalEnv` (system Python)
53+
- if venvManager.get returns undefined → fall back to system python manager
4954
- for global scope: use system python manager directly
5055

5156
2. get the environment from the winning priority level:
@@ -63,33 +68,55 @@ ASYNC (setImmediate callback, still in extension.ts):
6368

6469
managerDiscovery — P1, P2, or P4 won (manager → interpreter):
6570
`resolvePriorityChainCore` returns { manager, environment: undefined }
66-
→ result.environment is undefined → falls through to `await result.manager.get(scope)`
67-
`manager.get(scope)` (e.g. `CondaEnvManager.get()`, `PyEnvManager.get()`):
68-
4. `initialize()` — lazy, once-only per manager (guarded by deferred)
69-
a. `nativeFinder.refresh(hardRefresh=false)`:
70-
`handleSoftRefresh()` checks in-memory cache (Map) for key 'all'
71-
- on reload: cache is empty (Map was destroyed) → cache miss
72-
- falls through to `handleHardRefresh()`
73-
`handleHardRefresh()`:
74-
- adds request to WorkerPool queue (concurrency 1, so serialized)
75-
- when its turn comes, calls `doRefresh()`:
76-
1. `configure()` — JSON-RPC to PET with search paths, conda/poetry/pipenv paths, cache dir
77-
2. `refresh` — JSON-RPC to PET, PET scans filesystem
78-
- PET has had time to warm up since spawn (registration was fast)
79-
- PET may use its own on-disk cache (cacheDirectory) to speed this up
80-
- PET streams back results as 'environment' and 'manager' notifications
81-
- envs missing version/prefix get an inline resolve() call
82-
3. returns NativeInfo[] (all envs of all types)
83-
- result stored in in-memory cache under key 'all'
84-
→ subsequent managers calling nativeFinder.refresh(false) get cache hit → instant
85-
b. filter results to this manager's env type (e.g. conda filters to kind=conda, pyenv to kind=pyenv)
86-
c. for pipenv/poetry/pyenv: if tool CLI was not found via PATH during registration,
87-
extract tool executable from PET's manager info in the refresh results
88-
d. convert NativeEnvInfo → PythonEnvironment objects → populate collection
89-
e. `loadEnvMap()` — reads persisted env path from workspace state
90-
→ matches path against freshly discovered collection via `findEnvironmentByPath()`
91-
→ populates `fsPathToEnv` map
92-
5. look up scope in `fsPathToEnv` → return the matched env
71+
→ falls through to `await result.manager.get(scope)`
72+
73+
**--- inner fork: fast path vs slow path (tryFastPathGet in fastPath.ts) ---**
74+
Conditions checked before entering fast path:
75+
a. `_initialized` deferred is undefined (never created) OR has not yet completed
76+
b. scope is a `Uri` (not global/undefined)
77+
78+
FAST PATH (background init kickoff + optional early return):
79+
**Race-condition safety (runs before any await):**
80+
1. if `_initialized` doesn't exist yet:
81+
- create deferred and **register immediately** via `setInitialized()` callback
82+
- this blocks concurrent callers from spawning duplicate background inits
83+
- kick off `startBackgroundInit()` as fire-and-forget
84+
- this happens as soon as (a) and (b) are true, **even if** no persisted path exists
85+
2. get project fsPath: `getProjectFsPathForScope(api, scope)`
86+
- prefers resolved project path if available, falls back to scope.fsPath
87+
- shared across all managers to avoid lambda duplication
88+
3. read persisted path (only if scope is a `Uri`; may return undefined)
89+
4. if a persisted path exists:
90+
- attempt `resolve(persistedPath)`
91+
- failure (no env, mismatched manager, etc.) → fall through to SLOW PATH
92+
- success → return env immediately (background init continues in parallel)
93+
**Failure recovery (in startBackgroundInit error handler):**
94+
- if background init throws: `setInitialized(undefined)` — clear deferred so next `get()` call retries init
95+
96+
SLOW PATH — fast path conditions not met, or fast path failed:
97+
4. `initialize()` — lazy, once-only per manager (guarded by `_initialized` deferred)
98+
**Once-only guarantee:**
99+
- first caller creates `_initialized` deferred (if not already created by fast path)
100+
- concurrent callers see the existing deferred and await it instead of re-running init
101+
- deferred is **not cleared on failure** here (unlike in fast-path background handler)
102+
so only one init attempt runs, but subsequent calls still await the same failed init
103+
**Note:** In the fast path, if background init fails, the deferred is cleared to allow retry
104+
a. `nativeFinder.refresh(hardRefresh=false)`:
105+
→ internally calls `handleSoftRefresh()` → computes cache key from options
106+
- on reload: cache is empty (Map was destroyed) → cache miss
107+
- falls through to `handleHardRefresh()`
108+
→ `handleHardRefresh()` adds request to WorkerPool queue (concurrency 1):
109+
1. run `configure()` to setup PET search paths
110+
2. run `refresh` — PET scans filesystem
111+
- PET may use its own on-disk cache
112+
3. returns NativeInfo[] (all envs of all types)
113+
- result stored in in-memory cache so subsequent managers get instant cache hit
114+
b. filter results to this manager's env type (e.g. conda filters to kind=conda)
115+
c. convert NativeEnvInfo → PythonEnvironment objects → populate collection
116+
d. `loadEnvMap()` — reads persisted env path from workspace state
117+
→ matches path against PET discovery results
118+
→ populates `fsPathToEnv` map
119+
5. look up scope in `fsPathToEnv` → return the matched env
93120

94121
📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration (priority chain + manager.get), scope, prioritySource, managerId, path, hasPersistedSelection }
95122

@@ -111,4 +138,4 @@ POST-INIT:
111138
1. register terminal package watcher
112139
2. register settings change listener (`registerInterpreterSettingsChangeListener`) — re-runs priority chain if settings change
113140
3. initialize terminal manager
114-
4. send telemetry (manager selection, project structure, discovery summary)
141+
4. send telemetry (manager selection, project structure, discovery summary)

package-lock.json

Lines changed: 31 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vscode-python-envs",
33
"displayName": "Python Environments",
44
"description": "Provides a unified python environment experience",
5-
"version": "1.26.0",
5+
"version": "1.27.0",
66
"publisher": "ms-python",
77
"preview": true,
88
"engines": {

src/features/envCommands.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import { removePythonProjectSetting, setEnvironmentManager, setPackageManager }
3030

3131
import { executeCommand } from '../common/command.api';
3232
import { clipboardWriteText } from '../common/env.apis';
33-
import {} from '../common/errors/utils';
3433
import { Pickers } from '../common/localize';
3534
import { pickEnvironment } from '../common/pickers/environments';
3635
import {
@@ -606,8 +605,10 @@ export async function createTerminalCommand(
606605
api: PythonEnvironmentApi,
607606
tm: TerminalManager,
608607
): Promise<Terminal | undefined> {
609-
if (context === undefined) {
610-
const pw = await pickProject(api.getPythonProjects());
608+
const pythonProjects = api.getPythonProjects();
609+
// If no context is provided, or there are multiple projects, prompt the user to select a project for the terminal's cwd
610+
if (context === undefined || pythonProjects.length > 0) {
611+
const pw = await pickProject(pythonProjects);
611612
if (pw) {
612613
const env = await api.getEnvironment(pw.uri);
613614
const cwd = await findParentIfFile(pw.uri.fsPath);
@@ -641,7 +642,7 @@ export async function createTerminalCommand(
641642
}
642643
} else if (context instanceof PythonEnvTreeItem) {
643644
const view = context as PythonEnvTreeItem;
644-
const pw = await pickProject(api.getPythonProjects());
645+
const pw = await pickProject(pythonProjects);
645646
if (pw) {
646647
const cwd = await findParentIfFile(pw.uri.fsPath);
647648
const terminal = await tm.create(view.environment, { cwd });

src/managers/builtin/sysPythonManager.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { SysManagerStrings } from '../../common/localize';
2121
import { createDeferred, Deferred } from '../../common/utils/deferred';
2222
import { normalizePath } from '../../common/utils/pathUtils';
23+
import { getProjectFsPathForScope, tryFastPathGet } from '../common/fastPath';
2324
import { NativePythonFinder } from '../common/nativePythonFinder';
2425
import { getLatest } from '../common/utils';
2526
import {
@@ -145,6 +146,22 @@ export class SysPythonManager implements EnvironmentManager {
145146
}
146147

147148
async get(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
149+
const fastResult = await tryFastPathGet({
150+
initialized: this._initialized,
151+
setInitialized: (deferred) => {
152+
this._initialized = deferred;
153+
},
154+
scope,
155+
label: 'system',
156+
getProjectFsPath: (s) => getProjectFsPathForScope(this.api, s),
157+
getPersistedPath: (fsPath) => getSystemEnvForWorkspace(fsPath),
158+
resolve: (p) => resolveSystemPythonEnvironmentPath(p, this.nativeFinder, this.api, this),
159+
startBackgroundInit: () => this.internalRefresh(false, SysManagerStrings.sysManagerDiscovering),
160+
});
161+
if (fastResult) {
162+
return fastResult.env;
163+
}
164+
148165
await this.initialize();
149166

150167
if (scope instanceof Uri) {

0 commit comments

Comments
 (0)