/update becomes progressively slower as line plots grow — appending points is O(n²)
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 msThe full pane is around 50 KB, while the patch is only around 165 bytes:
pane -> ~50,093 bytes
patch -> ~165 bytesSo 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 patchFor 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.
Source: fossasia/visdom