Skip to content

Commit 61694c9

Browse files
committed
Swap controllers once kernel starts
1 parent 9bc1a06 commit 61694c9

31 files changed

+897
-355
lines changed

.github/workflows/build-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,8 @@ jobs:
281281
matrix:
282282
jupyter: [raw, local]
283283
python: [nonConda, conda]
284-
pythonVersion: ['3.9', '3.10']
285284
webOrDesktop: ['']
285+
pythonVersion: ['3.9', '3.10']
286286
# Whether we're using stable (<empty>) or pre-release versions of Python packages.
287287
# When installing pre-release versions, we're only focused on jupyter & related packages.
288288
# Not pre-release versions of pandas, numpy or other such packages that are not core to Jupyter.

TELEMETRY.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4141,7 +4141,7 @@ No properties for event
41414141
const telemetryEvent = isLocalConnection(this.kernelConnection)
41424142
? Telemetry.SelectLocalJupyterKernel
41434143
: Telemetry.SelectRemoteJupyterKernel;
4144-
sendKernelTelemetryEvent(document.uri, telemetryEvent);
4144+
sendKernelTelemetryEvent(notebook.uri, telemetryEvent);
41454145
this.notebookApi.notebookEditors
41464146
```
41474147
@@ -4168,9 +4168,9 @@ No properties for event
41684168
const telemetryEvent = isLocalConnection(this.kernelConnection)
41694169
? Telemetry.SelectLocalJupyterKernel
41704170
: Telemetry.SelectRemoteJupyterKernel;
4171-
sendKernelTelemetryEvent(document.uri, telemetryEvent);
4171+
sendKernelTelemetryEvent(notebook.uri, telemetryEvent);
41724172
this.notebookApi.notebookEditors
4173-
.filter((editor) => editor.notebook === document)
4173+
.filter((editor) => editor.notebook === notebook)
41744174
```
41754175
41764176
</details>
@@ -8871,7 +8871,7 @@ No properties for event
88718871
default:
88728872
// We don't know as its the default kernel on Jupyter server.
88738873
}
8874-
sendKernelTelemetryEvent(document.uri, Telemetry.SwitchKernel);
8874+
sendKernelTelemetryEvent(notebook.uri, Telemetry.SwitchKernel);
88758875
// If we have an existing kernel, then we know for a fact the user is changing the kernel.
88768876
// Else VSC is just setting a kernel for a notebook after it has opened.
88778877
if (existingKernel) {

news/2 Fixes/10572.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ensure the kernel picker points to the live Kernel Connection when starting a new remote kernel.

src/interactive-window/interactiveWindow.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { IConfigurationService, InteractiveWindowMode, IsWebExtension, Resource
3232
import { noop } from '../platform/common/utils/misc';
3333
import {
3434
IKernel,
35+
IKernelProvider,
3536
isLocalConnection,
3637
KernelAction,
3738
KernelConnectionMetadata,
@@ -41,7 +42,6 @@ import { chainable } from '../platform/common/utils/decorators';
4142
import { InteractiveCellResultError } from '../platform/errors/interactiveCellResultError';
4243
import { DataScience } from '../platform/common/utils/localize';
4344
import { createDeferred, Deferred } from '../platform/common/utils/async';
44-
import { IServiceContainer } from '../platform/ioc/types';
4545
import { SysInfoReason } from '../messageTypes';
4646
import { createOutputWithErrorMessageForDisplay } from '../platform/errors/errorUtils';
4747
import { INotebookExporter } from '../kernels/jupyter/types';
@@ -54,11 +54,9 @@ import {
5454
IInteractiveWindowDebuggingManager,
5555
InteractiveTab
5656
} from './types';
57-
import { generateInteractiveCode, isInteractiveInputTab } from './helpers';
57+
import { generateInteractiveCode, getInteractiveCellMetadata, isInteractiveInputTab } from './helpers';
5858
import { IControllerSelection, IVSCodeNotebookController } from '../notebooks/controllers/types';
5959
import { DisplayOptions } from '../kernels/displayOptions';
60-
import { getInteractiveCellMetadata } from './helpers';
61-
import { KernelConnector } from '../notebooks/controllers/kernelConnector';
6260
import { getFilePath } from '../platform/common/platform/fs-paths';
6361
import {
6462
ICodeGeneratorFactory,
@@ -71,6 +69,8 @@ import { updateNotebookMetadata } from '../kernels/execution/helpers';
7169
import { chainWithPendingUpdates } from '../kernels/execution/notebookUpdater';
7270
import { initializeInteractiveOrNotebookTelemetryBasedOnUserAction } from '../kernels/telemetry/helper';
7371
import { generateMarkdownFromCodeLines, parseForComments } from '../platform/common/utils';
72+
import { IServiceContainer } from '../platform/ioc/types';
73+
import { KernelConnector } from '../notebooks/controllers/kernelConnector';
7474

7575
/**
7676
* ViewModel for an interactive window from the Jupyter extension's point of view.
@@ -105,7 +105,8 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
105105
private kernelDisposables: Disposable[] = [];
106106
private _insertSysInfoPromise: Promise<NotebookCell> | undefined;
107107
private currentKernelInfo: {
108-
kernel?: Deferred<IKernel>;
108+
kernelStarted?: Deferred<void>;
109+
kernel?: IKernel;
109110
controller?: NotebookController;
110111
metadata?: KernelConnectionMetadata;
111112
} = {};
@@ -135,6 +136,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
135136
private readonly debuggingManager: IInteractiveWindowDebuggingManager;
136137
private readonly isWebExtension: boolean;
137138
private readonly commandManager: ICommandManager;
139+
private readonly kernelProvider: IKernelProvider;
138140
constructor(
139141
private readonly serviceContainer: IServiceContainer,
140142
private _owner: Resource,
@@ -156,6 +158,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
156158
this.errorHandler = this.serviceContainer.get<IDataScienceErrorHandler>(IDataScienceErrorHandler);
157159
this.codeGeneratorFactory = this.serviceContainer.get<ICodeGeneratorFactory>(ICodeGeneratorFactory);
158160
this.storageFactory = this.serviceContainer.get<IGeneratedCodeStorageFactory>(IGeneratedCodeStorageFactory);
161+
this.kernelProvider = this.serviceContainer.get<IKernelProvider>(IKernelProvider);
159162
this.debuggingManager = this.serviceContainer.get<IInteractiveWindowDebuggingManager>(
160163
IInteractiveWindowDebuggingManager
161164
);
@@ -229,31 +232,39 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
229232
// This cannot happen, but we need to make typescript happy.
230233
throw new Error('Controller not selected');
231234
}
232-
if (this.currentKernelInfo.kernel) {
233-
return this.currentKernelInfo.kernel.promise;
235+
if (this.currentKernelInfo.kernelStarted) {
236+
await this.currentKernelInfo.kernelStarted.promise;
237+
return this.currentKernelInfo.kernel!;
234238
}
235-
const kernelPromise = createDeferred<IKernel>();
236-
kernelPromise.promise.catch(noop);
237-
this.currentKernelInfo = { controller, metadata, kernel: kernelPromise };
239+
const kernelStarted = createDeferred<void>();
240+
kernelStarted.promise.catch(noop);
241+
this.currentKernelInfo = { controller, metadata, kernelStarted };
238242

239243
const sysInfoCell = this.insertSysInfoMessage(metadata, SysInfoReason.Start);
240244
try {
241245
// Try creating a kernel
242246
initializeInteractiveOrNotebookTelemetryBasedOnUserAction(this.owner, metadata);
243247

244-
const onStartKernel = (action: KernelAction, k: IKernel) => {
248+
const onKernelStarted = async (action: KernelAction, k: IKernel) => {
245249
if (action !== 'start' && action !== 'restart') {
246250
return;
247251
}
248252
// Id may be different if the user switched controllers
249-
this.currentKernelInfo.controller = k.controller;
250-
this.currentKernelInfo.metadata = k.kernelConnectionMetadata;
253+
this.currentKernelInfo.kernel = k;
251254
this.updateSysInfoMessage(
252255
this.getSysInfoMessage(k.kernelConnectionMetadata, SysInfoReason.Start),
253256
false,
254257
sysInfoCell
255258
);
256259
};
260+
const onKernelStartCompleted = async (action: KernelAction, _: unknown, k: IKernel) => {
261+
if (action !== 'start' && action !== 'restart') {
262+
return;
263+
}
264+
// Id may be different if the user switched controllers
265+
this.currentKernelInfo.controller = k.controller;
266+
this.currentKernelInfo.metadata = k.kernelConnectionMetadata;
267+
};
257268
// When connecting, we need to update the sys info message
258269
this.updateSysInfoMessage(this.getSysInfoMessage(metadata, SysInfoReason.Start), false, sysInfoCell);
259270
const kernel = await KernelConnector.connectToNotebookKernel(
@@ -264,7 +275,8 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
264275
new DisplayOptions(false),
265276
this.internalDisposables,
266277
'jupyterExtension',
267-
onStartKernel
278+
onKernelStarted,
279+
onKernelStartCompleted
268280
);
269281
this.currentKernelInfo.controller = kernel.controller;
270282
this.currentKernelInfo.metadata = kernel.kernelConnectionMetadata;
@@ -304,10 +316,11 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
304316
this.fileInKernel = undefined;
305317
await this.runInitialization(kernel, this.owner);
306318
this.finishSysInfoMessage(kernel, sysInfoCell, SysInfoReason.Start);
307-
kernelPromise.resolve(kernel);
319+
kernelStarted.resolve();
308320
return kernel;
309321
} catch (ex) {
310-
kernelPromise.reject(ex);
322+
kernelStarted.reject(ex);
323+
this.currentKernelInfo.kernelStarted = undefined;
311324
this.currentKernelInfo.kernel = undefined;
312325
this.disconnectKernel();
313326
if (this.owner) {
@@ -423,26 +436,8 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
423436
message = message.split('\n').join(' \n');
424437
this.updateSysInfoMessage(message, true, cellPromise);
425438
}
426-
private registerControllerChangeListener(controller: IVSCodeNotebookController) {
427-
const controllerChangeListener = controller.controller.onDidChangeSelectedNotebooks(
428-
(selectedEvent: { notebook: NotebookDocument; selected: boolean }) => {
429-
// Controller was deselected for this InteractiveWindow's NotebookDocument
430-
if (selectedEvent.selected === false && selectedEvent.notebook === this.notebookEditor.notebook) {
431-
controllerChangeListener.dispose();
432-
this.disconnectKernel();
433-
}
434-
},
435-
this,
436-
this.internalDisposables
437-
);
438-
}
439439

440440
private listenForControllerSelection() {
441-
const controller = this.notebookControllerSelection.getSelected(this.notebookEditor.notebook);
442-
if (controller !== undefined) {
443-
this.registerControllerChangeListener(controller);
444-
}
445-
446441
// Ensure we hear about any controller changes so we can update our cached promises
447442
this.notebookControllerSelection.onControllerSelected(
448443
(e: { notebook: NotebookDocument; controller: IVSCodeNotebookController }) => {
@@ -451,8 +446,13 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
451446
}
452447

453448
// Clear cached kernel when the selected controller for this document changes
454-
this.registerControllerChangeListener(e.controller);
455-
if (e.controller.id !== this.currentKernelInfo.controller?.id) {
449+
const kernel = this.kernelProvider.get(this.notebookDocument.uri);
450+
const isControllerAttachedToTheSameKernel =
451+
kernel && this.currentKernelInfo.kernel && kernel === this.currentKernelInfo.kernel;
452+
if (
453+
e.controller.connection.id !== this.currentKernelInfo.kernel?.kernelConnectionMetadata.id &&
454+
!isControllerAttachedToTheSameKernel
455+
) {
456456
this.disconnectKernel();
457457
this.startKernel(e.controller.controller, e.controller.connection).ignoreErrors();
458458
}
@@ -615,6 +615,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable {
615615
private disconnectKernel() {
616616
this.kernelDisposables.forEach((d) => d.dispose());
617617
this.kernelDisposables = [];
618+
this.currentKernelInfo.kernelStarted = undefined;
618619
this.currentKernelInfo.kernel = undefined;
619620
}
620621

src/kernels/ipywidgets/commonMessageCoordinator.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
'use strict';
55

66
import type { KernelMessage } from '@jupyterlab/services';
7-
import { Event, EventEmitter, NotebookDocument } from 'vscode';
8-
import { IApplicationShell, ICommandManager } from '../../platform/common/application/types';
9-
import { STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants';
7+
import { Event, EventEmitter, NotebookDocument, notebooks } from 'vscode';
8+
import { IApplicationShell, ICommandManager, IVSCodeNotebook } from '../../platform/common/application/types';
9+
import { IPyWidgetRendererId, STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants';
1010
import { traceVerbose, traceError, traceInfo, traceInfoIfCI } from '../../platform/logging';
1111
import {
1212
IDisposableRegistry,
@@ -65,7 +65,9 @@ export class CommonMessageCoordinator {
6565
private readonly configService: IConfigurationService;
6666
private webview: IWebviewCommunication | undefined;
6767
private modulesForWhichWeHaveDisplayedWidgetErrorMessage = new Set<string>();
68-
68+
private kernelProvider: IKernelProvider;
69+
private queuedMessages: { type: string; payload: unknown }[] = [];
70+
private readyMessageReceived?: boolean;
6971
public constructor(
7072
private readonly document: NotebookDocument,
7173
private readonly serviceContainer: IServiceContainer
@@ -75,6 +77,7 @@ export class CommonMessageCoordinator {
7577
this.appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell, IApplicationShell);
7678
this.commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
7779
this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
80+
this.kernelProvider = this.serviceContainer.get<IKernelProvider>(IKernelProvider);
7881
}
7982

8083
public dispose() {
@@ -103,6 +106,12 @@ export class CommonMessageCoordinator {
103106
response: webview.asWebviewUri(e.payload)
104107
});
105108
} else {
109+
if (!webview.isReady || !this.readyMessageReceived) {
110+
// Web view is not yet ready to receive messages, hence queue these to be sent later.
111+
this.queuedMessages.push({ type: e.message, payload: e.payload });
112+
return;
113+
}
114+
this.sendPendingWebViewMessages();
106115
webview.postMessage({ type: e.message, payload: e.payload }).then(noop, noop);
107116
}
108117
},
@@ -113,10 +122,55 @@ export class CommonMessageCoordinator {
113122
(m) => {
114123
traceInfoIfCI(`${ConsoleForegroundColors.Green}Widget Coordinator received ${m.type}`);
115124
this.onMessage(m.type, m.payload);
116-
125+
const kernel = this.kernelProvider.get(this.document.uri);
117126
// Special case the WidgetManager loaded message. It means we're ready
118127
// to use a kernel. (IPyWidget Dispatcher uses this too)
119128
if (m.type === IPyWidgetMessages.IPyWidgets_Ready) {
129+
if (kernel?.kernelConnectionMetadata.kind === 'startUsingRemoteKernelSpec') {
130+
traceInfoIfCI(
131+
'Web view is not ready to receive widget messages (kernel points to remote kernel spec)'
132+
);
133+
const nbEditor = this.serviceContainer
134+
.get<IVSCodeNotebook>(IVSCodeNotebook)
135+
.notebookEditors.find((item) => item.notebook === this.document);
136+
// With remote kernel specs, once the kernel is ready we create a live kernel controller and
137+
// switch to that. At that point the webview also changes, hence
138+
// there's no need to render anything while were in this state.
139+
notebooks
140+
.createRendererMessaging(IPyWidgetRendererId)
141+
.postMessage({ type: IPyWidgetMessages.IPyWidgets_DoNotRenderWidgets }, nbEditor)
142+
.then(noop, noop);
143+
return;
144+
}
145+
if (!webview.isReady) {
146+
traceInfoIfCI('Web view is not ready to receive widget messages');
147+
return;
148+
}
149+
traceInfoIfCI('Web view is ready to receive widget messages');
150+
this.readyMessageReceived = true;
151+
this.sendPendingWebViewMessages();
152+
// At this point, we know the kernels are ready, and the webview is ready to receive messages.
153+
// Its possible the webview was initially unable to render some widgets, but now that everything is ready we
154+
// should be able to render them now.
155+
// E.g assume we're dealing with remote kernel specs,
156+
// Then we start a kernel, next we create a controller that points to the live kernel & change the controller.
157+
// What happens now is the, webview is re-loaded (as theres a change in the controllers).
158+
// However if the user were to run a cell, then the output would get displayed and it could end up,
159+
// being displayed in the old webview & when the webview is re-loaded, it would be displayed in the new webview.
160+
// However its possble the kernel is not yet ready at that point in time.
161+
// We could solve this issue easily by not executing cells, until the webview is ready,
162+
// however that would have significant performance implications.
163+
// Hence for ipywidgets, once the webview has completely been initialized, we can attempt to re-render the widgets.
164+
const nbEditor = this.serviceContainer
165+
.get<IVSCodeNotebook>(IVSCodeNotebook)
166+
.notebookEditors.find((item) => item.notebook === this.document);
167+
if (nbEditor) {
168+
traceInfoIfCI('Re-rendering widgets');
169+
notebooks
170+
.createRendererMessaging(IPyWidgetRendererId)
171+
.postMessage({ type: IPyWidgetMessages.IPyWidgets_ReRenderWidgets }, nbEditor)
172+
.then(noop, noop);
173+
}
120174
promise.resolve();
121175
}
122176
},
@@ -155,6 +209,14 @@ export class CommonMessageCoordinator {
155209
this.getIPyWidgetScriptSource().onMessage(message, payload);
156210
}
157211

212+
private sendPendingWebViewMessages() {
213+
if (!this.webview || !this.webview.isReady || !this.readyMessageReceived) {
214+
return;
215+
}
216+
while (this.queuedMessages.length) {
217+
this.webview.postMessage(this.queuedMessages.shift()!).then(noop, noop);
218+
}
219+
}
158220
private initialize() {
159221
traceVerbose('initialize CommonMessageCoordinator');
160222
// First hook up the widget script source that will listen to messages even before we start sending messages.

0 commit comments

Comments
 (0)