private string GetDefaultAddress()
{
- try
- {
- Process process = Process.GetProcessById(_pid);
- }
- catch (ArgumentException)
- {
- throw new ServerNotAvailableException($"Process {_pid} is not running.");
- }
- catch (InvalidOperationException)
- {
- throw new ServerNotAvailableException($"Process {_pid} seems to be elevated.");
- }
-
- if (!TryGetDefaultAddress(_pid, out string transportName))
- {
- throw new ServerNotAvailableException($"Process {_pid} not running compatible .NET runtime.");
- }
-
- return transportName;
+ return GetDefaultAddress(_pid);
}
private static bool TryGetDefaultAddress(int pid, out string defaultAddress)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
defaultAddress = $"dotnet-diagnostic-{pid}";
+
+ try
+ {
+ string dsrouterAddress = Directory.GetFiles(IpcRootPath, $"dotnet-diagnostic-dsrouter-{pid}").FirstOrDefault();
+ if (!string.IsNullOrEmpty(dsrouterAddress))
+ {
+ defaultAddress = dsrouterAddress;
+ }
+ }
+ catch { }
}
else
{
defaultAddress = Directory.GetFiles(IpcRootPath, $"dotnet-diagnostic-{pid}-*-socket") // Try best match.
.OrderByDescending(f => new FileInfo(f).LastWriteTime)
.FirstOrDefault();
+
+ string dsrouterAddress = Directory.GetFiles(IpcRootPath, $"dotnet-diagnostic-dsrouter-{pid}-*-socket") // Try best match.
+ .OrderByDescending(f => new FileInfo(f).LastWriteTime)
+ .FirstOrDefault();
+
+ if (!string.IsNullOrEmpty(dsrouterAddress) && !string.IsNullOrEmpty(defaultAddress))
+ {
+ FileInfo defaultFile = new(defaultAddress);
+ FileInfo dsrouterFile = new(dsrouterAddress);
+
+ if (dsrouterFile.LastWriteTime >= defaultFile.LastWriteTime)
+ {
+ defaultAddress = dsrouterAddress;
+ }
+ }
}
- catch (InvalidOperationException)
- {
- }
+ catch { }
}
return !string.IsNullOrEmpty(defaultAddress);
}
+ public static string GetDefaultAddress(int pid)
+ {
+ try
+ {
+ Process process = Process.GetProcessById(pid);
+ }
+ catch (ArgumentException)
+ {
+ throw new ServerNotAvailableException($"Process {pid} is not running.");
+ }
+ catch (InvalidOperationException)
+ {
+ throw new ServerNotAvailableException($"Process {pid} seems to be elevated.");
+ }
+
+ if (!TryGetDefaultAddress(pid, out string defaultAddress))
+ {
+ throw new ServerNotAvailableException($"Process {pid} not running compatible .NET runtime.");
+ }
+
+ return defaultAddress;
+ }
+
+ public static bool IsDefaultAddressDSRouter(int pid, string address)
+ {
+ if (address.StartsWith(IpcRootPath, StringComparison.OrdinalIgnoreCase))
+ {
+ address = address.Substring(IpcRootPath.Length);
+ }
+
+ string dsrouterAddress = $"dotnet-diagnostic-dsrouter-{pid}";
+ return address.StartsWith(dsrouterAddress, StringComparison.OrdinalIgnoreCase);
+ }
+
public override bool Equals(object obj)
{
return Equals(obj as PidIpcEndpoint);
routerFactory.Logger?.LogInformation("Starting automatic shutdown.");
throw;
}
+
+ routerFactory.Logger?.LogTrace($"runRouter continues after exception: {ex.Message}");
}
}
}
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks Condition="'$(DotNetBuildFromSource)' != 'true'">netstandard2.0;net6.0</TargetFrameworks>
</ItemGroup>
<ItemGroup>
+ <InternalsVisibleTo Include="dotnet-gcdump" />
<InternalsVisibleTo Include="dotnet-counters" />
<InternalsVisibleTo Include="dotnet-dsrouter" />
<InternalsVisibleTo Include="dotnet-monitor" />
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.Diagnostics;
-
+using System.Collections.Generic;
using Microsoft.Diagnostics.NETCore.Client;
namespace Microsoft.Internal.Common.Utils
public static bool ValidateArgumentsForAttach(int processId, string name, string port, out int resolvedProcessId)
{
resolvedProcessId = -1;
- if (processId == 0 && name == null && string.IsNullOrEmpty(port))
+ if (processId == 0 && string.IsNullOrEmpty(name) && string.IsNullOrEmpty(port))
{
Console.WriteLine("Must specify either --process-id, --name, or --diagnostic-port.");
return false;
Console.WriteLine($"{processId} is not a valid process ID");
return false;
}
- else if (processId != 0 && name != null && !string.IsNullOrEmpty(port))
+ else if (processId != 0 && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(port))
{
Console.WriteLine("Only one of the --name, --process-id, or --diagnostic-port options may be specified.");
return false;
}
- else if (processId != 0 && name != null)
+ else if (processId != 0 && !string.IsNullOrEmpty(name))
{
- Console.WriteLine("Can only one of specify --name or --process-id.");
+ Console.WriteLine("Only one of the --name or --process-id options may be specified.");
return false;
}
else if (processId != 0 && !string.IsNullOrEmpty(port))
{
- Console.WriteLine("Can only one of specify --process-id or --diagnostic-port.");
+ Console.WriteLine("Only one of the --process-id or --diagnostic-port options may be specified.");
return false;
}
- else if (name != null && !string.IsNullOrEmpty(port))
+ else if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(port))
{
- Console.WriteLine("Can only one of specify --name or --diagnostic-port.");
+ Console.WriteLine("Only one of the --name or --diagnostic-port options may be specified.");
return false;
}
// If we got this far it means only one of --name/--diagnostic-port/--process-id was specified
return true;
}
// Resolve name option
- else if (name != null)
+ else if (!string.IsNullOrEmpty(name))
{
processId = CommandUtils.FindProcessIdWithName(name);
if (processId < 0)
{
return (int)ReturnCode.ArgumentError;
}
-
ct.Register(() => _shouldExit.TrySetResult((int)ReturnCode.Ok));
DiagnosticsClientBuilder builder = new("dotnet-counters", 10);
private static Option DiagnosticPortOption() =>
new(
- alias: "--diagnostic-port",
+ aliases: new[] { "--dport", "--diagnostic-port" },
description: "The path to diagnostic port to be used.")
{
Argument = new Argument<string>(name: "diagnosticPort", getDefaultValue: () => "")
ILogger logger = loggerFactory.CreateLogger("dotnet-dsrouter");
+ logger.LogInformation($"Starting dotnet-dsrouter using pid={Process.GetCurrentProcess().Id}");
+
Task<int> routerTask = createRouterTask(logger, Launcher, linkedCancelToken);
while (!linkedCancelToken.IsCancellationRequested)
}
}
}
- return routerTask.Result;
+
+ if (!routerTask.IsCompleted)
+ {
+ cancelRouterTask.Cancel();
+ }
+
+ await Task.WhenAny(routerTask, Task.Delay(1000, CancellationToken.None)).ConfigureAwait(false);
+ if (routerTask.IsCompleted)
+ {
+ return routerTask.Result;
+ }
+
+ return 0;
}
}
private static string GetDefaultIpcServerPath(ILogger logger)
{
+ string path = string.Empty;
int processId = Process.GetCurrentProcess().Id;
+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- string path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}");
- if (File.Exists(path))
- {
- logger?.LogWarning($"Default IPC server path, {path}, already in use. To disable default diagnostics for dotnet-dsrouter, set DOTNET_EnableDiagnostics=0 and re-run.");
-
- path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-dsrouter-{processId}");
- logger?.LogWarning($"Fallback using none default IPC server path, {path}.");
- }
-
- return path.Substring(PidIpcEndpoint.IpcRootPath.Length);
+ path = $"dotnet-diagnostic-dsrouter-{processId}";
}
else
{
unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
#endif
TimeSpan diff = Process.GetCurrentProcess().StartTime.ToUniversalTime() - unixEpoch;
-
- string path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-{(long)diff.TotalSeconds}-socket");
- if (Directory.GetFiles(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-*-socket").Length != 0)
- {
- logger?.LogWarning($"Default IPC server path, {Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-*-socket")}, already in use. To disable default diagnostics for dotnet-dsrouter, set DOTNET_EnableDiagnostics=0 and re-run.");
-
- path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-dsrouter-{processId}-{(long)diff.TotalSeconds}-socket");
- logger?.LogWarning($"Fallback using none default IPC server path, {path}.");
- }
-
- return path;
+ path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-dsrouter-{processId}-{(long)diff.TotalSeconds}-socket");
}
+ logger?.LogDebug($"Using default IPC server path, {path}.");
+ logger?.LogDebug($"Attach to default dotnet-dsrouter IPC server using --process-id {processId} diagnostic tooling argument.");
+
+ return path;
}
private static TcpClientRouterFactory.CreateInstanceDelegate ChooseTcpClientRouterFactory(string forwardPort, ILogger logger)
{
if (_deviceConnectionID == 0)
{
+ _logger.LogError($"Failed to connect device over USB, no device currently connected.");
throw new Exception($"Failed to connect device over USB, no device currently connected.");
}
if (result != 0)
{
+ _logger?.LogError($"Failed USBMuxConnectByPort: device = {_deviceConnectionID}, port = {_port}, result = {result}.");
throw new Exception($"Failed to connect device over USB using connection {_deviceConnectionID} and port {_port}.");
}
using System.Threading;
using System.Threading.Tasks;
using Graphs;
+using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Internal.Common.Utils;
using Microsoft.Tools.Common;
{
internal static class CollectCommandHandler
{
- private delegate Task<int> CollectDelegate(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name);
+ private delegate Task<int> CollectDelegate(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort);
/// <summary>
/// Collects a gcdump from a currently running process.
/// <param name="console"></param>
/// <param name="processId">The process to collect the gcdump from.</param>
/// <param name="output">The output path for the collected gcdump.</param>
+ /// <param name="timeout">The timeout for the collected gcdump.</param>
+ /// <param name="verbose">Enable verbose logging.</param>
+ /// <param name="name">The process name to collect the gcdump from.</param>
+ /// <param name="diagnosticPort">The diagnostic IPC channel to collect the gcdump from.</param>
/// <returns></returns>
- private static async Task<int> Collect(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name)
+ private static async Task<int> Collect(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort)
{
- if (name != null)
+ if (!CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out int resolvedProcessId))
{
- if (processId != 0)
- {
- Console.WriteLine("Can only specify either --name or --process-id option.");
- return -1;
- }
- processId = CommandUtils.FindProcessIdWithName(name);
- if (processId < 0)
- {
- return -1;
- }
+ return -1;
}
- try
+ processId = resolvedProcessId;
+
+ if (!string.IsNullOrEmpty(diagnosticPort))
{
- if (processId < 0)
+ try
{
- Console.Out.WriteLine($"The PID cannot be negative: {processId}");
- return -1;
+ IpcEndpointConfig config = IpcEndpointConfig.Parse(diagnosticPort);
+ if (!config.IsConnectConfig)
+ {
+ Console.Error.WriteLine("--diagnostic-port is only supporting connect mode.");
+ return -1;
+ }
}
-
- if (processId == 0)
+ catch (Exception ex)
{
- Console.Out.WriteLine("-p|--process-id is required");
+ Console.Error.WriteLine($"--diagnostic-port argument error: {ex.Message}");
return -1;
}
+ processId = 0;
+ }
+
+ try
+ {
output = string.IsNullOrEmpty(output)
? $"{DateTime.Now:yyyyMMdd\\_HHmmss}_{processId}.gcdump"
: output;
Console.Out.WriteLine($"Writing gcdump to '{outputFileInfo.FullName}'...");
Task<bool> dumpTask = Task.Run(() => {
- if (TryCollectMemoryGraph(ct, processId, timeout, verbose, out MemoryGraph memoryGraph))
+ if (TryCollectMemoryGraph(ct, processId, diagnosticPort, timeout, verbose, out MemoryGraph memoryGraph))
{
GCHeapDump.WriteMemoryGraph(memoryGraph, outputFileInfo.FullName, "dotnet-gcdump");
return true;
}
}
- internal static bool TryCollectMemoryGraph(CancellationToken ct, int processId, int timeout, bool verbose,
- out MemoryGraph memoryGraph)
+ internal static bool TryCollectMemoryGraph(CancellationToken ct, int processId, string diagnosticPort, int timeout, bool verbose, out MemoryGraph memoryGraph)
{
DotNetHeapInfo heapInfo = new();
TextWriter log = verbose ? Console.Out : TextWriter.Null;
memoryGraph = new MemoryGraph(50_000);
- if (!EventPipeDotNetHeapDumper.DumpFromEventPipe(ct, processId, memoryGraph, log, timeout, heapInfo))
+ if (!EventPipeDotNetHeapDumper.DumpFromEventPipe(ct, processId, diagnosticPort, memoryGraph, log, timeout, heapInfo))
{
return false;
}
// Handler
HandlerDescriptor.FromDelegate((CollectDelegate) Collect).GetCommandHandler(),
// Options
- ProcessIdOption(), OutputPathOption(), VerboseOption(), TimeoutOption(), NameOption()
+ ProcessIdOption(),
+ OutputPathOption(),
+ VerboseOption(),
+ TimeoutOption(),
+ NameOption(),
+ DiagnosticPortOption()
};
- private static Option ProcessIdOption() =>
+ private static Option<int> ProcessIdOption() =>
new(
aliases: new[] { "-p", "--process-id" },
description: "The process id to collect the gcdump from.")
Argument = new Argument<int>(name: "pid"),
};
- private static Option NameOption() =>
+ private static Option<string> NameOption() =>
new(
aliases: new[] { "-n", "--name" },
description: "The name of the process to collect the gcdump from.")
Argument = new Argument<string>(name: "name")
};
- private static Option OutputPathOption() =>
+ private static Option<string> OutputPathOption() =>
new(
aliases: new[] { "-o", "--output" },
description: $@"The path where collected gcdumps should be written. Defaults to '.\YYYYMMDD_HHMMSS_<pid>.gcdump' where YYYYMMDD is Year/Month/Day and HHMMSS is Hour/Minute/Second. Otherwise, it is the full path and file name of the dump.")
Argument = new Argument<string>(name: "gcdump-file-path", getDefaultValue: () => string.Empty)
};
- private static Option VerboseOption() =>
+ private static Option<bool> VerboseOption() =>
new(
aliases: new[] { "-v", "--verbose" },
description: "Output the log while collecting the gcdump.")
};
public static int DefaultTimeout = 30;
- private static Option TimeoutOption() =>
+ private static Option<int> TimeoutOption() =>
new(
aliases: new[] { "-t", "--timeout" },
description: $"Give up on collecting the gcdump if it takes longer than this many seconds. The default value is {DefaultTimeout}s.")
{
Argument = new Argument<int>(name: "timeout", getDefaultValue: () => DefaultTimeout)
};
+
+ private static Option<string> DiagnosticPortOption() =>
+ new(
+ aliases: new[] { "--dport", "--diagnostic-port" },
+ description: "The path to a diagnostic port to collect the dump from.")
+ {
+ Argument = new Argument<string>(name: "diagnostic-port", getDefaultValue: () => string.Empty)
+ };
}
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.CommandLine;
+using System.IO;
+using Graphs;
+using Microsoft.Tools.Common;
+
+namespace Microsoft.Diagnostics.Tools.GCDump
+{
+ internal static class ConvertCommandHandler
+ {
+ public static int ConvertFile(FileInfo input, string output, bool verbose)
+ {
+ if (!input.Exists)
+ {
+ Console.Error.WriteLine($"File '{input.FullName}' does not exist.");
+ return -1;
+ }
+
+ output = string.IsNullOrEmpty(output)
+ ? Path.ChangeExtension(input.FullName, "gcdump")
+ : output;
+
+ FileInfo outputFileInfo = new(output);
+
+ if (outputFileInfo.Exists)
+ {
+ outputFileInfo.Delete();
+ }
+
+ if (string.IsNullOrEmpty(outputFileInfo.Extension) || outputFileInfo.Extension != ".gcdump")
+ {
+ outputFileInfo = new FileInfo(outputFileInfo.FullName + ".gcdump");
+ }
+
+ Console.Out.WriteLine($"Writing gcdump to '{outputFileInfo.FullName}'...");
+
+ DotNetHeapInfo heapInfo = new();
+ TextWriter log = verbose ? Console.Out : TextWriter.Null;
+
+ MemoryGraph memoryGraph = new(50_000);
+
+ if (!EventPipeDotNetHeapDumper.DumpFromEventPipeFile(input.FullName, memoryGraph, log, heapInfo))
+ {
+ return -1;
+ }
+
+ memoryGraph.AllowReading();
+ GCHeapDump.WriteMemoryGraph(memoryGraph, outputFileInfo.FullName, "dotnet-gcdump");
+
+ return 0;
+ }
+
+ public static System.CommandLine.Command ConvertCommand() =>
+ new(
+ name: "convert",
+ description: "Converts nettrace file into .gcdump file handled by analysis tools. Can only convert from the nettrace format.")
+ {
+ // Handler
+ System.CommandLine.Invocation.CommandHandler.Create<FileInfo, string, bool>(ConvertFile),
+ // Arguments and Options
+ InputPathArgument(),
+ OutputPathOption(),
+ VerboseOption()
+ };
+
+ private static Argument<FileInfo> InputPathArgument() =>
+ new Argument<FileInfo>("input")
+ {
+ Description = "Input trace file to be converted.",
+ Arity = new ArgumentArity(0, 1)
+ }.ExistingOnly();
+
+ private static Option<string> OutputPathOption() =>
+ new(
+ aliases: new[] { "-o", "--output" },
+ description: $@"The path where converted gcdump should be written. Defaults to '<input>.gcdump'")
+ {
+ Argument = new Argument<string>(name: "output", getDefaultValue: () => string.Empty)
+ };
+
+ private static Option<bool> VerboseOption() =>
+ new(
+ aliases: new[] { "-v", "--verbose" },
+ description: "Output the log while converting the gcdump.")
+ {
+ Argument = new Argument<bool>(name: "verbose", getDefaultValue: () => false)
+ };
+ }
+}
using System.Threading.Tasks;
using Microsoft.Diagnostics.Tools.GCDump.CommandLine;
using Microsoft.Tools.Common;
+using Microsoft.Internal.Common.Utils;
+using Microsoft.Diagnostics.NETCore.Client;
namespace Microsoft.Diagnostics.Tools.GCDump
{
internal static class ReportCommandHandler
{
- private delegate Task<int> ReportDelegate(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType reportType = ReportType.HeapStat);
+ private delegate Task<int> ReportDelegate(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType reportType = ReportType.HeapStat, string diagnosticPort = null);
public static Command ReportCommand() =>
new(
// Handler
HandlerDescriptor.FromDelegate((ReportDelegate) Report).GetCommandHandler(),
// Options
- FileNameArgument(), ProcessIdOption(), ReportTypeOption()
+ FileNameArgument(),
+ ProcessIdOption(),
+ ReportTypeOption(),
+ DiagnosticPortOption(),
};
- private static Task<int> Report(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType type = ReportType.HeapStat)
+ private static Task<int> Report(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType type = ReportType.HeapStat, string diagnosticPort = null)
{
//
// Validation
//
- if (gcdump_filename == null && !processId.HasValue)
+ if (gcdump_filename == null && !processId.HasValue && string.IsNullOrEmpty(diagnosticPort))
{
- Console.Error.WriteLine("<gcdump_filename> or -p|--process-id is required");
+ Console.Error.WriteLine("<gcdump_filename> or -p|--process-id or --dport|--diagnostic-port is required");
return Task.FromResult(-1);
}
- if (gcdump_filename != null && processId.HasValue)
+ if (gcdump_filename != null && (processId.HasValue || !string.IsNullOrEmpty(diagnosticPort)))
{
- Console.Error.WriteLine("Specify only one of -f|--file or -p|--process-id.");
+ Console.Error.WriteLine("Specify only one of -f|--file or -p|--process-id or --dport|--diagnostic-port.");
+ return Task.FromResult(-1);
+ }
+
+ if (processId.HasValue && !string.IsNullOrEmpty(diagnosticPort))
+ {
+ Console.Error.WriteLine("Specify only one of -p|--process-id or -dport|--diagnostic-port.");
return Task.FromResult(-1);
}
{
source = ReportSource.DumpFile;
}
- else if (processId.HasValue)
+ else if (processId.HasValue || !string.IsNullOrEmpty(diagnosticPort))
{
source = ReportSource.Process;
}
return (source, type) switch
{
- (ReportSource.Process, ReportType.HeapStat) => ReportFromProcess(processId.Value, ct),
+ (ReportSource.Process, ReportType.HeapStat) => ReportFromProcess(processId ?? 0, diagnosticPort, ct),
(ReportSource.DumpFile, ReportType.HeapStat) => ReportFromFile(gcdump_filename),
_ => HandleUnknownParam()
};
return Task.FromResult(-1);
}
- private static Task<int> ReportFromProcess(int processId, CancellationToken ct)
+ private static Task<int> ReportFromProcess(int processId, string diagnosticPort, CancellationToken ct)
{
+ if (!CommandUtils.ValidateArgumentsForAttach(processId, string.Empty, diagnosticPort, out int resolvedProcessId))
+ {
+ return Task.FromResult(-1);
+ }
+
+ processId = resolvedProcessId;
+
+ if (!string.IsNullOrEmpty(diagnosticPort))
+ {
+ try
+ {
+ IpcEndpointConfig config = IpcEndpointConfig.Parse(diagnosticPort);
+ if (!config.IsConnectConfig)
+ {
+ Console.Error.WriteLine("--diagnostic-port is only supporting connect mode.");
+ return Task.FromResult(-1);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"--diagnostic-port argument error: {ex.Message}");
+ return Task.FromResult(-1);
+ }
+
+ processId = 0;
+ }
+
if (!CollectCommandHandler
- .TryCollectMemoryGraph(ct, processId, CollectCommandHandler.DefaultTimeout, false, out Graphs.MemoryGraph mg))
+ .TryCollectMemoryGraph(ct, processId, diagnosticPort, CollectCommandHandler.DefaultTimeout, false, out Graphs.MemoryGraph mg))
{
Console.Error.WriteLine("An error occured while collecting gcdump.");
return Task.FromResult(-1);
}.ExistingOnly();
private static Option<int> ProcessIdOption() =>
- new(new[] { "-p", "--process-id" }, "The process id to collect the gcdump from.");
+ new(
+ aliases: new[] { "-p", "--process-id" },
+ description: "The process id to collect the gcdump from.")
+ {
+ Argument = new Argument<int>(name: "pid"),
+ };
private static Option<ReportType> ReportTypeOption() =>
- new(new[] { "-t", "--report-type" }, "The type of report to generate. Available options: heapstat (default)")
+ new(
+ aliases: new[] { "-t", "--report-type" },
+ description: "The type of report to generate. Available options: heapstat (default)")
+ {
+ Argument = new Argument<ReportType>(name: "report-type", () => ReportType.HeapStat)
+ };
+
+ private static Option<string> DiagnosticPortOption() =>
+ new(
+ aliases: new[] { "--dport", "--diagnostic-port" },
+ description: "The path to a diagnostic port to collect the dump from.")
{
- Argument = new Argument<ReportType>(() => ReportType.HeapStat)
+ Argument = new Argument<string>(name: "diagnostic-port", getDefaultValue: () => string.Empty)
};
private enum ReportSource
internal static volatile bool eventPipeDataPresent;
internal static volatile bool dumpComplete;
+ /// <summary>
+ /// Given a nettrace file from a EventPipe session with the appropriate provider and keywords turned on,
+ /// generate a GCHeapDump using the resulting events.
+ /// </summary>
+ /// <param name="path"></param>
+ /// <param name="memoryGraph"></param>
+ /// <param name="log"></param>
+ /// <param name="dotNetInfo"></param>
+ /// <returns></returns>
+ public static bool DumpFromEventPipeFile(string path, MemoryGraph memoryGraph, TextWriter log, DotNetHeapInfo dotNetInfo)
+ {
+ DateTime start = DateTime.Now;
+ Func<TimeSpan> getElapsed = () => DateTime.Now - start;
+
+ DotNetHeapDumpGraphReader dumper = new(log)
+ {
+ DotNetHeapInfo = dotNetInfo
+ };
+
+ try
+ {
+ TimeSpan lastEventPipeUpdate = getElapsed();
+
+ int gcNum = -1;
+
+ EventPipeEventSource source = new(path);
+
+ source.Clr.GCStart += delegate (GCStartTraceData data)
+ {
+ eventPipeDataPresent = true;
+
+ if (gcNum < 0 && data.Depth == 2 && data.Type != GCType.BackgroundGC)
+ {
+ gcNum = data.Count;
+ log.WriteLine("{0,5:n1}s: .NET Dump Started...", getElapsed().TotalSeconds);
+ }
+ };
+
+ source.Clr.GCStop += delegate (GCEndTraceData data)
+ {
+ if (data.Count == gcNum)
+ {
+ log.WriteLine("{0,5:n1}s: .NET GC Complete.", getElapsed().TotalSeconds);
+ dumpComplete = true;
+ }
+ };
+
+ source.Clr.GCBulkNode += delegate (GCBulkNodeTraceData data)
+ {
+ eventPipeDataPresent = true;
+
+ if ((getElapsed() - lastEventPipeUpdate).TotalMilliseconds > 500)
+ {
+ log.WriteLine("{0,5:n1}s: Making GC Heap Progress...", getElapsed().TotalSeconds);
+ }
+
+ lastEventPipeUpdate = getElapsed();
+ };
+
+ if (memoryGraph != null)
+ {
+ dumper.SetupCallbacks(memoryGraph, source);
+ }
+
+ log.WriteLine("{0,5:n1}s: Starting to process events", getElapsed().TotalSeconds);
+ source.Process();
+ log.WriteLine("{0,5:n1}s: Finished processing events", getElapsed().TotalSeconds);
+
+ if (eventPipeDataPresent)
+ {
+ dumper.ConvertHeapDataToGraph();
+ }
+ }
+ catch (Exception e)
+ {
+ log.WriteLine($"{getElapsed().TotalSeconds,5:n1}s: [Error] Exception processing events: {e}");
+ }
+
+ log.WriteLine("[{0,5:n1}s: Done Dumping .NET heap success={1}]", getElapsed().TotalSeconds, dumpComplete);
+
+ return dumpComplete;
+ }
+
/// <summary>
/// Given a factory for creating an EventPipe session with the appropriate provider and keywords turned on,
/// generate a GCHeapDump using the resulting events. The correct keywords and provider name
/// are given as input to the Func eventPipeEventSourceFactory.
/// </summary>
- /// <param name="processID"></param>
- /// <param name="eventPipeEventSourceFactory">A delegate for creating and stopping EventPipe sessions</param>
+ /// <param name="processId"></param>
+ /// <param name="diagnosticPort"></param>
/// <param name="memoryGraph"></param>
/// <param name="log"></param>
+ /// <param name="timeout"></param>
/// <param name="dotNetInfo"></param>
/// <returns></returns>
- public static bool DumpFromEventPipe(CancellationToken ct, int processID, MemoryGraph memoryGraph, TextWriter log, int timeout, DotNetHeapInfo dotNetInfo = null)
+ public static bool DumpFromEventPipe(CancellationToken ct, int processId, string diagnosticPort, MemoryGraph memoryGraph, TextWriter log, int timeout, DotNetHeapInfo dotNetInfo)
{
DateTime start = DateTime.Now;
Func<TimeSpan> getElapsed = () => DateTime.Now - start;
bool fDone = false;
log.WriteLine("{0,5:n1}s: Creating type table flushing task", getElapsed().TotalSeconds);
- using (EventPipeSessionController typeFlushSession = new(processID, new List<EventPipeProvider> {
+ using (EventPipeSessionController typeFlushSession = new(processId, diagnosticPort, new List<EventPipeProvider> {
new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational)
}, false))
{
// Start the providers and trigger the GCs.
log.WriteLine("{0,5:n1}s: Requesting a .NET Heap Dump", getElapsed().TotalSeconds);
- using EventPipeSessionController gcDumpSession = new(processID, new List<EventPipeProvider> {
+ using EventPipeSessionController gcDumpSession = new(processId, diagnosticPort, new List<EventPipeProvider> {
new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, (long)(ClrTraceEventParser.Keywords.GCHeapSnapshot))
});
log.WriteLine("{0,5:n1}s: gcdump EventPipe Session started", getElapsed().TotalSeconds);
gcDumpSession.Source.Clr.GCStart += delegate (GCStartTraceData data)
{
- if (data.ProcessID != processID)
+ if (gcDumpSession.UseWildcardProcessId)
+ {
+ processId = data.ProcessID;
+ }
+ if (data.ProcessID != processId)
{
return;
}
gcDumpSession.Source.Clr.GCStop += delegate (GCEndTraceData data)
{
- if (data.ProcessID != processID)
+ if (data.ProcessID != processId)
{
return;
}
gcDumpSession.Source.Clr.GCBulkNode += delegate (GCBulkNodeTraceData data)
{
- if (data.ProcessID != processID)
+ if (data.ProcessID != processId)
{
return;
}
if (memoryGraph != null)
{
- dumper.SetupCallbacks(memoryGraph, gcDumpSession.Source, processID.ToString());
+ dumper.SetupCallbacks(memoryGraph, gcDumpSession.Source, gcDumpSession.UseWildcardProcessId ? null : processId.ToString());
}
// Set up a separate thread that will listen for EventPipe events coming back telling us we succeeded.
private EventPipeSession _session;
private EventPipeEventSource _source;
private int _pid;
+ private IpcEndpointConfig _diagnosticPort;
public IReadOnlyList<EventPipeProvider> Providers => _providers.AsReadOnly();
public EventPipeEventSource Source => _source;
- public EventPipeSessionController(int pid, List<EventPipeProvider> providers, bool requestRundown = true)
+ public bool UseWildcardProcessId => _diagnosticPort != null;
+
+ public EventPipeSessionController(int pid, string diagnosticPort, List<EventPipeProvider> providers, bool requestRundown = true)
{
+ if (string.IsNullOrEmpty(diagnosticPort))
+ {
+ try
+ {
+ string defaultAddress = PidIpcEndpoint.GetDefaultAddress(pid);
+ if (!string.IsNullOrEmpty(defaultAddress) && PidIpcEndpoint.IsDefaultAddressDSRouter(pid, defaultAddress))
+ {
+ diagnosticPort = defaultAddress + ",connect";
+ }
+ }
+ catch { }
+ }
+
+ if (!string.IsNullOrEmpty(diagnosticPort))
+ {
+ _diagnosticPort = IpcEndpointConfig.Parse(diagnosticPort);
+ if (!_diagnosticPort.IsConnectConfig)
+ {
+ throw new ArgumentException("DiagnosticPort is only supporting connect mode.");
+ }
+ }
+
_pid = pid;
_providers = providers;
- _client = new DiagnosticsClient(pid);
+
+ if (_diagnosticPort != null)
+ {
+ _client = new DiagnosticsClient(_diagnosticPort);
+ }
+ else
+ {
+ _client = new DiagnosticsClient(pid);
+ }
+
_session = _client.StartEventPipeSession(providers, requestRundown, 1024);
_source = new EventPipeEventSource(_session.EventStream);
}
.AddCommand(CollectCommandHandler.CollectCommand())
.AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that gcdumps can be collected from."))
.AddCommand(ReportCommandHandler.ReportCommand())
+ .AddCommand(ConvertCommandHandler.ConvertCommand())
.UseDefaults()
.Build();
};
private static Option DiagnosticPortOption() =>
new(
- alias: "--diagnostic-port",
+ aliases: new[] { "--dport", "--diagnostic-port" },
description: @"The path to a diagnostic port to be used.")
{
Argument = new Argument<string>(name: "diagnosticPort", getDefaultValue: () => string.Empty)