tools/perf-automation/Azure.Sdk.Tools.PerfAutomation/Program.cs (490 lines of code) (raw):

using Azure.Sdk.Tools.PerfAutomation.Models; using CommandLine; using CommandLine.Text; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading.Tasks; using YamlDotNet.Core; using YamlDotNet.Serialization; namespace Azure.Sdk.Tools.PerfAutomation { public static class Program { public const string PackageVersionSource = "source"; private static readonly Dictionary<Language, ILanguage> _languages = new Dictionary<Language, ILanguage> { { Language.Java, new Java() }, { Language.JS, new JavaScript() }, { Language.Net, new Net() }, { Language.Python, new Python() }, { Language.Cpp, new Cpp() }, { Language.Rust, new Rust() } }; public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = true, }; public class Options { [Option('a', "arguments", HelpText = "Regex of arguments to run")] public string Arguments { get; set; } [Option('d', "debug")] public bool Debug { get; set; } [Option('n', "dry-run")] public bool DryRun { get; set; } [Option("insecure", HelpText = "Allow untrusted SSL certs")] public bool Insecure { get; set; } [Option('i', "iterations", Default = 1)] public int Iterations { get; set; } [Option('l', "language", Required = true)] public Language Language { get; set; } [Option("language-version", Required = true, HelpText = ".NET: 6|7, Java: 8|17, JS: 16|18, Python: 3.10|3.11, Cpp: N/A, Rust: N/A")] public string LanguageVersion { get; set; } [Option("no-async")] public bool NoAsync { get; set; } [Option("no-cleanup", HelpText = "Disables test cleanup")] public bool NoCleanup { get; set; } [Option("no-sync")] public bool NoSync { get; set; } [Option('o', "output-file-prefix", Default = "results/results")] public string OutputFilePrefix { get; set; } [Option('p', "package-versions", HelpText = "Regex of package versions to run")] public string PackageVersions { get; set; } [Option("profile", HelpText = "Enables capture of profiling data")] public bool Profile { get; set; } [Option("profilerOpt", HelpText = "Provides additional profiler parameters")] public string ProfilerOptions { get; set; } [Option("repo-root", Required = true, HelpText = "Path to root of repository in which to run tests")] public string RepoRoot { get; set; } // TODO: Configure YAML serialization to print URI values [Option('x', "test-proxies", Separator = ';', HelpText = "URIs of TestProxy Servers")] [YamlMember(typeof(string))] public IEnumerable<Uri> TestProxies { get; set; } [Option("test-proxy", HelpText = "URI of TestProxy Server")] [YamlMember(typeof(string))] public Uri TestProxy { get; set; } [Option('t', "tests", HelpText = "Regex of tests to run")] public string Tests { get; set; } [Option("tests-file", Required = true)] public string TestsFile { get; set; } } public static async Task Main(string[] args) { var parser = new CommandLine.Parser(settings => { settings.CaseSensitive = false; settings.CaseInsensitiveEnumValues = true; settings.HelpWriter = null; }); var parserResult = parser.ParseArguments<Options>(args); await parserResult.MapResult( (Options options) => Run(options), errors => DisplayHelp(parserResult) ); } static Task DisplayHelp<T>(ParserResult<T> result) { var helpText = HelpText.AutoBuild(result, settings => { settings.AddEnumValuesToHelpText = true; return settings; }); Console.Error.WriteLine(helpText); return Task.CompletedTask; } private static async Task Run(Options options) { if (options.Language == Language.JS) { // JS is async-only options.NoSync = true; } else if (options.Language == Language.Cpp) { // Cpp is sync-only options.NoAsync = true; } else if (options.Language == Language.Rust) { // Rust is sync-only options.NoAsync = true; } var serviceInfo = DeserializeYaml<ServiceInfo>(options.TestsFile); var selectedPackageVersions = serviceInfo.PackageVersions.Where(d => String.IsNullOrEmpty(options.PackageVersions) || Regex.IsMatch(d[serviceInfo.PrimaryPackage], options.PackageVersions, RegexOptions.IgnoreCase)); var selectedTests = serviceInfo.Tests .Where(t => String.IsNullOrEmpty(options.Tests) || Regex.IsMatch(t.Test, options.Tests, RegexOptions.IgnoreCase)) .Select(t => new TestInfo { Test = t.Test, Class = t.Class, Arguments = t.Arguments.Where(a => String.IsNullOrEmpty(options.Arguments) || Regex.IsMatch(a, options.Arguments, RegexOptions.IgnoreCase)) }) .Where(t => t.Arguments.Any()); var serializer = new Serializer(); Console.WriteLine("=== Options ==="); serializer.Serialize(Console.Out, options); Console.WriteLine(); Console.WriteLine("=== Test Plan ==="); serializer.Serialize(Console.Out, new ServiceInfo() { Service = serviceInfo.Service, Project = serviceInfo.Project, PrimaryPackage = serviceInfo.PrimaryPackage, PackageVersions = selectedPackageVersions, Tests = selectedTests, }); if (options.DryRun) { Console.WriteLine(); Console.Write("Press 'y' to continue, or any other key to exit: "); var key = Console.ReadKey(); Console.WriteLine(); Console.WriteLine(); if (char.ToLowerInvariant(key.KeyChar) != 'y') { return; } } var outputFiles = Util.GetUniquePaths(options.OutputFilePrefix, ".json", ".csv", ".txt", ".md"); // Create output file early so user sees it immediately foreach (var outputFile in outputFiles) { Directory.CreateDirectory(Path.GetDirectoryName(outputFile)); using (File.Create(outputFile)) { } } var outputJson = outputFiles[0]; var outputCsv = outputFiles[1]; var outputTxt = outputFiles[2]; var outputMd = outputFiles[3]; var results = new List<Result>(); DirectoryInfo profileDirectory = null; if (options.Profile) { profileDirectory = Directory.CreateDirectory(Util.GetProfileDirectory(options.RepoRoot)); } foreach (var packageVersions in selectedPackageVersions) { await RunPackageVersion( options, serviceInfo.Service, serviceInfo.Project, serviceInfo.PrimaryPackage, packageVersions, selectedTests, outputJson, outputCsv, outputTxt, outputMd, results); } if (options.Profile) { ZipFile.CreateFromDirectory(profileDirectory.FullName, Path.Combine(profileDirectory.Parent.FullName, $"{options.Language}-{profileDirectory.Name}.zip")); } } private static async Task RunPackageVersion( Options options, string service, string project, string primaryPackage, IDictionary<string, string> packageVersions, IEnumerable<TestInfo> tests, string outputJson, string outputCsv, string outputTxt, string outputMd, List<Result> results) { var language = options.Language; var languageVersion = options.LanguageVersion; _languages[language].WorkingDirectory = options.RepoRoot; try { Console.WriteLine($"SetupAsync({project}, {languageVersion}, " + $"{JsonSerializer.Serialize(packageVersions)})"); Console.WriteLine(); string setupOutput = null; string setupError = null; object context = null; string setupException = null; try { (setupOutput, setupError, context) = await _languages[language].SetupAsync( project, languageVersion, primaryPackage, packageVersions, options.Debug); } catch (Exception e) { setupException = e.ToString(); Console.WriteLine(e); Console.WriteLine(); } foreach (var test in tests) { IEnumerable<string> selectedArguments; if (!options.NoAsync && !options.NoSync) { selectedArguments = test.Arguments.SelectMany(a => new string[] { a, a + " --sync" }); } else if (!options.NoSync) { selectedArguments = test.Arguments.Select(a => a + " --sync"); } else if (!options.NoAsync) { selectedArguments = test.Arguments; } else { throw new InvalidOperationException("Cannot set both --no-sync and --no-async"); } foreach (var arguments in selectedArguments) { var allArguments = arguments; if (options.Insecure) { allArguments += " --insecure"; } if (options.TestProxies != null && options.TestProxies.Any()) { allArguments += $" --test-proxies {String.Join(';', options.TestProxies)}"; } if (options.TestProxy != null) { allArguments += $" --test-proxy {options.TestProxy}"; } var result = new Result { Service = service, Test = test.Test, Start = DateTime.Now, Language = language, LanguageVersion = languageVersion, Project = project, LanguageTestName = test.Class, Arguments = allArguments, PrimaryPackage = primaryPackage, PackageVersions = packageVersions, SetupStandardOutput = setupOutput, SetupStandardError = setupError, SetupException = setupException, }; results.Add(result); await WriteResults(outputJson, outputCsv, outputTxt, outputMd, results); if (setupException == null) { for (var i = 0; i < options.Iterations; i++) { IterationResult iterationResult; try { Console.WriteLine($"RunAsync({project}, {languageVersion}, " + $"{test.Class}, {allArguments}, {context}, {options.Profile}, {options.ProfilerOptions})"); Console.WriteLine(); iterationResult = await _languages[language].RunAsync( project, languageVersion, primaryPackage, packageVersions, test.Class, allArguments, options.Profile, options.ProfilerOptions, context); } catch (Exception e) { iterationResult = new IterationResult { OperationsPerSecond = double.MinValue, Exception = e.ToString(), }; Console.WriteLine(e); Console.WriteLine(); } // Replace non-finite values with minvalue, since non-finite values // are not JSON serializable if (!double.IsFinite(iterationResult.OperationsPerSecond)) { iterationResult.OperationsPerSecond = double.MinValue; } result.Iterations.Add(iterationResult); await WriteResults(outputJson, outputCsv, outputTxt, outputMd, results); } } result.End = DateTime.Now; } } } finally { if (!options.NoCleanup) { Console.WriteLine($"CleanupAsync({project})"); Console.WriteLine(); try { await _languages[language].CleanupAsync(project); } catch (Exception e) { Console.WriteLine(e); Console.WriteLine(); } } } } private static async Task WriteResults(string outputJson, string outputCsv, string outputTxt, string outputMd, List<Result> results) { using (var stream = File.OpenWrite(outputJson)) { await JsonSerializer.SerializeAsync(stream, results, JsonOptions); } using (var streamWriter = new StreamWriter(outputCsv)) { await WriteResultsSummary(streamWriter, results, OutputFormat.Csv); } using (var streamWriter = new StreamWriter(outputTxt)) { await WriteResultsSummary(streamWriter, results, OutputFormat.Txt); } using (var streamWriter = new StreamWriter(outputMd)) { await WriteResultsSummary(streamWriter, results, OutputFormat.Md); } } public static async Task WriteResultsSummary(StreamWriter streamWriter, IEnumerable<Result> results, OutputFormat outputFormat) { var groups = results.GroupBy(r => (r.Language, r.LanguageVersion, r.Service, r.Test, r.Arguments)); var resultSummaries = groups.Select(g => { var requestedPackageVersions = g.Select(r => r.PackageVersions).Distinct(new DictionaryEqualityComparer<string, string>()); var runtimePackageVersions = requestedPackageVersions.Select(req => g.Where(r => r.PackageVersions == req).First().Iterations.FirstOrDefault()?.PackageVersions); var resultSummary = new ResultSummary() { Language = g.Key.Language, LanguageVersion = g.Key.LanguageVersion, Service = g.Key.Service, Test = g.Key.Test, Arguments = g.Key.Arguments, PrimaryPackage = g.First().PrimaryPackage, RequestedPackageVersions = requestedPackageVersions, RuntimePackageVersions = runtimePackageVersions, }; var operationsPerSecondMax = new List<(string version, double operationsPerSecond)>(); var operationsPerSecondMean = new List<(string version, double operationsPerSecond)>(); foreach (var result in g) { var primaryPackageVersion = result.PackageVersions?[resultSummary.PrimaryPackage]; operationsPerSecondMax.Add((primaryPackageVersion, result.OperationsPerSecondMax)); operationsPerSecondMean.Add((primaryPackageVersion, result.OperationsPerSecondMean)); } resultSummary.OperationsPerSecondMax = operationsPerSecondMax; resultSummary.OperationsPerSecondMean = operationsPerSecondMean; return resultSummary; }); var languageServiceGroups = resultSummaries.GroupBy(r => (r.Language, r.LanguageVersion, r.Service)); foreach (var group in languageServiceGroups) { await WriteResultsSummaryThroughput(streamWriter, group, "Max", r => r.OperationsPerSecondMax, r => r.OperationsPerSecondMaxDifferences, outputFormat); await WriteResultsSummaryThroughput(streamWriter, group, "Mean", r => r.OperationsPerSecondMean, r => r.OperationsPerSecondMeanDifferences, outputFormat); await WriteHeader(streamWriter, "Package Versions", outputFormat); var versionHeaders = new string[] { "Name", "Requested", "Runtime" }; var versionTable = new List<IList<IList<string>>>(); var primaryPackage = group.First().PrimaryPackage; var runtimePackageVersions = group.First().RuntimePackageVersions .Select(p => _languages[group.Key.Language].FilterRuntimePackageVersions(p)); var packageVersions = group.First().RequestedPackageVersions.Zip(runtimePackageVersions); foreach (var (requested, runtime) in packageVersions) { // requested is guaranteed to be non-null, runtime may be null var versionRows = new List<IList<string>>(); // Primary package first, azure core second, remaining sorted alphabetically var packageNames = requested.Keys.Concat(runtime?.Keys ?? Enumerable.Empty<string>()) .Distinct() .OrderBy(n => (n == primaryPackage) ? $"__{n}" : ((n.Contains("core", StringComparison.OrdinalIgnoreCase) && n.Contains("azure", StringComparison.OrdinalIgnoreCase)) ? $"_{n}" : n)); foreach (var packageName in packageNames) { requested.TryGetValue(packageName, out var requestedPackageVersion); string runtimePackageVersion = null; runtime?.TryGetValue(packageName, out runtimePackageVersion); versionRows.Add(new List<string> { packageName, requestedPackageVersion ?? "none", runtimePackageVersion ?? "unknown" }); } versionTable.Add(versionRows); } await streamWriter.WriteLineAsync(TableGenerator.Generate(versionHeaders, versionTable, outputFormat)); await WriteHeader(streamWriter, "Metadata", outputFormat); var metadataHeaders = new string[] { "Name", "Value" }; var metadataTable = new List<IList<IList<string>>>(); var metadataRowSets = new List<IList<string>>(); metadataRowSets.Add(new List<string>(new string[] { "Language", $"{group.Key.Language} ({group.Key.LanguageVersion})" })); metadataRowSets.Add(new List<string>(new string[] { "Service", $"{group.Key.Service}" })); metadataTable.Add(metadataRowSets); await streamWriter.WriteLineAsync(TableGenerator.Generate(metadataHeaders, metadataTable, outputFormat)); } } private static async Task WriteHeader(StreamWriter streamWriter, string header, OutputFormat outputFormat) { await streamWriter.WriteLineAsync($"## {header}"); } private static async Task WriteResultsSummaryThroughput( StreamWriter streamWriter, IEnumerable<ResultSummary> resultSummaries, string aggregateType, Func<ResultSummary, IEnumerable<(string version, double operationsPerSecond)>> operationsPerSecond, Func<ResultSummary, IEnumerable<double>> operationsPerSecondDifferences, OutputFormat outputFormat) { var versions = operationsPerSecond(resultSummaries.First()).Select(o => o.version); var headers = versions.Take(1).Concat(versions.Skip(1).Zip(Enumerable.Repeat("%Change", versions.Count() - 1), (f, s) => new[] { f, s }).SelectMany(f => f)); var testGroups = resultSummaries.GroupBy(g => g.Test); await WriteHeader(streamWriter, $"{aggregateType} throughput (ops/sec)", outputFormat); headers = headers.Prepend("Arguments").Prepend("Test"); var table = new List<IList<IList<string>>>(); foreach (var testGroup in testGroups) { var rowSet = new List<IList<string>>(); foreach (var resultSummary in testGroup) { var row = new List<string>(); row.Add(resultSummary.Test); row.Add(resultSummary.Arguments); var operationsPerSecondStrings = operationsPerSecond(resultSummary) .Select(o => $"{NumberFormatter.Format(o.operationsPerSecond, 4, groupSeparator: outputFormat != OutputFormat.Csv)}"); var operationsPerSecondDifferencesStrings = operationsPerSecondDifferences(resultSummary).Select(o => $"{o * 100:N1}%"); var values = operationsPerSecondStrings.Take(1).Concat(operationsPerSecondStrings.Skip(1) .Zip(operationsPerSecondDifferencesStrings, (f, s) => new[] { f, s }).SelectMany(f => f)); row.AddRange(values); rowSet.Add(row); } table.Add(rowSet); } await streamWriter.WriteLineAsync(TableGenerator.Generate(headers.ToList(), table, outputFormat)); } private static T DeserializeYaml<T>(string path) { using var fileReader = File.OpenText(path); var parser = new MergingParser(new YamlDotNet.Core.Parser(fileReader)); return new Deserializer().Deserialize<T>(parser); } } }