Skip to content

Skip pipe.send for hidden tabs to reduce session update cycle time #689

@SimonHeybrock

Description

@SimonHeybrock

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.md and .scratch/pipe_send_prototype.py.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions