diff --git a/eng/targets/GenerateServiceHubConfigurationFiles.targets b/eng/targets/GenerateServiceHubConfigurationFiles.targets
index 536fe09eb7c46789acbb97344b93b9da4c1d8793..4564bb6af060772908d8bb7e6a7c4a7cc706f220 100644
--- a/eng/targets/GenerateServiceHubConfigurationFiles.targets
+++ b/eng/targets/GenerateServiceHubConfigurationFiles.targets
@@ -10,6 +10,7 @@
+
@@ -56,4 +57,4 @@
-
\ No newline at end of file
+
diff --git a/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs b/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs
index 38c451b98d913464a61e91a133ce271acc35cf69..ce585437fc36f740f57809fbc19427ce4e360412 100644
--- a/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs
+++ b/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs
@@ -168,6 +168,7 @@ public InProcRemoteServices(bool runCacheCleanup)
RegisterService(WellKnownServiceHubServices.SnapshotService, (s, p) => new SnapshotService(s, p));
RegisterService(WellKnownServiceHubServices.RemoteSymbolSearchUpdateEngine, (s, p) => new RemoteSymbolSearchUpdateEngine(s, p));
RegisterService(WellKnownServiceHubServices.RemoteDesignerAttributeService, (s, p) => new RemoteDesignerAttributeService(s, p));
+ RegisterService(WellKnownServiceHubServices.RemoteProjectTelemetryService, (s, p) => new RemoteProjectTelemetryService(s, p));
RegisterService(WellKnownServiceHubServices.LanguageServer, (s, p) => new LanguageServer(s, p));
}
diff --git a/src/VisualStudio/Core/Def/Implementation/DesignerAttribute/VisualStudioDesignerAttributeService.cs b/src/VisualStudio/Core/Def/Implementation/DesignerAttribute/VisualStudioDesignerAttributeService.cs
index 5ed84a9c9113fe4adf9043062448892aef002cc3..50a298bbaccb4ab26ad6ad1a80a5edaaac9a92b6 100644
--- a/src/VisualStudio/Core/Def/Implementation/DesignerAttribute/VisualStudioDesignerAttributeService.cs
+++ b/src/VisualStudio/Core/Def/Implementation/DesignerAttribute/VisualStudioDesignerAttributeService.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -63,35 +64,7 @@ internal class VisualStudioDesignerAttributeService
// We'll get notifications from the OOP server about new attribute arguments. Batch those
// notifications up and deliver them to VS every second.
- #region protected by lock
-
- ///
- /// Lock we will use to ensure the remainder of these fields can be accessed in a threadsafe
- /// manner. When OOP calls back into us, we'll place the data it produced into
- /// . We'll then kick of a task to process this in the future if
- /// we don't already have an existing task in flight for that.
- ///
- private readonly object _gate = new object();
-
- ///
- /// Data produced by OOP that we want to process in our next update task.
- ///
- private readonly List _updatedInfos = new List();
-
- ///
- /// Task kicked off to do the next batch of processing of . These
- /// tasks form a chain so that the next task only processes when the previous one completes.
- ///
- private Task _updateTask = Task.CompletedTask;
-
- ///
- /// Whether or not there is an existing task in flight that will process the current batch
- /// of . If there is an existing in flight task, we don't need
- /// to kick off a new one if we receive more notifications before it runs.
- ///
- private bool _taskInFlight = false;
-
- #endregion
+ private AsyncBatchingWorkQueue _workQueue = null!;
public VisualStudioDesignerAttributeService(
VisualStudioWorkspaceImpl workspace,
@@ -117,6 +90,11 @@ void IDesignerAttributeService.Start(CancellationToken cancellationToken)
private async Task StartAsync(CancellationToken cancellationToken)
{
+ _workQueue = new AsyncBatchingWorkQueue(
+ TimeSpan.FromSeconds(1),
+ this.NotifyProjectSystemAsync,
+ cancellationToken);
+
// Have to catch all exceptions coming through here as this is called from a
// fire-and-forget method and we want to make sure nothing leaks out.
try
@@ -159,42 +137,23 @@ private async Task StartWorkerAsync(CancellationToken cancellationToken)
///
/// Callback from the OOP service back into us.
///
- public Task RegisterDesignerAttributesAsync(
- IList attributeInfos, CancellationToken cancellationToken)
+ public Task RegisterDesignerAttributesAsync(IList attributeInfos, CancellationToken cancellationToken)
{
- lock (_gate)
- {
- // add our work to the set we'll process in the next batch.
- _updatedInfos.AddRange(attributeInfos);
-
- if (!_taskInFlight)
- {
- // No in-flight task. Kick one off to process these messages a second from now.
- // We always attach the task to the previous one so that notifications to the ui
- // follow the same order as the notification the OOP server sent to us.
- _updateTask = _updateTask.ContinueWithAfterDelayFromAsync(
- _ => NotifyProjectSystemAsync(cancellationToken),
- cancellationToken,
- 1000/*ms*/,
- TaskContinuationOptions.RunContinuationsAsynchronously,
- TaskScheduler.Default);
- _taskInFlight = true;
- }
- }
-
+ _workQueue.AddWork(attributeInfos);
return Task.CompletedTask;
}
- private async Task NotifyProjectSystemAsync(CancellationToken cancellationToken)
+ private async Task NotifyProjectSystemAsync(
+ ImmutableArray infos, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
- using var _1 = ArrayBuilder.GetInstance(out var attributeInfos);
- AddInfosAndResetQueue(attributeInfos);
+ using var _1 = ArrayBuilder.GetInstance(out var filteredInfos);
+ AddFilteredInfos(infos, filteredInfos);
// Now, group all the notifications by project and update all the projects in parallel.
using var _2 = ArrayBuilder.GetInstance(out var tasks);
- foreach (var group in attributeInfos.GroupBy(a => a.DocumentId.ProjectId))
+ foreach (var group in filteredInfos.GroupBy(a => a.DocumentId.ProjectId))
{
cancellationToken.ThrowIfCancellationRequested();
tasks.Add(NotifyProjectSystemAsync(group.Key, group, cancellationToken));
@@ -204,27 +163,18 @@ private async Task NotifyProjectSystemAsync(CancellationToken cancellationToken)
await Task.WhenAll(tasks).ConfigureAwait(false);
}
- private void AddInfosAndResetQueue(ArrayBuilder attributeInfos)
+ private void AddFilteredInfos(ImmutableArray infos, ArrayBuilder filteredInfos)
{
using var _ = PooledHashSet.GetInstance(out var seenDocumentIds);
- lock (_gate)
+ // Walk the list of designer items in reverse, and skip any items for a project once
+ // we've already seen it once. That way, we're only reporting the most up to date
+ // information for a project, and we're skipping the stale information.
+ for (var i = infos.Length - 1; i >= 0; i--)
{
- // walk the set of updates in reverse, and ignore documents if we see them a second
- // time. This ensures that if we're batching up multiple notifications for the same
- // document, that we only bother processing the last one since it should beat out
- // all the prior ones.
- for (var i = _updatedInfos.Count - 1; i >= 0; i--)
- {
- var designerArg = _updatedInfos[i];
- if (seenDocumentIds.Add(designerArg.DocumentId))
- attributeInfos.Add(designerArg);
- }
-
- // mark there being no existing update task so that the next OOP notification will
- // kick one off.
- _updatedInfos.Clear();
- _taskInFlight = false;
+ var info = infos[i];
+ if (seenDocumentIds.Add(info.DocumentId))
+ filteredInfos.Add(info);
}
}
diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/IProjectTelemetryService.cs b/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/IProjectTelemetryService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..1de07b8572b78031fe170dedd9b1f3ce7a8ef411
--- /dev/null
+++ b/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/IProjectTelemetryService.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System.Threading;
+using Microsoft.CodeAnalysis.Host;
+
+namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectTelemetry
+{
+ ///
+ /// In process service responsible for listening to OOP telemetry notifications.
+ ///
+ internal interface IProjectTelemetryService : IWorkspaceService
+ {
+ ///
+ /// Called by a host to let this service know that it should start background
+ /// analysis of the workspace to determine project telemetry.
+ ///
+ void Start(CancellationToken cancellationToken);
+ }
+}
diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/VisualStudioProjectTelemetryService.cs b/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/VisualStudioProjectTelemetryService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..92a39a2086498fb0aaa155580a7b6040896d8898
--- /dev/null
+++ b/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/VisualStudioProjectTelemetryService.cs
@@ -0,0 +1,180 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
+using Microsoft.CodeAnalysis.ErrorReporting;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Microsoft.CodeAnalysis.ProjectTelemetry;
+using Microsoft.CodeAnalysis.Remote;
+using Microsoft.CodeAnalysis.Shared.Extensions;
+using Microsoft.Internal.VisualStudio.Shell;
+using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
+using Roslyn.Utilities;
+
+namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectTelemetry
+{
+ internal class VisualStudioProjectTelemetryService
+ : ForegroundThreadAffinitizedObject, IProjectTelemetryService, IProjectTelemetryServiceCallback
+ {
+ private const string EventPrefix = "VS/Compilers/Compilation/";
+ private const string PropertyPrefix = "VS.Compilers.Compilation.Inputs.";
+
+ private const string TelemetryEventPath = EventPrefix + "Inputs";
+ private const string TelemetryExceptionEventPath = EventPrefix + "TelemetryUnhandledException";
+
+ private const string TelemetryProjectIdName = PropertyPrefix + "ProjectId";
+ private const string TelemetryProjectGuidName = PropertyPrefix + "ProjectGuid";
+ private const string TelemetryLanguageName = PropertyPrefix + "Language";
+ private const string TelemetryAnalyzerReferencesCountName = PropertyPrefix + "AnalyzerReferences.Count";
+ private const string TelemetryProjectReferencesCountName = PropertyPrefix + "ProjectReferences.Count";
+ private const string TelemetryMetadataReferencesCountName = PropertyPrefix + "MetadataReferences.Count";
+ private const string TelemetryDocumentsCountName = PropertyPrefix + "Documents.Count";
+ private const string TelemetryAdditionalDocumentsCountName = PropertyPrefix + "AdditionalDocuments.Count";
+
+ private readonly VisualStudioWorkspaceImpl _workspace;
+
+ ///
+ /// Our connections to the remote OOP server. Created on demand when we startup and then
+ /// kept around for the lifetime of this service.
+ ///
+ private KeepAliveSession? _keepAliveSession;
+
+ ///
+ /// Queue where we enqueue the information we get from OOP to process in batch in the future.
+ ///
+ private AsyncBatchingWorkQueue _workQueue = null!;
+
+ public VisualStudioProjectTelemetryService(VisualStudioWorkspaceImpl workspace, IThreadingContext threadingContext) : base(threadingContext)
+ => _workspace = workspace;
+
+ void IProjectTelemetryService.Start(CancellationToken cancellationToken)
+ => _ = StartAsync(cancellationToken);
+
+ private async Task StartAsync(CancellationToken cancellationToken)
+ {
+ // Have to catch all exceptions coming through here as this is called from a
+ // fire-and-forget method and we want to make sure nothing leaks out.
+ try
+ {
+ await StartWorkerAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Cancellation is normal (during VS closing). Just ignore.
+ }
+ catch (Exception e) when (FatalError.ReportWithoutCrash(e))
+ {
+ // Otherwise report a watson for any other exception. Don't bring down VS. This is
+ // a BG service we don't want impacting the user experience.
+ }
+ }
+
+ private async Task StartWorkerAsync(CancellationToken cancellationToken)
+ {
+ _workQueue = new AsyncBatchingWorkQueue(
+ TimeSpan.FromSeconds(1),
+ NotifyTelemetryServiceAsync,
+ cancellationToken);
+
+ var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false);
+ if (client == null)
+ return;
+
+ // Pass ourselves in as the callback target for the OOP service. As it discovers
+ // designer attributes it will call back into us to notify VS about it.
+ _keepAliveSession = await client.TryCreateKeepAliveSessionAsync(
+ WellKnownServiceHubServices.RemoteProjectTelemetryService,
+ callbackTarget: this, cancellationToken).ConfigureAwait(false);
+ if (_keepAliveSession == null)
+ return;
+
+ // Now kick off scanning in the OOP process.
+ var success = await _keepAliveSession.TryInvokeAsync(
+ nameof(IRemoteProjectTelemetryService.ComputeProjectTelemetryAsync),
+ solution: null,
+ arguments: Array.Empty