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,
- };
}
}