Skip to content

Commit 8874d65

Browse files
committed
Base Manager: Use control comm target for fetching all widgets from the
kernel
1 parent 32f59ac commit 8874d65

File tree

3 files changed

+239
-163
lines changed

3 files changed

+239
-163
lines changed

packages/base-manager/src/manager-base.ts

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
import * as services from '@jupyterlab/services';
55
import * as widgets from '@jupyter-widgets/base';
66

7-
import { JSONObject, PartialJSONObject } from '@lumino/coreutils';
7+
import {
8+
JSONObject,
9+
PartialJSONObject,
10+
PromiseDelegate,
11+
} from '@lumino/coreutils';
812

913
import {
1014
DOMWidgetView,
@@ -32,6 +36,16 @@ import sanitize from 'sanitize-html';
3236

3337
const PROTOCOL_MAJOR_VERSION = PROTOCOL_VERSION.split('.', 1)[0];
3438

39+
/**
40+
* The control comm target name.
41+
*/
42+
export const CONTROL_COMM_TARGET = 'jupyter.widget.control';
43+
44+
/**
45+
* The supported version for the control comm channel.
46+
*/
47+
export const CONTROL_COMM_PROTOCOL_VERSION = '1.0.0';
48+
3549
/**
3650
* Sanitize HTML-formatted descriptions.
3751
*/
@@ -342,6 +356,191 @@ export abstract class ManagerBase implements IWidgetManager {
342356
return await modelPromise;
343357
}
344358

359+
/**
360+
* Fetch all widgets states from the kernel using the control comm channel
361+
* If this fails (control comm handler not implemented kernel side),
362+
* it will fallback to `_loadFromKernelSlow`.
363+
*/
364+
protected async _loadFromKernel(): Promise<void> {
365+
// Try fetching all widget states through the control comm
366+
let data: any;
367+
let buffers: any;
368+
try {
369+
const initComm = await this._create_comm(
370+
CONTROL_COMM_TARGET,
371+
uuid(),
372+
{ widgets: null },
373+
{ version: CONTROL_COMM_PROTOCOL_VERSION }
374+
);
375+
376+
await new Promise((resolve, reject) => {
377+
initComm.on_msg((msg: any) => {
378+
data = msg['content']['data'];
379+
380+
if (data.method !== 'update_states') {
381+
console.warn(`
382+
Unknown ${data.method} message on the Control channel
383+
`);
384+
return;
385+
}
386+
387+
buffers = (msg.buffers || []).map((b: any) => {
388+
if (b instanceof DataView) {
389+
return b;
390+
} else {
391+
return new DataView(b instanceof ArrayBuffer ? b : b.buffer);
392+
}
393+
});
394+
395+
resolve(null);
396+
});
397+
398+
initComm.on_close(reject);
399+
400+
// Send a states request msg
401+
initComm.send({ method: 'request_states' }, {});
402+
});
403+
404+
initComm.close();
405+
} catch (error) {
406+
console.warn(
407+
'Failed to open "jupyter.widget.control" comm channel, fallback to slow fetching of widgets.',
408+
error
409+
);
410+
// Fallback to the old implementation for old ipywidgets backend versions (<=7.6)
411+
return this._loadFromKernelSlow();
412+
}
413+
414+
const states: any = data.states;
415+
416+
// Extract buffer paths
417+
const bufferPaths: any = {};
418+
for (const bufferPath of data.buffer_paths) {
419+
if (!bufferPaths[bufferPath[0]]) {
420+
bufferPaths[bufferPath[0]] = [];
421+
}
422+
bufferPaths[bufferPath[0]].push(bufferPath.slice(1));
423+
}
424+
425+
// Start creating all widgets
426+
await Promise.all(
427+
Object.keys(states).map(async (widget_id) => {
428+
try {
429+
const state = states[widget_id];
430+
const comm = await this._create_comm('jupyter.widget', widget_id);
431+
432+
// Put binary buffers
433+
if (widget_id in bufferPaths) {
434+
const nBuffers = bufferPaths[widget_id].length;
435+
put_buffers(
436+
state,
437+
bufferPaths[widget_id],
438+
buffers.splice(0, nBuffers)
439+
);
440+
}
441+
442+
await this.new_model(
443+
{
444+
model_name: state.model_name,
445+
model_module: state.model_module,
446+
model_module_version: state.model_module_version,
447+
model_id: widget_id,
448+
comm: comm,
449+
},
450+
state.state
451+
);
452+
} catch (error) {
453+
// Failed to create a widget model, we continue creating other models so that
454+
// other widgets can render
455+
console.error(error);
456+
}
457+
})
458+
);
459+
}
460+
461+
/**
462+
* Old implementation of fetching widgets one by one using
463+
* the request_state message on each comm.
464+
*/
465+
protected async _loadFromKernelSlow(): Promise<void> {
466+
const comm_ids = await this._get_comm_info();
467+
468+
// For each comm id that we do not know about, create the comm, and request the state.
469+
const widgets_info = await Promise.all(
470+
Object.keys(comm_ids).map(async (comm_id) => {
471+
try {
472+
const model = this.get_model(comm_id);
473+
// TODO Have the same this.get_model implementation for
474+
// the widgetsnbextension and labextension, the one that
475+
// throws an error if the model is not found instead of
476+
// returning undefined
477+
if (model === undefined) {
478+
throw new Error('widget model not found');
479+
}
480+
await model;
481+
// If we successfully get the model, do no more.
482+
return;
483+
} catch (e) {
484+
// If we have the widget model not found error, then we can create the
485+
// widget. Otherwise, rethrow the error. We have to check the error
486+
// message text explicitly because the get_model function in this
487+
// class throws a generic error with this specific text.
488+
if (e.message !== 'widget model not found') {
489+
throw e;
490+
}
491+
const comm = await this._create_comm(this.comm_target_name, comm_id);
492+
493+
let msg_id = '';
494+
const info = new PromiseDelegate<Private.ICommUpdateData>();
495+
comm.on_msg((msg: services.KernelMessage.ICommMsgMsg) => {
496+
if (
497+
(msg.parent_header as any).msg_id === msg_id &&
498+
msg.header.msg_type === 'comm_msg' &&
499+
msg.content.data.method === 'update'
500+
) {
501+
const data = msg.content.data as any;
502+
const buffer_paths = data.buffer_paths || [];
503+
const buffers = msg.buffers || [];
504+
put_buffers(data.state, buffer_paths, buffers);
505+
info.resolve({ comm, msg });
506+
}
507+
});
508+
msg_id = comm.send(
509+
{
510+
method: 'request_state',
511+
},
512+
this.callbacks(undefined)
513+
);
514+
515+
return info.promise;
516+
}
517+
})
518+
);
519+
520+
// We put in a synchronization barrier here so that we don't have to
521+
// topologically sort the restored widgets. `new_model` synchronously
522+
// registers the widget ids before reconstructing their state
523+
// asynchronously, so promises to every widget reference should be available
524+
// by the time they are used.
525+
await Promise.all(
526+
widgets_info.map(async (widget_info) => {
527+
if (!widget_info) {
528+
return;
529+
}
530+
const content = widget_info.msg.content as any;
531+
await this.new_model(
532+
{
533+
model_name: content.data.state._model_name,
534+
model_module: content.data.state._model_module,
535+
model_module_version: content.data.state._model_module_version,
536+
comm: widget_info.comm,
537+
},
538+
content.data.state
539+
);
540+
})
541+
);
542+
}
543+
345544
async _make_model(
346545
options: RequiredSome<IModelOptions, 'model_id'>,
347546
serialized_state: any = {}
@@ -690,3 +889,13 @@ export function serialize_state(
690889
});
691890
return { version_major: 2, version_minor: 0, state: state };
692891
}
892+
893+
namespace Private {
894+
/**
895+
* Data promised when a comm info request resolves.
896+
*/
897+
export interface ICommUpdateData {
898+
comm: IClassicComm;
899+
msg: services.KernelMessage.ICommMsgMsg;
900+
}
901+
}

python/jupyterlab_widgets/src/manager.ts

Lines changed: 2 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
ExportData,
1010
WidgetModel,
1111
WidgetView,
12-
put_buffers,
1312
ICallbacks,
1413
} from '@jupyter-widgets/base';
1514

@@ -21,7 +20,7 @@ import {
2120

2221
import { IDisposable } from '@lumino/disposable';
2322

24-
import { PromiseDelegate, ReadonlyPartialJSONValue } from '@lumino/coreutils';
23+
import { ReadonlyPartialJSONValue } from '@lumino/coreutils';
2524

2625
import { INotebookModel } from '@jupyterlab/notebook';
2726

@@ -106,74 +105,8 @@ export abstract class LabWidgetManager
106105
// A "load" for a kernel that does not handle comms does nothing.
107106
return;
108107
}
109-
const comm_ids = await this._get_comm_info();
110108

111-
// For each comm id that we do not know about, create the comm, and request the state.
112-
const widgets_info = await Promise.all(
113-
Object.keys(comm_ids).map(async (comm_id) => {
114-
try {
115-
await this.get_model(comm_id);
116-
// If we successfully get the model, do no more.
117-
return;
118-
} catch (e) {
119-
// If we have the widget model not found error, then we can create the
120-
// widget. Otherwise, rethrow the error. We have to check the error
121-
// message text explicitly because the get_model function in this
122-
// class throws a generic error with this specific text.
123-
if (e.message !== 'widget model not found') {
124-
throw e;
125-
}
126-
const comm = await this._create_comm(this.comm_target_name, comm_id);
127-
128-
let msg_id = '';
129-
const info = new PromiseDelegate<Private.ICommUpdateData>();
130-
comm.on_msg((msg: KernelMessage.ICommMsgMsg) => {
131-
if (
132-
(msg.parent_header as any).msg_id === msg_id &&
133-
msg.header.msg_type === 'comm_msg' &&
134-
msg.content.data.method === 'update'
135-
) {
136-
const data = msg.content.data as any;
137-
const buffer_paths = data.buffer_paths || [];
138-
const buffers = msg.buffers || [];
139-
put_buffers(data.state, buffer_paths, buffers);
140-
info.resolve({ comm, msg });
141-
}
142-
});
143-
msg_id = comm.send(
144-
{
145-
method: 'request_state',
146-
},
147-
this.callbacks(undefined)
148-
);
149-
150-
return info.promise;
151-
}
152-
})
153-
);
154-
155-
// We put in a synchronization barrier here so that we don't have to
156-
// topologically sort the restored widgets. `new_model` synchronously
157-
// registers the widget ids before reconstructing their state
158-
// asynchronously, so promises to every widget reference should be available
159-
// by the time they are used.
160-
await Promise.all(
161-
widgets_info.map(async (widget_info) => {
162-
if (!widget_info) {
163-
return;
164-
}
165-
const content = widget_info.msg.content as any;
166-
await this.new_model(
167-
{
168-
model_name: content.data.state._model_name,
169-
model_module: content.data.state._model_module,
170-
model_module_version: content.data.state._model_module_version,
171-
comm: widget_info.comm,
172-
},
173-
content.data.state
174-
);
175-
})
176-
);
109+
return super._loadFromKernel();
177110
}
178111

179112
/**
@@ -668,13 +601,3 @@ export namespace WidgetManager {
668601
saveState: boolean;
669602
};
670603
}
671-
672-
namespace Private {
673-
/**
674-
* Data promised when a comm info request resolves.
675-
*/
676-
export interface ICommUpdateData {
677-
comm: IClassicComm;
678-
msg: KernelMessage.ICommMsgMsg;
679-
}
680-
}

0 commit comments

Comments
 (0)