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

Pub/sub for the left panel #5669

Merged
merged 2 commits into from Jan 30, 2019

Conversation

amaiorano
Copy link
Contributor

This is work based off the conversations from this other PR: #5617

Changes proposed in this pull request:

Rework RepoObjectsTree (ROT) so that instead of being "push" updated from FormBrowse via ROT.ReloadAsync, the Tree nodes (Branches, Remotes, and Tags) now "pull" for changes via UICommands.PostRepositoryChanged (i.e. "pubsub").

This refactor will allow for the Submodules tree work proposed in the other PR to be done more easily. In fact, I have a local commit on top of these changes for this, and it works very well now.

Screenshots before and after (if PR changes UI):

  • No screenshots as it behaves pretty much like before. Unfortunately, because each Tree node is responsible for updating itself independently, there's a bit more refresh glitchiness, but I tried to minimize it (see Tree.ReloadNodes).

What did I do to test the code and ensure quality:

This change removes the "push" update from FormBrowse, so I identified the places that used to perform this push, and made sure to address them, and test them. Namely:

  • Refresh (from button or menu): Refresh is used for when user does stuff in shell outside of GE, so we do care about this. This works by calling ROT.RefreshTree which acts as though the repo has changed.
  • FormReflog: Now uses GitUICommands instead of creating its own FormCreateBranch so that PostRepositoryChanged will get called.
  • Undo last commit: Nothing to do since undoing a commit shouldn't affect Branches/Remotes/Tags. (Note that with Submodules, we might revert a commit that affects submodule status, so maybe call SubmoduleProvider.Upate after? Or for now, just let user refresh until SubmoduleProvider can do its own polling for changes in the background?).
  • FormBrowse.RegisterPlugins: Nothing to do since ROT is not affected by plugins.

Apart from that, I tested:

  • Switching repos
  • Create/delete branches
  • Using FormReflog to create branches
  • Refreshing (both refresh button and "Reload" ROT context action) after making modifications outside the repo. Note that the "Reload" action should probably be removed since Refresh does the trick, and actually, if you create a branch outside of GE, then reload ROT, it will show the branch but FormBrowse will not, so probably better to just drop ROT.Reload and stick to Refresh only.

Has been tested on (remove any that don't apply):

  • GIT 2.19.1.windows.1
  • Windows 10

@codecov
Copy link

codecov bot commented Oct 31, 2018

Codecov Report

Merging #5669 into master will increase coverage by 0.02%.
The diff coverage is 82.05%.

@@            Coverage Diff             @@
##           master    #5669      +/-   ##
==========================================
+ Coverage   45.13%   45.15%   +0.02%     
==========================================
  Files         652      652              
  Lines       49433    49453      +20     
  Branches     6576     6573       -3     
==========================================
+ Hits        22310    22332      +22     
- Misses      25862    25864       +2     
+ Partials     1261     1257       -4

@gerhardol
Copy link
Member

Only browsed the code and see no breaking changes. I have been busy lately, will try to look closer in the next days.
It would be beneficial to rebase on master and to push the branch, to get more users trying this (at least me, it rebases cleanly on master right now). If the local branch is not in production status, just note that.

I tried this rebased on master and when pressing the refresh button the UI is frozen for like 30s..

The first discussion should probably be if this is the correct refactoring. Assuming so we need to convince @RussKie that this is an acceptable change for master. Hint: He is soft on tests. I have no suggestion really what to test though. There are really no tests on left side panel or submodule provider right now.

@amaiorano
Copy link
Contributor Author

amaiorano commented Nov 1, 2018

I'll rebase and push asap. 30 seconds? That's weird. I wonder if it's the increased async contention for the UI thread. Or maybe you refreshed while an update was in flight, which would cause cancellation. Though I tested that and it seemed to perform fine. Was it the GE repo?

Edit: rebased and pushed. Also, I've been trying to repro the long refresh, but have not been able to. I'm running a Release build without VS debugger attached. Were you testing Debug with VS attached? ?Maybe it's slower with VS's exception hooks.

Edit 2: Okay, I saw the slowdown compared to the previous version. Was not 30s for me, but sometimes around 5s. I compared to the previous code and there was a difference: formerly, LoadNodesAsync was awaited with ConfigureAwait(false), while in my version, I had not configured the awaiter. I added this, and now it runs much faster. If I understand correctly, ConfigureAwait(false) means we don't care about being on the same thread once the async function completes. In this case, we do want to get back on the UI thread, but I guess it's faster to ConfigureAwait(false) followed by await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();. Not sure why exactly, but it seems to do the trick. I squashed this fix and pushed the commit. Let me know if it's faster.

@amaiorano amaiorano force-pushed the amaiorano/pubsub-rot branch 2 times, most recently from 6514241 to 171bbc3 Compare November 1, 2018 03:56
@RussKie RussKie added this to the Future release milestone Nov 1, 2018
Copy link
Member

@drewnoakes drewnoakes left a comment

Choose a reason for hiding this comment

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

Quick review in the browser but it looks good to me, modulo comments.

{
TreeViewNode.NodeFont = new Font(AppSettings.Font, FontStyle.Bold);
TreeViewNode.NodeFont = new Font(AppSettings.Font, IsActive ? FontStyle.Bold : default);
Copy link
Member

Choose a reason for hiding this comment

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

Consider caching Font objects behind some logic that creates new instances if AppSettings.Font changes. Then you can select between two fonts based on IsActive and do a ReferenceEquals check before calling the NodeFont setter (if that setter doesn't itself deal with setting to the current value).

Also, Font is disposable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This makes sense, but I think perhaps should be tackled in a separate change? The reason I modified this code is that this fixes a bug with branch deletion where recycled TreeViewNodes wouldn't clear their bolded state.

uiCommands.UICommands.PostRepositoryChanged += UICommands_PostRepositoryChanged;
};

uiCommands.UICommands.PostRepositoryChanged += UICommands_PostRepositoryChanged;
Copy link
Member

Choose a reason for hiding this comment

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

Do we have to unsubscribe UICommandsChanged and PostRepositoryChanged at any point? Is this a leak?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the same pattern FormBrowse follows. The only leak is when the application is closed, but otherwise this makes sure to remove and add whenever the UICommands object has changed.

await loadNodesTask(token).ConfigureAwait(false);

token.ThrowIfCancellationRequested();
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(token);
Copy link
Member

Choose a reason for hiding this comment

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

If you remove ConfigureAwait(false) here you should continue on the main thread, in which case this line is not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, so what you said is what I thought, and my original push didn't have the ConfigureAwait(false). But then @gerhardol said he had a 30s stall when refreshing, and when I tested, I had some 5s stalls here and there. Comparing against the original code, I found that the await LoadNodesAsync would ConfigureAwait(false), despite joining on the UI thread right after. I tried it, and indeed it seems to no longer stall for that long.

I was wondering if perhaps ConfigureAwait(false) is also used as a hint to the task manager to run the task on a non-UI thread; or conversely, not having the ConfigureAwait(false) may make it more likely to run on the UI thread, which might cause thread contention, and thus bad performance? I don't know, I'm fairly new to async-await in C# (as as aside, I think it's really cool).

Copy link
Member

Choose a reason for hiding this comment

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

Yes ConfigureAwait(false) tells the awaiter to not capture the current SynchronizationContext (which in this case would be WinFormsSynchronizationContext) which has the effect of scheduling the continuation on the thread pool rather than the UI thread.

Removing ConfigureAwait it can highlight other threading issues. Possibly loadNodesTask is trying to do something on the UI thread. If you pause into the debugger when you have one of these slowdowns and look at the "parallel stacks" view of Visual Studio, you'll likely see what's going on.

{
TreeViewNode.TreeView.Nodes[0].EnsureVisible();
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Copy link
Member

Choose a reason for hiding this comment

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

Ditto.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So here, I was trying to handle if an exception gets thrown by loadNodesTask without ConfigureAwait(false), which I think means I might end up in the finally block on a non-UI thread, right? If we do decide to go for ConfigureAwait(false) (pending discussion on that above), then I can remove this SwitchToMainThreadAsync.

Copy link
Member

Choose a reason for hiding this comment

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

I assumed that if you removed ConfigureAwait(false) then all code would remain on the UI thread and you could avoid both switches.

ClearNodes();

await LoadNodesAsync(token).ConfigureAwait(false);
ThreadHelper.ThrowIfNotOnUIThread();
Copy link
Member

Choose a reason for hiding this comment

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

+1 for avoiding await here to get on the UI thread.

@@ -100,13 +100,47 @@ private abstract class Tree
protected readonly Nodes Nodes;
private readonly IGitUICommandsSource _uiCommandsSource;

protected readonly CancellationTokenSequence _reloadCancellation = new CancellationTokenSequence();
Copy link
Member

Choose a reason for hiding this comment

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

Nit: protected fields should have capital first letter. The name is a little confusing too. Recommend going long with ReloadCancellationTokenSequence or ReloadCancellationSequence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do on both counts.

}
finally
{
form.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Would using (form) be simpler here? Neighbours tend to use using statements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't think I could use using (form) here without allocating it. If so, I'll do that for sure.

Copy link
Member

Choose a reason for hiding this comment

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

using (value) is a valid alternative to using (declaration).

The form that accepts a declaration is a special case. Usually a declaration (var a = 1) doesn't produce a value (it's not an expression). An assignment does though, so you can write var i = (j = 0) and i will be zero too.

@gerhardol
Copy link
Member

gerhardol commented Nov 1, 2018

I was wrong, it was not 30s, it was more. After the change it takes 1:30 to refresh. I have 110 local branches and 10 remotes.
I got out-of-memory once too.
It is spending time in TreeViewNode.set DisplayText
To debug another day

@amaiorano
Copy link
Contributor Author

Thank you @gerhardol, I was able to repro the hang locally by creating a hierarchy of many branches. I've got something now that I can debug. I think I know what's going on, actually. My feeling is that this is because I no longer Begin/EndUpdate on the control, so it's constantly trying to render all the nodes that are being created.

@amaiorano
Copy link
Contributor Author

@gerhardol Okay, so adding Begin/EndUpdate seems to do the trick for the perf and OOM exceptions. Can you try now?

I still need to go through and make the changes @RussKie asked for. Also, I noticed a font bold bug that I thought came from my changes, but actually was introduced from this change. Basically it's possible now for branch folders to be bolded. I'll open an issue about it, or just fix it, but separate from this PR.

@gerhardol
Copy link
Member

Works fine for me after the change.
I will try to review this weekend.

@amaiorano
Copy link
Contributor Author

Pushed another commit based on @RussKie's review comments.

@amaiorano
Copy link
Contributor Author

amaiorano commented Nov 3, 2018

Pushed another commit where I revert a font change I made, since it actually didn't fix the main issue. Instead, I have a proper fix on another PR here: #5686

Edit: I'll be happy to squash this all down once it's approved.

@amaiorano
Copy link
Contributor Author

@RussKie I made the changes you requested. I know you're busy with v3, so whenever you have some time, could you review this again? Cheers.

@amaiorano amaiorano force-pushed the amaiorano/pubsub-rot branch 2 times, most recently from ff2555f to 6cec3e2 Compare November 8, 2018 13:38
@amaiorano
Copy link
Contributor Author

amaiorano commented Nov 9, 2018

@RussKie @gerhardol (Edit: sorry, it was @drewnoakes, not @RussKie, who reviewed this code. Meant to add him, but you're welcome too, @RussKie :) )

Hey guys,

I just wanted to clarify that this PR is necessary for my submodules ROT work. I've got a branch on my repo based on top of this one for submodules ROT, and I've been using it at work for the past week with no issue. I know you've both looked at this already; I've made the changes requested, and would just need another review, and hopefully a merge into master. After that, I can update the submodules PR with new commits.

Sorry, don't mean to sound pushy. If you're super busy, I get it. Just wanted to make sure this didn't slip through the cracks. Cheers!

@vbjay
Copy link
Contributor

vbjay commented Nov 9, 2018 via email

@amaiorano
Copy link
Contributor Author

/facepalm I just realized the recent release was an RC. For some reason, I thought it was the official v3 release. Okay, that makes sense. Thanks for clarifying.

@RussKie
Copy link
Member

RussKie commented Nov 9, 2018

Sorry, no major work will be merged into master right now except fixes. We're in a freeze period, if you like.

@RussKie RussKie changed the title Amaiorano/pubsub rot WIP Pub/sub for the left panel Nov 9, 2018
@amaiorano
Copy link
Contributor Author

Sorry, no major work will be merged into master right now except fixes. We're in a freeze period, if you like.

Yep, my bad. Will wait till post release. Thanks.

@amaiorano
Copy link
Contributor Author

amaiorano commented Jan 29, 2019

I managed to simulate some race conditions and duplicate nodes by abusing "refresh" button, however in the end it refreshed to the correct state.
One little issue spotted - select any local branches, hit refresh, the previously selected node isn't selected any longer.
EDIT: it appears to be an existing bug, raised #6181

I think you did find a legitimate problem with my change. The bug you reported in #6181 is something I believe I fixed in #6177 and is not exactly the same. I'll take a look. It's a separate issue. I will fix it as part of this PR, since I've already changes the node selection some.

@amaiorano
Copy link
Contributor Author

Okay, I just pushed a commit that improves node selection and top-level node expansion. As I wrote in my commit comment:

  • Only expand/collapse the top nodes (Branches, Remotes, Tags) the first time we've populated them. Subsequently, the expanded/collapsed state that the user sets will be restored upon refresh.
  • If there's no active branch (i.e. detached head), as before, we don't select any node.

This feels much better. When you open a repo, the Branches and Remotes node will initially expand, while Tags will be collapsed. However, after that, it will respect whatever state you put them in, thanks to the save/restore expanded nodes thing I added not too long ago. Also, if you open a repo that has no active branch, like submodules which typically are in detached head, Branches will be expanded initially thanks to my change here, but no node will be selected, which is how it worked before.

Give this is a whirl.

@RussKie
Copy link
Member

RussKie commented Jan 29, 2019

  • Only expand/collapse the top nodes (Branches, Remotes, Tags) the first time we've populated them. Subsequently, the expanded/collapsed state that the user sets will be restored upon refresh.

Ahh.... Initially I thought it was a bug, and almost raised an issue.

@amaiorano
Copy link
Contributor Author

  • Only expand/collapse the top nodes (Branches, Remotes, Tags) the first time we've populated them. Subsequently, the expanded/collapsed state that the user sets will be restored upon refresh.

Ahh.... Initially I thought it was a bug, and almost raised an issue.

If you think it shouldn't work this way, we can go back, but it means that if you decide to collapse Branches and Remotes, and the left panel gets refreshed, they will be re-expanded always. I think it makes more sense to only initially expand them, then respect the user's settings during the session.

One thing I noticed, though, is that if you switch repos, it will not see that as a "first time", so if you collapsed Branches, then switch repo, it will remain collapsed. We could change that behaviour, which might make more sense.

@RussKie
Copy link
Member

RussKie commented Jan 29, 2019

I don't mind that collapsed state is persistent for a given repo, but when a repo is changed I would think we should reset the state.
Opinions?

@amaiorano
Copy link
Contributor Author

I don't mind that collapsed state is persistent for a given repo, but when a repo is changed I would think we should reset the state.
Opinions?

I agree. I'll see what I can do about that.

@gerhardol
Copy link
Member

I would have preferred to never expand automatically at all, or just expand to selection only, but am OK with this.

Copy link
Member

@gerhardol gerhardol left a comment

Choose a reason for hiding this comment

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

No comments at all, which seem strange. Will likely approve tomorrow when I have let this spin another day.

@amaiorano
Copy link
Contributor Author

No comments at all, which seem strange. Will likely approve tomorrow when I have let this spin another day.

Well, if it makes you feel better, I've been working on this change for a long time. Also, @drewnoakes did review it a while ago.

@amaiorano
Copy link
Contributor Author

I've pushed a fixup commit which makes it so that changing repos effectively resets the state of the treeview nodes, so Branches/Remotes will start off expanded, and Tags collapsed.

I think this last change makes more sense, since I found it a little weird that because of how node recycling works, and with my change to save/restore expanded nodes, upon switching repos, if the target repo happened to have similar node paths, their expanded state would be restored, even though it was from a different repo.

@RussKie
Copy link
Member

RussKie commented Jan 30, 2019

Please squash the fixup before the merge

@amaiorano
Copy link
Contributor Author

Please squash the fixup before the merge

Of course. Using fixup commits during PRs as a way to apply requested changes, but without messing up the final set of commits that will be merged, is something I only recently started doing, and I love it. It allows for reviewers to see the new changes as separate commits, but once merged, we don't see "Apply fixes from PR" commits in the graph. Maybe it's just my OCD :)

…) registers for callbacks and handles its own update, instead of being told to reload
* Only expand/collapse the top nodes (Branches, Remotes, Tags) the first time we've populated them. Subsequently, the expanded/collapsed state that the user sets will be restored upon refresh.
* Expand Branches node the first time.
* If there's no active branch (i.e. detached head), as before, don't select any node.
@vbjay
Copy link
Contributor

vbjay commented Jan 30, 2019

I love them too. I use them to help me rebase all the time too, Autosquash is a great thing.

@amaiorano amaiorano changed the title WIP Pub/sub for the left panel Pub/sub for the left panel Jan 30, 2019
@amaiorano amaiorano merged commit c27b609 into gitextensions:master Jan 30, 2019
@ghost ghost removed the status: ready label Jan 30, 2019
@amaiorano amaiorano deleted the amaiorano/pubsub-rot branch January 30, 2019 13:44
@drewnoakes
Copy link
Member

Also really like fixup/squash commits while WIP.

I would occasionally forget to squash and merge PRs with them in. There are some in the GE history (you're welcome).

That was the motivation for adding warning icons in the revision grid alongside commits with messages starting with fixup! or squash! :)

@amaiorano
Copy link
Contributor Author

That was the motivation for adding warning icons in the revision grid alongside commits with messages starting with fixup! or squash! :)

Aha! For that, I thank you :)

@vbjay
Copy link
Contributor

vbjay commented Jan 30, 2019

That was the motivation for adding warning icons in the revision grid

LOL! I figured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🚧 status: in progress Issues which have associated PRs
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants