diff --git a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/CommandBuilders/GCPerfSim.CommandBuilder.cs b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/CommandBuilders/GCPerfSim.CommandBuilder.cs index 7643f48711b..2195f099b2d 100644 --- a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/CommandBuilders/GCPerfSim.CommandBuilder.cs +++ b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/CommandBuilders/GCPerfSim.CommandBuilder.cs @@ -104,7 +104,11 @@ public static (string, string) BuildForServer(GCPerfSimConfiguration configurati if (configuration.TraceConfigurations != null && !string.Equals(configuration.TraceConfigurations.Type, "none", StringComparison.OrdinalIgnoreCase)) { CollectType collectType = TraceCollector.StringToCollectTypeMap[configuration.TraceConfigurations.Type]; - string collectionCommand = os == OS.Windows ? TraceCollector.WindowsCollectTypeMap[collectType] : TraceCollector.LinuxCollectTypeMap[collectType]; + if (os == OS.Linux && !TraceCollector.LinuxServerRunCollectTypeMap.Keys.Contains(collectType)) + { + throw new Exception($"{nameof(GCPerfSimCommandBuilder)}: Trace collect type {configuration.TraceConfigurations.Type} is not supported for GCPerfsim Linux server run."); + } + string collectionCommand = os == OS.Windows ? TraceCollector.WindowsCollectTypeMap[collectType] : TraceCollector.LinuxServerRunCollectTypeMap[collectType]; collectionCommand = collectionCommand.Replace(" ", ";").Replace("/", ""); diff --git a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/GC.Infrastructure.Core.csproj b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/GC.Infrastructure.Core.csproj index 3c3ad5c0fba..cda42b8cc22 100644 --- a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/GC.Infrastructure.Core.csproj +++ b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/GC.Infrastructure.Core.csproj @@ -5,8 +5,8 @@ enable enable false - false - + false + diff --git a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/TraceCollection/TraceCollector.cs b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/TraceCollection/TraceCollector.cs index 752657610b5..dee739f21e8 100644 --- a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/TraceCollection/TraceCollector.cs +++ b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure.Core/TraceCollection/TraceCollector.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Microsoft.Diagnostics.Utilities; +using System.Diagnostics; using System.Net; namespace GC.Infrastructure.Core.TraceCollection @@ -19,12 +20,10 @@ public sealed class TraceCollector : IDisposable { private bool disposedValue; - private static readonly Lazy _client = new(); - - // TODO: Make this URL configurable. - private const string PERFVIEW_URL = "https://github.com/microsoft/perfview/releases/download/v3.0.0/PerfView.exe"; + private static readonly string DependenciesFolder = "./dependencies"; + + public string Name { get; init; } - private readonly string ALWAYS_ARGS = @$" /AcceptEULA /NoGUI /Merge:true"; internal static readonly Dictionary WindowsCollectTypeMap = new() { { CollectType.gc, "/GCCollectOnly" }, @@ -36,11 +35,18 @@ public sealed class TraceCollector : IDisposable { CollectType.join, " /BufferSizeMB:4096 /CircularMB:4096 /KernelEvents:Process+Thread+ImageLoad /ClrEvents:GC+Threading /ClrEventLevel=Verbose " }, }; - internal static readonly Dictionary LinuxCollectTypeMap = new() + internal static readonly Dictionary LinuxServerRunCollectTypeMap = new() { { CollectType.gc, "gcCollectOnly" }, { CollectType.cpu, "collect_cpu" }, - { CollectType.threadtime, "collect_threadTime" }, + { CollectType.threadtime, "collect_threadTime" } + }; + + internal static readonly Dictionary LinuxLocalRunCollectTypeMap = new() + { + { CollectType.gc, "--profile gc-collect" }, + { CollectType.cpu, "" }, + { CollectType.verbose, "--clrevents gc+stack --clreventlevel verbose" } }; internal static readonly Dictionary StringToCollectTypeMap = new(StringComparer.OrdinalIgnoreCase) @@ -55,44 +61,94 @@ public sealed class TraceCollector : IDisposable { "none", CollectType.none } }; + private readonly CollectType _collectType; + private readonly Process _traceProcess; private readonly string arguments; + private readonly string _collectorPath; private readonly Guid _sessionName; - private readonly Process _traceProcess; - private readonly CollectType _collectType; - public TraceCollector(string name, string collectType, string outputPath) + private static void InstallTraceCollector(string dependenciesFolder) { - // Get Perfview if it doesn't exist. - if (!Directory.Exists("./dependencies")) + if (OperatingSystem.IsWindows()) { - Directory.CreateDirectory("./dependencies"); + string perfviewPath = Path.Combine(DependenciesFolder, "Perfview.exe"); + if (File.Exists(perfviewPath)) + { + return; + } + + // TODO: Make this URL configurable. + const string perfviewUrl = "https://github.com/microsoft/perfview/releases/download/v3.0.0/PerfView.exe"; + using (HttpClient client = new()) + { + HttpResponseMessage response = client.GetAsync(perfviewUrl).Result; + response.EnsureSuccessStatusCode(); + + using (FileStream writer = File.OpenWrite(perfviewPath)) + { + response.Content.ReadAsStream().CopyTo(writer); + } + } } + if (OperatingSystem.IsLinux()) + { + string dotNetTracePath = Path.Combine(DependenciesFolder, "dotnet-trace"); + if (File.Exists(dotNetTracePath)) + { + return; + } + + using (Process dotNetTraceInstaller = new()) + { + dotNetTraceInstaller.StartInfo.FileName = "dotnet"; + dotNetTraceInstaller.StartInfo.Arguments = $"tool install dotnet-trace --tool-path {dependenciesFolder}"; + dotNetTraceInstaller.StartInfo.UseShellExecute = false; + dotNetTraceInstaller.StartInfo.CreateNoWindow = true; + dotNetTraceInstaller.StartInfo.RedirectStandardError = true; + dotNetTraceInstaller.StartInfo.RedirectStandardOutput = true; + dotNetTraceInstaller.Start(); + Console.WriteLine("install dotnet-trace"); + dotNetTraceInstaller.WaitForExit(); + } + } + } - if (!File.Exists(Path.Combine("./dependencies", "PerfView.exe"))) + public TraceCollector(string name, string collectType, string outputPath, int? pid = null) + { + if (!Directory.Exists(DependenciesFolder)) { - _client.Value.DownloadFile(PERFVIEW_URL, Path.Combine("./dependencies", "PerfView.exe")); + Directory.CreateDirectory(DependenciesFolder); } + InstallTraceCollector(DependenciesFolder); + _collectType = StringToCollectTypeMap[collectType]; - if (_collectType != CollectType.none) + if (_collectType == CollectType.none) { - _sessionName = Guid.NewGuid(); - foreach (var invalid in Path.GetInvalidPathChars()) - { - name = name.Replace(invalid.ToString(), string.Empty); - } + return; + } - name = name.Replace("<", ""); - name = name.Replace(">", ""); + foreach (var invalid in Path.GetInvalidPathChars()) + { + name = name.Replace(invalid.ToString(), string.Empty); + } - Name = Path.Combine(outputPath, $"{name}.etl"); + name = name.Replace("<", ""); + name = name.Replace(">", ""); + + if (OperatingSystem.IsWindows()) + { + _collectorPath = Path.Combine(DependenciesFolder, "Perfview.exe"); + _sessionName = Guid.NewGuid(); - arguments = $"{ALWAYS_ARGS} /sessionName:{_sessionName} {WindowsCollectTypeMap[_collectType]} /LogFile:{Path.Combine(outputPath, name)}.txt /DataFile:{Path.Combine(outputPath, $"{name}.etl")}"; + Name = Path.Combine(outputPath, $"{name}.etl"); + string ALWAYS_ARGS = @$" /AcceptEULA /NoGUI /Merge:true"; + arguments = $"{ALWAYS_ARGS} /sessionName:{_sessionName} {WindowsCollectTypeMap[_collectType]} /LogFile:{Path.Combine(outputPath, name)}.txt /DataFile:{Name}"; string command = $"start {arguments}"; _traceProcess = new(); - _traceProcess.StartInfo.FileName = "./dependencies/PerfView.exe"; + _traceProcess.StartInfo.FileName = _collectorPath; _traceProcess.StartInfo.Arguments = command; _traceProcess.StartInfo.UseShellExecute = false; _traceProcess.StartInfo.CreateNoWindow = true; @@ -103,6 +159,34 @@ public TraceCollector(string name, string collectType, string outputPath) // Give PerfView about a second to get started. Thread.Sleep(1000); } + + if (OperatingSystem.IsLinux()) + { + if (pid == null) + { + throw new Exception($"{nameof(TraceCollector)}: Must provide prcoess id in Linux case"); + } + + if (_collectType != CollectType.none && !LinuxLocalRunCollectTypeMap.Keys.Contains(_collectType)) + { + throw new Exception($"{nameof(TraceCollector)}: Trace collect type {collectType} is not supported for Linux local run."); + } + + _collectorPath = Path.Combine(DependenciesFolder, "dotnet-trace"); + + Name = Path.Combine(outputPath, $"{name}.nettrace"); + arguments = $"-p {pid} -o {Name} {LinuxLocalRunCollectTypeMap[_collectType]}"; + string command = $"collect {arguments}"; + + _traceProcess = new(); + _traceProcess.StartInfo.FileName = _collectorPath; + _traceProcess.StartInfo.Arguments = command; + _traceProcess.StartInfo.UseShellExecute = false; + _traceProcess.StartInfo.CreateNoWindow = true; + _traceProcess.StartInfo.RedirectStandardError = true; + _traceProcess.StartInfo.RedirectStandardOutput = true; + _traceProcess.Start(); + } } private void Dispose(bool disposing) @@ -114,55 +198,65 @@ private void Dispose(bool disposing) } // TODO: Parameterize the wait for exit time. - - if (!disposedValue) + if (OperatingSystem.IsWindows()) { - using (Process stopProcess = new()) + if (!disposedValue) { - stopProcess.StartInfo.FileName = Path.Combine("./dependencies", "PerfView.exe"); - string command = $"stop {arguments}"; - stopProcess.StartInfo.Arguments = command; - stopProcess.StartInfo.UseShellExecute = false; - stopProcess.StartInfo.CreateNoWindow = true; - stopProcess.StartInfo.RedirectStandardInput = true; - stopProcess.StartInfo.RedirectStandardError = true; - stopProcess.Start(); - stopProcess.WaitForExit(200_000); - _traceProcess?.Dispose(); - } + using (Process stopProcess = new()) + { + stopProcess.StartInfo.FileName = _collectorPath; + string command = $"stop {arguments}"; + stopProcess.StartInfo.Arguments = command; + stopProcess.StartInfo.UseShellExecute = false; + stopProcess.StartInfo.CreateNoWindow = true; + stopProcess.StartInfo.RedirectStandardInput = true; + stopProcess.StartInfo.RedirectStandardError = true; + stopProcess.Start(); + stopProcess.WaitForExit(200_000); + _traceProcess?.Dispose(); + } - // Clean up any dangling ETW sessions for both the Kernel and the session. - using (Process stopLogmanKernelProcess = new()) - { - stopLogmanKernelProcess.StartInfo.FileName = "logman"; - string etsStopCommand = $"-ets stop {_sessionName}Kernel"; - stopLogmanKernelProcess.StartInfo.Arguments = etsStopCommand; - stopLogmanKernelProcess.StartInfo.UseShellExecute = false; - stopLogmanKernelProcess.StartInfo.RedirectStandardOutput = false; - stopLogmanKernelProcess.StartInfo.RedirectStandardError = false; - stopLogmanKernelProcess.StartInfo.CreateNoWindow = true; - stopLogmanKernelProcess.Start(); - stopLogmanKernelProcess.WaitForExit(5_000); + // Clean up any dangling ETW sessions for both the Kernel and the session. + using (Process stopLogmanKernelProcess = new()) + { + stopLogmanKernelProcess.StartInfo.FileName = "logman"; + string etsStopCommand = $"-ets stop {_sessionName}Kernel"; + stopLogmanKernelProcess.StartInfo.Arguments = etsStopCommand; + stopLogmanKernelProcess.StartInfo.UseShellExecute = false; + stopLogmanKernelProcess.StartInfo.RedirectStandardOutput = false; + stopLogmanKernelProcess.StartInfo.RedirectStandardError = false; + stopLogmanKernelProcess.StartInfo.CreateNoWindow = true; + stopLogmanKernelProcess.Start(); + stopLogmanKernelProcess.WaitForExit(5_000); + } + + using (Process stopLogmanProcess = new()) + { + stopLogmanProcess.StartInfo.FileName = "logman"; + string etsStopCommand = $"-ets stop {_sessionName}"; + stopLogmanProcess.StartInfo.Arguments = etsStopCommand; + stopLogmanProcess.StartInfo.UseShellExecute = false; + stopLogmanProcess.StartInfo.RedirectStandardOutput = false; + stopLogmanProcess.StartInfo.RedirectStandardError = false; + stopLogmanProcess.StartInfo.CreateNoWindow = true; + stopLogmanProcess.Start(); + stopLogmanProcess.WaitForExit(5_000); + } + + disposedValue = true; } + } - using (Process stopLogmanProcess = new()) + if (OperatingSystem.IsLinux()) + { + if (!disposedValue) { - stopLogmanProcess.StartInfo.FileName = "logman"; - string etsStopCommand = $"-ets stop {_sessionName}"; - stopLogmanProcess.StartInfo.Arguments = etsStopCommand; - stopLogmanProcess.StartInfo.UseShellExecute = false; - stopLogmanProcess.StartInfo.RedirectStandardOutput = false; - stopLogmanProcess.StartInfo.RedirectStandardError = false; - stopLogmanProcess.StartInfo.CreateNoWindow = true; - stopLogmanProcess.Start(); - stopLogmanProcess.WaitForExit(5_000); + _traceProcess.WaitForExit(); + _traceProcess.Dispose(); } - - disposedValue = true; } } - public string Name { get; init; } ~TraceCollector() { diff --git a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Commands/GCPerfSim/GCPerfSimCommand.cs b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Commands/GCPerfSim/GCPerfSimCommand.cs index 87509b7eaea..f16385e5b43 100644 --- a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Commands/GCPerfSim/GCPerfSimCommand.cs +++ b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Commands/GCPerfSim/GCPerfSimCommand.cs @@ -176,20 +176,36 @@ internal static Dictionary ExecuteLocally(GCPer string key = $"{runInfo.RunDetails.Key}.{runInfo.CorerunDetails.Key}.{iterationIdx}"; string traceName = $"{runInfo.RunDetails.Key}.{runInfo.CorerunDetails.Key}.{iterationIdx}"; - using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, outputPath)) + + if (OperatingSystem.IsWindows()) + { + using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, outputPath)) + { + gcperfsimProcess.Start(); + output = gcperfsimProcess.StandardOutput.ReadToEnd(); + error = gcperfsimProcess.StandardError.ReadToEnd(); + gcperfsimProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000); + File.WriteAllText(Path.Combine(outputPath, key + ".txt"), "Standard Out: \n" + output + "\n Standard Error: \n" + error); + } + } + else { gcperfsimProcess.Start(); - output = gcperfsimProcess.StandardOutput.ReadToEnd(); - error = gcperfsimProcess.StandardError.ReadToEnd(); - gcperfsimProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000); - File.WriteAllText(Path.Combine(outputPath, key + ".txt"), "Standard Out: \n" + output + "\n Standard Error: \n" + error); + using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, outputPath, gcperfsimProcess.Id)) + { + output = gcperfsimProcess.StandardOutput.ReadToEnd(); + error = gcperfsimProcess.StandardError.ReadToEnd(); + gcperfsimProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000); + File.WriteAllText(Path.Combine(outputPath, key + ".txt"), "Standard Out: \n" + output + "\n Standard Error: \n" + error); + } } - + + // If a trace is requested, ensure the file exists. If not, there is was an error and alert the user. if (configuration.TraceConfigurations?.Type != "none") { - // Not checking Linux here since the local run only allows for Windows. - if (!File.Exists(Path.Combine(outputPath, traceName + ".etl.zip"))) + // On Windows, the trace file path ends with ".etl.zip"; On Linux, it ends with ".nettrace". + if (!File.Exists(Path.Combine(outputPath, traceName + ".etl.zip")) && !File.Exists(Path.Combine(outputPath, traceName + ".nettrace"))) { AnsiConsole.MarkupLine($"[yellow bold] ({DateTime.Now}) The trace for the run wasn't successfully captured. Please check the log file for more details: {Markup.Escape(output)} Full run details: {Path.GetFileNameWithoutExtension(configuration.Name)}: {runInfo.CorerunDetails.Key} for {runInfo.RunDetails.Key} [/]"); } diff --git a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Program.cs b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Program.cs index 0a505869860..29ba80d0a45 100644 --- a/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Program.cs +++ b/src/benchmarks/gc/GC.Infrastructure/GC.Infrastructure/Program.cs @@ -20,6 +20,7 @@ internal static void Main(string[] args) if (OperatingSystem.IsWindows()) { + bool IsAdministrator = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); if (!IsAdministrator) { AnsiConsole.WriteLine("Not running in admin mode - please elevate privileges to run this process."); @@ -79,9 +80,5 @@ internal static void Main(string[] args) } } } - - [SupportedOSPlatform("windows")] - internal static bool IsAdministrator => - new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); } } \ No newline at end of file