Skip to content

Commit 55b3129

Browse files
authored
Capture info for missing conda envs in native locator (microsoft#23796)
1 parent 5fd5098 commit 55b3129

File tree

2 files changed

+232
-99
lines changed

2 files changed

+232
-99
lines changed

src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts

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

4+
import * as fsPath from 'path';
45
import { Event, EventEmitter, workspace } from 'vscode';
56
import '../../../../common/extensions';
67
import { createDeferred, Deferred } from '../../../../common/utils/async';
@@ -28,7 +29,13 @@ import { createNativeGlobalPythonFinder, NativeEnvInfo } from '../common/nativeP
2829
import { pathExists } from '../../../../common/platform/fs-paths';
2930
import { noop } from '../../../../common/utils/misc';
3031
import { parseVersion } from '../../info/pythonVersion';
31-
import { Conda, isCondaEnvironment } from '../../../common/environmentManagers/conda';
32+
import {
33+
Conda,
34+
CONDAPATH_SETTING_KEY,
35+
getCondaEnvironmentsTxt,
36+
isCondaEnvironment,
37+
} from '../../../common/environmentManagers/conda';
38+
import { getConfiguration } from '../../../../common/vscodeApis/workspaceApis';
3239

3340
/**
3441
* A service which maintains the collection of known environments.
@@ -307,12 +314,8 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
307314
const missingEnvironments = {
308315
envsWithDuplicatePrefixes: 0,
309316
envsNotFound: 0,
310-
condaEnvsInEnvDir: 0,
311-
invalidCondaEnvs: 0,
312-
prefixNotExistsCondaEnvs: 0,
313-
condaEnvsWithoutPrefix: 0,
314-
nativeCondaEnvsInEnvDir: 0,
315317
missingNativeCondaEnvs: 0,
318+
missingNativeCondaEnvsFromTxt: 0,
316319
missingNativeCustomEnvs: 0,
317320
missingNativeMicrosoftStoreEnvs: 0,
318321
missingNativeGlobalEnvs: 0,
@@ -328,99 +331,220 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
328331
missingNativeOtherGlobalEnvs: 0,
329332
};
330333

331-
let canSpawnConda: boolean | undefined;
332-
let condaInfoEnvs: undefined | number;
333-
let condaInfoEnvsInvalid = 0;
334-
let condaInfoEnvsDuplicate = 0;
335-
let condaInfoEnvsInvalidPrefix = 0;
336-
let condaInfoEnvsDirs = 0;
337334
let envsDirs: string[] = [];
338-
let condaRcs: number | undefined;
339-
let condaRootPrefixFoundInInfoNotInNative: undefined | boolean;
340-
let condaDefaultPrefixFoundInInfoNotInNative: undefined | boolean;
341-
try {
342-
const conda = await Conda.getConda();
343-
const info = await conda?.getInfo();
344-
canSpawnConda = !!info;
345-
condaInfoEnvs = info?.envs?.length;
346-
// eslint-disable-next-line camelcase
347-
envsDirs = info?.envs_dirs || [];
335+
336+
type CondaTelemetry = {
337+
condaInfoEnvs: number;
338+
condaEnvsInEnvDir: number;
339+
prefixNotExistsCondaEnvs: number;
340+
condaEnvsWithoutPrefix: number;
341+
condaRootPrefixFoundInInfoNotInNative?: boolean;
342+
condaRootPrefixInCondaExePath?: boolean;
343+
condaDefaultPrefixFoundInInfoNotInNative?: boolean;
344+
condaDefaultPrefixInCondaExePath?: boolean;
345+
canSpawnConda?: boolean;
346+
nativeCanSpawnConda?: boolean;
347+
userProvidedCondaExe?: boolean;
348+
condaInfoEnvsInvalid: number;
349+
invalidCondaEnvs: number;
350+
condaInfoEnvsDuplicate: number;
351+
condaInfoEnvsInvalidPrefix: number;
352+
condaInfoEnvsDirs: number;
353+
nativeCondaEnvsInEnvDir: number;
354+
nativeCondaInfoEnvsDirs?: number;
355+
condaRcs?: number;
356+
nativeCondaRcs?: number;
357+
condaEnvsInTxt?: number;
358+
nativeCondaRcsNotFound: number;
359+
nativeCondaEnvDirsNotFound: number;
360+
nativeCondaEnvDirsNotFoundHasEnvs: number;
361+
nativeCondaEnvDirsNotFoundHasEnvsInTxt: number;
362+
};
363+
364+
const userProvidedCondaExe = fsPath
365+
.normalize((getConfiguration('python').get<string>(CONDAPATH_SETTING_KEY) || '').trim())
366+
.toLowerCase();
367+
const condaTelemetry: CondaTelemetry = {
368+
condaEnvsInEnvDir: 0,
369+
condaInfoEnvs: 0,
370+
prefixNotExistsCondaEnvs: 0,
371+
condaEnvsWithoutPrefix: 0,
372+
nativeCondaEnvsInEnvDir: 0,
373+
userProvidedCondaExe: userProvidedCondaExe.length > 0,
374+
condaInfoEnvsInvalid: 0,
375+
invalidCondaEnvs: 0,
376+
condaInfoEnvsDuplicate: 0,
377+
condaInfoEnvsInvalidPrefix: 0,
378+
condaInfoEnvsDirs: 0,
379+
nativeCondaRcsNotFound: 0,
380+
nativeCondaEnvDirsNotFound: 0,
381+
nativeCondaEnvDirsNotFoundHasEnvs: 0,
382+
nativeCondaEnvDirsNotFoundHasEnvsInTxt: 0,
383+
};
384+
385+
// Get conda telemetry
386+
{
387+
const [info, nativeCondaInfo, condaEnvsInEnvironmentsTxt] = await Promise.all([
388+
Conda.getConda()
389+
.catch((ex) => traceError('Failed to get conda info', ex))
390+
.then((conda) => conda?.getInfo()),
391+
this.nativeFinder
392+
.getCondaInfo()
393+
.catch((ex) => traceError(`Failed to get conda info from native locator`, ex)),
394+
getCondaEnvironmentsTxt()
395+
.then(async (items) => {
396+
const validEnvs = new Set<string>();
397+
await Promise.all(
398+
items.map(async (e) => {
399+
if ((await pathExists(e)) && (await isCondaEnvironment(e))) {
400+
validEnvs.add(fsPath.normalize(e).toLowerCase());
401+
}
402+
}),
403+
);
404+
return Array.from(validEnvs);
405+
})
406+
.catch((ex) => traceError(`Failed to get conda envs from environments.txt`, ex))
407+
.then((items) => items || []),
408+
]);
409+
410+
if (nativeCondaInfo) {
411+
condaTelemetry.nativeCanSpawnConda = nativeCondaInfo.canSpawnConda;
412+
condaTelemetry.nativeCondaInfoEnvsDirs = new Set(nativeCondaInfo.envDirs).size;
413+
condaTelemetry.nativeCondaRcs = new Set(nativeCondaInfo.condaRcs).size;
414+
}
415+
condaTelemetry.condaEnvsInTxt = condaEnvsInEnvironmentsTxt.length;
416+
condaTelemetry.canSpawnConda = !!info;
417+
418+
// Conda info rcs
348419
const condaRcFiles = new Set<string>();
349420
await Promise.all(
350421
// eslint-disable-next-line camelcase
351422
[info?.rc_path, info?.user_rc_path, info?.sys_rc_path, ...(info?.config_files || [])].map(
352423
async (rc) => {
353424
if (rc && (await pathExists(rc))) {
354-
condaRcFiles.add(rc);
425+
condaRcFiles.add(fsPath.normalize(rc).toLowerCase());
355426
}
356427
},
357428
),
358429
).catch(noop);
359-
condaRcs = condaRcFiles.size;
430+
const condaRcs = Array.from(condaRcFiles);
431+
condaTelemetry.condaRcs = condaRcs.length;
432+
433+
// Find the condarcs that were not found by native finder.
434+
const nativeCondaRcs = (nativeCondaInfo?.condaRcs || []).map((rc) => fsPath.normalize(rc).toLowerCase());
435+
condaTelemetry.nativeCondaRcsNotFound = condaRcs.filter((rc) => !nativeCondaRcs.includes(rc)).length;
436+
437+
// Conda info envs
438+
const validCondaInfoEnvs = new Set<string>();
360439
const duplicate = new Set<string>();
440+
// Duplicate, invalid conda environments.
361441
Promise.all(
362442
(info?.envs || []).map(async (e) => {
363443
if (duplicate.has(e)) {
364-
condaInfoEnvsDuplicate += 1;
444+
condaTelemetry.condaInfoEnvsDuplicate += 1;
365445
return;
366446
}
367447
duplicate.add(e);
368448
if (!(await pathExists(e))) {
369-
condaInfoEnvsInvalidPrefix += 1;
449+
condaTelemetry.condaInfoEnvsInvalidPrefix += 1;
450+
return;
370451
}
371452
if (!(await isCondaEnvironment(e))) {
372-
condaInfoEnvsInvalid += 1;
453+
condaTelemetry.condaInfoEnvsInvalid += 1;
454+
return;
373455
}
456+
validCondaInfoEnvs.add(fsPath.normalize(e).toLowerCase());
374457
}),
375458
);
459+
const condaInfoEnvs = Array.from(validCondaInfoEnvs);
460+
condaTelemetry.condaInfoEnvs = validCondaInfoEnvs.size;
461+
462+
// Conda env_dirs
463+
const validEnvDirs = new Set<string>();
376464
Promise.all(
377-
envsDirs.map(async (e) => {
465+
// eslint-disable-next-line camelcase
466+
(info?.envs_dirs || []).map(async (e) => {
378467
if (await pathExists(e)) {
379-
condaInfoEnvsDirs += 1;
468+
validEnvDirs.add(e);
380469
}
381470
}),
382471
);
383-
nativeEnvs
384-
.filter((e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Conda)
385-
.forEach((e) => {
386-
if (e.prefix && envsDirs.some((d) => e.prefix && e.prefix.startsWith(d))) {
387-
missingEnvironments.nativeCondaEnvsInEnvDir += 1;
388-
}
389-
});
472+
condaTelemetry.condaInfoEnvsDirs = validEnvDirs.size;
473+
envsDirs = Array.from(validEnvDirs).map((e) => fsPath.normalize(e).toLowerCase());
390474

391-
// Check if we have found the conda env that matches the `root_prefix` in the conda info.
392-
// eslint-disable-next-line camelcase
393-
const rootPrefix = (info?.root_prefix || '').toLowerCase();
394-
if (rootPrefix) {
395-
// Check if we have a conda env that matches this prefix.
475+
const nativeCondaEnvs = nativeEnvs.filter(
476+
(e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Conda,
477+
);
478+
479+
// Find the env_dirs that were not found by native finder.
480+
const nativeCondaEnvDirs = (nativeCondaInfo?.envDirs || []).map((envDir) =>
481+
fsPath.normalize(envDir).toLowerCase(),
482+
);
483+
const nativeCondaEnvPrefix = nativeCondaEnvs
484+
.filter((e) => e.prefix)
485+
.map((e) => fsPath.normalize(e.prefix || '').toLowerCase());
486+
487+
envsDirs.forEach((envDir) => {
396488
if (
397-
envs.some(
398-
(e) => e.executable.sysPrefix.toLowerCase() === rootPrefix && e.kind === PythonEnvKind.Conda,
399-
)
489+
!nativeCondaEnvDirs.includes(envDir) &&
490+
!nativeCondaEnvDirs.includes(fsPath.join(envDir, 'envs')) &&
491+
// If we have a native conda env from this env dir, then we're good.
492+
!nativeCondaEnvPrefix.some((prefix) => prefix.startsWith(envDir))
400493
) {
401-
condaRootPrefixFoundInInfoNotInNative = nativeEnvs.some(
402-
(e) => e.prefix?.toLowerCase() === rootPrefix.toLowerCase(),
403-
);
494+
condaTelemetry.nativeCondaEnvDirsNotFound += 1;
495+
496+
// Find what conda envs returned by `conda info` belong to this envdir folder.
497+
// And find which of those envs do not exist in native conda envs
498+
condaInfoEnvs.forEach((env) => {
499+
if (env.startsWith(envDir)) {
500+
condaTelemetry.nativeCondaEnvDirsNotFoundHasEnvs += 1;
501+
502+
// Check if this env was in the environments.txt file.
503+
if (condaEnvsInEnvironmentsTxt.includes(env)) {
504+
condaTelemetry.nativeCondaEnvDirsNotFoundHasEnvsInTxt += 1;
505+
}
506+
}
507+
});
404508
}
509+
});
510+
511+
// How many envs are in environments.txt that were not found by native locator.
512+
missingEnvironments.missingNativeCondaEnvsFromTxt = condaEnvsInEnvironmentsTxt.filter(
513+
(env) => !nativeCondaEnvPrefix.some((prefix) => prefix === env),
514+
).length;
515+
516+
// How many envs found by native locator & conda info are in the env dirs.
517+
condaTelemetry.condaEnvsInEnvDir = condaInfoEnvs.filter((e) =>
518+
envsDirs.some((d) => e.startsWith(d)),
519+
).length;
520+
condaTelemetry.nativeCondaEnvsInEnvDir = nativeCondaEnvs.filter((e) =>
521+
nativeCondaEnvDirs.some((d) => (e.prefix || '').startsWith(d)),
522+
).length;
523+
524+
// Check if we have found the conda env that matches the `root_prefix` in the conda info.
525+
// eslint-disable-next-line camelcase
526+
let rootPrefix = info?.root_prefix || '';
527+
if (rootPrefix && (await pathExists(rootPrefix)) && (await isCondaEnvironment(rootPrefix))) {
528+
rootPrefix = fsPath.normalize(rootPrefix).toLowerCase();
529+
condaTelemetry.condaRootPrefixInCondaExePath = userProvidedCondaExe.startsWith(rootPrefix);
530+
// Check if we have a conda env that matches this prefix but not found in native envs.
531+
condaTelemetry.condaRootPrefixFoundInInfoNotInNative =
532+
condaInfoEnvs.some((env) => env === rootPrefix) &&
533+
!nativeCondaEnvs.some((e) => fsPath.normalize(e.prefix || '').toLowerCase() === rootPrefix);
405534
}
535+
406536
// eslint-disable-next-line camelcase
407-
const defaultPrefix = (info?.default_prefix || '').toLowerCase();
408-
if (rootPrefix) {
409-
// Check if we have a conda env that matches this prefix.
410-
if (
411-
envs.some(
412-
(e) => e.executable.sysPrefix.toLowerCase() === defaultPrefix && e.kind === PythonEnvKind.Conda,
413-
)
414-
) {
415-
condaDefaultPrefixFoundInInfoNotInNative = nativeEnvs.some(
416-
(e) => e.prefix?.toLowerCase() === defaultPrefix.toLowerCase(),
417-
);
418-
}
537+
let defaultPrefix = info?.default_prefix || '';
538+
if (defaultPrefix && (await pathExists(defaultPrefix)) && (await isCondaEnvironment(defaultPrefix))) {
539+
defaultPrefix = fsPath.normalize(defaultPrefix).toLowerCase();
540+
condaTelemetry.condaDefaultPrefixInCondaExePath = userProvidedCondaExe.startsWith(defaultPrefix);
541+
// Check if we have a conda env that matches this prefix but not found in native envs.
542+
condaTelemetry.condaDefaultPrefixFoundInInfoNotInNative =
543+
condaInfoEnvs.some((env) => env === defaultPrefix) &&
544+
!nativeCondaEnvs.some((e) => fsPath.normalize(e.prefix || '').toLowerCase() === defaultPrefix);
419545
}
420-
} catch (ex) {
421-
canSpawnConda = false;
422546
}
423-
const nativeCondaInfoPromise = this.nativeFinder.getCondaInfo();
547+
424548
const prefixesSeenAlready = new Set<string>();
425549
await Promise.all(
426550
envs.map(async (env) => {
@@ -466,12 +590,6 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
466590
traceError(`Environment ${exe} is missing from native locator`);
467591
switch (env.kind) {
468592
case PythonEnvKind.Conda:
469-
if (
470-
env.executable.sysPrefix &&
471-
envsDirs.some((d) => env.executable.sysPrefix.startsWith(d))
472-
) {
473-
missingEnvironments.condaEnvsInEnvDir += 1;
474-
}
475593
missingEnvironments.missingNativeCondaEnvs += 1;
476594
break;
477595
case PythonEnvKind.Custom:
@@ -530,22 +648,6 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
530648
}),
531649
).catch((ex) => traceError('Failed to send telemetry for missing environments', ex));
532650

533-
const nativeCondaInfo = await nativeCondaInfoPromise.catch((ex) =>
534-
traceError(`Failed to get conda info from native locator`, ex),
535-
);
536-
537-
type CondaTelemetry = {
538-
nativeCanSpawnConda?: boolean;
539-
nativeCondaInfoEnvsDirs?: number;
540-
nativeCondaRcs?: number;
541-
};
542-
543-
const condaTelemetry: CondaTelemetry = {};
544-
if (nativeCondaInfo) {
545-
condaTelemetry.nativeCanSpawnConda = nativeCondaInfo.canSpawnConda;
546-
condaTelemetry.nativeCondaInfoEnvsDirs = new Set(nativeCondaInfo.envDirs).size;
547-
condaTelemetry.nativeCondaRcs = new Set(nativeCondaInfo.condaRcs).size;
548-
}
549651
const environmentsWithoutPython = envs.filter(
550652
(e) => getEnvPath(e.executable.filename, e.location).pathType === 'envFolderPath',
551653
).length;
@@ -572,15 +674,15 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
572674
e.kind === PythonEnvKind.OtherVirtual,
573675
).length;
574676

575-
missingEnvironments.condaEnvsWithoutPrefix = condaEnvs.filter((e) => !e.executable.sysPrefix).length;
677+
condaTelemetry.condaEnvsWithoutPrefix = condaEnvs.filter((e) => !e.executable.sysPrefix).length;
576678

577679
await Promise.all(
578680
condaEnvs.map(async (e) => {
579681
if (e.executable.sysPrefix && !(await pathExists(e.executable.sysPrefix))) {
580-
missingEnvironments.prefixNotExistsCondaEnvs += 1;
682+
condaTelemetry.prefixNotExistsCondaEnvs += 1;
581683
}
582684
if (e.executable.filename && !(await isCondaEnvironment(e.executable.filename))) {
583-
missingEnvironments.invalidCondaEnvs += 1;
685+
condaTelemetry.invalidCondaEnvs += 1;
584686
}
585687
}),
586688
);
@@ -634,19 +736,10 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
634736

635737
// Intent is to capture time taken for discovery of all envs to complete the first time.
636738
sendTelemetryEvent(EventName.PYTHON_INTERPRETER_DISCOVERY, elapsedTime, {
637-
telVer: 3,
638-
condaRcs,
639-
condaInfoEnvsInvalid,
640-
condaInfoEnvsDuplicate,
641-
condaInfoEnvsInvalidPrefix,
642-
condaRootPrefixFoundInInfoNotInNative,
643-
condaDefaultPrefixFoundInInfoNotInNative,
739+
telVer: 4,
644740
nativeDuration,
645741
workspaceFolderCount: (workspace.workspaceFolders || []).length,
646742
interpreters: this.cache.getAllEnvs().length,
647-
condaInfoEnvs,
648-
condaInfoEnvsDirs,
649-
canSpawnConda,
650743
environmentsWithoutPython,
651744
activeStateEnvs,
652745
condaEnvs: condaEnvs.length,

0 commit comments

Comments
 (0)