Skip to content

Commit 31e7fae

Browse files
committed
Add support for raw and live output (#7)
1 parent f15d1ec commit 31e7fae

7 files changed

Lines changed: 391 additions & 60 deletions

File tree

doc/profile_mode_live.png

58.1 KB
Loading

doc/readme.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ $ ultra.exe profile -- my_command.exe arg0 arg1 arg2...
1616

1717
This will create a `ultra_my_command_..._.json.gz` trace file in the current directory.
1818

19+
By default, ultra won't show the stdout/stderr of the program launched. You can change this behavior by specifying the `--mode` option:
20+
21+
- `silent` (default): won't mix program's output
22+
- `raw`: will mix ultra and program output together in a raw output. Ultra output will be prefixed at the start of a line with `>>ultra::`
23+
- `live`: will mix ultra and program output within a live table
24+
25+
For example, a profile with `live` mode:
26+
27+
```console
28+
$ ultra.exe profile --mode live -- my_command.exe arg0 arg1 arg2...
29+
```
30+
31+
will display the following live table when running your process:
32+
33+
![Live ultra mode](profile_mode_live.png)
34+
1935
When attaching an existing process, you can pass directly a PID to ultra.exe:
2036

2137
```console
@@ -116,11 +132,24 @@ Usage: ultra profile [Options] <pid | -- execName arg0 arg1...>
116132

117133
-h, -?, --help Show this message and exit
118134
--pid=PID The PID of the process to attach the profiler to.
119-
--sampling-interval=VALUE The VALUE of the sample interval in ms. Default is 8190Hz = 0.122ms.
120-
--symbol-path=VALUE The VALUE of symbol path. The default value is `;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https://msdl.microsoft.com/download/symbols;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https://
121-
symbols.nuget.org/download/symbols`.
135+
--sampling-interval=VALUE The VALUE of the sample interval in ms. Default
136+
is 8190Hz = 0.122ms.
137+
--symbol-path=VALUE The VALUE of symbol path. The default value is `;
138+
SRV*C:\Users\alexa\AppData\Local\Temp\
139+
SymbolCache*https://msdl.microsoft.com/download/
140+
symbols;SRV*C:\Users\alexa\AppData\Local\Temp\
141+
SymbolCache*https://symbols.nuget.org/download/
142+
symbols`.
122143
--keep-merged-etl-file Keep the merged ETL file.
123144
--keep-intermediate-etl-files Keep the intermediate ETL files before merging.
145+
--mode=VALUE Defines how the stdout/stderr of a program
146+
explicitly started by ultra should be
147+
integrated in its output. Default is `silent`
148+
which will not mix program's output. The other
149+
options are: `raw` is going to mix ultra and
150+
program output together in a raw output. `live`
151+
is going to mix ultra and program output within
152+
a live table.
124153
```
125154

126155
## Convert
@@ -134,6 +163,10 @@ Usage: ultra convert --pid xxx <etl_file_name.etl>
134163

135164
-h, -?, --help Show this message and exit
136165
--pid=PID The PID of the process
137-
--symbol-path=VALUE The VALUE of symbol path. The default value is `;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https://msdl.microsoft.com/download/symbols;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https://
138-
symbols.nuget.org/download/symbols`.
166+
--symbol-path=VALUE The VALUE of symbol path. The default value is `;
167+
SRV*C:\Users\alexa\AppData\Local\Temp\
168+
SymbolCache*https://msdl.microsoft.com/download/
169+
symbols;SRV*C:\Users\alexa\AppData\Local\Temp\
170+
SymbolCache*https://symbols.nuget.org/download/
171+
symbols`.
139172
```

src/Ultra.Core/EtwUltraProfiler.cs

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public async Task<string> Run(EtwUltraProfilerOptions ultraProfilerOptions)
104104
// Append the pid for a single process that we are attaching to
105105
if (singleProcess is not null)
106106
{
107-
baseName = $"{baseName}_{singleProcess.Id}";
107+
baseName = $"{baseName}_pid_{singleProcess.Id}";
108108
}
109109

110110
var options = new TraceEventProviderOptions()
@@ -181,23 +181,14 @@ public async Task<string> Run(EtwUltraProfilerOptions ultraProfilerOptions)
181181
// Start a command line process if needed
182182
if (ultraProfilerOptions.ProgramPath is not null)
183183
{
184-
var startInfo = new ProcessStartInfo
184+
var processState = StartProcess(ultraProfilerOptions);
185+
processList.Add(processState.Process);
186+
// Append the pid for a single process that we are attaching to
187+
if (singleProcess is null)
185188
{
186-
FileName = ultraProfilerOptions.ProgramPath,
187-
UseShellExecute = true,
188-
CreateNoWindow = true,
189-
WindowStyle = ProcessWindowStyle.Hidden
190-
};
191-
192-
foreach (var arg in ultraProfilerOptions.Arguments)
193-
{
194-
startInfo.ArgumentList.Add(arg);
189+
baseName = $"{baseName}_pid_{processState.Process.Id}";
195190
}
196-
197-
ultraProfilerOptions.LogProgress?.Invoke($"Starting Process {startInfo.FileName} {string.Join(" ", startInfo.ArgumentList)}");
198-
var process = System.Diagnostics.Process.Start(startInfo)!;
199-
processList.Add(process);
200-
singleProcess ??= process;
191+
singleProcess ??= processState.Process;
201192
}
202193

203194
foreach (var process in processList)
@@ -391,6 +382,97 @@ private async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options
391382
}
392383
}
393384

385+
386+
private static ProcessState StartProcess(EtwUltraProfilerOptions ultraProfilerOptions)
387+
{
388+
var mode = ultraProfilerOptions.ConsoleMode;
389+
390+
var process = new Process();
391+
392+
var startInfo = process.StartInfo;
393+
startInfo.FileName = ultraProfilerOptions.ProgramPath;
394+
395+
foreach (var arg in ultraProfilerOptions.Arguments)
396+
{
397+
startInfo.ArgumentList.Add(arg);
398+
}
399+
400+
ultraProfilerOptions.LogProgress?.Invoke($"Starting Process {startInfo.FileName} {string.Join(" ", startInfo.ArgumentList)}");
401+
402+
if (mode == EtwUltraProfilerConsoleMode.Silent)
403+
{
404+
startInfo.UseShellExecute = true;
405+
startInfo.CreateNoWindow = true;
406+
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
407+
408+
process.Start();
409+
}
410+
else
411+
{
412+
startInfo.UseShellExecute = false;
413+
startInfo.CreateNoWindow = true;
414+
startInfo.RedirectStandardOutput = true;
415+
startInfo.RedirectStandardError = true;
416+
startInfo.RedirectStandardInput = true;
417+
418+
process.OutputDataReceived += (sender, args) =>
419+
{
420+
if (args.Data != null)
421+
{
422+
ultraProfilerOptions.ProgramLogStdout?.Invoke(args.Data);
423+
}
424+
};
425+
426+
process.ErrorDataReceived += (sender, args) =>
427+
{
428+
if (args.Data != null)
429+
{
430+
ultraProfilerOptions.ProgramLogStderr?.Invoke(args.Data);
431+
}
432+
};
433+
434+
process.Start();
435+
436+
process.BeginOutputReadLine();
437+
process.BeginErrorReadLine();
438+
}
439+
440+
var state = new ProcessState(process);
441+
442+
// Make sure to call WaitForExit
443+
var thread = new Thread(() =>
444+
{
445+
try
446+
{
447+
process.WaitForExit();
448+
state.HasExited = true;
449+
}
450+
catch
451+
{
452+
// ignore
453+
}
454+
})
455+
{
456+
Name = "Ultra-ProcessWaitForExit",
457+
IsBackground = true
458+
};
459+
thread.Start();
460+
461+
return state;
462+
}
463+
464+
private class ProcessState
465+
{
466+
public ProcessState(Process process)
467+
{
468+
Process = process;
469+
}
470+
471+
public readonly Process Process;
472+
473+
public bool HasExited;
474+
}
475+
394476
public void Dispose()
395477
{
396478
_userSession?.Dispose();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// Licensed under the BSD-Clause 2 license.
3+
// See license.txt file in the project root for full license information.
4+
5+
namespace Ultra.Core;
6+
7+
/// <summary>
8+
/// The mode of the console output.
9+
/// </summary>
10+
public enum EtwUltraProfilerConsoleMode
11+
{
12+
/// <summary>
13+
/// No console output from the program started.
14+
/// </summary>
15+
Silent,
16+
17+
/// <summary>
18+
/// Redirect the console output from the program started to the current console, but live progress using Spectre.Console is disabled.
19+
/// </summary>
20+
Raw,
21+
22+
/// <summary>
23+
/// Redirect the last lines of the console output from the program started to the live progress using Spectre.Console.
24+
/// </summary>
25+
Live,
26+
}

src/Ultra.Core/EtwUltraProfilerOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public EtwUltraProfilerOptions()
3030

3131
public int TimeOutAfterInMs { get; set; }
3232

33+
public EtwUltraProfilerConsoleMode ConsoleMode { get; set; }
34+
3335
public Action<string>? LogProgress;
3436

3537
public Action<string>? LogStepProgress;
@@ -38,6 +40,10 @@ public EtwUltraProfilerOptions()
3840

3941
public Action<string>? WaitingFileToCompleteTimeOut;
4042

43+
public Action<string>? ProgramLogStdout;
44+
45+
public Action<string>? ProgramLogStderr;
46+
4147
public bool KeepEtlIntermediateFiles { get; set; }
4248

4349
public bool KeepMergedEtl { get; set; }

src/Ultra.Example/Program.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
for (int i = 0; i < countBenchMarkdig; i++)
1010
{
1111
var html = Markdig.Markdown.ToHtml(md);
12+
if (i % 100 == 0 && i > 0)
13+
{
14+
Console.WriteLine($"Markdig {i} conversions done");
15+
}
1216
}
1317
};
1418

@@ -24,6 +28,11 @@
2428
for (int i = 0; i < countBenchScriban; i++)
2529
{
2630
var text = template.Render(new { values = values });
31+
32+
if (i % 1000 == 0 && i > 0)
33+
{
34+
Console.WriteLine($"Scriban {i} conversions done");
35+
}
2736
}
2837
};
2938

0 commit comments

Comments
 (0)