-
-
Notifications
You must be signed in to change notification settings - Fork 1k
PerfCollect diagnoser #2117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
PerfCollect diagnoser #2117
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
7c09e43
initial LttngProfiler implementation
adamsitnik c4921ad
download the script to the right folder
adamsitnik c59b96d
escepe the arguments, wait until it starts actual collection, don't g…
adamsitnik 4171885
wait until the script ends post-processing
adamsitnik 77e3a52
use Mono.Posix to send SIGINT (Ctrl+C) to the script
adamsitnik 11c5e06
more changes
adamsitnik eedf8c0
Merge remote-tracking branch 'origin/master' into perfCollectDiagnoser
adamsitnik 2c63c4c
remove duplicated code
adamsitnik c77e7df
add perfcollect to the resources, don't download it
adamsitnik 93330c2
update doc link
adamsitnik bd7ceb0
use the new start and stop commands
adamsitnik 8472f20
use the new install -force option which allows us to avoid user being…
adamsitnik f08473b
Merge branch 'master' into perfCollectDiagnoser
adamsitnik c2d8bf7
refresh perfcollect
adamsitnik 4ffdf5d
use collect command, stop in with Ctrl+C by sending SIGINT
adamsitnik a6086f8
add an attribute and a sample
adamsitnik e85c830
enable BDN event source
adamsitnik d2db654
emit an error when perfcollect finishes sooner than expected (most li…
adamsitnik 220dcde
escape the arguments, store the result only if file was created, get …
adamsitnik 194f4bf
turn off precompiled code to resolve framework symbols without using …
adamsitnik c0d692a
install dotnet symbols to get symbols for native runtime parts
adamsitnik 7191b6b
add workaround for https://github.com/dotnet/runtime/issues/71786
adamsitnik bb0c66d
download symbols for all .so files
adamsitnik 5b04d07
polishing: new short name (perf instead PC), running for multiple run…
adamsitnik 5201ed1
don't turn off precompiled code
adamsitnik 7819a20
final polishing before merging
adamsitnik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
18 changes: 18 additions & 0 deletions
18
samples/BenchmarkDotNet.Samples/IntroPerfCollectProfiler.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/BenchmarkDotNet/Attributes/PerfCollectProfilerAttribute.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
{ | ||
/// <param name="performExtraBenchmarksRun">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.</param> | ||
/// <param name="timeoutInSeconds">How long should we wait for the perfcollect script to finish processing the trace. 120s by default.</param> | ||
public PerfCollectProfilerAttribute(bool performExtraBenchmarksRun = false, int timeoutInSeconds = 120) | ||
{ | ||
Config = ManualConfig.CreateEmpty().AddDiagnoser(new PerfCollectProfiler(new PerfCollectProfilerConfig(performExtraBenchmarksRun, timeoutInSeconds))); | ||
} | ||
|
||
public IConfig Config { get; } | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using BenchmarkDotNet.Analysers; | ||
using BenchmarkDotNet.Engines; | ||
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; | ||
|
||
namespace BenchmarkDotNet.Diagnosers | ||
{ | ||
public class PerfCollectProfiler : IProfiler | ||
{ | ||
public static readonly IDiagnoser Default = new PerfCollectProfiler(new PerfCollectProfilerConfig(performExtraBenchmarksRun: false)); | ||
|
||
private readonly PerfCollectProfilerConfig config; | ||
private readonly DateTime creationTime = DateTime.Now; | ||
private readonly Dictionary<BenchmarkCase, FileInfo> benchmarkToTraceFile = new (); | ||
private readonly HashSet<string> cliPathWithSymbolsInstalled = new (); | ||
private FileInfo perfCollectFile; | ||
private Process perfCollectProcess; | ||
|
||
[PublicAPI] | ||
public PerfCollectProfiler(PerfCollectProfilerConfig config) => this.config = config; | ||
|
||
public string ShortName => "perf"; | ||
|
||
public IEnumerable<string> Ids => new[] { nameof(PerfCollectProfiler) }; | ||
|
||
public IEnumerable<IExporter> Exporters => Array.Empty<IExporter>(); | ||
|
||
public IEnumerable<IAnalyser> Analysers => Array.Empty<IAnalyser>(); | ||
|
||
public IEnumerable<Metric> ProcessResults(DiagnoserResults results) => Array.Empty<Metric>(); | ||
|
||
public RunMode GetRunMode(BenchmarkCase benchmarkCase) => config.RunMode; | ||
|
||
public IEnumerable<ValidationError> Validate(ValidationParameters validationParameters) | ||
{ | ||
if (!RuntimeInformation.IsLinux()) | ||
{ | ||
yield return new ValidationError(true, "The PerfCollectProfiler works only on Linux!"); | ||
yield break; | ||
} | ||
|
||
if (Syscall.getuid() != 0) | ||
{ | ||
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/main/docs/project/linux-performance-tracing.md"); | ||
} | ||
} | ||
|
||
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) | ||
{ | ||
if (signal == HostSignal.BeforeProcessStart) | ||
perfCollectProcess = StartCollection(parameters); | ||
else if (signal == HostSignal.AfterProcessExit) | ||
StopCollection(parameters); | ||
} | ||
|
||
private bool TryInstallPerfCollect(ValidationParameters validationParameters) | ||
{ | ||
var scriptInstallationDirectory = new DirectoryInfo(validationParameters.Config.ArtifactsPath).CreateIfNotExists(); | ||
|
||
perfCollectFile = new FileInfo(Path.Combine(scriptInstallationDirectory.FullName, "perfcollect")); | ||
if (perfCollectFile.Exists) | ||
{ | ||
return true; | ||
} | ||
|
||
var logger = validationParameters.Config.GetCompositeLogger(); | ||
|
||
string script = ResourceHelper.LoadTemplate(perfCollectFile.Name); | ||
File.WriteAllText(perfCollectFile.FullName, script); | ||
|
||
if (Syscall.chmod(perfCollectFile.FullName, FilePermissions.S_IXUSR) != 0) | ||
{ | ||
logger.WriteError($"Unable to make perfcollect script an executable, the last error was: {Syscall.GetLastError()}"); | ||
} | ||
else | ||
{ | ||
(int exitCode, var output) = ProcessHelper.RunAndReadOutputLineByLine(perfCollectFile.FullName, "install -force", perfCollectFile.Directory.FullName, null, includeErrors: true, logger); | ||
|
||
if (exitCode == 0) | ||
{ | ||
logger.WriteLine("Successfully installed perfcollect"); | ||
return true; | ||
} | ||
|
||
logger.WriteLineError("Failed to install perfcollect"); | ||
foreach (var outputLine in output) | ||
{ | ||
logger.WriteLine(outputLine); | ||
} | ||
} | ||
|
||
if (perfCollectFile.Exists) | ||
{ | ||
perfCollectFile.Delete(); // if the file exists it means that perfcollect is installed | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private Process StartCollection(DiagnoserActionParameters parameters) | ||
{ | ||
EnsureSymbolsForNativeRuntime(parameters); | ||
|
||
var traceName = GetTraceFile(parameters, extension: null).Name; | ||
|
||
var start = new ProcessStartInfo | ||
{ | ||
FileName = perfCollectFile.FullName, | ||
Arguments = $"collect \"{traceName}\"", | ||
UseShellExecute = false, | ||
RedirectStandardOutput = true, | ||
CreateNoWindow = true, | ||
WorkingDirectory = perfCollectFile.Directory.FullName | ||
}; | ||
|
||
return Process.Start(start); | ||
} | ||
|
||
private void StopCollection(DiagnoserActionParameters parameters) | ||
{ | ||
var logger = parameters.Config.GetCompositeLogger(); | ||
|
||
try | ||
{ | ||
if (!perfCollectProcess.HasExited) | ||
{ | ||
if (Syscall.kill(perfCollectProcess.Id, Signum.SIGINT) != 0) | ||
{ | ||
var lastError = Stdlib.GetLastError(); | ||
logger.WriteLineError($"kill(perfcollect, SIGINT) failed with {lastError}"); | ||
} | ||
|
||
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 | ||
} | ||
|
||
FileInfo traceFile = GetTraceFile(parameters, "trace.zip"); | ||
if (traceFile.Exists) | ||
{ | ||
benchmarkToTraceFile[parameters.BenchmarkCase] = traceFile; | ||
} | ||
} | ||
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 | ||
{ | ||
perfCollectProcess.Dispose(); | ||
} | ||
} | ||
|
||
private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters) | ||
{ | ||
string cliPath = parameters.BenchmarkCase.GetToolchain() switch | ||
{ | ||
CsProjCoreToolchain core => core.CustomDotNetCliPath, | ||
CoreRunToolchain coreRun => coreRun.CustomDotNetCliPath.FullName, | ||
NativeAotToolchain nativeAot => nativeAot.CustomDotNetCliPath, | ||
_ => DotNetCliCommandExecutor.DefaultDotNetCliPath.Value | ||
}; | ||
|
||
if (!cliPathWithSymbolsInstalled.Add(cliPath)) | ||
{ | ||
return; | ||
} | ||
|
||
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(); | ||
|
||
if (!missingSymbols.Any()) | ||
{ | ||
return; // the symbol files are already where we need 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"); | ||
DotNetCliCommand cliCommand = new ( | ||
cliPath: cliPath, | ||
arguments: $"tool install dotnet-symbol --tool-path \"{toolPath}\"", | ||
generateResult: null, | ||
logger: logger, | ||
buildPartition: null, | ||
environmentVariables: Array.Empty<EnvironmentVariable>(), | ||
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."); | ||
return; | ||
} | ||
|
||
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}\"")); | ||
} | ||
|
||
private FileInfo GetTraceFile(DiagnoserActionParameters parameters, string extension) | ||
=> new (ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, extension) | ||
.Replace(" ", "_")); // perfcollect does not allow for spaces in the trace file name | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/BenchmarkDotNet/Diagnosers/PerfCollectProfilerConfig.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System; | ||
|
||
namespace BenchmarkDotNet.Diagnosers | ||
{ | ||
public class PerfCollectProfilerConfig | ||
{ | ||
/// <param name="performExtraBenchmarksRun">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.</param> | ||
/// <param name="timeoutInSeconds">How long should we wait for the perfcollect script to finish processing the trace. 120s by default.</param> | ||
public PerfCollectProfilerConfig(bool performExtraBenchmarksRun = false, int timeoutInSeconds = 120) | ||
{ | ||
RunMode = performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; | ||
Timeout = TimeSpan.FromSeconds(timeoutInSeconds); | ||
} | ||
|
||
public TimeSpan Timeout { get; } | ||
|
||
public RunMode RunMode { get; } | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brianrob if I want to enable all events for given event source, is setting
COMPlus_EventSourceFilter
enough?The definition of event source that I care about:
BenchmarkDotNet/src/BenchmarkDotNet/Engines/EngineEventSource.cs
Lines 7 to 10 in 21a2940
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that and setting
COMPlus_EnableEventLog=1
. If you setCOMPlus_EventSourceFilter
to the EventSource that you want to capture, this will disable all other EventSources from being sent through to LTTng to avoid the cost of emitting all other EventSource events that may not be needed.