Skip to content

Commit 545b139

Browse files
improve ipywidgets performance by synchronizing only output widgets state changes
1 parent 6e5df57 commit 545b139

File tree

1 file changed

+57
-63
lines changed

1 file changed

+57
-63
lines changed

src/client/datascience/ipywidgets/ipyWidgetMessageDispatcher.ts

Lines changed: 57 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,23 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
5454
private totalWaitTime: number = 0;
5555
private totalWaitedMessages: number = 0;
5656
private hookCount: number = 0;
57-
private fullHandleMessage?: { id: string; promise: Deferred<void> };
5857
/**
59-
* This will be true if user has executed something that has resulted in the use of ipywidgets.
60-
* We make this determinination based on whether we see messages coming from backend kernel of a specific shape.
61-
* E.g. if it contains ipywidget mime type, then widgets are in use.
58+
* The Output widget's model can set up or tear down a kernel message hook on state change.
59+
* We need to wait until the kernel message hook has been connected before it's safe to send
60+
* more messages to the UI kernel.
61+
*
62+
* To do this we:
63+
* - Keep track of the id of all the Output widget models in the outputWidgetIds instance variable.
64+
* We add/remove these ids by inspecting messages in onKernelSocketMessage.
65+
* - When a state update message is sent to one of these widgets, we synchronize with the UI and
66+
* stop sending messages until we receive a reply indicating that the state change has been fully handled.
67+
* We keep track of the message we're waiting for in the fullHandleMessage instance variable.
68+
* We start waiting for the state change to finish processing in onKernelSocketMessage,
69+
* and we stop waiting in iopubMessageHandled.
6270
*/
63-
private isUsingIPyWidgets?: boolean;
71+
private outputWidgetIds = new Set<string>();
72+
private fullHandleMessage?: { id: string; promise: Deferred<void> };
73+
private isUsingIPyWidgets = false;
6474
private readonly deserialize: (data: string | ArrayBuffer) => KernelMessage.IMessage<KernelMessage.MessageType>;
6575

6676
constructor(private readonly kernelProvider: IKernelProvider, public readonly document: NotebookDocument) {
@@ -251,14 +261,12 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
251261
// fully handled on both the UI and extension side before we process the next message incoming
252262
private messageNeedsFullHandle(message: any) {
253263
// We only get a handled callback for iopub messages, so this channel must be iopub
254-
if (message.channel === 'iopub') {
255-
if (message.header?.msg_type === 'comm_msg') {
256-
// IOPub comm messages need to be fully handled
257-
return true;
258-
}
259-
}
260-
261-
return false;
264+
return (
265+
message.channel === 'iopub' &&
266+
message.header?.msg_type === 'comm_msg' &&
267+
message.content?.data?.method === 'update' &&
268+
this.outputWidgetIds.has(message.content?.comm_id)
269+
);
262270
}
263271

264272
// Callback from the UI kernel when an iopubMessage has been fully handled
@@ -272,46 +280,11 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
272280
}
273281
private async onKernelSocketMessage(data: WebSocketData): Promise<void> {
274282
// Hooks expect serialized data as this normally comes from a WebSocket
275-
let message: undefined | KernelMessage.ICommOpenMsg; // = this.deserialize(data as any) as any;
276-
if (!this.isUsingIPyWidgets) {
277-
// Lets deserialize only if we know we have a potential case
278-
// where this message contains some data we're interested in.
279-
let mustDeserialize = false;
280-
if (typeof data === 'string') {
281-
mustDeserialize = data.includes(WIDGET_MIMETYPE) || data.includes(Identifiers.DefaultCommTarget);
282-
} else {
283-
// Array buffers (non-plain text data) must be deserialized.
284-
mustDeserialize = true;
285-
}
286-
if (!message && mustDeserialize) {
287-
message = this.deserialize(data as any) as any;
288-
}
289-
290-
// Check for hints that would indicate whether ipywidgest are used in outputs.
291-
if (
292-
message &&
293-
message.content &&
294-
message.content.data &&
295-
(message.content.data[WIDGET_MIMETYPE] || message.content.target_name === Identifiers.DefaultCommTarget)
296-
) {
297-
this.isUsingIPyWidgets = true;
298-
}
299-
}
300283

301284
const msgUuid = uuid();
302285
const promise = createDeferred<void>();
303286
this.waitingMessageIds.set(msgUuid, { startTime: Date.now(), resultPromise: promise });
304287

305-
// Check if we need to fully handle this message on UI and Extension side before we move to the next
306-
if (this.isUsingIPyWidgets) {
307-
if (!message) {
308-
message = this.deserialize(data as any) as any;
309-
}
310-
if (this.messageNeedsFullHandle(message)) {
311-
this.fullHandleMessage = { id: message!.header.msg_id, promise: createDeferred<void>() };
312-
}
313-
}
314-
315288
if (typeof data === 'string') {
316289
this.raisePostMessage(IPyWidgetMessages.IPyWidgets_msg, { id: msgUuid, data });
317290
} else {
@@ -321,21 +294,42 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
321294
});
322295
}
323296

324-
// There are three handling states that we have for messages here
325-
// 1. If we have not detected ipywidget usage at all, we just forward messages to the kernel
326-
// 2. If we have detected ipywidget usage. We wait on our message to be received, but not
327-
// possibly processed yet by the UI kernel. This make sure our ordering is in sync
328-
// 3. For iopub comm messages we wait for them to be fully handled by the UI kernel
329-
// and the Extension kernel as they may be required to do things like
330-
// register message hooks on both sides before we process the nextExtension message
331-
332-
// If there are no ipywidgets thusfar in the notebook, then no need to synchronize messages.
333-
if (this.isUsingIPyWidgets) {
334-
await promise.promise;
335-
336-
// Comm specific iopub messages we need to wait until they are full handled
337-
// by both the UI and extension side before we move forward
338-
if (this.fullHandleMessage) {
297+
// Lets deserialize only if we know we have a potential case
298+
// where this message contains some data we're interested in.
299+
const mustDeserialize =
300+
typeof data !== 'string' ||
301+
data.includes(WIDGET_MIMETYPE) ||
302+
data.includes(Identifiers.DefaultCommTarget) ||
303+
data.includes('comm_open') ||
304+
data.includes('comm_close') ||
305+
data.includes('comm_msg');
306+
if (mustDeserialize) {
307+
const message = this.deserialize(data as any) as any;
308+
309+
// Check for hints that would indicate whether ipywidgest are used in outputs.
310+
if (
311+
message &&
312+
message.content &&
313+
message.content.data &&
314+
(message.content.data[WIDGET_MIMETYPE] || message.content.target_name === Identifiers.DefaultCommTarget)
315+
) {
316+
this.isUsingIPyWidgets = true;
317+
}
318+
319+
const isIPYWidgetOutputModelOpen =
320+
message.header?.msg_type === 'comm_open' &&
321+
message.content?.data?.state?._model_module === '@jupyter-widgets/output' &&
322+
message.content?.data?.state?._model_name === 'OutputModel';
323+
const isIPYWidgetOutputModelClose =
324+
message.header?.msg_type === 'comm_close' && this.outputWidgetIds.has(message.content?.comm_id);
325+
326+
if (isIPYWidgetOutputModelOpen) {
327+
this.outputWidgetIds.add(message.content.comm_id);
328+
} else if (isIPYWidgetOutputModelClose) {
329+
this.outputWidgetIds.delete(message.content.comm_id);
330+
} else if (this.messageNeedsFullHandle(message)) {
331+
this.fullHandleMessage = { id: message.header.msg_id, promise: createDeferred<void>() };
332+
await promise.promise;
339333
await this.fullHandleMessage.promise.promise;
340334
this.fullHandleMessage = undefined;
341335
}

0 commit comments

Comments
 (0)