Skip to content
Merged
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
106 changes: 82 additions & 24 deletions src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ internal partial class CollectLinuxCommandHandler
{
private bool stopTracing;
private Stopwatch stopwatch = new();
private LineRewriter rewriter;
private long statusUpdateTimestamp;
private ProgressWriter progressWriter;
private Version minRuntimeSupportingUserEventsIPCCommand = new(10, 0, 0);
private readonly bool cancelOnEnter;
private readonly bool printStatusOverTime;

internal sealed record CollectLinuxArgs(
CancellationToken Ct,
Expand All @@ -42,9 +40,8 @@ internal sealed record CollectLinuxArgs(
public CollectLinuxCommandHandler(IConsole console = null)
{
Console = console ?? new DefaultConsole();
rewriter = new LineRewriter(Console);
cancelOnEnter = !Console.IsInputRedirected;
printStatusOverTime = !Console.IsOutputRedirected;
progressWriter = new ProgressWriter(Console, stopwatch);
}

internal static bool IsSupported()
Expand Down Expand Up @@ -495,26 +492,9 @@ private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen)
stopTracing = true;
}

if (printStatusOverTime && ot == OutputType.Progress)
if (ot == OutputType.Progress)
{
long currentTimestamp = Stopwatch.GetTimestamp();
if (statusUpdateTimestamp != 0 && currentTimestamp < statusUpdateTimestamp)
{
return stopTracing ? 1 : 0;
}

if (statusUpdateTimestamp == 0)
{
rewriter.LineToClear = Console.CursorTop - 1;
}
else
{
rewriter.RewriteConsoleLine();
}

statusUpdateTimestamp = currentTimestamp + Stopwatch.Frequency;
Console.Out.WriteLine($"[{stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace.");
Console.Out.WriteLine("Press <Enter> or <Ctrl-C> to exit...");
progressWriter.Update();
}

return stopTracing ? 1 : 0;
Expand Down Expand Up @@ -559,6 +539,84 @@ private static partial int RunRecordTrace(
UIntPtr commandLen,
recordTraceCallback callback);

/// <summary>
/// Encapsulates progress display for the native record-trace callback.
/// Probes console capability on the first Update() call, then handles throttled
/// in-place rewrites (interactive), a single static message (non-interactive),
/// or silent no-op (output redirected).
/// </summary>
private sealed class ProgressWriter
{
private readonly IConsole _console;
private readonly Stopwatch _stopwatch;
private bool _initialized;
// Non-null after initialization only when in-place rewriting is supported.
private LineRewriter _rewriter;
private long _nextUpdateTimestamp;

public ProgressWriter(IConsole console, Stopwatch stopwatch)
{
_console = console;
_stopwatch = stopwatch;
}

/// <summary>
/// Called on each Progress callback from native record-trace.
/// No-ops if output is redirected, non-interactive, or within the 1-second throttle window.
/// </summary>
public void Update()
{
if (!_initialized)
{
Initialize();
}

if (_rewriter == null)
{
return;
}

long now = Stopwatch.GetTimestamp();
if (_nextUpdateTimestamp != 0 && now < _nextUpdateTimestamp)
{
return;
}

if (_nextUpdateTimestamp != 0)
{
_rewriter.RewriteConsoleLine();
}

_nextUpdateTimestamp = now + Stopwatch.Frequency;
_console.Out.WriteLine($"[{_stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace.");
_console.Out.WriteLine("Press <Enter> or <Ctrl+C> to exit...");
}

private void Initialize()
{
_initialized = true;

if (_console.IsOutputRedirected)
{
return;
}

// Only capture the cursor position to rewrite once we've committed to writing progress output,
// otherwise the position becomes stale the moment any other console output occurs.
LineRewriter rewriter = new(_console);
rewriter.LineToClear = _console.CursorTop - 1;

if (rewriter.LineToClear >= 0 && rewriter.IsRewriteConsoleLineSupported)
{
_rewriter = rewriter;
}
else
{
_console.Out.WriteLine("Recording trace in progress. Press <Enter> or <Ctrl+C> to exit...");
}
}
}

#region testing seams
internal Func<byte[], UIntPtr, recordTraceCallback, int> RecordTraceInvoker { get; set; } = RunRecordTrace;
internal IConsole Console { get; set; }
Expand Down
12 changes: 12 additions & 0 deletions src/tests/Common/MockConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ public void Clear()
}
public void SetCursorPosition(int col, int row)
{
if (col < 0 || col >= BufferWidth)
{
throw new ArgumentOutOfRangeException(nameof(col),
col,
"The value must be greater than or equal to zero and less than the console's buffer size in that dimension.");
}
if (row < 0 || row >= WindowHeight)
{
throw new ArgumentOutOfRangeException(nameof(row),
row,
"The value must be greater than or equal to zero and less than the console's buffer size in that dimension.");
}
CursorTop = row;
_cursorLeft = col;
}
Comment thread
mdh1418 marked this conversation as resolved.
Expand Down
26 changes: 25 additions & 1 deletion src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,30 @@ public void CollectLinuxCommand_DoesNotReadKey_WhenInputIsRedirected()
Assert.True(callbackInvoked);
}

[ConditionalFact(nameof(IsCollectLinuxSupported))]
public void CollectLinuxCommand_PrintsStatusOnce_WhenCursorRepositioningUnsupported()
{
MockConsole console = new(200, 30, _outputHelper);

var handler = new CollectLinuxCommandHandler(console);
handler.RecordTraceInvoker = (cmd, len, cb) => {
// Simulate cursor repositioning unsupported by resetting the cursor
// during the callback, undoing any cursor advancement from the tool's
// earlier console output (banner, provider table, etc.).
console.Clear();
cb(3, IntPtr.Zero, UIntPtr.Zero);
cb(3, IntPtr.Zero, UIntPtr.Zero);
return 0;
};

int exitCode = handler.CollectLinux(TestArgs());
Assert.Equal((int)ReturnCode.Ok, exitCode);

string[] lines = console.Lines;
int statusLineCount = lines.Count(l => l.Contains("Recording trace", StringComparison.OrdinalIgnoreCase));
Assert.Equal(1, statusLineCount);
}
Comment thread
mdh1418 marked this conversation as resolved.

private static int Run(object args, MockConsole console)
{
var handler = new CollectLinuxCommandHandler(console);
Expand Down Expand Up @@ -532,7 +556,7 @@ private static string[] FormatException(string message)
DefaultOutputFile,
"",
"[dd:hh:mm:ss]\tRecording trace.",
"Press <Enter> or <Ctrl-C> to exit...",
"Press <Enter> or <Ctrl+C> to exit...",
];
private static string[] PreviewMessages = [
"==========================================================================================",
Expand Down
Loading