diff --git a/Directory.Packages.props b/Directory.Packages.props index 00b24ad6f..a63698699 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,8 +33,7 @@ - - + diff --git a/src/tools/PerfDiff/BDN/BenchmarkComparisonService.cs b/src/tools/PerfDiff/BDN/BenchmarkComparisonService.cs index f4253dd7c..c35aec1bb 100644 --- a/src/tools/PerfDiff/BDN/BenchmarkComparisonService.cs +++ b/src/tools/PerfDiff/BDN/BenchmarkComparisonService.cs @@ -23,10 +23,11 @@ public class BenchmarkComparisonService(ILogger logger) /// /// The folder containing baseline results. /// The folder containing new results. + /// Token to observe for cancellation requests. /// A indicating comparison success and regression detection. - public async Task CompareAsync(string baselineFolder, string resultsFolder) + public async Task CompareAsync(string baselineFolder, string resultsFolder, CancellationToken cancellationToken) { - BdnComparisonResult[]? comparison = await BenchmarkDotNetDiffer.TryGetBdnResultsAsync(baselineFolder, resultsFolder, logger).ConfigureAwait(false); + BdnComparisonResult[]? comparison = await BenchmarkDotNetDiffer.TryGetBdnResultsAsync(baselineFolder, resultsFolder, logger, cancellationToken).ConfigureAwait(false); if (comparison is null) { return new BenchmarkComparisonResult(CompareSucceeded: false, RegressionDetected: false); diff --git a/src/tools/PerfDiff/BDN/BenchmarkDotNetDiffer.cs b/src/tools/PerfDiff/BDN/BenchmarkDotNetDiffer.cs index ea7fe2d7c..6e1cee5a1 100644 --- a/src/tools/PerfDiff/BDN/BenchmarkDotNetDiffer.cs +++ b/src/tools/PerfDiff/BDN/BenchmarkDotNetDiffer.cs @@ -24,11 +24,12 @@ public static class BenchmarkDotNetDiffer /// The folder containing baseline results. /// The folder containing new results. /// Logger for reporting errors. + /// Token to observe for cancellation requests. /// A indicating comparison success and regression detection. - public static async Task TryCompareBenchmarkDotNetResultsAsync(string baselineFolder, string resultsFolder, ILogger logger) + public static async Task TryCompareBenchmarkDotNetResultsAsync(string baselineFolder, string resultsFolder, ILogger logger, CancellationToken cancellationToken) { BenchmarkComparisonService service = new(logger); - return await service.CompareAsync(baselineFolder, resultsFolder).ConfigureAwait(false); + return await service.CompareAsync(baselineFolder, resultsFolder, cancellationToken).ConfigureAwait(false); } /// @@ -37,8 +38,9 @@ public static async Task TryCompareBenchmarkDotNetRes /// The folder containing baseline results. /// The folder containing new results. /// Logger for reporting errors. + /// Token to observe for cancellation requests. /// An array of if successful; otherwise, . - internal static async Task TryGetBdnResultsAsync(string baselineFolder, string resultsFolder, ILogger logger) + internal static async Task TryGetBdnResultsAsync(string baselineFolder, string resultsFolder, ILogger logger, CancellationToken cancellationToken) { if (!TryGetFilesToParse(baselineFolder, out string[]? baseFiles)) { @@ -52,19 +54,19 @@ public static async Task TryCompareBenchmarkDotNetRes return null; } - if (!baseFiles.Any() || !resultsFiles.Any()) + if (baseFiles.Length == 0 || resultsFiles.Length == 0) { - logger.LogError($"Provided paths contained no '{FullBdnJsonFileExtension}' files."); + logger.LogError("Provided paths contained no '{FileExtension}' files.", FullBdnJsonFileExtension); return null; } - (bool baseResultsSuccess, BdnResult?[] baseResults) = await BenchmarkFileReader.TryGetBdnResultAsync(baseFiles, logger).ConfigureAwait(false); + (bool baseResultsSuccess, BdnResult?[] baseResults) = await BenchmarkFileReader.TryGetBdnResultAsync(baseFiles, logger, cancellationToken).ConfigureAwait(false); if (!baseResultsSuccess) { return null; } - (bool resultsSuccess, BdnResult?[] diffResults) = await BenchmarkFileReader.TryGetBdnResultAsync(resultsFiles, logger).ConfigureAwait(false); + (bool resultsSuccess, BdnResult?[] diffResults) = await BenchmarkFileReader.TryGetBdnResultAsync(resultsFiles, logger, cancellationToken).ConfigureAwait(false); if (!resultsSuccess) { return null; @@ -78,10 +80,16 @@ public static async Task TryCompareBenchmarkDotNetRes .SelectMany(result => result?.Benchmarks ?? Enumerable.Empty()) .ToDictionary(benchmarkResult => benchmarkResult.FullName ?? $"Unknown-{Guid.NewGuid():N}", benchmarkResult => benchmarkResult); - return benchmarkIdToBaseResults - .Where(baseResult => benchmarkIdToDiffResults.ContainsKey(baseResult.Key)) - .Select(baseResult => new BdnComparisonResult(baseResult.Key, baseResult.Value, benchmarkIdToDiffResults[baseResult.Key])) - .ToArray(); + List matched = []; + foreach (KeyValuePair baseResult in benchmarkIdToBaseResults) + { + if (benchmarkIdToDiffResults.TryGetValue(baseResult.Key, out Benchmark? diffBenchmark)) + { + matched.Add(new BdnComparisonResult(baseResult.Key, baseResult.Value, diffBenchmark)); + } + } + + return matched.ToArray(); } private static bool TryGetFilesToParse(string path, [NotNullWhen(true)] out string[]? files) diff --git a/src/tools/PerfDiff/BDN/BenchmarkFileReader.cs b/src/tools/PerfDiff/BDN/BenchmarkFileReader.cs index 7012e615d..4dad3e7fa 100644 --- a/src/tools/PerfDiff/BDN/BenchmarkFileReader.cs +++ b/src/tools/PerfDiff/BDN/BenchmarkFileReader.cs @@ -15,18 +15,19 @@ internal static class BenchmarkFileReader /// /// Array of file paths to read. /// Logger for reporting errors. + /// Token to observe for cancellation requests. /// A containing the loaded results and success status. - public static async Task TryGetBdnResultAsync(string[] paths, ILogger logger) + public static async Task TryGetBdnResultAsync(string[] paths, ILogger logger, CancellationToken cancellationToken) { - BdnResult?[] results = await Task.WhenAll(paths.Select(path => ReadFromFileAsync(path, logger))).ConfigureAwait(false); - return new BdnResults(!results.Any(static x => x is null), results); + BdnResult?[] results = await Task.WhenAll(paths.Select(path => ReadFromFileAsync(path, logger, cancellationToken))).ConfigureAwait(false); + return new BdnResults(Array.TrueForAll(results, static x => x is not null), results); } - private static async Task ReadFromFileAsync(string resultFilePath, ILogger logger) + private static async Task ReadFromFileAsync(string resultFilePath, ILogger logger, CancellationToken cancellationToken) { try { - return JsonConvert.DeserializeObject(await File.ReadAllTextAsync(resultFilePath).ConfigureAwait(false)); + return JsonConvert.DeserializeObject(await File.ReadAllTextAsync(resultFilePath, cancellationToken).ConfigureAwait(false)); } catch (Exception ex) when (ex is JsonReaderException or JsonSerializationException or IOException or UnauthorizedAccessException or SecurityException) { diff --git a/src/tools/PerfDiff/DiffCommand.cs b/src/tools/PerfDiff/DiffCommand.cs index b9939440a..c35a8f4f4 100644 --- a/src/tools/PerfDiff/DiffCommand.cs +++ b/src/tools/PerfDiff/DiffCommand.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Frozen; using System.CommandLine; +using Microsoft.Extensions.Logging; namespace PerfDiff; @@ -10,25 +12,69 @@ namespace PerfDiff; internal static class DiffCommand { /// - /// Delegate for handling the diff command. + /// Maps verbosity strings to their corresponding log levels. /// - /// Baseline results folder. - /// Results folder. - /// Verbosity level. - /// Whether to fail on regression. - /// Console for output. - /// Exit code. - internal delegate Task Handler( - string baseline, - string results, - string? verbosity, - bool failOnRegression, - IConsole console); + private static readonly FrozenDictionary VerbosityMap = new Dictionary(10) + { + ["q"] = LogLevel.Error, + ["quiet"] = LogLevel.Error, + ["m"] = LogLevel.Warning, + ["minimal"] = LogLevel.Warning, + ["n"] = LogLevel.Information, + ["normal"] = LogLevel.Information, + ["d"] = LogLevel.Debug, + ["detailed"] = LogLevel.Debug, + ["diag"] = LogLevel.Trace, + ["diagnostic"] = LogLevel.Trace, + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets the baseline option. + /// + internal static Option BaselineOption { get; } = CreateFilePathOption("--baseline", "folder that contains the baseline performance run data"); /// - /// Gets the allowed verbosity levels for the command. + /// Gets the results option. /// - private static readonly string[] VerbosityLevels = ["q", "quiet", "m", "minimal", "n", "normal", "d", "detailed", "diag", "diagnostic"]; + internal static Option ResultsOption { get; } = CreateFilePathOption("--results", "folder that contains the performance results"); + + /// + /// Gets the verbosity option. + /// + internal static Option VerbosityOption { get; } = CreateVerbosityOption(); + + /// + /// Gets the fail-on-regression option. + /// + internal static Option FailOnRegressionOption { get; } = new("--failOnRegression") + { + Description = "Should return non-zero exit code if regression detected", + }; + + private static Option CreateFilePathOption(string name, string description) + { + Option option = new(name) { Description = description, Required = true }; + option.AcceptLegalFilePathsOnly(); + return option; + } + + /// + /// Returns the for the given verbosity string. + /// Falls back to when the value is null or unrecognized. + /// + /// The verbosity string from the command line. + /// The corresponding . + internal static LogLevel GetLogLevel(string? verbosity) + => verbosity is not null && VerbosityMap.TryGetValue(verbosity, out LogLevel level) + ? level + : LogLevel.Information; + + private static Option CreateVerbosityOption() + { + Option option = new("--verbosity", "-v") { Description = "Set the verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" }; + option.AcceptOnlyFromAmong(VerbosityMap.Keys.ToArray()); + return option; + } /// /// Creates the root command with options for the diff command. @@ -36,17 +82,14 @@ internal delegate Task Handler( /// The configured . internal static RootCommand CreateCommandLineOptions() { - // Sync changes to option and argument names with the FormatCommand.Handler above. - RootCommand rootCommand = new RootCommand + RootCommand rootCommand = new RootCommand("diff two sets of performance results") { - new Option("--baseline", () => null, "folder that contains the baseline performance run data").LegalFilePathsOnly(), - new Option("--results", () => null, "folder that contains the performance results").LegalFilePathsOnly(), - new Option(["--verbosity", "-v"], "Set the verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]").FromAmong(VerbosityLevels), - new Option(["--failOnRegression"], "Should return non-zero exit code if regression detected"), + BaselineOption, + ResultsOption, + VerbosityOption, + FailOnRegressionOption, }; - rootCommand.Description = "diff two sets of performance results"; - return rootCommand; } } diff --git a/src/tools/PerfDiff/Logging/SimpleConsoleLogger.cs b/src/tools/PerfDiff/Logging/SimpleConsoleLogger.cs index 234a950f4..505187673 100644 --- a/src/tools/PerfDiff/Logging/SimpleConsoleLogger.cs +++ b/src/tools/PerfDiff/Logging/SimpleConsoleLogger.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. -using System.Collections.Immutable; -using System.CommandLine; -using System.CommandLine.Rendering; +using System.Collections.Frozen; using Microsoft.Extensions.Logging; namespace PerfDiff.Logging; @@ -12,14 +10,12 @@ namespace PerfDiff.Logging; /// internal sealed class SimpleConsoleLogger : ILogger { - private readonly Lock _gate = new(); + private static readonly Lock Gate = new(); - private readonly IConsole _console; - private readonly ITerminal _terminal; private readonly LogLevel _minimalLogLevel; private readonly LogLevel _minimalErrorLevel; - private static ImmutableDictionary LogLevelColorMap => new Dictionary + private static readonly FrozenDictionary LogLevelColorMap = new Dictionary { [LogLevel.Critical] = ConsoleColor.Red, [LogLevel.Error] = ConsoleColor.Red, @@ -28,18 +24,15 @@ internal sealed class SimpleConsoleLogger : ILogger [LogLevel.Debug] = ConsoleColor.Gray, [LogLevel.Trace] = ConsoleColor.Gray, [LogLevel.None] = ConsoleColor.White, - }.ToImmutableDictionary(); + }.ToFrozenDictionary(); /// /// Initializes a new instance of the class. /// - /// The console to write output to. /// The minimal log level for output. /// The minimal log level for error output. - public SimpleConsoleLogger(IConsole console, LogLevel minimalLogLevel, LogLevel minimalErrorLevel) + public SimpleConsoleLogger(LogLevel minimalLogLevel, LogLevel minimalErrorLevel) { - _terminal = console.GetTerminal(); - _console = console; _minimalLogLevel = minimalLogLevel; _minimalErrorLevel = minimalErrorLevel; } @@ -52,17 +45,20 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except return; } - lock (_gate) + lock (Gate) { string message = formatter(state, exception); bool logToErrorStream = logLevel >= _minimalErrorLevel; - if (_terminal is null) + ConsoleColor messageColor = LogLevelColorMap[logLevel]; + + Console.ForegroundColor = messageColor; + try { - LogToConsole(_console, message, logToErrorStream); + LogToConsole(message, logToErrorStream); } - else + finally { - LogToTerminal(message, logLevel, logToErrorStream); + Console.ResetColor(); } } } @@ -80,25 +76,15 @@ public IDisposable BeginScope(TState state) return NullScope.Instance; } - private void LogToTerminal(string message, LogLevel logLevel, bool logToErrorStream) - { - ConsoleColor messageColor = LogLevelColorMap[logLevel]; - _terminal.ForegroundColor = messageColor; - - LogToConsole(_terminal, message, logToErrorStream); - - _terminal.ResetColor(); - } - - private static void LogToConsole(IConsole console, string message, bool logToErrorStream) + private static void LogToConsole(string message, bool logToErrorStream) { if (logToErrorStream) { - console.Error.Write($"{message}{Environment.NewLine}"); + Console.Error.Write($"{message}{Environment.NewLine}"); } else { - console.Out.Write($" {message}{Environment.NewLine}"); + Console.Out.Write($" {message}{Environment.NewLine}"); } } } diff --git a/src/tools/PerfDiff/Logging/SimpleConsoleLoggerFactoryExtensions.cs b/src/tools/PerfDiff/Logging/SimpleConsoleLoggerFactoryExtensions.cs deleted file mode 100644 index 7cfcbe746..000000000 --- a/src/tools/PerfDiff/Logging/SimpleConsoleLoggerFactoryExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. - -using System.CommandLine; - -using Microsoft.Extensions.Logging; - -namespace PerfDiff.Logging; - -internal static class SimpleConsoleLoggerFactoryExtensions -{ - public static ILoggerFactory AddSimpleConsole(this ILoggerFactory factory, IConsole console, LogLevel minimalLogLevel, LogLevel minimalErrorLevel) - { - factory.AddProvider(new SimpleConsoleLoggerProvider(console, minimalLogLevel, minimalErrorLevel)); - return factory; - } -} diff --git a/src/tools/PerfDiff/Logging/SimpleConsoleLoggerProvider.cs b/src/tools/PerfDiff/Logging/SimpleConsoleLoggerProvider.cs index 6f404b0dc..8c78446cc 100644 --- a/src/tools/PerfDiff/Logging/SimpleConsoleLoggerProvider.cs +++ b/src/tools/PerfDiff/Logging/SimpleConsoleLoggerProvider.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. -using System.CommandLine; using Microsoft.Extensions.Logging; namespace PerfDiff.Logging; @@ -10,27 +9,22 @@ namespace PerfDiff.Logging; /// internal sealed class SimpleConsoleLoggerProvider : ILoggerProvider { - private readonly IConsole _console; - private readonly LogLevel _minimalLogLevel; - private readonly LogLevel _minimalErrorLevel; + private readonly SimpleConsoleLogger _logger; /// /// Initializes a new instance of the class. /// - /// The console to write output to. /// The minimal log level for output. /// The minimal log level for error output. - public SimpleConsoleLoggerProvider(IConsole console, LogLevel minimalLogLevel, LogLevel minimalErrorLevel) + public SimpleConsoleLoggerProvider(LogLevel minimalLogLevel, LogLevel minimalErrorLevel) { - _console = console; - _minimalLogLevel = minimalLogLevel; - _minimalErrorLevel = minimalErrorLevel; + _logger = new SimpleConsoleLogger(minimalLogLevel, minimalErrorLevel); } /// public ILogger CreateLogger(string categoryName) { - return new SimpleConsoleLogger(_console, _minimalLogLevel, _minimalErrorLevel); + return _logger; } /// diff --git a/src/tools/PerfDiff/PerfDiff.cs b/src/tools/PerfDiff/PerfDiff.cs index 6a3bc8d06..faf01faf9 100644 --- a/src/tools/PerfDiff/PerfDiff.cs +++ b/src/tools/PerfDiff/PerfDiff.cs @@ -3,103 +3,88 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using PerfDiff.BDN; -using PerfDiff.BDN.DataContracts; using PerfDiff.ETL; namespace PerfDiff; -public static class PerfDiff +internal static class PerfDiff { - public static async Task CompareAsync( +#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates +#pragma warning disable CA2254 // The logging message template should not vary between calls + internal static async Task CompareAsync( string baselineFolder, string resultsFolder, bool failOnRegression, ILogger logger, CancellationToken token) { token.ThrowIfCancellationRequested(); - (bool compareSucceeded, bool regressionDetected) = await BenchmarkDotNetDiffer.TryCompareBenchmarkDotNetResultsAsync(baselineFolder, resultsFolder, logger).ConfigureAwait(false); + (bool compareSucceeded, bool regressionDetected) = await BenchmarkDotNetDiffer.TryCompareBenchmarkDotNetResultsAsync(baselineFolder, resultsFolder, logger, token).ConfigureAwait(false); if (!compareSucceeded) { -#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogError(ILogger, string?, params object?[])' logger.LogError("Failed to compare the performance results see log."); -#pragma warning restore CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogError(ILogger, string?, params object?[])' return 1; } if (!regressionDetected) { -#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' logger.LogTrace("No performance regression found."); -#pragma warning restore CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' return 0; } - (bool etlCompareSucceeded, bool etlRegressionDetected) = CheckEltTraces(baselineFolder, resultsFolder, failOnRegression); + (bool etlCompareSucceeded, bool etlRegressionDetected) = CheckEltTraces(baselineFolder, resultsFolder, logger); if (!etlCompareSucceeded) { -#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' - logger.LogTrace("We detected a regression in BenchmarkDotNet and there is no ETL info."); -#pragma warning restore CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' - return 1; + logger.LogWarning("We detected a regression in BenchmarkDotNet and there is no ETL info."); + return failOnRegression ? 1 : 0; } if (etlRegressionDetected) { -#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' - logger.LogTrace(" We detected a regression in BenchmarkDotNet and there _is_ ETL info which agrees there was a regression."); -#pragma warning restore CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' - return 1; + logger.LogWarning("We detected a regression in BenchmarkDotNet and there _is_ ETL info which agrees there was a regression."); + return failOnRegression ? 1 : 0; } -#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' - logger.LogTrace("We detected a regression in BenchmarkDotNet but examining the ETL trace determined that is was noise."); -#pragma warning restore CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogTrace(ILogger, string?, params object?[])' + logger.LogTrace("We detected a regression in BenchmarkDotNet but examining the ETL trace determined that it was noise."); return 0; } +#pragma warning restore CA2254 +#pragma warning restore CA1848 - private static (bool compareSucceeded, bool regressionDetected) CheckEltTraces(string baselineFolder, string resultsFolder, bool failOnRegression) + private static (bool compareSucceeded, bool regressionDetected) CheckEltTraces(string baselineFolder, string resultsFolder, ILogger logger) { - bool regressionDetected = false; - // try look for ETL traces - if (!TryGetETLPaths(baselineFolder, out string? baselineEtlPath)) + if (!TryGetETLPaths(baselineFolder, logger, out string? baselineEtlPath)) { - return (false, regressionDetected); + return (false, false); } - if (!TryGetETLPaths(resultsFolder, out string? resultsEtlPath)) + if (!TryGetETLPaths(resultsFolder, logger, out string? resultsEtlPath)) { - return (false, regressionDetected); + return (false, false); } // Compare ETL - if (!EtlDiffer.TryCompareETL(resultsEtlPath, baselineEtlPath, out regressionDetected)) + if (!EtlDiffer.TryCompareETL(resultsEtlPath, baselineEtlPath, out bool regressionDetected)) { - return (false, regressionDetected); + return (false, false); } - if (regressionDetected && failOnRegression) - { - return (true, regressionDetected); - } - - return (false, regressionDetected); + return (true, regressionDetected); } private const string ETLFileExtension = "etl.zip"; - private static bool TryGetETLPaths(string path, [NotNullWhen(true)] out string? etlPath) + private static bool TryGetETLPaths(string path, ILogger logger, [NotNullWhen(true)] out string? etlPath) { if (Directory.Exists(path)) { string[] files = Directory.GetFiles(path, $"*{ETLFileExtension}", SearchOption.AllDirectories); - etlPath = files.SingleOrDefault(); - if (etlPath is null) + if (files.Length > 1) { - etlPath = null; - return false; + logger.LogWarning("Found {Count} ETL files in '{Path}'; using first match.", files.Length, path); } - return true; + etlPath = files.FirstOrDefault(); + return etlPath is not null; } else if (File.Exists(path) && path.EndsWith(ETLFileExtension, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/tools/PerfDiff/PerfDiff.csproj b/src/tools/PerfDiff/PerfDiff.csproj index 5b76e71f5..5f03cace2 100644 --- a/src/tools/PerfDiff/PerfDiff.csproj +++ b/src/tools/PerfDiff/PerfDiff.csproj @@ -9,7 +9,6 @@ - diff --git a/src/tools/PerfDiff/Program.cs b/src/tools/PerfDiff/Program.cs index 8c486de76..6a4439238 100644 --- a/src/tools/PerfDiff/Program.cs +++ b/src/tools/PerfDiff/Program.cs @@ -1,9 +1,6 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.CommandLine; -using System.CommandLine.Invocation; -using System.CommandLine.Parsing; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -17,94 +14,72 @@ internal sealed class Program { internal const int UnhandledExceptionExitCode = 1; internal const int CancelledExitCode = 2; - private static ParseResult? s_parseResult; private static async Task Main(string[] args) { RootCommand rootCommand = DiffCommand.CreateCommandLineOptions(); - rootCommand.Handler = CommandHandler.Create(new DiffCommand.Handler(RunAsync)); + rootCommand.SetAction(static async (parseResult, cancellationToken) => + { + string baseline = parseResult.GetValue(DiffCommand.BaselineOption)!; + string results = parseResult.GetValue(DiffCommand.ResultsOption)!; + string? verbosity = parseResult.GetValue(DiffCommand.VerbosityOption); + bool failOnRegression = parseResult.GetValue(DiffCommand.FailOnRegressionOption); - // Parse the incoming args so we can give warnings when deprecated options are used. - s_parseResult = rootCommand.Parse(args); + return await RunAsync(baseline, results, verbosity, failOnRegression, cancellationToken).ConfigureAwait(false); + }); - return await rootCommand.InvokeAsync(args).ConfigureAwait(false); + return await rootCommand.Parse(args).InvokeAsync().ConfigureAwait(false); } - public static async Task RunAsync( + internal static async Task RunAsync( string baseline, string results, string? verbosity, bool failOnRegression, - IConsole console) + CancellationToken cancellationToken) { - if (s_parseResult == null) - { - return 1; - } - // Setup logging. - LogLevel logLevel = GetLogLevel(verbosity); - (ILogger logger, ServiceProvider serviceProvider) = SetupLogging(console, minimalLogLevel: logLevel, minimalErrorLevel: LogLevel.Warning); + LogLevel logLevel = DiffCommand.GetLogLevel(verbosity); + ServiceProvider serviceProvider = SetupLogging(minimalLogLevel: logLevel, minimalErrorLevel: LogLevel.Warning); - // Hook so we can cancel and exit when ctrl+c is pressed. - using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - ConsoleCancelEventHandler cancelHandler = (sender, e) => + try { - e.Cancel = true; + ILogger logger = serviceProvider.GetRequiredService>(); + try { - cancellationTokenSource.Cancel(); + return await PerfDiff.CompareAsync(baseline, results, failOnRegression, logger, cancellationToken).ConfigureAwait(false); } - catch (ObjectDisposedException) + catch (FileNotFoundException fex) { - // CTS already disposed; safe to ignore. +#pragma warning disable CA1848, CA2254 // LoggerMessage delegates, varying template + logger.LogError(fex, "File not found: {FileName}", fex.FileName); +#pragma warning restore CA1848, CA2254 + return UnhandledExceptionExitCode; + } + catch (OperationCanceledException ex) + { +#pragma warning disable CA1848 // LoggerMessage delegates + logger.LogWarning(ex, "Operation was cancelled."); +#pragma warning restore CA1848 + return CancelledExitCode; } - }; - Console.CancelKeyPress += cancelHandler; - - try - { - return await PerfDiff.CompareAsync(baseline, results, failOnRegression, logger, cancellationTokenSource.Token).ConfigureAwait(false); - } - catch (FileNotFoundException fex) - { -#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogError(ILogger, string?, params object?[])' - logger.LogError(fex, "File not found: {FileName}", fex.FileName); -#pragma warning restore CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogError(ILogger, string?, params object?[])' - return UnhandledExceptionExitCode; - } - catch (OperationCanceledException ex) - { -#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogWarning(ILogger, string?, params object?[])' - logger.LogWarning(ex, "Operation was cancelled."); -#pragma warning restore CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogWarning(ILogger, string?, params object?[])' - return CancelledExitCode; } finally { - Console.CancelKeyPress -= cancelHandler; await serviceProvider.DisposeAsync().ConfigureAwait(false); } - static (ILogger Logger, ServiceProvider ServiceProvider) SetupLogging(IConsole console, LogLevel minimalLogLevel, LogLevel minimalErrorLevel) + static ServiceProvider SetupLogging(LogLevel minimalLogLevel, LogLevel minimalErrorLevel) { ServiceCollection serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(new LoggerFactory().AddSimpleConsole(console, minimalLogLevel, minimalErrorLevel)); - serviceCollection.AddLogging(); + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(minimalLogLevel); + builder.AddProvider(new SimpleConsoleLoggerProvider(minimalLogLevel, minimalErrorLevel)); + }); - ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); - return (serviceProvider.GetRequiredService>(), serviceProvider); + return serviceCollection.BuildServiceProvider(); } - - static LogLevel GetLogLevel(string? verbosity) - => verbosity switch - { - "q" or "quiet" => LogLevel.Error, - "m" or "minimal" => LogLevel.Warning, - "n" or "normal" => LogLevel.Information, - "d" or "detailed" => LogLevel.Debug, - "diag" or "diagnostic" => LogLevel.Trace, - _ => LogLevel.Information, - }; } }