diff --git a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj
index c006330b90..098fb2813b 100644
--- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj
+++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj
@@ -41,6 +41,7 @@
+
diff --git a/src/Tools/dotnet-stack/ReportCommand.cs b/src/Tools/dotnet-stack/ReportCommand.cs
index c8f2ecc365..79389c4009 100644
--- a/src/Tools/dotnet-stack/ReportCommand.cs
+++ b/src/Tools/dotnet-stack/ReportCommand.cs
@@ -27,22 +27,44 @@ internal static class ReportCommandHandler
/// The process to report the stack from.
/// The name of process to report the stack from.
/// The duration of to trace the target for.
+ /// The diagnostic port to connect to.
///
- private static async Task Report(CancellationToken ct, TextWriter stdOutput, TextWriter stdError, int processId, string name, TimeSpan duration)
+ private static async Task Report(CancellationToken ct, TextWriter stdOutput, TextWriter stdError, int processId, string name, TimeSpan duration, string diagnosticPort)
{
string tempNetTraceFilename = Path.Join(Path.GetTempPath(), Path.GetRandomFileName() + ".nettrace");
string tempEtlxFilename = "";
try
{
- // Either processName or processId has to be specified.
+ // Validate that only one of processId, name, or diagnosticPort is specified
+ int optionCount = 0;
+ if (processId != 0)
+ {
+ optionCount++;
+ }
+ if (!string.IsNullOrEmpty(name))
+ {
+ optionCount++;
+ }
+ if (!string.IsNullOrEmpty(diagnosticPort))
+ {
+ optionCount++;
+ }
+
+ if (optionCount == 0)
+ {
+ stdError.WriteLine("--process-id, --name, or --diagnostic-port is required");
+ return -1;
+ }
+ else if (optionCount > 1)
+ {
+ stdError.WriteLine("Only one of --process-id, --name, or --diagnostic-port can be specified");
+ return -1;
+ }
+
+ // Resolve process name to ID if needed
if (!string.IsNullOrEmpty(name))
{
- if (processId != 0)
- {
- Console.WriteLine("Can only specify either --name or --process-id option.");
- return -1;
- }
processId = CommandUtils.FindProcessIdWithName(name);
if (processId < 0)
{
@@ -55,91 +77,125 @@ private static async Task Report(CancellationToken ct, TextWriter stdOutput
stdError.WriteLine("Process ID should not be negative.");
return -1;
}
- else if (processId == 0)
+
+ DiagnosticsClientBuilder builder = new("dotnet-stack", 10);
+ using (DiagnosticsClientHolder holder = await builder.Build(ct, processId, diagnosticPort, showChildIO: false, printLaunchCommand: false).ConfigureAwait(false))
{
- stdError.WriteLine("--process-id is required");
- return -1;
- }
+ if (holder == null)
+ {
+ return -1;
+ }
+ DiagnosticsClient client = holder.Client;
- DiagnosticsClient client = new(processId);
- List providers = new()
- {
- new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational)
- };
-
- // collect a *short* trace with stack samples
- // the hidden '--duration' flag can increase the time of this trace in case 10ms
- // is too short in a given environment, e.g., resource constrained systems
- // N.B. - This trace INCLUDES rundown. For sufficiently large applications, it may take non-trivial time to collect
- // the symbol data in rundown.
- EventPipeSession session = await client.StartEventPipeSessionAsync(providers, requestRundown:true, token:ct).ConfigureAwait(false);
- using (session)
- using (FileStream fs = File.OpenWrite(tempNetTraceFilename))
- {
- Task copyTask = session.EventStream.CopyToAsync(fs, ct);
- await Task.Delay(duration, ct).ConfigureAwait(false);
- await session.StopAsync(ct).ConfigureAwait(false);
-
- // check if rundown is taking more than 5 seconds and add comment to report
- Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
- Task completedTask = await Task.WhenAny(copyTask, timeoutTask).ConfigureAwait(false);
- if (completedTask == timeoutTask)
+ // Resume runtime if it was suspended (similar to --resume-runtime:true in other tools)
+ // This is safe to call even if the runtime wasn't suspended - it's a no-op in that case
+ try
{
- stdOutput.WriteLine($"# Sufficiently large applications can cause this reportCommand to take non-trivial amounts of time");
+ await client.ResumeRuntimeAsync(ct).ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ // ResumeRuntime is a no-op if the runtime wasn't suspended,
+ // so we can safely ignore exceptions here
}
- await copyTask.ConfigureAwait(false);
- }
- // using the generated trace file, symbolicate and compute stacks.
- tempEtlxFilename = TraceLog.CreateFromEventPipeDataFile(tempNetTraceFilename);
- using (SymbolReader symbolReader = new(TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath })
- using (TraceLog eventLog = new(tempEtlxFilename))
- {
- MutableTraceEventStackSource stackSource = new(eventLog)
+ List providers = new()
{
- OnlyManagedCodeStacks = true
+ new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational)
};
- SampleProfilerThreadTimeComputer computer = new(eventLog, symbolReader);
- computer.GenerateThreadTimeStacks(stackSource);
-
- Dictionary> samplesForThread = new();
+ // collect a *short* trace with stack samples
+ // the hidden '--duration' flag can increase the time of this trace in case 10ms
+ // is too short in a given environment, e.g., resource constrained systems
+ // N.B. - This trace INCLUDES rundown. For sufficiently large applications, it may take non-trivial time to collect
+ // the symbol data in rundown.
+ EventPipeSession session;
+ try
+ {
+ session = await client.StartEventPipeSessionAsync(providers, requestRundown:true, token:ct).ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ stdError.WriteLine(EventPipeErrorMessage);
+ return -1;
+ }
- stackSource.ForEach((sample) => {
- StackSourceCallStackIndex stackIndex = sample.StackIndex;
- while (!stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), false).StartsWith("Thread ("))
+ try
+ {
+ using (session)
+ using (FileStream fs = File.OpenWrite(tempNetTraceFilename))
{
- stackIndex = stackSource.GetCallerIndex(stackIndex);
+ Task copyTask = session.EventStream.CopyToAsync(fs, ct);
+ await Task.Delay(duration, ct).ConfigureAwait(false);
+ await session.StopAsync(ct).ConfigureAwait(false);
+
+ // check if rundown is taking more than 5 seconds and add comment to report
+ Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
+ Task completedTask = await Task.WhenAny(copyTask, timeoutTask).ConfigureAwait(false);
+ if (completedTask == timeoutTask)
+ {
+ stdOutput.WriteLine($"# Sufficiently large applications can cause this reportCommand to take non-trivial amounts of time");
+ }
+ await copyTask.ConfigureAwait(false);
}
+ }
+ catch (Exception)
+ {
+ stdError.WriteLine(EventPipeErrorMessage);
+ return -1;
+ }
- // long form for: int.Parse(threadFrame["Thread (".Length..^1)])
- // Thread id is in the frame name as "Thread ()"
- string template = "Thread (";
- string threadFrame = stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), false);
+ // using the generated trace file, symbolicate and compute stacks.
+ tempEtlxFilename = TraceLog.CreateFromEventPipeDataFile(tempNetTraceFilename);
+ using (SymbolReader symbolReader = new(TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath })
+ using (TraceLog eventLog = new(tempEtlxFilename))
+ {
+ MutableTraceEventStackSource stackSource = new(eventLog)
+ {
+ OnlyManagedCodeStacks = true
+ };
- // we are looking for the first index of ) because
- // we need to handle a thread name like: Thread (4008) (.NET IO ThreadPool Worker)
- int firstIndex = threadFrame.IndexOf(')');
- int threadId = int.Parse(threadFrame.AsSpan(template.Length, firstIndex - template.Length));
+ SampleProfilerThreadTimeComputer computer = new(eventLog, symbolReader);
+ computer.GenerateThreadTimeStacks(stackSource);
- if (samplesForThread.TryGetValue(threadId, out List samples))
- {
- samples.Add(sample);
- }
- else
- {
- samplesForThread[threadId] = new List() { sample };
- }
- });
+ Dictionary> samplesForThread = new();
- // For every thread recorded in our trace, print the first stack
- foreach ((int threadId, List samples) in samplesForThread)
- {
+ stackSource.ForEach((sample) => {
+ StackSourceCallStackIndex stackIndex = sample.StackIndex;
+ while (!stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), false).StartsWith("Thread ("))
+ {
+ stackIndex = stackSource.GetCallerIndex(stackIndex);
+ }
+
+ // long form for: int.Parse(threadFrame["Thread (".Length..^1)])
+ // Thread id is in the frame name as "Thread ()"
+ string template = "Thread (";
+ string threadFrame = stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), false);
+
+ // we are looking for the first index of ) because
+ // we need to handle a thread name like: Thread (4008) (.NET IO ThreadPool Worker)
+ int firstIndex = threadFrame.IndexOf(')');
+ int threadId = int.Parse(threadFrame.AsSpan(template.Length, firstIndex - template.Length));
+
+ if (samplesForThread.TryGetValue(threadId, out List samples))
+ {
+ samples.Add(sample);
+ }
+ else
+ {
+ samplesForThread[threadId] = new List() { sample };
+ }
+ });
+
+ // For every thread recorded in our trace, print the first stack
+ foreach ((int threadId, List samples) in samplesForThread)
+ {
#if DEBUG
- stdOutput.WriteLine($"Found {samples.Count} stacks for thread 0x{threadId:X}");
+ stdOutput.WriteLine($"Found {samples.Count} stacks for thread 0x{threadId:X}");
#endif
- PrintStack(stdOutput, threadId, samples[0], stackSource);
+ PrintStack(stdOutput, threadId, samples[0], stackSource);
+ }
}
}
}
@@ -189,7 +245,8 @@ public static Command ReportCommand()
{
ProcessIdOption,
NameOption,
- DurationOption
+ DurationOption,
+ DiagnosticPortOption
};
reportCommand.SetAction((parseResult, ct) => Report(ct,
@@ -197,7 +254,8 @@ public static Command ReportCommand()
stdError: parseResult.Configuration.Error,
processId: parseResult.GetValue(ProcessIdOption),
name: parseResult.GetValue(NameOption),
- duration: parseResult.GetValue(DurationOption)));
+ duration: parseResult.GetValue(DurationOption),
+ diagnosticPort: parseResult.GetValue(DiagnosticPortOption)));
return reportCommand;
}
@@ -221,5 +279,14 @@ public static Command ReportCommand()
{
Description = "The name of the process to report the stack."
};
+
+ private static readonly Option DiagnosticPortOption =
+ new("--diagnostic-port", "--dport")
+ {
+ Description = "The path to a diagnostic port to be used."
+ };
+
+ private const string EventPipeErrorMessage =
+ "There was a failure in reading stack data. Possible reasons could be trying to connect to a runtime that has been suspended, an unexpected close of the IPC channel, etc.";
}
}
diff --git a/src/Tools/dotnet-stack/dotnet-stack.csproj b/src/Tools/dotnet-stack/dotnet-stack.csproj
index beacb323f4..9f0448817a 100644
--- a/src/Tools/dotnet-stack/dotnet-stack.csproj
+++ b/src/Tools/dotnet-stack/dotnet-stack.csproj
@@ -23,8 +23,10 @@
+
+
diff --git a/src/tests/dotnet-stack/StackTests.cs b/src/tests/dotnet-stack/StackTests.cs
index c689e5a5ba..cff55e2fc8 100644
--- a/src/tests/dotnet-stack/StackTests.cs
+++ b/src/tests/dotnet-stack/StackTests.cs
@@ -100,5 +100,26 @@ public async Task ReportsStacksCorrectly(TestConfiguration config)
Assert.True(correctStackParts[j] == stackParts[i], $"{correctStackParts[j]} != {stackParts[i]}");
}
}
+
+ [Fact]
+ public void DiagnosticPortOptionIsRegistered()
+ {
+ Command reportCommand = ReportCommandHandler.ReportCommand();
+
+ // Verify the diagnostic port option is registered
+ bool hasDiagnosticPortOption = false;
+ foreach (var option in reportCommand.Options)
+ {
+ if (option.Name == "diagnostic-port")
+ {
+ hasDiagnosticPortOption = true;
+ // Verify it has the short alias --dport
+ Assert.Contains("--dport", option.Aliases);
+ break;
+ }
+ }
+
+ Assert.True(hasDiagnosticPortOption, "The --diagnostic-port option should be registered in the report command");
+ }
}
}