bokeh: [BUG] Layout engine much slower on Chrome
When working with complex layouts the layout engine sometimes takes a considerable time to rerender because it has to measure the size of various components. For a long time I thought this was a general issue with the layout engine but it was just pointed out to me that this is much worse in Chrome.
In the contrived example below we have a reasonably complex layout of rows and columns and keep appending new rows with some text and plots.
from bokeh.layouts import Row, Column, Spacer
from bokeh.models.widgets import Div, Slider, Button
from bokeh.plotting import figure
from bokeh.io import curdoc
title = Div(text='<h1>Page title</h1>')
header = Row(children=[title, Spacer(sizing_mode='stretch_width'),
Div(text="Some link")],
height=60, sizing_mode="stretch_width")
main = Column(sizing_mode='stretch_width')
def on_click(event):
p = figure(sizing_mode='stretch_width')
p.line([1, 2, 3], [1, 2, 3])
row = Row(children=[Div(text="Some text") for i in range(10)]+[p],
sizing_mode='stretch_width')
main.children.append(row)
widgets = [Slider(start=0, end=10, title='Slider %d' % i) for i in range(10)]
button = Button(label='Click me')
button.on_click(on_click)
widgets.append(button)
nav = Column(children=widgets, width=200)
layout = Column(children=[header, Row(children=[nav, main])])
curdoc().add_root(layout)
The layout engine struggles the more items you add with the button presses in Chrome, while in Firefox it hardly slows down. Here are flame graphs generated by recording the performance in the two browsers.
Chrome
Firefox
You will note that it takes <2 seconds in Firefox and ~5 in Chrome. I’m not sure if this is just a Chrome performance issue but it would at least be good to investigate why it’s so much slower and if anything can be done to fix it.
Versions:
Bokeh 1.4.0 Chrome Version 78.0.3904.108 (Official Build) (64-bit) Firefox 70.0.1 (64-bit)
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 3
- Comments: 27 (26 by maintainers)
Hello don’t know if it helps but there it is performance on windows 10 chrome Version 78.0.3904.108 (Build officiel) (64 bits):
Same test on firefox 60.0.1 (64-bit):
Howver the difference seems mitigate when not using the debugger I made a rough test on 20 clicks an looking the scroll bar updating. It took me 27 seconds on chrome vs 23 seconds on firefox
I’m glad to hear of your successes @MarcSkovMadsen if you have some bandwidth in the next few months it would be really valuable to try and distill some small purposeful examples and documentation sections that could benefit all users.
I wanted to relay some findings and mitigation strategies from #9529. I’ve got a complex app that was composed of 4 logical “panels” that were arranged vertically as rows in a master layout and added to the document as a single call to
doc.add_root.Selecting a row in a DataTable would cause several plots to update. This resulted in 4 calls to
compute_layout, each taking about 7.5 seconds for a total run time of 28-32 seconds during which the browser was frozen. The chrome profiling output can be found here.Based on advice from @bryevdv I decomposed the main layout to into 7 columns and added each of these to the document as independent calls to
doc.add_root. By doing so it reduced calls tocompute_layoutfrom 4 to 2, and those calls now take 1.5 and 1.2 seconds respectively. The chrome profiling output after the layout changes can be found here.It’s not a solution to slow layout computation, but it may provide a mitigation strategy for others.
We need to start from defining the terms, which I’ve been using incorrectly myself. Layout can be either (re)computed or invalidated. There are other forms like resize, but they boil down to those two. The former doesn’t instantiate layouts, whereas the later does. So pretty much everywhere I mentioned layout invalidation, I actually meant recomputation. It doesn’t help that e.g. plot canvas uses those terms interchangeably.
This way your approach is correct, assuming no images, etc., as previously mentioned. Layout is invalidated when layout structure or properties change (on models), or when root element’s parent HTML element’s CSS display changes. Currently layout invalidation is clearly overzealous, but that can be improved on case-by-case basis, if needed. If invalidation happens, it will naturally result in cache invalidation, which is fine when e.g. changing HTML content of a markup widget. It’s also suboptimal if it happens due to unrelated changes in the layout. Currently layout is updated naively (destroy the world approach), but that once again can be improved, e.g. by just applying a diff.
@mattpap I’ve been digging into the measurement code a bit to try to figure out if there are any easy gains to be made and I noticed something strange, it seems that for each call to
Layoutable.measuretheLayoutable._measurecall is run exactly 4 times. In every scenario I’ve seen so far each of those calls returns exactly the same width and height values which makes it seem quite redundant.Is this on purpose? I can’t really see where that happens in the code but if I cache the
Sizableon theLayoutableon the first call toLayoutable._measureand return that on the subsequent three calls everything stays highly responsive and as far as I can tell the layout is no different. That said you clearly do have to invalidate the cache in certain cases, e.g. when the viewport size changes. It’s not even just a 4x speedup, it’s in excess of 10x (maybe because of GC).OK For me (and probably @mattpap) to look into this we really need a pure Bokeh reproducer. I’ve tried to approximate the structure of this how I would in Bokeh. Does this run OK for you? (It does for me on Chrome and Safari) If so, what to do we need to do to it it get it closer to what Panel does? @philippjfr