-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Context
Profiling the session update cycle on DREAM (4 grid tabs, ~12 active layers) shows a 7-second cycle, with 88% of the time (6.2s) spent inside Pipe.send — the HoloViews-to-Bokeh rendering pipeline. This runs for every layer on every tab, even though pn.Tabs(dynamic=True) means only the active tab is visible to the user.
Profile breakdown (run K8zHxgbd)
| Component | Time | % |
|---|---|---|
Pipe.send (all layers) |
6.19s | 88% |
get_grid deepcopy |
0.32s | 5% |
pn.io.hold() exit (flush) |
0.23s | 3% |
_create_cell_widget |
0.22s | 3% |
Proposed optimization
In _poll_for_plot_updates, check self._tabs.active to determine which grid tab is visible. Only call session_layer.update_pipe() for layers on the active grid tab. For layers on hidden tabs, the dirty flag stays set naturally (we skip calling update_pipe()). When the user switches tabs, the next poll cycle catches up by sending the latest state.
Benchmark results
Standalone prototype with pn.Tabs(dynamic=True), 4 plots per tab (2 images 200x200 + 2 curves), 20 measured cycles per mode:
mean | median | min | max (ms)
4 tabs (16 plots)
All tabs: 127 | 127 | 101 | 179
Visible only: 92 | 78 | 75 | 128
Speedup: 1.4x
8 tabs (32 plots)
All tabs: 177 | 182 | 137 | 254
Visible only: 103 | 94 | 76 | 170
Speedup: 1.7x
16 tabs (64 plots)
All tabs: 274 | 264 | 207 | 495
Visible only: 90 | 81 | 78 | 130
Speedup: 3.0x
- "Visible only" stays flat at ~80ms median regardless of total tab count.
- "All tabs" scales linearly with total plot count.
- Speedup is proportional to the hidden/total ratio — more tabs = bigger win.
We also tested dynamic=False + visible-only sends, which performed the same as dynamic=True + all tabs (~182ms for 4 tabs). With dynamic=False, all Bokeh models are in the document, so the sync cost negates the savings from fewer pipe.send calls. dynamic=True is necessary for this optimization to work.
Additional smaller optimization
_poll_for_plot_updates calls PlotOrchestrator.get_grid() every cycle, which does a copy.deepcopy() of the grid config (0.32s). The poll only reads the config; it never mutates it. A read-only accessor would eliminate this cost.
Notes
- The first render after switching tabs may show data from the previous cycle, but the next poll (~100ms later) brings it current. This is imperceptible.
- Detailed plan and benchmark prototype are in
docs/developer/plans/session-update-cycle-optimization.mdand.scratch/pipe_send_prototype.py.