Skip to content

Commit eabbbcd

Browse files
authored
Merge pull request #3335 from martinRenou/use_control_comm_target
Use control comm target in base Manager
2 parents 2a26374 + ba01c58 commit eabbbcd

File tree

3 files changed

+254
-163
lines changed

3 files changed

+254
-163
lines changed

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

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

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)