Performance for consecutive tab switching improvements
krassowski opened this issue · comments
Problem
Some actions in the UI are expensive. When user navigates open tabs they may want to switch a few tabs ahead using a keyboard shortcut. Even with relatively low latency with windowed notebooks for any single tab, switch n tabs with large notebooks in a row may take too long for the user.
Related to jupyterlab/jupyterlab#4292 and jupyterlab/benchmarks#75
Proposed Solution
We could:
- aggregate consecutive switch tab requests if they appear within a debounce period and then switch to the final tab immediately
- this means that for user who only switches one tab at a time the action will be slower (the time to switch will increase by the length of by the debounce period)
- delay the expensive operation of rendering the tab beyond the debounce period; in particular we could "fake it" by displaying a static screenshot of the tab in the state as it was when it was most recently rendered
- the screenshot of visible portion of each tab could be generated when it gets hidden (and possibly compressed in a webworker on idle)
- this implies a memory usage increase (as we are storing an extra PNG screenshot for each hidden tab)
- unfortunately making screenshots of DOM nodes is hard;
- it is easy for datagrid as it is just a canvas and we can read it off the buffer
- for non-canvas elements we basically need to render to canvas, for example:
- html2canvas - MIT license, 47 kB, 4.5k forks
- rasterizeHTML - MIT license, 14.0 kB, ~200 forks
- use
content-visibility
on tab-level to improve performance of tab switch. While previous work in jupyterlab-benchmarks repository investigated using content-visibility on cell level, this could be even more beneficial on widget level (in the same manner as #231) - render a single main area widget in a panel in an iframe and use browser navigation for instantenious switching leveraging back-forward cache (https://web.dev/bfcache/)
previously I suggested using Houdini CSS Layout API but I no longer believe that this particular API exposes enough data to implement the caching mechanism that we need
Additional comments:
- (1) I could not find any examples of applications using this approach
- (2) is a big ask for a lumino, especially if it involves bringing a new dependency. Maybe the better statement of the problem would be: lumino should allow to implement (2) in third-party extensions.
- (3) is not currently not supported by Firefox but maybe we should include it and only enable on Chrome; the support in Firefox appears to be work-in-progress with some required APIs already implemented.
- (4) is very high performance (DOM cache on browser level) but also highly disruptive and requires very careful managment of connections/requests etc
Additional context
Some browsers are also working on implementing another cache for closed tabs but this may be more difficult to leverage than back-froward cache.
Just if anyone was wondering, shadow-DOM does not help in this particular case (beyond reducing style recomputation scope) as it does not currently support any additional caching.
(3) achieves impressive results for very heavy notebooks in Chrome. You can test it by running the following cell in IPython notebook:
%%html
<style>
.jp-MainAreaWidget.lm-Widget.lm-mod-hidden {
content-visibility: hidden;
display: block!important;
z-index: -1;
}
</style>
To disable it switch the cell to raw. To re-enable switch it back to code and re-run.
Here is the comparison for tab switching obtained form ui-profiler with scenario parametrized as: [gh-9757-reproducer.ipynb
, Launcher
, all-html-elements.ipynb
], 50 repeats, JupyterLab 3.6.0b:
hidden mode | IQM [Q1 - Q3] | mean | min |
---|---|---|---|
display | 3008.8 [2967.2 – 3174.4] ms | 3086.7 ms | 2698.9 ms |
content-visibility | 547.9 [539.5 – 556.9] ms | 568.6 ms | 479.4 ms |
scale | 473.5 [467.8 – 481.2] ms | 516.9 ms | 461.7 ms |
Launchers in my setup have 32 cards.
To confirm that this is not a mistake, I re-run the benchmarks and and the results were in agreement (see details).
hidden mode | IQM [Q1 - Q3] | mean | min |
---|---|---|---|
display | 3107.7 [3020.8 – 3216.2] ms | 3145.7 ms | 2910.8 ms |
content-visibility | 616.6 [604.3 – 632.9] ms | 655.0 ms | 570.4 ms |
scale | 511.4 [499.6 – 534.5] ms | 588.7 ms | 480.4 ms |
Lightweight scenarios
Ok, so content-visibility is great for very heavy notebooks. How does it behave in lightweight (e.g. Launcher) or moderate scenarios (a small notebook)?
Previously we saw a performance drop for scale mode when many tabs are open. We can reproduce this by looking at an increasing number of launchers:
by # launchers | 10 | 20 | 30 |
---|---|---|---|
display | 425.5 [419.5 – 434.6] | 1019 [1008.3 – 1034] | 1907.2 [1883.6 – 1933.5] |
content-visibility | 386.6 [379.0 – 396.8] | 982.7 [967.2 – 994.6] | 1904.9 [1874.3 – 1935.8] |
scale | 381.0 [376.3 – 386.6] | 1038.9 [1021.9 – 1058.5] | 1912.9 [1894.9 – 1933.3] |
while scale
is faster for 10 lightweight tabs, it becomes the slowest for 20 and 30 tabs. At 30 tabs the differences are less notable.
When looking at notebooks (each with 20 code cells), content-visibility is a clean winner, irrespective of the number of tabs to traverse.
by # notebooks | 10 | 20 | 30 |
---|---|---|---|
display | 1646.0 [1581.3 – 1724.2] | 4311.4 [4180.0 – 4450.3] | 8192.9 [7927.9 – 8493.6] |
content-visibility | 1446.1 [1361.8 – 1532.3] | 4202.8 [4064.8 – 4372.9] | 7736.7 [7360.8 – 7939.1] |
scale | 1643.6 [1572.2 – 1720.4] | 4768.5 [4616.5 – 4942.2] | 9977.1 [9821.7 – 10152] |
In any case, the gains for lightweight scenarios are not as impressive.
Importantly, scale
mode is no longer default in JupyterLab because it was breaking behaviour of tabs when tabs with iframes/PDFs were open in Chromium browsers. I now verified that this does not affect content-visibility
(for scale mode I did reproduce the problem with iframes but not with PDFs, maybe it got fixed since).