#1805·visdom

/update becomes progressively slower as line plots grow — appending points is O(n²)

Author: tarone-saloniCreated Sep 7, 2026Updated Sep 7, 2026

Bug Description

I noticed that /update gets progressively slower as a line plot gets bigger.

I ran into this while profiling a training run that logs one scalar per step. The updates start out fast, but as more points are added, each update takes longer.

Appending one point at a time to a single line plot:

500 appends ->  0.185s  (0.37 ms/update)
1000 appends ->  0.685s  (0.68 ms/update)
2000 appends ->  2.687s  (1.34 ms/update)
4000 appends ->  9.965s  (2.49 ms/update)

The per-update cost roughly doubles when the number of points doubles. At around 20k points, an append takes about 12 ms, which adds up over a long training run.

Profiling

The biggest cost seems to come from this line in web_handlers.py:498:

if len(stringify(p)) <= len(stringify(diff_packet)):

stringify(p) serializes the entire pane just to compare its size with the generated patch.

For a pane containing around 4000 points, I measured:

stringify(p)                          3.075 ms
deepcopy(p["content"])    1.151 ms
jsonpatch.make_patch      0.308 ms
stringify(diff_packet)         0.011 ms

The full pane is around 50 KB, while the patch is only around 165 bytes:

pane -> ~50,093 bytes 
patch -> ~165 bytes

So for an append like this, the full pane is being serialized even though the patch is much smaller. The resulting string is then discarded.

stringify() also goes through recursive_order, which walks the entire structure, rebuilds dictionaries as sorted OrderedDicts, and copies lists. In my profiling, this size check accounts for roughly 67% of the runtime at 4000 appends.

There is also overhead from the deepcopy and JSON diff in web_handlers.py:136 and web_handlers.py:150.

For every append, the current flow is roughly:

whole pane
    ↓
deepcopy
    ↓
append one point
    ↓
diff the whole pane
    ↓
generate patch

For an append operation, we're therefore processing the whole pane to generate a patch that only contains the newly added point.

For an append operation, we're therefore processing the whole pane to generate a patch that only contains the newly added point.

Fix

  • The proper fix would be to handle append operations directly instead of reconstructing the patch by copying and diffing the entire pane.
  • Since the update already represents an append, the server could generate the corresponding JSON Patch directly for the new point.
  • That would make the work proportional to the size of the update rather than the size of the existing pane.

IOLoop impact

  • There may also be an impact on server responsiveness.
  • UpdateHandler.post awaits ensure_env_loaded and then calls wrap_func, so the serialization, deepcopy, and diff work happens synchronously in the update path.
  • I wasn't able to measure the end-to-end IOLoop blocking because loopback networking was blocked in my sandbox. This part is based on tracing the current call path rather than a direct benchmark.
  • With a large plot, a single expensive update could therefore potentially delay other requests handled by the same IOLoop.

Testing

The benchmarks were collected by calling UpdateHandler.update_packet directly in-process.

pytest -m unit passes.