mirror of
https://github.com/microsoft/microsoft-ui-xaml.git
synced 2026-10-28 08:25:50 +08:00
WinUI Profiler: record the kernel's GPU memory events (#12424)
## Description The WinUI Profiler (`tools/WinUIProfiler`) now also records the kernel's GPU memory events for the target process, live and from `.etl` files. Windows writes them in any WinUI build. ### Current Behavior The profiler reads the target's `Microsoft-Windows-XAML` and `Microsoft-Windows-XAML-Profiler` events. Its only GPU memory data was the profiler provider's `GpuMemorySnapshot` event, stored like any other event, and only from WinUI builds with `XamlProfilerEnabled`. #12416 removed that event. The native GPU sampler reads GPU engine utilization, not memory. ### New Behavior - **Events:** `Microsoft-Windows-DxgKrnl` keyword `0x4`: `VidMmProcessUsageChange` (367) and `VidMmProcessBudgetChange` (366), the process's usage and budget per adapter and memory segment group. They carry the values `IDXGIAdapter3::QueryVideoMemoryInfo` returns. The kernel writes them when the usage or budget changes, and writes the current values when the process opens a GPU adapter and when a session requests capture state. - **Matched by the event's `ProcessId` field**, not the event header: the System process writes some of a process's rows, such as those at exit that bring its usage down to 0. That field becomes the event's process ID; the thread ID stays the writing thread. - **Live:** each connection's session also enables DxgKrnl keyword `0x4`, with no process filter, and requests capture state when reading starts, so an attach gets the current usage and budget at once. Both are best effort: a failure is traced as a warning, and XAML events are read as before. - **`.etl` files:** the target's rows are read along with its XAML events. - **Session files** store them with provider `DxgKrnl`. The schema version is unchanged. - **App:** the connection panel's status line adds a "GPU memory" count, and its tooltip a line for it. - **CLI:** `processes` has a DxgKrnl column. For traces it still lists only processes with XAML or XAML-Profiler events, since every process that uses the GPU has GPU memory events; a session file always lists its process. The usage text and messages say "events" instead of "XAML events". - **README:** the provider and its events, how rows are matched, the live behavior, and a `logman` line for the provider. ## Impact and risk Developer tooling only, under `tools/WinUIProfiler`; no product code changes. Low risk. Session files that contain GPU memory rows can't be read by earlier builds of the tool, which don't know the `DxgKrnl` provider; other session files are unchanged. ## How Has This Been Tested? `tools/WinUIProfiler` has no test project yet. I validated with traces of a 1,000-item ListView test app, from a VM without a GPU (WARP) and from a Snapdragon X Elite laptop (integrated GPU), and live on the VM. | Test | Result | | --- | --- | | `dotnet build .\WinUIProfiler.Cli\WinUIProfiler.Cli.csproj -c Release` | Passed, 0 warnings | | `.\scripts\Build-App.ps1 -Configuration Release -Platform x64` | Passed | | CLI, VM trace | The app's 2 rows: usage 0 and budget 50,680,655,872 bytes, the values of the `GpuMemorySnapshot` events in the same trace | | CLI, laptop trace with the app running | 130 usage rows and 1 budget row; usage from 0 to about 22 MB | | CLI, laptop trace through app exit | 255 usage rows and 1 budget row; the last rows bring usage to 0 | | Which process wrote the laptop rows | The System process wrote 2 of the running app's rows and 127 of its exit rows; the app's own threads wrote the rest. Matching by `ProcessId` keeps all of them | | Live attach to a running app (Core live source, elevated) | The capture-state usage and budget rows arrive; 0 lost | | Live launch (same) | 6,230 XAML events and 2 GPU memory rows; 0 lost | | `logman` with the README's provider lines | Records the app's GPU memory rows | | `record`, then `summary` and `dump` of the `.sqlite` | DxgKrnl rows round-trip; a file with only GPU memory rows lists its process | | Status line strings | `EtwFormat` and `EtwDetailsFormat` format with the new arguments. The App UI itself wasn't driven | Not tested: discrete GPUs (non-local memory), machines with two GPUs, Windows 10.
This commit is contained in:
11 files changed
+166
-44
No files matched your search
@@ -50,7 +50,8 @@ units, missing-value sentinels and busiest-engine validation remain enforced.
|
||||
Native compile-time size assertions protect the x64 and ARM64 layouts.
|
||||
|
||||
This is the executable/connection/metrics baseline, not the full profiler.
|
||||
While connected, the app saves the target's XAML ETW events to a session file.
|
||||
While connected, the app saves the target's XAML ETW events, and the kernel's
|
||||
GPU memory events for it, to a session file.
|
||||
There is no object or frame view, heap capture or instrumented-product
|
||||
dependency in the app yet. The profiler UI uses its pinned SDK runtime; it does
|
||||
not replace the target's loaded runtime.
|
||||
@@ -122,19 +123,29 @@ errors and scope limitations are included in the JSON rather than fabricated val
|
||||
|
||||
## XAML events
|
||||
|
||||
`WinUIProfiler.Core` reads the XAML TraceLogging events of one process, live or
|
||||
from a recorded `.etl` file. Each event becomes a
|
||||
`ProfilerEvent` (`Events\ProfilerEvent.cs`). TraceLogging events carry their own
|
||||
field names and types, so no manifest is needed. Manifest events on the same
|
||||
providers are skipped.
|
||||
`WinUIProfiler.Core` reads the XAML TraceLogging events of one process, and the
|
||||
kernel's GPU memory events for it, live or from a recorded `.etl` file. Each
|
||||
event becomes a `ProfilerEvent` (`Events\ProfilerEvent.cs`). TraceLogging events
|
||||
carry their own field names and types, so no manifest is needed. Manifest events
|
||||
on the XAML providers are skipped. The GPU memory events are manifest events,
|
||||
decoded with the manifest installed with Windows.
|
||||
|
||||
| Provider | GUID | TraceLogging events |
|
||||
| Provider | GUID | Events read |
|
||||
| --- | --- | --- |
|
||||
| Microsoft-Windows-XAML | `531a35ab-63ce-4bcf-aa98-f88c7a89e455` | Frames (`CoreServices_Frame`), objects entering the tree (`DependencyObject_Enter`), and layout, focus and other operations with element IDs |
|
||||
| Microsoft-Windows-XAML-Profiler | `a1b2c3d4-e5f6-4a5b-9c8d-7e6f5a4b3c2d` | Tree, composition and graphics events |
|
||||
| Microsoft-Windows-DxgKrnl | `802ec45a-1e99-4b83-9920-87c98277ba9d` | GPU memory, keyword `0x4`: `VidMmProcessUsageChange` (367) and `VidMmProcessBudgetChange` (366), the process's usage and budget per adapter and memory segment group (`MemorySegmentGroup` 0 is local, 1 is non-local) |
|
||||
|
||||
Events with element IDs and all Microsoft-Windows-XAML-Profiler events are only
|
||||
in builds with `XamlProfilerEnabled`.
|
||||
in builds with `XamlProfilerEnabled`. The GPU memory events come from Windows,
|
||||
whatever the build. They carry the values `IDXGIAdapter3::QueryVideoMemoryInfo`
|
||||
returns, which earlier builds also reported in the profiler's `GpuMemorySnapshot`
|
||||
event. The kernel writes them when the usage or budget changes, and
|
||||
writes the current values when the process opens a GPU adapter and when a
|
||||
session requests capture state. The System process writes some of them, such as
|
||||
those written when the process exits, which bring the usage down to 0. So they
|
||||
are matched to the process by their `ProcessId` field, which becomes the
|
||||
event's process ID.
|
||||
|
||||
Microsoft-Windows-XAML is enabled with keyword `0x100000`. No manifest event
|
||||
uses that keyword, so ETW drops them at the source and only TraceLogging events
|
||||
@@ -160,6 +171,12 @@ The app reads ETW with one real-time session per connection
|
||||
- **Attach:** the session filters on the process ID from the start and reads
|
||||
from the time of connection.
|
||||
|
||||
The GPU memory events are enabled without a process filter, because the System
|
||||
process writes some of the target's; the events of other processes are skipped
|
||||
as they are read. When reading starts, the session requests capture state, so
|
||||
an attached target also gets its current values. If the GPU memory events
|
||||
cannot be enabled, the session still reads the XAML events.
|
||||
|
||||
The panel says whether the counts start at process start or at connection. A
|
||||
launch starts at connection when activation returns a packaged app that was
|
||||
already running, when the app declares no executable, or when the launched
|
||||
@@ -218,21 +235,24 @@ then `Seq`.
|
||||
|
||||
### Recorded, from a trace file
|
||||
|
||||
To record a trace, use `logman`. Then inspect it with `WinUIProfiler.Cli`, which
|
||||
To record a trace, use `logman`. Start it before the app: the kernel writes the
|
||||
app's starting GPU memory values when the app opens a GPU adapter, and after
|
||||
that only changes. Then inspect the trace with `WinUIProfiler.Cli`, which
|
||||
reads the file through `Ingestion\OfflineEtlSource.cs`. The CLI reads session
|
||||
files the same way, and `record` writes a session file from a trace. Run from
|
||||
this directory:
|
||||
|
||||
```powershell
|
||||
'{531A35AB-63CE-4BCF-AA98-F88C7A89E455} 0x100000 5',
|
||||
'{A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D} 0xFFFFFFFFFFFFFFFF 5' | Set-Content providers.txt
|
||||
'{A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D} 0xFFFFFFFFFFFFFFFF 5',
|
||||
'{802EC45A-1E99-4B83-9920-87C98277BA9D} 0x4 5' | Set-Content providers.txt
|
||||
logman create trace XamlTrace -o xaml.etl -pf providers.txt -ets
|
||||
# ... use the target app ...
|
||||
logman stop XamlTrace -ets
|
||||
|
||||
dotnet build .\WinUIProfiler.Cli\WinUIProfiler.Cli.csproj -c Release
|
||||
$cli = '.\WinUIProfiler.Cli\bin\Release\net10.0\WinUIProfiler.Cli.exe'
|
||||
& $cli processes xaml.etl # event count per process, to find the PID
|
||||
& $cli processes xaml.etl # event counts of the processes with XAML events, to find the PID
|
||||
& $cli summary xaml.etl --pid <pid> # event counts for that process
|
||||
& $cli dump xaml.etl --pid <pid> --name CoreServices_Frame --max 20 # the events themselves
|
||||
& $cli record xaml.etl --pid <pid> --out session.sqlite [--limit-mb 500] # a session file from the trace
|
||||
|
||||
@@ -215,7 +215,7 @@ JsonSerializer.Serialize(new
|
||||
{
|
||||
SetLine(EtwSummary,
|
||||
etw is null ? "" : Format("EtwFormat", Text(etw.FromProcessStart ? "EtwFromProcessStart" : "EtwFromConnection"),
|
||||
etw.XamlEvents, etw.ProfilerEvents, etw.LostEvents, etw.DroppedEvents),
|
||||
etw.XamlEvents, etw.ProfilerEvents, etw.GpuMemoryEvents, etw.LostEvents, etw.DroppedEvents),
|
||||
etw is null ? null : Format("EtwDetailsFormat", Text(etw.FromProcessStart ? "EtwCoverageProcessStart" : "EtwCoverageConnection")));
|
||||
}
|
||||
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
<data name="InvalidEndpoint" xml:space="preserve"><value>Select a whole diagnostics endpoint number between 1 and 65535.</value></data>
|
||||
<data name="IdentityFormat" xml:space="preserve"><value>PID {0} | WinUI from {1}</value></data>
|
||||
<data name="IdentityDetailsFormat" xml:space="preserve"><value>Process started {0} Last TAP heartbeat {1} WinUI runtime {2}</value></data>
|
||||
<data name="EtwFormat" xml:space="preserve"><value>{0} | XAML {1:N0} | Profiler {2:N0} | lost {3:N0} | dropped {4:N0}</value></data>
|
||||
<data name="EtwDetailsFormat" xml:space="preserve"><value>{0} XAML: Microsoft-Windows-XAML events Profiler: Microsoft-Windows-XAML-Profiler events Lost: events ETW could not deliver Dropped: events the profiler discarded because its queue was full</value></data>
|
||||
<data name="EtwFormat" xml:space="preserve"><value>{0} | XAML {1:N0} | Profiler {2:N0} | GPU memory {3:N0} | lost {4:N0} | dropped {5:N0}</value></data>
|
||||
<data name="EtwDetailsFormat" xml:space="preserve"><value>{0} XAML: Microsoft-Windows-XAML events Profiler: Microsoft-Windows-XAML-Profiler events GPU memory: the kernel's GPU memory usage and budget events for the process (Microsoft-Windows-DxgKrnl) Lost: events ETW could not deliver Dropped: events the profiler discarded because its queue was full</value></data>
|
||||
<data name="EtwFromProcessStart" xml:space="preserve"><value>ETW from process start</value></data>
|
||||
<data name="EtwFromConnection" xml:space="preserve"><value>ETW from connection</value></data>
|
||||
<data name="EtwCoverageProcessStart" xml:space="preserve"><value>Every event since the process started is included.</value></data>
|
||||
|
||||
@@ -19,14 +19,17 @@ internal static class CliApplication
|
||||
WinUIProfiler.Cli dump <trace.etl | session.sqlite> [--pid <pid>] [--name <text>] [--max <count>]
|
||||
WinUIProfiler.Cli record <trace.etl> --pid <pid> --out <session.sqlite> [--limit-mb <size>]
|
||||
|
||||
processes Counts XAML events per process, to find the target PID.
|
||||
summary Counts one process's XAML events by provider and event.
|
||||
dump Prints one process's XAML events in order. --name keeps events whose
|
||||
processes Counts the events of each process with XAML events, to find the target PID.
|
||||
A session file shows its process.
|
||||
summary Counts one process's events by provider and event.
|
||||
dump Prints one process's events in order. --name keeps events whose
|
||||
name contains <text>; --max limits the output (default 1000, 0 = all).
|
||||
record Writes one process's XAML events from a trace to a new session file, as the
|
||||
record Writes one process's events from a trace to a new session file, as the
|
||||
app does while connected. --limit-mb is the size limit (default 500).
|
||||
|
||||
A trace needs --pid; a session file holds one process.
|
||||
Events are the XAML TraceLogging events and the kernel's GPU memory usage and
|
||||
budget events (DxgKrnl) of the process. A trace needs --pid; a session file
|
||||
holds one process.
|
||||
""";
|
||||
|
||||
private sealed record Options(string Command, string InputPath, int? ProcessId, string? NameFilter, int Max,
|
||||
@@ -161,16 +164,21 @@ internal static class CliApplication
|
||||
{
|
||||
if (!counts.TryGetValue(profilerEvent.ProcessId, out var perProvider))
|
||||
{
|
||||
counts[profilerEvent.ProcessId] = perProvider = new long[2];
|
||||
counts[profilerEvent.ProcessId] = perProvider = new long[Enum.GetValues<XamlProvider>().Length];
|
||||
}
|
||||
|
||||
perProvider[(int)profilerEvent.Provider]++;
|
||||
}, cancellationToken);
|
||||
|
||||
output.WriteLine($"{"PID",8} {"Events",10} {"XAML",10} {"XAML-Prof",10}");
|
||||
foreach (var (processId, perProvider) in counts.OrderByDescending(entry => entry.Value.Sum()))
|
||||
// In a trace, every process that uses the GPU has GPU memory events; only those with XAML events are listed.
|
||||
output.WriteLine($"{"PID",8} {"Events",10} {"XAML",10} {"XAML-Prof",10} {"DxgKrnl",10}");
|
||||
foreach (var (processId, perProvider) in counts
|
||||
.Where(entry => options.IsSessionFile ||
|
||||
entry.Value[(int)XamlProvider.Xaml] + entry.Value[(int)XamlProvider.XamlProfiler] > 0)
|
||||
.OrderByDescending(entry => entry.Value.Sum()))
|
||||
{
|
||||
output.WriteLine($"{processId,8} {perProvider.Sum(),10} {perProvider[0],10} {perProvider[1],10}");
|
||||
output.WriteLine($"{processId,8} {perProvider.Sum(),10} {perProvider[(int)XamlProvider.Xaml],10} " +
|
||||
$"{perProvider[(int)XamlProvider.XamlProfiler],10} {perProvider[(int)XamlProvider.DxgKrnl],10}");
|
||||
}
|
||||
|
||||
output.WriteLine($"{(options.IsSessionFile ? "Session" : "Trace")}: {result.Duration.TotalSeconds:F1} s, {result.EventsLost} events lost.");
|
||||
@@ -199,12 +207,12 @@ internal static class CliApplication
|
||||
|
||||
if (result.EventsDelivered == 0)
|
||||
{
|
||||
output.WriteLine($"No XAML events from process {options.ProcessId}. Run 'processes' to list the PIDs.");
|
||||
output.WriteLine($"No events from process {options.ProcessId}. Run 'processes' to list the PIDs.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var seconds = Math.Max(0.001, (last - first) / 1000);
|
||||
output.WriteLine($"Process {processId}: {result.EventsDelivered} XAML events over {seconds:F1} s " +
|
||||
output.WriteLine($"Process {processId}: {result.EventsDelivered} events over {seconds:F1} s " +
|
||||
$"({result.EventsDelivered / seconds:F0}/s); {result.EventsLost} events lost in the {(options.IsSessionFile ? "session" : "trace")}.");
|
||||
output.WriteLine();
|
||||
foreach (var (key, count) in counts.OrderByDescending(entry => entry.Value))
|
||||
@@ -284,7 +292,12 @@ internal static class CliApplication
|
||||
}
|
||||
}
|
||||
|
||||
private static string ShortName(XamlProvider provider) => provider == XamlProvider.Xaml ? "XAML" : "XAML-Prof";
|
||||
private static string ShortName(XamlProvider provider) => provider switch
|
||||
{
|
||||
XamlProvider.Xaml => "XAML",
|
||||
XamlProvider.XamlProfiler => "XAML-Prof",
|
||||
_ => "DxgKrnl"
|
||||
};
|
||||
|
||||
// Element IDs, pointers and handles arrive as Int64; print them in hex like a debugger would.
|
||||
private static string Format(string name, object? value) => value switch
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace WinUIProfiler.Engine.Events;
|
||||
|
||||
public readonly record struct EventField(string Name, object? Value);
|
||||
|
||||
// A copy of one XAML TraceLogging event. TraceEvent reuses its event objects, so the data is copied out.
|
||||
// A copy of one XAML TraceLogging event, or of one of the kernel's GPU memory events for the process. TraceEvent
|
||||
// reuses its event objects, so the data is copied out. For a GPU memory event, ProcessId is the process the
|
||||
// memory belongs to (its ProcessId field), and ThreadId is the thread that wrote the event.
|
||||
public sealed record ProfilerEvent(
|
||||
long TimestampQpc,
|
||||
double RelativeMilliseconds,
|
||||
|
||||
@@ -6,14 +6,18 @@ namespace WinUIProfiler.Engine.Events;
|
||||
public enum XamlProvider
|
||||
{
|
||||
Xaml,
|
||||
XamlProfiler
|
||||
XamlProfiler,
|
||||
// The kernel's GPU memory usage and budget for the process (Microsoft-Windows-DxgKrnl).
|
||||
DxgKrnl
|
||||
}
|
||||
|
||||
public static class XamlProviders
|
||||
{
|
||||
public static readonly Guid Xaml = new("531a35ab-63ce-4bcf-aa98-f88c7a89e455");
|
||||
public static readonly Guid XamlProfiler = new("a1b2c3d4-e5f6-4a5b-9c8d-7e6f5a4b3c2d");
|
||||
public static readonly Guid DxgKrnl = new("802ec45a-1e99-4b83-9920-87c98277ba9d");
|
||||
|
||||
// The two XAML providers. DxgKrnl events are matched by ID in XamlEventSubscription.
|
||||
public static bool TryGet(Guid providerId, out XamlProvider provider)
|
||||
{
|
||||
if (providerId == Xaml)
|
||||
|
||||
@@ -8,12 +8,15 @@ using WinUIProfiler.Engine.Events;
|
||||
|
||||
namespace WinUIProfiler.Engine.Ingestion;
|
||||
|
||||
// A real-time ETW session that reads one process's XAML TraceLogging events while the profiler is connected.
|
||||
// A real-time ETW session that reads one process's XAML TraceLogging events, and the kernel's GPU memory events
|
||||
// for it, while the profiler is connected.
|
||||
public sealed class LiveEtwSource : IDisposable
|
||||
{
|
||||
// The TraceLogging events read here have no keyword, so ETW always delivers them. No manifest event uses
|
||||
// this bit, so asking only for it stops the runtime from writing manifest events we would skip anyway.
|
||||
private const ulong TraceLoggingOnlyKeyword = 0x100000;
|
||||
// Microsoft-Windows-DxgKrnl keyword of the per-process GPU memory usage and budget events.
|
||||
private const ulong GpuMemoryKeyword = 0x4;
|
||||
private const string SessionPrefix = "WinUIProfiler-";
|
||||
|
||||
private readonly object gate = new();
|
||||
@@ -111,6 +114,7 @@ public sealed class LiveEtwSource : IDisposable
|
||||
// Open the consumer side before enabling, so events written before reading starts are kept.
|
||||
_ = session.Source;
|
||||
EnableProviders(session, options);
|
||||
EnableGpuMemory(session);
|
||||
return new(session);
|
||||
}
|
||||
catch
|
||||
@@ -126,6 +130,37 @@ public sealed class LiveEtwSource : IDisposable
|
||||
session.EnableProvider(XamlProviders.XamlProfiler, TraceEventLevel.Verbose, ulong.MaxValue, options);
|
||||
}
|
||||
|
||||
// The System process writes some of the target's GPU memory events (such as those written when it exits), so
|
||||
// the provider is enabled for every process and XamlEventSubscription keeps the target's by their ProcessId
|
||||
// field. A kernel provider writes only to sessions in nonpaged memory, which TraceEventSession uses. Best
|
||||
// effort: without these events the session still reads the XAML events.
|
||||
private static void EnableGpuMemory(TraceEventSession session)
|
||||
{
|
||||
try
|
||||
{
|
||||
session.EnableProvider(XamlProviders.DxgKrnl, TraceEventLevel.Verbose, GpuMemoryKeyword);
|
||||
}
|
||||
catch (Exception error) when (error is not OutOfMemoryException)
|
||||
{
|
||||
Trace.TraceWarning($"Could not enable the kernel's GPU memory events: {error.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// The kernel writes a process's current GPU memory usage and budget when the process opens a GPU adapter, and
|
||||
// for every process when a session requests capture state. Request it, so a process that opened its adapter
|
||||
// before the session started also gets its current values.
|
||||
private void CaptureGpuMemoryState()
|
||||
{
|
||||
try
|
||||
{
|
||||
session.CaptureState(XamlProviders.DxgKrnl, GpuMemoryKeyword);
|
||||
}
|
||||
catch (Exception error) when (error is not OutOfMemoryException)
|
||||
{
|
||||
Trace.TraceWarning($"Could not request the kernel's GPU memory state: {error.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// A session outlives a profiler that crashed or was killed, and keeps the providers enabled in the processes
|
||||
// it filters on (for a launch, every new process with that executable name). Sessions are named
|
||||
// WinUIProfiler-<profiler process ID>-<unique ID>; stop those whose profiler process no longer exists.
|
||||
@@ -170,6 +205,7 @@ public sealed class LiveEtwSource : IDisposable
|
||||
private void BeginReading(int processId, Action<ProfilerEvent> sink)
|
||||
{
|
||||
XamlEventSubscription.Subscribe(session.Source, processId, sink);
|
||||
CaptureGpuMemoryState();
|
||||
reader.Start();
|
||||
reading = true;
|
||||
}
|
||||
|
||||
@@ -6,21 +6,40 @@ using WinUIProfiler.Engine.Events;
|
||||
|
||||
namespace WinUIProfiler.Engine.Ingestion;
|
||||
|
||||
// Delivers one process's XAML TraceLogging events from a TraceEvent source (an .etl file now, a live session
|
||||
// later). TraceLogging events carry their own field names and types; manifest events are not read.
|
||||
// Delivers one process's XAML TraceLogging events, and the kernel's GPU memory events for it, from a TraceEvent
|
||||
// source (an .etl file or a live session). TraceLogging events carry their own field names and types; the kernel's
|
||||
// events are decoded with its installed manifest. Manifest events on the XAML providers are not read.
|
||||
internal static class XamlEventSubscription
|
||||
{
|
||||
// WINEVENT_CHANNEL_TRACELOGGING (winmeta.h). Manifest events on the same providers use other channels.
|
||||
private const int TraceLoggingChannel = 11;
|
||||
|
||||
// Microsoft-Windows-DxgKrnl VidMmProcessBudgetChange and VidMmProcessUsageChange: a process's GPU memory budget
|
||||
// and usage on one adapter and memory segment group, the values DXGI's QueryVideoMemoryInfo returns.
|
||||
private const int VidMmProcessBudgetChange = 366;
|
||||
private const int VidMmProcessUsageChange = 367;
|
||||
|
||||
internal static void Subscribe(TraceEventDispatcher source, int? processId, Action<ProfilerEvent> sink)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(sink);
|
||||
|
||||
// The dynamic parser also decodes manifest events when a manifest is registered, so filter by channel.
|
||||
source.Dynamic.All += data =>
|
||||
{
|
||||
if (data.ProviderGuid == XamlProviders.DxgKrnl)
|
||||
{
|
||||
// The System process writes some of a process's rows, such as those written when it exits, so the
|
||||
// ProcessId field names the process, not the event header.
|
||||
if ((int)data.ID is VidMmProcessBudgetChange or VidMmProcessUsageChange &&
|
||||
PayloadProcessId(data) is { } owner && (processId is null || owner == processId))
|
||||
{
|
||||
sink(Capture(data, XamlProvider.DxgKrnl, owner));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// The dynamic parser also decodes manifest events when a manifest is registered, so filter by channel.
|
||||
if ((int)data.Channel != TraceLoggingChannel || !XamlProviders.TryGet(data.ProviderGuid, out var provider))
|
||||
{
|
||||
return;
|
||||
@@ -31,16 +50,36 @@ internal static class XamlEventSubscription
|
||||
return;
|
||||
}
|
||||
|
||||
sink(Capture(data, provider));
|
||||
sink(Capture(data, provider, data.ProcessID));
|
||||
};
|
||||
}
|
||||
|
||||
private static ProfilerEvent Capture(TraceEvent data, XamlProvider provider)
|
||||
private static int? PayloadProcessId(TraceEvent data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var index = data.PayloadIndex("ProcessId");
|
||||
return index < 0 ? null : data.PayloadValue(index) switch
|
||||
{
|
||||
int value => value,
|
||||
uint value => (int)value,
|
||||
long value => (int)value,
|
||||
ulong value => (int)value,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
catch (Exception error) when (error is not OutOfMemoryException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ProfilerEvent Capture(TraceEvent data, XamlProvider provider, int processId)
|
||||
{
|
||||
#pragma warning disable CS0618 // The raw QPC timestamp is the clock shared by live sessions and .etl files.
|
||||
var timestampQpc = data.TimeStampQPC;
|
||||
#pragma warning restore CS0618
|
||||
return new(timestampQpc, data.TimeStampRelativeMSec, data.ProcessID, data.ThreadID, provider, data.EventName,
|
||||
return new(timestampQpc, data.TimeStampRelativeMSec, processId, data.ThreadID, provider, data.EventName,
|
||||
(int)data.Opcode, CopyFields(data));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,25 @@ public sealed class EventCounter : IEventProcessor
|
||||
{
|
||||
private long xamlEvents;
|
||||
private long profilerEvents;
|
||||
private long gpuMemoryEvents;
|
||||
|
||||
public long XamlEvents => Interlocked.Read(ref xamlEvents);
|
||||
public long ProfilerEvents => Interlocked.Read(ref profilerEvents);
|
||||
public long GpuMemoryEvents => Interlocked.Read(ref gpuMemoryEvents);
|
||||
|
||||
public void Process(long sequence, ProfilerEvent profilerEvent)
|
||||
{
|
||||
if (profilerEvent.Provider == XamlProvider.Xaml)
|
||||
switch (profilerEvent.Provider)
|
||||
{
|
||||
Interlocked.Increment(ref xamlEvents);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref profilerEvents);
|
||||
case XamlProvider.Xaml:
|
||||
Interlocked.Increment(ref xamlEvents);
|
||||
break;
|
||||
case XamlProvider.XamlProfiler:
|
||||
Interlocked.Increment(ref profilerEvents);
|
||||
break;
|
||||
case XamlProvider.DxgKrnl:
|
||||
Interlocked.Increment(ref gpuMemoryEvents);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ internal static class SessionSchema
|
||||
DroppedByProfiler INTEGER);
|
||||
CREATE TABLE EventTypes (
|
||||
TypeId INTEGER PRIMARY KEY,
|
||||
Provider TEXT NOT NULL,
|
||||
Provider TEXT NOT NULL, -- Xaml, XamlProfiler or DxgKrnl (XamlProvider)
|
||||
Name TEXT NOT NULL,
|
||||
Opcode INTEGER NOT NULL,
|
||||
FieldNames TEXT NOT NULL, -- comma-separated names; the Nth names the Nth value in Events.Payload
|
||||
|
||||
@@ -28,7 +28,8 @@ long? ExpectedCreationTimeFileTime = null,
|
||||
public sealed record ConnectionProgress(string State, string Detail);
|
||||
// FromProcessStart is true when the session was listening before the target was created, so its startup is
|
||||
// included; otherwise the counts start when the profiler connected.
|
||||
public sealed record EtwStatus(bool FromProcessStart, long XamlEvents, long ProfilerEvents, int LostEvents, long DroppedEvents);
|
||||
public sealed record EtwStatus(bool FromProcessStart, long XamlEvents, long ProfilerEvents, long GpuMemoryEvents, int LostEvents,
|
||||
long DroppedEvents);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class TargetConnection : IAsyncDisposable
|
||||
@@ -75,7 +76,8 @@ public sealed class TargetConnection : IAsyncDisposable
|
||||
? new DateTime(ticks, DateTimeKind.Utc) : null;
|
||||
public Task Completion => lifetime ?? Task.CompletedTask;
|
||||
public EtwStatus? Etw => eventCounter is { } counter && etw is { } source && pipeline is { } events
|
||||
? new(etwFromProcessStart, counter.XamlEvents, counter.ProfilerEvents, source.EventsLost, events.DroppedEvents) : null;
|
||||
? new(etwFromProcessStart, counter.XamlEvents, counter.ProfilerEvents, counter.GpuMemoryEvents, source.EventsLost,
|
||||
events.DroppedEvents) : null;
|
||||
// Set when the ETW session could not start or stopped with an error; process metrics keep working.
|
||||
public string? EtwError
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user