Search bar stuttering improvements#15726
Conversation
UI Smoke TestsTest: success. 11 passed, 0 failed. |
| if (_cts != null) | ||
| _cts.Cancel(); | ||
| _cts = new CancellationTokenSource(); | ||
| System.Threading.Tasks.Task.Delay(timeout, _cts.Token).ContinueWith(t => |
There was a problem hiding this comment.
hmm, unfortunately I find this hard to reason about, it seems to me that the continuation will execute on a thread pool thread, not the main UI thread - which means that the dispatcher invoke call will be made from another thread. I think you should use BeginInvoke to avoid any super hard to reason about deadlocks.
There was a problem hiding this comment.
Changed to use BeginInvoke. Maybe it could benefit from explicitly setting the DispatcherPriority as well?
There was a problem hiding this comment.
Invoke and BeginInvoke should both execute on the dispatcher's associated thread (in our case the UI thread).
It should not matter if the dispatcher invoke calls are coming from other threads. Performance could be an issue (thread switching)...
We could also use the run the Continuation on the SynchronizationContext.Current (which in WPF apps, by default, is always associated with the UI thread)
|
Thanks for trying to address this! besides the review comments - some more thoughts.
@QilongTang do you agree on point 1 and 2? @bendikberg you can check feature flags here: but we'd need someone to add a feature flag for debug and release builds online for you to check. |
Fine by me, do you want me to create a flag for him, or a flag for the overall performance improvements? |
Let’s do granular flags for each change. Thanks Aaron. |
|
Thanks for the input @QilongTang and @mjkkirschner I tried implementing the suggested changes, as well as update the test to include some debounced cases as well. Should those be in separate functions instead? I am a little unsure what to do about the feature flag, will you set one up for this specific case? |
| // run with debouncer | ||
| count = 0; | ||
| searchControl.SearchTextBox.Text = "dsfdfdsfdf"; | ||
| Thread.Sleep(vm.searchDelayTimeout * 2); |
There was a problem hiding this comment.
Are Thread.Sleep()'s okay during tests? I'm not sure how else to verify that the debounces work as expected.
There was a problem hiding this comment.
I think it's acceptable since these are relatively small. - I guess you could also make the delay time even smaller for testing though too small and it's probably not reliable anyway.
|
|
||
| int count = 0; | ||
| (searchControl.DataContext as SearchViewModel).SearchCommand = new Dynamo.UI.Commands.DelegateCommand((object _) => { count++; }); | ||
| var vm = searchControl.DataContext as SearchViewModel; |
There was a problem hiding this comment.
would you mind splitting this into 2 tests?
| private string searchText; | ||
| internal int searchDelayTimeout = 150; | ||
| private bool useDebouncer = true; // replace with feature flag check | ||
| private ActionDebouncer searchDebouncer = new ActionDebouncer(Dispatcher.CurrentDispatcher); |
There was a problem hiding this comment.
so you have to be a bit careful using method of retrieving the dispatcher - if this code is called from a non ui thread the dispatcher will not be the WPF dispatcher.
That seems unlikely in most scenarios. If you have access to the DynamoViewModel here there is a property UIDispatcher that should always be correct, as its set from the WPF main window's dispatcher.
There was a problem hiding this comment.
Yeah, that Dispatcher retrieval issue was the original reason for having the initialization inside the SearchText setter, with the assumption that the setter would only be called from the UI thread.
The DynamoViewModel UIDispatcher seems to be set by the View it belongs to after the SearchViewModel is constructed, so that requires the debouncer to be constructed lazily at some point in the future when the UIDispatcher has been set, rather than during the SearchViewModel construction.
So then I think we're back to some variation on the first version with a null check inside of the SearchText-setter?
There was a problem hiding this comment.
Yeah, that Dispatcher retrieval issue was the original reason for having the initialization inside the SearchText setter, with the assumption that the setter would only be called from the UI thread. ah ok.
I'm not sure that's a strong assumption. It's a public method on a public class running in an application full of extensions and packages.
For now, what about
- moving it back to your original initialization with a comment
- wrapping this call in a try catch and logging, it seems unlikely, but possible it could be a non-UI thread dispatcher and, in that case, it might crash when the search results get modified from the wrong thread - unless this only causes binding updates anyhow that will get marshalled to the UI thread by WPF?
There was a problem hiding this comment.
That's fair, it probably was the wrong assumption on my part.
- Alright, I went back to the lazy initialization.
- I wrapped the call as suggested. I don't think the debouncer can ever use anything other than UIDispatcher now, unless someone sets the wrong dispatcher on the DynamoViewModel, but then I think bigger issues are in store.
I did try setting up a test where the Dispatcher gets set from another thread, and the SearchText is set from that same thread, and then again set from inside. It all works, even with the wrong Dispatcher.
Where it does fail is if I try to set the Text property directly on the control, which throws an exception due to using the wrong Dispatcher, but I don't think that has anything to do with the debouncer dispatcher.
|
test failed with: |
|
I've created It will return true in debug builds and false in release builds. |
|
I tried increasing the test debounce timeout somewhat, to see if that helps when running in CI. Since it returns 0 none of the debounces were allowed to finish before the sleep expired. When running locally the average time of running a test is about 5 seconds with the window opening and closing, and the sleeps are at most 500 ms for one test. |
so does your test work locally? Does it work if you run a bunch of view tests in serial in one test job? |
| DefineFullCategoryNames(LibraryRootCategories, ""); | ||
| InsertClassesIntoTree(LibraryRootCategories); | ||
|
|
||
| useDebouncer = DynamoModel.FeatureFlags?.CheckFeatureFlag("searchbar_debounce", false) ?? false; |
There was a problem hiding this comment.
I am not sure how often this class is instantiated, or if it's only once during dynamo view model creation.
If it's frequent, then I think this use of CheckFeatureFlag is safe - if it's only once at startup then I think it will be more predictable to either:
- check the feature flag onTextChanged, it's probably safe to cache at that point, or don't if it does not affect performance.
- use the static
DynamoFeatureFlagsManager.FlagsRetrievedevent to make the check only after we have received flags.... unfortunately, If you try to use that from this class's constructor - I think you could get into a situation where you then subscribe this event too late, it would need to be done before the feature flags retrieval is made, which is from dynamo model. If you want to only make this check once you could lump it into here:Dynamo/src/DynamoCore/Models/DynamoModel.cs
Line 1015 in 23bf463
It's not obvious, but CheckFeatureFlag will only check the local cache of flags which may not have been retrieved yet from the flags service.
Personally, I would go for 1 because it's simpler to reason about IMO, touches less files etc unless performance is an issue but under the hood it's just a dictionary lookup.
|
@pinzart can you help me out here the tests are giving us a bunch of random failures, webview2 initialization errors, random libG loading errors... and I'm suspiciously eyeing the dispatcher do events stuff - can you remind me and @bendikberg of best practices when using that? |
| // Dipatcher.CurrentDispatcher has a chance of returning the wrong dispatcher, so we need to | ||
| // use the dynamoViewModel.UIDispatcher. The UIDispatcher is set after the SearchViewModel | ||
| // constructor runs, otherwise the debouncer could be initalized there | ||
| searchDebouncer = new ActionDebouncer(dispatcher); |
There was a problem hiding this comment.
why not initialize this class in the SearchViewModel constructor ?
You could also pass the logger to the ActionDebouncer instance and do the try catch inside there (I am assuming this class is supposed to be reused through out dynamo)
| dispatcher.BeginInvoke(action); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
If you simply assume (document) that this class should only be called on the UI thread (not enforced) if it updates UI elements, you could simply schedule the continuation on the same thread by using the TaskScheduler.FromCurrentSynchronizationContext() option.
Note: TaskContinuationOptions.ExecuteSynchronously does not guarantee runing on the same thread.
This way you do not even need the dispatcher.
Passing a generic dispatcher to the constructor does not really enforce it being use in the UI thread anyway.
There was a problem hiding this comment.
that sounds like a really good simplification.
There was a problem hiding this comment.
I decided to go with async await instead of continuations. After reading some more about it, it seems async/await has better performance and allocates less resources
There was a problem hiding this comment.
Changed my mind on the async/await vs continuations. Async/Await throws errors and is a nuisance during debugging. The difference in performance is at the level of microseconds and should not be an issue
| // To prevent any debounces from triggering at some unexpected point before or after the control | ||
| // becomes visible, this flag is only set once the searchText value is set by the user | ||
| // (unless it somehow gets set somewhere else) | ||
| searchInitialized = searchInitialized || !string.IsNullOrEmpty(searchText); |
There was a problem hiding this comment.
why not just run the debouncer if (!string.IsNullOrEmpty(searchText)) and the non debounced version if the searchtext is empty or null ?
searchText is set multiple times before the control becomes visible and interactabl
Is it called with non empty content ?
| internal ActionDebouncer searchDebouncer; | ||
| private bool searchInitialized = false; | ||
|
|
||
| private string searchText = string.Empty; |
There was a problem hiding this comment.
I am hoping we can just keep searchDelayTimeout and searchDebouncer
I only see some python tests failing with a Load exception. There are also some structured exceptions thrown (which historically have been hard to figure out). I would suggest to run the tests and log onto the build machine. Maybe some test gets stuck (with a message box on screen) and messes up the rest. Webview2 exceptions would lead more to misuses of DoEvents (probably would have to use DoEventsLoop until the desired UI state is achieved). @mjkkirschner can you point out the webview2 exceptions? |
The webview 2 initialization issues were here: |
|
@bendikberg can you try using |
| namespace Dynamo.Wpf.Utilities | ||
| { | ||
| /// <summary> | ||
| /// The ActionDebouncer class offers a means to redunce the number of UI notifications for a specified time. |
|
Passed here https://master-5.jenkins.autodesk.com/job/Dynamo/job/DynamoSelfServe/job/pullRequestValidation/17049 |
This reverts commit 7334501.
This reverts commit 7334501.
This reverts commit 47629b3.


Purpose
The Dynamo search bar feels stuttery when typing many characters in succession, as every key stroke starts a new search. These searches then block the Dynamo UI and will in some cases leave the UI hanging for multiple seconds at a time while all the searches complete. Since the user most likely didn't want to search for 'e', 'el', 'ele', ... etc. when they typed 'element', this both breaks user flow and leads to search feeling way slower than it has to be.
This change adds a debounce to the search (which could potentially be configurable by the user), which postpones the search until the user has paused typing for a short while.
Current search
xSuE3gCjMb.mp4
Proposed change
mfDyPu2TBW.mp4
Declarations
Check these if you believe they are true
*.resxfilesRelease 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
(FILL ME IN) Reviewer 1 (If possible, assign the Reviewer for the PR)
@mjkkirschner
(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
@dimven