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(), + cancellationToken).ConfigureAwait(false); + } + + /// + /// Callback from the OOP service back into us. + /// + public Task RegisterProjectTelemetryInfoAsync(ProjectTelemetryInfo info, CancellationToken cancellationToken) + { + _workQueue.AddWork(info); + return Task.CompletedTask; + } + + private async Task NotifyTelemetryServiceAsync( + ImmutableArray infos, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + using var _1 = ArrayBuilder.GetInstance(out var filteredInfos); + AddFilteredInfos(infos, filteredInfos); + + using var _2 = ArrayBuilder.GetInstance(out var tasks); + foreach (var info in filteredInfos) + tasks.Add(Task.Run(() => NotifyTelemetryService(info), cancellationToken)); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + private void AddFilteredInfos(ImmutableArray infos, ArrayBuilder filteredInfos) + { + using var _ = PooledHashSet.GetInstance(out var seenProjectIds); + + // Walk the list of telemetry 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--) + { + var info = infos[i]; + if (seenProjectIds.Add(info.ProjectId)) + filteredInfos.Add(info); + } + } + + private void NotifyTelemetryService(ProjectTelemetryInfo info) + { + try + { + var telemetryEvent = TelemetryHelper.TelemetryService.CreateEvent(TelemetryEventPath); + telemetryEvent.SetStringProperty(TelemetryProjectIdName, info.ProjectId.Id.ToString()); + telemetryEvent.SetStringProperty(TelemetryProjectGuidName, Guid.Empty.ToString()); + telemetryEvent.SetStringProperty(TelemetryLanguageName, info.Language); + telemetryEvent.SetIntProperty(TelemetryAnalyzerReferencesCountName, info.AnalyzerReferencesCount); + telemetryEvent.SetIntProperty(TelemetryProjectReferencesCountName, info.ProjectReferencesCount); + telemetryEvent.SetIntProperty(TelemetryMetadataReferencesCountName, info.MetadataReferencesCount); + telemetryEvent.SetIntProperty(TelemetryDocumentsCountName, info.DocumentsCount); + telemetryEvent.SetIntProperty(TelemetryAdditionalDocumentsCountName, info.AdditionalDocumentsCount); + + TelemetryHelper.DefaultTelemetrySession.PostEvent(telemetryEvent); + } + catch (Exception e) + { + // The telemetry service itself can throw. + // So, to be very careful, put this in a try/catch too. + try + { + var exceptionEvent = TelemetryHelper.TelemetryService.CreateEvent(TelemetryExceptionEventPath); + exceptionEvent.SetStringProperty("Type", e.GetTypeDisplayName()); + exceptionEvent.SetStringProperty("Message", e.Message); + exceptionEvent.SetStringProperty("StackTrace", e.StackTrace); + TelemetryHelper.DefaultTelemetrySession.PostEvent(exceptionEvent); + } + catch + { + } + } + } + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/VisualStudioProjectTelemetryServiceFactory.cs b/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/VisualStudioProjectTelemetryServiceFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..ff833a63c629040fc0d5e45e70887dcc914864d1 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/ProjectTelemetry/VisualStudioProjectTelemetryServiceFactory.cs @@ -0,0 +1,34 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectTelemetry +{ + [ExportWorkspaceServiceFactory(typeof(IProjectTelemetryService), ServiceLayer.Host), Shared] + internal class VisualStudioProjectTelemetryServiceFactory : IWorkspaceServiceFactory + { + private readonly IThreadingContext _threadingContext; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public VisualStudioProjectTelemetryServiceFactory(IThreadingContext threadingContext) + => _threadingContext = threadingContext; + + public IWorkspaceService? CreateService(HostWorkspaceServices workspaceServices) + { + if (!(workspaceServices.Workspace is VisualStudioWorkspaceImpl workspace)) + return null; + + return new VisualStudioProjectTelemetryService(workspace, _threadingContext); + } + } +} diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 15bd2c23d8cd832832911df1cdd114563df01cf6..ee982e6eaf53c37851ab885c91b8206997776215 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -4,7 +4,6 @@ using System; using System.ComponentModel.Design; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -28,6 +27,7 @@ using Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.RuleSets; +using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectTelemetry; using Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource; using Microsoft.VisualStudio.LanguageServices.Telemetry; using Microsoft.VisualStudio.PlatformUI; @@ -158,8 +158,12 @@ private async Task LoadComponentsBackgroundAsync(CancellationToken cancellationT // Load the designer attribute service and tell it to start watching the solution for // designable files. - var designerAttributeService = _workspace.Services.GetService(); + var designerAttributeService = _workspace.Services.GetRequiredService(); designerAttributeService.Start(this.DisposalToken); + + // Load the telemetry service and tell it to start watching the solution for project info. + var projectTelemetryService = _workspace.Services.GetRequiredService(); + projectTelemetryService.Start(this.DisposalToken); } private async Task LoadInteractiveMenusAsync(CancellationToken cancellationToken) diff --git a/src/VisualStudio/Core/Def/Telemetry/ProjectTelemetryIncrementalAnalyzerProvider.cs b/src/VisualStudio/Core/Def/Telemetry/ProjectTelemetryIncrementalAnalyzerProvider.cs deleted file mode 100644 index f2fc630a63edf6d26a7ad0f5f84f690c29f205b9..0000000000000000000000000000000000000000 --- a/src/VisualStudio/Core/Def/Telemetry/ProjectTelemetryIncrementalAnalyzerProvider.cs +++ /dev/null @@ -1,246 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.SolutionCrawler; -using Microsoft.Internal.VisualStudio.Shell; -using Microsoft.Internal.VisualStudio.Shell.Interop; -using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; - -namespace Microsoft.VisualStudio.LanguageServices.Telemetry -{ - /// - /// Creates an that collects basic information on inputs - /// and reports it to the . - /// - /// - /// This includes data such an source file counts, project, metadata, and analyzer reference counts, and so on. - /// - [ExportIncrementalAnalyzerProvider(nameof(ProjectTelemetryIncrementalAnalyzerProvider), new[] { WorkspaceKind.Host }), Shared] - internal sealed class ProjectTelemetryIncrementalAnalyzerProvider : IIncrementalAnalyzerProvider - { - [ImportingConstructor] - public ProjectTelemetryIncrementalAnalyzerProvider() - { - } - - public IIncrementalAnalyzer CreateIncrementalAnalyzer(Microsoft.CodeAnalysis.Workspace workspace) - { - return new Analyzer(); - } - - private sealed class Analyzer : IIncrementalAnalyzer - { - /// - /// For a given , stores the most recent set of data reported to the - /// telemetry service. - /// - private sealed class Cache - { - private class Inputs - { - public string Language; - public int AnalyzerReferencesCount; - public int ProjectReferencesCount; - public int MetadataReferencesCount; - public int DocumentsCount; - public int AdditionalDocumentsCount; - - public Inputs(string language, int analyzerReferencesCount, int projectReferencesCount, int metadataReferencesCount, int documentsCount, int additionalDocumentsCount) - { - this.Language = language; - this.AnalyzerReferencesCount = analyzerReferencesCount; - this.ProjectReferencesCount = projectReferencesCount; - this.MetadataReferencesCount = metadataReferencesCount; - this.DocumentsCount = documentsCount; - this.AdditionalDocumentsCount = additionalDocumentsCount; - } - - public bool Equals(Inputs other) - { - return this.Language.Equals(other.Language) && - this.AnalyzerReferencesCount == other.AnalyzerReferencesCount && - this.ProjectReferencesCount == other.ProjectReferencesCount && - this.MetadataReferencesCount == other.MetadataReferencesCount && - this.DocumentsCount == other.DocumentsCount && - this.AdditionalDocumentsCount == other.AdditionalDocumentsCount; - } - } - - private readonly object _lockObject = new object(); - - private readonly Dictionary _items = new Dictionary(); - - /// - /// Adds or updates the data for the indicated by . - /// - /// - /// True if the data was added or updated, false if the data matches what is already in the cache. - /// - public bool TryAddOrUpdate(ProjectId projectId, string language, int analyzerReferenceCount, int projectReferencesCount, int metadataReferencesCount, int documentsCount, int additionalDocumentsCount) - { - lock (_lockObject) - { - var newInputs = new Inputs( - language, - analyzerReferenceCount, - projectReferencesCount, - metadataReferencesCount, - documentsCount, - additionalDocumentsCount); - if (!_items.TryGetValue(projectId, out var existingInputs) || - !existingInputs.Equals(newInputs)) - { - _items[projectId] = newInputs; - return true; - } - - return false; - } - } - - /// - /// Removes all data associated with . - /// - public void Remove(ProjectId projectId) - { - lock (_lockObject) - { - _items.Remove(projectId); - } - } - } - - 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 Cache _cache = new Cache(); - - public Task AnalyzeDocumentAsync(Document document, SyntaxNode bodyOpt, InvocationReasons reasons, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - /// - /// Collects data from and reports it to the telemetry service. - /// - /// - /// Only sends data to the telemetry service when one of the collected data points changes, - /// not necessarily every time this code is called. - /// - public Task AnalyzeProjectAsync(Project project, bool semanticsChanged, InvocationReasons reasons, CancellationToken cancellationToken) - { - if (!semanticsChanged) - { - return Task.CompletedTask; - } - - var projectId = project.Id; - var language = project.Language; - var analyzerReferencesCount = project.AnalyzerReferences.Count; - var projectReferencesCount = project.AllProjectReferences.Count; - var metadataReferencesCount = project.MetadataReferences.Count; - var documentsCount = project.DocumentIds.Count; - var additionalDocumentsCount = project.AdditionalDocumentIds.Count; - - if (_cache.TryAddOrUpdate(projectId, language, analyzerReferencesCount, projectReferencesCount, metadataReferencesCount, documentsCount, additionalDocumentsCount)) - { - try - { - var workspace = (VisualStudioWorkspaceImpl)project.Solution.Workspace; - - var telemetryEvent = TelemetryHelper.TelemetryService.CreateEvent(TelemetryEventPath); - telemetryEvent.SetStringProperty(TelemetryProjectIdName, projectId.Id.ToString()); - // TODO: reconnect project GUID - telemetryEvent.SetStringProperty(TelemetryProjectGuidName, Guid.Empty.ToString()); - telemetryEvent.SetStringProperty(TelemetryLanguageName, language); - telemetryEvent.SetIntProperty(TelemetryAnalyzerReferencesCountName, analyzerReferencesCount); - telemetryEvent.SetIntProperty(TelemetryProjectReferencesCountName, projectReferencesCount); - telemetryEvent.SetIntProperty(TelemetryMetadataReferencesCountName, metadataReferencesCount); - telemetryEvent.SetIntProperty(TelemetryDocumentsCountName, documentsCount); - telemetryEvent.SetIntProperty(TelemetryAdditionalDocumentsCountName, additionalDocumentsCount); - - TelemetryHelper.DefaultTelemetrySession.PostEvent(telemetryEvent); - } - catch (Exception e) - { - // The telemetry service itself can throw. - // So, to be very careful, put this in a try/catch too. - try - { - var exceptionEvent = TelemetryHelper.TelemetryService.CreateEvent(TelemetryExceptionEventPath); - exceptionEvent.SetStringProperty("Type", e.GetTypeDisplayName()); - exceptionEvent.SetStringProperty("Message", e.Message); - exceptionEvent.SetStringProperty("StackTrace", e.StackTrace); - TelemetryHelper.DefaultTelemetrySession.PostEvent(exceptionEvent); - } - catch - { - } - } - } - - return Task.CompletedTask; - } - - public Task AnalyzeSyntaxAsync(Document document, InvocationReasons reasons, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public Task DocumentOpenAsync(Document document, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public Task DocumentCloseAsync(Document document, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public Task DocumentResetAsync(Document document, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public bool NeedsReanalysisOnOptionChanged(object sender, OptionChangedEventArgs e) - { - return false; - } - - public Task NewSolutionSnapshotAsync(Solution solution, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public void RemoveDocument(DocumentId documentId) - { - } - - public void RemoveProject(ProjectId projectId) - { - _cache.Remove(projectId); - } - } - } -} diff --git a/src/VisualStudio/Setup/source.extension.vsixmanifest b/src/VisualStudio/Setup/source.extension.vsixmanifest index c25aecee1b04257cf36c55a7f47b30ed7a869f4d..97724ee30e46043f2c663ddd5faebae7d3fa73f5 100644 --- a/src/VisualStudio/Setup/source.extension.vsixmanifest +++ b/src/VisualStudio/Setup/source.extension.vsixmanifest @@ -35,12 +35,14 @@ + + diff --git a/src/Workspaces/Core/Portable/ProjectTelemetry/IProjectTelemetryServiceCallback.cs b/src/Workspaces/Core/Portable/ProjectTelemetry/IProjectTelemetryServiceCallback.cs new file mode 100644 index 0000000000000000000000000000000000000000..e5f0ded5e838c5670c9ea35b69c24a0f5910a359 --- /dev/null +++ b/src/Workspaces/Core/Portable/ProjectTelemetry/IProjectTelemetryServiceCallback.cs @@ -0,0 +1,21 @@ +// 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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.ProjectTelemetry +{ + /// + /// Callback the host (VS) passes to the OOP service to allow it to send batch notifications + /// about telemetry. + /// + internal interface IProjectTelemetryServiceCallback + { + Task RegisterProjectTelemetryInfoAsync(ProjectTelemetryInfo infos, CancellationToken cancellationToken); + } +} diff --git a/src/Workspaces/Core/Portable/ProjectTelemetry/IRemoteProjectTelemetryService.cs b/src/Workspaces/Core/Portable/ProjectTelemetry/IRemoteProjectTelemetryService.cs new file mode 100644 index 0000000000000000000000000000000000000000..2a16ef3cbedeead0dcd5be1d0a5c6b79e274c8cd --- /dev/null +++ b/src/Workspaces/Core/Portable/ProjectTelemetry/IRemoteProjectTelemetryService.cs @@ -0,0 +1,20 @@ +// 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 System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.ProjectTelemetry +{ + /// + /// Interface to allow host (VS) to inform the OOP service to start incrementally analyzing and + /// reporting results back to the host. + /// + internal interface IRemoteProjectTelemetryService + { + Task ComputeProjectTelemetryAsync(CancellationToken cancellation); + } +} diff --git a/src/Workspaces/Core/Portable/ProjectTelemetry/ProjectTelemetryInfo.cs b/src/Workspaces/Core/Portable/ProjectTelemetry/ProjectTelemetryInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf09536f222b0cf9e19b91356c808250ad26c62b --- /dev/null +++ b/src/Workspaces/Core/Portable/ProjectTelemetry/ProjectTelemetryInfo.cs @@ -0,0 +1,30 @@ +// 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 + +namespace Microsoft.CodeAnalysis.ProjectTelemetry +{ + /// + /// Serialization typed used to pass information to/from OOP and VS. + /// + internal struct ProjectTelemetryInfo + { + public ProjectId ProjectId; + public string Language; + public int AnalyzerReferencesCount; + public int ProjectReferencesCount; + public int MetadataReferencesCount; + public int DocumentsCount; + public int AdditionalDocumentsCount; + + public bool Equals(ProjectTelemetryInfo other) + => Language.Equals(other.Language) && + AnalyzerReferencesCount == other.AnalyzerReferencesCount && + ProjectReferencesCount == other.ProjectReferencesCount && + MetadataReferencesCount == other.MetadataReferencesCount && + DocumentsCount == other.DocumentsCount && + AdditionalDocumentsCount == other.AdditionalDocumentsCount; + } +} diff --git a/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs b/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs index 699c420025214f2f085b7892afbad11f4709da76..31375db652d9a1745da2806d7042bb52d33ab0d7 100644 --- a/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs +++ b/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs @@ -13,6 +13,7 @@ public static void Set64bit(bool x64) SnapshotService = "roslynSnapshot" + bit; CodeAnalysisService = "roslynCodeAnalysis" + bit; RemoteDesignerAttributeService = "roslynRemoteDesignerAttributeService" + bit; + RemoteProjectTelemetryService = "roslynRemoteProjectTelemetryService" + bit; RemoteSymbolSearchUpdateEngine = "roslynRemoteSymbolSearchUpdateEngine" + bit; LanguageServer = "roslynLanguageServer" + bit; } @@ -21,6 +22,7 @@ public static void Set64bit(bool x64) public static string CodeAnalysisService { get; private set; } = "roslynCodeAnalysis"; public static string RemoteSymbolSearchUpdateEngine { get; private set; } = "roslynRemoteSymbolSearchUpdateEngine"; public static string RemoteDesignerAttributeService { get; private set; } = "roslynRemoteDesignerAttributeService"; + public static string RemoteProjectTelemetryService { get; private set; } = "roslynRemoteProjectTelemetryService"; public static string LanguageServer { get; private set; } = "roslynLanguageServer"; // these are OOP implementation itself should care. not features that consume OOP care diff --git a/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryIncrementalAnalyzer.cs b/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryIncrementalAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..5a6e819901afb191b2b504d27f65216d9b280222 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryIncrementalAnalyzer.cs @@ -0,0 +1,85 @@ +// 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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ProjectTelemetry; +using Microsoft.CodeAnalysis.SolutionCrawler; + +namespace Microsoft.CodeAnalysis.Remote +{ + internal class RemoteProjectTelemetryIncrementalAnalyzer : IncrementalAnalyzerBase + { + /// + /// Channel back to VS to inform it of the designer attributes we discover. + /// + private readonly RemoteEndPoint _endPoint; + + private readonly object _gate = new object(); + private readonly Dictionary _projectToInfo = new Dictionary(); + + public RemoteProjectTelemetryIncrementalAnalyzer(RemoteEndPoint endPoint) + => _endPoint = endPoint; + + /// + /// Collects data from and reports it to the telemetry service. + /// + /// + /// Only sends data to the telemetry service when one of the collected data points changes, + /// not necessarily every time this code is called. + /// + public override async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, InvocationReasons reasons, CancellationToken cancellationToken) + { + if (!semanticsChanged) + return; + + var projectId = project.Id; + var language = project.Language; + var analyzerReferencesCount = project.AnalyzerReferences.Count; + var projectReferencesCount = project.AllProjectReferences.Count; + var metadataReferencesCount = project.MetadataReferences.Count; + var documentsCount = project.DocumentIds.Count; + var additionalDocumentsCount = project.AdditionalDocumentIds.Count; + + var info = new ProjectTelemetryInfo + { + ProjectId = projectId, + Language = language, + AnalyzerReferencesCount = analyzerReferencesCount, + ProjectReferencesCount = projectReferencesCount, + MetadataReferencesCount = metadataReferencesCount, + DocumentsCount = documentsCount, + AdditionalDocumentsCount = additionalDocumentsCount, + }; + + lock (_gate) + { + if (_projectToInfo.TryGetValue(projectId, out var existingInfo) && + existingInfo.Equals(info)) + { + // already have reported this. No need to notify VS. + return; + } + + _projectToInfo[projectId] = info; + } + + await _endPoint.InvokeAsync( + nameof(IProjectTelemetryServiceCallback.RegisterProjectTelemetryInfoAsync), + new object[] { info }, + cancellationToken).ConfigureAwait(false); + } + + public override void RemoveProject(ProjectId projectId) + { + lock (_gate) + { + _projectToInfo.Remove(projectId); + } + } + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryIncrementalAnalyzerProvider.cs b/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryIncrementalAnalyzerProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..6af5bfe3de0078206acbffd3909e96aeb4ae332b --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryIncrementalAnalyzerProvider.cs @@ -0,0 +1,28 @@ +// 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 Microsoft.CodeAnalysis.SolutionCrawler; + +namespace Microsoft.CodeAnalysis.Remote +{ + /// Note: this is explicitly not exported. We don't want the to automatically load this. Instead, VS waits until it is ready + /// and then calls into OOP to tell it to start analyzing the solution. At that point we'll get + /// created and added to the solution crawler. + /// + internal class RemoteProjectTelemetryIncrementalAnalyzerProvider : IIncrementalAnalyzerProvider + { + private readonly RemoteEndPoint _endPoint; + + public RemoteProjectTelemetryIncrementalAnalyzerProvider(RemoteEndPoint endPoint) + { + _endPoint = endPoint; + } + + public IIncrementalAnalyzer CreateIncrementalAnalyzer(Workspace workspace) + => new RemoteProjectTelemetryIncrementalAnalyzer(_endPoint); + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryService.cs b/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryService.cs new file mode 100644 index 0000000000000000000000000000000000000000..cb67bfbbbc2a342b9e5edccfbc6a2ea788ef4be7 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/ProjectTelemetry/RemoteProjectTelemetryService.cs @@ -0,0 +1,45 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ProjectTelemetry; +using Microsoft.CodeAnalysis.SolutionCrawler; + +namespace Microsoft.CodeAnalysis.Remote +{ + internal partial class RemoteProjectTelemetryService : ServiceBase, IRemoteProjectTelemetryService + { + public RemoteProjectTelemetryService( + Stream stream, IServiceProvider serviceProvider) + : base(serviceProvider, stream) + { + StartService(); + } + + public Task ComputeProjectTelemetryAsync(CancellationToken cancellation) + { + return RunServiceAsync(() => + { + var workspace = SolutionService.PrimaryWorkspace; + var endpoint = this.EndPoint; + var registrationService = workspace.Services.GetRequiredService(); + var analyzerProvider = new RemoteProjectTelemetryIncrementalAnalyzerProvider(endpoint); + + registrationService.AddAnalyzerProvider( + analyzerProvider, + new IncrementalAnalyzerProviderMetadata( + nameof(RemoteProjectTelemetryIncrementalAnalyzerProvider), + highPriorityForActiveFile: false, + workspaceKinds: WorkspaceKind.RemoteWorkspace)); + + return Task.CompletedTask; + }, cancellation); + } + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 8a4cf845d2f9801dbdfd100b86486d3bb44a54d0..e152a5198f3af9cd456de70f413e4f34bdc195cb 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -318,6 +318,7 @@ + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncBatchingWorkQueue.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncBatchingWorkQueue.cs new file mode 100644 index 0000000000000000000000000000000000000000..243deadbc994cc2df5fa21404df2e92660c4d263 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncBatchingWorkQueue.cs @@ -0,0 +1,132 @@ +// 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.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Roslyn.Utilities +{ + /// + /// A queue where items can be added to to be processed in batches after some delay has passed. + /// When processing happens, all the items added since the last processing point will be passed + /// along to be worked on. Rounds of processing happen serially, only starting up after a + /// previous round has completed. + /// + internal class AsyncBatchingWorkQueue + { + /// + /// Delay we wait after finishing the processing of one batch and starting up on then. + /// + private readonly TimeSpan _delay; + + /// + /// Callback to actually perform the processing of the next batch of work. + /// + private readonly Func, CancellationToken, Task> _processBatchAsync; + private readonly CancellationToken _cancellationToken; + + #region protected by lock + + /// + /// Lock we will use to ensure the remainder of these fields can be accessed in a threadsafe + /// manner. When work is added we'll place the data 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 added that we want to process in our next update task. + /// + private readonly List _nextBatch = 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 work before it runs. + /// + private bool _taskInFlight = false; + + #endregion + + public AsyncBatchingWorkQueue( + TimeSpan delay, + Func, CancellationToken, Task> processBatchAsync, + CancellationToken cancellationToken) + { + _delay = delay; + _processBatchAsync = processBatchAsync; + _cancellationToken = cancellationToken; + } + + public void AddWork(TItem item) + { + using var _ = ArrayBuilder.GetInstance(out var items); + items.Add(item); + + AddWork(items); + } + + public void AddWork(IEnumerable items) + { + // Don't do any more work if we've been asked to shutdown. + if (_cancellationToken.IsCancellationRequested) + return; + + lock (_gate) + { + // add our work to the set we'll process in the next batch. + _nextBatch.AddRange(items); + + 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( + _ => ProcessNextBatchAsync(_cancellationToken), + _cancellationToken, + (int)_delay.TotalMilliseconds, + TaskContinuationOptions.RunContinuationsAsynchronously, + TaskScheduler.Default); + _taskInFlight = true; + } + } + } + + private Task ProcessNextBatchAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return _processBatchAsync(GetNextBatchAndResetQueue(), _cancellationToken); + } + + private ImmutableArray GetNextBatchAndResetQueue() + { + lock (_gate) + { + var result = ArrayBuilder.GetInstance(); + result.AddRange(_nextBatch); + + // mark there being no existing update task so that the next OOP notification will + // kick one off. + _nextBatch.Clear(); + _taskInFlight = false; + + return result.ToImmutableAndFree(); + } + } + } +}