Skip to content

Commit f99ba3a

Browse files
authored
Merge pull request #31 from Redth/copilot/profiler-native-memory-kind
Clarify profiler native memory semantics
2 parents dece52c + 7a77cf9 commit f99ba3a

File tree

8 files changed

+98
-6
lines changed

8 files changed

+98
-6
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ auto-assigned by the broker (range 10223–10899), or configurable via `.mauidev
333333
| `/api/profiler/span` | POST | Publish manual span `{"kind":"ui.operation","name":"...","startTsUtc":"...","endTsUtc":"..."}` |
334334
| `/api/profiler/hotspots?kind=ui.operation&minDurationMs=16&limit=20` | GET | Aggregated slow-operation hotspots ordered by P95 duration |
335335

336+
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.
337+
336338
## Project Structure
337339

338340
```

src/MauiDevFlow.Agent.Core/Profiling/INativeFrameStatsProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public sealed class NativeFrameStatsSnapshot
1111
public int JankFrameCount { get; set; }
1212
public int UiThreadStallCount { get; set; }
1313
public long? NativeMemoryBytes { get; set; }
14+
public string? NativeMemoryKind { get; set; }
1415
}
1516

1617
public interface INativeFrameStatsProvider : IDisposable

src/MauiDevFlow.Agent.Core/Profiling/ProfilerContracts.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class ProfilerSample
2020
public int Gc1 { get; set; }
2121
public int Gc2 { get; set; }
2222
public long? NativeMemoryBytes { get; set; }
23+
public string? NativeMemoryKind { get; set; }
2324
public double? CpuPercent { get; set; }
2425
public int? ThreadCount { get; set; }
2526
public int JankFrameCount { get; set; }

src/MauiDevFlow.Agent.Core/Profiling/RuntimeProfilerCollector.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ public bool TryCollect(out ProfilerSample sample)
133133
sample.Gc0 = GC.CollectionCount(0);
134134
sample.Gc1 = GC.CollectionCount(1);
135135
sample.Gc2 = GC.CollectionCount(2);
136-
sample.NativeMemoryBytes ??= TryReadNativeMemoryBytes(processSnapshotAvailable, sample.ManagedBytes);
136+
if (!sample.NativeMemoryBytes.HasValue)
137+
{
138+
var nativeMemory = TryReadNativeMemory(processSnapshotAvailable, sample.ManagedBytes);
139+
sample.NativeMemoryBytes = nativeMemory.Bytes;
140+
sample.NativeMemoryKind = nativeMemory.Kind;
141+
}
137142
sample.CpuPercent = cpuPercent;
138143
sample.ThreadCount = threadCount;
139144

@@ -158,6 +163,7 @@ private ProfilerSample BuildFrameSample(DateTime now)
158163
JankFrameCount = nativeSnapshot.JankFrameCount,
159164
UiThreadStallCount = nativeSnapshot.UiThreadStallCount,
160165
NativeMemoryBytes = nativeSnapshot.NativeMemoryBytes,
166+
NativeMemoryKind = nativeSnapshot.NativeMemoryKind,
161167
FrameSource = nativeSnapshot.Source,
162168
FrameQuality = _nativeFrameStatsProvider.ProvidesExactFrameTimings
163169
? "native.exact"
@@ -255,26 +261,26 @@ ex is InvalidOperationException
255261
}
256262
}
257263

258-
private long? TryReadNativeMemoryBytes(bool processSnapshotAvailable, long managedBytes)
264+
private (long? Bytes, string? Kind) TryReadNativeMemory(bool processSnapshotAvailable, long managedBytes)
259265
{
260266
if (!_capabilities.NativeMemorySupported || !processSnapshotAvailable)
261-
return null;
267+
return (null, null);
262268

263269
try
264270
{
265271
var workingSetBytes = _process.WorkingSet64;
266272
if (workingSetBytes <= 0)
267-
return null;
273+
return (null, null);
268274

269-
return Math.Max(0L, workingSetBytes - managedBytes);
275+
return (Math.Max(0L, workingSetBytes - managedBytes), "process.working-set-minus-managed");
270276
}
271277
catch (Exception ex) when (
272278
ex is InvalidOperationException
273279
|| ex is NotSupportedException
274280
|| ex is PlatformNotSupportedException)
275281
{
276282
_capabilities.NativeMemorySupported = false;
277-
return null;
283+
return (null, null);
278284
}
279285
}
280286

src/MauiDevFlow.Agent/Profiling/NativeFrameStatsProviderFactory.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
182182
return false;
183183

184184
snapshot.NativeMemoryBytes = TryReadAndroidNativeMemoryBytes();
185+
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
186+
? "android.native-heap-allocated"
187+
: null;
185188
return true;
186189
}
187190

@@ -278,6 +281,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
278281
return false;
279282

280283
snapshot.NativeMemoryBytes = TryReadAndroidNativeMemoryBytes();
284+
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
285+
? "android.native-heap-allocated"
286+
: null;
281287
return true;
282288
}
283289

@@ -386,6 +392,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
386392
return false;
387393

388394
snapshot.NativeMemoryBytes = TryReadPhysFootprint();
395+
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
396+
? "apple.phys-footprint"
397+
: null;
389398
return true;
390399
}
391400

@@ -527,6 +536,9 @@ public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
527536
return false;
528537

529538
snapshot.NativeMemoryBytes = TryReadResidentMemoryBytes();
539+
snapshot.NativeMemoryKind = snapshot.NativeMemoryBytes.HasValue
540+
? "windows.working-set"
541+
: null;
530542
return true;
531543
}
532544

src/MauiDevFlow.Driver/AgentClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,8 @@ public class ProfilerSample
464464
public int Gc2 { get; set; }
465465
[System.Text.Json.Serialization.JsonPropertyName("nativeMemoryBytes")]
466466
public long? NativeMemoryBytes { get; set; }
467+
[System.Text.Json.Serialization.JsonPropertyName("nativeMemoryKind")]
468+
public string? NativeMemoryKind { get; set; }
467469
[System.Text.Json.Serialization.JsonPropertyName("cpuPercent")]
468470
public double? CpuPercent { get; set; }
469471
[System.Text.Json.Serialization.JsonPropertyName("threadCount")]

tests/MauiDevFlow.Tests/ProfilerAgentClientTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public async Task Profiler_StartStopAndPollFlow_WorksThroughAgentClient()
5454
"worstFrameTimeMs": 48.2,
5555
"managedBytes": 2048,
5656
"nativeMemoryBytes": 8192,
57+
"nativeMemoryKind": "android.native-heap-allocated",
5758
"gc0": 1,
5859
"gc1": 0,
5960
"gc2": 0,
@@ -130,6 +131,7 @@ public async Task Profiler_StartStopAndPollFlow_WorksThroughAgentClient()
130131
Assert.Equal("native.android.choreographer", batch.Samples[0].FrameSource);
131132
Assert.Equal(3, batch.Samples[0].JankFrameCount);
132133
Assert.Equal(8192, batch.Samples[0].NativeMemoryBytes);
134+
Assert.Equal("android.native-heap-allocated", batch.Samples[0].NativeMemoryKind);
133135
Assert.Equal(1, batch.SampleCursor);
134136
Assert.Equal(1, batch.MarkerCursor);
135137
Assert.Equal(1, batch.SpanCursor);

tests/MauiDevFlow.Tests/ProfilerCoreTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public void ProfilerBatch_SerializesAndDeserializes()
3030
WorstFrameTimeMs = 31.2,
3131
ManagedBytes = 123_456,
3232
NativeMemoryBytes = 654_321,
33+
NativeMemoryKind = "android.native-heap-allocated",
3334
Gc0 = 10,
3435
Gc1 = 4,
3536
Gc2 = 1,
@@ -79,6 +80,7 @@ public void ProfilerBatch_SerializesAndDeserializes()
7980
Assert.Equal(4, parsed.SpanCursor);
8081
Assert.Equal(123_456, parsed.Samples[0].ManagedBytes);
8182
Assert.Equal(654_321, parsed.Samples[0].NativeMemoryBytes);
83+
Assert.Equal("android.native-heap-allocated", parsed.Samples[0].NativeMemoryKind);
8284
Assert.Equal("native.android.choreographer", parsed.Samples[0].FrameSource);
8385
Assert.Equal(2, parsed.Samples[0].JankFrameCount);
8486
}
@@ -175,6 +177,10 @@ public void RuntimeProfilerCollector_CollectsRuntimeMetrics()
175177
Assert.StartsWith("estimated", sample1.FrameQuality);
176178
Assert.True(sample1.Fps > 0);
177179
Assert.True(sample1.FrameTimeMsP95 > 0);
180+
if (sample1.NativeMemoryBytes.HasValue)
181+
Assert.Equal("process.working-set-minus-managed", sample1.NativeMemoryKind);
182+
else
183+
Assert.Null(sample1.NativeMemoryKind);
178184
Assert.True(sample2.TsUtc > sample1.TsUtc);
179185
}
180186

@@ -213,6 +219,30 @@ public void RuntimeProfilerCollector_WhenNativeProviderStartFails_CleansUpAndFal
213219
Assert.False(capabilities.UiThreadStallSupported);
214220
}
215221

222+
[Fact]
223+
public void RuntimeProfilerCollector_PropagatesNativeMemoryKindFromProvider()
224+
{
225+
var provider = new SnapshotNativeProvider(new NativeFrameStatsSnapshot
226+
{
227+
Source = "native.test",
228+
Fps = 60,
229+
FrameTimeMsP50 = 16.7,
230+
FrameTimeMsP95 = 20.5,
231+
WorstFrameTimeMs = 24.1,
232+
NativeMemoryBytes = 42_000,
233+
NativeMemoryKind = "apple.phys-footprint"
234+
});
235+
var collector = new RuntimeProfilerCollector(provider);
236+
237+
collector.Start(100);
238+
var collected = collector.TryCollect(out var sample);
239+
collector.Stop();
240+
241+
Assert.True(collected);
242+
Assert.Equal(42_000, sample.NativeMemoryBytes);
243+
Assert.Equal("apple.phys-footprint", sample.NativeMemoryKind);
244+
}
245+
216246
[Fact]
217247
public void ProfilerContractModels_StayAlignedWithDriverModels()
218248
{
@@ -351,4 +381,40 @@ public void Dispose()
351381
{
352382
}
353383
}
384+
385+
private sealed class SnapshotNativeProvider(NativeFrameStatsSnapshot snapshotToReturn) : INativeFrameStatsProvider
386+
{
387+
public bool IsSupported => true;
388+
public bool ProvidesExactFrameTimings => true;
389+
public string Source => snapshotToReturn.Source;
390+
391+
public void Start()
392+
{
393+
}
394+
395+
public void Stop()
396+
{
397+
}
398+
399+
public bool TryCollect(out NativeFrameStatsSnapshot snapshot)
400+
{
401+
snapshot = new NativeFrameStatsSnapshot
402+
{
403+
Source = snapshotToReturn.Source,
404+
Fps = snapshotToReturn.Fps,
405+
FrameTimeMsP50 = snapshotToReturn.FrameTimeMsP50,
406+
FrameTimeMsP95 = snapshotToReturn.FrameTimeMsP95,
407+
WorstFrameTimeMs = snapshotToReturn.WorstFrameTimeMs,
408+
JankFrameCount = snapshotToReturn.JankFrameCount,
409+
UiThreadStallCount = snapshotToReturn.UiThreadStallCount,
410+
NativeMemoryBytes = snapshotToReturn.NativeMemoryBytes,
411+
NativeMemoryKind = snapshotToReturn.NativeMemoryKind
412+
};
413+
return true;
414+
}
415+
416+
public void Dispose()
417+
{
418+
}
419+
}
354420
}

0 commit comments

Comments
 (0)