From 7c09e4363a83772a3d1f4fdf63404a505bc41e3a Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Sun, 9 Jun 2019 18:05:39 +0200 Subject: [PATCH 01/24] initial LttngProfiler implementation --- .../Sessions.cs | 27 +-- .../Diagnosers/DiagnosersLoader.cs | 12 +- .../Diagnosers/LttngProfiler.cs | 187 ++++++++++++++++++ .../Diagnosers/TraceFileHelper.cs | 37 ++++ .../Extensions/CommonExtensions.cs | 8 + .../Extensions/ProcessExtensions.cs | 8 + 6 files changed, 251 insertions(+), 28 deletions(-) create mode 100644 src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs create mode 100644 src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs index b73db2125d..086e90db51 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs @@ -87,7 +87,7 @@ protected Session(string sessionName, DiagnoserActionParameters details, EtwProf { Details = details; Config = config; - FilePath = EnsureFolderExists(GetFilePath(details, creationTime)); + FilePath = TraceFileHelper.EnsureFolderExists(TraceFileHelper.GetFilePath(details.BenchmarkCase, details.Config, creationTime, FileExtension)).FullName; TraceEventSession = new TraceEventSession(sessionName, FilePath) { @@ -123,30 +123,5 @@ internal string MergeFiles(Session other) private void OnConsoleCancelKeyPress(object sender, ConsoleCancelEventArgs e) => Stop(); private void OnProcessExit(object sender, EventArgs e) => Stop(); - - private string GetFilePath(DiagnoserActionParameters details, DateTime creationTime) - { - string fileName = $@"{FolderNameHelper.ToFolderName(details.BenchmarkCase.Descriptor.Type)}.{FullNameProvider.GetMethodName(details.BenchmarkCase)}"; - - // if we run for more than one toolchain, the output file name should contain the name too so we can differ net461 vs netcoreapp2.1 etc - if (details.Config.GetJobs().Select(job => job.GetToolchain()).Distinct().Count() > 1) - fileName += $"-{details.BenchmarkCase.Job.Environment.Runtime?.Name ?? details.BenchmarkCase.Job.GetToolchain()?.Name ?? details.BenchmarkCase.Job.Id}"; - - fileName += $"-{creationTime.ToString(BenchmarkRunnerClean.DateTimeFormat)}"; - - fileName = FolderNameHelper.ToFolderName(fileName); - - return Path.Combine(details.Config.ArtifactsPath, $"{fileName}{FileExtension}"); - } - - private string EnsureFolderExists(string filePath) - { - string directoryPath = Path.GetDirectoryName(filePath); - - if (!Directory.Exists(directoryPath)) - Directory.CreateDirectory(directoryPath); - - return filePath; - } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs b/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs index 068433d643..199da2febb 100644 --- a/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs +++ b/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -41,10 +43,16 @@ private static IDiagnoser[] LoadDiagnosers() if (RuntimeInformation.IsWindows()) return LoadClassic(); - return LoadCore(); + return LoadCore().ToArray(); } - private static IDiagnoser[] LoadCore() => new IDiagnoser[] { MemoryDiagnoser.Default }; + private static IEnumerable LoadCore() + { + yield return MemoryDiagnoser.Default; + + if (RuntimeInformation.IsLinux()) + yield return LttngProfiler.Default; + } private static IDiagnoser[] LoadMono() => new IDiagnoser[] diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs new file mode 100644 index 0000000000..dd9b7bc370 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Portability; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Validators; +using JetBrains.Annotations; + +namespace BenchmarkDotNet.Diagnosers +{ + public class LttngProfiler : IProfiler + { + private const string PerfCollectFileName = "perfcollect"; + + public static readonly IDiagnoser Default = new LttngProfiler(new LttngProfilerConfig(performExtraBenchmarksRun: false)); + + private readonly LttngProfilerConfig config; + private readonly DateTime creationTime = DateTime.Now; + private readonly Dictionary benchmarkToTraceFile = new Dictionary(); + + private Process perfCollectProcess; + private ConsoleExitHandler consoleExitHandler; + + [PublicAPI] + public LttngProfiler(LttngProfilerConfig config) => this.config = config; + + public string ShortName => "LTTng"; + + public IEnumerable Ids => new[] { nameof(LttngProfiler) }; + + public IEnumerable Exporters => Array.Empty(); + + public IEnumerable Analysers => Array.Empty(); + + public IEnumerable ProcessResults(DiagnoserResults results) => Array.Empty(); + + public RunMode GetRunMode(BenchmarkCase benchmarkCase) => config.RunMode; + + public IEnumerable Validate(ValidationParameters validationParameters) + { + if (!RuntimeInformation.IsLinux()) + { + yield return new ValidationError(true, "The LttngProfiler works only on Linux!"); + } + if (validationParameters.Benchmarks.Any() && !TryInstallPerfCollect(validationParameters)) + { + yield return new ValidationError(true, "Please run as sudo, it's required to use LttngProfiler."); + } + } + + public void DisplayResults(ILogger logger) + { + if (!benchmarkToTraceFile.Any()) + return; + + logger.WriteLineInfo($"Exported {benchmarkToTraceFile.Count} trace file(s). Example:"); + logger.WriteLineInfo(benchmarkToTraceFile.Values.First().FullName); + } + + public void Handle(HostSignal signal, DiagnoserActionParameters parameters) + { + // it's crucial to start the trace before the process starts and stop it after the benchmarked process stops to have all of the necessary events in the trace file! + if (signal == HostSignal.BeforeProcessStart) + Start(parameters); + else if (signal == HostSignal.AfterProcessExit) + Stop(parameters); + } + + private bool TryInstallPerfCollect(ValidationParameters validationParameters) + { + var scriptInstallationDirectory = new DirectoryInfo(validationParameters.Config.ArtifactsPath).CreateIfNotExists(); + + var perfCollectFile = scriptInstallationDirectory.GetFiles(PerfCollectFileName).SingleOrDefault(); + if (perfCollectFile != default) + { + return true; + } + + var logger = validationParameters.Config.GetCompositeLogger(); + perfCollectFile = new FileInfo(Path.Combine(scriptInstallationDirectory.FullName, PerfCollectFileName)); + using (var client = new WebClient()) + { + logger.WriteLineInfo($"downloading perfcollect: {perfCollectFile.FullName}"); + client.DownloadFile("https://aka.ms/perfcollect", "perfcollect"); + } + + var processOutput = ProcessHelper.RunAndReadOutput("/bin/bash", $"-c \"sudo chmod +x {perfCollectFile.FullName}\"", logger); + if (processOutput != null) + { + processOutput = ProcessHelper.RunAndReadOutput("/bin/bash", $"-c \"sudo {perfCollectFile.FullName} install\"", logger); + } + + if (processOutput != null) + { + return true; + } + + if (perfCollectFile.Exists) + { + perfCollectFile.Delete(); // if the file exists it means that perfcollect is installed + } + + return false; + } + + private void Start(DiagnoserActionParameters parameters) + { + var perfCollectFile = new FileInfo(Directory.GetFiles(parameters.Config.ArtifactsPath, PerfCollectFileName).Single()); + + perfCollectProcess = CreatePerfCollectProcess(parameters, perfCollectFile); + + consoleExitHandler = new ConsoleExitHandler(perfCollectProcess, parameters.Config.GetCompositeLogger()); + + perfCollectProcess.Start(); + } + + private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, FileInfo perfCollectFile) + { + var traceName = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, fileExtension: null).Name; + + var start = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = $"-c \"sudo {perfCollectFile.FullName} collect {traceName}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = perfCollectFile.Directory.FullName + }; + + return new Process { StartInfo = start }; + } + + private void Stop(DiagnoserActionParameters parameters) + { + try + { + perfCollectProcess.StandardInput.Close(); // signal Ctrl + C to the script to tell it to stop profiling + + if (!perfCollectProcess.WaitForExit((int)config.TracePostProcessingTimeout.TotalMilliseconds)) + { + var logger = parameters.Config.GetCompositeLogger(); + logger.WriteLineError($"The perfcollect script did not finish the post processing in {config.TracePostProcessingTimeout.TotalSeconds}s."); + logger.WriteLineInfo("You can create LttngProfiler providing LttngProfilerConfig with custom timeout value."); + + perfCollectProcess.KillTree(); + } + + benchmarkToTraceFile[parameters.BenchmarkCase] = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, ".trace.zip"); + } + finally + { + consoleExitHandler.Dispose(); + consoleExitHandler = null; + perfCollectProcess.Dispose(); + perfCollectProcess = null; + } + } + } + + public class LttngProfilerConfig + { + /// if set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. True by default. + /// how long should we wait for the perfcollect script to finish processing trace. 30s by default + public LttngProfilerConfig(bool performExtraBenchmarksRun = true, int tracePostProcessingTimeoutInSeconds = 30) + { + RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; + TracePostProcessingTimeout = TimeSpan.FromSeconds(tracePostProcessingTimeoutInSeconds); + } + + public TimeSpan TracePostProcessingTimeout { get; } + + public RunMode RunMode { get; } + } +} diff --git a/src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs b/src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs new file mode 100644 index 0000000000..4cedee1e7a --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs @@ -0,0 +1,37 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains; +using System; +using System.IO; +using System.Linq; + +namespace BenchmarkDotNet.Diagnosers +{ + internal static class TraceFileHelper + { + internal static FileInfo GetFilePath(BenchmarkCase benchmarkCase, ImmutableConfig config, DateTime creationTime, string fileExtension) + { + string fileName = $@"{FolderNameHelper.ToFolderName(benchmarkCase.Descriptor.Type)}.{FullNameProvider.GetMethodName(benchmarkCase)}"; + + // if we run for more than one toolchain, the output file name should contain the name too so we can differ net461 vs netcoreapp2.1 etc + if (config.GetJobs().Select(job => job.GetToolchain()).Distinct().Count() > 1) + fileName += $"-{benchmarkCase.Job.Environment.Runtime?.Name ?? benchmarkCase.Job.GetToolchain()?.Name ?? benchmarkCase.Job.Id}"; + + fileName += $"-{creationTime.ToString(BenchmarkRunnerClean.DateTimeFormat)}"; + + fileName = FolderNameHelper.ToFolderName(fileName); + + return new FileInfo(Path.Combine(config.ArtifactsPath, $"{fileName}{fileExtension}")); + } + + internal static FileInfo EnsureFolderExists(this FileInfo file) + { + if (!file.Directory.Exists) + file.Directory.Create(); + + return file; + } + } +} diff --git a/src/BenchmarkDotNet/Extensions/CommonExtensions.cs b/src/BenchmarkDotNet/Extensions/CommonExtensions.cs index bddafaf690..6259c6dbfd 100644 --- a/src/BenchmarkDotNet/Extensions/CommonExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/CommonExtensions.cs @@ -123,6 +123,14 @@ internal static string CreateIfNotExists(this string directoryPath) return directoryPath; } + internal static DirectoryInfo CreateIfNotExists(this DirectoryInfo directory) + { + if (!directory.Exists) + directory.Create(); + + return directory; + } + internal static bool IsNotNullButDoesNotExist(this FileSystemInfo fileInfo) => fileInfo != null && !fileInfo.Exists; } diff --git a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs index 16a32df2b7..62fb666be0 100644 --- a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs @@ -3,7 +3,9 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Linq; using BenchmarkDotNet.Characteristics; +using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; @@ -119,6 +121,12 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm if (benchmarkCase.Job.Environment.Runtime is MonoRuntime monoRuntime && !string.IsNullOrEmpty(monoRuntime.MonoBclPath)) start.EnvironmentVariables["MONO_PATH"] = monoRuntime.MonoBclPath; + if (benchmarkCase.Config.GetDiagnosers().OfType().Any()) + { + start.EnvironmentVariables["COMPlus_PerfMapEnabled"] = "1"; + start.EnvironmentVariables["COMPlus_EnableEventLog"] = "1"; + } + if (!benchmarkCase.Job.HasValue(EnvironmentMode.EnvironmentVariablesCharacteristic)) return; From c4921ada1f969d951cda9380cdd84523427b2d63 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Sun, 9 Jun 2019 18:16:50 +0200 Subject: [PATCH 02/24] download the script to the right folder --- src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index dd9b7bc370..8dfe326bec 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -91,7 +91,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) using (var client = new WebClient()) { logger.WriteLineInfo($"downloading perfcollect: {perfCollectFile.FullName}"); - client.DownloadFile("https://aka.ms/perfcollect", "perfcollect"); + client.DownloadFile("https://aka.ms/perfcollect", perfCollectFile.FullName); } var processOutput = ProcessHelper.RunAndReadOutput("/bin/bash", $"-c \"sudo chmod +x {perfCollectFile.FullName}\"", logger); From c59b96d8822aa4229a0f58e3500c0973ce953812 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Sun, 9 Jun 2019 18:57:56 +0200 Subject: [PATCH 03/24] escepe the arguments, wait until it starts actual collection, don't generate the path if it has failed --- src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index 8dfe326bec..b849abcaf2 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -122,6 +122,11 @@ private void Start(DiagnoserActionParameters parameters) consoleExitHandler = new ConsoleExitHandler(perfCollectProcess, parameters.Config.GetCompositeLogger()); perfCollectProcess.Start(); + + while(perfCollectProcess.StandardOutput.ReadLine()?.IndexOf("Collection started", StringComparison.OrdinalIgnoreCase) < 0) + { + // wait until the script starts the actual collection + } } private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, FileInfo perfCollectFile) @@ -131,7 +136,7 @@ private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, F var start = new ProcessStartInfo { FileName = "/bin/bash", - Arguments = $"-c \"sudo {perfCollectFile.FullName} collect {traceName}\"", + Arguments = $"-c \"sudo '{perfCollectFile.FullName}' collect '{traceName}'\"", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardInput = true, @@ -158,7 +163,10 @@ private void Stop(DiagnoserActionParameters parameters) perfCollectProcess.KillTree(); } - benchmarkToTraceFile[parameters.BenchmarkCase] = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, ".trace.zip"); + if (perfCollectProcess.HasExited && perfCollectProcess.ExitCode == 0) + { + benchmarkToTraceFile[parameters.BenchmarkCase] = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, ".trace.zip"); + } } finally { From 41718858b3d48f690515f73fb980250cde432f15 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Sun, 9 Jun 2019 19:03:22 +0200 Subject: [PATCH 04/24] wait until the script ends post-processing --- src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index b849abcaf2..bc164ff90c 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -154,7 +154,12 @@ private void Stop(DiagnoserActionParameters parameters) { perfCollectProcess.StandardInput.Close(); // signal Ctrl + C to the script to tell it to stop profiling - if (!perfCollectProcess.WaitForExit((int)config.TracePostProcessingTimeout.TotalMilliseconds)) + while (perfCollectProcess.StandardOutput.ReadLine()?.IndexOf("Trace saved", StringComparison.OrdinalIgnoreCase) < 0) + { + // wait until the script ends post-processing + } + + if (!perfCollectProcess.HasExited && !perfCollectProcess.WaitForExit((int)config.TracePostProcessingTimeout.TotalMilliseconds)) { var logger = parameters.Config.GetCompositeLogger(); logger.WriteLineError($"The perfcollect script did not finish the post processing in {config.TracePostProcessingTimeout.TotalSeconds}s."); From 77e3a523f2091a20db2eac7ea9e3e8e1d4b5e969 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Sun, 9 Jun 2019 22:15:52 +0200 Subject: [PATCH 05/24] use Mono.Posix to send SIGINT (Ctrl+C) to the script --- src/BenchmarkDotNet/BenchmarkDotNet.csproj | 1 + src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index 5a7488e52f..6c4fd2510e 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -17,6 +17,7 @@ + diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index bc164ff90c..aff921dc00 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -152,7 +152,7 @@ private void Stop(DiagnoserActionParameters parameters) { try { - perfCollectProcess.StandardInput.Close(); // signal Ctrl + C to the script to tell it to stop profiling + Mono.Unix.Native.Syscall.kill(perfCollectProcess.Id, Mono.Unix.Native.Signum.SIGINT); // signal Ctrl + C to the script to tell it to stop profiling while (perfCollectProcess.StandardOutput.ReadLine()?.IndexOf("Trace saved", StringComparison.OrdinalIgnoreCase) < 0) { From 11c5e063610a0c1397ec2d8503ee6b0b1a9c03db Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 1 Jul 2019 17:25:06 +0200 Subject: [PATCH 06/24] more changes --- .../Diagnosers/LttngProfiler.cs | 131 ++++++++++++------ 1 file changed, 91 insertions(+), 40 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index aff921dc00..0702261c97 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Threading; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; @@ -20,8 +21,8 @@ namespace BenchmarkDotNet.Diagnosers { public class LttngProfiler : IProfiler { + private const int SuccesExitCode = 0; private const string PerfCollectFileName = "perfcollect"; - public static readonly IDiagnoser Default = new LttngProfiler(new LttngProfilerConfig(performExtraBenchmarksRun: false)); private readonly LttngProfilerConfig config; @@ -29,7 +30,7 @@ public class LttngProfiler : IProfiler private readonly Dictionary benchmarkToTraceFile = new Dictionary(); private Process perfCollectProcess; - private ConsoleExitHandler consoleExitHandler; + private ManualResetEventSlim signal = new ManualResetEventSlim(); [PublicAPI] public LttngProfiler(LttngProfilerConfig config) => this.config = config; @@ -51,10 +52,18 @@ public IEnumerable Validate(ValidationParameters validationPara if (!RuntimeInformation.IsLinux()) { yield return new ValidationError(true, "The LttngProfiler works only on Linux!"); + yield break; + } + + if (Mono.Unix.Native.Syscall.getuid() != 0) + { + yield return new ValidationError(true, "You must run as root to use LttngProfiler."); + yield break; } + if (validationParameters.Benchmarks.Any() && !TryInstallPerfCollect(validationParameters)) { - yield return new ValidationError(true, "Please run as sudo, it's required to use LttngProfiler."); + yield return new ValidationError(true, "Failed to install perfcollect script. Please follow the instructions from https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#preparing-your-machine"); } } @@ -70,7 +79,7 @@ public void DisplayResults(ILogger logger) public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { // it's crucial to start the trace before the process starts and stop it after the benchmarked process stops to have all of the necessary events in the trace file! - if (signal == HostSignal.BeforeProcessStart) + if (signal == HostSignal.BeforeAnythingElse) Start(parameters); else if (signal == HostSignal.AfterProcessExit) Stop(parameters); @@ -90,19 +99,28 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) perfCollectFile = new FileInfo(Path.Combine(scriptInstallationDirectory.FullName, PerfCollectFileName)); using (var client = new WebClient()) { - logger.WriteLineInfo($"downloading perfcollect: {perfCollectFile.FullName}"); + logger.WriteLineInfo($"// Downloading perfcollect: {perfCollectFile.FullName}"); client.DownloadFile("https://aka.ms/perfcollect", perfCollectFile.FullName); } - var processOutput = ProcessHelper.RunAndReadOutput("/bin/bash", $"-c \"sudo chmod +x {perfCollectFile.FullName}\"", logger); - if (processOutput != null) + if (Mono.Unix.Native.Syscall.chmod(perfCollectFile.FullName, Mono.Unix.Native.FilePermissions.S_IXUSR) != SuccesExitCode) { - processOutput = ProcessHelper.RunAndReadOutput("/bin/bash", $"-c \"sudo {perfCollectFile.FullName} install\"", logger); + logger.WriteError($"Unable to make perfcollect script an executable, the last error was: {Mono.Unix.Native.Syscall.GetLastError()}"); } - - if (processOutput != null) + else { - return true; + (int exitCode, var output) = ProcessHelper.RunAndReadOutputLineByLine(perfCollectFile.FullName, "install", perfCollectFile.Directory.FullName, null, includeErrors: true, logger); + + if (exitCode == SuccesExitCode) + { + return true; + } + + logger.WriteLineError("Failed to install perfcollect"); + foreach(var outputLine in output) + { + logger.WriteLine(outputLine); + } } if (perfCollectFile.Exists) @@ -119,28 +137,46 @@ private void Start(DiagnoserActionParameters parameters) perfCollectProcess = CreatePerfCollectProcess(parameters, perfCollectFile); - consoleExitHandler = new ConsoleExitHandler(perfCollectProcess, parameters.Config.GetCompositeLogger()); + var logger = parameters.Config.GetCompositeLogger(); + + perfCollectProcess.OutputDataReceived += OnOutputDataReceived; + + signal.Reset(); perfCollectProcess.Start(); + perfCollectProcess.BeginOutputReadLine(); - while(perfCollectProcess.StandardOutput.ReadLine()?.IndexOf("Collection started", StringComparison.OrdinalIgnoreCase) < 0) + WaitForSignal(logger, "// Collection with perfcollect started"); // wait until the script starts the actual collection + } + + private void Stop(DiagnoserActionParameters parameters) + { + if (perfCollectProcess == null) { - // wait until the script starts the actual collection + return; + } + + var logger = parameters.Config.GetCompositeLogger(); + + if (WaitForSignal(logger, "// Collection with perfcollect stopped")) + { + benchmarkToTraceFile[parameters.BenchmarkCase] = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, ".trace.zip"); + + CleanupPerfCollectProcess(logger); } } private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, FileInfo perfCollectFile) { var traceName = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, fileExtension: null).Name; + // todo: escape characters bash does not like ' ', '(' etc var start = new ProcessStartInfo { - FileName = "/bin/bash", - Arguments = $"-c \"sudo '{perfCollectFile.FullName}' collect '{traceName}'\"", + FileName = perfCollectFile.FullName, + Arguments = $"collect {traceName} -pid {parameters.Process.Id}", UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardInput = true, - RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = perfCollectFile.Directory.FullName }; @@ -148,35 +184,50 @@ private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, F return new Process { StartInfo = start }; } - private void Stop(DiagnoserActionParameters parameters) + private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) { - try + if (e.Data?.IndexOf("Collection started", StringComparison.OrdinalIgnoreCase) >= 0) + signal.Set(); + else if (e.Data?.IndexOf("Trace saved", StringComparison.OrdinalIgnoreCase) >= 0) + signal.Set(); + else if (e.Data?.IndexOf("This script must be run as root", StringComparison.OrdinalIgnoreCase) >= 0) + Environment.FailFast("To use LttngProfiler you must run as root."); // should never happen, ensured by Validate() + } + + private bool WaitForSignal(ILogger logger, string message) + { + if (signal.Wait(config.Timeout)) { - Mono.Unix.Native.Syscall.kill(perfCollectProcess.Id, Mono.Unix.Native.Signum.SIGINT); // signal Ctrl + C to the script to tell it to stop profiling + signal.Reset(); - while (perfCollectProcess.StandardOutput.ReadLine()?.IndexOf("Trace saved", StringComparison.OrdinalIgnoreCase) < 0) - { - // wait until the script ends post-processing - } + logger.WriteLineInfo(message); - if (!perfCollectProcess.HasExited && !perfCollectProcess.WaitForExit((int)config.TracePostProcessingTimeout.TotalMilliseconds)) - { - var logger = parameters.Config.GetCompositeLogger(); - logger.WriteLineError($"The perfcollect script did not finish the post processing in {config.TracePostProcessingTimeout.TotalSeconds}s."); - logger.WriteLineInfo("You can create LttngProfiler providing LttngProfilerConfig with custom timeout value."); + return true; + } - perfCollectProcess.KillTree(); - } + logger.WriteLineError($"The perfcollect script did not start/finish in {config.Timeout.TotalSeconds}s."); + logger.WriteLineInfo("You can create LttngProfiler providing LttngProfilerConfig with custom timeout value."); + + CleanupPerfCollectProcess(logger); + + return false; + } + + private void CleanupPerfCollectProcess(ILogger logger) + { + logger.Flush(); // flush recently logged message to disk + + try + { + perfCollectProcess.OutputDataReceived -= OnOutputDataReceived; - if (perfCollectProcess.HasExited && perfCollectProcess.ExitCode == 0) + if (!perfCollectProcess.HasExited) { - benchmarkToTraceFile[parameters.BenchmarkCase] = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, ".trace.zip"); + perfCollectProcess.KillTree(); // kill the entire process tree } } finally { - consoleExitHandler.Dispose(); - consoleExitHandler = null; perfCollectProcess.Dispose(); perfCollectProcess = null; } @@ -186,14 +237,14 @@ private void Stop(DiagnoserActionParameters parameters) public class LttngProfilerConfig { /// if set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. True by default. - /// how long should we wait for the perfcollect script to finish processing trace. 30s by default - public LttngProfilerConfig(bool performExtraBenchmarksRun = true, int tracePostProcessingTimeoutInSeconds = 30) + /// how long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 30s by default + public LttngProfilerConfig(bool performExtraBenchmarksRun = true, int timeoutInSeconds = 60) { RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; - TracePostProcessingTimeout = TimeSpan.FromSeconds(tracePostProcessingTimeoutInSeconds); + Timeout = TimeSpan.FromSeconds(timeoutInSeconds); } - public TimeSpan TracePostProcessingTimeout { get; } + public TimeSpan Timeout { get; } public RunMode RunMode { get; } } From 2c63c4c06d94ccd64272de5b8d112cee58cd7a59 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 3 Mar 2020 15:24:56 +0100 Subject: [PATCH 07/24] remove duplicated code --- .../Diagnosers/LttngProfiler.cs | 6 +-- .../Diagnosers/TraceFileHelper.cs | 37 ------------------- .../Helpers/ArtifactFileNameHelper.cs | 2 +- 3 files changed, 4 insertions(+), 41 deletions(-) delete mode 100644 src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index 0702261c97..b28082cb83 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -88,7 +88,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) private bool TryInstallPerfCollect(ValidationParameters validationParameters) { var scriptInstallationDirectory = new DirectoryInfo(validationParameters.Config.ArtifactsPath).CreateIfNotExists(); - + var perfCollectFile = scriptInstallationDirectory.GetFiles(PerfCollectFileName).SingleOrDefault(); if (perfCollectFile != default) { @@ -160,7 +160,7 @@ private void Stop(DiagnoserActionParameters parameters) if (WaitForSignal(logger, "// Collection with perfcollect stopped")) { - benchmarkToTraceFile[parameters.BenchmarkCase] = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, ".trace.zip"); + benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetFilePath(parameters, creationTime, ".trace.zip")); CleanupPerfCollectProcess(logger); } @@ -168,7 +168,7 @@ private void Stop(DiagnoserActionParameters parameters) private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, FileInfo perfCollectFile) { - var traceName = TraceFileHelper.GetFilePath(parameters.BenchmarkCase, parameters.Config, creationTime, fileExtension: null).Name; + var traceName = new FileInfo(ArtifactFileNameHelper.GetFilePath(parameters, creationTime, fileExtension: null)).Name; // todo: escape characters bash does not like ' ', '(' etc var start = new ProcessStartInfo diff --git a/src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs b/src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs deleted file mode 100644 index 4cedee1e7a..0000000000 --- a/src/BenchmarkDotNet/Diagnosers/TraceFileHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Exporters; -using BenchmarkDotNet.Helpers; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Toolchains; -using System; -using System.IO; -using System.Linq; - -namespace BenchmarkDotNet.Diagnosers -{ - internal static class TraceFileHelper - { - internal static FileInfo GetFilePath(BenchmarkCase benchmarkCase, ImmutableConfig config, DateTime creationTime, string fileExtension) - { - string fileName = $@"{FolderNameHelper.ToFolderName(benchmarkCase.Descriptor.Type)}.{FullNameProvider.GetMethodName(benchmarkCase)}"; - - // if we run for more than one toolchain, the output file name should contain the name too so we can differ net461 vs netcoreapp2.1 etc - if (config.GetJobs().Select(job => job.GetToolchain()).Distinct().Count() > 1) - fileName += $"-{benchmarkCase.Job.Environment.Runtime?.Name ?? benchmarkCase.Job.GetToolchain()?.Name ?? benchmarkCase.Job.Id}"; - - fileName += $"-{creationTime.ToString(BenchmarkRunnerClean.DateTimeFormat)}"; - - fileName = FolderNameHelper.ToFolderName(fileName); - - return new FileInfo(Path.Combine(config.ArtifactsPath, $"{fileName}{fileExtension}")); - } - - internal static FileInfo EnsureFolderExists(this FileInfo file) - { - if (!file.Directory.Exists) - file.Directory.Create(); - - return file; - } - } -} diff --git a/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs index 52bbdab888..2a8a5dd57c 100644 --- a/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs @@ -7,7 +7,7 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains; -namespace BenchmarkDotNet.Helpers +namespace BenchmarkDotNet.Helpers { internal static class ArtifactFileNameHelper { From c77e7dfe7c51fc62fcb28619858e5b4455c1f3c0 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 3 Mar 2020 15:27:07 +0100 Subject: [PATCH 08/24] add perfcollect to the resources, don't download it --- .../Diagnosers/LttngProfiler.cs | 10 +- src/BenchmarkDotNet/Templates/perfcollect | 1982 +++++++++++++++++ 2 files changed, 1986 insertions(+), 6 deletions(-) create mode 100644 src/BenchmarkDotNet/Templates/perfcollect diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index b28082cb83..c62c3fc731 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Net; using System.Threading; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Engines; @@ -23,6 +22,7 @@ public class LttngProfiler : IProfiler { private const int SuccesExitCode = 0; private const string PerfCollectFileName = "perfcollect"; + public static readonly IDiagnoser Default = new LttngProfiler(new LttngProfilerConfig(performExtraBenchmarksRun: false)); private readonly LttngProfilerConfig config; @@ -97,11 +97,9 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) var logger = validationParameters.Config.GetCompositeLogger(); perfCollectFile = new FileInfo(Path.Combine(scriptInstallationDirectory.FullName, PerfCollectFileName)); - using (var client = new WebClient()) - { - logger.WriteLineInfo($"// Downloading perfcollect: {perfCollectFile.FullName}"); - client.DownloadFile("https://aka.ms/perfcollect", perfCollectFile.FullName); - } + + string script = ResourceHelper.LoadTemplate(PerfCollectFileName); + File.WriteAllText(perfCollectFile.FullName, script); if (Mono.Unix.Native.Syscall.chmod(perfCollectFile.FullName, Mono.Unix.Native.FilePermissions.S_IXUSR) != SuccesExitCode) { diff --git a/src/BenchmarkDotNet/Templates/perfcollect b/src/BenchmarkDotNet/Templates/perfcollect new file mode 100644 index 0000000000..a15a484a5a --- /dev/null +++ b/src/BenchmarkDotNet/Templates/perfcollect @@ -0,0 +1,1982 @@ +#!/bin/bash + +############################################################################################################# +# .NET Performance Data Collection Script +############################################################################################################# + +############################################################################################################# +# +# ***** HOW TO USE THIS SCRIPT ***** +# +# This script can be used to collect and view performance data collected with perf_event on Linux. + +# It's job is to make it simple to collect performance traces. +# +# How to collect a performance trace: +# 1. Prior to starting the .NET process, set the environment variable COMPlus_PerfMapEnabled=1. +# This tells the runtime to emit information that enables perf_event to resolve JIT-compiled code symbols. +# +# 2. Setup your system to reproduce the performance issue you'd like to capture. Data collection can be +# started on already running processes. +# +# 3. [Usage #1] use "collect" command +# - Run this script: sudo ./perfcollect collect samplePerfTrace. This will start data collection. +# - Let the repro run as long as you need to capture the performance problem. +# - Hit CTRL+C to stop collection. +# +# [Usage #2] use "start" and "stop" command +# - Run this script: sudo ./perfcollect start samplePerfTrace. This will start data colletion. +# - Let the repro run as long as you need to capture the performance problem. +# - Run: sudo ./perfcollect stop samplePerfTrace. This will stop collection. +# +# 4. When collection is stopped, the script will create a trace.zip file matching the name specified on the +# command line. This file will contain the trace, JIT-compiled symbol information, and all debugging +# symbols for binaries referenced by this trace that were available on the machine at collection time. +# +# How to view a performance trace: +# 1. Run this script: ./perfcollect view samplePerfTrace.trace.zip +# This will extract the trace, place and register all symbol files and JIT-compiled symbol information +# and start the perf_event viewer. By default, you will be looking at a callee view - stacks are ordered +# top down. For a caller or bottom up view, specify '-graphtype caller'. +############################################################################################################# + +###################################### +## FOR DEBUGGING ONLY +###################################### +# set -x + +###################################### +## Collection Options +## NOTE: These values represent the collection defaults. +###################################### + +# Set when we parse command line arguments to determine if we should enable specific collection options. +collect_cpu=1 +collect_threadTime=0 + +###################################### +## .NET Event Categories +###################################### + +# Separate GCCollectOnly list because our LTTng implementation doesn't set tracepoint verbosity. +# Once tracepoint verbosity is set, we can set verbosity and collapse this wtih DotNETRuntime_GCKeyword. +declare -a DotNETRuntime_GCKeyword_GCCollectOnly=( + DotNETRuntime:GCStart + DotNETRuntime:GCStart_V1 + DotNETRuntime:GCStart_V2 + DotNETRuntime:GCEnd + DotNETRuntime:GCEnd_V1 + DotNETRuntime:GCRestartEEEnd + DotNETRuntime:GCRestartEEEnd_V1 + DotNETRuntime:GCHeapStats + DotNETRuntime:GCHeapStats_V1 + DotNETRuntime:GCCreateSegment + DotNETRuntime:GCCreateSegment_V1 + DotNETRuntime:GCFreeSegment + DotNETRuntime:GCFreeSegment_V1 + DotNETRuntime:GCRestartEEBegin + DotNETRuntime:GCRestartEEBegin_V1 + DotNETRuntime:GCSuspendEEEnd + DotNETRuntime:GCSuspendEEEnd_V1 + DotNETRuntime:GCSuspendEEBegin + DotNETRuntime:GCSuspendEEBegin_V1 + DotNETRuntime:GCCreateConcurrentThread + DotNETRuntime:GCTerminateConcurrentThread + DotNETRuntime:GCFinalizersEnd + DotNETRuntime:GCFinalizersEnd_V1 + DotNETRuntime:GCFinalizersBegin + DotNETRuntime:GCFinalizersBegin_V1 + DotNETRuntime:GCMarkStackRoots + DotNETRuntime:GCMarkFinalizeQueueRoots + DotNETRuntime:GCMarkHandles + DotNETRuntime:GCMarkOlderGenerationRoots + DotNETRuntime:FinalizeObject + DotNETRuntime:GCTriggered + DotNETRuntime:IncreaseMemoryPressure + DotNETRuntime:DecreaseMemoryPressure + DotNETRuntime:GCMarkWithType + DotNETRuntime:GCPerHeapHistory_V3 + DotNETRuntime:GCGlobalHeapHistory_V2 + DotNETRuntime:GCCreateConcurrentThread_V1 + DotNETRuntime:GCTerminateConcurrentThread_V1 +) + + +declare -a DotNETRuntime_GCKeyword=( + DotNETRuntime:GCStart + DotNETRuntime:GCStart_V1 + DotNETRuntime:GCStart_V2 + DotNETRuntime:GCEnd + DotNETRuntime:GCEnd_V1 + DotNETRuntime:GCRestartEEEnd + DotNETRuntime:GCRestartEEEnd_V1 + DotNETRuntime:GCHeapStats + DotNETRuntime:GCHeapStats_V1 + DotNETRuntime:GCCreateSegment + DotNETRuntime:GCCreateSegment_V1 + DotNETRuntime:GCFreeSegment + DotNETRuntime:GCFreeSegment_V1 + DotNETRuntime:GCRestartEEBegin + DotNETRuntime:GCRestartEEBegin_V1 + DotNETRuntime:GCSuspendEEEnd + DotNETRuntime:GCSuspendEEEnd_V1 + DotNETRuntime:GCSuspendEEBegin + DotNETRuntime:GCSuspendEEBegin_V1 + DotNETRuntime:GCAllocationTick + DotNETRuntime:GCAllocationTick_V1 + DotNETRuntime:GCAllocationTick_V2 + DotNETRuntime:GCAllocationTick_V3 + DotNETRuntime:GCCreateConcurrentThread + DotNETRuntime:GCTerminateConcurrentThread + DotNETRuntime:GCFinalizersEnd + DotNETRuntime:GCFinalizersEnd_V1 + DotNETRuntime:GCFinalizersBegin + DotNETRuntime:GCFinalizersBegin_V1 + DotNETRuntime:GCMarkStackRoots + DotNETRuntime:GCMarkFinalizeQueueRoots + DotNETRuntime:GCMarkHandles + DotNETRuntime:GCMarkOlderGenerationRoots + DotNETRuntime:FinalizeObject + DotNETRuntime:PinObjectAtGCTime + DotNETRuntime:GCTriggered + DotNETRuntime:IncreaseMemoryPressure + DotNETRuntime:DecreaseMemoryPressure + DotNETRuntime:GCMarkWithType + DotNETRuntime:GCJoin_V2 + DotNETRuntime:GCPerHeapHistory_V3 + DotNETRuntime:GCGlobalHeapHistory_V2 + DotNETRuntime:GCCreateConcurrentThread_V1 + DotNETRuntime:GCTerminateConcurrentThread_V1 +) + +declare -a DotNETRuntime_TypeKeyword=( + DotNETRuntime:BulkType +) + +declare -a DotNETRuntime_GCHeapDumpKeyword=( + DotNETRuntime:GCBulkRootEdge + DotNETRuntime:GCBulkRootConditionalWeakTableElementEdge + DotNETRuntime:GCBulkNode + DotNETRuntime:GCBulkEdge + DotNETRuntime:GCBulkRootCCW + DotNETRuntime:GCBulkRCW + DotNETRuntime:GCBulkRootStaticVar +) + +declare -a DotNETRuntime_GCSampledObjectAllocationHighKeyword=( + DotNETRuntime:GCSampledObjectAllocationHigh +) + +declare -a DotNETRuntime_GCHeapSurvivalAndMovementKeyword=( + DotNETRuntime:GCBulkSurvivingObjectRanges + DotNETRuntime:GCBulkMovedObjectRanges + DotNETRuntime:GCGenerationRange +) + +declare -a DotNETRuntime_GCHandleKeyword=( + DotNETRuntime:SetGCHandle + DotNETRuntime:DestroyGCHandle +) + +declare -a DotNETRuntime_GCSampledObjectAllocationLowKeyword=( + DotNETRuntime:GCSampledObjectAllocationLow +) + +declare -a DotNETRuntime_ThreadingKeyword=( + DotNETRuntime:WorkerThreadCreate + DotNETRuntime:WorkerThreadTerminate + DotNETRuntime:WorkerThreadRetire + DotNETRuntime:WorkerThreadUnretire + DotNETRuntime:IOThreadCreate + DotNETRuntime:IOThreadCreate_V1 + DotNETRuntime:IOThreadTerminate + DotNETRuntime:IOThreadTerminate_V1 + DotNETRuntime:IOThreadRetire + DotNETRuntime:IOThreadRetire_V1 + DotNETRuntime:IOThreadUnretire + DotNETRuntime:IOThreadUnretire_V1 + DotNETRuntime:ThreadpoolSuspensionSuspendThread + DotNETRuntime:ThreadpoolSuspensionResumeThread + DotNETRuntime:ThreadPoolWorkerThreadStart + DotNETRuntime:ThreadPoolWorkerThreadStop + DotNETRuntime:ThreadPoolWorkerThreadRetirementStart + DotNETRuntime:ThreadPoolWorkerThreadRetirementStop + DotNETRuntime:ThreadPoolWorkerThreadAdjustmentSample + DotNETRuntime:ThreadPoolWorkerThreadAdjustmentAdjustment + DotNETRuntime:ThreadPoolWorkerThreadAdjustmentStats + DotNETRuntime:ThreadPoolWorkerThreadWait + DotNETRuntime:ThreadPoolWorkingThreadCount + DotNETRuntime:ThreadPoolIOPack + DotNETRuntime:GCCreateConcurrentThread_V1 + DotNETRuntime:GCTerminateConcurrentThread_V1 +) + +declare -a DotNETRuntime_ThreadingKeyword_ThreadTransferKeyword=( + DotNETRuntime:ThreadPoolEnqueue + DotNETRuntime:ThreadPoolDequeue + DotNETRuntime:ThreadPoolIOEnqueue + DotNETRuntime:ThreadPoolIODequeue + DotNETRuntime:ThreadCreating + DotNETRuntime:ThreadRunning +) + +declare -a DotNETRuntime_NoKeyword=( + DotNETRuntime:ExceptionThrown + DotNETRuntime:Contention + DotNETRuntime:RuntimeInformationStart + DotNETRuntime:EventSource +) + +declare -a DotNETRuntime_ExceptionKeyword=( + DotNETRuntime:ExceptionThrown_V1 + DotNETRuntime:ExceptionCatchStart + DotNETRuntime:ExceptionCatchStop + DotNETRuntime:ExceptionFinallyStart + DotNETRuntime:ExceptionFinallyStop + DotNETRuntime:ExceptionFilterStart + DotNETRuntime:ExceptionFilterStop + DotNETRuntime:ExceptionThrownStop +) + +declare -a DotNETRuntime_ContentionKeyword=( + DotNETRuntime:ContentionStart_V1 + DotNETRuntime:ContentionStop + DotNETRuntime:ContentionStop_V1 +) + +declare -a DotNETRuntime_StackKeyword=( + DotNETRuntime:CLRStackWalk +) + +declare -a DotNETRuntime_AppDomainResourceManagementKeyword=( + DotNETRuntime:AppDomainMemAllocated + DotNETRuntime:AppDomainMemSurvived +) + +declare -a DotNETRuntime_AppDomainResourceManagementKeyword_ThreadingKeyword=( + DotNETRuntime:ThreadCreated + DotNETRuntime:ThreadTerminated + DotNETRuntime:ThreadDomainEnter +) + +declare -a DotNETRuntime_InteropKeyword=( + DotNETRuntime:ILStubGenerated + DotNETRuntime:ILStubCacheHit +) + +declare -a DotNETRuntime_JitKeyword_NGenKeyword=( + DotNETRuntime:DCStartCompleteV2 + DotNETRuntime:DCEndCompleteV2 + DotNETRuntime:MethodDCStartV2 + DotNETRuntime:MethodDCEndV2 + DotNETRuntime:MethodDCStartVerboseV2 + DotNETRuntime:MethodDCEndVerboseV2 + DotNETRuntime:MethodLoad + DotNETRuntime:MethodLoad_V1 + DotNETRuntime:MethodLoad_V2 + DotNETRuntime:MethodUnload + DotNETRuntime:MethodUnload_V1 + DotNETRuntime:MethodUnload_V2 + DotNETRuntime:MethodLoadVerbose + DotNETRuntime:MethodLoadVerbose_V1 + DotNETRuntime:MethodLoadVerbose_V2 + DotNETRuntime:MethodUnloadVerbose + DotNETRuntime:MethodUnloadVerbose_V1 + DotNETRuntime:MethodUnloadVerbose_V2 +) + +declare -a DotNETRuntime_JitKeyword=( + DotNETRuntime:MethodJittingStarted + DotNETRuntime:MethodJittingStarted_V1 +) + +declare -a DotNETRuntime_JitTracingKeyword=( + DotNETRuntime:MethodJitInliningSucceeded + DotNETRuntime:MethodJitInliningFailed + DotNETRuntime:MethodJitTailCallSucceeded + DotNETRuntime:MethodJitTailCallFailed +) + +declare -a DotNETRuntime_JittedMethodILToNativeMapKeyword=( + DotNETRuntime:MethodILToNativeMap +) + +declare -a DotNETRuntime_LoaderKeyword=( + DotNETRuntime:ModuleDCStartV2 + DotNETRuntime:ModuleDCEndV2 + DotNETRuntime:DomainModuleLoad + DotNETRuntime:DomainModuleLoad_V1 + DotNETRuntime:ModuleLoad + DotNETRuntime:ModuleUnload + DotNETRuntime:AssemblyLoad + DotNETRuntime:AssemblyLoad_V1 + DotNETRuntime:AssemblyUnload + DotNETRuntime:AssemblyUnload_V1 + DotNETRuntime:AppDomainLoad + DotNETRuntime:AppDomainLoad_V1 + DotNETRuntime:AppDomainUnload + DotNETRuntime:AppDomainUnload_V1 +) + +declare -a DotNETRuntime_LoaderKeyword=( + DotNETRuntime:ModuleLoad_V1 + DotNETRuntime:ModuleLoad_V2 + DotNETRuntime:ModuleUnload_V1 + DotNETRuntime:ModuleUnload_V2 +) + +declare -a DotNETRuntime_SecurityKeyword=( + DotNETRuntime:StrongNameVerificationStart + DotNETRuntime:StrongNameVerificationStart_V1 + DotNETRuntime:StrongNameVerificationStop + DotNETRuntime:StrongNameVerificationStop_V1 + DotNETRuntime:AuthenticodeVerificationStart + DotNETRuntime:AuthenticodeVerificationStart_V1 + DotNETRuntime:AuthenticodeVerificationStop + DotNETRuntime:AuthenticodeVerificationStop_V1 +) + +declare -a DotNETRuntime_DebuggerKeyword=( + DotNETRuntime:DebugIPCEventStart + DotNETRuntime:DebugIPCEventEnd + DotNETRuntime:DebugExceptionProcessingStart + DotNETRuntime:DebugExceptionProcessingEnd +) + +declare -a DotNETRuntime_CodeSymbolsKeyword=( + DotNETRuntime:CodeSymbols +) + +declare -a DotNETRuntime_CompilationKeyword=( + DotNETRuntime:TieredCompilationSettings + DotNETRuntime:TieredCompilationPause + DotNETRuntime:TieredCompilationResume + DotNETRuntime:TieredCompilationBackgroundJitStart + DotNETRuntime:TieredCompilationBackgroundJitStop + DotNETRuntimeRundown:TieredCompilationSettingsDCStart +) + +# Separate GCCollectOnly list because our LTTng implementation doesn't set tracepoint verbosity. +# Once tracepoint verbosity is set, we can set verbosity and collapse this wtih DotNETRuntimePrivate_GCPrivateKeyword. +declare -a DotNETRuntimePrivate_GCPrivateKeyword_GCCollectOnly=( + DotNETRuntimePrivate:GCDecision + DotNETRuntimePrivate:GCDecision_V1 + DotNETRuntimePrivate:GCSettings + DotNETRuntimePrivate:GCSettings_V1 + DotNETRuntimePrivate:GCPerHeapHistory + DotNETRuntimePrivate:GCPerHeapHistory_V1 + DotNETRuntimePrivate:GCGlobalHeapHistory + DotNETRuntimePrivate:GCGlobalHeapHistory_V1 + DotNETRuntimePrivate:PrvGCMarkStackRoots + DotNETRuntimePrivate:PrvGCMarkStackRoots_V1 + DotNETRuntimePrivate:PrvGCMarkFinalizeQueueRoots + DotNETRuntimePrivate:PrvGCMarkFinalizeQueueRoots_V1 + DotNETRuntimePrivate:PrvGCMarkHandles + DotNETRuntimePrivate:PrvGCMarkHandles_V1 + DotNETRuntimePrivate:PrvGCMarkCards + DotNETRuntimePrivate:PrvGCMarkCards_V1 + DotNETRuntimePrivate:BGCBegin + DotNETRuntimePrivate:BGC1stNonConEnd + DotNETRuntimePrivate:BGC1stConEnd + DotNETRuntimePrivate:BGC2ndNonConBegin + DotNETRuntimePrivate:BGC2ndNonConEnd + DotNETRuntimePrivate:BGC2ndConBegin + DotNETRuntimePrivate:BGC2ndConEnd + DotNETRuntimePrivate:BGCPlanEnd + DotNETRuntimePrivate:BGCSweepEnd + DotNETRuntimePrivate:BGCDrainMark + DotNETRuntimePrivate:BGCRevisit + DotNETRuntimePrivate:BGCOverflow + DotNETRuntimePrivate:BGCAllocWaitBegin + DotNETRuntimePrivate:BGCAllocWaitEnd + DotNETRuntimePrivate:GCFullNotify + DotNETRuntimePrivate:GCFullNotify_V1 + DotNETRuntimePrivate:PrvFinalizeObject + DotNETRuntimePrivate:PinPlugAtGCTime +) + +declare -a DotNETRuntimePrivate_GCPrivateKeyword=( + DotNETRuntimePrivate:GCDecision + DotNETRuntimePrivate:GCDecision_V1 + DotNETRuntimePrivate:GCSettings + DotNETRuntimePrivate:GCSettings_V1 + DotNETRuntimePrivate:GCOptimized + DotNETRuntimePrivate:GCOptimized_V1 + DotNETRuntimePrivate:GCPerHeapHistory + DotNETRuntimePrivate:GCPerHeapHistory_V1 + DotNETRuntimePrivate:GCGlobalHeapHistory + DotNETRuntimePrivate:GCGlobalHeapHistory_V1 + DotNETRuntimePrivate:GCJoin + DotNETRuntimePrivate:GCJoin_V1 + DotNETRuntimePrivate:PrvGCMarkStackRoots + DotNETRuntimePrivate:PrvGCMarkStackRoots_V1 + DotNETRuntimePrivate:PrvGCMarkFinalizeQueueRoots + DotNETRuntimePrivate:PrvGCMarkFinalizeQueueRoots_V1 + DotNETRuntimePrivate:PrvGCMarkHandles + DotNETRuntimePrivate:PrvGCMarkHandles_V1 + DotNETRuntimePrivate:PrvGCMarkCards + DotNETRuntimePrivate:PrvGCMarkCards_V1 + DotNETRuntimePrivate:BGCBegin + DotNETRuntimePrivate:BGC1stNonConEnd + DotNETRuntimePrivate:BGC1stConEnd + DotNETRuntimePrivate:BGC2ndNonConBegin + DotNETRuntimePrivate:BGC2ndNonConEnd + DotNETRuntimePrivate:BGC2ndConBegin + DotNETRuntimePrivate:BGC2ndConEnd + DotNETRuntimePrivate:BGCPlanEnd + DotNETRuntimePrivate:BGCSweepEnd + DotNETRuntimePrivate:BGCDrainMark + DotNETRuntimePrivate:BGCRevisit + DotNETRuntimePrivate:BGCOverflow + DotNETRuntimePrivate:BGCAllocWaitBegin + DotNETRuntimePrivate:BGCAllocWaitEnd + DotNETRuntimePrivate:GCFullNotify + DotNETRuntimePrivate:GCFullNotify_V1 + DotNETRuntimePrivate:PrvFinalizeObject + DotNETRuntimePrivate:PinPlugAtGCTime +) + +declare -a DotNETRuntimePrivate_StartupKeyword=( + DotNETRuntimePrivate:EEStartupStart + DotNETRuntimePrivate:EEStartupStart_V1 + DotNETRuntimePrivate:EEStartupEnd + DotNETRuntimePrivate:EEStartupEnd_V1 + DotNETRuntimePrivate:EEConfigSetup + DotNETRuntimePrivate:EEConfigSetup_V1 + DotNETRuntimePrivate:EEConfigSetupEnd + DotNETRuntimePrivate:EEConfigSetupEnd_V1 + DotNETRuntimePrivate:LdSysBases + DotNETRuntimePrivate:LdSysBases_V1 + DotNETRuntimePrivate:LdSysBasesEnd + DotNETRuntimePrivate:LdSysBasesEnd_V1 + DotNETRuntimePrivate:ExecExe + DotNETRuntimePrivate:ExecExe_V1 + DotNETRuntimePrivate:ExecExeEnd + DotNETRuntimePrivate:ExecExeEnd_V1 + DotNETRuntimePrivate:Main + DotNETRuntimePrivate:Main_V1 + DotNETRuntimePrivate:MainEnd + DotNETRuntimePrivate:MainEnd_V1 + DotNETRuntimePrivate:ApplyPolicyStart + DotNETRuntimePrivate:ApplyPolicyStart_V1 + DotNETRuntimePrivate:ApplyPolicyEnd + DotNETRuntimePrivate:ApplyPolicyEnd_V1 + DotNETRuntimePrivate:LdLibShFolder + DotNETRuntimePrivate:LdLibShFolder_V1 + DotNETRuntimePrivate:LdLibShFolderEnd + DotNETRuntimePrivate:LdLibShFolderEnd_V1 + DotNETRuntimePrivate:PrestubWorker + DotNETRuntimePrivate:PrestubWorker_V1 + DotNETRuntimePrivate:PrestubWorkerEnd + DotNETRuntimePrivate:PrestubWorkerEnd_V1 + DotNETRuntimePrivate:GetInstallationStart + DotNETRuntimePrivate:GetInstallationStart_V1 + DotNETRuntimePrivate:GetInstallationEnd + DotNETRuntimePrivate:GetInstallationEnd_V1 + DotNETRuntimePrivate:OpenHModule + DotNETRuntimePrivate:OpenHModule_V1 + DotNETRuntimePrivate:OpenHModuleEnd + DotNETRuntimePrivate:OpenHModuleEnd_V1 + DotNETRuntimePrivate:ExplicitBindStart + DotNETRuntimePrivate:ExplicitBindStart_V1 + DotNETRuntimePrivate:ExplicitBindEnd + DotNETRuntimePrivate:ExplicitBindEnd_V1 + DotNETRuntimePrivate:ParseXml + DotNETRuntimePrivate:ParseXml_V1 + DotNETRuntimePrivate:ParseXmlEnd + DotNETRuntimePrivate:ParseXmlEnd_V1 + DotNETRuntimePrivate:InitDefaultDomain + DotNETRuntimePrivate:InitDefaultDomain_V1 + DotNETRuntimePrivate:InitDefaultDomainEnd + DotNETRuntimePrivate:InitDefaultDomainEnd_V1 + DotNETRuntimePrivate:InitSecurity + DotNETRuntimePrivate:InitSecurity_V1 + DotNETRuntimePrivate:InitSecurityEnd + DotNETRuntimePrivate:InitSecurityEnd_V1 + DotNETRuntimePrivate:AllowBindingRedirs + DotNETRuntimePrivate:AllowBindingRedirs_V1 + DotNETRuntimePrivate:AllowBindingRedirsEnd + DotNETRuntimePrivate:AllowBindingRedirsEnd_V1 + DotNETRuntimePrivate:EEConfigSync + DotNETRuntimePrivate:EEConfigSync_V1 + DotNETRuntimePrivate:EEConfigSyncEnd + DotNETRuntimePrivate:EEConfigSyncEnd_V1 + DotNETRuntimePrivate:FusionBinding + DotNETRuntimePrivate:FusionBinding_V1 + DotNETRuntimePrivate:FusionBindingEnd + DotNETRuntimePrivate:FusionBindingEnd_V1 + DotNETRuntimePrivate:LoaderCatchCall + DotNETRuntimePrivate:LoaderCatchCall_V1 + DotNETRuntimePrivate:LoaderCatchCallEnd + DotNETRuntimePrivate:LoaderCatchCallEnd_V1 + DotNETRuntimePrivate:FusionInit + DotNETRuntimePrivate:FusionInit_V1 + DotNETRuntimePrivate:FusionInitEnd + DotNETRuntimePrivate:FusionInitEnd_V1 + DotNETRuntimePrivate:FusionAppCtx + DotNETRuntimePrivate:FusionAppCtx_V1 + DotNETRuntimePrivate:FusionAppCtxEnd + DotNETRuntimePrivate:FusionAppCtxEnd_V1 + DotNETRuntimePrivate:Fusion2EE + DotNETRuntimePrivate:Fusion2EE_V1 + DotNETRuntimePrivate:Fusion2EEEnd + DotNETRuntimePrivate:Fusion2EEEnd_V1 + DotNETRuntimePrivate:SecurityCatchCall + DotNETRuntimePrivate:SecurityCatchCall_V1 + DotNETRuntimePrivate:SecurityCatchCallEnd + DotNETRuntimePrivate:SecurityCatchCallEnd_V1 +) + +declare -a DotNETRuntimePrivate_StackKeyword=( + DotNETRuntimePrivate:CLRStackWalkPrivate +) + +declare -a DotNETRuntimePrivate_PerfTrackPrivateKeyword=( + DotNETRuntimePrivate:ModuleRangeLoadPrivate +) + +declare -a DotNETRuntimePrivate_BindingKeyword=( + DotNETRuntimePrivate:BindingPolicyPhaseStart + DotNETRuntimePrivate:BindingPolicyPhaseEnd + DotNETRuntimePrivate:BindingNgenPhaseStart + DotNETRuntimePrivate:BindingNgenPhaseEnd + DotNETRuntimePrivate:BindingLookupAndProbingPhaseStart + DotNETRuntimePrivate:BindingLookupAndProbingPhaseEnd + DotNETRuntimePrivate:LoaderPhaseStart + DotNETRuntimePrivate:LoaderPhaseEnd + DotNETRuntimePrivate:BindingPhaseStart + DotNETRuntimePrivate:BindingPhaseEnd + DotNETRuntimePrivate:BindingDownloadPhaseStart + DotNETRuntimePrivate:BindingDownloadPhaseEnd + DotNETRuntimePrivate:LoaderAssemblyInitPhaseStart + DotNETRuntimePrivate:LoaderAssemblyInitPhaseEnd + DotNETRuntimePrivate:LoaderMappingPhaseStart + DotNETRuntimePrivate:LoaderMappingPhaseEnd + DotNETRuntimePrivate:LoaderDeliverEventsPhaseStart + DotNETRuntimePrivate:LoaderDeliverEventsPhaseEnd + DotNETRuntimePrivate:FusionMessageEvent + DotNETRuntimePrivate:FusionErrorCodeEvent +) + +declare -a DotNETRuntimePrivate_SecurityPrivateKeyword=( + DotNETRuntimePrivate:EvidenceGenerated + DotNETRuntimePrivate:ModuleTransparencyComputationStart + DotNETRuntimePrivate:ModuleTransparencyComputationEnd + DotNETRuntimePrivate:TypeTransparencyComputationStart + DotNETRuntimePrivate:TypeTransparencyComputationEnd + DotNETRuntimePrivate:MethodTransparencyComputationStart + DotNETRuntimePrivate:MethodTransparencyComputationEnd + DotNETRuntimePrivate:FieldTransparencyComputationStart + DotNETRuntimePrivate:FieldTransparencyComputationEnd + DotNETRuntimePrivate:TokenTransparencyComputationStart + DotNETRuntimePrivate:TokenTransparencyComputationEnd +) + +declare -a DotNETRuntimePrivate_PrivateFusionKeyword=( + DotNETRuntimePrivate:NgenBindEvent +) + +declare -a DotNETRuntimePrivate_NoKeyword=( + DotNETRuntimePrivate:FailFast +) + +declare -a DotNETRuntimePrivate_InteropPrivateKeyword=( + DotNETRuntimePrivate:CCWRefCountChange +) + +declare -a DotNETRuntimePrivate_GCHandlePrivateKeyword=( + DotNETRuntimePrivate:PrvSetGCHandle + DotNETRuntimePrivate:PrvDestroyGCHandle +) + +declare -a DotNETRuntimePrivate_LoaderHeapPrivateKeyword=( + DotNETRuntimePrivate:AllocRequest +) + +declare -a DotNETRuntimePrivate_MulticoreJitPrivateKeyword=( + DotNETRuntimePrivate:MulticoreJit + DotNETRuntimePrivate:MulticoreJitMethodCodeReturned +) + +declare -a DotNETRuntimePrivate_DynamicTypeUsageKeyword=( + DotNETRuntimePrivate:IInspectableRuntimeClassName + DotNETRuntimePrivate:WinRTUnbox + DotNETRuntimePrivate:CreateRCW + DotNETRuntimePrivate:RCWVariance + DotNETRuntimePrivate:RCWIEnumerableCasting + DotNETRuntimePrivate:CreateCCW + DotNETRuntimePrivate:CCWVariance + DotNETRuntimePrivate:ObjectVariantMarshallingToNative + DotNETRuntimePrivate:GetTypeFromGUID + DotNETRuntimePrivate:GetTypeFromProgID + DotNETRuntimePrivate:ConvertToCallbackEtw + DotNETRuntimePrivate:BeginCreateManagedReference + DotNETRuntimePrivate:EndCreateManagedReference + DotNETRuntimePrivate:ObjectVariantMarshallingToManaged +) + +declare -a LTTng_Kernel_ProcessLifetime=( + sched_process_exec + sched_process_exit +) + +###################################### +## Global Variables +###################################### + +# Declare an array of events to collect. +declare -a eventsToCollect + +# Use Perf_Event +usePerf=1 + +# Use LTTng +useLTTng=1 + +# LTTng Installed +lttngInstalled=0 + +# Collect hardware events +collect_HWevents=0 + +# Set to 1 when the CTRLC_Handler gets invoked. +handlerInvoked=0 + +# Log file +declare logFile +logFilePrefix='/tmp/perfcollect' +logEnabled=1 + +# Collect info to pass between processes +collectInfoFile=$(dirname `mktemp -u`)/'perfcollect.sessioninfo' + +###################################### +## Logging Functions +###################################### +LogAppend() +{ + if (( $logEnabled == 1 )) + then + echo $* >> $logFile + fi +} + +RunSilent() +{ + if (( $logEnabled == 1 )) + then + echo "Running \"$*\"" >> $logFile + $* >> $logFile 2>&1 + echo "" >> $logFile + else + $* > /dev/null 2>&1 + fi +} + +InitializeLog() +{ + # Pick the log file name. + logFile="$logFilePrefix.log" + while [ -f $logFile ]; + do + logFile="$logFilePrefix.$RANDOM.log" + done + + # Start the log + date=`date` + echo "Log started at ${date}" > $logFile + echo '' >> $logFile + + # The system information. + LogAppend 'Machine info: ' `uname -a` + LogAppend 'perf version:' `$perfcmd --version` + LogAppend 'LTTng version: ' `lttng --version` + LogAppend +} + +CloseLog() +{ + LogAppend "END LOG FILE" + LogAppend "NOTE: It is normal for the log file to end right before the trace is actually compressed. This occurs because the log file is part of the archive, and thus can't be written anymore." + + # The log itself doesn't need to be closed, + # but we need to tell the script not to log anymore. + logEnabled=0 +} + + +###################################### +## Helper Functions +###################################### + +## +# Console text color modification helpers. +## +RedText() +{ + tput setaf 1 +} + +GreenText() +{ + tput setaf 2 +} + +BlueText() +{ + tput setaf 6 +} +YellowText() +{ + tput setaf 3 +} + +ResetText() +{ + tput sgr0 +} + +# $1 == Status message +WriteStatus() +{ + LogAppend $* + BlueText + echo $1 + ResetText +} + +# $1 == Message. +WriteWarning() +{ + LogAppend $* + YellowText + echo $1 + ResetText +} + +# $1 == Message. +FatalError() +{ + RedText + echo "ERROR: $1" + ResetText + PrintUsage + exit 1 +} + +EnsureRoot() +{ + # Warn non-root users. + if [ `whoami` != "root" ] + then + RedText + echo "This script must be run as root." + ResetText + exit 1; + fi +} + +###################################### +# Command Discovery +###################################### +DiscoverCommands() +{ + perfcmd=`GetCommandFullPath "perf"` + if [ "$(IsDebian)" == "1" ] + then + # Test perf to see if it successfully runs or fails because it doesn't match the kernel version. + $perfcmd --version > /dev/null 2>&1 + if [ "$?" == "1" ] + then + perf49Cmd=`GetCommandFullPath "perf_4.9"` + $perf49Cmd --version > /dev/null 2>&1 + if [ "$?" == "0" ] + then + perfcmd=$perf49Cmd + else + perf419Cmd=`GetCommandFullPath "perf_4.19"` + $perf419Cmd --version > /dev/null 2>&1 + if [ "$?" == "0" ] + then + perfcmd=$perf419Cmd + else + perf316Cmd=`GetCommandFullPath "perf_3.16"` + $perf316Cmd --version > /dev/null 2>&1 + if [ "$?" == "0" ] + then + perfcmd=$perf316Cmd + fi + fi + fi + fi + fi + lttngcmd=`GetCommandFullPath "lttng"` + zipcmd=`GetCommandFullPath "zip"` + unzipcmd=`GetCommandFullPath "unzip"` +} + +GetCommandFullPath() +{ + echo `command -v $1` +} + +###################################### +# Prerequisite Installation +###################################### +IsRHEL() +{ + local rhel=0 + if [ -f /etc/redhat-release ] + then + rhel=1 + fi + + echo $rhel +} + +InstallPerf_RHEL() +{ + # Disallow non-root users. + EnsureRoot + + # Install perf + yum install perf zip unzip +} + +IsDebian() +{ + local debian=0 + local uname=`uname -a` + if [[ $uname =~ .*Debian.* ]] + then + debian=1 + elif [ -f /etc/debian_version ] + then + debian=1 + fi + + echo $debian +} + +InstallPerf_Debian() +{ + # Disallow non-root users. + EnsureRoot + + # Check for the existence of the linux-tools package. + pkgName='linux-tools' + pkgCount=`apt-cache search $pkgName | grep -c $pkgName` + if [ "$pkgCount" == "0" ] + then + pkgName='linux-perf' + pkgCount=`apt-cache search $pkgName | grep -c $pkgName` + if [ "$pkgCount" == "0" ] + then + FatalError "Unable to find a perf package to install." + fi + fi + + # Install zip and perf. + apt-get install -y zip binutils $pkgName +} + +IsSUSE() +{ + local suse=0 + if [ -f /usr/bin/zypper ] + then + suse=1 + fi + + echo $suse +} + +InstallPerf_SUSE() +{ + # Disallow non-root users. + EnsureRoot + + # Install perf. + zypper install perf zip unzip +} + +IsUbuntu() +{ + local ubuntu=0 + if [ -f /etc/lsb-release ] + then + local flavor=`cat /etc/lsb-release | grep DISTRIB_ID` + if [ "$flavor" == "DISTRIB_ID=Ubuntu" ] + then + ubuntu=1 + fi + fi + + echo $ubuntu +} + +InstallPerf_Ubuntu() +{ + # Disallow non-root users. + EnsureRoot + + # Install packages. + BlueText + echo "Installing perf_event packages." + ResetText + apt-get install -y linux-tools-common linux-tools-`uname -r` linux-cloud-tools-`uname -r` zip software-properties-common +} + +InstallPerf() +{ + if [ "$(IsUbuntu)" == "1" ] + then + InstallPerf_Ubuntu + elif [ "$(IsSUSE)" == "1" ] + then + InstallPerf_SUSE + elif [ "$(IsDebian)" == "1" ] + then + InstallPerf_Debian + elif [ "$(IsRHEL)" == "1" ] + then + InstallPerf_RHEL + else + FatalError "Auto install unsupported for this distribution. Install perf manually to continue." + fi +} + +InstallLTTng_RHEL() +{ + # Disallow non-root users. + EnsureRoot + + packageRepo="https://packages.efficios.com/repo.files/EfficiOS-RHEL7-x86-64.repo" + + # Prompt for confirmation, since we need to add a new repository. + BlueText + echo "LTTng installation requires that a new package repo be added to your yum configuration." + echo "The package repo url is: $packageRepo" + echo "" + read -p "Would you like to add the LTTng package repo to your YUM configuration? [Y/N]" resp + ResetText + if [ "$resp" == "Y" ] || [ "$resp" == "y" ] + then + # Make sure that wget is installed. + BlueText + echo "Installing wget. Required to add package repo." + ResetText + yum install wget + + # Connect to the LTTng package repo. + wget -P /etc/yum.repos.d/ $packageRepo + + # Import package signing key. + rpmkeys --import https://packages.efficios.com/rhel/repo.key + + # Update the yum package database. + yum updateinfo + + # Install LTTng + yum install lttng-tools lttng-ust kmod-lttng-modules babeltrace + fi +} + +InstallLTTng_Debian() +{ + # Disallow non-root users. + EnsureRoot + + # Install LTTng + apt-get install -y lttng-tools liblttng-ust-dev +} + +InstallLTTng_SUSE() +{ + # Disallow non-root users. + EnsureRoot + + # Package repo url + packageRepo="http://download.opensuse.org/repositories/devel:/tools:/lttng/openSUSE_13.2/devel:tools:lttng.repo" + + # Prompt for confirmation, since we need to add a new repository. + BlueText + echo "LTTng installation requires that a new package repo be added to your zypper configuration." + echo "The package repo url is: $packageRepo" + echo "" + read -p "Would you like to add the LTTng package repo to your zypper configuration? [Y/N]" resp + ResetText + if [ "$resp" == "Y" ] || [ "$resp" == "y" ] + then + + # Add package repo. + BlueText + echo "Adding LTTng repo and running zypper refresh." + ResetText + zypper addrepo $packageRepo + zypper refresh + + # Install packages. + BlueText + echo "Installing LTTng packages." + ResetText + zypper install lttng-tools lttng-modules lttng-ust-devel + fi +} + +InstallLTTng_Ubuntu() +{ + # Disallow non-root users. + EnsureRoot + + # Add the PPA feed as a repository. + BlueText + echo "LTTng can be installed using default Ubuntu packages via the Ubuntu package feeds or using the latest" + echo "stable package feed published by LTTng (PPA feed). It is recommended that LTTng be installed using the PPA feed." + echo "" + ResetText + echo " If you select yes, then the LTTng PPA feed will be added to your apt configuration." + echo " If you select no, then LTTng will be installed from an existing feed (either default Ubuntu or PPA if previously added." + echo "" + BlueText + read -p "Would you like to add the LTTng PPA feed to your apt configuration? [Y/N]" resp + ResetText + if [ "$resp" == "Y" ] || [ "$resp" == "y" ] + then + BlueText + echo "Adding LTTng PPA feed and running apt-get update." + ResetText + apt-add-repository ppa:lttng/ppa + apt-get update + fi + + # Install packages. + BlueText + echo "Installing LTTng packages." + ResetText + apt-get install -y lttng-tools lttng-modules-dkms liblttng-ust0 +} + +InstallLTTng() +{ + if [ "$(IsUbuntu)" == "1" ] + then + InstallLTTng_Ubuntu + elif [ "$(IsSUSE)" == "1" ] + then + InstallLTTng_SUSE + elif [ "$(IsDebian)" == "1" ] + then + InstallLTTng_Debian + elif [ "$(IsRHEL)" == "1" ] + then + InstallLTTng_RHEL + else + FatalError "Auto install unsupported for this distribution. Install lttng and lttng-ust packages manually." + fi +} + +SupportsAutoInstall() +{ + local supportsAutoInstall=0 + if [ "$(IsUbuntu)" == "1" ] || [ "$(IsSUSE)" == "1" ] + then + supportsAutoInstall=1 + fi + + echo $supportsAutoInstall +} + +EnsurePrereqsInstalled() +{ + # Discover commands and then determine if they're all present. + DiscoverCommands + + # If perf is not installed, then bail, as it is currently required. + if [ "$perfcmd" == "" ] + then + RedText + echo "Perf not installed." + if [ "$(SupportsAutoInstall)" == "1" ] + then + echo "Run ./perfcollect install" + echo "or install perf manually." + else + echo "Install perf to proceed." + fi + ResetText + exit 1 + fi + + # If LTTng is installed, consider using it. + if [ "$lttngcmd" == "" ] && [ "$useLTTng" == "1" ] + then + RedText + echo "LTTng not installed." + if [ "$(SupportsAutoInstall)" == "1" ] + then + echo "Run ./perfcollect install" + echo "or install LTTng manually." + else + echo "Install LTTng to proceed." + fi + ResetText + exit 1 + + fi + + # If zip or unzip are not installing, then bail. + if [ "$zipcmd" == "" ] || [ "$unzipcmd" == "" ] + then + RedText + echo "Zip and unzip are not installed." + if [ "$(SupportsAutoInstall)" == "1" ] + then + echo "Run ./perfcollect install" + echo "or install zip and unzip manually." + else + echo "Install zip and unzip to proceed." + fi + ResetText + exit 1 + fi +} + +###################################### +# Argument Processing +###################################### +action='' +inputTraceName='' +collectionPid='' +processFilter='' +graphType='' +perfOpt='' +viewer='perf' +gcCollectOnly='' +gcOnly='' +gcWithHeap='' +events='' + +ProcessArguments() +{ + # No arguments + if [ "$#" == "0" ] + then + PrintUsage + exit 0 + fi + + # Set the action + action=$1 + + # Actions with no arguments. + if [ "$action" == "livetrace" ] + then + return + fi + + # Not enough arguments. + if [ "$#" -le "1" ] + then + FatalError "Not enough arguments have been specified." + fi + + # Validate action name. + if [ "$action" != "collect" ] && [ "$action" != "view" ] \ + && [ "$action" != "start" ] && [ "$action" != "stop" ] + then + FatalError "Invalid action specified." + fi + + # Set the data file. + inputTraceName=$2 + if [ "$inputTraceName" == "" ] + then + FatalError "Invalid trace name specified." + fi + + # Process remaining arguments. + # First copy the args into an array so that we can walk the array. + args=( "$@" ) + for (( i=2; i<${#args[@]}; i++ )) + do + # Get the arg. + local arg=${args[$i]} + + # Convert the arg to lower case. + arg=`echo $arg | tr '[:upper:]' '[:lower:]'` + + # Get the arg value. + if [ ${i+1} -lt $# ] + then + local value=${args[$i+1]} + + # Convert the value to lower case. + value=`echo $value | tr '[:upper:]' '[:lower:]'` + fi + + # Match the arg to a known value. + if [ "-pid" == "$arg" ] + then + collectionPid=$value + i=$i+1 + elif [ "-processfilter" == "$arg" ] + then + processFilter=$value + i=$i+1 + elif [ "-graphtype" == "$arg" ] + then + graphType=$value + i=$i+1 + elif [ "-threadtime" == "$arg" ] + then + collect_threadTime=1 + elif [ "-hwevents" == "$arg" ] + then + collect_HWevents=1 + elif [ "-perfopt" == "$arg" ] + then + perfOpt=$value + i=$i+1 + elif [ "-viewer" == "$arg" ] + then + viewer=$value + i=$i+1 + + # Validate the viewer. + if [ "$viewer" != "perf" ] && [ "$viewer" != "lttng" ] + then + FatalError "Invalid viewer specified. Valid values are 'perf' and 'lttng'." + fi + elif [ "-nolttng" == "$arg" ] + then + useLTTng=0 + elif [ "-noperf" == "$arg" ] + then + usePerf=0 + elif [ "-gccollectonly" == "$arg" ] + then + gcCollectOnly=1 + elif [ "-gconly" == "$arg" ] + then + gcOnly=1 + elif [ "-gcwithheap" == "$arg" ] + then + gcWithHeap=1 + elif [ "-events" == "$arg" ] + then + events=$value + i=$i+1 + elif [ "-collectsec" == "$arg" ] + then + duration=$value + i=$i+1 + else + echo "Unknown arg ${arg}, ignored..." + fi + done + +} + + + +## +# LTTng collection +## +lttngSessionName='' +lttngTraceDir='' +CreateLTTngSession() +{ + if [ "$action" == "livetrace" ] + then + output=`lttng create --live` + else + output=`lttng create` + fi + + lttngSessionName=`echo $output | grep -o "Session.*created." | sed 's/\(Session \| created.\)//g'` + lttngTraceDir=`echo $output | grep -o "Traces.*" | sed 's/\(Traces will be written in \|\)//g'` +} + +SetupLTTngSession() +{ + + # Setup per-event context information. + RunSilent "lttng add-context --userspace --type vpid" + RunSilent "lttng add-context --userspace --type vtid" + RunSilent "lttng add-context --userspace --type procname" + RunSilent "lttng add-context --kernel -t pid -t procname" + + if [ "$action" == "livetrace" ] + then + RunSilent "lttng enable-event --userspace --tracepoint DotNETRuntime:EventSource" + elif [ "$gcCollectOnly" == "1" ] + then + usePerf=0 + EnableLTTngEvents ${DotNETRuntime_GCKeyword_GCCollectOnly[@]} + EnableLTTngEvents ${DotNETRuntimePrivate_GCPrivateKeyword_GCCollectOnly[@]} + EnableLTTngEvents ${DotNETRuntime_ExceptionKeyword[@]} + elif [ "$gcOnly" == "1" ] + then + usePerf=0 + EnableLTTngEvents ${DotNETRuntime_GCKeyword[@]} + EnableLTTngEvents ${DotNETRuntimePrivate_GCPrivateKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_JitKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_LoaderKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_ExceptionKeyword[@]} + elif [ "$gcWithHeap" == "1" ] + then + usePerf=0 + EnableLTTngEvents ${DotNETRuntime_GCKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_GCHeapSurvivalAndMovementKeyword[@]} + else + if [ "$events" == "" ] + then + # Enable the default set of events. + EnableLTTngEvents ${DotNETRuntime_ThreadingKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_ThreadingKeyword_ThreadTransferKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_NoKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_ExceptionKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_ContentionKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_JitKeyword_NGenKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_JitKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_LoaderKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_GCKeyword_GCCollectOnly[@]} + EnableLTTngEvents ${DotNETRuntimePrivate_GCPrivateKeyword_GCCollectOnly[@]} + EnableLTTngEvents ${DotNETRuntimePrivate_BindingKeyword[@]} + EnableLTTngEvents ${DotNETRuntimePrivate_MulticoreJitPrivateKeyword[@]} + EnableLTTngEvents ${DotNETRuntime_CompilationKeyword[@]} + elif [ "$events" == "threading" ] + then + EnableLTTngEvents ${DotNETRuntime_ThreadingKeyword[@]} + elif [ "$events" == "processlifetime" ] + then + # Setup kernel events. + # TODO: enable triggers of different kernel events, instead of a default set of kernel events + EnableLTTngKernelEvents ${LTTng_Kernel_ProcessLifetime[@]} + fi + fi +} + +DestroyLTTngSession() +{ + RunSilent "lttng destroy $lttngSessionName" +} + +StartLTTngCollection() +{ + CreateLTTngSession + SetupLTTngSession + + RunSilent "lttng start $lttngSessionName" +} + +StopLTTngCollection() +{ + RunSilent "lttng stop $lttngSessionName" + DestroyLTTngSession +} + +# $@ == event names to be enabled +EnableLTTngEvents() +{ + args=( "$@" ) + for (( i=0; i<${#args[@]}; i++ )) + do + RunSilent "lttng enable-event -s $lttngSessionName -u --tracepoint ${args[$i]}" + done +} + +EnableLTTngKernelEvents() +{ + args=( "$@" ) + for (( i=0; i<${#args[@]}; i++ )) + do + RunSilent "lttng enable-event -s $lttngSessionName -k ${args[$i]}" + done +} + +## +# Helper that processes collected data. +# This helper is called when the CTRL+C signal is handled. +## +ProcessCollectedData() +{ + # Make a new target directory. + local traceSuffix=".trace" + local traceName=$inputTraceName + local directoryName=$traceName$traceSuffix + mkdir $directoryName + + # Save LTTng trace files. + if [ "$useLTTng" == "1" ] + then + LogAppend "Saving LTTng trace files." + + if [ -d $lttngTraceDir ] + then + RunSilent "mkdir lttngTrace" + RunSilent "cp -r $lttngTraceDir lttngTrace" + fi + fi + + if [ "$usePerf" == 1 ] + then + # Get any perf-$pid.map files that were used by the + # trace and store them alongside the trace. + LogAppend "Saving perf.map files." + RunSilent "$perfcmd buildid-list --with-hits" + local mapFiles=`$perfcmd buildid-list --with-hits | grep /tmp/perf- | cut -d ' ' -f 2` + for mapFile in $mapFiles + do + if [ -f $mapFile ] + then + LogAppend "Saving $mapFile" + + # Change permissions on the file before saving, as perf will need to access the file later + # in this script when running perf script. + RunSilent "chown root $mapFile" + RunSilent "cp $mapFile ." + + local perfinfoFile=${mapFile/perf/perfinfo} + + LogAppend "Attempting to find ${perfinfoFile}" + + if [ -f $perfinfoFile ] + then + LogAppend "Saving $perfinfoFile" + RunSilent "chown root $perfinfoFile" + RunSilent "cp $perfinfoFile ." + else + LogAppend "Skipping ${perfinfoFile}." + fi + else + LogAppend "Skipping $mapFile. Some managed symbols may not be resolvable, but trace is still valid." + fi + done + + WriteStatus "Generating native image symbol files" + + # Get the list of loaded images and use the path to libcoreclr.so to find crossgen. + # crossgen is expected to sit next to libcoreclr.so. + local buildidList=`$perfcmd buildid-list | grep libcoreclr.so | cut -d ' ' -f 2` + local crossgenCmd='' + local crossgenDir='' + for file in $buildidList + do + crossgenDir=`dirname "${file}"` + if [ -f ${crossgenDir}/crossgen ] + then + crossgenCmd=${crossgenDir}/crossgen + LogAppend "Found crossgen at ${crossgenCmd}" + break + fi + done + + OLDIFS=$IFS + + imagePaths="" + + if [ "$crossgenCmd" != "" ] + then + local perfinfos=`ls . | grep perfinfo | cut -d ' ' -f 2` + for perfinfo in $perfinfos + do + if [ -f $perfinfo ] + then + IFS=";" + while read command dll guid; do + if [ $command ]; then + if [ $command = "ImageLoad" ]; then + if [ -f $dll ]; then + imagePaths="${dll}:${imagePaths}" + fi + fi + fi + done < $perfinfo + IFS=$OLDIFS + fi + done + + IFS=":" + LogAppend "Generating PerfMaps for native images" + for path in $imagePaths + do + if [ `echo ${path} | grep ^.*\.dll$` ] + then + IFS="" + LogAppend "Generating PerfMap for ${path}" + LogAppend "Running ${crossgenCmd} /r $imagePaths /CreatePerfMap . ${path}" + ${crossgenCmd} /r $imagePaths /CreatePerfMap . ${path} >> $logFile 2>&1 + IFS=":" + else + LogAppend "Skipping ${path}" + fi + done + + WriteStatus "...FINISHED" + + else + if [ "$buildidList" != "" ] + then + LogAppend "crossgen not found, skipping native image map generation." + WriteStatus "...SKIPPED" + WriteWarning "Crossgen not found. Framework symbols will be unavailable." + WriteWarning "See https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#resolving-framework-symbols for details." + else + WriteWarning "libcoreclr.so not found in perf data. Please verify that your .NET Core process is running and consuming CPU." + fi + fi + + IFS=$OLDIFS + + # Create debuginfo files (separate symbols) for all modules in the trace. + WriteStatus "Saving native symbols" + + # Get the list of DSOs with hits in the trace file (those that are actually used). + # Filter out /tmp/perf-$pid.map files and files that end in .dll. + local dsosWithHits=`$perfcmd buildid-list --with-hits | grep -v /tmp/perf- | grep -v .dll$` + for dso in $dsosWithHits + do + # Build up tuples of buildid and binary path. + local processEntry=0 + if [ -f $dso ] + then + local pathToBinary=$dso + processEntry=1 + else + local buildid=$dso + pathToBinary='' + fi + + # Once we have a tuple for a binary path that exists, process it. + if [ "$processEntry" == "1" ] + then + # Get the binary name without path. + local binaryName=`basename $pathToBinary` + + # Build the debuginfo file name. + local destFileName=$binaryName.debuginfo + + # Build the destination directory for the debuginfo file. + local currentDir=`pwd` + local destDir=$currentDir/debuginfo/$buildid + + # Build the full path to the debuginfo file. + local destPath=$destDir/$destFileName + + # Check to see if the DSO contains symbols, and if so, build the debuginfo file. + local noSymbols=`objdump -t $pathToBinary | grep "no symbols" -c` + if [ "$noSymbols" == "0" ] + then + LogAppend "Generating debuginfo for $binaryName with buildid=$buildid" + RunSilent "mkdir -p $destDir" + RunSilent "objcopy --only-keep-debug $pathToBinary $destPath" + else + LogAppend "Skipping $binaryName with buildid=$buildid. No symbol information." + fi + fi + done + + WriteStatus "...FINISHED" + + WriteStatus "Exporting perf.data file" + + # Merge sched_stat and sched_switch events. + outputDumpFile="perf.data.txt" + mergedFile="perf.data.merged" + RunSilent "$perfcmd inject -v -s -i perf.data -o $mergedFile" + + # I've not found a good way to get the behavior that we want here - running the command and redirecting the output + # when passing the command line to a function. Thus, this case is hardcoded. + + # There is a breaking change where the capitalization of the -f parameter changed. + LogAppend "Running $perfcmd script -i $mergedFile -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile" + $perfcmd script -i $mergedFile -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile + LogAppend + + if [ $? -ne 0 ] + then + LogAppend "Running $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile" + $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile + LogAppend + fi + + # If the dump file is zero length, try to collect without the period field, which was added recently. + if [ ! -s $outputDumpFile ] + then + LogAppend "Running $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace > $outputDumpFile" + $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile + LogAppend + fi + + WriteStatus "...FINISHED" + fi + + WriteStatus "Compressing trace files" + + # Move all collected files to the new directory. + RunSilent "mv -f * $directoryName" + + # Close the log - this stops all writing to the log, so that we can move it into the archive. + CloseLog + + # Move the log file to the new directory and rename it to the standard log name. + RunSilent "mv $logFile $directoryName/perfcollect.log" + + # Compress the data. + local archiveSuffix=".zip" + local archiveName=$directoryName$archiveSuffix + RunSilent "$zipcmd -r $archiveName $directoryName" + + # Move back to the original directory. + popd > /dev/null + + # Move the archive. + RunSilent "mv $tempDir/$archiveName ." + + WriteStatus "...FINISHED" + + WriteStatus "Cleaning up artifacts" + + # Delete the temp directory. + RunSilent "rm -rf $tempDir $collectInfoFile" + + WriteStatus "...FINISHED" + + # Tell the user where the trace is. + WriteStatus + WriteStatus "Trace saved to $archiveName" +} + +## +# Handle the CTRL+C signal. +## +CTRLC_Handler() +{ + # Mark the handler invoked. + handlerInvoked=1 +} + +EndCollect() +{ + # The user must either use "collect stop" or CTRL+C to stop collection. + if [ "$action" == "stop" ] + then + # Recover trace info in the previous program. + source $collectInfoFile # TODO: exit and dispose upon missing file + # New program started, so go back to the temp directory. + pushd $tempDir > /dev/null + fi + + if [ "$useLTTng" == "1" ] + then + StopLTTngCollection + fi + + # Update the user. + WriteStatus + WriteStatus "...STOPPED." + WriteStatus + WriteStatus "Starting post-processing. This may take some time." + WriteStatus + + # The user used CTRL+C to stop collection. + # When this happens, we catch the signal and finish our work. + ProcessCollectedData +} + +## +# Print usage information. +## +PrintUsage() +{ + echo "This script uses perf_event and LTTng to collect and view performance traces for .NET applications." + echo "For detailed collection and viewing steps, view this script in a text editor or viewer." + echo "" + echo "./perfcollect " + echo "Valid Actions: collect start/stop view livetrace install" + echo "" + echo "collect options:" + echo "By default, collection includes CPU samples collected every ms." + echo " -pid : Only collect data from the specified process id." + echo " -threadtime : Collect context switch events." + echo " -hwevents : Collect (some) hardware counters." + echo "" + echo "start:" + echo " Start collection, but with Lttng trace ONLY. It needs to be used with 'stop' action." + echo "" + echo "stop:" + echo " Stop collection if 'start' action is used." + echo "" + echo "view options:" + echo " -processfilter : Filter data by the specified process name." + echo " -graphtype : Specify the type of graph. Valid values are 'caller' and 'callee'. Default is 'callee'." + echo " -viewer : Specify the data viewer. Valid values are 'perf' and 'lttng'. Default is 'perf'." + echo "" + echo "livetrace:" + echo " Print EventSource events directly to the console. Root privileges not required." + echo "" + echo "install:" + echo " Useful for first-time setup. Installs/upgrades perf_event and LTTng." + echo "" +} + +## +# Validate and set arguments. +## + +BuildPerfRecordArgs() +{ + # Start with default collection arguments that record all CPUs (-a) and collect call stacks (-g) + collectionArgs="record -g" + + # Filter to a single process if desired + if [ "$collectionPid" != "" ] + then + collectionArgs="$collectionArgs --pid=$collectionPid" + else + collectionArgs="$collectionArgs -a" + fi + + # Enable CPU Collection + if [ $collect_cpu -eq 1 ] + then + collectionArgs="$collectionArgs -F 999" + eventsToCollect=( "${eventsToCollect[@]}" "cpu-clock" ) + fi + + # Enable HW counters event collection + if [ $collect_HWevents -eq 1 ] + then + collectionArgs="$collectionArgs -e cycles,instructions,branches,cache-misses" + fi + + # Enable context switches. + if [ $collect_threadTime -eq 1 ] + then + eventsToCollect=( "${eventsToCollect[@]}" "sched:sched_stat_sleep" "sched:sched_switch" "sched:sched_process_exit" ) + fi + + # Build up the set of events. + local eventString="" + local comma="," + for (( i=0; i<${#eventsToCollect[@]}; i++ )) + do + # Get the arg. + eventName=${eventsToCollect[$i]} + + # Build up the comma separated list. + if [ "$eventString" == "" ] + then + eventString=$eventName + else + eventString="$eventString$comma$eventName" + fi + + done + + if [ ! -z ${duration} ] + then + durationString="sleep ${duration}" + fi + + # Add the events onto the collection command line args. + collectionArgs="$collectionArgs -e $eventString $durationString" +} + +DoCollect() +{ + # Ensure the script is run as root. + EnsureRoot + + # Build collection args. + # Places the resulting args in $collectionArgs + BuildPerfRecordArgs + + # Trap CTRL+C + trap CTRLC_Handler SIGINT + + # Create a temp directory to use for collection. + local tempDir=`mktemp -d` + LogAppend "Created temp directory $tempDir" + + # Switch to the directory. + pushd $tempDir > /dev/null + + # Start LTTng collection. + if [ "$useLTTng" == "1" ] + then + StartLTTngCollection + fi + + # Tell the user that collection has started and how to exit. + if [ "$duration" != "" ] + then + WriteStatus "Collection started. Collection will automatically stop in $duration second(s). Press CTRL+C to stop early." + elif [ "$action" == "start" ] + then + WriteStatus "Collection started." + else + WriteStatus "Collection started. Press CTRL+C to stop." + fi + + # Start perf record. + if [ "$action" == "start" ] + then + # Skip perf, which can only be stopped by CTRL+C. + # Pass trace directory and session name to the stop process. + popd > /dev/null + usePerf=0 + declare -p | grep -e lttngTraceDir -e lttngSessionName -e tempDir -e usePerf -e useLttng -e logFile > $collectInfoFile + else + if [ "$usePerf" == "1" ] + then + RunSilent $perfcmd $collectionArgs + else + # Wait here until CTRL+C handler gets called when user types CTRL+C. + LogAppend "Waiting for CTRL+C handler to get called." + + waitTime=0 + for (( ; ; )) + do + if [ "$handlerInvoked" == "1" ] + then + break; + fi + + # Wait and then check to see if the handler has been invoked or we've crossed the duration threshold. + sleep 1 + waitTime=$waitTime+1 + if (( duration > 0 && duration <= waitTime )) + then + break; + fi + done + fi + # End collection if action is 'collect'. + EndCollect + fi +} + +DoLiveTrace() +{ + # Start the session + StartLTTngCollection + + # View the event stream (until the user hits CTRL+C) + WriteStatus "Listening for events from LTTng. Hit CTRL+C to stop." + $lttngcmd view + + # Stop the LTTng sessoin + StopLTTngCollection +} + +# $1 == Path to directory containing trace files +PropSymbolsAndMapFilesForView() +{ + # Get the current directory + local currentDir=`pwd` + + # Copy map files to /tmp since they aren't supported by perf buildid-cache. + local mapFiles=`find -name *.map` + for mapFile in $mapFiles + do + echo "Copying $mapFile to /tmp." + cp $mapFile /tmp + done + + # Cache all debuginfo files saved with the trace in the buildid cache. + local debugInfoFiles=`find $currentDir -name *.debuginfo` + for debugInfoFile in $debugInfoFiles + do + echo "Caching $debugInfoFile in buildid cache using perf buildid-cache." + $perfcmd buildid-cache --add=$debugInfoFile + done +} + +DoView() +{ + # Generate a temp directory to extract the trace files into. + local tempDir=`mktemp -d` + + # Extract the trace files. + $unzipcmd $inputTraceName -d $tempDir + + # Move the to temp directory. + pushd $tempDir + cd `ls` + + # Select the viewer. + if [ "$viewer" == "perf" ] + then + # Prop symbols and map files. + PropSymbolsAndMapFilesForView `pwd` + + # Choose the view + if [ "$graphType" == "" ] + then + graphType="callee" + elif [ "$graphType" != "callee" ] && [ "$graphType" != "caller"] + then + FatalError "Invalid graph type specified. Valid values are 'callee' and 'caller'." + fi + + # Filter to specific process names if desired. + if [ "$processFilter" != "" ] + then + processFilter="--comms=$processFilter" + fi + + # Execute the viewer. + $perfcmd report -n -g graph,0.5,$graphType $processFilter $perfOpt + elif [ "$viewer" == "lttng" ] + then + babeltrace lttngTrace/ | more + fi + + # Switch back to the original directory. + popd + + # Delete the temp directory. + rm -rf $tempDir +} + +##################################### +## Main Script Start +##################################### + +# Install perf if requested. Do this before all other validation. +if [ "$1" == "install" ] +then + InstallPerf + InstallLTTng + exit 0 +fi + +# Ensure prerequisites are installed. +EnsurePrereqsInstalled + +# Initialize the log. +if [ "$1" != "stop" ] +then + InitializeLog +fi + +# Process arguments. +ProcessArguments $@ + +# Take the appropriate action. +if [ "$action" == "collect" ] || [ "$action" == "start" ] +then + DoCollect +elif [ "$action" == "stop" ] +then + EndCollect +elif [ "$action" == "view" ] +then + DoView +elif [ "$action" == "livetrace" ] +then + DoLiveTrace +fi From 93330c2d08982eccdd4175c538eb5b3464ca987f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 3 Mar 2020 15:32:44 +0100 Subject: [PATCH 09/24] update doc link --- src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index c62c3fc731..782caa1c9f 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -63,7 +63,7 @@ public IEnumerable Validate(ValidationParameters validationPara if (validationParameters.Benchmarks.Any() && !TryInstallPerfCollect(validationParameters)) { - yield return new ValidationError(true, "Failed to install perfcollect script. Please follow the instructions from https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#preparing-your-machine"); + yield return new ValidationError(true, "Failed to install perfcollect script. Please follow the instructions from https://github.com/dotnet/runtime/blob/master/docs/project/linux-performance-tracing.md"); } } From bd7ceb0a67afc8ad2b679024aaecd5dd197c66d2 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 3 Mar 2020 15:50:08 +0100 Subject: [PATCH 10/24] use the new start and stop commands --- .../Diagnosers/LttngProfiler.cs | 94 +++++-------------- 1 file changed, 21 insertions(+), 73 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index 782caa1c9f..36fa7fa156 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Threading; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; @@ -29,9 +28,6 @@ public class LttngProfiler : IProfiler private readonly DateTime creationTime = DateTime.Now; private readonly Dictionary benchmarkToTraceFile = new Dictionary(); - private Process perfCollectProcess; - private ManualResetEventSlim signal = new ManualResetEventSlim(); - [PublicAPI] public LttngProfiler(LttngProfilerConfig config) => this.config = config; @@ -115,7 +111,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) } logger.WriteLineError("Failed to install perfcollect"); - foreach(var outputLine in output) + foreach (var outputLine in output) { logger.WriteLine(outputLine); } @@ -133,38 +129,17 @@ private void Start(DiagnoserActionParameters parameters) { var perfCollectFile = new FileInfo(Directory.GetFiles(parameters.Config.ArtifactsPath, PerfCollectFileName).Single()); - perfCollectProcess = CreatePerfCollectProcess(parameters, perfCollectFile); - - var logger = parameters.Config.GetCompositeLogger(); - - perfCollectProcess.OutputDataReceived += OnOutputDataReceived; - - signal.Reset(); - - perfCollectProcess.Start(); - perfCollectProcess.BeginOutputReadLine(); - - WaitForSignal(logger, "// Collection with perfcollect started"); // wait until the script starts the actual collection + ExecutePerfCollectCommand(parameters, perfCollectFile, "start"); } private void Stop(DiagnoserActionParameters parameters) { - if (perfCollectProcess == null) - { - return; - } - - var logger = parameters.Config.GetCompositeLogger(); - - if (WaitForSignal(logger, "// Collection with perfcollect stopped")) - { - benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetFilePath(parameters, creationTime, ".trace.zip")); + var perfCollectFile = new FileInfo(Directory.GetFiles(parameters.Config.ArtifactsPath, PerfCollectFileName).Single()); - CleanupPerfCollectProcess(logger); - } + ExecutePerfCollectCommand(parameters, perfCollectFile, "stop"); } - private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, FileInfo perfCollectFile) + private void ExecutePerfCollectCommand(DiagnoserActionParameters parameters, FileInfo perfCollectFile, string command) { var traceName = new FileInfo(ArtifactFileNameHelper.GetFilePath(parameters, creationTime, fileExtension: null)).Name; // todo: escape characters bash does not like ' ', '(' etc @@ -172,63 +147,36 @@ private Process CreatePerfCollectProcess(DiagnoserActionParameters parameters, F var start = new ProcessStartInfo { FileName = perfCollectFile.FullName, - Arguments = $"collect {traceName} -pid {parameters.Process.Id}", + Arguments = $"{command} {traceName} -pid {parameters.Process.Id}", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, WorkingDirectory = perfCollectFile.Directory.FullName }; - return new Process { StartInfo = start }; - } - - private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) - { - if (e.Data?.IndexOf("Collection started", StringComparison.OrdinalIgnoreCase) >= 0) - signal.Set(); - else if (e.Data?.IndexOf("Trace saved", StringComparison.OrdinalIgnoreCase) >= 0) - signal.Set(); - else if (e.Data?.IndexOf("This script must be run as root", StringComparison.OrdinalIgnoreCase) >= 0) - Environment.FailFast("To use LttngProfiler you must run as root."); // should never happen, ensured by Validate() - } - - private bool WaitForSignal(ILogger logger, string message) - { - if (signal.Wait(config.Timeout)) + using (var perfCollectProcess = new Process { StartInfo = start }) { - signal.Reset(); - - logger.WriteLineInfo(message); - - return true; - } - - logger.WriteLineError($"The perfcollect script did not start/finish in {config.Timeout.TotalSeconds}s."); - logger.WriteLineInfo("You can create LttngProfiler providing LttngProfilerConfig with custom timeout value."); - - CleanupPerfCollectProcess(logger); + perfCollectProcess.Start(); - return false; - } + if (!perfCollectProcess.WaitForExit((int)config.Timeout.TotalMilliseconds)) + { + var logger = parameters.Config.GetCompositeLogger(); - private void CleanupPerfCollectProcess(ILogger logger) - { - logger.Flush(); // flush recently logged message to disk + logger.WriteLineError($"The perfcollect script did not start/stop in {config.Timeout.TotalSeconds}s."); + logger.WriteLineInfo("You can create LttngProfiler providing LttngProfilerConfig with custom timeout value."); - try - { - perfCollectProcess.OutputDataReceived -= OnOutputDataReceived; + logger.Flush(); // flush recently logged message to disk - if (!perfCollectProcess.HasExited) + if (!perfCollectProcess.HasExited) + { + perfCollectProcess.KillTree(); // kill the entire process tree + } + } + else if (command == "stop") { - perfCollectProcess.KillTree(); // kill the entire process tree + benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetFilePath(parameters, creationTime, ".trace.zip")); } } - finally - { - perfCollectProcess.Dispose(); - perfCollectProcess = null; - } } } From 8472f20f9e365c9a2f4ad1f7af65aee33540a1ee Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 16 Mar 2020 11:57:23 +0100 Subject: [PATCH 11/24] use the new install -force option which allows us to avoid user being asked for permission to install stuff (and redirecting input and all that stuff) --- .../Diagnosers/LttngProfiler.cs | 2 +- src/BenchmarkDotNet/Templates/perfcollect | 71 +++++++++---------- 2 files changed, 33 insertions(+), 40 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index 36fa7fa156..86bae2dd8c 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -103,7 +103,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) } else { - (int exitCode, var output) = ProcessHelper.RunAndReadOutputLineByLine(perfCollectFile.FullName, "install", perfCollectFile.Directory.FullName, null, includeErrors: true, logger); + (int exitCode, var output) = ProcessHelper.RunAndReadOutputLineByLine(perfCollectFile.FullName, "install -force", perfCollectFile.Directory.FullName, null, includeErrors: true, logger); if (exitCode == SuccesExitCode) { diff --git a/src/BenchmarkDotNet/Templates/perfcollect b/src/BenchmarkDotNet/Templates/perfcollect index a15a484a5a..0947a064aa 100644 --- a/src/BenchmarkDotNet/Templates/perfcollect +++ b/src/BenchmarkDotNet/Templates/perfcollect @@ -624,6 +624,9 @@ declare -a LTTng_Kernel_ProcessLifetime=( ## Global Variables ###################################### +# Install without waiting for standard input +forceInstall=0 + # Declare an array of events to collect. declare -a eventsToCollect @@ -953,14 +956,17 @@ InstallLTTng_RHEL() packageRepo="https://packages.efficios.com/repo.files/EfficiOS-RHEL7-x86-64.repo" - # Prompt for confirmation, since we need to add a new repository. - BlueText - echo "LTTng installation requires that a new package repo be added to your yum configuration." - echo "The package repo url is: $packageRepo" - echo "" - read -p "Would you like to add the LTTng package repo to your YUM configuration? [Y/N]" resp - ResetText - if [ "$resp" == "Y" ] || [ "$resp" == "y" ] + if [ "$forceInstall" != 1 ] + then + # Prompt for confirmation, since we need to add a new repository. + BlueText + echo "LTTng installation requires that a new package repo be added to your yum configuration." + echo "The package repo url is: $packageRepo" + echo "" + read -p "Would you like to add the LTTng package repo to your YUM configuration? [Y/N]" resp + ResetText + fi + if [ "$resp" == "Y" ] || [ "$resp" == "y" ] || [ "$forceInstall" == 1 ] then # Make sure that wget is installed. BlueText @@ -999,14 +1005,17 @@ InstallLTTng_SUSE() # Package repo url packageRepo="http://download.opensuse.org/repositories/devel:/tools:/lttng/openSUSE_13.2/devel:tools:lttng.repo" - # Prompt for confirmation, since we need to add a new repository. - BlueText - echo "LTTng installation requires that a new package repo be added to your zypper configuration." - echo "The package repo url is: $packageRepo" - echo "" - read -p "Would you like to add the LTTng package repo to your zypper configuration? [Y/N]" resp - ResetText - if [ "$resp" == "Y" ] || [ "$resp" == "y" ] + if [ "$forceInstall" != 1 ] + then + # Prompt for confirmation, since we need to add a new repository. + BlueText + echo "LTTng installation requires that a new package repo be added to your zypper configuration." + echo "The package repo url is: $packageRepo" + echo "" + read -p "Would you like to add the LTTng package repo to your zypper configuration? [Y/N]" resp + ResetText + fi + if [ "$resp" == "Y" ] || [ "$resp" == "y" ] || [ "$forceInstall" == 1 ] then # Add package repo. @@ -1029,27 +1038,6 @@ InstallLTTng_Ubuntu() # Disallow non-root users. EnsureRoot - # Add the PPA feed as a repository. - BlueText - echo "LTTng can be installed using default Ubuntu packages via the Ubuntu package feeds or using the latest" - echo "stable package feed published by LTTng (PPA feed). It is recommended that LTTng be installed using the PPA feed." - echo "" - ResetText - echo " If you select yes, then the LTTng PPA feed will be added to your apt configuration." - echo " If you select no, then LTTng will be installed from an existing feed (either default Ubuntu or PPA if previously added." - echo "" - BlueText - read -p "Would you like to add the LTTng PPA feed to your apt configuration? [Y/N]" resp - ResetText - if [ "$resp" == "Y" ] || [ "$resp" == "y" ] - then - BlueText - echo "Adding LTTng PPA feed and running apt-get update." - ResetText - apt-add-repository ppa:lttng/ppa - apt-get update - fi - # Install packages. BlueText echo "Installing LTTng packages." @@ -1295,7 +1283,7 @@ CreateLTTngSession() fi lttngSessionName=`echo $output | grep -o "Session.*created." | sed 's/\(Session \| created.\)//g'` - lttngTraceDir=`echo $output | grep -o "Traces.*" | sed 's/\(Traces will be written in \|\)//g'` + lttngTraceDir=`echo $output | grep -o "Traces.*" | sed 's/\(Traces will be written in \|\)//g' | sed 's/\(Traces will be output to \|\)//g'` } SetupLTTngSession() @@ -1717,8 +1705,9 @@ PrintUsage() echo "livetrace:" echo " Print EventSource events directly to the console. Root privileges not required." echo "" - echo "install:" + echo "install options:" echo " Useful for first-time setup. Installs/upgrades perf_event and LTTng." + echo " -force : Force installation of required packages without prompt" echo "" } @@ -1949,6 +1938,10 @@ DoView() # Install perf if requested. Do this before all other validation. if [ "$1" == "install" ] then + if [ "$2" == "-force" ] + then + forceInstall=1 + fi InstallPerf InstallLTTng exit 0 From c2d8bf72530694ebdb3f083fc2ad9ed02efaf438 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 20 Sep 2022 20:47:52 +0200 Subject: [PATCH 12/24] refresh perfcollect --- .../Diagnosers/DiagnosersLoader.cs | 2 +- .../Diagnosers/LttngProfiler.cs | 24 +- .../Extensions/ProcessExtensions.cs | 2 +- src/BenchmarkDotNet/Templates/perfcollect | 475 +++++++++++++----- 4 files changed, 365 insertions(+), 138 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs b/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs index 585db1fd11..57bb5ee30e 100644 --- a/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs +++ b/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs @@ -41,7 +41,7 @@ private static IEnumerable LoadDiagnosers() yield return EventPipeProfiler.Default; if (RuntimeInformation.IsLinux()) - yield return LttngProfiler.Default; + yield return PerfCollectProfiler.Default; } if (!RuntimeInformation.IsWindows()) diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs index dccceac789..a097b6045d 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs @@ -17,23 +17,23 @@ namespace BenchmarkDotNet.Diagnosers { - public class LttngProfiler : IProfiler + public class PerfCollectProfiler : IProfiler { private const int SuccesExitCode = 0; private const string PerfCollectFileName = "perfcollect"; - public static readonly IDiagnoser Default = new LttngProfiler(new LttngProfilerConfig(performExtraBenchmarksRun: false)); + public static readonly IDiagnoser Default = new PerfCollectProfiler(new PerfCollectProfilerConfig(performExtraBenchmarksRun: false)); - private readonly LttngProfilerConfig config; + private readonly PerfCollectProfilerConfig config; private readonly DateTime creationTime = DateTime.Now; private readonly Dictionary benchmarkToTraceFile = new Dictionary(); [PublicAPI] - public LttngProfiler(LttngProfilerConfig config) => this.config = config; + public PerfCollectProfiler(PerfCollectProfilerConfig config) => this.config = config; - public string ShortName => "LTTng"; + public string ShortName => "PC"; - public IEnumerable Ids => new[] { nameof(LttngProfiler) }; + public IEnumerable Ids => new[] { nameof(PerfCollectProfiler) }; public IEnumerable Exporters => Array.Empty(); @@ -47,19 +47,19 @@ public IEnumerable Validate(ValidationParameters validationPara { if (!RuntimeInformation.IsLinux()) { - yield return new ValidationError(true, "The LttngProfiler works only on Linux!"); + yield return new ValidationError(true, "The PerfCollectProfiler works only on Linux!"); yield break; } if (Mono.Unix.Native.Syscall.getuid() != 0) { - yield return new ValidationError(true, "You must run as root to use LttngProfiler."); + yield return new ValidationError(true, "You must run as root to use PerfCollectProfiler."); yield break; } if (validationParameters.Benchmarks.Any() && !TryInstallPerfCollect(validationParameters)) { - yield return new ValidationError(true, "Failed to install perfcollect script. Please follow the instructions from https://github.com/dotnet/runtime/blob/master/docs/project/linux-performance-tracing.md"); + yield return new ValidationError(true, "Failed to install perfcollect script. Please follow the instructions from https://github.com/dotnet/runtime/blob/main/docs/project/linux-performance-tracing.md"); } } @@ -163,7 +163,7 @@ private void ExecutePerfCollectCommand(DiagnoserActionParameters parameters, Fil var logger = parameters.Config.GetCompositeLogger(); logger.WriteLineError($"The perfcollect script did not start/stop in {config.Timeout.TotalSeconds}s."); - logger.WriteLineInfo("You can create LttngProfiler providing LttngProfilerConfig with custom timeout value."); + logger.WriteLineInfo("You can create PerfCollectProfiler providing PerfCollectProfilerConfig with custom timeout value."); logger.Flush(); // flush recently logged message to disk @@ -180,11 +180,11 @@ private void ExecutePerfCollectCommand(DiagnoserActionParameters parameters, Fil } } - public class LttngProfilerConfig + public class PerfCollectProfilerConfig { /// if set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. True by default. /// how long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 30s by default - public LttngProfilerConfig(bool performExtraBenchmarksRun = true, int timeoutInSeconds = 60) + public PerfCollectProfilerConfig(bool performExtraBenchmarksRun = true, int timeoutInSeconds = 60) { RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; Timeout = TimeSpan.FromSeconds(timeoutInSeconds); diff --git a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs index e474ae3c49..4bcc5f210b 100644 --- a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs @@ -130,7 +130,7 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm if (benchmarkCase.Job.Environment.Runtime is MonoRuntime monoRuntime && !string.IsNullOrEmpty(monoRuntime.MonoBclPath)) start.EnvironmentVariables["MONO_PATH"] = monoRuntime.MonoBclPath; - if (benchmarkCase.Config.GetDiagnosers().OfType().Any()) + if (benchmarkCase.Config.GetDiagnosers().OfType().Any()) { start.EnvironmentVariables["COMPlus_PerfMapEnabled"] = "1"; start.EnvironmentVariables["COMPlus_EnableEventLog"] = "1"; diff --git a/src/BenchmarkDotNet/Templates/perfcollect b/src/BenchmarkDotNet/Templates/perfcollect index 0947a064aa..c266c9c06a 100644 --- a/src/BenchmarkDotNet/Templates/perfcollect +++ b/src/BenchmarkDotNet/Templates/perfcollect @@ -53,13 +53,14 @@ # Set when we parse command line arguments to determine if we should enable specific collection options. collect_cpu=1 collect_threadTime=0 +collect_offcpu=0 ###################################### ## .NET Event Categories ###################################### # Separate GCCollectOnly list because our LTTng implementation doesn't set tracepoint verbosity. -# Once tracepoint verbosity is set, we can set verbosity and collapse this wtih DotNETRuntime_GCKeyword. +# Once tracepoint verbosity is set, we can set verbosity and collapse this with DotNETRuntime_GCKeyword. declare -a DotNETRuntime_GCKeyword_GCCollectOnly=( DotNETRuntime:GCStart DotNETRuntime:GCStart_V1 @@ -357,7 +358,7 @@ declare -a DotNETRuntime_CompilationKeyword=( ) # Separate GCCollectOnly list because our LTTng implementation doesn't set tracepoint verbosity. -# Once tracepoint verbosity is set, we can set verbosity and collapse this wtih DotNETRuntimePrivate_GCPrivateKeyword. +# Once tracepoint verbosity is set, we can set verbosity and collapse this with DotNETRuntimePrivate_GCPrivateKeyword. declare -a DotNETRuntimePrivate_GCPrivateKeyword_GCCollectOnly=( DotNETRuntimePrivate:GCDecision DotNETRuntimePrivate:GCDecision_V1 @@ -615,11 +616,16 @@ declare -a DotNETRuntimePrivate_DynamicTypeUsageKeyword=( DotNETRuntimePrivate:ObjectVariantMarshallingToManaged ) -declare -a LTTng_Kernel_ProcessLifetime=( +declare -a EventSource=( + DotNETRuntime:EventSource +) + +declare -a LTTng_Kernel_ProcessLifetimeKeyword=( sched_process_exec sched_process_exit ) + ###################################### ## Global Variables ###################################### @@ -648,7 +654,7 @@ handlerInvoked=0 # Log file declare logFile logFilePrefix='/tmp/perfcollect' -logEnabled=1 +logEnabled=0 # Collect info to pass between processes collectInfoFile=$(dirname `mktemp -u`)/'perfcollect.sessioninfo' @@ -676,6 +682,34 @@ RunSilent() fi } +RunSilentBackground() +{ + if (( $logEnabled == 1 )) + then + echo "Running \"$*\"" >> $logFile + $* >> $logFile 2>&1 & sleep 1 + echo "" >> $logFile + else + $* > /dev/null 2>&1 & sleep 1 + fi + + # When handler is invoked, kill the process. + pid=$! + for (( ; ; )) + do + if [ "$handlerInvoked" == "1" ] + then + kill -INT $pid + break; + else + sleep 1 + fi + done + + # Wait for the process to exit. + wait $pid +} + InitializeLog() { # Pick the log file name. @@ -685,6 +719,9 @@ InitializeLog() logFile="$logFilePrefix.$RANDOM.log" done + # Mark the log as enabled. + logEnabled=1 + # Start the log date=`date` echo "Log started at ${date}" > $logFile @@ -692,8 +729,14 @@ InitializeLog() # The system information. LogAppend 'Machine info: ' `uname -a` - LogAppend 'perf version:' `$perfcmd --version` - LogAppend 'LTTng version: ' `lttng --version` + if [ "$perfcmd" != "" ] + then + LogAppend 'perf version:' `$perfcmd --version 2>&1` + fi + if [ "$lttngcmd" != "" ] + then + LogAppend 'LTTng version: ' `$lttngcmd --version` + fi LogAppend } @@ -717,32 +760,52 @@ CloseLog() ## RedText() { - tput setaf 1 + tput=`GetCommandFullPath "tput"` + if [ "$tput" != "" ] + then + $tput setaf 1 + fi } GreenText() { - tput setaf 2 + tput=`GetCommandFullPath "tput"` + if [ "$tput" != "" ] + then + $tput setaf 2 + fi } BlueText() { - tput setaf 6 + tput=`GetCommandFullPath "tput"` + if [ "$tput" != "" ] + then + $tput setaf 6 + fi } YellowText() { - tput setaf 3 + tput=`GetCommandFullPath "tput"` + if [ "$tput" != "" ] + then + $tput setaf 3 + fi } ResetText() { - tput sgr0 + tput=`GetCommandFullPath "tput"` + if [ "$tput" != "" ] + then + $tput sgr0 + fi } # $1 == Status message WriteStatus() { - LogAppend $* + LogAppend $* BlueText echo $1 ResetText @@ -813,9 +876,49 @@ DiscoverCommands() fi fi fi + + # Check to see if perf is installed, but doesn't exactly match the current kernel version. + # This happens when the kernel gets upgraded, or when running inside of a container where the + # host and container OS versions don't match. + perfoutput=$($perfcmd 2>&1) + if [ $? -eq 2 ] + then + # Check the beginning of the output to see if it matches the kernel warning. + warningText="WARNING: perf not found for kernel" + if [[ "$perfoutput" == "$warningText"* ]] + then + foundWorkingPerf=0 + WriteWarning "Perf is installed, but does not exactly match the version of the running kernel." + WriteWarning "This is often OK, and we'll try to workaround this." + WriteStatus "Attempting to find a working copy of perf." + # Attempt to find an existing version of perf to use. + # Order the search by newest directory first. + baseDir="/usr/lib/linux-tools" + searchPath="$baseDir/*/" + for dirName in $(ls -d --sort=time $searchPath) + do + candidatePerfPath="$dirName""perf" + $($candidatePerfPath > /dev/null 2>&1) + if [ $? -eq 1 ] + then + # If $? == 1, then use this copy of perf. + perfcmd=$candidatePerfPath + foundWorkingPerf=1 + break; + fi + done + if [ $foundWorkingPerf -eq 0 ] + then + FatalError "Unable to find a working copy of perf. Try re-installing via ./perfcollect install." + fi + WriteStatus "...FINISHED" + fi + fi + lttngcmd=`GetCommandFullPath "lttng"` zipcmd=`GetCommandFullPath "zip"` unzipcmd=`GetCommandFullPath "unzip"` + objdumpcmd=`GetCommandFullPath "objdump"` } GetCommandFullPath() @@ -826,6 +929,30 @@ GetCommandFullPath() ###################################### # Prerequisite Installation ###################################### +IsAlpine() +{ + local alpine=0 + local apk=`GetCommandFullPath "apk"` + if [ "$apk" != "" ] + then + alpine=1 + fi + + echo $alpine +} + +InstallPerf_Alpine() +{ + # Disallow non-root users. + EnsureRoot + + # Install perf + apk add perf --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community + + # Install zip and unzip + apk add zip unzip +} + IsRHEL() { local rhel=0 @@ -927,7 +1054,15 @@ InstallPerf_Ubuntu() BlueText echo "Installing perf_event packages." ResetText - apt-get install -y linux-tools-common linux-tools-`uname -r` linux-cloud-tools-`uname -r` zip software-properties-common + + # Handle Azure instances. + release=`uname -r` + if [[ "$release" == *"-azure" ]] + then + apt-get install -y linux-tools-azure zip software-properties-common + else + apt-get install -y linux-tools-common linux-tools-`uname -r` linux-cloud-tools-`uname -r` zip software-properties-common + fi } InstallPerf() @@ -944,6 +1079,9 @@ InstallPerf() elif [ "$(IsRHEL)" == "1" ] then InstallPerf_RHEL + elif [ "$(IsAlpine)" == "1" ] + then + InstallPerf_Alpine else FatalError "Auto install unsupported for this distribution. Install perf manually to continue." fi @@ -954,20 +1092,32 @@ InstallLTTng_RHEL() # Disallow non-root users. EnsureRoot - packageRepo="https://packages.efficios.com/repo.files/EfficiOS-RHEL7-x86-64.repo" - - if [ "$forceInstall" != 1 ] - then - # Prompt for confirmation, since we need to add a new repository. - BlueText - echo "LTTng installation requires that a new package repo be added to your yum configuration." - echo "The package repo url is: $packageRepo" - echo "" - read -p "Would you like to add the LTTng package repo to your YUM configuration? [Y/N]" resp - ResetText + local isRHEL7=0 + local isRHEL8=0 + if [ -e /etc/redhat-release ]; then + local redhatRelease=$(.dump files. + # Convert to jit dump file name. + local pid=`echo $mapFile | awk -F"-" '{print $NF}' | awk -F"." '{print $1}'` + local path=`echo $mapFile | awk -F"/" '{OFS="/";NF--;print $0;}'` + local jitDumpFile="$path/jit-$pid.dump" + + if [ -f $jitDumpFile ] + then + LogAppend "Saving $jitDumpFile" + RunSilent "cp $jitDumpFile ." + writeCrossgenWarning=0 + fi done WriteStatus "Generating native image symbol files" @@ -1501,100 +1694,105 @@ ProcessCollectedData() LogAppend "Skipping ${path}" fi done - - WriteStatus "...FINISHED" - else - if [ "$buildidList" != "" ] + if [ "$buildidList" != "" ] && [ $writeCrossgenWarning -eq 1 ] then LogAppend "crossgen not found, skipping native image map generation." WriteStatus "...SKIPPED" WriteWarning "Crossgen not found. Framework symbols will be unavailable." WriteWarning "See https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#resolving-framework-symbols for details." - else - WriteWarning "libcoreclr.so not found in perf data. Please verify that your .NET Core process is running and consuming CPU." fi fi IFS=$OLDIFS - # Create debuginfo files (separate symbols) for all modules in the trace. - WriteStatus "Saving native symbols" + WriteStatus "...FINISHED" - # Get the list of DSOs with hits in the trace file (those that are actually used). - # Filter out /tmp/perf-$pid.map files and files that end in .dll. - local dsosWithHits=`$perfcmd buildid-list --with-hits | grep -v /tmp/perf- | grep -v .dll$` - for dso in $dsosWithHits - do - # Build up tuples of buildid and binary path. - local processEntry=0 - if [ -f $dso ] - then - local pathToBinary=$dso - processEntry=1 - else - local buildid=$dso - pathToBinary='' - fi + if [ "$objdumpcmd" != "" ] + then + # Create debuginfo files (separate symbols) for all modules in the trace. + WriteStatus "Saving native symbols" - # Once we have a tuple for a binary path that exists, process it. - if [ "$processEntry" == "1" ] - then - # Get the binary name without path. - local binaryName=`basename $pathToBinary` + # Get the list of DSOs with hits in the trace file (those that are actually used). + # Filter out /tmp/perf-$pid.map files and files that end in .dll. + local dsosWithHits=`$perfcmd buildid-list --with-hits | grep -v /tmp/perf- | grep -v .dll$` + for dso in $dsosWithHits + do + # Build up tuples of buildid and binary path. + local processEntry=0 + if [ -f $dso ] + then + local pathToBinary=$dso + processEntry=1 + else + local buildid=$dso + pathToBinary='' + fi + + # Once we have a tuple for a binary path that exists, process it. + if [ "$processEntry" == "1" ] + then + # Get the binary name without path. + local binaryName=`basename $pathToBinary` - # Build the debuginfo file name. - local destFileName=$binaryName.debuginfo + # Build the debuginfo file name. + local destFileName=$binaryName.debuginfo - # Build the destination directory for the debuginfo file. - local currentDir=`pwd` - local destDir=$currentDir/debuginfo/$buildid + # Build the destination directory for the debuginfo file. + local currentDir=`pwd` + local destDir=$currentDir/debuginfo/$buildid - # Build the full path to the debuginfo file. - local destPath=$destDir/$destFileName + # Build the full path to the debuginfo file. + local destPath=$destDir/$destFileName - # Check to see if the DSO contains symbols, and if so, build the debuginfo file. - local noSymbols=`objdump -t $pathToBinary | grep "no symbols" -c` - if [ "$noSymbols" == "0" ] - then - LogAppend "Generating debuginfo for $binaryName with buildid=$buildid" - RunSilent "mkdir -p $destDir" - RunSilent "objcopy --only-keep-debug $pathToBinary $destPath" - else - LogAppend "Skipping $binaryName with buildid=$buildid. No symbol information." + # Check to see if the DSO contains symbols, and if so, build the debuginfo file. + local noSymbols=`$objdumpcmd -t $pathToBinary | grep "no symbols" -c` + if [ "$noSymbols" == "0" ] + then + LogAppend "Generating debuginfo for $binaryName with buildid=$buildid" + RunSilent "mkdir -p $destDir" + RunSilent "objcopy --only-keep-debug $pathToBinary $destPath" + else + LogAppend "Skipping $binaryName with buildid=$buildid. No symbol information." + fi fi - fi - done + done + + WriteStatus "...FINISHED" + fi + + WriteStatus "Resolving JIT and R2R symbols" + + originalFile="perf.data" + inputFile="perf-jit.data" + RunSilent $perfcmd inject --input $originalFile --jit --output $inputFile WriteStatus "...FINISHED" - WriteStatus "Exporting perf.data file" + WriteStatus "Exporting perf.data file" - # Merge sched_stat and sched_switch events. outputDumpFile="perf.data.txt" - mergedFile="perf.data.merged" - RunSilent "$perfcmd inject -v -s -i perf.data -o $mergedFile" - # I've not found a good way to get the behavior that we want here - running the command and redirecting the output - # when passing the command line to a function. Thus, this case is hardcoded. + # I've not found a good way to get the behavior that we want here - running the command and redirecting the output + # when passing the command line to a function. Thus, this case is hardcoded. # There is a breaking change where the capitalization of the -f parameter changed. - LogAppend "Running $perfcmd script -i $mergedFile -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile" - $perfcmd script -i $mergedFile -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile + LogAppend "Running $perfcmd script -i $inputFile -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile" + $perfcmd script -i $inputFile -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile LogAppend if [ $? -ne 0 ] then - LogAppend "Running $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile" - $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile + LogAppend "Running $perfcmd script -i $inputFile -f comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile" + $perfcmd script -i $inputFile -f comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile LogAppend fi # If the dump file is zero length, try to collect without the period field, which was added recently. if [ ! -s $outputDumpFile ] then - LogAppend "Running $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace > $outputDumpFile" - $perfcmd script -i $mergedFile -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile + LogAppend "Running $perfcmd script -i $inputFile -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace > $outputDumpFile" + $perfcmd script -i $inputFile -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace > $outputDumpFile 2>>$logFile LogAppend fi @@ -1688,8 +1886,9 @@ PrintUsage() echo "collect options:" echo "By default, collection includes CPU samples collected every ms." echo " -pid : Only collect data from the specified process id." - echo " -threadtime : Collect context switch events." - echo " -hwevents : Collect (some) hardware counters." + echo " -threadtime : Collect events for thread time analysis (on and off cpu)." + echo " -offcpu : Collect events for off-cpu analysis." + echo " -hwevents : Collect (some) hardware counters." echo "" echo "start:" echo " Start collection, but with Lttng trace ONLY. It needs to be used with 'stop' action." @@ -1717,8 +1916,8 @@ PrintUsage() BuildPerfRecordArgs() { - # Start with default collection arguments that record all CPUs (-a) and collect call stacks (-g) - collectionArgs="record -g" + # Start with default collection arguments that record at realtime priority, all CPUs (-a), and collect call stacks (-g) + collectionArgs="record -k 1 -g" # Filter to a single process if desired if [ "$collectionPid" != "" ] @@ -1731,8 +1930,15 @@ BuildPerfRecordArgs() # Enable CPU Collection if [ $collect_cpu -eq 1 ] then - collectionArgs="$collectionArgs -F 999" + collectionArgs="$collectionArgs" eventsToCollect=( "${eventsToCollect[@]}" "cpu-clock" ) + + # If only collecting CPU events, set the sampling rate to 1000. + # Otherwise, use the default sampling rate to avoid sampling sched events. + if [ $collect_threadTime -eq 0 ] && [ $collect_offcpu -eq 0 ] + then + collectionArgs="$collectionArgs -F 1000" + fi fi # Enable HW counters event collection @@ -1747,6 +1953,12 @@ BuildPerfRecordArgs() eventsToCollect=( "${eventsToCollect[@]}" "sched:sched_stat_sleep" "sched:sched_switch" "sched:sched_process_exit" ) fi + # Enable offcpu collection + if [ $collect_offcpu -eq 1 ] + then + eventsToCollect=( "${eventsToCollect[@]}" "sched:sched_stat_sleep" "sched:sched_switch" "sched:sched_process_exit" ) + fi + # Build up the set of events. local eventString="" local comma="," @@ -1821,7 +2033,12 @@ DoCollect() else if [ "$usePerf" == "1" ] then - RunSilent $perfcmd $collectionArgs + if [ ! -z ${duration} ] + then + RunSilent $perfcmd $collectionArgs + else + RunSilentBackground $perfcmd $collectionArgs + fi else # Wait here until CTRL+C handler gets called when user types CTRL+C. LogAppend "Waiting for CTRL+C handler to get called." @@ -1935,6 +2152,13 @@ DoView() ## Main Script Start ##################################### +# No arguments +if [ "$#" == "0" ] +then + PrintUsage + exit 0 +fi + # Install perf if requested. Do this before all other validation. if [ "$1" == "install" ] then @@ -1947,8 +2171,8 @@ then exit 0 fi -# Ensure prerequisites are installed. -EnsurePrereqsInstalled +# Discover external commands that will be called by this script. +DiscoverCommands # Initialize the log. if [ "$1" != "stop" ] @@ -1959,6 +2183,9 @@ fi # Process arguments. ProcessArguments $@ +# Ensure prerequisites are installed. +EnsurePrereqsInstalled + # Take the appropriate action. if [ "$action" == "collect" ] || [ "$action" == "start" ] then @@ -1972,4 +2199,4 @@ then elif [ "$action" == "livetrace" ] then DoLiveTrace -fi +fi \ No newline at end of file From 4ffdf5d38adfcf074bb070f6c6efd3e9080b74c4 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 22 Sep 2022 19:10:12 +0200 Subject: [PATCH 13/24] use collect command, stop in with Ctrl+C by sending SIGINT --- ...ttngProfiler.cs => PerfCollectProfiler.cs} | 95 ++++++++----------- .../Diagnosers/PerfCollectProfilerConfig.cs | 19 ++++ 2 files changed, 58 insertions(+), 56 deletions(-) rename src/BenchmarkDotNet/Diagnosers/{LttngProfiler.cs => PerfCollectProfiler.cs} (61%) create mode 100644 src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs diff --git a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs similarity index 61% rename from src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs rename to src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index a097b6045d..a09a264297 100644 --- a/src/BenchmarkDotNet/Diagnosers/LttngProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -14,19 +14,22 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; using JetBrains.Annotations; +using Mono.Unix.Native; namespace BenchmarkDotNet.Diagnosers { public class PerfCollectProfiler : IProfiler { - private const int SuccesExitCode = 0; + private const int SuccessExitCode = 0; private const string PerfCollectFileName = "perfcollect"; public static readonly IDiagnoser Default = new PerfCollectProfiler(new PerfCollectProfilerConfig(performExtraBenchmarksRun: false)); private readonly PerfCollectProfilerConfig config; private readonly DateTime creationTime = DateTime.Now; - private readonly Dictionary benchmarkToTraceFile = new Dictionary(); + private readonly Dictionary benchmarkToTraceFile = new (); + private FileInfo perfCollectFile; + private Process perfCollectProcess; [PublicAPI] public PerfCollectProfiler(PerfCollectProfilerConfig config) => this.config = config; @@ -51,7 +54,7 @@ public IEnumerable Validate(ValidationParameters validationPara yield break; } - if (Mono.Unix.Native.Syscall.getuid() != 0) + if (Syscall.getuid() != 0) { yield return new ValidationError(true, "You must run as root to use PerfCollectProfiler."); yield break; @@ -74,18 +77,17 @@ public void DisplayResults(ILogger logger) public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { - // it's crucial to start the trace before the process starts and stop it after the benchmarked process stops to have all of the necessary events in the trace file! - if (signal == HostSignal.BeforeAnythingElse) - Start(parameters); + if (signal == HostSignal.BeforeProcessStart) + perfCollectProcess = StartCollection(parameters); else if (signal == HostSignal.AfterProcessExit) - Stop(parameters); + StopCollection(parameters); } private bool TryInstallPerfCollect(ValidationParameters validationParameters) { var scriptInstallationDirectory = new DirectoryInfo(validationParameters.Config.ArtifactsPath).CreateIfNotExists(); - var perfCollectFile = scriptInstallationDirectory.GetFiles(PerfCollectFileName).SingleOrDefault(); + perfCollectFile = scriptInstallationDirectory.GetFiles(PerfCollectFileName).SingleOrDefault(); if (perfCollectFile != default) { return true; @@ -97,7 +99,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) string script = ResourceHelper.LoadTemplate(PerfCollectFileName); File.WriteAllText(perfCollectFile.FullName, script); - if (Mono.Unix.Native.Syscall.chmod(perfCollectFile.FullName, Mono.Unix.Native.FilePermissions.S_IXUSR) != SuccesExitCode) + if (Syscall.chmod(perfCollectFile.FullName, Mono.Unix.Native.FilePermissions.S_IXUSR) != SuccessExitCode) { logger.WriteError($"Unable to make perfcollect script an executable, the last error was: {Mono.Unix.Native.Syscall.GetLastError()}"); } @@ -105,8 +107,9 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) { (int exitCode, var output) = ProcessHelper.RunAndReadOutputLineByLine(perfCollectFile.FullName, "install -force", perfCollectFile.Directory.FullName, null, includeErrors: true, logger); - if (exitCode == SuccesExitCode) + if (exitCode == SuccessExitCode) { + logger.WriteLine("Successfully installed perfcollect"); return true; } @@ -125,21 +128,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) return false; } - private void Start(DiagnoserActionParameters parameters) - { - var perfCollectFile = new FileInfo(Directory.GetFiles(parameters.Config.ArtifactsPath, PerfCollectFileName).Single()); - - ExecutePerfCollectCommand(parameters, perfCollectFile, "start"); - } - - private void Stop(DiagnoserActionParameters parameters) - { - var perfCollectFile = new FileInfo(Directory.GetFiles(parameters.Config.ArtifactsPath, PerfCollectFileName).Single()); - - ExecutePerfCollectCommand(parameters, perfCollectFile, "stop"); - } - - private void ExecutePerfCollectCommand(DiagnoserActionParameters parameters, FileInfo perfCollectFile, string command) + private Process StartCollection(DiagnoserActionParameters parameters) { var traceName = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, fileExtension: null)).Name; // todo: escape characters bash does not like ' ', '(' etc @@ -147,51 +136,45 @@ private void ExecutePerfCollectCommand(DiagnoserActionParameters parameters, Fil var start = new ProcessStartInfo { FileName = perfCollectFile.FullName, - Arguments = $"{command} {traceName} -pid {parameters.Process.Id}", + Arguments = $"collect {traceName}", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, WorkingDirectory = perfCollectFile.Directory.FullName }; - using (var perfCollectProcess = new Process { StartInfo = start }) - { - perfCollectProcess.Start(); + return Process.Start(start); + } - if (!perfCollectProcess.WaitForExit((int)config.Timeout.TotalMilliseconds)) + private void StopCollection(DiagnoserActionParameters parameters) + { + try + { + if (!perfCollectProcess.HasExited) { var logger = parameters.Config.GetCompositeLogger(); - logger.WriteLineError($"The perfcollect script did not start/stop in {config.Timeout.TotalSeconds}s."); - logger.WriteLineInfo("You can create PerfCollectProfiler providing PerfCollectProfilerConfig with custom timeout value."); - - logger.Flush(); // flush recently logged message to disk + if (Syscall.kill(perfCollectProcess.Id, Signum.SIGINT) != 0) + { + var lastError = Syscall.GetLastError(); + logger.WriteLineError($"kill(perfcollect, SIGINT) failed with {lastError}"); + } - if (!perfCollectProcess.HasExited) + if (!perfCollectProcess.WaitForExit((int)config.Timeout.TotalMilliseconds)) { + logger.WriteLineError($"The perfcollect script did not stop in {config.Timeout.TotalSeconds}s. It's going to be force killed now."); + logger.WriteLineInfo("You can create PerfCollectProfiler providing PerfCollectProfilerConfig with custom timeout value."); + perfCollectProcess.KillTree(); // kill the entire process tree } } - else if (command == "stop") - { - benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, ".trace.zip")); - } - } - } - } - public class PerfCollectProfilerConfig - { - /// if set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. True by default. - /// how long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 30s by default - public PerfCollectProfilerConfig(bool performExtraBenchmarksRun = true, int timeoutInSeconds = 60) - { - RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; - Timeout = TimeSpan.FromSeconds(timeoutInSeconds); + benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, "trace.zip")); + } + finally + { + perfCollectProcess.Dispose(); + } } - - public TimeSpan Timeout { get; } - - public RunMode RunMode { get; } } -} +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs new file mode 100644 index 0000000000..90fb4ccc45 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs @@ -0,0 +1,19 @@ +using System; + +namespace BenchmarkDotNet.Diagnosers +{ + public class PerfCollectProfilerConfig + { + /// if set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. True by default. + /// how long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 30s by default + public PerfCollectProfilerConfig(bool performExtraBenchmarksRun = true, int timeoutInSeconds = 60) + { + RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; + Timeout = TimeSpan.FromSeconds(timeoutInSeconds); + } + + public TimeSpan Timeout { get; } + + public RunMode RunMode { get; } + } +} From a6086f850bfdce38db10144d67155d44ff4220f5 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 22 Sep 2022 20:17:32 +0200 Subject: [PATCH 14/24] add an attribute and a sample --- .../IntroPerfCollectProfiler.cs | 18 ++++++++++++++++++ .../PerfCollectProfilerAttribute.cs | 19 +++++++++++++++++++ .../Diagnosers/PerfCollectProfilerConfig.cs | 6 +++--- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 samples/BenchmarkDotNet.Samples/IntroPerfCollectProfiler.cs create mode 100644 src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs diff --git a/samples/BenchmarkDotNet.Samples/IntroPerfCollectProfiler.cs b/samples/BenchmarkDotNet.Samples/IntroPerfCollectProfiler.cs new file mode 100644 index 0000000000..9e6a7fa5ae --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroPerfCollectProfiler.cs @@ -0,0 +1,18 @@ +using System.IO; +using BenchmarkDotNet.Attributes; + +namespace BenchmarkDotNet.Samples +{ + [PerfCollectProfiler(performExtraBenchmarksRun: false)] + public class IntroPerfCollectProfiler + { + private readonly string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + private readonly string content = new string('a', 100_000); + + [Benchmark] + public void WriteAllText() => File.WriteAllText(path, content); + + [GlobalCleanup] + public void Delete() => File.Delete(path); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs b/src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs new file mode 100644 index 0000000000..fe4c1c9a6e --- /dev/null +++ b/src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs @@ -0,0 +1,19 @@ +using System; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; + +namespace BenchmarkDotNet.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)] + public class PerfCollectProfilerAttribute : Attribute, IConfigSource + { + /// When set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. False by default. + /// How long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 120s by default + public PerfCollectProfilerAttribute(bool performExtraBenchmarksRun = false, int timeoutInSeconds = 120) + { + Config = ManualConfig.CreateEmpty().AddDiagnoser(new PerfCollectProfiler(new PerfCollectProfilerConfig(performExtraBenchmarksRun, timeoutInSeconds))); + } + + public IConfig Config { get; } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs index 90fb4ccc45..602f2ad3c6 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs @@ -4,9 +4,9 @@ namespace BenchmarkDotNet.Diagnosers { public class PerfCollectProfilerConfig { - /// if set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. True by default. - /// how long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 30s by default - public PerfCollectProfilerConfig(bool performExtraBenchmarksRun = true, int timeoutInSeconds = 60) + /// When set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. False by default. + /// How long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 120s by default + public PerfCollectProfilerConfig(bool performExtraBenchmarksRun = false, int timeoutInSeconds = 120) { RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; Timeout = TimeSpan.FromSeconds(timeoutInSeconds); From e85c830cc8e15d9ff1cf65aa6f9970525b37bfb5 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 22 Sep 2022 20:36:18 +0200 Subject: [PATCH 15/24] enable BDN event source --- src/BenchmarkDotNet/Extensions/ProcessExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs index 4bcc5f210b..f0b73b4457 100644 --- a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; @@ -134,6 +135,7 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm { start.EnvironmentVariables["COMPlus_PerfMapEnabled"] = "1"; start.EnvironmentVariables["COMPlus_EnableEventLog"] = "1"; + start.EnvironmentVariables["COMPlus_EventSourceFilter"] = EngineEventSource.SourceName; } // corerun does not understand runtimeconfig.json files; From d2db654296daafdb9bd19865c385b1d05696a83d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 23 Sep 2022 12:44:18 +0200 Subject: [PATCH 16/24] emit an error when perfcollect finishes sooner than expected (most likely due to failed install) --- .../Diagnosers/PerfCollectProfiler.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index a09a264297..aab4b43521 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -99,7 +99,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) string script = ResourceHelper.LoadTemplate(PerfCollectFileName); File.WriteAllText(perfCollectFile.FullName, script); - if (Syscall.chmod(perfCollectFile.FullName, Mono.Unix.Native.FilePermissions.S_IXUSR) != SuccessExitCode) + if (Syscall.chmod(perfCollectFile.FullName, FilePermissions.S_IXUSR) != SuccessExitCode) { logger.WriteError($"Unable to make perfcollect script an executable, the last error was: {Mono.Unix.Native.Syscall.GetLastError()}"); } @@ -148,15 +148,15 @@ private Process StartCollection(DiagnoserActionParameters parameters) private void StopCollection(DiagnoserActionParameters parameters) { + var logger = parameters.Config.GetCompositeLogger(); + try { if (!perfCollectProcess.HasExited) { - var logger = parameters.Config.GetCompositeLogger(); - if (Syscall.kill(perfCollectProcess.Id, Signum.SIGINT) != 0) { - var lastError = Syscall.GetLastError(); + var lastError = Stdlib.GetLastError(); logger.WriteLineError($"kill(perfcollect, SIGINT) failed with {lastError}"); } @@ -167,9 +167,14 @@ private void StopCollection(DiagnoserActionParameters parameters) perfCollectProcess.KillTree(); // kill the entire process tree } - } - benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, "trace.zip")); + benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, "trace.zip")); + } + else + { + logger.WriteLineError("For some reason the perfcollect script has finished sooner than expected."); + logger.WriteLineInfo($"Please run '{perfCollectFile.FullName} install' as root and re-try."); + } } finally { From 220dcde9541f9a879ec1dc73ea16781b8343e3a4 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 26 Sep 2022 12:13:00 +0200 Subject: [PATCH 17/24] escape the arguments, store the result only if file was created, get rid of extra dot --- src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs | 9 ++++++--- src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index aab4b43521..fb7f7269db 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -131,12 +131,11 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) private Process StartCollection(DiagnoserActionParameters parameters) { var traceName = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, fileExtension: null)).Name; - // todo: escape characters bash does not like ' ', '(' etc var start = new ProcessStartInfo { FileName = perfCollectFile.FullName, - Arguments = $"collect {traceName}", + Arguments = $"collect \"{traceName}\"", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, @@ -168,7 +167,11 @@ private void StopCollection(DiagnoserActionParameters parameters) perfCollectProcess.KillTree(); // kill the entire process tree } - benchmarkToTraceFile[parameters.BenchmarkCase] = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, "trace.zip")); + FileInfo traceFile = new (ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, "trace.zip")); + if (traceFile.Exists) + { + benchmarkToTraceFile[parameters.BenchmarkCase] = traceFile; + } } else { diff --git a/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs index 20de492a82..70d80c332a 100644 --- a/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs @@ -71,7 +71,9 @@ private static string GetFilePath(string fileName, DiagnoserActionParameters det fileName = FolderNameHelper.ToFolderName(fileName); - return Path.Combine(details.Config.ArtifactsPath, $"{fileName}.{fileExtension}"); + return string.IsNullOrEmpty(fileExtension) + ? Path.Combine(details.Config.ArtifactsPath, fileName) + : Path.Combine(details.Config.ArtifactsPath, $"{fileName}.{fileExtension}"); } } } From 194f4bfbafa1858e1e284a0bcbe03da4200fab9e Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 27 Sep 2022 19:40:54 +0200 Subject: [PATCH 18/24] turn off precompiled code to resolve framework symbols without using crossgen(2) --- src/BenchmarkDotNet/Configs/ImmutableConfig.cs | 2 ++ src/BenchmarkDotNet/Extensions/ProcessExtensions.cs | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index fadd50ff91..83b7b9374a 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -106,6 +106,8 @@ internal ImmutableConfig( public bool HasThreadingDiagnoser() => diagnosers.Contains(ThreadingDiagnoser.Default); + internal bool HasPerfCollectProfiler() => diagnosers.OfType().Any(); + public bool HasExtraStatsDiagnoser() => HasMemoryDiagnoser() || HasThreadingDiagnoser(); public IDiagnoser GetCompositeDiagnoser(BenchmarkCase benchmarkCase, RunMode runMode) diff --git a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs index f0b73b4457..d71a9ce24e 100644 --- a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs @@ -3,9 +3,7 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; using BenchmarkDotNet.Characteristics; -using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; @@ -131,11 +129,15 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm if (benchmarkCase.Job.Environment.Runtime is MonoRuntime monoRuntime && !string.IsNullOrEmpty(monoRuntime.MonoBclPath)) start.EnvironmentVariables["MONO_PATH"] = monoRuntime.MonoBclPath; - if (benchmarkCase.Config.GetDiagnosers().OfType().Any()) + if (benchmarkCase.Config.HasPerfCollectProfiler()) { + // enable tracing configuration inside of CoreCLR (https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#collecting-a-trace) start.EnvironmentVariables["COMPlus_PerfMapEnabled"] = "1"; start.EnvironmentVariables["COMPlus_EnableEventLog"] = "1"; + // enable BDN Event Source (https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#filtering) start.EnvironmentVariables["COMPlus_EventSourceFilter"] = EngineEventSource.SourceName; + // turn off precompiled code to resolve framework symbols without using crossgen(2) (https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#alternative-turn-off-use-of-precompiled-code) + start.EnvironmentVariables["COMPlus_ZapDisable"] = "1"; } // corerun does not understand runtimeconfig.json files; From c0d692ae85996e693fc70af1623518603be18f50 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 27 Sep 2022 21:06:22 +0200 Subject: [PATCH 19/24] install dotnet symbols to get symbols for native runtime parts --- .../Diagnosers/PerfCollectProfiler.cs | 88 ++++++++++++++++++- .../Toolchains/DotNetCli/DotNetCliCommand.cs | 8 +- .../DotNetCli/DotNetCliCommandExecutor.cs | 4 +- .../NativeAot/NativeAotToolchain.cs | 3 + 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index fb7f7269db..3512f8dd54 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -8,10 +8,16 @@ using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Portability; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains; +using BenchmarkDotNet.Toolchains.CoreRun; +using BenchmarkDotNet.Toolchains.CsProj; +using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.NativeAot; using BenchmarkDotNet.Validators; using JetBrains.Annotations; using Mono.Unix.Native; @@ -28,6 +34,7 @@ public class PerfCollectProfiler : IProfiler private readonly PerfCollectProfilerConfig config; private readonly DateTime creationTime = DateTime.Now; private readonly Dictionary benchmarkToTraceFile = new (); + private readonly HashSet cliPathWithSymbolsInstalled = new (); private FileInfo perfCollectFile; private Process perfCollectProcess; @@ -101,7 +108,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) if (Syscall.chmod(perfCollectFile.FullName, FilePermissions.S_IXUSR) != SuccessExitCode) { - logger.WriteError($"Unable to make perfcollect script an executable, the last error was: {Mono.Unix.Native.Syscall.GetLastError()}"); + logger.WriteError($"Unable to make perfcollect script an executable, the last error was: {Syscall.GetLastError()}"); } else { @@ -130,6 +137,8 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) private Process StartCollection(DiagnoserActionParameters parameters) { + EnsureDotnetSymbolIsInstalled(parameters); + var traceName = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, fileExtension: null)).Name; var start = new ProcessStartInfo @@ -184,5 +193,82 @@ private void StopCollection(DiagnoserActionParameters parameters) perfCollectProcess.Dispose(); } } + + private void EnsureDotnetSymbolIsInstalled(DiagnoserActionParameters parameters) + { + string cliPath = parameters.BenchmarkCase.GetToolchain() switch + { + CsProjCoreToolchain core => core.CustomDotNetCliPath, + CoreRunToolchain coreRun => coreRun.CustomDotNetCliPath.FullName, + NativeAotToolchain nativeAot => nativeAot.CustomDotNetCliPath, + _ => null // custom toolchain, dotnet from $PATH will be used + }; + + if (cliPathWithSymbolsInstalled.Contains(cliPath)) + { + return; + } + + cliPathWithSymbolsInstalled.Add(cliPath); + + ILogger logger = parameters.Config.GetCompositeLogger(); + DotNetCliCommand cliCommand = new ( + cliPath: cliPath, + arguments: "--info", + generateResult: null, + logger: logger, + buildPartition: null, + environmentVariables: Array.Empty(), + timeout: TimeSpan.FromMinutes(3), + logOutput: false); + + var dotnetInfoResult = DotNetCliCommandExecutor.Execute(cliCommand); + if (!dotnetInfoResult.IsSuccess) + { + logger.WriteError($"Unable to run `dotnet --info` for `{cliPath}`, dotnet symbol won't be installed"); + return; + } + + // sth like "Microsoft.NETCore.App 7.0.0-rc.2.22451.11 [/home/adam/projects/performance/tools/dotnet/x64/shared/Microsoft.NETCore.App]" + // or "Microsoft.NETCore.App 7.0.0-rc.1.22423.16 [/usr/share/dotnet/shared/Microsoft.NETCore.App]" + string netCoreAppPath = dotnetInfoResult + .StandardOutput.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Where(line => line.EndsWith("Microsoft.NETCore.App]")) + .Select(line => line.Split('[')[1]) + .Distinct() + .Single(); // I assume there will be only one such folder + netCoreAppPath = netCoreAppPath.Substring(0, netCoreAppPath.Length - 1); // remove trailing `]` + + string[] missingSymbols = Directory.GetFiles(netCoreAppPath, "lib*.so", SearchOption.AllDirectories) + .Where(nativeLibPath => !File.Exists(Path.ChangeExtension(nativeLibPath, "so.dbg"))) + .Select(Path.GetDirectoryName) + .Distinct() + .ToArray(); + + if (!missingSymbols.Any()) + { + return; // the symbol files are already where we need them! + } + + cliCommand = cliCommand.WithLogOutput(true); // the following commands might take a while and fail, let's log them + + // We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs. + string toolPath = Path.Combine(Path.GetTempPath(), "BenchmarkDotNet", "symbols"); + var installResult = DotNetCliCommandExecutor.Execute(cliCommand.WithArguments($"tool install dotnet-symbol --tool-path \"{toolPath}\"")); + if (!installResult.IsSuccess) + { + logger.WriteError($"Unable to install dotnet symbol."); + return; + } + + foreach (var directoryPath in missingSymbols) + { + DotNetCliCommandExecutor.Execute(cliCommand + .WithCliPath(Path.Combine(toolPath, "dotnet-symbol")) + .WithArguments($"--symbols --output {directoryPath} {directoryPath}/lib*.so")); + } + + DotNetCliCommandExecutor.Execute(cliCommand.WithArguments($"tool uninstall dotnet-symbol --tool-path \"{toolPath}\"")); + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index 4592063359..bad9f153fb 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -44,13 +44,19 @@ public DotNetCliCommand(string cliPath, string arguments, GenerateResult generat BuildPartition = buildPartition; EnvironmentVariables = environmentVariables; Timeout = timeout; - LogOutput = logOutput || buildPartition.LogBuildOutput; + LogOutput = logOutput || (buildPartition is not null && buildPartition.LogBuildOutput); RetryFailedBuildWithNoDeps = retryFailedBuildWithNoDeps; } + public DotNetCliCommand WithLogOutput(bool logOutput) + => new (CliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: logOutput); + public DotNetCliCommand WithArguments(string arguments) => new (CliPath, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); + public DotNetCliCommand WithCliPath(string cliPath) + => new (cliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); + [PublicAPI] public BuildResult RestoreThenBuild() { diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs index 9d4cefea0c..bd63ee595f 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs @@ -22,11 +22,11 @@ public static class DotNetCliCommandExecutor [PublicAPI] public static DotNetCliCommandResult Execute(DotNetCliCommand parameters) { - using (var process = new Process { StartInfo = BuildStartInfo(parameters.CliPath, parameters.GenerateResult.ArtifactsPaths.BuildArtifactsDirectoryPath, parameters.Arguments, parameters.EnvironmentVariables) }) + using (var process = new Process { StartInfo = BuildStartInfo(parameters.CliPath, parameters.GenerateResult?.ArtifactsPaths.BuildArtifactsDirectoryPath, parameters.Arguments, parameters.EnvironmentVariables) }) using (var outputReader = new AsyncProcessOutputReader(process, parameters.LogOutput, parameters.Logger)) using (new ConsoleExitHandler(process, parameters.Logger)) { - parameters.Logger.WriteLineInfo($"// start {parameters.CliPath ?? "dotnet"} {parameters.Arguments} in {parameters.GenerateResult.ArtifactsPaths.BuildArtifactsDirectoryPath}"); + parameters.Logger.WriteLineInfo($"// start {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}"); var stopwatch = Stopwatch.StartNew(); diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs index a2eb6183f0..fb30e94a5a 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs @@ -36,8 +36,11 @@ internal NativeAotToolchain(string displayName, new DotNetCliPublisher(customDotNetCliPath, GetExtraArguments(runtimeIdentifier)), new Executor()) { + CustomDotNetCliPath = customDotNetCliPath; } + internal string CustomDotNetCliPath { get; } + public static NativeAotToolchainBuilder CreateBuilder() => NativeAotToolchainBuilder.Create(); public static string GetExtraArguments(string runtimeIdentifier) => $"-r {runtimeIdentifier}"; From 7191b6bbbf839c4fa8433a21d0943f364006d021 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 28 Sep 2022 12:51:41 +0200 Subject: [PATCH 20/24] add workaround for https://github.com/dotnet/runtime/issues/71786 --- src/BenchmarkDotNet/Extensions/ProcessExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs index d71a9ce24e..54eaac9d6b 100644 --- a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs @@ -138,6 +138,8 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm start.EnvironmentVariables["COMPlus_EventSourceFilter"] = EngineEventSource.SourceName; // turn off precompiled code to resolve framework symbols without using crossgen(2) (https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#alternative-turn-off-use-of-precompiled-code) start.EnvironmentVariables["COMPlus_ZapDisable"] = "1"; + // workaround for https://github.com/dotnet/runtime/issues/71786, will be solved by next perf version + start.EnvironmentVariables["DOTNET_EnableWriteXorExecute"] = "0"; } // corerun does not understand runtimeconfig.json files; From bb0c66dd372b1d3905560a247401a7ce69e2babe Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 28 Sep 2022 13:27:08 +0200 Subject: [PATCH 21/24] download symbols for all .so files --- .../Diagnosers/PerfCollectProfiler.cs | 66 +++++++------------ .../Toolchains/DotNetCli/DotNetCliCommand.cs | 3 - .../DotNetCli/DotNetCliCommandExecutor.cs | 23 +++++++ 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index 3512f8dd54..4570ea96a2 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -201,46 +201,18 @@ private void EnsureDotnetSymbolIsInstalled(DiagnoserActionParameters parameters) CsProjCoreToolchain core => core.CustomDotNetCliPath, CoreRunToolchain coreRun => coreRun.CustomDotNetCliPath.FullName, NativeAotToolchain nativeAot => nativeAot.CustomDotNetCliPath, - _ => null // custom toolchain, dotnet from $PATH will be used + _ => DotNetCliCommandExecutor.DefaultDotNetCliPath.Value }; - if (cliPathWithSymbolsInstalled.Contains(cliPath)) + if (!cliPathWithSymbolsInstalled.Add(cliPath)) { return; } - cliPathWithSymbolsInstalled.Add(cliPath); - - ILogger logger = parameters.Config.GetCompositeLogger(); - DotNetCliCommand cliCommand = new ( - cliPath: cliPath, - arguments: "--info", - generateResult: null, - logger: logger, - buildPartition: null, - environmentVariables: Array.Empty(), - timeout: TimeSpan.FromMinutes(3), - logOutput: false); - - var dotnetInfoResult = DotNetCliCommandExecutor.Execute(cliCommand); - if (!dotnetInfoResult.IsSuccess) - { - logger.WriteError($"Unable to run `dotnet --info` for `{cliPath}`, dotnet symbol won't be installed"); - return; - } - - // sth like "Microsoft.NETCore.App 7.0.0-rc.2.22451.11 [/home/adam/projects/performance/tools/dotnet/x64/shared/Microsoft.NETCore.App]" - // or "Microsoft.NETCore.App 7.0.0-rc.1.22423.16 [/usr/share/dotnet/shared/Microsoft.NETCore.App]" - string netCoreAppPath = dotnetInfoResult - .StandardOutput.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Where(line => line.EndsWith("Microsoft.NETCore.App]")) - .Select(line => line.Split('[')[1]) - .Distinct() - .Single(); // I assume there will be only one such folder - netCoreAppPath = netCoreAppPath.Substring(0, netCoreAppPath.Length - 1); // remove trailing `]` - - string[] missingSymbols = Directory.GetFiles(netCoreAppPath, "lib*.so", SearchOption.AllDirectories) - .Where(nativeLibPath => !File.Exists(Path.ChangeExtension(nativeLibPath, "so.dbg"))) + string sdkPath = DotNetCliCommandExecutor.GetSdkPath(cliPath); // /usr/share/dotnet/sdk/ + string dotnetPath = Path.GetDirectoryName(sdkPath); // /usr/share/dotnet/ + string[] missingSymbols = Directory.GetFiles(dotnetPath, "lib*.so", SearchOption.AllDirectories) + .Where(nativeLibPath => !nativeLibPath.Contains("FallbackFolder") && !File.Exists(Path.ChangeExtension(nativeLibPath, "so.dbg"))) .Select(Path.GetDirectoryName) .Distinct() .ToArray(); @@ -250,23 +222,29 @@ private void EnsureDotnetSymbolIsInstalled(DiagnoserActionParameters parameters) return; // the symbol files are already where we need them! } - cliCommand = cliCommand.WithLogOutput(true); // the following commands might take a while and fail, let's log them - + ILogger logger = parameters.Config.GetCompositeLogger(); // We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs. string toolPath = Path.Combine(Path.GetTempPath(), "BenchmarkDotNet", "symbols"); - var installResult = DotNetCliCommandExecutor.Execute(cliCommand.WithArguments($"tool install dotnet-symbol --tool-path \"{toolPath}\"")); + DotNetCliCommand cliCommand = new ( + cliPath: cliPath, + arguments: $"tool install dotnet-symbol --tool-path \"{toolPath}\"", + generateResult: null, + logger: logger, + buildPartition: null, + environmentVariables: Array.Empty(), + timeout: TimeSpan.FromMinutes(3), + logOutput: true); // the following commands might take a while and fail, let's log them + + var installResult = DotNetCliCommandExecutor.Execute(cliCommand); if (!installResult.IsSuccess) { - logger.WriteError($"Unable to install dotnet symbol."); + logger.WriteError("Unable to install dotnet symbol."); return; } - foreach (var directoryPath in missingSymbols) - { - DotNetCliCommandExecutor.Execute(cliCommand - .WithCliPath(Path.Combine(toolPath, "dotnet-symbol")) - .WithArguments($"--symbols --output {directoryPath} {directoryPath}/lib*.so")); - } + DotNetCliCommandExecutor.Execute(cliCommand + .WithCliPath(Path.Combine(toolPath, "dotnet-symbol")) + .WithArguments($"--recurse-subdirectories --symbols \"{dotnetPath}/dotnet\" \"{dotnetPath}/lib*.so\"")); DotNetCliCommandExecutor.Execute(cliCommand.WithArguments($"tool uninstall dotnet-symbol --tool-path \"{toolPath}\"")); } diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index bad9f153fb..bdcafc06e2 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -48,9 +48,6 @@ public DotNetCliCommand(string cliPath, string arguments, GenerateResult generat RetryFailedBuildWithNoDeps = retryFailedBuildWithNoDeps; } - public DotNetCliCommand WithLogOutput(bool logOutput) - => new (CliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: logOutput); - public DotNetCliCommand WithArguments(string arguments) => new (CliPath, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs index bd63ee595f..a15e30e9cf 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs @@ -156,5 +156,28 @@ private static string GetDefaultDotNetCliPath() [DllImport("libc")] private static extern int getppid(); + + internal static string GetSdkPath(string cliPath) + { + DotNetCliCommand cliCommand = new ( + cliPath: cliPath, + arguments: "--info", + generateResult: null, + logger: NullLogger.Instance, + buildPartition: null, + environmentVariables: Array.Empty(), + timeout: TimeSpan.FromMinutes(3), + logOutput: false); + + // sth like " 3.1.423 [/usr/share/dotnet/sdk] + string sdkPath = Execute(cliCommand) + .StandardOutput.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Where(line => line.EndsWith("/sdk]")) + .Select(line => line.Split('[')[1]) + .Distinct() + .Single(); // I assume there will be only one such folder + + return sdkPath.Substring(0, sdkPath.Length - 1); // remove trailing `]` + } } } \ No newline at end of file From 5b04d07def361687aed0bf60e72904b51ee52e41 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 28 Sep 2022 16:53:19 +0200 Subject: [PATCH 22/24] polishing: new short name (perf instead PC), running for multiple runtimes and code refactor --- .../Diagnosers/PerfCollectProfiler.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index 4570ea96a2..eb3aae2129 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -26,9 +26,6 @@ namespace BenchmarkDotNet.Diagnosers { public class PerfCollectProfiler : IProfiler { - private const int SuccessExitCode = 0; - private const string PerfCollectFileName = "perfcollect"; - public static readonly IDiagnoser Default = new PerfCollectProfiler(new PerfCollectProfilerConfig(performExtraBenchmarksRun: false)); private readonly PerfCollectProfilerConfig config; @@ -41,7 +38,7 @@ public class PerfCollectProfiler : IProfiler [PublicAPI] public PerfCollectProfiler(PerfCollectProfilerConfig config) => this.config = config; - public string ShortName => "PC"; + public string ShortName => "perf"; public IEnumerable Ids => new[] { nameof(PerfCollectProfiler) }; @@ -94,19 +91,18 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) { var scriptInstallationDirectory = new DirectoryInfo(validationParameters.Config.ArtifactsPath).CreateIfNotExists(); - perfCollectFile = scriptInstallationDirectory.GetFiles(PerfCollectFileName).SingleOrDefault(); - if (perfCollectFile != default) + perfCollectFile = new FileInfo(Path.Combine(scriptInstallationDirectory.FullName, "perfcollect")); + if (perfCollectFile.Exists) { return true; } var logger = validationParameters.Config.GetCompositeLogger(); - perfCollectFile = new FileInfo(Path.Combine(scriptInstallationDirectory.FullName, PerfCollectFileName)); - string script = ResourceHelper.LoadTemplate(PerfCollectFileName); + string script = ResourceHelper.LoadTemplate(perfCollectFile.Name); File.WriteAllText(perfCollectFile.FullName, script); - if (Syscall.chmod(perfCollectFile.FullName, FilePermissions.S_IXUSR) != SuccessExitCode) + if (Syscall.chmod(perfCollectFile.FullName, FilePermissions.S_IXUSR) != 0) { logger.WriteError($"Unable to make perfcollect script an executable, the last error was: {Syscall.GetLastError()}"); } @@ -114,7 +110,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) { (int exitCode, var output) = ProcessHelper.RunAndReadOutputLineByLine(perfCollectFile.FullName, "install -force", perfCollectFile.Directory.FullName, null, includeErrors: true, logger); - if (exitCode == SuccessExitCode) + if (exitCode == 0) { logger.WriteLine("Successfully installed perfcollect"); return true; @@ -139,7 +135,7 @@ private Process StartCollection(DiagnoserActionParameters parameters) { EnsureDotnetSymbolIsInstalled(parameters); - var traceName = new FileInfo(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, fileExtension: null)).Name; + var traceName = GetTraceFile(parameters, extension: null).Name; var start = new ProcessStartInfo { @@ -176,7 +172,7 @@ private void StopCollection(DiagnoserActionParameters parameters) perfCollectProcess.KillTree(); // kill the entire process tree } - FileInfo traceFile = new (ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, "trace.zip")); + FileInfo traceFile = GetTraceFile(parameters, "trace.zip"); if (traceFile.Exists) { benchmarkToTraceFile[parameters.BenchmarkCase] = traceFile; @@ -248,5 +244,9 @@ private void EnsureDotnetSymbolIsInstalled(DiagnoserActionParameters parameters) DotNetCliCommandExecutor.Execute(cliCommand.WithArguments($"tool uninstall dotnet-symbol --tool-path \"{toolPath}\"")); } + + // perfcollect does not allow for spaces in the trace file name + private FileInfo GetTraceFile(DiagnoserActionParameters parameters, string extension) + => new (ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, extension).Replace(" ", "_")); } } \ No newline at end of file From 5201ed1eb153cdb029481b4e30ce00c7c0b0c48a Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 28 Sep 2022 21:00:51 +0200 Subject: [PATCH 23/24] don't turn off precompiled code --- src/BenchmarkDotNet/Extensions/ProcessExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs index 54eaac9d6b..9dffbf5eff 100644 --- a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs @@ -136,8 +136,6 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm start.EnvironmentVariables["COMPlus_EnableEventLog"] = "1"; // enable BDN Event Source (https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#filtering) start.EnvironmentVariables["COMPlus_EventSourceFilter"] = EngineEventSource.SourceName; - // turn off precompiled code to resolve framework symbols without using crossgen(2) (https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md#alternative-turn-off-use-of-precompiled-code) - start.EnvironmentVariables["COMPlus_ZapDisable"] = "1"; // workaround for https://github.com/dotnet/runtime/issues/71786, will be solved by next perf version start.EnvironmentVariables["DOTNET_EnableWriteXorExecute"] = "0"; } From 7819a20159e3d691ae54491d022b4ebb75f50571 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 29 Sep 2022 16:34:50 +0200 Subject: [PATCH 24/24] final polishing before merging --- .../Attributes/PerfCollectProfilerAttribute.cs | 2 +- src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs | 8 ++++---- .../Diagnosers/PerfCollectProfilerConfig.cs | 2 +- src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs | 7 ++++--- .../Toolchains/DotNetCli/DotNetCliCommandExecutor.cs | 5 ++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs b/src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs index fe4c1c9a6e..8b3e0bb1d2 100644 --- a/src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs +++ b/src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs @@ -8,7 +8,7 @@ namespace BenchmarkDotNet.Attributes public class PerfCollectProfilerAttribute : Attribute, IConfigSource { /// When set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. False by default. - /// How long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 120s by default + /// How long should we wait for the perfcollect script to finish processing the trace. 120s by default. public PerfCollectProfilerAttribute(bool performExtraBenchmarksRun = false, int timeoutInSeconds = 120) { Config = ManualConfig.CreateEmpty().AddDiagnoser(new PerfCollectProfiler(new PerfCollectProfilerConfig(performExtraBenchmarksRun, timeoutInSeconds))); diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index eb3aae2129..a050beb5dd 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -133,7 +133,7 @@ private bool TryInstallPerfCollect(ValidationParameters validationParameters) private Process StartCollection(DiagnoserActionParameters parameters) { - EnsureDotnetSymbolIsInstalled(parameters); + EnsureSymbolsForNativeRuntime(parameters); var traceName = GetTraceFile(parameters, extension: null).Name; @@ -190,7 +190,7 @@ private void StopCollection(DiagnoserActionParameters parameters) } } - private void EnsureDotnetSymbolIsInstalled(DiagnoserActionParameters parameters) + private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters) { string cliPath = parameters.BenchmarkCase.GetToolchain() switch { @@ -245,8 +245,8 @@ private void EnsureDotnetSymbolIsInstalled(DiagnoserActionParameters parameters) DotNetCliCommandExecutor.Execute(cliCommand.WithArguments($"tool uninstall dotnet-symbol --tool-path \"{toolPath}\"")); } - // perfcollect does not allow for spaces in the trace file name private FileInfo GetTraceFile(DiagnoserActionParameters parameters, string extension) - => new (ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, extension).Replace(" ", "_")); + => new (ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, extension) + .Replace(" ", "_")); // perfcollect does not allow for spaces in the trace file name } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs index 602f2ad3c6..8d025c3366 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs @@ -5,7 +5,7 @@ namespace BenchmarkDotNet.Diagnosers public class PerfCollectProfilerConfig { /// When set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. False by default. - /// How long should we wait for the perfcollect script to start collecting and/or finish processing the trace. 120s by default + /// How long should we wait for the perfcollect script to finish processing the trace. 120s by default. public PerfCollectProfilerConfig(bool performExtraBenchmarksRun = false, int timeoutInSeconds = 120) { RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; diff --git a/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs index 70d80c332a..490d47c2ea 100644 --- a/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs @@ -71,9 +71,10 @@ private static string GetFilePath(string fileName, DiagnoserActionParameters det fileName = FolderNameHelper.ToFolderName(fileName); - return string.IsNullOrEmpty(fileExtension) - ? Path.Combine(details.Config.ArtifactsPath, fileName) - : Path.Combine(details.Config.ArtifactsPath, $"{fileName}.{fileExtension}"); + if (!string.IsNullOrEmpty(fileExtension)) + fileName = $"{fileName}.{fileExtension}"; + + return Path.Combine(details.Config.ArtifactsPath, fileName); } } } diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs index a15e30e9cf..a6583ad734 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs @@ -166,13 +166,12 @@ internal static string GetSdkPath(string cliPath) logger: NullLogger.Instance, buildPartition: null, environmentVariables: Array.Empty(), - timeout: TimeSpan.FromMinutes(3), + timeout: TimeSpan.FromMinutes(1), logOutput: false); - // sth like " 3.1.423 [/usr/share/dotnet/sdk] string sdkPath = Execute(cliCommand) .StandardOutput.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Where(line => line.EndsWith("/sdk]")) + .Where(line => line.EndsWith("/sdk]")) // sth like " 3.1.423 [/usr/share/dotnet/sdk] .Select(line => line.Split('[')[1]) .Distinct() .Single(); // I assume there will be only one such folder