Description
Removing or replacing a child of a rendered GridSpec never calls the child's _cleanup(root). For panes with document/stream hooks this leaks live objects: a pn.pane.HoloViews wrapping a stream-driven DynamicMap keeps its rendered plot subscribed to the stream, so each slot replacement permanently adds one more plot doing full render work on every stream event.
Cause: GridSpec.__setitem__/__delitem__ mutate self.objects in place and then self.param.trigger('objects'). The removed-child cleanup in GridSpec._get_objects diffs old_objects against the current objects — but after an in-place mutation the event's old value is the same dict, so the diff is always empty and no removed child is ever cleaned. Other layouts reassign objects and are unaffected (control below). Present in 1.9.3; the mutation pattern is unchanged on main.
Complete, minimal, self-contained example
import holoviews as hv, panel as pn
hv.extension('bokeh')
pipe = hv.streams.Pipe(data=hv.Curve([1, 2, 3]))
dmap = hv.DynamicMap(lambda data: hv.Curve([]), streams=[pipe])
grid = pn.GridSpec()
grid[0, 0] = pn.pane.HoloViews(dmap)
root = grid.get_root()
print(len(pipe.subscribers)) # 1
del grid[0, 0]
print(len(pipe.subscribers)) # 1 <- removed pane was never cleaned up
grid[0, 0] = pn.pane.HoloViews(dmap)
print(len(pipe.subscribers)) # 2 <- one leaked live plot per replacement
Control showing Column cleans up correctly (only GridSpec is affected):
pipe2 = hv.streams.Pipe(data=hv.Curve([1, 2, 3]))
dmap2 = hv.DynamicMap(lambda data: hv.Curve([]), streams=[pipe2])
col = pn.Column(pn.pane.HoloViews(dmap2))
root2 = col.get_root()
print(len(pipe2.subscribers)) # 1
col[:] = []
print(len(pipe2.subscribers)) # 0
Versions: panel 1.9.3, bokeh 3.9.2, holoviews 1.23.1, Python 3.12, Linux.
Found while chasing a production leak (scipp/esslivedata#1224): each rebuild of a grid cell hosting a live plot permanently doubled the render work for that cell's streams.
Related: holoviz/holoviews#6988 — Plot.cleanup() currently severs all weakly-wrapped subscribers on shared streams, which matters for how a fix orders cleanup vs. rendering of a replacement child.
Description
Removing or replacing a child of a rendered
GridSpecnever calls the child's_cleanup(root). For panes with document/stream hooks this leaks live objects: apn.pane.HoloViewswrapping a stream-drivenDynamicMapkeeps its rendered plot subscribed to the stream, so each slot replacement permanently adds one more plot doing full render work on every stream event.Cause:
GridSpec.__setitem__/__delitem__mutateself.objectsin place and thenself.param.trigger('objects'). The removed-child cleanup inGridSpec._get_objectsdiffsold_objectsagainst the current objects — but after an in-place mutation the event's old value is the same dict, so the diff is always empty and no removed child is ever cleaned. Other layouts reassignobjectsand are unaffected (control below). Present in 1.9.3; the mutation pattern is unchanged onmain.Complete, minimal, self-contained example
Control showing
Columncleans up correctly (onlyGridSpecis affected):Versions: panel 1.9.3, bokeh 3.9.2, holoviews 1.23.1, Python 3.12, Linux.
Found while chasing a production leak (scipp/esslivedata#1224): each rebuild of a grid cell hosting a live plot permanently doubled the render work for that cell's streams.
Related: holoviz/holoviews#6988 —
Plot.cleanup()currently severs all weakly-wrapped subscribers on shared streams, which matters for how a fix orders cleanup vs. rendering of a replacement child.