diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Telemetry/VsTelemetryService.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Telemetry/VsTelemetryService.cs index 67c457dd256..550ca4aa50f 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Telemetry/VsTelemetryService.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Telemetry/VsTelemetryService.cs @@ -38,7 +38,14 @@ public void PostProperties(string eventName, IEnumerable<(string propertyName, o var telemetryEvent = new TelemetryEvent(eventName); foreach ((string propertyName, object propertyValue) in properties) { - telemetryEvent.Properties.Add(propertyName, propertyValue); + if (propertyValue is ComplexPropertyValue complexProperty) + { + telemetryEvent.Properties.Add(propertyName, new TelemetryComplexProperty(complexProperty.Data)); + } + else + { + telemetryEvent.Properties.Add(propertyName, propertyValue); + } } PostTelemetryEvent(telemetryEvent); diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependenciesProjectTreeProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependenciesProjectTreeProvider.cs index 0eadbb1adf3..39d9741f23d 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependenciesProjectTreeProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependenciesProjectTreeProvider.cs @@ -377,10 +377,7 @@ async Task UpdateTreeAsync( { dependenciesNode = await viewProvider.BuildTreeAsync(dependenciesNode, snapshot, cancellationToken); - if (_treeTelemetryService.IsActive) - { - await _treeTelemetryService.ObserveTreeUpdateCompletedAsync(snapshot.MaximumVisibleDiagnosticLevel != DiagnosticLevel.None); - } + await _treeTelemetryService.ObserveTreeUpdateCompletedAsync(snapshot.MaximumVisibleDiagnosticLevel != DiagnosticLevel.None); } return new TreeUpdateResult(dependenciesNode); diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyTreeTelemetryService.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyTreeTelemetryService.cs index 5cd27acf2a6..563b3181404 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyTreeTelemetryService.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyTreeTelemetryService.cs @@ -4,8 +4,10 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies.Snapshot; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies @@ -22,13 +24,14 @@ namespace Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies /// [Export(typeof(IDependencyTreeTelemetryService))] [AppliesTo(ProjectCapability.DependenciesTree)] - internal sealed class DependencyTreeTelemetryService : IDependencyTreeTelemetryService + internal sealed class DependencyTreeTelemetryService : IDependencyTreeTelemetryService, IDisposable { private const int MaxEventCount = 10; private readonly UnconfiguredProject _project; private readonly ITelemetryService? _telemetryService; private readonly ISafeProjectGuidService _safeProjectGuidService; + private readonly Stopwatch _projectLoadTime = Stopwatch.StartNew(); private readonly object _stateUpdateLock = new object(); /// @@ -39,6 +42,7 @@ internal sealed class DependencyTreeTelemetryService : IDependencyTreeTelemetryS private string? _projectId; private int _eventCount; + private DependenciesSnapshot? _dependenciesSnapshot; [ImportingConstructor] public DependencyTreeTelemetryService( @@ -56,8 +60,6 @@ public DependencyTreeTelemetryService( } } - public bool IsActive => _stateByFramework != null; - public void InitializeTargetFrameworkRules(ImmutableArray targetFrameworks, IReadOnlyCollection rules) { if (_stateByFramework == null) @@ -103,11 +105,13 @@ public void ObserveTargetFrameworkRules(TargetFramework targetFramework, IEnumer } } - public async Task ObserveTreeUpdateCompletedAsync(bool hasUnresolvedDependency) + public async ValueTask ObserveTreeUpdateCompletedAsync(bool hasUnresolvedDependency) { if (_stateByFramework == null) return; + Assumes.NotNull(_telemetryService); + bool observedAllRules; lock (_stateUpdateLock) { @@ -126,7 +130,7 @@ public async Task ObserveTreeUpdateCompletedAsync(bool hasUnresolvedDependency) if (hasUnresolvedDependency) { - _telemetryService!.PostProperties(TelemetryEventName.TreeUpdatedUnresolved, new[] + _telemetryService.PostProperties(TelemetryEventName.TreeUpdatedUnresolved, new[] { (TelemetryPropertyName.TreeUpdatedUnresolvedProject, (object)_projectId), (TelemetryPropertyName.TreeUpdatedUnresolvedObservedAllRules, observedAllRules) @@ -134,7 +138,7 @@ public async Task ObserveTreeUpdateCompletedAsync(bool hasUnresolvedDependency) } else { - _telemetryService!.PostProperties(TelemetryEventName.TreeUpdatedResolved, new[] + _telemetryService.PostProperties(TelemetryEventName.TreeUpdatedResolved, new[] { (TelemetryPropertyName.TreeUpdatedResolvedProject, (object)_projectId), (TelemetryPropertyName.TreeUpdatedResolvedObservedAllRules, observedAllRules) @@ -153,11 +157,83 @@ async Task GetProjectIdAsync() } else { - return _telemetryService!.HashValue(_project.FullPath); + return _telemetryService.HashValue(_project.FullPath); } } } + public void ObserveSnapshot(DependenciesSnapshot dependenciesSnapshot) + { + _dependenciesSnapshot = dependenciesSnapshot; + } + + public void Dispose() + { + if (_telemetryService != null && _dependenciesSnapshot != null && _projectId != null) + { + var data = new Dictionary>(); + int totalDependencyCount = 0; + int unresolvedDependencyCount = 0; + + // Scan the snapshot and tally dependencies + foreach ((TargetFramework targetFramework, TargetedDependenciesSnapshot targetedSnapshot) in _dependenciesSnapshot.DependenciesByTargetFramework) + { + var countsByType = new Dictionary(); + + data[targetFramework.ShortName] = countsByType; + + foreach (IDependency dependency in targetedSnapshot.Dependencies) + { + // Only include visible dependencies in telemetry counts + if (!dependency.Visible) + { + continue; + } + + if (!countsByType.TryGetValue(dependency.ProviderType, out DependencyCount counts)) + { + counts = new DependencyCount(); + } + + countsByType[dependency.ProviderType] = counts.Add(dependency.Resolved); + + totalDependencyCount++; + + if (!dependency.Resolved) + { + unresolvedDependencyCount++; + } + } + } + + _telemetryService.PostProperties(TelemetryEventName.ProjectUnloadDependencies, new (string, object)[] + { + (TelemetryPropertyName.ProjectUnloadDependenciesProject, _projectId), + (TelemetryPropertyName.ProjectUnloadProjectAgeMillis, _projectLoadTime.ElapsedMilliseconds), + (TelemetryPropertyName.ProjectUnloadTotalDependencyCount, totalDependencyCount), + (TelemetryPropertyName.ProjectUnloadUnresolvedDependencyCount, unresolvedDependencyCount), + (TelemetryPropertyName.ProjectUnloadTargetFrameworkCount, _dependenciesSnapshot.DependenciesByTargetFramework.Count), + (TelemetryPropertyName.ProjectUnloadDependencyBreakdown, new ComplexPropertyValue(data)) + }); + } + } + + private readonly struct DependencyCount + { + public int TotalCount { get; } + public int UnresolvedCount { get; } + + public DependencyCount(int totalCount, int unresolvedCount) + { + TotalCount = totalCount; + UnresolvedCount = unresolvedCount; + } + + public DependencyCount Add(bool isResolved) => new DependencyCount( + TotalCount + 1, + isResolved ? UnresolvedCount : UnresolvedCount + 1); + } + /// /// Maintain state for a single target framework. /// diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/IDependencyTreeTelemetryService.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/IDependencyTreeTelemetryService.cs index 2df546ba233..46bdc47fbed 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/IDependencyTreeTelemetryService.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/IDependencyTreeTelemetryService.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Threading.Tasks; using Microsoft.VisualStudio.Composition; +using Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies.Snapshot; namespace Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies { @@ -13,13 +14,6 @@ namespace Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies [ProjectSystemContract(ProjectSystemContractScope.UnconfiguredProject, ProjectSystemContractProvider.Private, Cardinality = ImportCardinality.ExactlyOne)] internal interface IDependencyTreeTelemetryService { - /// - /// Gets a value indicating whether this telemetry service is active. - /// If not, then it will remain inactive and no methods need be called on it. - /// Note that an instance may become inactive during its lifetime. - /// - bool IsActive { get; } - /// /// Initialize telemetry state with the set of target frameworks and rules we expect to observe. /// @@ -36,6 +30,13 @@ internal interface IDependencyTreeTelemetryService /// Fire telemetry when dependency tree completes an update /// /// indicates if the snapshot used for the update had any unresolved dependencies - Task ObserveTreeUpdateCompletedAsync(bool hasUnresolvedDependency); + ValueTask ObserveTreeUpdateCompletedAsync(bool hasUnresolvedDependency); + + /// + /// Provides an updated dependency snapshot so that telemetry may be reported about the + /// state of the project's dependencies. + /// + /// The dependency snapshot. + void ObserveSnapshot(DependenciesSnapshot dependenciesSnapshot); } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.SnapshotUpdater.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.SnapshotUpdater.cs index 27c3c317651..e68d150ae9f 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.SnapshotUpdater.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.SnapshotUpdater.cs @@ -48,20 +48,23 @@ public SnapshotUpdater(IProjectThreadingService projectThreadingService, Cancell /// Executes on the current snapshot within a lock. If a new snapshot /// object is returned, is updated and the update is posted to . /// - public void TryUpdate(Func updateFunc, CancellationToken token = default) + /// The updated snapshot, or if no update occurred. + public DependenciesSnapshot? TryUpdate(Func updateFunc, CancellationToken token = default) { if (_isDisposed != 0) { throw new ObjectDisposedException(nameof(SnapshotUpdater)); } + DependenciesSnapshot updatedSnapshot; + lock (_lock) { - DependenciesSnapshot updatedSnapshot = updateFunc(_currentSnapshot); + updatedSnapshot = updateFunc(_currentSnapshot); if (ReferenceEquals(_currentSnapshot, updatedSnapshot)) { - return; + return null; } _currentSnapshot = updatedSnapshot; @@ -83,6 +86,8 @@ public void TryUpdate(Func updateFun return Task.CompletedTask; }, token); + + return updatedSnapshot; } public void Dispose() diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.cs index c27465c56bf..58a79f66587 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/DependenciesSnapshotProvider.cs @@ -39,6 +39,7 @@ internal sealed partial class DependenciesSnapshotProvider : OnceInitializedOnce private readonly IUnconfiguredProjectCommonServices _commonServices; private readonly IUnconfiguredProjectTasksService _tasksService; private readonly IActiveConfiguredProjectSubscriptionService _activeConfiguredProjectSubscriptionService; + private readonly IDependencyTreeTelemetryService _dependencyTreeTelemetryService; [ImportMany] private readonly OrderPrecedenceImportCollection _dependencySubscribers; [ImportMany] private readonly OrderPrecedenceImportCollection _subTreeProviders; @@ -70,13 +71,15 @@ public DependenciesSnapshotProvider( IUnconfiguredProjectTasksService tasksService, IActiveConfiguredProjectSubscriptionService activeConfiguredProjectSubscriptionService, IActiveProjectConfigurationRefreshService activeProjectConfigurationRefreshService, - ITargetFrameworkProvider targetFrameworkProvider) + ITargetFrameworkProvider targetFrameworkProvider, + IDependencyTreeTelemetryService dependencyTreeTelemetryService) : base(commonServices.ThreadingService.JoinableTaskContext) { _commonServices = commonServices; _tasksService = tasksService; _activeConfiguredProjectSubscriptionService = activeConfiguredProjectSubscriptionService; _targetFrameworkProvider = targetFrameworkProvider; + _dependencyTreeTelemetryService = dependencyTreeTelemetryService; _dependencySubscribers = new OrderPrecedenceImportCollection( projectCapabilityCheckProvider: commonServices.Project); @@ -276,7 +279,7 @@ private void UpdateDependenciesSnapshot( IImmutableSet? projectItemSpecs = GetProjectItemSpecs(catalogs?.Project?.ProjectInstance.Items); - _snapshot.TryUpdate( + DependenciesSnapshot? updatedSnapshot = _snapshot.TryUpdate( previousSnapshot => DependenciesSnapshot.FromChanges( previousSnapshot, changedTargetFramework, @@ -289,6 +292,11 @@ private void UpdateDependenciesSnapshot( projectItemSpecs), token); + if (updatedSnapshot != null) + { + _dependencyTreeTelemetryService.ObserveSnapshot(updatedSnapshot); + } + return; // Gets the set of items defined directly the project, and not included by imports. diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/ComplexPropertyValue.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/ComplexPropertyValue.cs new file mode 100644 index 00000000000..b732b962e98 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/ComplexPropertyValue.cs @@ -0,0 +1,18 @@ +// 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.md file in the project root for more information. + +namespace Microsoft.VisualStudio.Telemetry +{ + /// + /// Wrapper for complex (non-scalar) property values being reported + /// via . + /// + internal readonly struct ComplexPropertyValue + { + public object Data { get; } + + public ComplexPropertyValue(object data) + { + Data = data; + } + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryEventName.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryEventName.cs index ad6586d97ef..b64c9010ff6 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryEventName.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryEventName.cs @@ -38,20 +38,25 @@ internal static class TelemetryEventName public static readonly string DesignTimeBuildComplete = BuildEventName("DesignTimeBuildComplete"); /// - /// Indicates that .NET Core SDK version. + /// Indicates the .NET Core SDK version. /// public static readonly string SDKVersion = BuildEventName("SDKVersion"); /// - /// Indicates that the TempPE compilation queue has been processed + /// Indicates that the TempPE compilation queue has been processed. /// public static readonly string TempPEProcessQueue = BuildEventName("TempPE/ProcessCompileQueue"); /// - /// Indicates that the TempPE compilation has occurred on demand from a designer + /// Indicates that the TempPE compilation has occurred on demand from a designer. /// public static readonly string TempPECompileOnDemand = BuildEventName("TempPE/CompileOnDemand"); + /// + /// Indicates that the summary of a project's dependencies is being reported during project unload. + /// + public static readonly string ProjectUnloadDependencies = BuildEventName("ProjectUnload/Dependencies"); + private static string BuildEventName(string eventName) { return Prefix + "/" + eventName.ToLowerInvariant(); diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryPropertyName.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryPropertyName.cs index 54817771430..d9f4348d02f 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryPropertyName.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Telemetry/TelemetryPropertyName.cs @@ -27,6 +27,42 @@ internal static class TelemetryPropertyName /// public static readonly string TreeUpdatedUnresolvedProject = BuildPropertyName(TelemetryEventName.TreeUpdatedUnresolved, "Project"); + /// + /// Identifies the project to which data in the telemetry event applies. + /// + public static readonly string ProjectUnloadDependenciesProject = BuildPropertyName(TelemetryEventName.ProjectUnloadDependencies, "Project"); + + /// + /// Identifies the time between project load and unload, in milliseconds. + /// + public static readonly string ProjectUnloadProjectAgeMillis = BuildPropertyName(TelemetryEventName.ProjectUnloadDependencies, "ProjectAgeMillis"); + + /// + /// Identifies the total number of visible dependencies in the project. + /// If a project multi-targets (i.e. is greater than one) then the count of dependencies + /// in each target is summed together to produce this single value. If a breakdown is required, + /// may be used. + /// + public static readonly string ProjectUnloadTotalDependencyCount = BuildPropertyName(TelemetryEventName.ProjectUnloadDependencies, "TotalDependencyCount"); + + /// + /// Identifies the total number of visible unresolved dependencies in the project. + /// If a project multi-targets (i.e. is greater than one) then the count of unresolved dependencies + /// in each target is summed together to produce this single value. If a breakdown is required, + /// may be used. + /// + public static readonly string ProjectUnloadUnresolvedDependencyCount = BuildPropertyName(TelemetryEventName.ProjectUnloadDependencies, "UnresolvedDependencyCount"); + + /// + /// Identifies the number of frameworks this project targets. + /// + public static readonly string ProjectUnloadTargetFrameworkCount = BuildPropertyName(TelemetryEventName.ProjectUnloadDependencies, "TargetFrameworkCount"); + + /// + /// Contains structured data describing the number of total/unresolved dependencies broken down by target framework and dependency type. + /// + public static readonly string ProjectUnloadDependencyBreakdown = BuildPropertyName(TelemetryEventName.ProjectUnloadDependencies, "DependencyBreakdown"); + /// /// Indicates whether seen all rules initialized when the dependency tree is updated with all resolved dependencies. ///