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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ auto-assigned by the broker (range 10223–10899), or configurable via `.mauidev
| `/api/profiler/span` | POST | Publish manual span `{"kind":"ui.operation","name":"...","startTsUtc":"...","endTsUtc":"..."}` |
| `/api/profiler/hotspots?kind=ui.operation&minDurationMs=16&limit=20` | GET | Aggregated slow-operation hotspots ordered by P95 duration |

Profiler sample payloads also include `nativeMemoryKind` to disambiguate what `nativeMemoryBytes` means for that sample. Current values include `apple.phys-footprint`, `android.native-heap-allocated`, `windows.working-set`, and `process.working-set-minus-managed` when the collector falls back to process working set minus managed memory.

## Project Structure

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public sealed class NativeFrameStatsSnapshot
public int JankFrameCount { get; set; }
public int UiThreadStallCount { get; set; }
public long? NativeMemoryBytes { get; set; }
public string? NativeMemoryKind { get; set; }
}

public interface INativeFrameStatsProvider : IDisposable
Expand Down
1 change: 1 addition & 0 deletions src/MauiDevFlow.Agent.Core/Profiling/ProfilerContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class ProfilerSample
public int Gc1 { get; set; }
public int Gc2 { get; set; }
public long? NativeMemoryBytes { get; set; }
public string? NativeMemoryKind { get; set; }
public double? CpuPercent { get; set; }
public int? ThreadCount { get; set; }
public int JankFrameCount { get; set; }
Expand Down
18 changes: 12 additions & 6 deletions src/MauiDevFlow.Agent.Core/Profiling/RuntimeProfilerCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ public bool TryCollect(out ProfilerSample sample)
sample.Gc0 = GC.CollectionCount(0);
sample.Gc1 = GC.CollectionCount(1);
sample.Gc2 = GC.CollectionCount(2);
sample.NativeMemoryBytes ??= TryReadNativeMemoryBytes(processSnapshotAvailable, sample.ManagedBytes);
if (!sample.NativeMemoryBytes.HasValue)
{
var nativeMemory = TryReadNativeMemory(processSnapshotAvailable, sample.ManagedBytes);
sample.NativeMemoryBytes = nativeMemory.Bytes;
sample.NativeMemoryKind = nativeMemory.Kind;
}
sample.CpuPercent = cpuPercent;
sample.ThreadCount = threadCount;

Expand All @@ -158,6 +163,7 @@ private ProfilerSample BuildFrameSample(DateTime now)
JankFrameCount = nativeSnapshot.JankFrameCount,
UiThreadStallCount = nativeSnapshot.UiThreadStallCount,
NativeMemoryBytes = nativeSnapshot.NativeMemoryBytes,
NativeMemoryKind = nativeSnapshot.NativeMemoryKind,
FrameSource = nativeSnapshot.Source,
FrameQuality = _nativeFrameStatsProvider.ProvidesExactFrameTimings
? "native.exact"
Expand Down Expand Up @@ -255,26 +261,26 @@ ex is InvalidOperationException
}
}

private long? TryReadNativeMemoryBytes(bool processSnapshotAvailable, long managedBytes)
private (long? Bytes, string? Kind) TryReadNativeMemory(bool processSnapshotAvailable, long managedBytes)
{
if (!_capabilities.NativeMemorySupported || !processSnapshotAvailable)
return null;
return (null, null);

try
{
var workingSetBytes = _process.WorkingSet64;
if (workingSetBytes <= 0)
return null;
return (null, null);

return Math.Max(0L, workingSetBytes - managedBytes);
return (Math.Max(0L, workingSetBytes - managedBytes), "process.working-set-minus-managed");
}
catch (Exception ex) when (
ex is InvalidOperationException
|| ex is NotSupportedException
|| ex is PlatformNotSupportedException)
{
_capabilities.NativeMemorySupported = false;
return null;
return (null, null);
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/MauiDevFlow.Agent/Profiling/NativeFrameStatsProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
return false;

snapshot.NativeMemoryBytes = TryReadAndroidNativeMemoryBytes();
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
? "android.native-heap-allocated"
: null;
return true;
}

Expand Down Expand Up @@ -278,6 +281,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
return false;

snapshot.NativeMemoryBytes = TryReadAndroidNativeMemoryBytes();
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
? "android.native-heap-allocated"
: null;
return true;
}

Expand Down Expand Up @@ -386,6 +392,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
return false;

snapshot.NativeMemoryBytes = TryReadPhysFootprint();
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
? "apple.phys-footprint"
: null;
return true;
}

Expand Down Expand Up @@ -527,6 +536,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
return false;

snapshot.NativeMemoryBytes = TryReadResidentMemoryBytes();
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
? "windows.working-set"
: null;
return true;
}

Expand Down
2 changes: 2 additions & 0 deletions src/MauiDevFlow.Driver/AgentClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ public class ProfilerSample
public int Gc2 { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("nativeMemoryBytes")]
public long? NativeMemoryBytes { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("nativeMemoryKind")]
public string? NativeMemoryKind { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("cpuPercent")]
public double? CpuPercent { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("threadCount")]
Expand Down
2 changes: 2 additions & 0 deletions tests/MauiDevFlow.Tests/ProfilerAgentClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public async Task Profiler_StartStopAndPollFlow_WorksThroughAgentClient()
"worstFrameTimeMs": 48.2,
"managedBytes": 2048,
"nativeMemoryBytes": 8192,
"nativeMemoryKind": "android.native-heap-allocated",
"gc0": 1,
"gc1": 0,
"gc2": 0,
Expand Down Expand Up @@ -130,6 +131,7 @@ public async Task Profiler_StartStopAndPollFlow_WorksThroughAgentClient()
Assert.Equal("native.android.choreographer", batch.Samples[0].FrameSource);
Assert.Equal(3, batch.Samples[0].JankFrameCount);
Assert.Equal(8192, batch.Samples[0].NativeMemoryBytes);
Assert.Equal("android.native-heap-allocated", batch.Samples[0].NativeMemoryKind);
Assert.Equal(1, batch.SampleCursor);
Assert.Equal(1, batch.MarkerCursor);
Assert.Equal(1, batch.SpanCursor);
Expand Down
66 changes: 66 additions & 0 deletions tests/MauiDevFlow.Tests/ProfilerCoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public void ProfilerBatch_SerializesAndDeserializes()
WorstFrameTimeMs = 31.2,
ManagedBytes = 123_456,
NativeMemoryBytes = 654_321,
NativeMemoryKind = "android.native-heap-allocated",
Gc0 = 10,
Gc1 = 4,
Gc2 = 1,
Expand Down Expand Up @@ -79,6 +80,7 @@ public void ProfilerBatch_SerializesAndDeserializes()
Assert.Equal(4, parsed.SpanCursor);
Assert.Equal(123_456, parsed.Samples[0].ManagedBytes);
Assert.Equal(654_321, parsed.Samples[0].NativeMemoryBytes);
Assert.Equal("android.native-heap-allocated", parsed.Samples[0].NativeMemoryKind);
Assert.Equal("native.android.choreographer", parsed.Samples[0].FrameSource);
Assert.Equal(2, parsed.Samples[0].JankFrameCount);
}
Expand Down Expand Up @@ -175,6 +177,10 @@ public void RuntimeProfilerCollector_CollectsRuntimeMetrics()
Assert.StartsWith("estimated", sample1.FrameQuality);
Assert.True(sample1.Fps > 0);
Assert.True(sample1.FrameTimeMsP95 > 0);
if (sample1.NativeMemoryBytes.HasValue)
Assert.Equal("process.working-set-minus-managed", sample1.NativeMemoryKind);
else
Assert.Null(sample1.NativeMemoryKind);
Assert.True(sample2.TsUtc > sample1.TsUtc);
}

Expand Down Expand Up @@ -213,6 +219,30 @@ public void RuntimeProfilerCollector_WhenNativeProviderStartFails_CleansUpAndFal
Assert.False(capabilities.UiThreadStallSupported);
}

[Fact]
public void RuntimeProfilerCollector_PropagatesNativeMemoryKindFromProvider()
{
var provider = new SnapshotNativeProvider(new NativeFrameStatsSnapshot
{
Source = "native.test",
Fps = 60,
FrameTimeMsP50 = 16.7,
FrameTimeMsP95 = 20.5,
WorstFrameTimeMs = 24.1,
NativeMemoryBytes = 42_000,
NativeMemoryKind = "apple.phys-footprint"
});
var collector = new RuntimeProfilerCollector(provider);

collector.Start(100);
var collected = collector.TryCollect(out var sample);
collector.Stop();

Assert.True(collected);
Assert.Equal(42_000, sample.NativeMemoryBytes);
Assert.Equal("apple.phys-footprint", sample.NativeMemoryKind);
}

[Fact]
public void ProfilerContractModels_StayAlignedWithDriverModels()
{
Expand Down Expand Up @@ -351,4 +381,40 @@ public void Dispose()
{
}
}

private sealed class SnapshotNativeProvider(NativeFrameStatsSnapshot snapshotToReturn) : INativeFrameStatsProvider
{
public bool IsSupported => true;
public bool ProvidesExactFrameTimings => true;
public string Source => snapshotToReturn.Source;

public void Start()
{
}

public void Stop()
{
}

public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
{
snapshot = new NativeFrameStatsSnapshot
{
Source = snapshotToReturn.Source,
Fps = snapshotToReturn.Fps,
FrameTimeMsP50 = snapshotToReturn.FrameTimeMsP50,
FrameTimeMsP95 = snapshotToReturn.FrameTimeMsP95,
WorstFrameTimeMs = snapshotToReturn.WorstFrameTimeMs,
JankFrameCount = snapshotToReturn.JankFrameCount,
UiThreadStallCount = snapshotToReturn.UiThreadStallCount,
NativeMemoryBytes = snapshotToReturn.NativeMemoryBytes,
NativeMemoryKind = snapshotToReturn.NativeMemoryKind
};
return true;
}

public void Dispose()
{
}
}
}
Loading