Skip to content

Commit 7b77743

Browse files
authored
Merge branch 'main' into copilot/overhaul-logging-for-debugging
2 parents 1f8c526 + 7c06c11 commit 7b77743

9 files changed

Lines changed: 231 additions & 51 deletions

File tree

api/src/main.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,8 @@ export interface PythonEnvironmentManagerRegistrationApi {
896896
*
897897
* @param manager Environment Manager implementation to register.
898898
* @param options Optional registration options.
899-
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
900-
* automatic extension detection fails, such as during F5 debugging where the extension's file path
901-
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
899+
* @param options.extensionId The extension ID of the calling extension. When this is not specified,
900+
* or when the specified extension cannot be found, the extension ID will be automatically detected.
902901
* @returns A disposable that can be used to unregister the environment manager.
903902
* @see {@link EnvironmentManager}
904903
*/
@@ -1005,9 +1004,8 @@ export interface PythonPackageManagerRegistrationApi {
10051004
*
10061005
* @param manager Package Manager implementation to register.
10071006
* @param options Optional registration options.
1008-
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
1009-
* automatic extension detection fails, such as during F5 debugging where the extension's file path
1010-
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
1007+
* @param options.extensionId The extension ID of the calling extension. When this is not specified,
1008+
* or when the specified extension cannot be found, the extension ID will be automatically detected.
10111009
* @returns A disposable that can be used to unregister the package manager.
10121010
* @see {@link PackageManager}
10131011
*/

src/api.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import {
4+
import type {
55
Disposable,
66
Event,
77
FileChangeType,
@@ -890,9 +890,8 @@ export interface PythonEnvironmentManagerRegistrationApi {
890890
*
891891
* @param manager Environment Manager implementation to register.
892892
* @param options Optional registration options.
893-
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
894-
* automatic extension detection fails, such as during F5 debugging where the extension's file path
895-
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
893+
* @param options.extensionId The extension ID of the calling extension. When this is not specified,
894+
* or when the specified extension cannot be found, the extension ID will be automatically detected.
896895
* @returns A disposable that can be used to unregister the environment manager.
897896
* @see {@link EnvironmentManager}
898897
*/
@@ -999,9 +998,8 @@ export interface PythonPackageManagerRegistrationApi {
999998
*
1000999
* @param manager Package Manager implementation to register.
10011000
* @param options Optional registration options.
1002-
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
1003-
* automatic extension detection fails, such as during F5 debugging where the extension's file path
1004-
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
1001+
* @param options.extensionId The extension ID of the calling extension. When this is not specified,
1002+
* or when the specified extension cannot be found, the extension ID will be automatically detected.
10051003
* @returns A disposable that can be used to unregister the package manager.
10061004
* @see {@link PackageManager}
10071005
*/

src/common/telemetry/constants.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ export enum EventNames {
124124
* - errorType: string (classified error category, on failure only)
125125
*/
126126
MANAGER_LAZY_INIT = 'MANAGER.LAZY_INIT',
127+
/**
128+
* Telemetry event fired when a manager's fast path attempts to resolve
129+
* a cached global environment (cross-session cache).
130+
* Properties:
131+
* - managerLabel: string (the manager's label, e.g. 'system')
132+
* - result: 'hit' | 'miss' | 'stale' ('hit' = cached path resolved successfully,
133+
* 'miss' = no cached path, 'stale' = cached path found but resolve failed)
134+
*/
135+
GLOBAL_ENV_CACHE = 'GLOBAL_ENV.CACHE',
127136
/**
128137
* Telemetry event fired when the JSON CLI fallback is used for environment discovery.
129138
* Triggered when the PET JSON-RPC server mode is exhausted after all restart attempts.
@@ -473,6 +482,18 @@ export interface IEventNamePropertyMapping {
473482
errorType?: string;
474483
};
475484

485+
/* __GDPR__
486+
"global_env.cache": {
487+
"managerLabel": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
488+
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
489+
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
490+
}
491+
*/
492+
[EventNames.GLOBAL_ENV_CACHE]: {
493+
managerLabel: string;
494+
result: 'hit' | 'miss' | 'stale';
495+
};
496+
476497
/* __GDPR__
477498
"pet.json_cli_fallback": {
478499
"operation": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },

src/common/utils/frameUtils.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,28 @@ function getFrameData(): FrameData[] {
2222
}
2323

2424
function getPathFromFrame(frame: FrameData): string {
25-
if (frame.filePath && frame.filePath.startsWith('file://')) {
25+
if (frame.filePath?.startsWith('file://')) {
2626
return Uri.parse(frame.filePath).fsPath;
2727
}
2828
return frame.filePath;
2929
}
3030

31-
export function getCallingExtension(extensionIdHint?: string): string {
31+
export function getCallingExtension(extensionId?: string): string {
3232
const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID];
3333
const extensions = allExtensions();
34+
35+
// Use the provided extensionId when available.
36+
// Only accept if it matches an actually loaded extension so we can always return something.
37+
if (extensionId) {
38+
const hintExt = extensions.find((ext) => ext.id === extensionId);
39+
if (hintExt) {
40+
traceVerbose(`Using provided extensionId: ${extensionId}`);
41+
return extensionId;
42+
}
43+
traceWarn(`Provided extensionId '${extensionId}' not found in loaded extensions, ignoring`);
44+
}
45+
46+
// Search the stack as a fallback when no extensionId is provided
3447
const otherExts = extensions.filter((ext) => !pythonExts.includes(ext.id));
3548
const frames = getFrameData();
3649

@@ -94,19 +107,6 @@ export function getCallingExtension(extensionIdHint?: string): string {
94107
}
95108
}
96109

97-
// Use the provided extensionId hint as a fallback (e.g., during F5 debugging where
98-
// stack-based detection fails because the file path doesn't contain the extension ID).
99-
// Only accept the hint if it matches an actually loaded extension for safety.
100-
if (extensionIdHint) {
101-
const hintExt = extensions.find((ext) => ext.id === extensionIdHint);
102-
if (hintExt) {
103-
traceVerbose(`Using provided extensionId hint: ${extensionIdHint}`);
104-
extensionIdCache.set(cacheKey, extensionIdHint);
105-
return extensionIdHint;
106-
}
107-
traceWarn(`Provided extensionId hint '${extensionIdHint}' not found in loaded extensions, ignoring`);
108-
}
109-
110110
// Fallback - we're likely being called from Python extension or built-in managers
111111
traceWarn(
112112
`Could not determine calling extension from stack frames. ` +

src/managers/builtin/cache.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { ENVS_EXTENSION_ID } from '../../common/constants';
2-
import { getWorkspacePersistentState } from '../../common/persistentState';
2+
import { getGlobalPersistentState, getWorkspacePersistentState } from '../../common/persistentState';
33

44
export const SYSTEM_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:system:WORKSPACE_SELECTED`;
55
export const SYSTEM_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:system:GLOBAL_SELECTED`;
66

77
export async function clearSystemEnvCache(): Promise<void> {
8-
const keys = [SYSTEM_WORKSPACE_KEY, SYSTEM_GLOBAL_KEY];
9-
const state = await getWorkspacePersistentState();
10-
await state.clear(keys);
8+
const workspaceState = await getWorkspacePersistentState();
9+
await workspaceState.clear([SYSTEM_WORKSPACE_KEY]);
10+
const globalState = await getGlobalPersistentState();
11+
await globalState.clear([SYSTEM_GLOBAL_KEY]);
1112
}
1213

1314
export async function getSystemEnvForWorkspace(fsPath: string): Promise<string | undefined> {
@@ -48,11 +49,11 @@ export async function setSystemEnvForWorkspaces(fsPath: string[], envPath: strin
4849
}
4950

5051
export async function getSystemEnvForGlobal(): Promise<string | undefined> {
51-
const state = await getWorkspacePersistentState();
52+
const state = await getGlobalPersistentState();
5253
return await state.get(SYSTEM_GLOBAL_KEY);
5354
}
5455

5556
export async function setSystemEnvForGlobal(envPath: string | undefined): Promise<void> {
56-
const state = await getWorkspacePersistentState();
57+
const state = await getGlobalPersistentState();
5758
await state.set(SYSTEM_GLOBAL_KEY, envPath);
5859
}

src/managers/builtin/sysPythonManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ export class SysPythonManager implements EnvironmentManager {
115115
const discard = this.collection.map((c) => c);
116116

117117
// hit here is fine...
118-
this.collection = (await refreshPythons(hardRefresh, this.nativeFinder, this.api, this.log, this)) ?? [];
118+
this.collection =
119+
(await refreshPythons(hardRefresh, this.nativeFinder, this.api, this.log, this)) ?? [];
119120
await this.loadEnvMap();
120121

121122
const args = [
@@ -155,6 +156,7 @@ export class SysPythonManager implements EnvironmentManager {
155156
label: 'system',
156157
getProjectFsPath: (s) => getProjectFsPathForScope(this.api, s),
157158
getPersistedPath: (fsPath) => getSystemEnvForWorkspace(fsPath),
159+
getGlobalPersistedPath: () => getSystemEnvForGlobal(),
158160
resolve: (p) => resolveSystemPythonEnvironmentPath(p, this.nativeFinder, this.api, this),
159161
startBackgroundInit: () => this.internalRefresh(false, SysManagerStrings.sysManagerDiscovering),
160162
});

src/managers/common/fastPath.ts

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import { Uri } from 'vscode';
55
import { GetEnvironmentScope, PythonEnvironment, PythonEnvironmentApi } from '../../api';
66
import { traceError, traceVerbose, traceWarn } from '../../common/logging';
7+
import { StopWatch } from '../../common/stopWatch';
8+
import { EventNames } from '../../common/telemetry/constants';
9+
import { sendTelemetryEvent } from '../../common/telemetry/sender';
710
import { createDeferred, Deferred } from '../../common/utils/deferred';
811

912
/**
@@ -26,6 +29,8 @@ export interface FastPathOptions {
2629
resolve: (persistedPath: string) => Promise<PythonEnvironment | undefined>;
2730
/** Starts background initialization (full discovery). Returns a promise that completes when init is done. */
2831
startBackgroundInit: () => Promise<void> | Thenable<void>;
32+
/** Optional: reads the persisted env path for global scope (when scope is undefined). */
33+
getGlobalPersistedPath?: () => Promise<string | undefined>;
2934
}
3035

3136
/**
@@ -52,7 +57,10 @@ export function getProjectFsPathForScope(api: Pick<PythonEnvironmentApi, 'getPyt
5257
* to fall through to the normal init path.
5358
*/
5459
export async function tryFastPathGet(opts: FastPathOptions): Promise<FastPathResult | undefined> {
55-
if (!(opts.scope instanceof Uri)) {
60+
const isGlobalScope = !(opts.scope instanceof Uri);
61+
62+
// Global scope is only supported when the caller provides getGlobalPersistedPath
63+
if (isGlobalScope && !opts.getGlobalPersistedPath) {
5664
return undefined;
5765
}
5866

@@ -82,23 +90,66 @@ export async function tryFastPathGet(opts: FastPathOptions): Promise<FastPathRes
8290
}
8391
}
8492

85-
const fsPath = opts.getProjectFsPath(opts.scope);
86-
const persistedPath = await opts.getPersistedPath(fsPath);
93+
// Look up the persisted path — either from workspace cache or global cache
94+
if (isGlobalScope) {
95+
// Safe: guarded by the early return above
96+
const getGlobalPersistedPath = opts.getGlobalPersistedPath as () => Promise<string | undefined>;
8797

88-
if (persistedPath) {
89-
try {
90-
const resolved = await opts.resolve(persistedPath);
91-
if (resolved) {
92-
return { env: resolved };
98+
// Track end-to-end cross-session cache performance for global scope, including persisted-path lookup.
99+
const cacheStopWatch = new StopWatch();
100+
const persistedPath = await getGlobalPersistedPath();
101+
102+
if (persistedPath) {
103+
try {
104+
const resolved = await opts.resolve(persistedPath);
105+
if (resolved) {
106+
sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, {
107+
managerLabel: opts.label,
108+
result: 'hit',
109+
});
110+
return { env: resolved };
111+
}
112+
// Cached path found but resolve returned undefined (e.g., Python was uninstalled)
113+
sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, {
114+
managerLabel: opts.label,
115+
result: 'stale',
116+
});
117+
} catch (err) {
118+
sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, {
119+
managerLabel: opts.label,
120+
result: 'stale',
121+
});
122+
traceWarn(
123+
`[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`,
124+
err,
125+
);
93126
}
94-
} catch (err) {
95-
traceWarn(
96-
`[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`,
97-
err,
98-
);
127+
} else {
128+
sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, {
129+
managerLabel: opts.label,
130+
result: 'miss',
131+
});
132+
traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`);
99133
}
100134
} else {
101-
traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`);
135+
const scope = opts.scope as Uri;
136+
const persistedPath = await opts.getPersistedPath(opts.getProjectFsPath(scope));
137+
138+
if (persistedPath) {
139+
try {
140+
const resolved = await opts.resolve(persistedPath);
141+
if (resolved) {
142+
return { env: resolved };
143+
}
144+
} catch (err) {
145+
traceWarn(
146+
`[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`,
147+
err,
148+
);
149+
}
150+
} else {
151+
traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`);
152+
}
102153
}
103154

104155
return undefined;

0 commit comments

Comments
 (0)