Cross-session cache for discovered system Python path#1455
Cross-session cache for discovered system Python path#1455eleanorjboyd wants to merge 2 commits intomicrosoft:mainfrom
Conversation
d78fc30 to
c27bac3
Compare
c27bac3 to
e9f2f7d
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR adds a cross-session “fast path” for resolving the system Python by persisting the discovered global Python path and using it immediately on next startup, while verifying validity in the background.
Changes:
- Add optional global-scope persisted-path support to
tryFastPathGet()and emit telemetry for global cache hit/miss/stale. - Update system Python manager/cache to read/write the global persisted path via global persistent state.
- Extend unit tests to cover global-scope fast-path behavior and telemetry emission.
Show a summary per file
| File | Description |
|---|---|
| src/test/managers/fastPath.get.unit.test.ts | Stubs global cache getter to avoid test behavior changes with new global fast-path support. |
| src/test/managers/common/fastPath.unit.test.ts | Adds test coverage for global persisted-path behavior and verifies telemetry results. |
| src/managers/common/fastPath.ts | Implements optional global persisted-path lookup and emits global cache telemetry. |
| src/managers/builtin/sysPythonManager.ts | Wires system manager into new global persisted-path fast-path option. |
| src/managers/builtin/cache.ts | Moves global key storage to global persistent state and clears workspace/global keys in their respective stores. |
| src/common/telemetry/constants.ts | Adds GLOBAL_ENV_CACHE event and GDPR property mapping for the new telemetry. |
Copilot's findings
- Files reviewed: 6/6 changed files
- Comments generated: 2
| // Look up the persisted path — either from workspace cache or global cache | ||
| const persistedPath = isGlobalScope | ||
| ? await opts.getGlobalPersistedPath!() | ||
| : await opts.getPersistedPath(opts.getProjectFsPath(opts.scope as Uri)); | ||
|
|
||
| // Track cross-session cache performance for global scope | ||
| const cacheStopWatch = isGlobalScope ? new StopWatch() : undefined; | ||
|
|
There was a problem hiding this comment.
The stopwatch is started after awaiting the persisted-path lookup, so the emitted <duration> excludes time spent reading the global cache (and will also be near-zero for miss). If the intent is to measure end-to-end global fast-path latency, start timing before the lookup (or rename/comment to clarify that the duration is resolve-only).
| // Look up the persisted path — either from workspace cache or global cache | |
| const persistedPath = isGlobalScope | |
| ? await opts.getGlobalPersistedPath!() | |
| : await opts.getPersistedPath(opts.getProjectFsPath(opts.scope as Uri)); | |
| // Track cross-session cache performance for global scope | |
| const cacheStopWatch = isGlobalScope ? new StopWatch() : undefined; | |
| // Track end-to-end cross-session cache performance for global scope, including persisted-path lookup. | |
| const cacheStopWatch = isGlobalScope ? new StopWatch() : undefined; | |
| // Look up the persisted path — either from workspace cache or global cache | |
| const persistedPath = isGlobalScope | |
| ? await opts.getGlobalPersistedPath!() | |
| : await opts.getPersistedPath(opts.getProjectFsPath(opts.scope as Uri)); |
| const persistedPath = isGlobalScope | ||
| ? await opts.getGlobalPersistedPath!() | ||
| : await opts.getPersistedPath(opts.getProjectFsPath(opts.scope as Uri)); | ||
|
|
||
| // Track cross-session cache performance for global scope | ||
| const cacheStopWatch = isGlobalScope ? new StopWatch() : undefined; | ||
|
|
||
| if (persistedPath) { | ||
| try { | ||
| const resolved = await opts.resolve(persistedPath); | ||
| if (resolved) { | ||
| if (isGlobalScope) { | ||
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | ||
| managerLabel: opts.label, | ||
| result: 'hit', | ||
| }); | ||
| } | ||
| return { env: resolved }; | ||
| } | ||
| // Cached path found but resolve returned undefined (e.g., Python was uninstalled) | ||
| if (isGlobalScope) { | ||
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | ||
| managerLabel: opts.label, | ||
| result: 'stale', | ||
| }); | ||
| } | ||
| } catch (err) { | ||
| if (isGlobalScope) { | ||
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | ||
| managerLabel: opts.label, | ||
| result: 'stale', | ||
| }); | ||
| } | ||
| traceWarn( | ||
| `[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`, | ||
| err, | ||
| ); | ||
| } | ||
| } else { | ||
| if (isGlobalScope) { | ||
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | ||
| managerLabel: opts.label, | ||
| result: 'miss', | ||
| }); | ||
| } | ||
| traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`); |
There was a problem hiding this comment.
Using non-null assertions (opts.getGlobalPersistedPath!() and opts.scope as Uri) makes control-flow assumptions less explicit. Consider branching on isGlobalScope and assigning strongly-typed locals (e.g., const getGlobalPersistedPath = opts.getGlobalPersistedPath; / const scope = opts.scope as Uri) to avoid repeated assertions and make the two code paths clearer.
| const persistedPath = isGlobalScope | |
| ? await opts.getGlobalPersistedPath!() | |
| : await opts.getPersistedPath(opts.getProjectFsPath(opts.scope as Uri)); | |
| // Track cross-session cache performance for global scope | |
| const cacheStopWatch = isGlobalScope ? new StopWatch() : undefined; | |
| if (persistedPath) { | |
| try { | |
| const resolved = await opts.resolve(persistedPath); | |
| if (resolved) { | |
| if (isGlobalScope) { | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'hit', | |
| }); | |
| } | |
| return { env: resolved }; | |
| } | |
| // Cached path found but resolve returned undefined (e.g., Python was uninstalled) | |
| if (isGlobalScope) { | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'stale', | |
| }); | |
| } | |
| } catch (err) { | |
| if (isGlobalScope) { | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'stale', | |
| }); | |
| } | |
| traceWarn( | |
| `[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`, | |
| err, | |
| ); | |
| } | |
| } else { | |
| if (isGlobalScope) { | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch!.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'miss', | |
| }); | |
| } | |
| traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`); | |
| if (isGlobalScope) { | |
| const getGlobalPersistedPath = opts.getGlobalPersistedPath; | |
| if (!getGlobalPersistedPath) { | |
| return undefined; | |
| } | |
| const cacheStopWatch = new StopWatch(); | |
| const persistedPath = await getGlobalPersistedPath(); | |
| if (persistedPath) { | |
| try { | |
| const resolved = await opts.resolve(persistedPath); | |
| if (resolved) { | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'hit', | |
| }); | |
| return { env: resolved }; | |
| } | |
| // Cached path found but resolve returned undefined (e.g., Python was uninstalled) | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'stale', | |
| }); | |
| } catch (err) { | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'stale', | |
| }); | |
| traceWarn( | |
| `[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`, | |
| err, | |
| ); | |
| } | |
| } else { | |
| sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, { | |
| managerLabel: opts.label, | |
| result: 'miss', | |
| }); | |
| traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`); | |
| } | |
| } else { | |
| const scope = opts.scope as Uri; | |
| const persistedPath = await opts.getPersistedPath(opts.getProjectFsPath(scope)); | |
| if (persistedPath) { | |
| try { | |
| const resolved = await opts.resolve(persistedPath); | |
| if (resolved) { | |
| return { env: resolved }; | |
| } | |
| } catch (err) { | |
| traceWarn( | |
| `[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`, | |
| err, | |
| ); | |
| } | |
| } else { | |
| traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`); | |
| } |
Persist the discovered system Python path (e.g.,
/usr/bin/python3orC:\Python312\python.exe) toglobalState. On next startup, use the cached path immediately and verify it's still valid in the background.Potential risk: The cache could become stale if the user uninstalls/upgrades Python. Background verification mitigates this — the cached env is used immediately, and if verification fails, a re-scan is triggered. The user would see a brief env switch in the rare case the cache is wrong.