Skip to content

Commit 3cc6adb

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

File tree

3 files changed

+250
-163
lines changed

3 files changed

+250
-163
lines changed

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

Lines changed: 221 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,21 @@ 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+
49+
/**
50+
* Time (in ms) after which we consider the control comm target not responding.
51+
*/
52+
export const CONTROL_COMM_TIMEOUT = 4000;
53+
3554
/**
3655
* Sanitize HTML-formatted descriptions.
3756
*/
@@ -342,6 +361,197 @@ export abstract class ManagerBase implements IWidgetManager {
342361
return await modelPromise;
343362
}
344363

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

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)