Skip to content

Commit 191336a

Browse files
committed
ipywidgets: thinking through sending buffers from client to kernel
1 parent cb8d9f8 commit 191336a

File tree

4 files changed

+71
-56
lines changed

4 files changed

+71
-56
lines changed

src/packages/frontend/jupyter/output-messages/ipywidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ interface WidgetProps {
2929
}
3030

3131
export function IpyWidget({ value, actions }: WidgetProps) {
32-
console.log("IpyWidget", { value: value.toJS(), actions });
32+
// console.log("IpyWidget", { value: value.toJS(), actions });
3333
const [unknown, setUnknown] = useState<boolean>(false);
3434
const divRef = useRef<any>(null);
3535

src/packages/frontend/jupyter/widgets/manager2.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -139,26 +139,21 @@ export class WidgetManager {
139139
model.set_state(state);
140140
};
141141

142+
// ipywidgets_state_ValueChange is called when a value entry of the ipywidgets_state
143+
// table is changed, e.g., when the backend decides a model should change or another client
144+
// changes something, or even this client changes something. When another client is
145+
// responsible for the change, we make the change to the ipywidgets model here.
142146
private ipywidgets_state_ValueChange = async (model_id: string) => {
143147
// log("handleValueChange: ", model_id);
144148
const changed = this.ipywidgets_state.get_model_value(model_id);
145149
log("handleValueChange: changed=", model_id, changed);
146-
for (const k in changed) {
147-
if (typeof changed[k] == "string" && changed[k].startsWith("IPY_MODEL")) {
148-
delete changed[k];
149-
} else if (is_array(changed[k]) && typeof changed[k][0] == "string") {
150-
if (changed[k][0]?.startsWith("IPY_MODEL")) {
151-
delete changed[k];
152-
}
153-
}
154-
}
155150
if (
156151
this.last_changed[model_id] != null &&
157152
changed.last_changed != null &&
158153
changed.last_changed <= this.last_changed[model_id].last_changed
159154
) {
160155
log(
161-
"handleValueChange: skipping due to last change time",
156+
"handleValueChange: skipping due to last_changed sequence number -- i.e., change caused by this client",
162157
this.last_changed[model_id],
163158
changed.last_changed,
164159
);
@@ -273,10 +268,15 @@ export class WidgetManager {
273268
const model = await this.manager.get_model(model_id);
274269
model.on("change", this.handleModelChange);
275270
this.watching.add(model_id);
271+
this.last_changed[model_id] = { last_changed: 0 };
276272
};
277273

274+
// handleModelChange is called when an ipywidgets model changes.
275+
// This function serializes the change and saves it to
276+
// ipywidgets_state, so that it is gets sync'd to the backend
277+
// and any other clients.
278278
private handleModelChange = async (model): Promise<void> => {
279-
// log("handleModelChange", model);
279+
log("handleModelChange", model);
280280
const { model_id } = model;
281281
await model.state_change;
282282
if (this.state_lock.has(model_id)) {
@@ -292,21 +292,18 @@ export class WidgetManager {
292292
}
293293
// increment sequence number.
294294
changed.last_changed =
295-
Math.max(
296-
last_changed ?? 0,
297-
this.last_changed[model_id]?.last_changed ?? 0,
298-
) + 1;
295+
Math.max(last_changed ?? 0, this.last_changed[model_id].last_changed) + 1;
299296
this.last_changed[model_id] = changed;
300-
// log("handleModelChange", changed);
301-
this.ipywidgets_state.set_model_value(model_id, changed, true);
297+
log("handleModelChange", changed);
298+
this.ipywidgets_state.set_model_value(model_id, changed, false);
302299
this.ipywidgets_state.save();
303300
};
304301

305302
private deserializeState = async (
306303
model: base.DOMWidgetModel,
307304
serialized_state: ModelState,
308305
): Promise<ModelState> => {
309-
log("deserializeState", { model, serialized_state });
306+
// log("deserializeState", { model, serialized_state });
310307
// NOTE: this is a reimplementation of soemething in
311308
// ipywidgets/packages/base/src/widget.ts
312309
// but we untagle unpacking and deserializing, which is
@@ -386,7 +383,7 @@ export class WidgetManager {
386383
};
387384

388385
private dereferenceModelLinks = async (state): Promise<boolean> => {
389-
log("dereferenceModelLinks", "BEFORE", state);
386+
// log("dereferenceModelLinks", "BEFORE", state);
390387
for (const key in state) {
391388
const val = state[key];
392389
if (typeof val === "string") {
@@ -430,7 +427,7 @@ export class WidgetManager {
430427
}
431428
}
432429
}
433-
log("dereferenceModelLinks", "AFTER (success)", state);
430+
// log("dereferenceModelLinks", "AFTER (success)", state);
434431
return true;
435432
};
436433
}
@@ -442,7 +439,7 @@ class Environment implements WidgetEnvironment {
442439
}
443440

444441
async getModelState(model_id) {
445-
log("getModelState", model_id);
442+
// log("getModelState", model_id);
446443
if (this.manager.ipywidgets_state.get_state() != "ready") {
447444
await once(this.manager.ipywidgets_state, "ready");
448445
}

src/packages/jupyter/redux/project-actions.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,10 @@ export class JupyterActions extends JupyterActions0 {
13121312
});
13131313
}
13141314

1315+
// handle_ipywidgets_state_change is called when the project ipywidgets_state
1316+
// object changes, e.g., in response to a user moving a slider in the browser.
1317+
// It crafts a comm message that is sent to the running Jupyter kernel telling
1318+
// it about this change by calling send_comm_message_to_kernel.
13151319
private handle_ipywidgets_state_change(keys): void {
13161320
if (this.is_closed()) {
13171321
return;
@@ -1338,7 +1342,20 @@ export class JupyterActions extends JupyterActions0 {
13381342
data,
13391343
);
13401344
} else if (type === "buffers") {
1341-
// nothing to do on the backend (?)
1345+
// TODO: we will implement this soon. A good example where this is required
1346+
// is by the file upload widget:
1347+
// https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#file-upload
1348+
// which creates a buffer from the content of the file, then sends it to the backend,
1349+
// which sees a change and has to write that buffer to the kernel (here) so that
1350+
// the running python process can actually do something with the file contents (e.g.,
1351+
// process data, save file to disk, etc).
1352+
// We need to be careful though to not send buffers to the kernel that the kernel sent us,
1353+
// since that would be a waste.
1354+
// TODO
1355+
// I think the format for the send_comm_mmessage_to_kernel call here is easy to deduce
1356+
// from what process_comm_message_from_kernel unpacks.
1357+
// ***
1358+
// TODO
13421359
} else if (type === "state") {
13431360
// TODO: currently ignoring this, since it seems chatty and pointless,
13441361
// and could lead to race conditions probably with multiple users, etc.

src/packages/sync/editor/generic/ipywidgets-state.ts

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export class IpywidgetsState extends EventEmitter {
281281

282282
private set_model_buffers = (
283283
model_id: string,
284-
buffer_paths: string[],
284+
buffer_paths: string[][],
285285
buffers: Buffer[],
286286
fire_change_event: boolean = true,
287287
): void => {
@@ -294,7 +294,7 @@ export class IpywidgetsState extends EventEmitter {
294294
}
295295
for (let i = 0; i < buffer_paths.length; i++) {
296296
const key = JSON.stringify(buffer_paths[i]);
297-
// we set to the sha1 of the buffer not to make getting
297+
// we set to the sha1 of the buffer not just to make getting
298298
// the buffer easy, but to make it easy to KNOW if we
299299
// even need to get the buffer.
300300
const hash = sha1(buffers[i]);
@@ -388,14 +388,15 @@ export class IpywidgetsState extends EventEmitter {
388388
this.set_state("closed");
389389
};
390390

391-
private dbg(_f): Function {
391+
private dbg = (_f): Function => {
392392
if (this.client.is_project()) {
393393
return this.client.dbg(`IpywidgetsState.${_f}`);
394394
} else {
395395
return (..._) => {};
396396
}
397-
}
398-
public async clear(): Promise<void> {
397+
};
398+
399+
clear = async (): Promise<void> => {
399400
// This is used when we restart the kernel -- we reset
400401
// things so no information about any models is known
401402
// and delete all Buffers.
@@ -417,13 +418,13 @@ export class IpywidgetsState extends EventEmitter {
417418
this.table.set({ string_id, type, model_id, data: null }, "none", false);
418419
}
419420
await this.table.save();
420-
}
421+
};
421422

422423
// Clean up all data in the table about models that are not
423424
// referenced (directly or indirectly) in any cell in the notebook.
424425
// There is also a comm:close event/message somewhere, which
425426
// could also be useful....?
426-
public async deleteUnused(): Promise<void> {
427+
deleteUnused = async (): Promise<void> => {
427428
this.assert_state("ready");
428429
const dbg = this.dbg("deleteUnused");
429430
dbg();
@@ -442,13 +443,13 @@ export class IpywidgetsState extends EventEmitter {
442443
}
443444
});
444445
await this.table.save();
445-
}
446+
};
446447

447448
// For each model in init, we add in all the ids of models
448449
// that it explicitly references, e.g., by IPY_MODEL_[model_id] fields
449450
// and by output messages and other things we learn about (e.g., k3d
450451
// has its own custom references).
451-
public getReferencedModelIds(init: string | Set<string>): Set<string> {
452+
getReferencedModelIds = (init: string | Set<string>): Set<string> => {
452453
const modelIds =
453454
typeof init == "string" ? new Set([init]) : new Set<string>(init);
454455
let before = 0;
@@ -470,14 +471,14 @@ export class IpywidgetsState extends EventEmitter {
470471
this.includeThirdPartyReferences(modelIds);
471472

472473
return modelIds;
473-
}
474+
};
474475

475476
// We find the ids of all models that are explicitly referenced
476477
// in the current version of the Jupyter notebook by iterating through
477478
// the output of all cells, then expanding the result to everything
478479
// that these models reference. This is used as a foundation for
479480
// garbage collection.
480-
private getActiveModelIds(): Set<string> {
481+
private getActiveModelIds = (): Set<string> => {
481482
const modelIds: Set<string> = new Set();
482483
this.syncdoc.get({ type: "cell" }).forEach((cell) => {
483484
const output = cell.get("output");
@@ -497,9 +498,9 @@ export class IpywidgetsState extends EventEmitter {
497498
}
498499
});
499500
return this.getReferencedModelIds(modelIds);
500-
}
501+
};
501502

502-
private includeThirdPartyReferences(modelIds: Set<string>) {
503+
private includeThirdPartyReferences = (modelIds: Set<string>) => {
503504
/*
504505
Motivation (RANT):
505506
It seems to me that third party widgets can just invent their own
@@ -545,33 +546,33 @@ export class IpywidgetsState extends EventEmitter {
545546
modelIds.add(model_id);
546547
}
547548
});
548-
}
549+
};
549550

550551
// The finite state machine state, e.g., 'init' --> 'ready' --> 'close'
551-
private set_state(state: State): void {
552+
private set_state = (state: State): void => {
552553
this.state = state;
553554
this.emit(state);
554-
}
555+
};
555556

556-
public get_state(): State {
557+
get_state = (): State => {
557558
return this.state;
558-
}
559+
};
559560

560-
private assert_state(state: string): void {
561+
private assert_state = (state: string): void => {
561562
if (this.state != state) {
562563
throw Error(`state must be "${state}" but it is "${this.state}"`);
563564
}
564-
}
565+
};
565566

566567
/*
567568
process_comm_message_from_kernel gets called whenever the
568569
kernel emits a comm message related to widgets. This updates
569570
the state of the table, which results in frontends creating widgets
570571
or updating state of widgets.
571572
*/
572-
public async process_comm_message_from_kernel(
573+
process_comm_message_from_kernel = async (
573574
msg: CommMessage,
574-
): Promise<void> {
575+
): Promise<void> => {
575576
const dbg = this.dbg("process_comm_message_from_kernel");
576577
// WARNING: serializing any msg could cause huge server load, e.g., it could contain
577578
// a 20MB buffer in it.
@@ -697,7 +698,7 @@ export class IpywidgetsState extends EventEmitter {
697698
}
698699

699700
await this.save();
700-
}
701+
};
701702

702703
/*
703704
process_comm_message_from_browser gets called whenever a
@@ -707,20 +708,20 @@ export class IpywidgetsState extends EventEmitter {
707708
kernel changing the value of variables (and possibly
708709
updating other widgets).
709710
*/
710-
public async process_comm_message_from_browser(
711+
process_comm_message_from_browser = async (
711712
msg: CommMessage,
712-
): Promise<void> {
713+
): Promise<void> => {
713714
const dbg = this.dbg("process_comm_message_from_browser");
714715
dbg(msg);
715716
this.assert_state("ready");
716717
// TODO: not implemented!
717-
}
718+
};
718719

719720
// The mesg here is exactly what came over the IOPUB channel
720721
// from the kernel.
721722

722723
// TODO: deal with buffers
723-
public capture_output_message(mesg: any): boolean {
724+
capture_output_message = (mesg: any): boolean => {
724725
const msg_id = mesg.parent_header.msg_id;
725726
if (this.capture_output[msg_id] == null) {
726727
return false;
@@ -759,13 +760,13 @@ export class IpywidgetsState extends EventEmitter {
759760
outputs.push(mesg.content);
760761
this.set_model_value(model_id, { outputs });
761762
return true;
762-
}
763+
};
763764

764-
private async sendCustomMessage(
765+
private sendCustomMessage = async (
765766
model_id: string,
766767
message: object,
767768
fire_change_event: boolean = true,
768-
): Promise<void> {
769+
): Promise<void> => {
769770
/*
770771
Send a custom message.
771772
@@ -782,11 +783,11 @@ export class IpywidgetsState extends EventEmitter {
782783
// Actually, delete is not implemented for synctable, so for
783784
// now we just set it to an empty message.
784785
this.set(model_id, "message", {}, fire_change_event);
785-
}
786+
};
786787

787-
public get_message(model_id: string) {
788+
get_message = (model_id: string) => {
788789
return this.get(model_id, "message")?.toJS();
789-
}
790+
};
790791
}
791792

792793
// Get model id's that appear either as serialized references

0 commit comments

Comments
 (0)