зеркало из https://github.com/dotnet/msbuild.git
Add BuildCheck basic telemetry (#10652)
* Add initial version of telemetry data extracting * Initial tracing data transport * Move buildcheck enabled telemetry to buildtelemetry * Add unittests * Fix typos * Add CheckFriendlyName to telemetry * Add SAC telemetry * Adjust after merging
This commit is contained in:
Родитель
e4797f3f00
Коммит
aeabd8b112
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||
using FluentAssertions;
|
||||
using Microsoft.Build.BackEnd;
|
||||
using Microsoft.Build.Execution;
|
||||
using Microsoft.Build.Experimental.BuildCheck;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Shared;
|
||||
using Xunit;
|
||||
|
@ -77,6 +78,7 @@ namespace Microsoft.Build.UnitTests.BackEnd
|
|||
EnvironmentVariableReadEventArgs environmentVariableRead = new("env", "message", "file", 0, 0);
|
||||
GeneratedFileUsedEventArgs generatedFileUsed = new GeneratedFileUsedEventArgs("path", "some content");
|
||||
BuildSubmissionStartedEventArgs buildSubmissionStarted = new(new Dictionary<string, string> { { "Value1", "Value2" } }, ["Path1"], ["TargetName"], BuildRequestDataFlags.ReplaceExistingProjectInstance, 123);
|
||||
BuildCheckTracingEventArgs buildCheckTracing = new();
|
||||
|
||||
VerifyLoggingPacket(buildFinished, LoggingEventType.BuildFinishedEvent);
|
||||
VerifyLoggingPacket(buildStarted, LoggingEventType.BuildStartedEvent);
|
||||
|
@ -111,6 +113,7 @@ namespace Microsoft.Build.UnitTests.BackEnd
|
|||
VerifyLoggingPacket(environmentVariableRead, LoggingEventType.EnvironmentVariableReadEvent);
|
||||
VerifyLoggingPacket(generatedFileUsed, LoggingEventType.GeneratedFileUsedEvent);
|
||||
VerifyLoggingPacket(buildSubmissionStarted, LoggingEventType.BuildSubmissionStartedEvent);
|
||||
VerifyLoggingPacket(buildCheckTracing, LoggingEventType.BuildCheckTracingEvent);
|
||||
}
|
||||
|
||||
private static BuildEventContext CreateBuildEventContext()
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Build.BackEnd;
|
||||
using Microsoft.Build.Experimental.BuildCheck;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Framework.Profiler;
|
||||
using Microsoft.Build.Logging;
|
||||
|
@ -530,6 +532,27 @@ namespace Microsoft.Build.UnitTests
|
|||
e => string.Join(", ", e.RawArguments ?? Array.Empty<object>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundtripBuildCheckTracingEventArgs()
|
||||
{
|
||||
string key1 = "AA";
|
||||
TimeSpan span1 = TimeSpan.FromSeconds(5);
|
||||
string key2 = "b";
|
||||
TimeSpan span2 = TimeSpan.FromSeconds(15);
|
||||
string key3 = "cCc";
|
||||
TimeSpan span3 = TimeSpan.FromSeconds(50);
|
||||
|
||||
Dictionary<string, TimeSpan> stats = new() { { key1, span1 }, { key2, span2 }, { key3, span3 } };
|
||||
|
||||
BuildCheckTracingEventArgs args = new BuildCheckTracingEventArgs(stats);
|
||||
|
||||
Roundtrip(args,
|
||||
e => e.TracingData.InfrastructureTracingData.Keys.Count.ToString(),
|
||||
e => e.TracingData.InfrastructureTracingData.Keys.ToCsvString(false),
|
||||
e => e.TracingData.InfrastructureTracingData.Values
|
||||
.Select(v => v.TotalSeconds.ToString(CultureInfo.InvariantCulture)).ToCsvString(false));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
|
|
|
@ -1070,6 +1070,11 @@ namespace Microsoft.Build.Execution
|
|||
}
|
||||
_buildTelemetry.Host = host;
|
||||
|
||||
_buildTelemetry.BuildCheckEnabled = _buildParameters!.IsBuildCheckEnabled;
|
||||
var sacState = NativeMethodsShared.GetSACState();
|
||||
// The Enforcement would lead to build crash - but let's have the check for completeness sake.
|
||||
_buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement;
|
||||
|
||||
loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties());
|
||||
// Clean telemetry to make it ready for next build submission.
|
||||
_buildTelemetry = null;
|
||||
|
|
|
@ -43,6 +43,8 @@ public abstract class Check : IDisposable
|
|||
/// </param>
|
||||
public abstract void RegisterActions(IBuildCheckRegistrationContext registrationContext);
|
||||
|
||||
internal virtual bool IsBuiltIn => false;
|
||||
|
||||
public virtual void Dispose()
|
||||
{ }
|
||||
}
|
||||
|
|
|
@ -28,4 +28,6 @@ internal abstract class InternalCheck : Check
|
|||
|
||||
this.RegisterInternalActions(internalRegistrationContext);
|
||||
}
|
||||
|
||||
internal override bool IsBuiltIn => true;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using Microsoft.Build.Experimental.BuildCheck.Infrastructure;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Framework.Telemetry;
|
||||
using Microsoft.Build.Shared;
|
||||
|
||||
namespace Microsoft.Build.Experimental.BuildCheck.Acquisition;
|
||||
|
@ -53,21 +54,24 @@ internal class BuildCheckAcquisitionModule : IBuildCheckAcquisitionModule
|
|||
.ForEach(t => checkContext.DispatchAsComment(MessageImportance.Normal, "CustomCheckBaseTypeNotAssignable", t.Name, t.Assembly));
|
||||
}
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
catch (ReflectionTypeLoadException ex) when (ex.LoaderExceptions.Length != 0)
|
||||
{
|
||||
if (ex.LoaderExceptions.Length != 0)
|
||||
foreach (Exception? unrolledEx in ex.LoaderExceptions.Where(e => e != null).Prepend(ex))
|
||||
{
|
||||
foreach (Exception? loaderException in ex.LoaderExceptions)
|
||||
{
|
||||
checkContext.DispatchAsComment(MessageImportance.Normal, "CustomCheckFailedRuleLoading", loaderException?.Message);
|
||||
}
|
||||
ReportLoadingError(unrolledEx!);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
checkContext.DispatchAsComment(MessageImportance.Normal, "CustomCheckFailedRuleLoading", ex?.Message);
|
||||
ReportLoadingError(ex);
|
||||
}
|
||||
|
||||
return checksFactories;
|
||||
|
||||
void ReportLoadingError(Exception ex)
|
||||
{
|
||||
checkContext.DispatchAsComment(MessageImportance.Normal, "CustomCheckFailedRuleLoading", ex.Message);
|
||||
checkContext.DispatchFailedAcquisitionTelemetry(System.IO.Path.GetFileName(checkAcquisitionData.AssemblyPath), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,8 @@ internal sealed class DoubleWritesCheck : Check
|
|||
registrationContext.RegisterTaskInvocationAction(TaskInvocationAction);
|
||||
}
|
||||
|
||||
internal override bool IsBuiltIn => true;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the first project file + task that wrote the given file during the build.
|
||||
/// </summary>
|
||||
|
@ -126,5 +128,5 @@ internal sealed class DoubleWritesCheck : Check
|
|||
_filesWritten.Add(fileBeingWritten, (context.Data.ProjectFilePath, context.Data.TaskName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ internal sealed class NoEnvironmentVariablePropertyCheck : Check
|
|||
|
||||
public override void RegisterActions(IBuildCheckRegistrationContext registrationContext) => registrationContext.RegisterEnvironmentVariableReadAction(ProcessEnvironmentVariableReadAction);
|
||||
|
||||
internal override bool IsBuiltIn => true;
|
||||
|
||||
private void ProcessEnvironmentVariableReadAction(BuildCheckDataContext<EnvironmentVariableCheckData> context)
|
||||
{
|
||||
EnvironmentVariableIdentityKey identityKey = new(context.Data.EnvironmentVariableName, context.Data.EnvironmentVariableLocation);
|
||||
|
|
|
@ -118,6 +118,8 @@ internal class PropertiesUsageCheck : InternalCheck
|
|||
}
|
||||
}
|
||||
|
||||
internal override bool IsBuiltIn => true;
|
||||
|
||||
private Dictionary<string, IMSBuildElementLocation?> _writenProperties = new(MSBuildNameIgnoreCaseComparer.Default);
|
||||
private HashSet<string> _readProperties = new(MSBuildNameIgnoreCaseComparer.Default);
|
||||
// For the 'Property Initialized after used' check - we are interested in cases where:
|
||||
|
|
|
@ -35,6 +35,8 @@ internal sealed class SharedOutputPathCheck : Check
|
|||
registrationContext.RegisterEvaluatedPropertiesAction(EvaluatedPropertiesAction);
|
||||
}
|
||||
|
||||
internal override bool IsBuiltIn => true;
|
||||
|
||||
private readonly Dictionary<string, string> _projectsPerOutputPath = new(StringComparer.CurrentCultureIgnoreCase);
|
||||
private readonly HashSet<string> _projects = new(StringComparer.CurrentCultureIgnoreCase);
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ internal class BuildCheckBuildEventHandler
|
|||
{
|
||||
if (!eventArgs.IsAggregatedGlobalReport)
|
||||
{
|
||||
_stats.Merge(eventArgs.TracingData, (span1, span2) => span1 + span2);
|
||||
_tracingData.MergeIn(eventArgs.TracingData);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,36 +138,25 @@ internal class BuildCheckBuildEventHandler
|
|||
|
||||
private bool IsMetaProjFile(string? projectFile) => projectFile?.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
private readonly Dictionary<string, TimeSpan> _stats = new Dictionary<string, TimeSpan>();
|
||||
private readonly BuildCheckTracingData _tracingData = new BuildCheckTracingData();
|
||||
|
||||
private void HandleBuildFinishedEvent(BuildFinishedEventArgs eventArgs)
|
||||
{
|
||||
_buildCheckManager.ProcessBuildFinished(_checkContextFactory.CreateCheckContext(eventArgs.BuildEventContext!));
|
||||
|
||||
_stats.Merge(_buildCheckManager.CreateCheckTracingStats(), (span1, span2) => span1 + span2);
|
||||
_tracingData.MergeIn(_buildCheckManager.CreateCheckTracingStats());
|
||||
|
||||
LogCheckStats(_checkContextFactory.CreateCheckContext(GetBuildEventContext(eventArgs)));
|
||||
}
|
||||
|
||||
private void LogCheckStats(ICheckContext checkContext)
|
||||
{
|
||||
Dictionary<string, TimeSpan> infraStats = new Dictionary<string, TimeSpan>();
|
||||
Dictionary<string, TimeSpan> checkStats = new Dictionary<string, TimeSpan>();
|
||||
Dictionary<string, TimeSpan> infraStats = _tracingData.InfrastructureTracingData;
|
||||
// Stats are per rule, while runtime is per check - and check can have multiple rules.
|
||||
// In case of multi-rule check, the runtime stats are duplicated for each rule.
|
||||
Dictionary<string, TimeSpan> checkStats = _tracingData.ExtractCheckStats();
|
||||
|
||||
foreach (var stat in _stats)
|
||||
{
|
||||
if (stat.Key.StartsWith(BuildCheckConstants.infraStatPrefix))
|
||||
{
|
||||
string newKey = stat.Key.Substring(BuildCheckConstants.infraStatPrefix.Length);
|
||||
infraStats[newKey] = stat.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkStats[stat.Key] = stat.Value;
|
||||
}
|
||||
}
|
||||
|
||||
BuildCheckTracingEventArgs statEvent = new BuildCheckTracingEventArgs(_stats, true)
|
||||
BuildCheckTracingEventArgs statEvent = new BuildCheckTracingEventArgs(_tracingData, true)
|
||||
{ BuildEventContext = checkContext.BuildEventContext };
|
||||
|
||||
checkContext.DispatchBuildEvent(statEvent);
|
||||
|
@ -177,6 +166,7 @@ internal class BuildCheckBuildEventHandler
|
|||
checkContext.DispatchAsCommentFromText(MessageImportance.Low, infraData);
|
||||
string checkData = BuildCsvString("Checks run times", checkStats);
|
||||
checkContext.DispatchAsCommentFromText(MessageImportance.Low, checkData);
|
||||
checkContext.DispatchTelemetry(_tracingData);
|
||||
}
|
||||
|
||||
private string BuildCsvString(string title, Dictionary<string, TimeSpan> rowData)
|
||||
|
|
|
@ -229,10 +229,10 @@ internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
|
|||
CheckConfigurationEffective[] configurations;
|
||||
if (checkFactoryContext.MaterializedCheck == null)
|
||||
{
|
||||
CheckConfiguration[] userConfigs =
|
||||
CheckConfiguration[] userEditorConfigs =
|
||||
_configurationProvider.GetUserConfigurations(projectFullPath, checkFactoryContext.RuleIds);
|
||||
|
||||
if (userConfigs.All(c => !(c.IsEnabled ?? checkFactoryContext.IsEnabledByDefault)))
|
||||
if (userEditorConfigs.All(c => !(c.IsEnabled ?? checkFactoryContext.IsEnabledByDefault)))
|
||||
{
|
||||
// the check was not yet instantiated nor mounted - so nothing to do here now.
|
||||
return;
|
||||
|
@ -242,7 +242,7 @@ internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
|
|||
_configurationProvider.GetCustomConfigurations(projectFullPath, checkFactoryContext.RuleIds);
|
||||
|
||||
Check uninitializedCheck = checkFactoryContext.Factory();
|
||||
configurations = _configurationProvider.GetMergedConfigurations(userConfigs, uninitializedCheck);
|
||||
configurations = _configurationProvider.GetMergedConfigurations(userEditorConfigs, uninitializedCheck);
|
||||
|
||||
ConfigurationContext configurationContext = ConfigurationContext.FromDataEnumeration(customConfigData, configurations);
|
||||
|
||||
|
@ -271,7 +271,7 @@ internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
|
|||
// price to be paid in that case is slight performance cost.
|
||||
|
||||
// Create the wrapper and register to central context
|
||||
wrapper.StartNewProject(projectFullPath, configurations);
|
||||
wrapper.StartNewProject(projectFullPath, configurations, userEditorConfigs);
|
||||
var wrappedContext = new CheckRegistrationContext(wrapper, _buildCheckCentralContext);
|
||||
check.RegisterActions(wrappedContext);
|
||||
}
|
||||
|
@ -279,13 +279,15 @@ internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
|
|||
{
|
||||
wrapper = checkFactoryContext.MaterializedCheck;
|
||||
|
||||
configurations = _configurationProvider.GetMergedConfigurations(projectFullPath, wrapper.Check);
|
||||
CheckConfiguration[] userEditorConfigs =
|
||||
_configurationProvider.GetUserConfigurations(projectFullPath, checkFactoryContext.RuleIds);
|
||||
configurations = _configurationProvider.GetMergedConfigurations(userEditorConfigs, wrapper.Check);
|
||||
|
||||
_configurationProvider.CheckCustomConfigurationDataValidity(projectFullPath,
|
||||
checkFactoryContext.RuleIds[0]);
|
||||
|
||||
// Update the wrapper
|
||||
wrapper.StartNewProject(projectFullPath, configurations);
|
||||
wrapper.StartNewProject(projectFullPath, configurations, userEditorConfigs);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,7 +348,7 @@ internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
|
|||
if (checkToRemove.MaterializedCheck is not null)
|
||||
{
|
||||
_buildCheckCentralContext.DeregisterCheck(checkToRemove.MaterializedCheck);
|
||||
_tracingReporter.AddCheckStats(checkToRemove.MaterializedCheck.Check.FriendlyName, checkToRemove.MaterializedCheck.Elapsed);
|
||||
_ruleTelemetryData.AddRange(checkToRemove.MaterializedCheck.GetRuleTelemetryData());
|
||||
checkToRemove.MaterializedCheck.Check.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -411,19 +413,18 @@ internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
|
|||
=> _buildEventsProcessor
|
||||
.ProcessTaskParameterEventArgs(checkContext, taskParameterEventArgs);
|
||||
|
||||
public Dictionary<string, TimeSpan> CreateCheckTracingStats()
|
||||
private readonly List<BuildCheckRuleTelemetryData> _ruleTelemetryData = [];
|
||||
public BuildCheckTracingData CreateCheckTracingStats()
|
||||
{
|
||||
foreach (CheckFactoryContext checkFactoryContext in _checkRegistry)
|
||||
{
|
||||
if (checkFactoryContext.MaterializedCheck != null)
|
||||
{
|
||||
_tracingReporter.AddCheckStats(checkFactoryContext.FriendlyName, checkFactoryContext.MaterializedCheck.Elapsed);
|
||||
checkFactoryContext.MaterializedCheck.ClearStats();
|
||||
_ruleTelemetryData.AddRange(checkFactoryContext.MaterializedCheck.GetRuleTelemetryData());
|
||||
}
|
||||
}
|
||||
|
||||
_tracingReporter.AddCheckInfraStats();
|
||||
return _tracingReporter.TracingStats;
|
||||
return new BuildCheckTracingData(_ruleTelemetryData, _tracingReporter.GetInfrastructureTracingStats());
|
||||
}
|
||||
|
||||
public void FinalizeProcessing(LoggingContext loggingContext)
|
||||
|
|
|
@ -69,4 +69,12 @@ internal class CheckDispatchingContext : ICheckContext
|
|||
|
||||
_eventDispatcher.Dispatch(buildEvent);
|
||||
}
|
||||
|
||||
public void DispatchFailedAcquisitionTelemetry(string assemblyName, Exception exception)
|
||||
// This is it - no action for replay mode.
|
||||
{ }
|
||||
|
||||
public void DispatchTelemetry(BuildCheckTracingData data)
|
||||
// This is it - no action for replay mode.
|
||||
{ }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.Build.BackEnd.Logging;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Framework.Telemetry;
|
||||
using Microsoft.Build.Shared;
|
||||
|
||||
namespace Microsoft.Build.Experimental.BuildCheck;
|
||||
|
@ -43,4 +44,18 @@ internal readonly struct CheckLoggingContext(ILoggingService loggingService, Bui
|
|||
public void DispatchAsWarningFromText(string? subcategoryResourceName, string? errorCode, string? helpKeyword, BuildEventFileInfo file, string message)
|
||||
=> loggingService
|
||||
.LogWarningFromText(eventContext, subcategoryResourceName, errorCode, helpKeyword, file, message);
|
||||
|
||||
public void DispatchFailedAcquisitionTelemetry(string assemblyName, Exception exception)
|
||||
{
|
||||
var telemetryTransportData = KnownTelemetry.BuildCheckTelemetry.ProcessCustomCheckLoadingFailure(assemblyName, exception);
|
||||
loggingService.LogTelemetry(eventContext, telemetryTransportData.Item1, telemetryTransportData.Item2);
|
||||
}
|
||||
|
||||
public void DispatchTelemetry(BuildCheckTracingData data)
|
||||
{
|
||||
foreach ((string, IDictionary<string, string>) telemetryTransportData in KnownTelemetry.BuildCheckTelemetry.ProcessBuildCheckTracingData(data))
|
||||
{
|
||||
loggingService.LogTelemetry(eventContext, telemetryTransportData.Item1, telemetryTransportData.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,4 +45,14 @@ internal interface ICheckContext
|
|||
/// Dispatch the instance of <see cref="BuildEventContext"/> as a warning message.
|
||||
/// </summary>
|
||||
void DispatchAsWarningFromText(string? subcategoryResourceName, string? errorCode, string? helpKeyword, BuildEventFileInfo file, string message);
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch the telemetry data for a failed acquisition.
|
||||
/// </summary>
|
||||
void DispatchFailedAcquisitionTelemetry(string assemblyName, Exception exception);
|
||||
|
||||
/// <summary>
|
||||
/// If supported - dispatches the telemetry data.
|
||||
/// </summary>
|
||||
void DispatchTelemetry(BuildCheckTracingData data);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
|
|||
internal sealed class CheckWrapper
|
||||
{
|
||||
private readonly Stopwatch _stopwatch = new Stopwatch();
|
||||
private readonly BuildCheckRuleTelemetryData[] _ruleTelemetryData;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount of messages that could be sent per check rule.
|
||||
|
@ -41,6 +42,23 @@ internal sealed class CheckWrapper
|
|||
public CheckWrapper(Check check)
|
||||
{
|
||||
Check = check;
|
||||
_ruleTelemetryData = new BuildCheckRuleTelemetryData[check.SupportedRules.Count];
|
||||
|
||||
InitializeTelemetryData(_ruleTelemetryData, check);
|
||||
}
|
||||
|
||||
private static void InitializeTelemetryData(BuildCheckRuleTelemetryData[] ruleTelemetryData, Check check)
|
||||
{
|
||||
int idx = 0;
|
||||
foreach (CheckRule checkRule in check.SupportedRules)
|
||||
{
|
||||
ruleTelemetryData[idx++] = new BuildCheckRuleTelemetryData(
|
||||
ruleId: checkRule.Id,
|
||||
checkFriendlyName: check.FriendlyName,
|
||||
isBuiltIn: check.IsBuiltIn,
|
||||
defaultSeverity: (checkRule.DefaultConfiguration.Severity ??
|
||||
CheckConfigurationEffective.Default.Severity).ToDiagnosticSeverity());
|
||||
}
|
||||
}
|
||||
|
||||
internal Check Check { get; }
|
||||
|
@ -51,29 +69,87 @@ internal sealed class CheckWrapper
|
|||
// In such case - configuration will be same for all projects. So we do not need to store it per project in a collection.
|
||||
internal CheckConfigurationEffective? CommonConfig { get; private set; }
|
||||
|
||||
// start new project
|
||||
/// <summary>
|
||||
/// Ensures the check being configured for a new project (as each project can have different settings)
|
||||
/// </summary>
|
||||
/// <param name="fullProjectPath"></param>
|
||||
/// <param name="effectiveConfigs">Resulting merged configurations per rule (merged from check default and explicit user editorconfig).</param>
|
||||
/// <param name="editorConfigs">Configurations from editorconfig per rule.</param>
|
||||
internal void StartNewProject(
|
||||
string fullProjectPath,
|
||||
IReadOnlyList<CheckConfigurationEffective> userConfigs)
|
||||
IReadOnlyList<CheckConfigurationEffective> effectiveConfigs,
|
||||
IReadOnlyList<CheckConfiguration> editorConfigs)
|
||||
{
|
||||
// Let's first update the telemetry data for the rules.
|
||||
int idx = 0;
|
||||
foreach (BuildCheckRuleTelemetryData ruleTelemetryData in _ruleTelemetryData)
|
||||
{
|
||||
CheckConfigurationEffective effectiveConfig = effectiveConfigs[Math.Max(idx, effectiveConfigs.Count - 1)];
|
||||
if (editorConfigs[idx].Severity != null)
|
||||
{
|
||||
ruleTelemetryData.ExplicitSeverities.Add(editorConfigs[idx].Severity!.Value.ToDiagnosticSeverity());
|
||||
}
|
||||
|
||||
if (effectiveConfig.IsEnabled)
|
||||
{
|
||||
ruleTelemetryData.ProjectNamesWhereEnabled.Add(fullProjectPath);
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (!_areStatsInitialized)
|
||||
{
|
||||
_areStatsInitialized = true;
|
||||
CommonConfig = userConfigs[0];
|
||||
CommonConfig = effectiveConfigs[0];
|
||||
|
||||
if (userConfigs.Count == 1)
|
||||
if (effectiveConfigs.Count == 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The Common configuration is not common anymore - let's nullify it and we will need to fetch configuration per project.
|
||||
if (CommonConfig == null || !userConfigs.All(t => t.IsSameConfigurationAs(CommonConfig)))
|
||||
if (CommonConfig == null || !effectiveConfigs.All(t => t.IsSameConfigurationAs(CommonConfig)))
|
||||
{
|
||||
CommonConfig = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDiagnostic(CheckConfigurationEffective configurationEffective)
|
||||
{
|
||||
BuildCheckRuleTelemetryData? telemetryData =
|
||||
_ruleTelemetryData.FirstOrDefault(td => td.RuleId.Equals(configurationEffective.RuleId));
|
||||
|
||||
if (telemetryData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (configurationEffective.Severity)
|
||||
{
|
||||
|
||||
case CheckResultSeverity.Suggestion:
|
||||
telemetryData.IncrementMessagesCount();
|
||||
break;
|
||||
case CheckResultSeverity.Warning:
|
||||
telemetryData.IncrementWarningsCount();
|
||||
break;
|
||||
case CheckResultSeverity.Error:
|
||||
telemetryData.IncrementErrorsCount();
|
||||
break;
|
||||
case CheckResultSeverity.Default:
|
||||
case CheckResultSeverity.None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsThrottled)
|
||||
{
|
||||
telemetryData.SetThrottled();
|
||||
}
|
||||
}
|
||||
|
||||
internal void ReportResult(BuildCheckResult result, ICheckContext checkContext, CheckConfigurationEffective config)
|
||||
{
|
||||
if (!IsThrottled)
|
||||
|
@ -93,6 +169,9 @@ internal sealed class CheckWrapper
|
|||
IsThrottled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the diagnostic to the check wrapper for telemetry purposes.
|
||||
AddDiagnostic(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,9 +181,15 @@ internal sealed class CheckWrapper
|
|||
_areStatsInitialized = false;
|
||||
}
|
||||
|
||||
internal TimeSpan Elapsed => _stopwatch.Elapsed;
|
||||
internal IReadOnlyList<BuildCheckRuleTelemetryData> GetRuleTelemetryData()
|
||||
{
|
||||
foreach (BuildCheckRuleTelemetryData ruleTelemetryData in _ruleTelemetryData)
|
||||
{
|
||||
ruleTelemetryData.TotalRuntime = _stopwatch.Elapsed;
|
||||
}
|
||||
|
||||
internal void ClearStats() => _stopwatch.Reset();
|
||||
return _ruleTelemetryData;
|
||||
}
|
||||
|
||||
internal CleanupScope StartSpan()
|
||||
{
|
||||
|
|
|
@ -61,7 +61,7 @@ internal interface IBuildCheckManager
|
|||
|
||||
void ProcessCheckAcquisition(CheckAcquisitionData acquisitionData, ICheckContext checksContext);
|
||||
|
||||
Dictionary<string, TimeSpan> CreateCheckTracingStats();
|
||||
BuildCheckTracingData CreateCheckTracingStats();
|
||||
|
||||
void FinalizeProcessing(LoggingContext loggingContext);
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ internal class NullBuildCheckManager : IBuildCheckManager, IBuildEngineDataRoute
|
|||
{
|
||||
}
|
||||
|
||||
public Dictionary<string, TimeSpan> CreateCheckTracingStats() => new Dictionary<string, TimeSpan>();
|
||||
public BuildCheckTracingData CreateCheckTracingStats() => new BuildCheckTracingData();
|
||||
|
||||
public void ProcessPropertyRead(PropertyReadInfo propertyReadInfo, CheckLoggingContext buildEventContext)
|
||||
{ }
|
||||
|
|
|
@ -13,26 +13,10 @@ namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
|
|||
|
||||
internal class TracingReporter
|
||||
{
|
||||
internal Dictionary<string, TimeSpan> TracingStats { get; } = new();
|
||||
|
||||
// Infrastructure time keepers
|
||||
// TODO: add more timers throughout BuildCheck run
|
||||
private TimeSpan checkAcquisitionTime;
|
||||
private TimeSpan checkSetDataSourceTime;
|
||||
private TimeSpan newProjectChecksTime;
|
||||
|
||||
public void AddCheckStats(string name, TimeSpan subtotal)
|
||||
{
|
||||
if (TracingStats.TryGetValue(name, out TimeSpan existing))
|
||||
{
|
||||
TracingStats[name] = existing + subtotal;
|
||||
}
|
||||
else
|
||||
{
|
||||
TracingStats[name] = subtotal;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddAcquisitionStats(TimeSpan subtotal)
|
||||
{
|
||||
checkAcquisitionTime += subtotal;
|
||||
|
@ -48,14 +32,11 @@ internal class TracingReporter
|
|||
newProjectChecksTime += subtotal;
|
||||
}
|
||||
|
||||
public void AddCheckInfraStats()
|
||||
{
|
||||
var infraStats = new Dictionary<string, TimeSpan>() {
|
||||
{ $"{BuildCheckConstants.infraStatPrefix}checkAcquisitionTime", checkAcquisitionTime },
|
||||
{ $"{BuildCheckConstants.infraStatPrefix}checkSetDataSourceTime", checkSetDataSourceTime },
|
||||
{ $"{BuildCheckConstants.infraStatPrefix}newProjectChecksTime", newProjectChecksTime }
|
||||
};
|
||||
|
||||
TracingStats.Merge(infraStats, (span1, span2) => span1 + span2);
|
||||
}
|
||||
public Dictionary<string, TimeSpan> GetInfrastructureTracingStats()
|
||||
=> new Dictionary<string, TimeSpan>()
|
||||
{
|
||||
{ $"{BuildCheckConstants.infraStatPrefix}checkAcquisitionTime", checkAcquisitionTime },
|
||||
{ $"{BuildCheckConstants.infraStatPrefix}checkSetDataSourceTime", checkSetDataSourceTime },
|
||||
{ $"{BuildCheckConstants.infraStatPrefix}newProjectChecksTime", newProjectChecksTime }
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Build.Experimental.BuildCheck;
|
||||
internal static class CheckResultSeverityExtensions
|
||||
{
|
||||
public static DiagnosticSeverity? ToDiagnosticSeverity(this CheckResultSeverity? severity)
|
||||
{
|
||||
if (severity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ToDiagnosticSeverity(severity.Value);
|
||||
}
|
||||
|
||||
public static DiagnosticSeverity ToDiagnosticSeverity(this CheckResultSeverity severity)
|
||||
{
|
||||
return severity switch
|
||||
{
|
||||
CheckResultSeverity.Default => DiagnosticSeverity.Default,
|
||||
CheckResultSeverity.None => DiagnosticSeverity.None,
|
||||
CheckResultSeverity.Suggestion => DiagnosticSeverity.Suggestion,
|
||||
CheckResultSeverity.Warning => DiagnosticSeverity.Warning,
|
||||
CheckResultSeverity.Error => DiagnosticSeverity.Error,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null)
|
||||
};
|
||||
}
|
||||
}
|
|
@ -338,7 +338,11 @@ namespace Microsoft.Build.Logging
|
|||
private BinaryLogRecordKind Write(BuildCheckTracingEventArgs e)
|
||||
{
|
||||
WriteBuildEventArgsFields(e, writeMessage: false);
|
||||
WriteProperties(e.TracingData);
|
||||
|
||||
Dictionary<string, TimeSpan> stats = e.TracingData.ExtractCheckStats();
|
||||
stats.Merge(e.TracingData.InfrastructureTracingData, (span1, span2) => span1 + span2);
|
||||
|
||||
WriteProperties(stats);
|
||||
|
||||
return BinaryLogRecordKind.BuildCheckTracing;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Build.Experimental.BuildCheck;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Build.Framework.UnitTests
|
||||
{
|
||||
public class BuildCheckTracingEventArgs_Tests
|
||||
{
|
||||
[Fact]
|
||||
public void SerializationDeserializationTest()
|
||||
{
|
||||
string key1 = "AA";
|
||||
TimeSpan span1 = TimeSpan.FromSeconds(5);
|
||||
string key2 = "b";
|
||||
TimeSpan span2 = TimeSpan.FromSeconds(15);
|
||||
string key3 = "cCc";
|
||||
TimeSpan span3 = TimeSpan.FromSeconds(50);
|
||||
|
||||
Dictionary<string, TimeSpan> stats = new() { { key1, span1 }, { key2, span2 }, { key3, span3 } };
|
||||
|
||||
BuildCheckRuleTelemetryData ruleData1 = new("id1", "name1", true, DiagnosticSeverity.Suggestion,
|
||||
new HashSet<DiagnosticSeverity>() { DiagnosticSeverity.Default, DiagnosticSeverity.Suggestion },
|
||||
new HashSet<string>() { "aa", "b" }, 5, 2, 8, true, TimeSpan.FromSeconds(123));
|
||||
|
||||
BuildCheckRuleTelemetryData ruleData2 = new("id2", "name2", false, DiagnosticSeverity.Error,
|
||||
new HashSet<DiagnosticSeverity>(),
|
||||
new HashSet<string>(), 0, 0, 500, false, TimeSpan.FromSeconds(1234));
|
||||
|
||||
BuildCheckTracingData data = new(new [] {ruleData1, ruleData2}, stats);
|
||||
BuildCheckTracingEventArgs arg = new(data);
|
||||
|
||||
using MemoryStream stream = new MemoryStream();
|
||||
using BinaryWriter bw = new BinaryWriter(stream);
|
||||
arg.WriteToStream(bw);
|
||||
|
||||
stream.Position = 0;
|
||||
using BinaryReader br = new BinaryReader(stream);
|
||||
BuildCheckTracingEventArgs argDeserialized = new();
|
||||
int packetVersion = (Environment.Version.Major * 10) + Environment.Version.Minor;
|
||||
argDeserialized.CreateFromStream(br, packetVersion);
|
||||
|
||||
argDeserialized.TracingData.InfrastructureTracingData.ShouldBeEquivalentTo(arg.TracingData.InfrastructureTracingData);
|
||||
argDeserialized.TracingData.TelemetryData.Keys.ShouldBeEquivalentTo(arg.TracingData.TelemetryData.Keys);
|
||||
|
||||
argDeserialized.TracingData.TelemetryData["id1"].ShouldBeEquivalentTo(arg.TracingData.TelemetryData["id1"]);
|
||||
argDeserialized.TracingData.TelemetryData["id2"].ShouldBeEquivalentTo(arg.TracingData.TelemetryData["id2"]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,15 +19,20 @@ internal abstract class BuildCheckEventArgs : BuildEventArgs
|
|||
/// <summary>
|
||||
/// Transport mean for the BuildCheck tracing data from additional nodes.
|
||||
/// </summary>
|
||||
/// <param name="tracingData"></param>
|
||||
internal sealed class BuildCheckTracingEventArgs(Dictionary<string, TimeSpan> tracingData) : BuildCheckEventArgs
|
||||
internal sealed class BuildCheckTracingEventArgs(
|
||||
BuildCheckTracingData tracingData) : BuildCheckEventArgs
|
||||
{
|
||||
internal BuildCheckTracingEventArgs()
|
||||
: this([])
|
||||
{
|
||||
}
|
||||
: this(new BuildCheckTracingData())
|
||||
{ }
|
||||
|
||||
internal BuildCheckTracingEventArgs(Dictionary<string, TimeSpan> data, bool isAggregatedGlobalReport) : this(data) => IsAggregatedGlobalReport = isAggregatedGlobalReport;
|
||||
internal BuildCheckTracingEventArgs(Dictionary<string, TimeSpan> executionData)
|
||||
: this(new BuildCheckTracingData(executionData))
|
||||
{ }
|
||||
|
||||
internal BuildCheckTracingEventArgs(
|
||||
BuildCheckTracingData tracingData,
|
||||
bool isAggregatedGlobalReport) : this(tracingData) => IsAggregatedGlobalReport = isAggregatedGlobalReport;
|
||||
|
||||
/// <summary>
|
||||
/// When true, the tracing information is from the whole build for logging purposes
|
||||
|
@ -35,18 +40,42 @@ internal sealed class BuildCheckTracingEventArgs(Dictionary<string, TimeSpan> tr
|
|||
/// </summary>
|
||||
public bool IsAggregatedGlobalReport { get; private set; } = false;
|
||||
|
||||
public Dictionary<string, TimeSpan> TracingData { get; private set; } = tracingData;
|
||||
public BuildCheckTracingData TracingData { get; private set; } = tracingData;
|
||||
|
||||
internal override void WriteToStream(BinaryWriter writer)
|
||||
{
|
||||
base.WriteToStream(writer);
|
||||
|
||||
writer.Write7BitEncodedInt(TracingData.Count);
|
||||
foreach (KeyValuePair<string, TimeSpan> kvp in TracingData)
|
||||
writer.Write7BitEncodedInt(TracingData.InfrastructureTracingData.Count);
|
||||
foreach (KeyValuePair<string, TimeSpan> kvp in TracingData.InfrastructureTracingData)
|
||||
{
|
||||
writer.Write(kvp.Key);
|
||||
writer.Write(kvp.Value.Ticks);
|
||||
}
|
||||
|
||||
writer.Write7BitEncodedInt(TracingData.TelemetryData.Count);
|
||||
foreach (BuildCheckRuleTelemetryData data in TracingData.TelemetryData.Values)
|
||||
{
|
||||
writer.Write(data.RuleId);
|
||||
writer.Write(data.CheckFriendlyName);
|
||||
writer.Write(data.IsBuiltIn);
|
||||
writer.Write7BitEncodedInt((int)data.DefaultSeverity);
|
||||
writer.Write7BitEncodedInt(data.ExplicitSeverities.Count);
|
||||
foreach (DiagnosticSeverity severity in data.ExplicitSeverities)
|
||||
{
|
||||
writer.Write7BitEncodedInt((int)severity);
|
||||
}
|
||||
writer.Write7BitEncodedInt(data.ProjectNamesWhereEnabled.Count);
|
||||
foreach (string projectName in data.ProjectNamesWhereEnabled)
|
||||
{
|
||||
writer.Write(projectName);
|
||||
}
|
||||
writer.Write7BitEncodedInt(data.ViolationMessagesCount);
|
||||
writer.Write7BitEncodedInt(data.ViolationWarningsCount);
|
||||
writer.Write7BitEncodedInt(data.ViolationErrorsCount);
|
||||
writer.Write(data.IsThrottled);
|
||||
writer.Write(data.TotalRuntime.Ticks);
|
||||
}
|
||||
}
|
||||
|
||||
internal override void CreateFromStream(BinaryReader reader, int version)
|
||||
|
@ -54,14 +83,59 @@ internal sealed class BuildCheckTracingEventArgs(Dictionary<string, TimeSpan> tr
|
|||
base.CreateFromStream(reader, version);
|
||||
|
||||
int count = reader.Read7BitEncodedInt();
|
||||
TracingData = new Dictionary<string, TimeSpan>(count);
|
||||
var infrastructureTracingData = new Dictionary<string, TimeSpan>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string key = reader.ReadString();
|
||||
TimeSpan value = TimeSpan.FromTicks(reader.ReadInt64());
|
||||
|
||||
TracingData.Add(key, value);
|
||||
infrastructureTracingData.Add(key, value);
|
||||
}
|
||||
|
||||
count = reader.Read7BitEncodedInt();
|
||||
List<BuildCheckRuleTelemetryData> tracingData = new List<BuildCheckRuleTelemetryData>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string ruleId = reader.ReadString();
|
||||
string checkFriendlyName = reader.ReadString();
|
||||
bool isBuiltIn = reader.ReadBoolean();
|
||||
DiagnosticSeverity defaultSeverity = (DiagnosticSeverity)reader.Read7BitEncodedInt();
|
||||
int explicitSeveritiesCount = reader.Read7BitEncodedInt();
|
||||
HashSet<DiagnosticSeverity> explicitSeverities =
|
||||
#if NETSTANDARD2_0
|
||||
new HashSet<DiagnosticSeverity>();
|
||||
#else
|
||||
new HashSet<DiagnosticSeverity>(explicitSeveritiesCount);
|
||||
#endif
|
||||
for (int j = 0; j < explicitSeveritiesCount; j++)
|
||||
{
|
||||
explicitSeverities.Add((DiagnosticSeverity)reader.Read7BitEncodedInt());
|
||||
}
|
||||
int projectNamesWhereEnabledCount = reader.Read7BitEncodedInt();
|
||||
HashSet<string> projectNamesWhereEnabled =
|
||||
#if NETSTANDARD2_0
|
||||
new HashSet<string>();
|
||||
#else
|
||||
new HashSet<string>(projectNamesWhereEnabledCount);
|
||||
#endif
|
||||
for (int j = 0; j < projectNamesWhereEnabledCount; j++)
|
||||
{
|
||||
projectNamesWhereEnabled.Add(reader.ReadString());
|
||||
}
|
||||
int violationMessagesCount = reader.Read7BitEncodedInt();
|
||||
int violationWarningsCount = reader.Read7BitEncodedInt();
|
||||
int violationErrorsCount = reader.Read7BitEncodedInt();
|
||||
bool isThrottled = reader.ReadBoolean();
|
||||
TimeSpan totalRuntime = TimeSpan.FromTicks(reader.ReadInt64());
|
||||
|
||||
BuildCheckRuleTelemetryData data = new BuildCheckRuleTelemetryData(
|
||||
ruleId, checkFriendlyName, isBuiltIn, defaultSeverity, explicitSeverities, projectNamesWhereEnabled,
|
||||
violationMessagesCount, violationWarningsCount, violationErrorsCount, isThrottled, totalRuntime);
|
||||
|
||||
tracingData.Add(data);
|
||||
}
|
||||
|
||||
TracingData = new BuildCheckTracingData(tracingData, infrastructureTracingData);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Build.Experimental.BuildCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Telemetry data for a single build check rule.
|
||||
/// </summary>
|
||||
/// <param name="ruleId"></param>
|
||||
/// <param name="checkFriendlyName"></param>
|
||||
/// <param name="isBuiltIn"></param>
|
||||
/// <param name="defaultSeverity"></param>
|
||||
internal sealed class BuildCheckRuleTelemetryData(
|
||||
string ruleId,
|
||||
string checkFriendlyName,
|
||||
bool isBuiltIn,
|
||||
DiagnosticSeverity defaultSeverity)
|
||||
{
|
||||
public BuildCheckRuleTelemetryData(
|
||||
string ruleId,
|
||||
string checkFriendlyName,
|
||||
bool isBuiltIn,
|
||||
DiagnosticSeverity defaultSeverity,
|
||||
HashSet<DiagnosticSeverity> explicitSeverities,
|
||||
HashSet<string> projectNamesWhereEnabled,
|
||||
int violationMessagesCount,
|
||||
int violationWarningsCount,
|
||||
int violationErrorsCount,
|
||||
bool isThrottled,
|
||||
TimeSpan totalRuntime) : this(ruleId, checkFriendlyName, isBuiltIn,
|
||||
defaultSeverity)
|
||||
{
|
||||
ExplicitSeverities = explicitSeverities;
|
||||
ProjectNamesWhereEnabled = projectNamesWhereEnabled;
|
||||
ViolationMessagesCount = violationMessagesCount;
|
||||
ViolationWarningsCount = violationWarningsCount;
|
||||
ViolationErrorsCount = violationErrorsCount;
|
||||
IsThrottled = isThrottled;
|
||||
TotalRuntime = totalRuntime;
|
||||
}
|
||||
|
||||
public static BuildCheckRuleTelemetryData Merge(
|
||||
BuildCheckRuleTelemetryData data1,
|
||||
BuildCheckRuleTelemetryData data2)
|
||||
{
|
||||
if (data1.RuleId != data2.RuleId)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot merge telemetry data for different rules.");
|
||||
}
|
||||
return new BuildCheckRuleTelemetryData(
|
||||
data1.RuleId,
|
||||
data1.CheckFriendlyName,
|
||||
data1.IsBuiltIn,
|
||||
data1.DefaultSeverity,
|
||||
new HashSet<DiagnosticSeverity>(data1.ExplicitSeverities.Union(data2.ExplicitSeverities)),
|
||||
new HashSet<string>(data1.ProjectNamesWhereEnabled.Union(data2.ProjectNamesWhereEnabled)),
|
||||
data1.ViolationMessagesCount + data2.ViolationMessagesCount,
|
||||
data1.ViolationWarningsCount + data2.ViolationWarningsCount,
|
||||
data1.ViolationErrorsCount + data2.ViolationErrorsCount,
|
||||
data1.IsThrottled || data2.IsThrottled,
|
||||
data1.TotalRuntime + data2.TotalRuntime);
|
||||
}
|
||||
|
||||
public string RuleId { get; init; } = ruleId;
|
||||
public string CheckFriendlyName { get; init; } = checkFriendlyName;
|
||||
public bool IsBuiltIn { get; init; } = isBuiltIn;
|
||||
public DiagnosticSeverity DefaultSeverity { get; init; } = defaultSeverity;
|
||||
|
||||
/// <summary>
|
||||
/// A set of explicitly set severities (through editorconfig(s)) for the rule. There can be multiple - as different projects can have different settings.
|
||||
/// </summary>
|
||||
public HashSet<DiagnosticSeverity> ExplicitSeverities { get; init; } = [];
|
||||
public HashSet<string> ProjectNamesWhereEnabled { get; init; } = [];
|
||||
public int ViolationMessagesCount { get; private set; }
|
||||
public int ViolationWarningsCount { get; private set; }
|
||||
public int ViolationErrorsCount { get; private set; }
|
||||
public int ViolationsCount => ViolationMessagesCount + ViolationWarningsCount + ViolationErrorsCount;
|
||||
public bool IsThrottled { get; private set; }
|
||||
public TimeSpan TotalRuntime { get; set; }
|
||||
|
||||
public void IncrementMessagesCount() => ViolationMessagesCount++;
|
||||
public void IncrementWarningsCount() => ViolationWarningsCount++;
|
||||
public void IncrementErrorsCount() => ViolationErrorsCount++;
|
||||
public void SetThrottled() => IsThrottled = true;
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Build.Experimental.BuildCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for the tracing data to be transferred from the worker nodes to the central node.
|
||||
/// </summary>
|
||||
/// <param name="telemetryData"></param>
|
||||
/// <param name="infrastructureTracingData"></param>
|
||||
internal sealed class BuildCheckTracingData(
|
||||
Dictionary<string, BuildCheckRuleTelemetryData> telemetryData,
|
||||
Dictionary<string, TimeSpan> infrastructureTracingData)
|
||||
{
|
||||
public BuildCheckTracingData(IReadOnlyList<BuildCheckRuleTelemetryData> telemetryData, Dictionary<string, TimeSpan> infrastructureTracingData)
|
||||
: this(telemetryData.ToDictionary(data => data.RuleId), infrastructureTracingData)
|
||||
{ }
|
||||
|
||||
public BuildCheckTracingData()
|
||||
: this(new Dictionary<string, BuildCheckRuleTelemetryData>(), [])
|
||||
{ }
|
||||
|
||||
internal BuildCheckTracingData(Dictionary<string, TimeSpan> executionData)
|
||||
: this(new Dictionary<string, BuildCheckRuleTelemetryData>(), executionData)
|
||||
{ }
|
||||
|
||||
public Dictionary<string, BuildCheckRuleTelemetryData> TelemetryData { get; private set; } = telemetryData;
|
||||
public Dictionary<string, TimeSpan> InfrastructureTracingData { get; private set; } = infrastructureTracingData;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the runtime stats per individual checks friendly names
|
||||
/// </summary>
|
||||
public Dictionary<string, TimeSpan> ExtractCheckStats() =>
|
||||
// Stats are per rule, while runtime is per check - and check can have multiple rules.
|
||||
// In case of multi-rule check, the runtime stats are duplicated for each rule.
|
||||
TelemetryData
|
||||
.GroupBy(d => d.Value.CheckFriendlyName)
|
||||
.ToDictionary(g => g.Key, g => g.First().Value.TotalRuntime);
|
||||
|
||||
public void MergeIn(BuildCheckTracingData other)
|
||||
{
|
||||
InfrastructureTracingData.Merge(other.InfrastructureTracingData, (span1, span2) => span1 + span2);
|
||||
TelemetryData.Merge(other.TelemetryData, BuildCheckRuleTelemetryData.Merge);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.Build.Experimental.BuildCheck;
|
||||
|
||||
internal enum DiagnosticSeverity
|
||||
{
|
||||
/// <summary>
|
||||
/// When set, the default value of the BuildCheck rule will be used.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// When set to None the rule will not run.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Information level message.
|
||||
/// </summary>
|
||||
Suggestion,
|
||||
|
||||
/// <summary>
|
||||
/// Results a warning in build if the BuildCheck rule applied.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Results an error in build if the BuildCheck rule applied.
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
|
@ -66,4 +66,33 @@ internal static class EnumerableExtensions
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a content of given list to current dictionary.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="dict">Dictionary to receive another values.</param>
|
||||
/// <param name="another">List to be merged into current.</param>
|
||||
/// <param name="extractKey">Way of getting a key of an incoming value.</param>
|
||||
/// <param name="mergeValues">Way of resolving keys conflicts.</param>
|
||||
public static void Merge<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dict,
|
||||
IReadOnlyList<TValue> another,
|
||||
Func<TValue, TKey> extractKey,
|
||||
Func<TValue, TValue, TValue> mergeValues)
|
||||
{
|
||||
foreach (var mergeValue in another)
|
||||
{
|
||||
TKey key = extractKey(mergeValue);
|
||||
if (!dict.TryGetValue(key, out TValue? value))
|
||||
{
|
||||
dict[key] = mergeValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
dict[key] = mergeValues(value, mergeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -127,7 +127,7 @@ public abstract class BuildExceptionBase : Exception
|
|||
string? deserializedStackTrace = reader.ReadOptionalString();
|
||||
string? source = reader.ReadOptionalString();
|
||||
string? helpLink = reader.ReadOptionalString();
|
||||
int hResult = reader.ReadOptionalInt32();
|
||||
int hResult = reader.ReadOptionalInt32() ?? 0;
|
||||
|
||||
IDictionary<string, string?>? customKeyedSerializedData = null;
|
||||
if (reader.ReadByte() == 1)
|
||||
|
|
|
@ -664,11 +664,20 @@ internal static class NativeMethods
|
|||
}
|
||||
}
|
||||
|
||||
private static SAC_State? s_sacState;
|
||||
|
||||
/// <summary>
|
||||
/// Get from registry state of the Smart App Control (SAC) on the system.
|
||||
/// </summary>
|
||||
/// <returns>State of SAC</returns>
|
||||
internal static SAC_State GetSACState()
|
||||
{
|
||||
s_sacState ??= GetSACStateInternal();
|
||||
|
||||
return s_sacState.Value;
|
||||
}
|
||||
|
||||
internal static SAC_State GetSACStateInternal()
|
||||
{
|
||||
if (IsWindows)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Experimental.BuildCheck;
|
||||
|
||||
namespace Microsoft.Build.Framework.Telemetry;
|
||||
|
||||
internal class BuildCheckTelemetry
|
||||
{
|
||||
private const string FailedAcquisitionEventName = "buildcheck/acquisitionfailure";
|
||||
private const string RunEventName = "buildcheck/run";
|
||||
private const string RuleStatsEventName = "buildcheck/rule";
|
||||
private Guid _submissionId = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Translates failed acquisition event to telemetry transport data.
|
||||
/// </summary>
|
||||
internal (string, IDictionary<string, string>) ProcessCustomCheckLoadingFailure(string assemblyName,
|
||||
Exception exception)
|
||||
{
|
||||
var properties = new Dictionary<string, string>();
|
||||
properties["SubmissionId"] = _submissionId.ToString();
|
||||
properties["AssemblyName"] = assemblyName;
|
||||
string? exceptionType = exception.GetType().FullName;
|
||||
if (exceptionType != null)
|
||||
{
|
||||
properties["ExceptionType"] = exceptionType;
|
||||
}
|
||||
if (exception.Message != null)
|
||||
{
|
||||
properties["ExceptionMessage"] = exception.Message;
|
||||
}
|
||||
|
||||
return (FailedAcquisitionEventName, properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates BuildCheck tracing data to telemetry transport data.
|
||||
/// </summary>
|
||||
internal IEnumerable<(string, IDictionary<string, string>)> ProcessBuildCheckTracingData(BuildCheckTracingData data)
|
||||
{
|
||||
int rulesCount = data.TelemetryData.Count;
|
||||
int customRulesCount = data.TelemetryData.Count(t => !t.Value.IsBuiltIn);
|
||||
int violationsCount = data.TelemetryData.Sum(t => t.Value.ViolationsCount);
|
||||
long runtimeTicks = data.ExtractCheckStats().Sum(v => v.Value.Ticks);
|
||||
runtimeTicks += data.InfrastructureTracingData.Sum(v => v.Value.Ticks);
|
||||
TimeSpan totalRuntime = new TimeSpan(runtimeTicks);
|
||||
|
||||
var properties = new Dictionary<string, string>();
|
||||
properties["SubmissionId"] = _submissionId.ToString();
|
||||
properties["RulesCount"] = rulesCount.ToString(CultureInfo.InvariantCulture);
|
||||
properties["CustomRulesCount"] = customRulesCount.ToString(CultureInfo.InvariantCulture);
|
||||
properties["ViolationsCount"] = violationsCount.ToString(CultureInfo.InvariantCulture);
|
||||
properties["TotalRuntimeInMilliseconds"] = totalRuntime.TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
yield return (RunEventName, properties);
|
||||
|
||||
foreach (BuildCheckRuleTelemetryData buildCheckRuleTelemetryData in data.TelemetryData.Values)
|
||||
{
|
||||
properties = new Dictionary<string, string>();
|
||||
properties["SubmissionId"] = _submissionId.ToString();
|
||||
properties["RuleId"] = buildCheckRuleTelemetryData.RuleId;
|
||||
properties["CheckFriendlyName"] = buildCheckRuleTelemetryData.CheckFriendlyName;
|
||||
properties["IsBuiltIn"] = buildCheckRuleTelemetryData.IsBuiltIn.ToString(CultureInfo.InvariantCulture);
|
||||
properties["DefaultSeverityId"] = ((int)buildCheckRuleTelemetryData.DefaultSeverity).ToString(CultureInfo.InvariantCulture);
|
||||
properties["DefaultSeverity"] = buildCheckRuleTelemetryData.DefaultSeverity.ToString();
|
||||
properties["EnabledProjectsCount"] = buildCheckRuleTelemetryData.ProjectNamesWhereEnabled.Count.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
if (buildCheckRuleTelemetryData.ExplicitSeverities.Any())
|
||||
{
|
||||
properties["ExplicitSeverities"] = buildCheckRuleTelemetryData.ExplicitSeverities
|
||||
.Select(s => s.ToString()).ToCsvString(false);
|
||||
properties["ExplicitSeveritiesIds"] = buildCheckRuleTelemetryData.ExplicitSeverities
|
||||
.Select(s => ((int)s).ToString(CultureInfo.InvariantCulture)).ToCsvString(false);
|
||||
}
|
||||
|
||||
properties["ViolationMessagesCount"] = buildCheckRuleTelemetryData.ViolationMessagesCount.ToString(CultureInfo.InvariantCulture);
|
||||
properties["ViolationWarningsCount"] = buildCheckRuleTelemetryData.ViolationWarningsCount.ToString(CultureInfo.InvariantCulture);
|
||||
properties["ViolationErrorsCount"] = buildCheckRuleTelemetryData.ViolationErrorsCount.ToString(CultureInfo.InvariantCulture);
|
||||
properties["IsThrottled"] = buildCheckRuleTelemetryData.IsThrottled.ToString(CultureInfo.InvariantCulture);
|
||||
properties["TotalRuntimeInMilliseconds"] = buildCheckRuleTelemetryData.TotalRuntime.TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
yield return (RuleStatsEventName, properties);
|
||||
}
|
||||
|
||||
|
||||
// set for the new submission in case of build server
|
||||
_submissionId = Guid.NewGuid();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +74,16 @@ namespace Microsoft.Build.Framework.Telemetry
|
|||
/// </summary>
|
||||
public string? Host { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if buildcheck was used.
|
||||
/// </summary>
|
||||
public bool? BuildCheckEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if Smart Application Control was enabled.
|
||||
/// </summary>
|
||||
public bool? SACEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// State of MSBuild server process before this build.
|
||||
/// One of 'cold', 'hot', null (if not run as server)
|
||||
|
@ -145,6 +155,16 @@ namespace Microsoft.Build.Framework.Telemetry
|
|||
properties["BuildEngineVersion"] = Version.ToString();
|
||||
}
|
||||
|
||||
if (BuildCheckEnabled != null)
|
||||
{
|
||||
properties["BuildCheckEnabled"] = BuildCheckEnabled.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (SACEnabled != null)
|
||||
{
|
||||
properties["SACEnabled"] = SACEnabled.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,4 +20,9 @@ internal static class KnownTelemetry
|
|||
/// Describes how logging was configured.
|
||||
/// </summary>
|
||||
public static LoggingConfigurationTelemetry LoggingConfigurationTelemetry { get; } = new LoggingConfigurationTelemetry();
|
||||
|
||||
/// <summary>
|
||||
/// Describes if and how BuildCheck was used.
|
||||
/// </summary>
|
||||
public static BuildCheckTelemetry BuildCheckTelemetry { get; } = new BuildCheckTelemetry();
|
||||
}
|
||||
|
|
|
@ -22,9 +22,9 @@ namespace Microsoft.Build.Shared
|
|||
#if !TASKHOST
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
public static int ReadOptionalInt32(this BinaryReader reader)
|
||||
public static int? ReadOptionalInt32(this BinaryReader reader)
|
||||
{
|
||||
return reader.ReadByte() == 0 ? 0 : reader.ReadInt32();
|
||||
return reader.ReadByte() == 0 ? null : reader.ReadInt32();
|
||||
}
|
||||
|
||||
#if !TASKHOST
|
||||
|
|
Загрузка…
Ссылка в новой задаче