Skip to content

Commit 0a0493a

Browse files
authored
Support kernel selection in VSC Native Notebooks (#13245)
1 parent 44fd2d8 commit 0a0493a

35 files changed

+1629
-1075
lines changed

src/client/common/application/notebook.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5-
import { Disposable, Event, EventEmitter, GlobPattern } from 'vscode';
5+
import { Disposable, Event, EventEmitter } from 'vscode';
66
import type {
77
notebook,
88
NotebookCellsChangeEvent as VSCNotebookCellsChangeEvent,
99
NotebookContentProvider,
1010
NotebookDocument,
11+
NotebookDocumentFilter,
1112
NotebookEditor,
1213
NotebookKernel,
14+
NotebookKernelProvider,
1315
NotebookOutputRenderer,
1416
NotebookOutputSelector
1517
} from 'vscode-proposed';
@@ -25,6 +27,17 @@ import {
2527

2628
@injectable()
2729
export class VSCodeNotebook implements IVSCodeNotebook {
30+
public get onDidChangeActiveNotebookKernel(): Event<{
31+
document: NotebookDocument;
32+
kernel: NotebookKernel | undefined;
33+
}> {
34+
return this.canUseNotebookApi
35+
? this.notebook.onDidChangeActiveNotebookKernel
36+
: new EventEmitter<{
37+
document: NotebookDocument;
38+
kernel: NotebookKernel | undefined;
39+
}>().event;
40+
}
2841
public get onDidChangeActiveNotebookEditor(): Event<NotebookEditor | undefined> {
2942
return this.canUseNotebookApi
3043
? this.notebook.onDidChangeActiveNotebookEditor
@@ -88,8 +101,11 @@ export class VSCodeNotebook implements IVSCodeNotebook {
88101
public registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider): Disposable {
89102
return this.notebook.registerNotebookContentProvider(notebookType, provider);
90103
}
91-
public registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable {
92-
return this.notebook.registerNotebookKernel(id, selectors, kernel);
104+
public registerNotebookKernelProvider(
105+
selector: NotebookDocumentFilter,
106+
provider: NotebookKernelProvider
107+
): Disposable {
108+
return this.notebook.registerNotebookKernelProvider(selector, provider);
93109
}
94110
public registerNotebookOutputRenderer(
95111
id: string,

src/client/common/application/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ import type {
6464
NotebookCellsChangeEvent as VSCNotebookCellsChangeEvent,
6565
NotebookContentProvider,
6666
NotebookDocument,
67+
NotebookDocumentFilter,
6768
NotebookEditor,
6869
NotebookKernel,
70+
NotebookKernelProvider,
6971
NotebookOutputRenderer,
7072
NotebookOutputSelector
7173
} from 'vscode-proposed';
@@ -1530,6 +1532,10 @@ export type NotebookCellChangedEvent =
15301532
| NotebookCellLanguageChangeEvent;
15311533
export const IVSCodeNotebook = Symbol('IVSCodeNotebook');
15321534
export interface IVSCodeNotebook {
1535+
readonly onDidChangeActiveNotebookKernel: Event<{
1536+
document: NotebookDocument;
1537+
kernel: NotebookKernel | undefined;
1538+
}>;
15331539
readonly notebookDocuments: ReadonlyArray<NotebookDocument>;
15341540
readonly onDidOpenNotebookDocument: Event<NotebookDocument>;
15351541
readonly onDidCloseNotebookDocument: Event<NotebookDocument>;
@@ -1539,7 +1545,7 @@ export interface IVSCodeNotebook {
15391545
readonly activeNotebookEditor: NotebookEditor | undefined;
15401546
registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider): Disposable;
15411547

1542-
registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable;
1548+
registerNotebookKernelProvider(selector: NotebookDocumentFilter, provider: NotebookKernelProvider): Disposable;
15431549

15441550
registerNotebookOutputRenderer(
15451551
id: string,

src/client/datascience/baseJupyterSession.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ export abstract class BaseJupyterSession implements IJupyterSession {
490490
): Promise<void | null> {
491491
// Wait for this kernel promise to happen
492492
try {
493-
return await waitForPromise(kernelPromise, timeout);
493+
await waitForPromise(kernelPromise, timeout);
494494
} catch (e) {
495495
if (!e) {
496496
// We timed out. Throw a specific exception

src/client/datascience/jupyter/jupyterNotebook.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,14 @@ export class JupyterNotebookBase implements INotebook {
159159
private _executionInfo: INotebookExecutionInfo;
160160
private onStatusChangedEvent: EventEmitter<ServerStatus> | undefined;
161161
public get onDisposed(): Event<void> {
162-
return this.disposed.event;
162+
return this.disposedEvent.event;
163163
}
164164
public get onKernelChanged(): Event<IJupyterKernelSpec | LiveKernelModel> {
165165
return this.kernelChanged.event;
166166
}
167+
public get disposed() {
168+
return this._disposed;
169+
}
167170
private kernelChanged = new EventEmitter<IJupyterKernelSpec | LiveKernelModel>();
168171
public get onKernelRestarted(): Event<void> {
169172
return this.kernelRestarted.event;
@@ -174,7 +177,7 @@ export class JupyterNotebookBase implements INotebook {
174177
}
175178
private readonly kernelRestarted = new EventEmitter<void>();
176179
private readonly kernelInterrupted = new EventEmitter<void>();
177-
private disposed = new EventEmitter<void>();
180+
private disposedEvent = new EventEmitter<void>();
178181
private sessionStatusChanged: Disposable | undefined;
179182
private initializedMatplotlib = false;
180183
private ioPubListeners = new Set<(msg: KernelMessage.IIOPubMessage, requestId: string) => void>();
@@ -227,7 +230,7 @@ export class JupyterNotebookBase implements INotebook {
227230
this.onStatusChangedEvent = undefined;
228231
}
229232
this.loggers.forEach((d) => d.dispose());
230-
this.disposed.fire();
233+
this.disposedEvent.fire();
231234

232235
try {
233236
traceInfo(`Shutting down session ${this.identity.toString()}`);
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { nbformat } from '@jupyterlab/coreutils';
7+
import type { KernelMessage } from '@jupyterlab/services';
8+
import { Observable } from 'rxjs/Observable';
9+
import { Subject } from 'rxjs/Subject';
10+
import { CancellationToken, Event, EventEmitter, Uri } from 'vscode';
11+
import { ServerStatus } from '../../../../datascience-ui/interactive-common/mainState';
12+
import { traceError } from '../../../common/logger';
13+
import { IDisposableRegistry } from '../../../common/types';
14+
import { createDeferred, Deferred } from '../../../common/utils/async';
15+
import { getDefaultNotebookContent, updateNotebookMetadata } from '../../notebookStorage/baseModel';
16+
import type {
17+
ICell,
18+
IJupyterKernelSpec,
19+
INotebook,
20+
INotebookProvider,
21+
INotebookProviderConnection,
22+
InterruptResult,
23+
KernelSocketInformation
24+
} from '../../types';
25+
import type { IKernel, KernelSelection, LiveKernelModel } from './types';
26+
27+
export class Kernel implements IKernel {
28+
get connection(): INotebookProviderConnection | undefined {
29+
return this._notebook?.connection;
30+
}
31+
get kernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined {
32+
if (this._notebook) {
33+
return this._notebook.getKernelSpec();
34+
}
35+
return this._metadata.kernelSpec || this._metadata.kernelModel;
36+
}
37+
get onStatusChanged(): Event<ServerStatus> {
38+
return this._onStatusChanged.event;
39+
}
40+
get onRestarted(): Event<void> {
41+
return this._onRestarted.event;
42+
}
43+
get onDisposed(): Event<void> {
44+
return this._onDisposed.event;
45+
}
46+
get status(): ServerStatus {
47+
return this._notebook?.status ?? ServerStatus.NotStarted;
48+
}
49+
get disposed(): boolean {
50+
return this._disposed === true || this._notebook?.disposed === true;
51+
}
52+
get kernelSocket(): Observable<KernelSocketInformation | undefined> {
53+
return this._kernelSocket.asObservable();
54+
}
55+
private _disposed?: boolean;
56+
private readonly _kernelSocket = new Subject<KernelSocketInformation | undefined>();
57+
private readonly _onStatusChanged = new EventEmitter<ServerStatus>();
58+
private readonly _onRestarted = new EventEmitter<void>();
59+
private readonly _onDisposed = new EventEmitter<void>();
60+
private _notebook?: INotebook;
61+
private _notebookPromise?: Promise<INotebook | undefined>;
62+
private readonly hookedNotebookForEvents = new WeakSet<INotebook>();
63+
private restarting?: Deferred<void>;
64+
constructor(
65+
public readonly uri: Uri,
66+
private readonly _metadata: KernelSelection,
67+
private readonly notebookProvider: INotebookProvider,
68+
private readonly disposables: IDisposableRegistry,
69+
private readonly waitForIdleTimeoutMs: number,
70+
private readonly launchingFile?: string
71+
) {}
72+
public executeObservable(
73+
code: string,
74+
file: string,
75+
line: number,
76+
id: string,
77+
silent: boolean
78+
): Observable<ICell[]> {
79+
if (!this._notebook) {
80+
throw new Error('executeObservable cannot be called if kernel has not been started!');
81+
}
82+
this._notebook.clear(id);
83+
return this._notebook.executeObservable(code, file, line, id, silent);
84+
}
85+
public async start(options?: { disableUI?: boolean; token?: CancellationToken }): Promise<void> {
86+
if (this.restarting) {
87+
await this.restarting.promise;
88+
}
89+
if (this._notebookPromise) {
90+
await this._notebookPromise;
91+
return;
92+
} else {
93+
const metadata = ((getDefaultNotebookContent().metadata || {}) as unknown) as nbformat.INotebookMetadata;
94+
updateNotebookMetadata(
95+
metadata,
96+
this._metadata.interpreter,
97+
this._metadata.kernelSpec || this._metadata.kernelModel
98+
);
99+
100+
this._notebookPromise = this.notebookProvider.getOrCreateNotebook({
101+
identity: this.uri,
102+
resource: this.uri,
103+
disableUI: options?.disableUI,
104+
getOnly: false,
105+
metadata,
106+
token: options?.token
107+
});
108+
109+
this._notebookPromise
110+
.then((nb) => (this._notebook = nb))
111+
.catch((ex) => traceError('failed to create INotebook in kernel', ex));
112+
await this._notebookPromise;
113+
await this.initializeAfterStart();
114+
}
115+
}
116+
public async interrupt(timeoutInMs: number): Promise<InterruptResult> {
117+
if (this.restarting) {
118+
await this.restarting.promise;
119+
}
120+
if (!this._notebook) {
121+
throw new Error('No notebook to interrupt');
122+
}
123+
return this._notebook.interruptKernel(timeoutInMs);
124+
}
125+
public async dispose(): Promise<void> {
126+
this.restarting = undefined;
127+
if (this._notebook) {
128+
await this._notebook.dispose();
129+
this._disposed = true;
130+
this._onDisposed.fire();
131+
this._onStatusChanged.fire(ServerStatus.Dead);
132+
this._notebook = undefined;
133+
}
134+
}
135+
public async restart(timeoutInMs: number): Promise<void> {
136+
if (this.restarting) {
137+
return this.restarting.promise;
138+
}
139+
if (this._notebook) {
140+
this.restarting = createDeferred<void>();
141+
try {
142+
await this._notebook.restartKernel(timeoutInMs);
143+
await this.initializeAfterStart();
144+
this.restarting.resolve();
145+
} catch (ex) {
146+
this.restarting.reject(ex);
147+
} finally {
148+
this.restarting = undefined;
149+
}
150+
}
151+
}
152+
public registerIOPubListener(listener: (msg: KernelMessage.IIOPubMessage, requestId: string) => void): void {
153+
if (!this._notebook) {
154+
throw new Error('Notebook not defined');
155+
}
156+
this._notebook.registerIOPubListener(listener);
157+
}
158+
private async initializeAfterStart() {
159+
if (!this._notebook) {
160+
return;
161+
}
162+
if (!this.hookedNotebookForEvents.has(this._notebook)) {
163+
this.hookedNotebookForEvents.add(this._notebook);
164+
this._notebook.kernelSocket.subscribe(this._kernelSocket);
165+
this._notebook.onDisposed(() => {
166+
this._onDisposed.fire();
167+
});
168+
this._notebook.onKernelRestarted(() => {
169+
this._onRestarted.fire();
170+
});
171+
this._notebook.onSessionStatusChanged((e) => this._onStatusChanged.fire(e), this, this.disposables);
172+
}
173+
if (this.launchingFile) {
174+
await this._notebook.setLaunchingFile(this.launchingFile);
175+
}
176+
await this._notebook.waitForIdle(this.waitForIdleTimeoutMs);
177+
}
178+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { Uri } from 'vscode';
8+
import { traceWarning } from '../../../common/logger';
9+
import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../../common/types';
10+
import { INotebookProvider } from '../../types';
11+
import { Kernel } from './kernel';
12+
import { IKernel, KernelOptions } from './types';
13+
14+
@injectable()
15+
export class KernelProvider {
16+
private readonly kernelsByUri = new Map<string, { options: KernelOptions; kernel: IKernel }>();
17+
constructor(
18+
@inject(IAsyncDisposableRegistry) private asyncDisposables: IAsyncDisposableRegistry,
19+
@inject(IDisposableRegistry) private disposables: IDisposableRegistry,
20+
@inject(INotebookProvider) private notebookProvider: INotebookProvider,
21+
@inject(IConfigurationService) private configService: IConfigurationService
22+
) {}
23+
public get(uri: Uri): IKernel | undefined {
24+
return this.kernelsByUri.get(uri.toString())?.kernel;
25+
}
26+
public getOrCreate(uri: Uri, options: KernelOptions): IKernel | undefined {
27+
const existingKernelInfo = this.kernelsByUri.get(uri.toString());
28+
if (
29+
existingKernelInfo &&
30+
JSON.stringify(existingKernelInfo.options.metadata) === JSON.stringify(options.metadata)
31+
) {
32+
return existingKernelInfo.kernel;
33+
}
34+
35+
this.disposeOldKernel(uri);
36+
37+
const waitForIdleTimeout =
38+
options?.waitForIdleTimeout ?? this.configService.getSettings(uri).datascience.jupyterLaunchTimeout;
39+
const kernel = new Kernel(
40+
uri,
41+
options.metadata,
42+
this.notebookProvider,
43+
this.disposables,
44+
waitForIdleTimeout,
45+
options.launchingFile
46+
);
47+
this.asyncDisposables.push(kernel);
48+
this.kernelsByUri.set(uri.toString(), { options, kernel });
49+
this.deleteMappingIfKernelIsDisposed(uri, kernel);
50+
return kernel;
51+
}
52+
/**
53+
* If a kernel has been disposed, then remove the mapping of Uri + Kernel.
54+
*/
55+
private deleteMappingIfKernelIsDisposed(uri: Uri, kernel: IKernel) {
56+
kernel.onDisposed(
57+
() => {
58+
if (this.get(uri) === kernel) {
59+
this.kernelsByUri.delete(uri.toString());
60+
}
61+
},
62+
this,
63+
this.disposables
64+
);
65+
}
66+
private disposeOldKernel(uri: Uri) {
67+
this.kernelsByUri
68+
.get(uri.toString())
69+
?.kernel.dispose()
70+
.catch((ex) => traceWarning('Failed to dispose old kernel', ex)); // NOSONAR.
71+
this.kernelsByUri.delete(uri.toString());
72+
}
73+
}
74+
75+
// export class KernelProvider {

0 commit comments

Comments
 (0)