File: BackEnd\Components\Logging\BuildErrorTelemetryTracker.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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.Runtime.CompilerServices;
using Microsoft.Build.Framework.Telemetry;
using static Microsoft.Build.Framework.Telemetry.BuildInsights;
 
#nullable enable
 
namespace Microsoft.Build.BackEnd.Logging
{
    /// <summary>
    /// Tracks and categorizes build errors for telemetry purposes.
    /// </summary>
    internal sealed class BuildErrorTelemetryTracker
    {
        // Use an enum internally for efficient tracking, convert to string only when needed
        internal enum ErrorCategory
        {
            Compiler,
            MSBuildGeneral,
            MSBuildEvaluation,
            MSBuildExecution,
            MSBuildGraph,
            Tasks,
            SDKResolvers,
            NETSDK,
            NuGet,
            BuildCheck,
            NativeToolchain,
            CodeAnalysis,
            Razor,
            WPF,
            AspNet,
            Other,
        }
 
        /// <summary>
        /// Error counts by category index (using enum ordinal).
        /// </summary>
        private readonly int[] _errorCounts = new int[Enum.GetValues(typeof(ErrorCategory)).Length];
 
        /// <summary>
        /// Tracks the primary failure category (category with highest count).
        /// </summary>
        private ErrorCategory _primaryCategory;
 
        /// <summary>
        /// Tracks the highest error count for primary category determination.
        /// </summary>
        private int _primaryCategoryCount;
 
        /// <summary>
        /// Tracks an error for telemetry purposes by categorizing it.
        /// </summary>
        /// <param name="errorCode">The error code from the BuildErrorEventArgs.</param>
        /// <param name="subcategory">The subcategory from the BuildErrorEventArgs.</param>
        public void TrackError(string? errorCode, string? subcategory)
        {
            // Categorize the error
            ErrorCategory category = CategorizeError(errorCode, subcategory);
            int categoryIndex = (int)category;
 
            // Increment the count for this category using Interlocked for thread safety
            int newCount = System.Threading.Interlocked.Increment(ref _errorCounts[categoryIndex]);
 
            // Update primary category if this one now has the highest count
            // Use a simple compare-and-swap pattern for thread-safe update
            int currentMax = System.Threading.Interlocked.CompareExchange(ref _primaryCategoryCount, 0, 0);
            if (newCount > currentMax)
            {
                // Try to update both the count and category atomically
                if (System.Threading.Interlocked.CompareExchange(ref _primaryCategoryCount, newCount, currentMax) == currentMax)
                {
                    _primaryCategory = category;
                }
            }
        }
 
        /// <summary>
        /// Populates build telemetry with error categorization data.
        /// </summary>
        /// <param name="buildTelemetry">The BuildTelemetry object to populate with error data.</param>
        public void PopulateBuildTelemetry(BuildTelemetry buildTelemetry)
        {
            buildTelemetry.ErrorCounts = new ErrorCountsInfo(
                Compiler: GetCountOrNull(ErrorCategory.Compiler),
                MsBuildGeneral: GetCountOrNull(ErrorCategory.MSBuildGeneral),
                MsBuildEvaluation: GetCountOrNull(ErrorCategory.MSBuildEvaluation),
                MsBuildExecution: GetCountOrNull(ErrorCategory.MSBuildExecution),
                MsBuildGraph: GetCountOrNull(ErrorCategory.MSBuildGraph),
                Task: GetCountOrNull(ErrorCategory.Tasks),
                SdkResolvers: GetCountOrNull(ErrorCategory.SDKResolvers),
                NetSdk: GetCountOrNull(ErrorCategory.NETSDK),
                NuGet: GetCountOrNull(ErrorCategory.NuGet),
                BuildCheck: GetCountOrNull(ErrorCategory.BuildCheck),
                NativeToolchain: GetCountOrNull(ErrorCategory.NativeToolchain),
                CodeAnalysis: GetCountOrNull(ErrorCategory.CodeAnalysis),
                Razor: GetCountOrNull(ErrorCategory.Razor),
                Wpf: GetCountOrNull(ErrorCategory.WPF),
                AspNet: GetCountOrNull(ErrorCategory.AspNet),
                Other: GetCountOrNull(ErrorCategory.Other));
 
            // Set the primary failure category
            if (_primaryCategoryCount > 0)
            {
                buildTelemetry.FailureCategory = _primaryCategory.ToString();
            }
        }
 
        /// <summary>
        /// Gets the error count for a category, returning null if zero.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private int? GetCountOrNull(ErrorCategory category)
        {
            int count = System.Threading.Interlocked.CompareExchange(ref _errorCounts[(int)category], 0, 0);
            return count > 0 ? count : null;
        }
 
        /// <summary>
        /// Categorizes an error based on its error code and subcategory.
        /// Uses a two-level character switch for O(1) prefix matching.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static ErrorCategory CategorizeError(string? errorCode, string? subcategory)
        {
            if (string.IsNullOrEmpty(errorCode))
            {
                return ErrorCategory.Other;
            }
 
            // Check subcategory for compiler errors (CS*, VBC*, FS*)
            if (!string.IsNullOrEmpty(subcategory) && IsCompilerPrefix(subcategory!))
            {
                return ErrorCategory.Compiler;
            }
 
            // Check error code patterns
            if (IsCompilerPrefix(errorCode!))
            {
                return ErrorCategory.Compiler;
            }
 
            if (errorCode!.Length < 2)
            {
                return ErrorCategory.Other;
            }
 
            // Two-level switch on first two characters for efficient prefix matching
            char c0 = char.ToUpperInvariant(errorCode[0]);
            char c1 = char.ToUpperInvariant(errorCode[1]);
 
            return (c0, c1) switch
            {
                // A*
                ('A', 'S') when StartsWithASP(errorCode) => ErrorCategory.AspNet,
 
                // B*
                ('B', 'C') => ErrorCategory.BuildCheck,
                ('B', 'L') => ErrorCategory.AspNet,  // Blazor
 
                // C* (careful: CS is handled by IsCompilerPrefix above)
                ('C', 'A') => ErrorCategory.CodeAnalysis,
                ('C', 'L') => ErrorCategory.NativeToolchain,
                ('C', 'V') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'T' => ErrorCategory.NativeToolchain,  // CVT*
                ('C', >= '0' and <= '9') => ErrorCategory.NativeToolchain,  // C1*, C2*, C4* (C/C++ compiler)
 
                // I*
                ('I', 'D') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'E' => ErrorCategory.CodeAnalysis,  // IDE*
 
                // L*
                ('L', 'N') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'K' => ErrorCategory.NativeToolchain,  // LNK*
 
                // M*
                ('M', 'C') => ErrorCategory.WPF,  // MC* (Markup Compiler)
                ('M', 'S') when errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'B' => CategorizeMSBError(errorCode.AsSpan()),
                ('M', 'T') => ErrorCategory.NativeToolchain,  // MT* (Manifest Tool)
 
                // N*
                ('N', 'E') when StartsWithNETSDK(errorCode) => ErrorCategory.NETSDK,
                ('N', 'U') => ErrorCategory.NuGet,
 
                // R*
                ('R', 'C') => ErrorCategory.NativeToolchain,  // RC* (Resource Compiler)
                ('R', 'Z') => ErrorCategory.Razor,
 
                // X*
                ('X', 'C') => ErrorCategory.WPF,  // XC* (XAML Compiler)
 
                _ => ErrorCategory.Other
            };
        }
 
        /// <summary>
        /// Checks if the error code starts with "ASP" (case-insensitive).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool StartsWithASP(string errorCode)
            => errorCode.Length >= 3 && char.ToUpperInvariant(errorCode[2]) == 'P';
 
        /// <summary>
        /// Checks if the error code starts with "NETSDK" (case-insensitive).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool StartsWithNETSDK(string errorCode)
            => errorCode.Length >= 6 &&
               char.ToUpperInvariant(errorCode[2]) == 'T' &&
               char.ToUpperInvariant(errorCode[3]) == 'S' &&
               char.ToUpperInvariant(errorCode[4]) == 'D' &&
               char.ToUpperInvariant(errorCode[5]) == 'K';
 
        /// <summary>
        /// Checks if the string starts with a compiler error prefix (CS, FS, VBC).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool IsCompilerPrefix(string value)
        {
            if (value.Length < 2)
            {
                return false;
            }
 
            char c0 = char.ToUpperInvariant(value[0]);
            char c1 = char.ToUpperInvariant(value[1]);
 
            return (c0, c1) switch
            {
                ('C', 'S') => true,  // CS* -> C# compiler
                ('F', 'S') => true,  // FS* -> F# compiler
                ('V', 'B') => value.Length >= 3 && char.ToUpperInvariant(value[2]) == 'C',  // VBC* -> VB compiler
                _ => false
            };
        }
 
        /// <summary>
        /// Categorizes MSB error codes into granular MSBuild categories.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static ErrorCategory CategorizeMSBError(ReadOnlySpan<char> codeSpan)
        {
            // MSB error codes: 3-letter prefix + 4-digit number (e.g., MSB3026)
            if (codeSpan.Length < 7 || !TryParseErrorNumber(codeSpan, out int errorNumber))
            {
                return ErrorCategory.Other;
            }
 
            return errorNumber switch
            {
                >= 3001 and <= 3999 => ErrorCategory.Tasks,
                >= 4001 and <= 4099 => ErrorCategory.MSBuildGeneral,
                >= 4100 and <= 4199 => ErrorCategory.MSBuildEvaluation,
                >= 4200 and <= 4299 => ErrorCategory.SDKResolvers,
                >= 4300 and <= 4399 => ErrorCategory.MSBuildExecution,
                >= 4400 and <= 4499 => ErrorCategory.MSBuildGraph,
                >= 4500 and <= 4999 => ErrorCategory.MSBuildGeneral,
                >= 5001 and <= 5999 => ErrorCategory.MSBuildExecution,
                >= 6001 and <= 6999 => ErrorCategory.MSBuildExecution,
                _ => ErrorCategory.Other
            };
        }
 
        /// <summary>
        /// Parses the 4-digit error number from an MSB error code span (e.g., "MSB3026" -> 3026).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool TryParseErrorNumber(ReadOnlySpan<char> codeSpan, out int errorNumber)
        {
            // Extract digits after "MSB" prefix (positions 3-6)
            ReadOnlySpan<char> digits = codeSpan.Slice(3, 4);
#if NET
            return int.TryParse(digits, out errorNumber);
#else
            return int.TryParse(digits.ToString(), out errorNumber);
#endif
        }
    }
}