Skip to content

Commit bc9cd8b

Browse files
Add an optional onUpdated event to the iterator returned by ILocator.iterEnvs(). (#13950)
In order to ensure that ILocator.iterEnvs() can finish as fast as possible, we add a side-channel event for each iteration. This event fires any time an already-yielded env info object is updated (e.g. reduced/merged or resolved/completed). The update only relates to operations triggered by that particular iteration. ILocator.onChanged remains separate and only relates to when the locators finds a new env, notices one was removed, or that one was otherwise fundamentally changed. There are two similar approaches we could take for this update event. Either we added the event as a property of IPythonEnvsIterator or we change the return value of ILocator.iterEnvs() to be a 2-tuple ([PythonEnvsIterator, Event<PythonEnvUpdatedEvent>]). We took the property approach since most of the time callers won't need to worry about the update events.
1 parent 40bc21c commit bc9cd8b

File tree

5 files changed

+126
-21
lines changed

5 files changed

+126
-21
lines changed

src/client/pythonEnvironments/base/locator.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,68 @@
44
import { Event, Uri } from 'vscode';
55
import { iterEmpty } from '../../common/utils/async';
66
import { PythonEnvInfo, PythonEnvKind } from './info';
7-
import { BasicPythonEnvsChangedEvent, IPythonEnvsWatcher, PythonEnvsChangedEvent, PythonEnvsWatcher } from './watcher';
7+
import {
8+
BasicPythonEnvsChangedEvent,
9+
IPythonEnvsWatcher,
10+
PythonEnvsChangedEvent,
11+
PythonEnvsWatcher
12+
} from './watcher';
813

914
/**
10-
* An async iterator of `PythonEnvInfo`.
15+
* A single update to a previously provided Python env object.
1116
*/
12-
export type PythonEnvsIterator = AsyncIterator<PythonEnvInfo, void>;
17+
export type PythonEnvUpdatedEvent = {
18+
/**
19+
* The env info that was previously provided.
20+
*
21+
* If the event comes from `IPythonEnvsIterator.onUpdated` then
22+
* `old` was previously yielded during iteration.
23+
*/
24+
old: PythonEnvInfo;
25+
/**
26+
* The env info that replaces the old info.
27+
*/
28+
new: PythonEnvInfo;
29+
};
30+
31+
/**
32+
* A fast async iterator of Python envs, which may have incomplete info.
33+
*
34+
* Each object yielded by the iterator represents a unique Python
35+
* environment.
36+
*
37+
* The iterator is not required to have provide all info about
38+
* an environment. However, each yielded item will at least
39+
* include all the `PythonEnvBaseInfo` data.
40+
*
41+
* During iteration the information for an already
42+
* yielded object may be updated. Rather than updating the yielded
43+
* object or yielding it again with updated info, the update is
44+
* emitted by the iterator's `onUpdated` (event) property. Once there are no more updates, the event emits
45+
* `null`.
46+
*
47+
* If the iterator does not have `onUpdated` then it means the
48+
* provider does not support updates.
49+
*
50+
* Callers can usually ignore the update event entirely and rely on
51+
* the locator to provide sufficiently complete information.
52+
*/
53+
export interface IPythonEnvsIterator extends AsyncIterator<PythonEnvInfo, void> {
54+
/**
55+
* Provides possible updates for already-iterated envs.
56+
*
57+
* Once there are no more updates, `null` is emitted.
58+
*
59+
* If this property is not provided then it means the iterator does
60+
* not support updates.
61+
*/
62+
onUpdated?: Event<PythonEnvUpdatedEvent | null>;
63+
}
1364

1465
/**
1566
* An empty Python envs iterator.
1667
*/
17-
export const NOOP_ITERATOR: PythonEnvsIterator = iterEmpty<PythonEnvInfo>();
68+
export const NOOP_ITERATOR: IPythonEnvsIterator = iterEmpty<PythonEnvInfo>();
1869

1970
/**
2071
* The most basic info to send to a locator when requesting environments.
@@ -64,11 +115,18 @@ export interface ILocator<E extends BasicPythonEnvsChangedEvent = PythonEnvsChan
64115
*
65116
* Locators are not required to have provide all info about
66117
* an environment. However, each yielded item will at least
67-
* include all the `PythonEnvBaseInfo` data.
118+
* include all the `PythonEnvBaseInfo` data. To ensure all
119+
* possible information is filled in, call `ILocator.resolveEnv()`.
120+
*
121+
* Updates to yielded objects may be provided via the optional
122+
* `onUpdated` property of the iterator. However, callers can
123+
* usually ignore the update event entirely and rely on the
124+
* locator to provide sufficiently complete information.
68125
*
69126
* @param query - if provided, the locator will limit results to match
127+
* @returns - the fast async iterator of Python envs, which may have incomplete info
70128
*/
71-
iterEnvs(query?: QueryForEvent<E>): PythonEnvsIterator;
129+
iterEnvs(query?: QueryForEvent<E>): IPythonEnvsIterator;
72130

73131
/**
74132
* Find the given Python environment and fill in as much missing info as possible.
@@ -111,7 +169,7 @@ export abstract class LocatorBase<E extends BasicPythonEnvsChangedEvent = Python
111169
this.onChanged = watcher.onChanged;
112170
}
113171

114-
public abstract iterEnvs(query?: QueryForEvent<E>): PythonEnvsIterator;
172+
public abstract iterEnvs(query?: QueryForEvent<E>): IPythonEnvsIterator;
115173

116174
public async resolveEnv(_env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
117175
return undefined;

src/client/pythonEnvironments/base/locators.ts

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

4+
import { EventEmitter } from 'vscode';
45
import { chain } from '../../common/utils/async';
56
import { PythonEnvInfo } from './info';
6-
import { ILocator, NOOP_ITERATOR, PythonEnvsIterator, PythonLocatorQuery } from './locator';
7+
import {
8+
ILocator,
9+
IPythonEnvsIterator,
10+
NOOP_ITERATOR,
11+
PythonEnvUpdatedEvent,
12+
PythonLocatorQuery
13+
} from './locator';
714
import { DisableableEnvsWatcher, PythonEnvsWatchers } from './watchers';
815

16+
/**
17+
* Combine the `onUpdated` event of the given iterators into a single event.
18+
*/
19+
export function combineIterators(iterators: IPythonEnvsIterator[]): IPythonEnvsIterator {
20+
const result: IPythonEnvsIterator = chain(iterators);
21+
const events = iterators.map((it) => it.onUpdated).filter((v) => v);
22+
if (!events || events.length === 0) {
23+
// There are no sub-events, so we leave `onUpdated` undefined.
24+
return result;
25+
}
26+
27+
const emitter = new EventEmitter<PythonEnvUpdatedEvent | null>();
28+
let numActive = events.length;
29+
events.forEach((event) => {
30+
event!((e: PythonEnvUpdatedEvent | null) => {
31+
if (e === null) {
32+
numActive -= 1;
33+
if (numActive === 0) {
34+
// All the sub-events are done so we're done.
35+
emitter.fire(null);
36+
}
37+
} else {
38+
emitter.fire(e);
39+
}
40+
});
41+
});
42+
result.onUpdated = emitter.event;
43+
return result;
44+
}
45+
946
/**
1047
* A wrapper around a set of locators, exposing them as a single locator.
1148
*
@@ -19,9 +56,9 @@ export class Locators extends PythonEnvsWatchers implements ILocator {
1956
super(locators);
2057
}
2158

22-
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
59+
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator {
2360
const iterators = this.locators.map((loc) => loc.iterEnvs(query));
24-
return chain(iterators);
61+
return combineIterators(iterators);
2562
}
2663

2764
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
@@ -50,7 +87,7 @@ export class DisableableLocator extends DisableableEnvsWatcher implements ILocat
5087
super(locator);
5188
}
5289

53-
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
90+
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator {
5491
if (!this.enabled) {
5592
return NOOP_ITERATOR;
5693
}

src/client/pythonEnvironments/discovery/locators/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
import { traceDecorators } from '../../../common/logger';
66
import { IPlatformService } from '../../../common/platform/types';
77
import { IDisposableRegistry } from '../../../common/types';
8-
import { chain, createDeferred, Deferred } from '../../../common/utils/async';
8+
import { createDeferred, Deferred } from '../../../common/utils/async';
99
import { OSType } from '../../../common/utils/platform';
1010
import {
1111
CONDA_ENV_FILE_SERVICE,
@@ -21,8 +21,18 @@ import {
2121
} from '../../../interpreter/contracts';
2222
import { IServiceContainer } from '../../../ioc/types';
2323
import { PythonEnvInfo } from '../../base/info';
24-
import { ILocator, Locator, NOOP_ITERATOR, PythonEnvsIterator, PythonLocatorQuery } from '../../base/locator';
25-
import { DisableableLocator, Locators } from '../../base/locators';
24+
import {
25+
ILocator,
26+
IPythonEnvsIterator,
27+
Locator,
28+
NOOP_ITERATOR,
29+
PythonLocatorQuery,
30+
} from '../../base/locator';
31+
import {
32+
combineIterators,
33+
DisableableLocator,
34+
Locators,
35+
} from '../../base/locators';
2636
import { PythonEnvironment } from '../../info';
2737
import { isHiddenInterpreter } from './services/interpreterFilter';
2838
import { GetInterpreterLocatorOptions } from './types';
@@ -83,7 +93,7 @@ export class WorkspaceLocators extends Locator {
8393
folders.onRemoved((root: Uri) => this.removeRoot(root));
8494
}
8595

86-
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
96+
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator {
8797
const iterators = Object.keys(this.locators).map((key) => {
8898
if (query?.searchLocations) {
8999
const root = this.roots[key];
@@ -95,7 +105,7 @@ export class WorkspaceLocators extends Locator {
95105
const locator = this.locators[key];
96106
return locator.iterEnvs(query);
97107
});
98-
return chain(iterators);
108+
return combineIterators(iterators);
99109
}
100110

101111
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {

src/client/pythonEnvironments/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import * as vscode from 'vscode';
55
import { IServiceContainer, IServiceManager } from '../ioc/types';
66
import { PythonEnvInfo } from './base/info';
7-
import { ILocator, PythonEnvsIterator, PythonLocatorQuery } from './base/locator';
7+
import { ILocator, IPythonEnvsIterator, PythonLocatorQuery } from './base/locator';
88
import { PythonEnvsChangedEvent } from './base/watcher';
99
import { ExtensionLocators, WorkspaceLocators } from './discovery/locators';
1010
import { registerForIOC } from './legacyIOC';
@@ -33,7 +33,7 @@ export class PythonEnvironments implements ILocator {
3333
return this.locators.onChanged;
3434
}
3535

36-
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
36+
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator {
3737
return this.locators.iterEnvs(query);
3838
}
3939

src/test/pythonEnvironments/base/common.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
PythonReleaseLevel,
1111
PythonVersion
1212
} from '../../../client/pythonEnvironments/base/info';
13-
import { Locator, PythonEnvsIterator, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator';
13+
import { IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator';
1414
import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher';
1515

1616
export function createEnv(
@@ -111,7 +111,7 @@ export class SimpleLocator extends Locator {
111111
public fire(event: PythonEnvsChangedEvent) {
112112
this.emitter.fire(event);
113113
}
114-
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
114+
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator {
115115
const deferred = this.deferred;
116116
const callbacks = this.callbacks;
117117
let envs = this.envs;
@@ -161,6 +161,6 @@ export class SimpleLocator extends Locator {
161161
}
162162
}
163163

164-
export async function getEnvs(iterator: PythonEnvsIterator): Promise<PythonEnvInfo[]> {
164+
export async function getEnvs(iterator: IPythonEnvsIterator): Promise<PythonEnvInfo[]> {
165165
return flattenIterator(iterator);
166166
}

0 commit comments

Comments
 (0)