Skip to content

Commit

Permalink
Merge pull request #1507 from savpek/feature/background-analysis
Browse files Browse the repository at this point in the history
Background analysis support
  • Loading branch information
david-driscoll authored Jul 10, 2019
2 parents 4de169b + 8acbd35 commit c2c6cfc
Show file tree
Hide file tree
Showing 24 changed files with 584 additions and 184 deletions.
2 changes: 2 additions & 0 deletions src/OmniSharp.Abstractions/Models/Events/EventTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ public static class EventTypes
public const string PackageRestoreFinished = nameof(PackageRestoreFinished);
public const string UnresolvedDependencies = nameof(UnresolvedDependencies);
public const string ProjectConfiguration = nameof(ProjectConfiguration);
public const string ProjectDiagnosticStatus = nameof(ProjectDiagnosticStatus);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace OmniSharp.Models.Events
{
public class ProjectDiagnosticStatusMessage
{
public ProjectDiagnosticStatus Status { get; set; }
public string ProjectFilePath { get; set; }
public string Type = "background";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace OmniSharp.Models.Events
{
public enum ProjectDiagnosticStatus
{
Started = 0,
Ready = 1
}
}
10 changes: 10 additions & 0 deletions src/OmniSharp.Abstractions/Models/v1/ReAnalyze/ReanalyzeRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using OmniSharp.Mef;
using OmniSharp.Models;

namespace OmniSharp.Abstractions.Models.V1.ReAnalyze
{
[OmniSharpEndpoint(OmniSharpEndpoints.ReAnalyze, typeof(ReAnalyzeRequest), typeof(ReanalyzeResponse))]
public class ReAnalyzeRequest: SimpleFileRequest
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using OmniSharp.Models;

namespace OmniSharp.Abstractions.Models.V1.ReAnalyze
{
public class ReanalyzeResponse : IAggregateResponse
{
public IAggregateResponse Merge(IAggregateResponse response) { return response; }
}
}
2 changes: 2 additions & 0 deletions src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public static class OmniSharpEndpoints
public const string Close = "/close";
public const string Diagnostics = "/diagnostics";

public const string ReAnalyze = "/reanalyze";

public static class V2
{
public const string GetCodeActions = "/v2/getcodeactions";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public Task<DiagnosticsResponse> Handle(DiagnosticsRequest request)
_forwarder.IsEnabled = true;
}

_diagWorker.QueueAllDocumentsForDiagnostics();
_diagWorker.QueueDocumentsForDiagnostics();

return Task.FromResult(new DiagnosticsResponse());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using OmniSharp.Abstractions.Models.V1.ReAnalyze;
using OmniSharp.Mef;
using OmniSharp.Roslyn.CSharp.Workers.Diagnostics;

namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics
{
[OmniSharpHandler(OmniSharpEndpoints.ReAnalyze, LanguageNames.CSharp)]
public class ReAnalyzeService : IRequestHandler<ReAnalyzeRequest, ReanalyzeResponse>
{
private readonly ICsDiagnosticWorker _diagWorker;
private readonly OmniSharpWorkspace _workspace;
private readonly ILogger<ReAnalyzeService> _logger;

[ImportingConstructor]
public ReAnalyzeService(ICsDiagnosticWorker diagWorker, OmniSharpWorkspace workspace, ILoggerFactory loggerFactory)
{
_diagWorker = diagWorker;
_workspace = workspace;
_logger = loggerFactory.CreateLogger<ReAnalyzeService>();
}

public Task<ReanalyzeResponse> Handle(ReAnalyzeRequest request)
{

if(!string.IsNullOrEmpty(request.FileName))
{
var currentSolution = _workspace.CurrentSolution;

var projectIds = WhenRequestIsProjectFileItselfGetFilesFromIt(request.FileName, currentSolution)
?? GetProjectIdsFromDocumentFilePaths(request.FileName, currentSolution);

_logger.LogInformation($"Queue analysis for project(s) {string.Join(", ", projectIds)}");

_diagWorker.QueueDocumentsForDiagnostics(projectIds);
}
else
{
_logger.LogInformation($"Queue analysis for all projects.");
_diagWorker.QueueDocumentsForDiagnostics();
}

return Task.FromResult(new ReanalyzeResponse());
}

private ImmutableArray<ProjectId>? WhenRequestIsProjectFileItselfGetFilesFromIt(string FileName, Solution currentSolution)
{
var projects = currentSolution.Projects.Where(x => CompareProjectPath(FileName, x)).Select(x => x.Id).ToImmutableArray();

if(!projects.Any())
return null;

return projects;
}

private static bool CompareProjectPath(string FileName, Project x)
{
return String.Compare(
x.FilePath,
FileName,
StringComparison.InvariantCultureIgnoreCase) == 0;
}

private static ImmutableArray<ProjectId> GetProjectIdsFromDocumentFilePaths(string FileName, Solution currentSolution)
{
return currentSolution
.GetDocumentIdsWithFilePath(FileName)
.Select(docId => currentSolution.GetDocument(docId).Project.Id)
.Distinct()
.ToImmutableArray();
}
}
}
131 changes: 74 additions & 57 deletions src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
Expand All @@ -12,98 +10,117 @@ namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics
{
public class AnalyzerWorkQueue
{
private readonly int _throttlingMs = 300;
private class Queue
{
public Queue(TimeSpan throttling)
{
Throttling = throttling;
}

private readonly ConcurrentDictionary<DocumentId, (DateTime modified, CancellationTokenSource workDoneSource)> _workQueue =
new ConcurrentDictionary<DocumentId, (DateTime modified, CancellationTokenSource workDoneSource)>();
public ImmutableHashSet<DocumentId> WorkWaitingToExecute { get; set; } = ImmutableHashSet<DocumentId>.Empty;
public ImmutableHashSet<DocumentId> WorkExecuting { get; set; } = ImmutableHashSet<DocumentId>.Empty;
public DateTime LastThrottlingBegan { get; set; } = DateTime.UtcNow;
public TimeSpan Throttling { get; }
public CancellationTokenSource WorkPendingToken { get; set; }
}

private readonly ConcurrentDictionary<DocumentId, (DateTime modified, CancellationTokenSource workDoneSource)> _currentWork =
new ConcurrentDictionary<DocumentId, (DateTime modified, CancellationTokenSource workDoneSource)>();
private readonly Dictionary<AnalyzerWorkType, Queue> _queues = null;

private readonly ILogger<AnalyzerWorkQueue> _logger;
private readonly Func<DateTime> _utcNow;
private readonly int _maximumDelayWhenWaitingForResults;
private readonly ILogger<AnalyzerWorkQueue> _logger;
private readonly object _queueLock = new object();

public AnalyzerWorkQueue(ILoggerFactory loggerFactory, Func<DateTime> utcNow = null, int timeoutForPendingWorkMs = 15*1000)
public AnalyzerWorkQueue(ILoggerFactory loggerFactory, int timeoutForPendingWorkMs, Func<DateTime> utcNow = null)
{
utcNow = utcNow ?? (() => DateTime.UtcNow);
_queues = new Dictionary<AnalyzerWorkType, Queue>
{
{ AnalyzerWorkType.Foreground, new Queue(TimeSpan.FromMilliseconds(150)) },
{ AnalyzerWorkType.Background, new Queue(TimeSpan.FromMilliseconds(1500)) }
};

_logger = loggerFactory.CreateLogger<AnalyzerWorkQueue>();
_utcNow = utcNow;
_utcNow = utcNow ?? (() => DateTime.UtcNow);
_maximumDelayWhenWaitingForResults = timeoutForPendingWorkMs;
}

public void PutWork(DocumentId documentId)
public void PutWork(IReadOnlyCollection<DocumentId> documentIds, AnalyzerWorkType workType)
{
_workQueue.AddOrUpdate(documentId,
(modified: DateTime.UtcNow, new CancellationTokenSource()),
(_, oldValue) => (modified: DateTime.UtcNow, oldValue.workDoneSource));
lock (_queueLock)
{
var queue = _queues[workType];

if (queue.WorkWaitingToExecute.IsEmpty)
queue.LastThrottlingBegan = _utcNow();

if (queue.WorkPendingToken == null)
queue.WorkPendingToken = new CancellationTokenSource();

queue.WorkWaitingToExecute = queue.WorkWaitingToExecute.Union(documentIds);
}
}

public ImmutableArray<DocumentId> TakeWork()
public IReadOnlyCollection<DocumentId> TakeWork(AnalyzerWorkType workType)
{
lock (_workQueue)
lock (_queueLock)
{
var now = _utcNow();
var currentWork = _workQueue
.Where(x => ThrottlingPeriodNotActive(x.Value.modified, now))
.OrderByDescending(x => x.Value.modified)
.Take(50)
.ToImmutableArray();

foreach (var work in currentWork)
{
_workQueue.TryRemove(work.Key, out _);
_currentWork.TryAdd(work.Key, work.Value);
}

return currentWork.Select(x => x.Key).ToImmutableArray();
var queue = _queues[workType];

if (IsThrottlingActive(queue) || queue.WorkWaitingToExecute.IsEmpty)
return ImmutableHashSet<DocumentId>.Empty;

queue.WorkExecuting = queue.WorkWaitingToExecute;
queue.WorkWaitingToExecute = ImmutableHashSet<DocumentId>.Empty;
return queue.WorkExecuting;
}
}

private bool ThrottlingPeriodNotActive(DateTime modified, DateTime now)
private bool IsThrottlingActive(Queue queue)
{
return (now - modified).TotalMilliseconds >= _throttlingMs;
return (_utcNow() - queue.LastThrottlingBegan).TotalMilliseconds <= queue.Throttling.TotalMilliseconds;
}

public void MarkWorkAsCompleteForDocumentId(DocumentId documentId)
public void WorkComplete(AnalyzerWorkType workType)
{
if(_currentWork.TryGetValue(documentId, out var work))
lock (_queueLock)
{
work.workDoneSource.Cancel();
_currentWork.TryRemove(documentId, out _);
if(_queues[workType].WorkExecuting.IsEmpty)
return;

_queues[workType].WorkPendingToken?.Cancel();
_queues[workType].WorkPendingToken = null;
_queues[workType].WorkExecuting = ImmutableHashSet<DocumentId>.Empty;
}
}

// Omnisharp V2 api expects that it can request current information of diagnostics any time,
// Omnisharp V2 api expects that it can request current information of diagnostics any time (single file/current document),
// however analysis is worker based and is eventually ready. This method is used to make api look
// like it's syncronous even that actual analysis may take a while.
public async Task WaitForResultsAsync(ImmutableArray<DocumentId> documentIds)
public Task WaitForegroundWorkComplete()
{
var items = new List<(DateTime modified, CancellationTokenSource workDoneSource)>();
var queue = _queues[AnalyzerWorkType.Foreground];

foreach (var documentId in documentIds)
if (queue.WorkPendingToken == null || (queue.WorkPendingToken == null && queue.WorkWaitingToExecute.IsEmpty))
return Task.CompletedTask;

return Task.Delay(_maximumDelayWhenWaitingForResults, queue.WorkPendingToken.Token)
.ContinueWith(task => LogTimeouts(task));
}

public bool TryPromote(DocumentId id)
{
if (_queues[AnalyzerWorkType.Background].WorkWaitingToExecute.Contains(id) || _queues[AnalyzerWorkType.Background].WorkExecuting.Contains(id))
{
if (_currentWork.ContainsKey(documentId))
{
items.Add(_currentWork[documentId]);
}
else if (_workQueue.ContainsKey(documentId))
{
items.Add(_workQueue[documentId]);
}
PutWork(new[] { id }, AnalyzerWorkType.Foreground);
return true;
}

await Task.WhenAll(items.Select(item =>
Task.Delay(_maximumDelayWhenWaitingForResults, item.workDoneSource.Token)
.ContinueWith(task => LogTimeouts(task, documentIds))));
return false;
}

// This logs wait's for documentId diagnostics that continue without getting current version from analyzer.
// This happens on larger solutions during initial load or situations where analysis slows down remarkably.
private void LogTimeouts(Task task, IEnumerable<DocumentId> documentIds)
private void LogTimeouts(Task task)
{
if (!task.IsCanceled) _logger.LogDebug($"Timeout before work got ready for one of documents {string.Join(",", documentIds)}.");
if (!task.IsCanceled) _logger.LogWarning($"Timeout before work got ready for foreground analysis queue. This is assertion to prevent complete api hang in case of error.");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics
{
public enum AnalyzerWorkType
{
Background, Foreground
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,16 @@ private static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsForDocument(
}
}

public ImmutableArray<DocumentId> QueueAllDocumentsForDiagnostics()
public ImmutableArray<DocumentId> QueueDocumentsForDiagnostics()
{
var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).ToImmutableArray();
var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents);
QueueForDiagnosis(documents.Select(x => x.FilePath).ToImmutableArray());
return documents.Select(x => x.Id).ToImmutableArray();
}

public ImmutableArray<DocumentId> QueueDocumentsForDiagnostics(ImmutableArray<ProjectId> projectIds)
{
var documents = projectIds.SelectMany(projectId => _workspace.CurrentSolution.GetProject(projectId).Documents);
QueueForDiagnosis(documents.Select(x => x.FilePath).ToImmutableArray());
return documents.Select(x => x.Id).ToImmutableArray();
}
Expand Down
Loading

0 comments on commit c2c6cfc

Please sign in to comment.