Skip to content

Make TraceCollector work on Linux #4706

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,10 @@ public static IReadOnlyDictionary<Run, ConcurrentDictionary<string, Microbenchma

foreach (var analyzer in analyzers)
{
List<GCProcessData> allPertinentProcesses = analyzer.Value.GetProcessGCData("dotnet");
List<GCProcessData> corerunProcesses = analyzer.Value.GetProcessGCData("corerun");
allPertinentProcesses.AddRange(corerunProcesses);
foreach (var benchmark in runsToResults[run.Value])
{
GCProcessData? benchmarkGCData = null;
foreach (var process in allPertinentProcesses)
{
string commandLine = process.CommandLine.Replace("\"", "").Replace("\\", "");
string runCleaned = benchmark.Key.Replace("\"", "").Replace("\\", "");
if (commandLine.Contains(runCleaned) && commandLine.Contains("--benchmarkName"))
{
benchmarkGCData = process;
break;
}
}

GCProcessData? benchmarkGCData = analyzer.Value.GetProcessGCData("MicroBenchmarks").FirstOrDefault();

if (benchmarkGCData != null)
{
int processID = benchmarkGCData.ProcessID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static (string, string) Build(MicrobenchmarkConfiguration configuration,
string filter = benchmark ?? configuration.MicrobenchmarkConfigurations.Filter;

// Base command: Add mandatory commands.
string command = $"run -f {frameworkVersion} --filter \"{filter}\" -c Release --noOverwrite --no-build";
string command = $"run -f {frameworkVersion} --filter \"{filter}\" -c Release --inProcess --noOverwrite --no-build";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that dotnet-trace only collect the first app(-- <command>) or specified process(--process-id <PID>), we start inProcess run for microbenchmarks


// [Optional] Add corerun.
if (!string.IsNullOrEmpty(run.Value.corerun))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
</PropertyGroup>
<PropertyGroup Condition="'$(IsWindows)'=='true'">
<DefineConstants>Windows</DefineConstants>
</PropertyGroup>

<Import Project="../Versions.props" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using Microsoft.Diagnostics.Utilities;
using System.Diagnostics;
using System.Net;

namespace GC.Infrastructure.Core.TraceCollection
Expand All @@ -19,12 +20,10 @@ public sealed class TraceCollector : IDisposable
{
private bool disposedValue;

private static readonly Lazy<WebClient> _client = new();

// TODO: Make this URL configurable.
private const string PERFVIEW_URL = "https://github.com/microsoft/perfview/releases/download/v3.0.0/PerfView.exe";
private static readonly string DependenciesFolder = "./dependencies";

public string Name { get; init; }

private readonly string ALWAYS_ARGS = @$" /AcceptEULA /NoGUI /Merge:true";
internal static readonly Dictionary<CollectType, string> WindowsCollectTypeMap = new()
{
{ CollectType.gc, "/GCCollectOnly" },
Expand All @@ -40,7 +39,14 @@ public sealed class TraceCollector : IDisposable
{
{ CollectType.gc, "gcCollectOnly" },
{ CollectType.cpu, "collect_cpu" },
{ CollectType.threadtime, "collect_threadTime" },
{ CollectType.threadtime, "collect_threadTime" }
};

internal static readonly Dictionary<CollectType, string> LinuxLocalRunCollectTypeMap = new()
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably rename:

internal static readonly Dictionary<CollectType, string> LinuxCollectTypeMap = new()
to LinuxServerRunCollectTypeMap.

{ CollectType.gc, "--clrevents gc" },
{ CollectType.cpu, "--clrevents gc+stack --clreventlevel informational" },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ CollectType.threadtime, "--clrevents gc --clreventlevel Verbose" },
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This configuration doesn't match with the threadtime based trace collection - let's get rid of this until we find a good enough alternative.


internal static readonly Dictionary<string, CollectType> StringToCollectTypeMap = new(StringComparer.OrdinalIgnoreCase)
Expand All @@ -55,54 +61,122 @@ public sealed class TraceCollector : IDisposable
{ "none", CollectType.none }
};

private readonly CollectType _collectType;
private readonly Process _traceProcess;
private readonly string arguments;
private readonly string _collectorPath;
#if Windows
private readonly Guid _sessionName;
private readonly Process _traceProcess;
private readonly CollectType _collectType;
#endif

public TraceCollector(string name, string collectType, string outputPath)
private static void InstallTraceCollector(string dependenciesFolder)
{
// Get Perfview if it doesn't exist.
if (!Directory.Exists("./dependencies"))
#if Windows
string perfviewPath = Path.Combine(DependenciesFolder, "Perfview.exe");
if (File.Exists(perfviewPath))
{
return;
}

// TODO: Make this URL configurable.
const string perfviewUrl = "https://github.com/microsoft/perfview/releases/download/v3.0.0/PerfView.exe";
using (HttpClient client = new())
{
HttpResponseMessage response = client.GetAsync(perfviewUrl).Result;
response.EnsureSuccessStatusCode();

using (FileStream writer = File.OpenWrite(perfviewPath))
{
response.Content.ReadAsStream().CopyTo(writer);
}
}
#else
string dotNetTracePath = Path.Combine(DependenciesFolder, "dotnet-trace");
if (File.Exists(dotNetTracePath))
{
return;
}

using (Process dotNetTraceInstaller = new())
{
Directory.CreateDirectory("./dependencies");
dotNetTraceInstaller.StartInfo.FileName = "dotnet";
dotNetTraceInstaller.StartInfo.Arguments = $"tool install dotnet-trace --tool-path {dependenciesFolder}";
dotNetTraceInstaller.StartInfo.UseShellExecute = false;
dotNetTraceInstaller.StartInfo.CreateNoWindow = true;
dotNetTraceInstaller.StartInfo.RedirectStandardError = true;
dotNetTraceInstaller.StartInfo.RedirectStandardOutput = true;
dotNetTraceInstaller.Start();
dotNetTraceInstaller.WaitForExit();
}
#endif
}

if (!File.Exists(Path.Combine("./dependencies", "PerfView.exe")))
public TraceCollector(string name, string collectType, string outputPath, int? pid = null)
{
if (!Directory.Exists(DependenciesFolder))
{
_client.Value.DownloadFile(PERFVIEW_URL, Path.Combine("./dependencies", "PerfView.exe"));
Directory.CreateDirectory(DependenciesFolder);
}

InstallTraceCollector(DependenciesFolder);

_collectType = StringToCollectTypeMap[collectType];

if (_collectType != CollectType.none)
if (_collectType == CollectType.none)
{
_sessionName = Guid.NewGuid();
foreach (var invalid in Path.GetInvalidPathChars())
{
name = name.Replace(invalid.ToString(), string.Empty);
}
return;
}

foreach (var invalid in Path.GetInvalidPathChars())
{
name = name.Replace(invalid.ToString(), string.Empty);
}

name = name.Replace("<", "");
name = name.Replace(">", "");
name = name.Replace("<", "");
name = name.Replace(">", "");

Name = Path.Combine(outputPath, $"{name}.etl");
#if Windows
_collectorPath = Path.Combine(DependenciesFolder, "Perfview.exe");
_sessionName = Guid.NewGuid();

arguments = $"{ALWAYS_ARGS} /sessionName:{_sessionName} {WindowsCollectTypeMap[_collectType]} /LogFile:{Path.Combine(outputPath, name)}.txt /DataFile:{Path.Combine(outputPath, $"{name}.etl")}";
string command = $"start {arguments}";
Name = Path.Combine(outputPath, $"{name}.etl");
string ALWAYS_ARGS = @$" /AcceptEULA /NoGUI /Merge:true";
arguments = $"{ALWAYS_ARGS} /sessionName:{_sessionName} {WindowsCollectTypeMap[_collectType]} /LogFile:{Path.Combine(outputPath, name)}.txt /DataFile:{Name}";
string command = $"start {arguments}";

_traceProcess = new();
_traceProcess.StartInfo.FileName = "./dependencies/PerfView.exe";
_traceProcess.StartInfo.Arguments = command;
_traceProcess.StartInfo.UseShellExecute = false;
_traceProcess.StartInfo.CreateNoWindow = true;
_traceProcess.StartInfo.RedirectStandardError = true;
_traceProcess.StartInfo.RedirectStandardOutput = true;
_traceProcess.Start();
_traceProcess = new();
_traceProcess.StartInfo.FileName = _collectorPath;
_traceProcess.StartInfo.Arguments = command;
_traceProcess.StartInfo.UseShellExecute = false;
_traceProcess.StartInfo.CreateNoWindow = true;
_traceProcess.StartInfo.RedirectStandardError = true;
_traceProcess.StartInfo.RedirectStandardOutput = true;
_traceProcess.Start();

// Give PerfView about a second to get started.
Thread.Sleep(1000);
// Give PerfView about a second to get started.
Thread.Sleep(1000);

#else
if (pid == null)
{
throw new Exception($"{nameof(TraceCollector)}: Must provide prcoess id in Linux case");
}

_collectorPath = Path.Combine(DependenciesFolder, "dotnet-trace");

Name = Path.Combine(outputPath, $"{name}.nettrace");
arguments = $"-p {pid} -o {Name} {LinuxLocalRunCollectTypeMap[_collectType]}";
string command = $"collect {arguments}";

_traceProcess = new();
_traceProcess.StartInfo.FileName = _collectorPath;
_traceProcess.StartInfo.Arguments = command;
_traceProcess.StartInfo.UseShellExecute = false;
_traceProcess.StartInfo.CreateNoWindow = true;
_traceProcess.StartInfo.RedirectStandardError = true;
_traceProcess.StartInfo.RedirectStandardOutput = true;
_traceProcess.Start();
#endif
}

private void Dispose(bool disposing)
Expand All @@ -115,11 +189,12 @@ private void Dispose(bool disposing)

// TODO: Parameterize the wait for exit time.

#if Windows
if (!disposedValue)
{
using (Process stopProcess = new())
{
stopProcess.StartInfo.FileName = Path.Combine("./dependencies", "PerfView.exe");
stopProcess.StartInfo.FileName = _collectorPath;
string command = $"stop {arguments}";
stopProcess.StartInfo.Arguments = command;
stopProcess.StartInfo.UseShellExecute = false;
Expand Down Expand Up @@ -160,9 +235,15 @@ private void Dispose(bool disposing)

disposedValue = true;
}
#else
if (!disposedValue)
{
_traceProcess.WaitForExit();
_traceProcess.Dispose();
}
#endif
}

public string Name { get; init; }

~TraceCollector()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,36 @@ internal static Dictionary<string, ProcessExecutionDetails> ExecuteLocally(GCPer

string key = $"{runInfo.RunDetails.Key}.{runInfo.CorerunDetails.Key}.{iterationIdx}";
string traceName = $"{runInfo.RunDetails.Key}.{runInfo.CorerunDetails.Key}.{iterationIdx}";
using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, outputPath))

if (OperatingSystem.IsWindows())
{
using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, outputPath))
{
gcperfsimProcess.Start();
output = gcperfsimProcess.StandardOutput.ReadToEnd();
error = gcperfsimProcess.StandardError.ReadToEnd();
gcperfsimProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000);
File.WriteAllText(Path.Combine(outputPath, key + ".txt"), "Standard Out: \n" + output + "\n Standard Error: \n" + error);
}
}
else
{
gcperfsimProcess.Start();
output = gcperfsimProcess.StandardOutput.ReadToEnd();
error = gcperfsimProcess.StandardError.ReadToEnd();
gcperfsimProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000);
File.WriteAllText(Path.Combine(outputPath, key + ".txt"), "Standard Out: \n" + output + "\n Standard Error: \n" + error);
using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, outputPath, gcperfsimProcess.Id))
{
output = gcperfsimProcess.StandardOutput.ReadToEnd();
error = gcperfsimProcess.StandardError.ReadToEnd();
gcperfsimProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000);
File.WriteAllText(Path.Combine(outputPath, key + ".txt"), "Standard Out: \n" + output + "\n Standard Error: \n" + error);
}
}



// If a trace is requested, ensure the file exists. If not, there is was an error and alert the user.
if (configuration.TraceConfigurations?.Type != "none")
{
// Not checking Linux here since the local run only allows for Windows.
if (!File.Exists(Path.Combine(outputPath, traceName + ".etl.zip")))
// On Windows, the trace file path ends with ".etl.zip"; On Linux, it ends with ".nettrace".
if (!File.Exists(Path.Combine(outputPath, traceName + ".etl.zip")) && !File.Exists(Path.Combine(outputPath, traceName + ".nettrace")))
{
AnsiConsole.MarkupLine($"[yellow bold] ({DateTime.Now}) The trace for the run wasn't successfully captured. Please check the log file for more details: {Markup.Escape(output)} Full run details: {Path.GetFileNameWithoutExtension(configuration.Name)}: {runInfo.CorerunDetails.Key} for {runInfo.RunDetails.Key} [/]");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,36 @@ public static MicrobenchmarkOutputResults RunMicrobenchmarks(MicrobenchmarkConfi
};

string traceName = $"{benchmarkCleanedName}_{index}";
using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, runPath))
if (OperatingSystem.IsWindows())
{
using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, runPath))
{
bdnProcess.Start();
bdnProcess.BeginOutputReadLine();
bdnProcess.BeginErrorReadLine();
bdnProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000);
}
}
else
{
Process microBenchmarksProcess = new();
bdnProcess.Start();
bdnProcess.BeginOutputReadLine();
bdnProcess.BeginErrorReadLine();
bdnProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000);
// wait for Microbenchmark to start
while (true)
{
Process[] microBenchmarksProcessList = Process.GetProcessesByName("MicroBenchmarks");
if (microBenchmarksProcessList.Count() != 0)
{
microBenchmarksProcess = microBenchmarksProcessList.First();
break;
}
}
using (TraceCollector traceCollector = new TraceCollector(traceName, collectType, runPath, microBenchmarksProcess.Id))
{
bdnProcess.BeginOutputReadLine();
bdnProcess.BeginErrorReadLine();
bdnProcess.WaitForExit((int)configuration.Environment.default_max_seconds * 1000);
}
}

string processDetailsKey = $"{run.Key}_{benchmark}_{index}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal static void Main(string[] args)

if (OperatingSystem.IsWindows())
{
bool IsAdministrator = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
if (!IsAdministrator)
{
AnsiConsole.WriteLine("Not running in admin mode - please elevate privileges to run this process.");
Expand Down Expand Up @@ -79,9 +80,5 @@ internal static void Main(string[] args)
}
}
}

[SupportedOSPlatform("windows")]
internal static bool IsAdministrator =>
new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
}
}
Loading