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

unblock main thread when fetching user votes #14793

Merged
merged 5 commits into from
Feb 12, 2024

Conversation

dnenov
Copy link
Collaborator

@dnenov dnenov commented Dec 20, 2023

Purpose

A small PR introducing an async loading of UserVotes to unload the main thread from potentially long operation (under slow or now internet?) and unblock Dynamo loading process.

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

  • before fetching user votes could potentially slow down dynamo loading as it was being executed on the main thread
  • now using Task.Run to execute on a separate thread and unblock the loading process

Reviewers

@mjkkirschner
@aparajit-pratap

FYIs

@pinzart

- before fetching user votes could potentially slow down dynamo loading as it was being executed on the main thread
- now using Task.Run to execute on a separate thread and unblock the loading process
@@ -285,7 +285,7 @@ internal PackageManagerClientViewModel(DynamoViewModel dynamoViewModel, PackageM

if (AuthenticationManager.LoginState.Equals(LoginState.LoggedIn))
{
this.Uservotes = this.Model.UserVotes();
Task.Run(() => this.Uservotes = this.Model.UserVotes());
Copy link
Member

@mjkkirschner mjkkirschner Dec 21, 2023

Choose a reason for hiding this comment

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

this is going to execute from other some other thread pool thread.

Does anything else write to UserVotes? (another thread?) - it seems like it could be made readonly if not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hi, sorry for the late reply on this thread!

I am not sure if I got this correctly - the UserVotes() returns the results of the Greg call:

image

It is only used once when we create the PackageManagerClientViewModel and fetches any votes from the current user, I think that's about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I say keep it in a Task.Run, but move it closer to the greg client call.
Having it in the VIewModel is risky, because someone might add UI code (code that interacts with UI elements) in the UserVotes body
As @mjkkirschner mentioned, we need to make sure all data that is accessed by other thread is thread safe (in some way or another)
ALso we need to make sure we handle exceptions properly. If ExecuteAndDeserialize throws an exception, what happens?

Copy link
Collaborator Author

@dnenov dnenov Jan 12, 2024

Choose a reason for hiding this comment

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

I think I got it. I moved the call to right before using the Uservotes for the first time.

@mjkkirschner
Copy link
Member

mjkkirschner commented Dec 21, 2023

hey @pinzart90 what do you think about this note a the very start of the docs...

https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios#overview-of-the-asynchronous-model

For I/O-bound code, you await an operation that returns a Task or Task<T> inside of an async method.
For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method.

If the work you have is I/O-bound, use async and await without Task.Run. You should not use the Task Parallel Library.

I think in general refactoring all of our code to be async/await might not be worth the pain - though I guess the point of the above advice is that eventually if we continue this pattern of moving io bound code to threads, we'll be using threads just to unblock the main thread and taking on that overhead when we could have used async/await and avoided the thread overhead/ potential deadlocks,livelocks.

@pinzart90
Copy link
Contributor

pinzart90 commented Dec 21, 2023

hey @pinzart90 what do you think about this note a the very start of the docs...

https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios#overview-of-the-asynchronous-model

For I/O-bound code, you await an operation that returns a Task or Task<T> inside of an async method. For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method.

If the work you have is I/O-bound, use async and await without Task.Run. You should not use the Task Parallel Library.

I think in general refactoring all of our code to be async/await might not be worth the pain - though I guess the point of the above advice is that eventually if we continue this pattern of moving io bound code to threads, we'll be using threads just to unblock the main thread and taking on that overhead when we could have used async/await and avoided the thread overhead/ potential deadlocks,livelocks.

I know we talked about this in the past a bit:
https://autodesk.slack.com/archives/C01C8GGT9EY/p1701112257601019?thread_ts=1701109330.370119&cid=C01C8GGT9EY

My take on it is that we should not block use/block threadPool threads for something as simple as I/O code (file read/write, web access etc) when we already have an awaitable I/O operation to deal with. It might not be simple to refactor existing code to go from Task.Run to async/await. Example : all of our Greg code (that deals with packageManager requests) is not awaitable, so of course it is easier to stick it in another thread.
I say let's not refactor existing code just yet, but keep this in mind going forward.

@reddyashish
Copy link
Contributor

@mjkkirschner @pinzart Just to confirm, do we want to hold off this change for 3.0.2 and refactor it in 3.1?

Copy link

UI Smoke Tests

Test: success. 2 passed, 0 failed.
TestComplete Test Result
Workflow Run: UI Smoke Tests
Check: UI Smoke Tests - net8.0

Copy link

UI Smoke Tests

Test: success. 2 passed, 0 failed.
TestComplete Test Result
Workflow Run: UI Smoke Tests
Check: UI Smoke Tests - net8.0

@@ -482,6 +477,12 @@ public List<PackageManagerSearchElement> ListAll()
{
CachedPackageList = new List<PackageManagerSearchElement>();

// Attempt to load user votes prior to using it
if (AuthenticationManager.LoginState.Equals(LoginState.LoggedIn) && Uservotes == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

So you are basically caching UserVotes.
Will it be a problem that UserVotes will not refresh for subsequent calls to public List<PackageManagerSearchElement> ListAll() ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is a 15 minute lag until the database picks up the changes, as far as I know. The data is specific to the user. Which means, that they won't be able to see the subsequent change until the database picks it up, again this is as far as my knowledge goes on how fast the data will be registered.

With that, I think there is no problem that the UserVotes won't refresh in subsequent calls of ListAll().

// Attempt to load user votes prior to using it
if (AuthenticationManager.LoginState.Equals(LoginState.LoggedIn) && Uservotes == null)
{
Task.Run(() => this.Uservotes = this.Model.UserVotes());
Copy link
Contributor

Choose a reason for hiding this comment

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

also Task.Run() will run asynchronously on another thread. That means that the rest of the code will execute without waiting for the result.
Also won't Model.ListAll() cause the same UI blocking issue ?

Ideally we should make all these methods async and cascade them down to a UI method that makes sense to be async. Something like DynamoViewModelRequestShowPackageManagerSearch should be async (and all the async-able calls downstream) and the UI should relate that it is doing something async (UI spinner etc). I realize this is too much for 3.0.x release.

I am no longer sure how we can make this work properly, since a part of the code needs the information synchronously (which means the UI thread needs to wait for that information)

Copy link
Contributor

Choose a reason for hiding this comment

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

OF course, just by moving the offending outside the constructor, you will no longer block the DynamoView initialization code (basically Dynamo load logic). We will now block the UI, but later on when the user actually requests PackageManager information.
Maybe we it is better than nothing.

We could also try to parallelize the calls something like

           IEnumerable<PackageHeader> allPackages;
           var task1 = Task.Run(() => Model.UserVotes());
           var task2 = Task.Run(() => Model.ListAll());

           Task.WhenAll(task1, task2).Wait();

           Uservotes = task1.Result;
           allPackages = task2.Result;

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, looks like there are some unresolved discussions around REstSHarp's thread safety.
I think it is ok to call it from multiple threads, just not in parallel
So, runnig the code on the UI thread might be safer (There is no real value in using Task.Run except if you can do it async or parallelize it)

@dnenov
Copy link
Collaborator Author

dnenov commented Jan 26, 2024

Hi @pinzart90 @mjkkirschner - would it be OK to merge this PR at this point? Or close it down in favor of a more centralized effort to align the correct behavior of all Greg async calls? Just to remind us, this PR addresses a lag/freeze on Dynamo loading on slow machines blocking the main thread detected by @mjkkirschner. It is probably a very minor change relative to the holistic conversation about thread safety.

@@ -482,6 +477,8 @@ public List<PackageManagerSearchElement> ListAll()
{
CachedPackageList = new List<PackageManagerSearchElement>();

// Calls to Model.UserVotes and Model.ListAll might take a long time to run (so do not use them syncronously in the UI thread)
Copy link
Contributor

@pinzart90 pinzart90 Feb 7, 2024

Choose a reason for hiding this comment

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

public List<PackageManagerSearchElement> ListAll() is called from an async method upstream
It is not going to block the UI thread anymore.

We do need a clear way to mark these methods (that they are Task based or async) . This will be part of another task

Copy link
Member

Choose a reason for hiding this comment

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

what async method calls ListAll()?

Copy link
Contributor

Choose a reason for hiding this comment

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

Task<IEnumerable<PackageManagerSearchElementViewModel>>.Factory.StartNew(RefreshAndSearch).ContinueWith((t) =>

@pinzart90 pinzart90 merged commit 0435f08 into DynamoDS:master Feb 12, 2024
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants