Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Typing in notebooks is laggy #1977

Merged
merged 8 commits into from
May 2, 2024
Merged

Conversation

mofojed
Copy link
Member

@mofojed mofojed commented May 1, 2024

  • We were checking the diff of the layouts on each keystroke
  • DashboardLayout should debounce checking that diff
  • Tested by displaying FPS counter, having a Code Studio with a bunch of tables in it, adding a notebook, and then typing in it. FPS is much higher after the change

- We were checking the diff of the layouts on each keystroke
- DashboardLayout should debounce checking that diff
- Tested by displaying FPS counter, having a Code Studio with a bunch of tables in it, adding a notebook, and then typing in it. FPS is much higher after the change
@mofojed mofojed requested a review from mattrunyon May 1, 2024 14:56
@mofojed mofojed self-assigned this May 1, 2024
@mattrunyon
Copy link
Collaborator

Does the layout contain the entire session or something in DHE? While it should be devounced, it still seemed like it was taking way too long on the deep equals. Are we deep comparing something huge?

@mofojed
Copy link
Member Author

mofojed commented May 1, 2024

@mattrunyon this is slow in DHC as well, not just DHE.
Either way this should be debounced, but it does look like there is some extra junk in the dehydrated dashboard state:

  1. It looks like itemIds in ConsolePanel are full of objects that no longer exist/have been closed - we should be removing those from the state when they are closed. I'll file a separate ticket for this:
image 2. IrisGridPanel exports state that it doesn't necessarily need to, we could use defaults for all this empty stuff. A little more concern about breaking something if changing this: image

I'll file tickets for those two, but this should be debounced still. As the dashboard grows, doing a comparison of the layouts is going to take longer and longer, we can't be doing it every keystroke/every state update.

@mofojed
Copy link
Member Author

mofojed commented May 1, 2024

And actually, we may want to throttle instead of debounce... otherwise if there's a panel that emits events more often than the debounce period, the dashboard will never save...

- Otherwise it's possible for a dashboard to never save if it is constantly updating
@mofojed
Copy link
Member Author

mofojed commented May 1, 2024

That isEqual takes a significant amount of time though:

XXX toConfig time: 0.02392578125 ms
11:30:02.025 DashboardLayout.tsx:216 XXX dehydrate time: 0.017822265625 ms
11:30:02.172 DashboardLayout.tsx:222 XXX isEqual time: 147.446044921875 ms

@dsmmcken
Copy link
Contributor

dsmmcken commented May 1, 2024

This seems like the perfect place to use a requestIdleCallback

@mofojed
Copy link
Member Author

mofojed commented May 1, 2024

For reference, here's the JSON string of the layout that's being checked:

[{"type":"stack","isClosable":true,"isFocusOnShow":true,"reorderEnabled":true,"title":"","width":100,"activeItemIndex":4,"id":"mPsU29Z4yG","height":100,"content":[{"type":"react-component","component":"ConsolePanel","title":"Console","isClosable":false,"id":"r5iwbx0Xk","props":{"metadata":{},"panelState":{"consoleSettings":{"isAutoLaunchPanelsEnabled":true,"isPrintStdOutEnabled":true,"isClosePanelsOnDisconnectEnabled":false},"itemIds":[["simple_ticking","mlgltE2qk"],["complex_ticking","w6b4nP3e22"],["random_values_with_null_NaN","m5HNqP30Jt"],["random_values_source","ccee1ZDDyf"],["data_xy","MXWWnpOfP"],["data_cat","nTqyzFiJwx"],["data_cat_hist","X9ex4wiZTR"],["data_pie","tdP2HdE2XT"],["plot_pie","VtU5jGc6Ax"],["t","ZIkTwiJPX"],["p","Xr3eqZVpdc"],["p1","IZvVnCP2Z"],["p2","HarlvUULaS"],["t_pb","X4fDhNeAx"],["static_source_1","69kco859D"],["static_source_2","ZlWoiE46OH"],["table_3_keys","KFRA7CbJ6"],["p_pb","qrqMlQKWc"],["rows","4N7-geJ5q"],["changes","DoDM3gmnv"],["tree2","YEgJJIt91Y"],["rollup2","Ubi-9Z_Vu5"],["rollupA","EOAIwj3Xc9"],["h","fzWu57uWy"],["source","FJHwX1ZD0"],["result_no_orphans","Txp9VcesUb"],["result_w_orphans","JRuVbZ9ijO"],["fig","BmDOOfYeJ"],["reproducer","N-qNJ9clb"],["plot_single","an6YQIZme"],["v2_plot","dxJXywkeh"],["crypto_trades","cp3A5d4RF"],["btc_bin","S9SHj8UWX2"],["t_ohlc","T7QigiFJ3d"],["ohlc_plot","UizqQOb65P"],["plot1","GnuKp1pOi"],["bt","OTjXA5NMO"],["table","vRu9EuuaF"],["lc","d8ryZwBmm"],["result","Q_84kOZ9m"],["f","1EBE1WKt0"],["new_row","a0qQDW4fO"],["students","A2BWdjAzP"],["data_bars","mLScSp6l-"],["t1","jXDIWZV94"],["t2","4UQkViqAuB"],["plot_static_and_rt","yJI0BjGFe3"],["insurance","dhD7FW3ew"],["test_rollup","j4yvu-7N5M"],["insurance_rollup","ipJlPGNGna"],["data","7b1XUVfGu"],["points","6YnQkznxGk"],["plot","JwjC7UYmgW"],["tAu10TAYR","tAu10TAYR"],["r_L3Z2XMH","r_L3Z2XMH"],["b","QsaK0QOQf"],["mw2","M7NiwBHuY"],["stocks","6Ndd8y48r"],["st","hqhtqACBZA"],["monitor","-JAFaWXOL"],["tt","yS6nTHTbe"],["c","yEHVJ-eZQ"],["mi","wEv5KGRUb"],["ce","HZwzFIDFj"],["fe","SYCP3nux3"],["pp","UK0kIyQrq"],["srt","Hy7e1chnn"],["swt","HlUMKGCIl"],["swp","qmQwLBVD0"],["my_dash","kmsq68WzT"],["my_dash2","oRmGkEaWl"],["simple_plot","NczLz71Rc"],["trig_table","purcF27u90"],["trig_figure","GmgsMYAdXg"],["trades_clickhouse","ryxGjlRf2"],["trades","VJ3EHfyVe"],["ui_tt","-PW6fSIo0"],["small_insurance_rollup","tepB2zhag"],["no_group_tree","rtUxUCRy8"],["no_group_agg","juPTMjv8_r"],["group_rollup_no_agg","yXvySfNWTO"],["group_rollup_agg","8PkvPxWBEf"],["ui_common_example","ZTusv3a90"],["ui_common_example_row","N7k4337WWq"],["t3","xRfrl7bkeV"],["t4","fggZLT8FZh"],["t5","Hv2aH8JXiH"],["t6","LvNUOWuNOw"],["t7","ma-35WhL_r"],["t8","ZcSvpsunrF"],["t9","gIeGYS-rYe"]]}},"componentName":"lm-react-component","isFocusOnShow":true,"reorderEnabled":true,"componentState":null},{"type":"react-component","component":"LogPanel","title":"Log","isClosable":false,"id":"nJ-l7uep-c","props":{"metadata":{}},"componentName":"lm-react-component","isFocusOnShow":true,"reorderEnabled":true,"componentState":null},{"type":"react-component","component":"CommandHistoryPanel","title":"Command History","isClosable":false,"id":"e_MPTTa9xm","props":{"metadata":{},"panelState":{}},"componentName":"lm-react-component","isFocusOnShow":true,"reorderEnabled":true,"componentState":null},{"type":"react-component","component":"FileExplorerPanel","title":"File Explorer","isClosable":false,"id":"tKhr0Oirtb","props":{"metadata":{}},"componentName":"lm-react-component","isFocusOnShow":true,"reorderEnabled":true,"componentState":null},{"type":"react-component","component":"NotebookPanel","isFocusOnShow":false,"props":{"metadata":{"id":"2gZXyJGjZ"},"panelState":{"settings":{"language":"python","value":"jfhdjshafkjdhsakf\n\n\nfdsafdsa\n\nfdsafdsad"},"fileMetadata":{"id":null,"itemName":"Untitled-0.py"},"isPreview":false}},"title":"Untitled-0.py","id":"2gZXyJGjZ","componentName":"lm-react-component","isClosable":true,"reorderEnabled":true,"componentState":null}]}]

Oddly, sometimes it seems to take 1ms and other times it's taking 40ms??
image

🤔

@dsmmcken
Copy link
Contributor

dsmmcken commented May 1, 2024

It probably sucks more with multiple notebooks each with very large scripts in them.

@dsmmcken
Copy link
Contributor

dsmmcken commented May 1, 2024

Also according to this benchmark, we are using one of the slowest deep-equal implementations.

https://www.npmjs.com/package/fast-deep-equal#performance-benchmark

@mattrunyon
Copy link
Collaborator

So we already listen to an event here before comparing. Do we even need to compare the layout? How do you get that event to emit without the layout actually changing in some way that you want to save?

We could just save on a throttle/debounce if the save is cheaper than the comparison.

@mattrunyon
Copy link
Collaborator

Or is that state changed a react state change?

@@ -228,6 +231,11 @@ export function DashboardLayout({
}
}, [dehydrateComponent, isItemDragging, lastConfig, layout, onLayoutChange]);

const throttledHandleLayoutStateChanged = useMemo(
() => throttle(handleLayoutStateChanged, STATE_CHANGE_DEBOUNCE_MS),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need the trailing option I think so the final call is always run. Not sure if that's a default or how the specifics work, but make sure the final call is always executed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trailing=true is the default.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the default is leading=true and trailing=true. So I think it should be fine? But might want to add a flush call on unmount just in case there's a pending update

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep so this will currently call immediately, then again at the end of the timeout if the throttled function has been called again. flush will call whatever the last invoked was ignoring timeout and will not trigger a call at the end unless it has been invoked again.

So flush on unmount to ensure there's no queued saves

@mofojed
Copy link
Member Author

mofojed commented May 1, 2024

@dsmmcken requestIdleCallback is not available in Safari. We can switch to fast-deep-equal, though I will keep that as a separate PR in case there are unexpected side effects (in my tests it doesn't look like there would be, but better safe than sorry).
@mattrunyon We need a check to avoid an infinite loop - this eventually triggers a workspace data update, which causes any of the props read from redux workspace data to potentially update, causing the component setState to be called (because it's shimmed in instead of componentWillUpdate) in golden-layout, triggering a stateChanged event. I don't want to change that wiring, especially for this which will need to be a hotfix to V+.

@dsmmcken
Copy link
Contributor

dsmmcken commented May 1, 2024

requestIdleCallback is not available in Safari.

Sorry, lame. Looks like it's in TP now at least.

@dsmmcken
Copy link
Contributor

dsmmcken commented May 1, 2024

DEBOUNCE_PANEL_STATE_UPDATE is 400ms in the notebook panel, that could probably also be bumped up in time.

@mofojed
Copy link
Member Author

mofojed commented May 1, 2024

@dsmmcken the DEBOUNCE_PANEL_STATE_UPDATE is not the issue here and is not coming into play at all. Because workspace data is updating, the redux data updates, triggering a re-render, triggering the golden layout stateChanged event to be fired, even if none of the props have changed.
Potentially could be fixed by making all of our panel components PureComponents... though that may have other unintended side effects.
I can't figure out why the heck this debouncing is affecting the e2e tests. It's like the layout is getting reset just before taking the screenshot.

@mofojed
Copy link
Member Author

mofojed commented May 1, 2024

Actually the PureComponent thing won't matter, since even calling this.setState in NotebookPanel will trigger a componentWillUpdate, which golden-layout directly wires into.

- Need to use refs so that the callbacks don't change each time, and the flush/cancel are stable
- Fun!
@mofojed mofojed requested a review from mattrunyon May 2, 2024 02:23
Comment on lines +229 to +232
useEffect(
() => () => throttledProcessDehydratedLayoutConfig.flush(),
[throttledProcessDehydratedLayoutConfig]
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of the hook instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it to the hook.

packages/dashboard/src/DashboardLayout.tsx Outdated Show resolved Hide resolved
packages/dashboard/src/DashboardLayout.tsx Outdated Show resolved Hide resolved
packages/react-hooks/src/useThrottledCallback.ts Outdated Show resolved Hide resolved

/**
* Wraps a given callback in a cancelable, throttled function. The throttled
* callback is automatically cancelled if the callback reference changes or the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this? Since flushOnUnmount also triggers with the callback changing

Suggested change
* callback is automatically cancelled if the callback reference changes or the
* callback is automatically cancelled or flushed if flushOnUnmount is set if the callback reference changes or the

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I don't think we want to actually flush or cancel when the callback reference changes, we only want to cancel or flush on unmount, and we want the throttled callback to always call the latest callback reference.
I've changed it to always return a stable callback... as I think the behaviour is too ambiguous/confusing if you try to introduce cancel/flush whenever the callback reference changes (do you want to cancel or flush? Should the throttle timer reset? etc).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also of note the useDebouncedCallback should probably be structured similarly, as right now it could continually cancel if the callback passed in changes and then never gets called.

@mofojed mofojed requested a review from mattrunyon May 2, 2024 15:24
@mofojed mofojed merged commit 47f9a57 into deephaven:main May 2, 2024
9 checks passed
@mofojed mofojed deleted the fix-notebook-lag branch May 2, 2024 17:25
@github-actions github-actions bot locked and limited conversation to collaborators May 2, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants