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 all 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 @@ -104,7 +104,11 @@ public static (string, string) BuildForServer(GCPerfSimConfiguration configurati
if (configuration.TraceConfigurations != null && !string.Equals(configuration.TraceConfigurations.Type, "none", StringComparison.OrdinalIgnoreCase))
{
CollectType collectType = TraceCollector.StringToCollectTypeMap[configuration.TraceConfigurations.Type];
string collectionCommand = os == OS.Windows ? TraceCollector.WindowsCollectTypeMap[collectType] : TraceCollector.LinuxCollectTypeMap[collectType];
if (os == OS.Linux && !TraceCollector.LinuxServerRunCollectTypeMap.Keys.Contains(collectType))
{
throw new Exception($"{nameof(GCPerfSimCommandBuilder)}: Trace collect type {configuration.TraceConfigurations.Type} is not supported for GCPerfsim Linux server run.");
}
string collectionCommand = os == OS.Windows ? TraceCollector.WindowsCollectTypeMap[collectType] : TraceCollector.LinuxServerRunCollectTypeMap[collectType];

collectionCommand = collectionCommand.Replace(" ", ";").Replace("/", "");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
</PropertyGroup>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
</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 @@ -36,11 +35,18 @@ public sealed class TraceCollector : IDisposable
{ CollectType.join, " /BufferSizeMB:4096 /CircularMB:4096 /KernelEvents:Process+Thread+ImageLoad /ClrEvents:GC+Threading /ClrEventLevel=Verbose " },
};

internal static readonly Dictionary<CollectType, string> LinuxCollectTypeMap = new()
internal static readonly Dictionary<CollectType, string> LinuxServerRunCollectTypeMap = new()
{
{ 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, "--profile gc-collect" },
{ CollectType.cpu, "" },
{ CollectType.verbose, "--clrevents gc+stack --clreventlevel verbose" }
};

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

private readonly CollectType _collectType;
private readonly Process _traceProcess;
private readonly string arguments;
private readonly string _collectorPath;
private readonly Guid _sessionName;
private readonly Process _traceProcess;
private readonly CollectType _collectType;

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

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

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

using (Process dotNetTraceInstaller = new())
{
dotNetTraceInstaller.StartInfo.FileName = "dotnet";
dotNetTraceInstaller.StartInfo.Arguments = $"tool install dotnet-trace --tool-path {dependenciesFolder}";
dotNetTraceInstaller.StartInfo.UseShellExecute = false;
dotNetTraceInstaller.StartInfo.CreateNoWindow = true;
dotNetTraceInstaller.StartInfo.RedirectStandardError = true;
dotNetTraceInstaller.StartInfo.RedirectStandardOutput = true;
dotNetTraceInstaller.Start();
Console.WriteLine("install dotnet-trace");
dotNetTraceInstaller.WaitForExit();
}
}
}

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

InstallTraceCollector(DependenciesFolder);

_collectType = StringToCollectTypeMap[collectType];

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

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

Name = Path.Combine(outputPath, $"{name}.etl");
name = name.Replace("<", "");
name = name.Replace(">", "");

if (OperatingSystem.IsWindows())
{
_collectorPath = Path.Combine(DependenciesFolder, "Perfview.exe");
_sessionName = Guid.NewGuid();

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

_traceProcess = new();
_traceProcess.StartInfo.FileName = "./dependencies/PerfView.exe";
_traceProcess.StartInfo.FileName = _collectorPath;
_traceProcess.StartInfo.Arguments = command;
_traceProcess.StartInfo.UseShellExecute = false;
_traceProcess.StartInfo.CreateNoWindow = true;
Expand All @@ -103,6 +159,34 @@ public TraceCollector(string name, string collectType, string outputPath)
// Give PerfView about a second to get started.
Thread.Sleep(1000);
}

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

if (_collectType != CollectType.none && !LinuxLocalRunCollectTypeMap.Keys.Contains(_collectType))
{
throw new Exception($"{nameof(TraceCollector)}: Trace collect type {collectType} is not supported for Linux local run.");
}

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

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

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

private void Dispose(bool disposing)
Expand All @@ -114,55 +198,65 @@ private void Dispose(bool disposing)
}

// TODO: Parameterize the wait for exit time.

if (!disposedValue)
if (OperatingSystem.IsWindows())
{
using (Process stopProcess = new())
if (!disposedValue)
{
stopProcess.StartInfo.FileName = Path.Combine("./dependencies", "PerfView.exe");
string command = $"stop {arguments}";
stopProcess.StartInfo.Arguments = command;
stopProcess.StartInfo.UseShellExecute = false;
stopProcess.StartInfo.CreateNoWindow = true;
stopProcess.StartInfo.RedirectStandardInput = true;
stopProcess.StartInfo.RedirectStandardError = true;
stopProcess.Start();
stopProcess.WaitForExit(200_000);
_traceProcess?.Dispose();
}
using (Process stopProcess = new())
{
stopProcess.StartInfo.FileName = _collectorPath;
string command = $"stop {arguments}";
stopProcess.StartInfo.Arguments = command;
stopProcess.StartInfo.UseShellExecute = false;
stopProcess.StartInfo.CreateNoWindow = true;
stopProcess.StartInfo.RedirectStandardInput = true;
stopProcess.StartInfo.RedirectStandardError = true;
stopProcess.Start();
stopProcess.WaitForExit(200_000);
_traceProcess?.Dispose();
}

// Clean up any dangling ETW sessions for both the Kernel and the session.
using (Process stopLogmanKernelProcess = new())
{
stopLogmanKernelProcess.StartInfo.FileName = "logman";
string etsStopCommand = $"-ets stop {_sessionName}Kernel";
stopLogmanKernelProcess.StartInfo.Arguments = etsStopCommand;
stopLogmanKernelProcess.StartInfo.UseShellExecute = false;
stopLogmanKernelProcess.StartInfo.RedirectStandardOutput = false;
stopLogmanKernelProcess.StartInfo.RedirectStandardError = false;
stopLogmanKernelProcess.StartInfo.CreateNoWindow = true;
stopLogmanKernelProcess.Start();
stopLogmanKernelProcess.WaitForExit(5_000);
// Clean up any dangling ETW sessions for both the Kernel and the session.
using (Process stopLogmanKernelProcess = new())
{
stopLogmanKernelProcess.StartInfo.FileName = "logman";
string etsStopCommand = $"-ets stop {_sessionName}Kernel";
stopLogmanKernelProcess.StartInfo.Arguments = etsStopCommand;
stopLogmanKernelProcess.StartInfo.UseShellExecute = false;
stopLogmanKernelProcess.StartInfo.RedirectStandardOutput = false;
stopLogmanKernelProcess.StartInfo.RedirectStandardError = false;
stopLogmanKernelProcess.StartInfo.CreateNoWindow = true;
stopLogmanKernelProcess.Start();
stopLogmanKernelProcess.WaitForExit(5_000);
}

using (Process stopLogmanProcess = new())
{
stopLogmanProcess.StartInfo.FileName = "logman";
string etsStopCommand = $"-ets stop {_sessionName}";
stopLogmanProcess.StartInfo.Arguments = etsStopCommand;
stopLogmanProcess.StartInfo.UseShellExecute = false;
stopLogmanProcess.StartInfo.RedirectStandardOutput = false;
stopLogmanProcess.StartInfo.RedirectStandardError = false;
stopLogmanProcess.StartInfo.CreateNoWindow = true;
stopLogmanProcess.Start();
stopLogmanProcess.WaitForExit(5_000);
}

disposedValue = true;
}
}

using (Process stopLogmanProcess = new())
if (OperatingSystem.IsLinux())
{
if (!disposedValue)
{
stopLogmanProcess.StartInfo.FileName = "logman";
string etsStopCommand = $"-ets stop {_sessionName}";
stopLogmanProcess.StartInfo.Arguments = etsStopCommand;
stopLogmanProcess.StartInfo.UseShellExecute = false;
stopLogmanProcess.StartInfo.RedirectStandardOutput = false;
stopLogmanProcess.StartInfo.RedirectStandardError = false;
stopLogmanProcess.StartInfo.CreateNoWindow = true;
stopLogmanProcess.Start();
stopLogmanProcess.WaitForExit(5_000);
_traceProcess.WaitForExit();
_traceProcess.Dispose();
}

disposedValue = true;
}
}

public string Name { get; init; }

~TraceCollector()
{
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
Loading
Loading