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

Delay preview creation till it is actually needed. #15823

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

dimven-adsk
Copy link
Contributor

Purpose

Node preview bubbles, while incredibly useful, eat up a lot of resources the first time they are instantiated. Currently, when a graph loads the previews are not initialized (great!), but as soon as the mouse passes over the node, even for a fraction of a second, you have to pay the loading cost (not great :( ). Let's say you're just trying to pan a large graph from one side to the other. You're just trying to navigate, to a different spot in the graph and for some reason there are constant spikes of lag.

This PR tries to resolve the problem in two ways - first, we make sure that just passing over a node with the mouse does not immediately instantiate the preview. Instead we start a short timer and initialize the preview only if the mouse focus stays until it fires. Secondly, we make sure the state machine is updated correctly when you pan the graph with middle mouse button (which is by far the most common navigation method; I don't have the numbers but I'm sure a lot more people use the MMB instead of the pan switch in the top right corner) and disable all popups during panning.


Metrics

When trying to quickly navigate the MaRS graph (~700 nodes) from start to finish, preview instantiation can easily take up over 10% of the CPU time:

devenv_I1Z5UIpETR

devenv_QcuBAklpOQ

We can completely negate that with the proposed updates (I did get an extra ping in the second run because I was a bit slower and hovered over a node for too long):

devenv_00No0TNpYT
devenv_Q70oSgelGK

Declarations

Check these if you believe they are true

  • The codebase is in a better state after this PR
  • Is documented according to the standards
  • The level of testing this PR includes is appropriate
  • User facing strings, if any, are extracted into *.resx files
  • All tests pass using the self-service CI.
  • Snapshot of UI changes, if any.
  • Changes to the API follow Semantic Versioning and are documented in the API Changes document.
  • This PR modifies some build requirements and the readme is updated
  • This PR contains no files larger than 50 MB

Release Notes

(FILL ME IN) Brief description of the fix / enhancement. Use N/A to indicate that the changes in this pull request do not apply to Release Notes. Mandatory section

Reviewers

@pinzart90 , @mjkkirschner

(FILL ME IN) Reviewer 1 (If possible, assign the Reviewer for the PR)

(FILL ME IN, optional) Any additional notes to reviewers or testers.

FYIs

(FILL ME IN, Optional) Names of anyone else you wish to be notified of

till when it is actually needed
{
delayPreviewControlTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher);
delayPreviewControlTimer.Interval = TimeSpan.FromMilliseconds(300);
delayPreviewControlTimer.Tick += DelayPreviewControlAction;
Copy link
Member

Choose a reason for hiding this comment

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

can you unsubscribe this handler somewhere that makes sense - maybe inside the DelayPreviewControl Action handler?

Copy link
Member

Choose a reason for hiding this comment

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

or in OnNodeViewMouseLeave maybe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a static timer and there's only going to be one instance per process. Plus the handler does not create any new references. Is it really necessary to unsubscribe it?

Copy link
Member

@mjkkirschner mjkkirschner Feb 13, 2025

Choose a reason for hiding this comment

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

yes, unless you want to prove that it doesn't cause leaks / new retention paths with a memory profiler when run against our serial test suite.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it. I think it makes the most sense to put it inside the RemoveViewModelsubscriptions method of the workspace view

/// node views, instead of each having its own timer with additional overhead.
/// </summary>
private static DispatcherTimer delayPreviewControlTimer = null;
private static NodeView previewControlHost = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not so sure the previewControlHost is necessary.

Copy link
Contributor Author

@dimven-adsk dimven-adsk Feb 14, 2025

Choose a reason for hiding this comment

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

it's necessary if we want to have a single timer as a shared resource between nodes. We use that field to store a ref to the last hovered node and we clear it at the end of the timer event action.

It's not necessary with the action debouncer

}

// Always set old ZIndex to the last value, even if mouse is not over the node.
oldZIndex = NodeViewModel.StaticZIndex;
Copy link
Contributor

Choose a reason for hiding this comment

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

it looks like now we are setting the oldZIndex in more places than before.
Is it intentional ? could it cause issues?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, it's exactly the same as before - twice. I only moved that code from the TryShowPreviewBubbles to one level up in the new InitialTryShowPreviewBubble. That's because the nested method will only run conditionally but we need to get the stored Z height every time. Every time the mouse leaves the node control, the Z height gets set again and if we don't first read it, it defaults to -1(or was it 0?) and the node ends up hidden behind everything else.

/// needed. It is static so that it can be shared for all node views, instead of
/// each node having its own debouncer with additional overhead.
/// </summary>
private static ActionDebouncer delayPreviewControl = null;
Copy link
Contributor

@pinzart90 pinzart90 Feb 14, 2025

Choose a reason for hiding this comment

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

An alternative to having a static that you need to manage for every WorkspaceModel change would be to store the delayPreviewControl debouncer as an internal (non-static) member inside the WorkspaceViewModel class.
That way you can better control the life cycle (initialize on workspace constructor and cleanup on dispose).
NodeView has access to the workspace view model (ex. viewModel?.WorkspaceViewModel?.delayPreviewControl?.Debounce) and you still get a single instance per Workspace

Copy link
Contributor

Choose a reason for hiding this comment

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

Great idea, it worked just as well and produced better structured code.

@pinzart90
Copy link
Contributor

I think we should wrap this behavior under a feature flag.
ALso the tests that are failing will probably have to add DispatcherUtil.DoEventsLoop() with a condition set as the desired state (ex. preview enabled)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants