Skip to content

Commit e27185a

Browse files
author
Kartik Raj
authored
Use worker threads for fetching Windows Registry interpreters (#22479)
For #22146
1 parent 7a4de92 commit e27185a

File tree

15 files changed

+182
-42
lines changed

15 files changed

+182
-42
lines changed

.github/workflows/pr-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ jobs:
293293
with:
294294
run: npm run testSingleWorkspace
295295
working-directory: ${{ env.special-working-directory }}
296-
if: matrix.test-suite == 'venv' && matrix.os == 'ubuntu-latest'
296+
if: matrix.test-suite == 'venv'
297297

298298
- name: Run single-workspace tests
299299
env:

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@
538538
"pythonSurveyNotification",
539539
"pythonPromptNewToolsExt",
540540
"pythonTerminalEnvVarActivation",
541+
"pythonDiscoveryUsingWorkers",
541542
"pythonTestAdapter",
542543
"pythonREPLSmartSend",
543544
"pythonRecommendTensorboardExt"
@@ -547,6 +548,7 @@
547548
"%python.experiments.pythonSurveyNotification.description%",
548549
"%python.experiments.pythonPromptNewToolsExt.description%",
549550
"%python.experiments.pythonTerminalEnvVarActivation.description%",
551+
"%python.experiments.pythonDiscoveryUsingWorkers.description%",
550552
"%python.experiments.pythonTestAdapter.description%",
551553
"%python.experiments.pythonREPLSmartSend.description%",
552554
"%python.experiments.pythonRecommendTensorboardExt.description%"
@@ -565,6 +567,7 @@
565567
"pythonSurveyNotification",
566568
"pythonPromptNewToolsExt",
567569
"pythonTerminalEnvVarActivation",
570+
"pythonDiscoveryUsingWorkers",
568571
"pythonTestAdapter",
569572
"pythonREPLSmartSend"
570573
],
@@ -573,6 +576,7 @@
573576
"%python.experiments.pythonSurveyNotification.description%",
574577
"%python.experiments.pythonPromptNewToolsExt.description%",
575578
"%python.experiments.pythonTerminalEnvVarActivation.description%",
579+
"%python.experiments.pythonDiscoveryUsingWorkers.description%",
576580
"%python.experiments.pythonTestAdapter.description%",
577581
"%python.experiments.pythonREPLSmartSend.description%"
578582
]

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"python.experiments.pythonSurveyNotification.description": "Denotes the Python Survey Notification experiment.",
4141
"python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.",
4242
"python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.",
43+
"python.experiments.pythonDiscoveryUsingWorkers.description": "Enables use of worker threads to do heavy computation when discovering interpreters.",
4344
"python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.",
4445
"python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.",
4546
"python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.",

src/client/common/experiments/groups.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export enum TerminalEnvVarActivation {
1111
experiment = 'pythonTerminalEnvVarActivation',
1212
}
1313

14+
export enum DiscoveryUsingWorkers {
15+
experiment = 'pythonDiscoveryUsingWorkers',
16+
}
17+
1418
// Experiment to enable the new testing rewrite.
1519
export enum EnableTestAdapterRewrite {
1620
experiment = 'pythonTestAdapter',

src/client/pythonEnvironments/base/locator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export interface ILocator<I = PythonEnvInfo, E = PythonEnvsChangedEvent> extends
178178
* @param query - if provided, the locator will limit results to match
179179
* @returns - the fast async iterator of Python envs, which may have incomplete info
180180
*/
181-
iterEnvs(query?: QueryForEvent<E>): IPythonEnvsIterator<I>;
181+
iterEnvs(query?: QueryForEvent<E>, useWorkerThreads?: boolean): IPythonEnvsIterator<I>;
182182
}
183183

184184
export type ICompositeLocator<I = PythonEnvInfo, E = PythonEnvsChangedEvent> = Omit<ILocator<I, E>, 'providerId'>;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async function updateEnvUsingRegistry(env: PythonEnvInfo): Promise<void> {
109109
let interpreters = getRegistryInterpretersSync();
110110
if (!interpreters) {
111111
traceError('Expected registry interpreter cache to be initialized already');
112-
interpreters = await getRegistryInterpreters();
112+
interpreters = await getRegistryInterpreters(true);
113113
}
114114
const data = interpreters.find((i) => arePathsSame(i.interpreterPath, env.executable.filename));
115115
if (data) {

src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ export class WindowsRegistryLocator extends Locator<BasicEnvInfo> {
1212
public readonly providerId: string = 'windows-registry';
1313

1414
// eslint-disable-next-line class-methods-use-this
15-
public iterEnvs(): IPythonEnvsIterator<BasicEnvInfo> {
15+
public iterEnvs(_?: unknown, useWorkerThreads = true): IPythonEnvsIterator<BasicEnvInfo> {
1616
const iterator = async function* () {
1717
traceVerbose('Searching for windows registry interpreters');
18-
const interpreters = await getRegistryInterpreters();
18+
const interpreters = await getRegistryInterpreters(useWorkerThreads);
1919
for (const interpreter of interpreters) {
2020
try {
2121
// Filter out Microsoft Store app directories. We have a store app locator that handles this.

src/client/pythonEnvironments/base/locators/wrappers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
// eslint-disable-next-line max-classes-per-file
55
import { Uri } from 'vscode';
6+
import { DiscoveryUsingWorkers } from '../../../common/experiments/groups';
67
import { IDisposable } from '../../../common/types';
78
import { iterEmpty } from '../../../common/utils/async';
89
import { getURIFilter } from '../../../common/utils/misc';
910
import { Disposables } from '../../../common/utils/resourceLifecycle';
11+
import { traceLog } from '../../../logging';
12+
import { inExperiment } from '../../common/externalDependencies';
1013
import { PythonEnvInfo } from '../info';
1114
import { BasicEnvInfo, ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../locator';
1215
import { combineIterators, Locators } from '../locators';
@@ -17,6 +20,8 @@ import { LazyResourceBasedLocator } from './common/resourceBasedLocator';
1720
*/
1821

1922
export class ExtensionLocators<I = PythonEnvInfo> extends Locators<I> {
23+
private readonly useWorkerThreads: boolean;
24+
2025
constructor(
2126
// These are expected to be low-level locators (e.g. system).
2227
private readonly nonWorkspace: ILocator<I>[],
@@ -25,6 +30,10 @@ export class ExtensionLocators<I = PythonEnvInfo> extends Locators<I> {
2530
private readonly workspace: ILocator<I>,
2631
) {
2732
super([...nonWorkspace, workspace]);
33+
this.useWorkerThreads = inExperiment(DiscoveryUsingWorkers.experiment);
34+
if (this.useWorkerThreads) {
35+
traceLog('Using worker threads for discovery...');
36+
}
2837
}
2938

3039
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<I> {
@@ -33,7 +42,7 @@ export class ExtensionLocators<I = PythonEnvInfo> extends Locators<I> {
3342
const nonWorkspace = query?.providerId
3443
? this.nonWorkspace.filter((locator) => query.providerId === locator.providerId)
3544
: this.nonWorkspace;
36-
iterators.push(...nonWorkspace.map((loc) => loc.iterEnvs(query)));
45+
iterators.push(...nonWorkspace.map((loc) => loc.iterEnvs(query, this.useWorkerThreads)));
3746
}
3847
return combineIterators(iterators);
3948
}

src/client/pythonEnvironments/common/environmentManagers/conda.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ export class Conda {
307307
}
308308

309309
async function* getCandidatesFromRegistry() {
310-
const interps = await getRegistryInterpreters();
310+
const interps = await getRegistryInterpreters(true);
311311
const candidates = interps
312312
.filter((interp) => interp.interpreterPath && interp.distroOrgName === 'ContinuumAnalytics')
313313
.map((interp) => path.join(path.win32.dirname(interp.interpreterPath), suffix));

src/client/pythonEnvironments/common/externalDependencies.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
import * as fsapi from 'fs-extra';
55
import * as path from 'path';
6+
import { Worker } from 'worker_threads';
67
import * as vscode from 'vscode';
78
import { IWorkspaceService } from '../../common/application/types';
89
import { ExecutionResult, IProcessServiceFactory, ShellOptions, SpawnOptions } from '../../common/process/types';
9-
import { IDisposable, IConfigurationService } from '../../common/types';
10+
import { IDisposable, IConfigurationService, IExperimentService } from '../../common/types';
1011
import { chain, iterable } from '../../common/utils/async';
1112
import { getOSType, OSType } from '../../common/utils/platform';
1213
import { IServiceContainer } from '../../ioc/types';
@@ -29,6 +30,11 @@ export async function exec(file: string, args: string[], options: SpawnOptions =
2930
return service.exec(file, args, options);
3031
}
3132

33+
export function inExperiment(experimentName: string): boolean {
34+
const service = internalServiceContainer.get<IExperimentService>(IExperimentService);
35+
return service.inExperimentSync(experimentName);
36+
}
37+
3238
// Workspace
3339

3440
export function isVirtualWorkspace(): boolean {
@@ -195,3 +201,25 @@ export function onDidChangePythonSetting(name: string, callback: () => void, roo
195201
}
196202
});
197203
}
204+
205+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
206+
export async function executeWorkerFile(workerFileName: string, workerData: any): Promise<any> {
207+
return new Promise((resolve, reject) => {
208+
const worker = new Worker(workerFileName, { workerData });
209+
worker.on('message', (res: { err: Error; res: unknown }) => {
210+
if (res.err) {
211+
reject(res.err);
212+
}
213+
resolve(res.res);
214+
});
215+
worker.on('error', (ex: Error) => {
216+
traceError(`Error in worker ${workerFileName}`, ex);
217+
reject(ex);
218+
});
219+
worker.on('exit', (code) => {
220+
if (code !== 0) {
221+
reject(new Error(`Worker ${workerFileName} stopped with exit code ${code}`));
222+
}
223+
});
224+
});
225+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Registry } from 'winreg';
2+
import { parentPort, workerData } from 'worker_threads';
3+
import { IRegistryKey } from './windowsRegistry';
4+
5+
const WinReg = require('winreg');
6+
7+
const regKey = new WinReg(workerData);
8+
9+
function copyRegistryKeys(keys: IRegistryKey[]): IRegistryKey[] {
10+
// Use the map function to create a new array with copies of the specified properties.
11+
return keys.map((key) => ({
12+
hive: key.hive,
13+
arch: key.arch,
14+
key: key.key,
15+
}));
16+
}
17+
18+
regKey.keys((err: Error, res: Registry[]) => {
19+
if (!parentPort) {
20+
throw new Error('Not in a worker thread');
21+
}
22+
const messageRes = copyRegistryKeys(res);
23+
parentPort.postMessage({ err, res: messageRes });
24+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { RegistryItem } from 'winreg';
2+
import { parentPort, workerData } from 'worker_threads';
3+
import { IRegistryValue } from './windowsRegistry';
4+
5+
const WinReg = require('winreg');
6+
7+
const regKey = new WinReg(workerData);
8+
9+
function copyRegistryValues(values: IRegistryValue[]): IRegistryValue[] {
10+
// Use the map function to create a new array with copies of the specified properties.
11+
return values.map((value) => ({
12+
hive: value.hive,
13+
arch: value.arch,
14+
key: value.key,
15+
name: value.name,
16+
type: value.type,
17+
value: value.value,
18+
}));
19+
}
20+
21+
regKey.values((err: Error, res: RegistryItem[]) => {
22+
if (!parentPort) {
23+
throw new Error('Not in a worker thread');
24+
}
25+
const messageRes = copyRegistryValues(res);
26+
parentPort.postMessage({ err, res: messageRes });
27+
});
Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
// Copyright (c) Microsoft Corporation. All rights reserved.
23
// Licensed under the MIT License.
34

45
import { HKCU, HKLM, Options, REG_SZ, Registry, RegistryItem } from 'winreg';
6+
import * as path from 'path';
57
import { createDeferred } from '../../common/utils/async';
8+
import { executeWorkerFile } from './externalDependencies';
69

710
export { HKCU, HKLM, REG_SZ, Options };
811

@@ -22,30 +25,36 @@ export interface IRegistryValue {
2225
value: string;
2326
}
2427

25-
export async function readRegistryValues(options: Options): Promise<IRegistryValue[]> {
26-
// eslint-disable-next-line global-require
27-
const WinReg = require('winreg');
28-
const regKey = new WinReg(options);
29-
const deferred = createDeferred<RegistryItem[]>();
30-
regKey.values((err: Error, res: RegistryItem[]) => {
31-
if (err) {
32-
deferred.reject(err);
33-
}
34-
deferred.resolve(res);
35-
});
36-
return deferred.promise;
28+
export async function readRegistryValues(options: Options, useWorkerThreads: boolean): Promise<IRegistryValue[]> {
29+
if (!useWorkerThreads) {
30+
// eslint-disable-next-line global-require
31+
const WinReg = require('winreg');
32+
const regKey = new WinReg(options);
33+
const deferred = createDeferred<RegistryItem[]>();
34+
regKey.values((err: Error, res: RegistryItem[]) => {
35+
if (err) {
36+
deferred.reject(err);
37+
}
38+
deferred.resolve(res);
39+
});
40+
return deferred.promise;
41+
}
42+
return executeWorkerFile(path.join(__dirname, 'registryValuesWorker.js'), options);
3743
}
3844

39-
export async function readRegistryKeys(options: Options): Promise<IRegistryKey[]> {
40-
// eslint-disable-next-line global-require
41-
const WinReg = require('winreg');
42-
const regKey = new WinReg(options);
43-
const deferred = createDeferred<Registry[]>();
44-
regKey.keys((err: Error, res: Registry[]) => {
45-
if (err) {
46-
deferred.reject(err);
47-
}
48-
deferred.resolve(res);
49-
});
50-
return deferred.promise;
45+
export async function readRegistryKeys(options: Options, useWorkerThreads: boolean): Promise<IRegistryKey[]> {
46+
if (!useWorkerThreads) {
47+
// eslint-disable-next-line global-require
48+
const WinReg = require('winreg');
49+
const regKey = new WinReg(options);
50+
const deferred = createDeferred<Registry[]>();
51+
regKey.keys((err: Error, res: Registry[]) => {
52+
if (err) {
53+
deferred.reject(err);
54+
}
55+
deferred.resolve(res);
56+
});
57+
return deferred.promise;
58+
}
59+
return executeWorkerFile(path.join(__dirname, 'registryKeysWorker.js'), options);
5160
}

0 commit comments

Comments
 (0)