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:
Hemant Kumar authored and GitHub committed 2026-10-28 01:51:24 +05:30
commit b9fa30e549
11 files changed
+166 -44

No files matched your search

+31 -11
View File
@@ -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}&#10;Last TAP heartbeat {1}&#10;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}&#10;XAML: Microsoft-Windows-XAML events&#10;Profiler: Microsoft-Windows-XAML-Profiler events&#10;Lost: events ETW could not deliver&#10;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}&#10;XAML: Microsoft-Windows-XAML events&#10;Profiler: Microsoft-Windows-XAML-Profiler events&#10;GPU memory: the kernel's GPU memory usage and budget events for the process (Microsoft-Windows-DxgKrnl)&#10;Lost: events ETW could not deliver&#10;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
{