@@ -26,6 +26,8 @@ const RESOLVE_TIMEOUT_MS = 30_000; // 30 seconds for single resolve
2626
2727// CLI fallback timeout: generous budget since it's a full process spawn doing a full scan
2828const CLI_FALLBACK_TIMEOUT_MS = 120_000 ; // 2 minutes
29+ // Limit concurrent resolve subprocesses to avoid CPU/memory pressure on machines with many envs
30+ const CLI_RESOLVE_CONCURRENCY = 4 ;
2931
3032// Restart/recovery constants
3133const MAX_RESTART_ATTEMPTS = 3 ;
@@ -885,13 +887,13 @@ class NativePythonFinderImpl implements NativePythonFinder {
885887 let parsed : { managers : NativeEnvManagerInfo [ ] ; environments : NativeEnvInfo [ ] } ;
886888 try {
887889 parsed = parseRefreshCliOutput ( stdout ) ;
888- } catch {
890+ } catch ( ex ) {
889891 sendTelemetryEvent ( EventNames . PET_JSON_CLI_FALLBACK , stopWatch . elapsedTime , {
890892 operation : 'refresh' ,
891893 result : 'error' ,
892894 } ) ;
893- this . outputChannel . error ( '[pet] JSON CLI fallback: Failed to parse find output:' , stdout . slice ( 0 , 500 ) ) ;
894- throw new Error ( 'Failed to parse PET find --json output' ) ;
895+ this . outputChannel . error ( '[pet] JSON CLI fallback: Failed to parse find output:' , stdout . slice ( 0 , 500 ) , ex ) ;
896+ throw new Error ( 'Failed to parse PET find --json output' , { cause : ex } ) ;
895897 }
896898
897899 const nativeInfo : NativeInfo [ ] = [ ] ;
@@ -901,13 +903,28 @@ class NativePythonFinderImpl implements NativePythonFinder {
901903 nativeInfo . push ( manager ) ;
902904 }
903905
904- // Resolve incomplete environments in parallel, mirroring doRefreshAttempt's Promise.all pattern.
905- const resolvePromises : Promise < void > [ ] = [ ] ;
906+ // Collect environments that need individual resolve calls.
907+ // Incomplete environments have an executable but are missing version or prefix.
908+ const toResolve : NativeEnvInfo [ ] = [ ] ;
906909 for ( const env of parsed . environments ?? [ ] ) {
907910 if ( env . executable && ( ! env . version || ! env . prefix ) ) {
908- // Environment has an executable but incomplete metadata — resolve individually
909- resolvePromises . push (
910- this . resolveViaJsonCli ( env . executable )
911+ toResolve . push ( env ) ;
912+ } else {
913+ this . outputChannel . info ( `[pet CLI] Discovered env: ${ env . executable ?? env . prefix } ` ) ;
914+ nativeInfo . push ( env ) ;
915+ }
916+ }
917+
918+ // Resolve incomplete environments with bounded concurrency to avoid spawning too many
919+ // subprocesses at once on machines with many incomplete environments.
920+ // Each resolveViaJsonCli() spawns a new OS process, unlike server mode where all resolve
921+ // calls share a single long-lived process — so unbounded parallelism would cause CPU/memory
922+ // pressure. Process in batches of CLI_RESOLVE_CONCURRENCY.
923+ for ( let i = 0 ; i < toResolve . length ; i += CLI_RESOLVE_CONCURRENCY ) {
924+ const batch = toResolve . slice ( i , i + CLI_RESOLVE_CONCURRENCY ) ;
925+ await Promise . all (
926+ batch . map ( ( env ) =>
927+ this . resolveViaJsonCli ( env . executable ! )
911928 . then ( ( resolved ) => {
912929 this . outputChannel . info ( `[pet CLI] Resolved env: ${ resolved . executable } ` ) ;
913930 nativeInfo . push ( resolved ) ;
@@ -919,13 +936,9 @@ class NativePythonFinderImpl implements NativePythonFinder {
919936 ) ;
920937 nativeInfo . push ( env ) ;
921938 } ) ,
922- ) ;
923- } else {
924- this . outputChannel . info ( `[pet CLI] Discovered env: ${ env . executable ?? env . prefix } ` ) ;
925- nativeInfo . push ( env ) ;
926- }
939+ ) ,
940+ ) ;
927941 }
928- await Promise . all ( resolvePromises ) ;
929942
930943 sendTelemetryEvent ( EventNames . PET_JSON_CLI_FALLBACK , stopWatch . elapsedTime , {
931944 operation : 'refresh' ,
@@ -1090,7 +1103,8 @@ export function parseResolveCliOutput(stdout: string, executable: string): Nativ
10901103 * @param options Optional refresh options: a kind filter string or an array of URIs to search.
10911104 * @param venvFolders Additional virtual environment folder paths to include when searching
10921105 * URI-based paths (needed because searchPaths may override environmentDirectories in PET).
1093- * @returns The args array to pass to the PET binary (after 'find --json').
1106+ * @returns The args array to pass directly to the PET binary, starting with `['find', '--json']`
1107+ * followed by the positional search paths and configuration flags.
10941108 */
10951109export function buildFindCliArgs (
10961110 config : ConfigurationOptions ,
0 commit comments