Allow VSGitExt to be constructed and subscribed to from background thread #1506
Changes from all commits
323f330
5200e72
68d098a
dcd806f
2a3cef5
16aabcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,25 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using System.Collections.Generic; | ||
using System.ComponentModel.Composition; | ||
using GitHub.Models; | ||
using GitHub.Services; | ||
using GitHub.Logging; | ||
using GitHub.Helpers; | ||
using GitHub.TeamFoundation.Services; | ||
using Serilog; | ||
using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; | ||
using Task = System.Threading.Tasks.Task; | ||
|
||
namespace GitHub.VisualStudio.Base | ||
{ | ||
/// <summary> | ||
/// This service acts as an always available version of <see cref="IGitExt"/>. | ||
/// </summary> | ||
/// <remarks> | ||
/// Initialization for this service will be done asynchronously and the <see cref="IGitExt" /> service will be | ||
/// retrieved on the Main thread. This means the service can be constructed and subscribed to on a background thread. | ||
/// </remarks> | ||
[Export(typeof(IVSGitExt))] | ||
[PartCreationPolicy(CreationPolicy.Shared)] | ||
public class VSGitExt : IVSGitExt | ||
|
@@ -24,7 +29,6 @@ public class VSGitExt : IVSGitExt | |
readonly IGitHubServiceProvider serviceProvider; | ||
readonly IVSUIContext context; | ||
readonly ILocalRepositoryModelFactory repositoryFactory; | ||
readonly object refreshLock = new object(); | ||
|
||
IGitExt gitService; | ||
IReadOnlyList<ILocalRepositoryModel> activeRepositories; | ||
|
@@ -41,52 +45,75 @@ public VSGitExt(IGitHubServiceProvider serviceProvider, IVSUIContextFactory fact | |
this.repositoryFactory = repositoryFactory; | ||
|
||
// The IGitExt service isn't available when a TFS based solution is opened directly. | ||
// It will become available when moving to a Git based solution (cause a UIContext event to fire). | ||
// It will become available when moving to a Git based solution (and cause a UIContext event to fire). | ||
context = factory.GetUIContext(new Guid(Guids.GitSccProviderId)); | ||
|
||
// Start with empty array until we have a change to initialize. | ||
// Start with empty array until we have a chance to initialize. | ||
ActiveRepositories = Array.Empty<ILocalRepositoryModel>(); | ||
|
||
if (context.IsActive && TryInitialize()) | ||
PendingTasks = InitializeAsync(); | ||
} | ||
|
||
async Task InitializeAsync() | ||
{ | ||
try | ||
{ | ||
// Refresh ActiveRepositories on background thread so we don't delay startup. | ||
InitializeTask = Task.Run(() => RefreshActiveRepositories()); | ||
if (!context.IsActive || !await TryInitialize()) | ||
{ | ||
// If we're not in the UIContext or TryInitialize fails, have another go when the UIContext changes. | ||
context.UIContextChanged += ContextChanged; | ||
log.Debug("VSGitExt will be initialized later"); | ||
} | ||
} | ||
else | ||
catch (Exception e) | ||
{ | ||
// If we're not in the UIContext or TryInitialize fails, have another go when the UIContext changes. | ||
context.UIContextChanged += ContextChanged; | ||
log.Debug("VSGitExt will be initialized later"); | ||
InitializeTask = Task.CompletedTask; | ||
log.Error(e, "Initializing"); | ||
} | ||
} | ||
|
||
void ContextChanged(object sender, VSUIContextChangedEventArgs e) | ||
{ | ||
// If we're in the UIContext and TryInitialize succeeds, we can stop listening for events. | ||
// NOTE: this event can fire with UIContext=true in a TFS solution (not just Git). | ||
if (e.Activated && TryInitialize()) | ||
if (e.Activated) | ||
{ | ||
PendingTasks = ContextChangedAsync(); | ||
} | ||
} | ||
|
||
async Task ContextChangedAsync() | ||
{ | ||
try | ||
{ | ||
// If we're in the UIContext and TryInitialize succeeds, we can stop listening for events. | ||
// NOTE: this event can fire with UIContext=true in a TFS solution (not just Git). | ||
if (await TryInitialize()) | ||
{ | ||
context.UIContextChanged -= ContextChanged; | ||
log.Debug("Initialized VSGitExt on UIContextChanged"); | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
// Refresh ActiveRepositories on background thread so we don't delay UI context change. | ||
InitializeTask = Task.Run(() => RefreshActiveRepositories()); | ||
context.UIContextChanged -= ContextChanged; | ||
log.Debug("Initialized VSGitExt on UIContextChanged"); | ||
log.Error(e, "UIContextChanged"); | ||
} | ||
} | ||
|
||
bool TryInitialize() | ||
async Task<bool> TryInitialize() | ||
{ | ||
gitService = serviceProvider.GetService<IGitExt>(); | ||
gitService = await GetServiceAsync<IGitExt>(); | ||
if (gitService != null) | ||
{ | ||
gitService.PropertyChanged += (s, e) => | ||
{ | ||
if (e.PropertyName == nameof(gitService.ActiveRepositories)) | ||
{ | ||
RefreshActiveRepositories(); | ||
// Execute tasks in sequence using thread pool (TaskScheduler.Default). | ||
PendingTasks = PendingTasks.ContinueWith(_ => RefreshActiveRepositories(), TaskScheduler.Default); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there could still be a (very small chance of a) race here if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure the issue before was with It was called with ...and the I don't think we need to worry about the |
||
} | ||
}; | ||
|
||
// Do this after we start listening so we don't miss an event. | ||
await Task.Run(() => RefreshActiveRepositories()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line should be part of the initial |
||
|
||
log.Debug("Found IGitExt service and initialized VSGitExt"); | ||
return true; | ||
} | ||
|
@@ -95,19 +122,23 @@ bool TryInitialize() | |
return false; | ||
} | ||
|
||
async Task<T> GetServiceAsync<T>() where T : class | ||
{ | ||
// GetService must be called from the Main thread. | ||
await ThreadingHelper.SwitchToMainThreadAsync(); | ||
return serviceProvider.GetService<T>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way we're exposing MEF implementations as services is by having a MEF shim that redirects to the real instance instead, like we do for GitHubServiceProvider, UsageTracker (https://github.com/github/VisualStudio/blob/docs/clarify-tokens/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs and https://github.com/github/VisualStudio/blob/docs/clarify-tokens/src/GitHub.VisualStudio/Services/UsageTracker.cs) and registering the service in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll do something similar in this PR. I can avoid switching threads by using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've done as you've suggested in #1510. This PR is now obsolete. |
||
} | ||
|
||
void RefreshActiveRepositories() | ||
{ | ||
try | ||
{ | ||
lock (refreshLock) | ||
{ | ||
log.Debug( | ||
"IGitExt.ActiveRepositories (#{Id}) returned {Repositories}", | ||
gitService.GetHashCode(), | ||
gitService?.ActiveRepositories.Select(x => x.RepositoryPath)); | ||
log.Debug( | ||
"IGitExt.ActiveRepositories (#{Id}) returned {Repositories}", | ||
gitService.GetHashCode(), | ||
gitService?.ActiveRepositories.Select(x => x.RepositoryPath)); | ||
|
||
ActiveRepositories = gitService?.ActiveRepositories.Select(x => repositoryFactory.Create(x.RepositoryPath)).ToList(); | ||
} | ||
ActiveRepositories = gitService?.ActiveRepositories.Select(x => repositoryFactory.Create(x.RepositoryPath)).ToList(); | ||
} | ||
catch (Exception e) | ||
{ | ||
|
@@ -136,6 +167,9 @@ private set | |
|
||
public event Action ActiveRepositoriesChanged; | ||
|
||
public Task InitializeTask { get; private set; } | ||
/// <summary> | ||
/// Tasks that are pending execution on the thread pool. | ||
/// </summary> | ||
public Task PendingTasks { get; private set; } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So should we be on a b/g thread for this? Or are we now saying that it can be run on the UI thread too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be on a background or Main thread. We explicitly change to a background or Main thread when required.