Skip to content

feat: implement fast path resolution for Python environment managers#1408

Open
eleanorjboyd wants to merge 7 commits intomicrosoft:mainfrom
eleanorjboyd:vague-owl
Open

feat: implement fast path resolution for Python environment managers#1408
eleanorjboyd wants to merge 7 commits intomicrosoft:mainfrom
eleanorjboyd:vague-owl

Conversation

@eleanorjboyd
Copy link
Copy Markdown
Member

  • Added fast path resolution logic in SysPythonManager, VenvManager, CondaEnvManager, PipenvManager, and PyEnvManager to directly resolve persisted environments without waiting for full initialization.
  • Enhanced error handling with logging for failed resolutions in the fast path.
  • Introduced unit tests to validate the fast path behavior across different environment managers, ensuring correct resolution and background initialization handling.

dmitrivMS
dmitrivMS previously approved these changes Mar 27, 2026
@eleanorjboyd eleanorjboyd marked this pull request as draft March 27, 2026 19:15
@eleanorjboyd eleanorjboyd added the feature-request Request for new features or functionality label Mar 27, 2026
@eleanorjboyd eleanorjboyd marked this pull request as ready for review March 27, 2026 23:03
@eleanorjboyd eleanorjboyd requested a review from Copilot March 31, 2026 01:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a shared “fast path” for EnvironmentManager.get() so managers can immediately resolve a persisted environment selection (from workspace state) without waiting for full environment discovery to complete, while discovery continues in the background.

Changes:

  • Added a reusable tryFastPathGet() helper (plus a shared getProjectFsPathForScope() accessor) and integrated it into built-in and tool-based managers.
  • Updated get() implementations in SysPython/Venv/Conda/Pipenv/PyEnv managers to attempt fast resolution and trigger background initialization.
  • Added unit tests for both the shared helper and manager-level behavior; updated startup-flow documentation to describe the new execution fork.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/managers/common/fastPath.ts New shared helper implementing the fast-path resolution + background init orchestration.
src/managers/builtin/sysPythonManager.ts Uses fast-path helper before falling back to standard initialization.
src/managers/builtin/venvManager.ts Uses fast-path helper and starts background refresh via internalRefresh.
src/managers/conda/condaEnvManager.ts Uses fast-path helper and runs discovery under withProgress.
src/managers/pipenv/pipenvManager.ts Uses fast-path helper and runs discovery under withProgress.
src/managers/pyenv/pyenvManager.ts Uses fast-path helper and runs discovery under withProgress.
src/test/managers/common/fastPath.unit.test.ts New unit tests validating tryFastPathGet() behavior and background init semantics.
src/test/managers/fastPath.get.unit.test.ts New unit tests validating fast-path behavior across multiple managers’ get() implementations.
docs/startup-flow.md Documentation updates describing the new fast-path vs slow-path fork during startup/selection.
Comments suppressed due to low confidence (1)

src/managers/common/fastPath.ts:94

  • Same as the traceError case above: traceWarn(... ${err}) will often lose the error stack/details. Prefer passing the error object as a separate argument to traceWarn so util.format can render it more helpfully.
            traceWarn(
                `[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init: ${err}`,
            );
        }
    }

Comment on lines +72 to +76
traceError(`[${opts.label}] Background initialization failed: ${err}`);
// Allow subsequent get()/initialize() calls to retry after a background init failure.
opts.setInitialized(undefined);
deferredRef.resolve();
},
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opts.startBackgroundInit() is invoked inside Promise.resolve(...). If startBackgroundInit throws synchronously (before returning a Promise/Thenable), the exception will escape and tryFastPathGet() will throw, breaking manager.get() calls. Wrap the call in try/catch (or a helper that converts sync-throws into a rejected promise) so failures are handled by the rejection path and the deferred is resolved/cleared consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +76
opts.setInitialized(undefined);
deferredRef.resolve();
},
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error is logged via string interpolation (... ${err}), which discards useful details (e.g., stack traces) and can produce [object Object]. Since traceError/traceWarn ultimately call util.format, pass the error as a separate argument (e.g. traceError('...', err)) to preserve structured formatting.

This issue also appears on line 90 of the same file.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +82
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)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc says fast path runs only when a persisted env path exists (condition c), but tryFastPathGet() currently creates _initialized and starts startBackgroundInit() before it awaits getPersistedPath(). Please adjust this section to reflect the actual control flow (e.g., background init can start even if no persisted path; the early return only happens when a persisted path exists and resolves).

Suggested change
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)
key checks:
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 (background init kickoff + optional early return):
**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
- this happens as soon as (a) and (b) are true, **even if** no persisted path exists
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 (only if scope is a `Uri`; may return undefined)
4. if a persisted path exists:
- attempt `resolve(persistedPath)`
- failure (no env, mismatched manager, etc.) → fall through to SLOW PATH
- success → return env immediately (background init continues in parallel)

Copilot uses AI. Check for mistakes.
const lastCallArg = setInitialized.lastCall.args[0] as unknown;
assert.strictEqual(lastCallArg, undefined, 'Should clear initialized after background init failure');
});

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s good coverage for rejected/Thenable startBackgroundInit, but no test for startBackgroundInit throwing synchronously (before returning a Promise/Thenable). Since tryFastPathGet currently calls it inline, adding a unit test for a sync throw would prevent regressions once it’s fixed to handle that case gracefully and to clear/resolve _initialized appropriately.

Suggested change
test('synchronous background init failure resets initialized for retry', async () => {
const startBackgroundInit = sinon.stub().throws(new Error('init crashed sync'));
const { opts, setInitialized } = createOpts({ startBackgroundInit });
const result = await tryFastPathGet(opts);
assert.ok(
result,
'Should still return resolved env even when background init throws synchronously',
);
assert.ok(
setInitialized.called,
'Should set initialized before attempting background init even when it throws synchronously',
);
// Allow any background init error handling to run.
await new Promise((resolve) => setImmediate(resolve));
const lastCallArg = setInitialized.lastCall.args[0] as unknown;
assert.strictEqual(
lastCallArg,
undefined,
'Should clear initialized after synchronous background init failure',
);
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-request Request for new features or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants