Skip to content

Commit 7613271

Browse files
committed
widgets: improve implementation of garbage collection to handle case of references *to* things in dom, e.g., jsdlink
1 parent 67984cb commit 7613271

File tree

2 files changed

+46
-17
lines changed

2 files changed

+46
-17
lines changed

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,6 @@ export class WidgetManager {
6565
}
6666

6767
private initAllModels = async () => {
68-
// TODO: this can do a lot that makes no sense
69-
// due to lack of garbage collection and any link to what is actually
70-
// in the notebook.
7168
/*
7269
With this disabled, this breaks for RTC or close/open:
7370
@@ -76,7 +73,6 @@ s1 = IntSlider(max=200, value=100); s2 = IntSlider(value=40)
7673
jsdlink((s1, 'value'), (s2, 'max'))
7774
VBox([s1, s2])
7875
*/
79-
// log("initAllModels: temporarily disabled"); return;
8076

8177
if (this.ipywidgets_state.get_state() == "init") {
8278
await once(this.ipywidgets_state, "ready");

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

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,18 @@ type Value = { [key: string]: any };
3434
// backend project, and not by frontend browser clients.
3535
// The garbage collection is deleting models and related
3636
// data when they are not referenced in the notebook.
37+
// Also, we don't implement complete object delete yet so instead we
38+
// set the data field to null, which clears all state about and
39+
// object and makes it easy to know to ignore it.
3740
const GC_DEBOUNCE_MS = 10000;
3841

39-
// actually doing GC is bonus points; I don't think upstream even does it,
40-
// and it seems very hard to get right without causing subtle bugs. We disable
41-
// it for now. I think this core example breaks it, because the link isn't
42-
// taken into account properly:
43-
// from ipywidgets import VBox, jsdlink, IntSlider, Button
44-
// s1 = IntSlider(max=200, value=100); s2 = IntSlider(value=40)
45-
// jsdlink((s1, 'value'), (s2, 'max'))
46-
// VBox([s1, s2])
47-
const DISABLE_GC = true;
42+
// If for some reason GC needs to be deleted, e.g., maybe you
43+
// suspect a bug, just toggle this flag. In particular, note
44+
// includeThirdPartyReferences below that has to deal with a special
45+
// case schema that k3d uses for references, which they just made up,
46+
// which works with official upstream, since that has no garbage
47+
// collection.
48+
const DISABLE_GC = false;
4849

4950
interface CommMessage {
5051
header: { msg_id: string };
@@ -168,7 +169,9 @@ export class IpywidgetsState extends EventEmitter {
168169

169170
// assembles together state we know about the widget with given model_id
170171
// from info in the table, and returns it as a Javascript object.
171-
getSerializedModelState = (model_id: string): SerializedModelState | undefined => {
172+
getSerializedModelState = (
173+
model_id: string,
174+
): SerializedModelState | undefined => {
172175
this.assert_state("ready");
173176
const state = this.get(model_id, "state");
174177
if (state == null) {
@@ -379,7 +382,7 @@ export class IpywidgetsState extends EventEmitter {
379382
// new ones come or current ones change. With shallow merge,
380383
// the existing ones go away, which is very broken, e.g.,
381384
// see this with this example:
382-
/*
385+
/*
383386
import bqplot.pyplot as plt
384387
import numpy as np
385388
x, y = np.random.rand(2, 10)
@@ -466,7 +469,10 @@ scat.x, scat.y = np.random.rand(2, 50)
466469
// which is why we just set the data to null.
467470
const activeIds = this.getActiveModelIds();
468471
this.table.get()?.forEach((val, key) => {
469-
if (key == null || val == null || val.get("data") == null) return; // already deleted
472+
if (key == null || val?.get("data") == null) {
473+
// already deleted
474+
return;
475+
}
470476
const [string_id, model_id, type] = JSON.parse(key);
471477
if (!activeIds.has(model_id)) {
472478
this.table.set(
@@ -501,9 +507,12 @@ scat.x, scat.y = np.random.rand(2, 50)
501507
}
502508
after = modelIds.size;
503509
}
504-
// Also any custom ways of doing referencing...
510+
// Also any custom ways of doing referencing -- e.g., k3d does this.
505511
this.includeThirdPartyReferences(modelIds);
506512

513+
// Also anything that references any modelIds
514+
this.includeReferenceTo(modelIds);
515+
507516
return modelIds;
508517
};
509518

@@ -534,6 +543,30 @@ scat.x, scat.y = np.random.rand(2, 50)
534543
return this.getReferencedModelIds(modelIds);
535544
};
536545

546+
private includeReferenceTo = (modelIds: Set<string>) => {
547+
// This example is extra tricky and one version of our GC broke it:
548+
// from ipywidgets import VBox, jsdlink, IntSlider, Button; s1 = IntSlider(max=200, value=100); s2 = IntSlider(value=40); jsdlink((s1, 'value'), (s2, 'max')); VBox([s1, s2])
549+
// What happens here is that this jsdlink model ends up referencing live widgets,
550+
// but is not referenced by any cell, so it would get garbage collected.
551+
552+
let before = -1;
553+
let after = modelIds.size;
554+
while (before < after) {
555+
before = modelIds.size;
556+
this.table.get()?.forEach((val) => {
557+
const data = val?.get("data");
558+
if (data != null) {
559+
for (const model_id of getModelIds(data)) {
560+
if (modelIds.has(model_id)) {
561+
modelIds.add(val.get("model_id"));
562+
}
563+
}
564+
}
565+
});
566+
after = modelIds.size;
567+
}
568+
};
569+
537570
private includeThirdPartyReferences = (modelIds: Set<string>) => {
538571
/*
539572
Motivation (RANT):

0 commit comments

Comments
 (0)