Skip to content

Commit 8f5606b

Browse files
committed
Done
1 parent 20dfe87 commit 8f5606b

5 files changed

Lines changed: 122 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## [56.1.0]
2-
- Test
2+
- [LayoutDiagnostics] Added runtime layout diagnostics API for profiling measure/arrange counts per element type. Includes a floating overlay visible over modals and bottom sheets, automatic snapshot capture during page and bottom sheet lifecycle, per-instance thrashing detection, and JSON export.
33

44
## [56.0.2]
55
- [iOS][ItemPicker] Fixed bug where selected item could reset randomly

src/library/DIPS.Mobile.UI/API/Diagnostics/Android/LayoutDiagnosticsService.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,9 @@ private static void AddOverlayToActivityContent()
9393

9494
private static Android.Widget.FrameLayout.LayoutParams CreateOverlayLayoutParams()
9595
{
96-
var activity = Platform.CurrentActivity;
97-
var density = activity?.Resources?.DisplayMetrics?.Density ?? 1;
98-
9996
return new Android.Widget.FrameLayout.LayoutParams(
100-
ViewGroup.LayoutParams.WrapContent,
101-
ViewGroup.LayoutParams.WrapContent,
102-
GravityFlags.Top | GravityFlags.End)
103-
{
104-
TopMargin = (int)(54 * density),
105-
RightMargin = (int)(8 * density)
106-
};
97+
ViewGroup.LayoutParams.MatchParent,
98+
ViewGroup.LayoutParams.MatchParent);
10799
}
108100

109101
private static partial void UpdateOverlay(LayoutDiagnosticsSnapshot snapshot)

src/library/DIPS.Mobile.UI/API/Diagnostics/LayoutDiagnosticsOverlay.cs

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
using DIPS.Mobile.UI.Resources.Styles;
55
using DIPS.Mobile.UI.Resources.Styles.Button;
66
using DIPS.Mobile.UI.Resources.Styles.Label;
7+
using Microsoft.Maui.Controls.Shapes;
78
using Colors = DIPS.Mobile.UI.Resources.Colors.Colors;
8-
using LayoutEffect = DIPS.Mobile.UI.Effects.Layout.Layout;
99

1010
namespace DIPS.Mobile.UI.API.Diagnostics;
1111

@@ -19,8 +19,8 @@ internal class LayoutDiagnosticsOverlay : Grid
1919
private readonly Label m_detailLabel;
2020
private readonly Button m_toggleButton;
2121
private readonly Grid m_expandedContent;
22-
private readonly Grid m_collapsedPill;
23-
private readonly Grid m_expandedPanel;
22+
private readonly Border m_collapsedPill;
23+
private readonly Border m_expandedPanel;
2424
private readonly Label m_collapsedCountLabel;
2525
private readonly BoxView m_recordingDot;
2626
private readonly BoxView m_expandedRecordingDot;
@@ -33,15 +33,6 @@ public LayoutDiagnosticsOverlay()
3333
{
3434
InputTransparent = false;
3535
CascadeInputTransparent = false;
36-
37-
HorizontalOptions = LayoutOptions.End;
38-
VerticalOptions = LayoutOptions.Start;
39-
40-
Margin = new Thickness(
41-
Sizes.GetSize(SizeName.content_margin_small),
42-
54,
43-
Sizes.GetSize(SizeName.content_margin_small),
44-
0);
4536

4637
// ── Collapsed pill ──
4738
m_recordingDot = new BoxView
@@ -70,16 +61,19 @@ public LayoutDiagnosticsOverlay()
7061
Children = { m_recordingDot, m_collapsedCountLabel }
7162
};
7263

73-
m_collapsedPill = new Grid
64+
m_collapsedPill = new Border
7465
{
7566
BackgroundColor = Colors.GetColor(ColorName.color_surface_backdrop),
7667
Padding = new Thickness(
77-
Sizes.GetSize(SizeName.content_margin_medium),
78-
Sizes.GetSize(SizeName.content_margin_small)),
79-
HorizontalOptions = LayoutOptions.End
68+
Sizes.GetSize(SizeName.content_margin_small),
69+
Sizes.GetSize(SizeName.size_1)),
70+
HorizontalOptions = LayoutOptions.End,
71+
VerticalOptions = LayoutOptions.Start,
72+
Margin = new Thickness(0, 54, Sizes.GetSize(SizeName.content_margin_small), 0),
73+
Stroke = Brush.Transparent,
74+
StrokeShape = new RoundRectangle { CornerRadius = new CornerRadius(Sizes.GetSize(SizeName.radius_xlarge)) }
8075
};
81-
LayoutEffect.SetCornerRadius(m_collapsedPill, new CornerRadius(Sizes.GetSize(SizeName.radius_xlarge)));
82-
m_collapsedPill.Add(pillContent);
76+
m_collapsedPill.Content = pillContent;
8377

8478
var collapsedTap = new TapGestureRecognizer();
8579
collapsedTap.Tapped += async (_, _) => await AnimateExpand();
@@ -139,9 +133,7 @@ public LayoutDiagnosticsOverlay()
139133
Style = Styles.GetButtonStyle(ButtonStyle.CloseIconSmall),
140134
VerticalOptions = LayoutOptions.Center
141135
};
142-
var collapseTap = new TapGestureRecognizer();
143-
collapseTap.Tapped += async (_, _) => await AnimateCollapse();
144-
collapseButton.GestureRecognizers.Add(collapseTap);
136+
collapseButton.Clicked += async (_, _) => await AnimateCollapse();
145137

146138
var textStack = new VerticalStackLayout { Spacing = Sizes.GetSize(SizeName.size_half) };
147139
textStack.Add(statusRow);
@@ -173,13 +165,16 @@ public LayoutDiagnosticsOverlay()
173165
m_expandedContent.Add(buttonRow);
174166
Grid.SetColumn(buttonRow, 1);
175167

176-
m_expandedPanel = new Grid
168+
m_expandedPanel = new Border
177169
{
178170
BackgroundColor = Colors.GetColor(ColorName.color_surface_backdrop),
179-
HorizontalOptions = LayoutOptions.End
171+
HorizontalOptions = LayoutOptions.End,
172+
VerticalOptions = LayoutOptions.Start,
173+
Margin = new Thickness(0, 54, Sizes.GetSize(SizeName.content_margin_small), 0),
174+
Stroke = Brush.Transparent,
175+
StrokeShape = new RoundRectangle { CornerRadius = new CornerRadius(Sizes.GetSize(SizeName.radius_large)) }
180176
};
181-
LayoutEffect.SetCornerRadius(m_expandedPanel, new CornerRadius(Sizes.GetSize(SizeName.radius_large)));
182-
m_expandedPanel.Add(m_expandedContent);
177+
m_expandedPanel.Content = m_expandedContent;
183178

184179
this.Add(m_collapsedPill);
185180
this.Add(m_expandedPanel);

src/library/DIPS.Mobile.UI/API/Diagnostics/iOS/LayoutDiagnosticsService.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ private static partial void AttachOverlay()
3636

3737
rootVc.View!.AddSubview(uiView);
3838

39+
// Pin to all edges — MAUI's layout system positions content internally
40+
// via HorizontalOptions.End + VerticalOptions.Start + Margin on the overlay
3941
NSLayoutConstraint.ActivateConstraints([
40-
uiView.TopAnchor.ConstraintEqualTo(rootVc.View.SafeAreaLayoutGuide.TopAnchor, 8),
41-
uiView.TrailingAnchor.ConstraintEqualTo(rootVc.View.SafeAreaLayoutGuide.TrailingAnchor, -8)
42+
uiView.TopAnchor.ConstraintEqualTo(rootVc.View.TopAnchor),
43+
uiView.LeadingAnchor.ConstraintEqualTo(rootVc.View.LeadingAnchor),
44+
uiView.TrailingAnchor.ConstraintEqualTo(rootVc.View.TrailingAnchor),
45+
uiView.BottomAnchor.ConstraintEqualTo(rootVc.View.BottomAnchor)
4246
]);
4347
}
4448

@@ -88,7 +92,9 @@ private class OverlayWindow(UIWindowScene scene) : UIWindow(scene)
8892
public override UIView? HitTest(CGPoint point, UIEvent? uievent)
8993
{
9094
var hit = base.HitTest(point, uievent);
91-
if (hit is null || hit == this || hit == RootViewController?.View)
95+
// Pass through touches on empty areas: window, VC view, or the full-screen MAUI container
96+
if (hit is null || hit == this || hit == RootViewController?.View
97+
|| hit.Tag == LayoutDiagnosticsOverlay.OverlayIdentifier)
9298
return null;
9399
return hit;
94100
}

wiki/Layout-Diagnostics.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
A runtime diagnostics tool for profiling .NET MAUI layout performance on iOS and Android. It captures per-element **measure** and **arrange** counts during page navigation and bottom sheet presentation, helping developers identify layout thrashing — views that are measured or arranged far more than necessary.
2+
3+
# How It Works
4+
5+
.NET MAUI's layout system uses a two-phase process for every frame where layout is needed ([source](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/layouts/custom#layout-process)):
6+
7+
1. **Measure phase** — The framework calls `IView.Measure()` on each view to determine its desired size given constraints. Layouts call `Measure()` on all their children recursively, bottom-up.
8+
2. **Arrange phase** — The framework calls `IView.Arrange()` on each view to assign its final position and bounds within the parent layout.
9+
10+
These phases can be triggered multiple times per frame. A parent layout may speculatively measure children, change constraints, and re-measure. Deeply nested layouts with complex constraints can cause exponential measure/arrange calls — known as **layout thrashing**.
11+
12+
Layout Diagnostics hooks into MAUI's built-in `System.Diagnostics.Metrics` instruments (`maui.layout.measure_count`, `maui.layout.arrange_count`) via `MeterListener` to count every measure and arrange call per element type and instance.
13+
14+
# Usage
15+
16+
## Prerequisites
17+
18+
Register metrics in `MauiProgram.cs` **before** `.UseMauiApp<App>()`:
19+
20+
```csharp
21+
builder.Services.AddMetrics(); // Microsoft.Extensions.Diagnostics
22+
```
23+
24+
## Initialize
25+
26+
```csharp
27+
// Show the floating overlay (dormant — not yet recording)
28+
LayoutDiagnosticsService.Initialize();
29+
```
30+
31+
## API
32+
33+
| Member | Description |
34+
|---|---|
35+
| `Initialize()` | Shows the floating overlay pill |
36+
| `Teardown()` | Removes the overlay and tears down the system |
37+
| `Enable()` | Starts capturing layout metrics |
38+
| `Disable()` | Stops capturing, returns all snapshots |
39+
| `Toggle()` | Toggles between Enable/Disable |
40+
| `ClearSnapshots()` | Clears all completed snapshots |
41+
| `ExportAllToJson()` | Exports all snapshots as a JSON string |
42+
| `IsEnabled` | Whether diagnostics are currently capturing |
43+
| `IsInitialized` | Whether the overlay has been initialized |
44+
| `CompletedSnapshots` | All snapshots captured during this session |
45+
| `CurrentSnapshot` | The in-progress snapshot (null if none) |
46+
| `SnapshotCompleted` | Event raised when a snapshot finishes |
47+
48+
## Automatic Snapshots
49+
50+
Snapshots are automatically created and ended during page and bottom sheet lifecycle:
51+
52+
- `ContentPage.OnNavigatedTo()``BeginSnapshot("Page: {TypeName}")`
53+
- `ContentPage.OnDisappearing()``EndSnapshot()`
54+
- `BottomSheet.OnAppearing()``BeginSnapshot("BottomSheet: {TypeName}")`
55+
- `BottomSheet.OnDisappearing()``EndSnapshot()`
56+
57+
## Reading Snapshot Data
58+
59+
Each `LayoutDiagnosticsSnapshot` contains:
60+
61+
- **`SourceName`** — What triggered it (e.g., "Page: HomePage")
62+
- **`TotalMeasureCount` / `TotalArrangeCount`** — Aggregate counts
63+
- **`MeasureCountsByType` / `ArrangeCountsByType`** — Per-type breakdown (e.g., `"Microsoft.Maui.Controls.Label": 45`)
64+
- **`MeasureInstancesByType`** — Unique instances per type, enabling per-instance averages
65+
- **`Warnings`** — Types exceeding an average of 3 layout passes per instance
66+
67+
## Interpreting Results
68+
69+
| Metric | Healthy | Investigate |
70+
|---|---|---|
71+
| Avg measures per instance | 1–2× | > 3× (flagged with ⚠️) |
72+
| Total measures for a page | < 200 | > 500 |
73+
74+
Common causes of high measure counts:
75+
- Deeply nested layouts (Grid inside StackLayout inside ScrollView)
76+
- `Auto`-sized rows/columns that cause re-measure cascades
77+
- Changing `IsVisible` or `Text` on elements during layout
78+
79+
## Overlay
80+
81+
The diagnostics overlay is a floating pill that stays visible over all content, including modals and bottom sheets:
82+
83+
- **iOS**: Rendered in a separate `UIWindow` at `UIWindowLevel.Alert - 1`
84+
- **Android**: Uses `TranslationZ` for z-ordering and re-parents to dialog `DecorView` when dialogs open
85+
86+
Tap the pill to expand and see live recording status, start/stop controls, and the current snapshot summary.
87+
88+
# Properties
89+
90+
Inspect the [`LayoutDiagnosticsService`](https://github.com/DIPSAS/DIPS.Mobile.UI/blob/main/src/library/DIPS.Mobile.UI/API/Diagnostics/LayoutDiagnosticsService.cs) and [`LayoutDiagnosticsSnapshot`](https://github.com/DIPSAS/DIPS.Mobile.UI/blob/main/src/library/DIPS.Mobile.UI/API/Diagnostics/LayoutDiagnosticsSnapshot.cs) classes for the full API surface.

0 commit comments

Comments
 (0)