Skip to content

Commit a3ee42a

Browse files
Vetle444CopilotCopilot
authored
[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. (#849)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent a38861c commit a3ee42a

25 files changed

Lines changed: 2045 additions & 19 deletions
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Enable Layout Diagnostics
2+
3+
Enable DIPS.Mobile.UI layout diagnostics in this app to track measure/arrange counts per page and bottom sheet.
4+
5+
## What you get
6+
- A floating overlay pill (tap to expand) with Start/Stop recording and live snapshot info
7+
- A Diagnostics tab in the Shell TabBar showing all captured snapshots, with warnings for types that have excessive layout passes
8+
- JSON export of all snapshots via `LayoutDiagnosticsService.ExportAllToJson()`
9+
10+
## Steps
11+
12+
### 1. Add the metrics package
13+
14+
Add `<PackageReference Include="Microsoft.Extensions.Diagnostics" />` to the app's `.csproj`. If `Directory.Packages.props` is used, add it there with a version (e.g. `10.0.0`) and reference without version in the `.csproj`.
15+
16+
### 2. Register metrics in MauiProgram.cs
17+
18+
Add `builder.Services.AddMetrics()` **before** `.UseMauiApp<App>()`. This registers `IMeterFactory` which MAUI's diagnostics system requires to create its layout instruments.
19+
20+
```csharp
21+
var builder = MauiApp.CreateBuilder();
22+
builder.Services.AddMetrics(); // Required for MAUI diagnostics metrics
23+
builder
24+
.UseMauiApp<App>()
25+
.UseDIPSUI(configurator => { ... });
26+
```
27+
28+
### 3. Initialize the overlay
29+
30+
In `App.xaml.cs` (or wherever the Shell is constructed), call `LayoutDiagnosticsService.Initialize()` — this shows the floating overlay pill.
31+
32+
```csharp
33+
using DIPS.Mobile.UI.API.Diagnostics;
34+
35+
// After creating the Shell:
36+
LayoutDiagnosticsService.Initialize();
37+
```
38+
39+
Optionally, build a diagnostics tab using the API (see reference implementations in `src/app/Components/DiagnosticsSamples/LayoutDiagnosticsPage.xaml` or `src/app/Playground/DiagnosticsSamples/LayoutDiagnosticsPage.xaml`).
40+
41+
### 4. Usage
42+
43+
1. Tap the overlay pill → expand → tap **Start**
44+
2. Navigate between pages and open/close bottom sheets — each transition captures a snapshot
45+
3. Go to the Diagnostics tab to see all snapshots with per-type measure/arrange counts
46+
4. Types with avg > 3 layout passes per instance are flagged with ⚠️ warnings
47+
5. Use `LayoutDiagnosticsService.ExportAllToJson()` to get all snapshot data as JSON
48+
49+
## API Reference
50+
51+
All types are in `DIPS.Mobile.UI.API.Diagnostics`.
52+
53+
### LayoutDiagnosticsService (static)
54+
55+
| Member | Description |
56+
|---|---|
57+
| `Initialize()` | Shows the floating overlay pill (dormant, not capturing) |
58+
| `Enable()` | Starts capturing layout metrics |
59+
| `Disable()` | Stops capturing, returns `IReadOnlyList<LayoutDiagnosticsSnapshot>` of all captured snapshots |
60+
| `Toggle()` | Toggles between Enable/Disable |
61+
| `Teardown()` | Removes overlay and fully tears down the system |
62+
| `ClearSnapshots()` | Clears all completed snapshots |
63+
| `ExportAllToJson()` | Returns all snapshots as a JSON string |
64+
| `IsEnabled` | Whether diagnostics are currently capturing |
65+
| `IsInitialized` | Whether the overlay has been initialized |
66+
| `CompletedSnapshots` | `IReadOnlyList<LayoutDiagnosticsSnapshot>` of all captured snapshots |
67+
| `CurrentSnapshot` | The in-progress snapshot, or null |
68+
| `SnapshotCompleted` | `event Action<LayoutDiagnosticsSnapshot>` — raised when a snapshot finishes |
69+
70+
### LayoutDiagnosticsSnapshot
71+
72+
| Property | Type | Description |
73+
|---|---|---|
74+
| `SourceName` | `string` | Page or bottom sheet type name that triggered the snapshot |
75+
| `StartedAt` | `DateTime` | When capture started (UTC) |
76+
| `EndedAt` | `DateTime?` | When capture ended (UTC) |
77+
| `TotalMeasureCount` | `int` | Total measure operations |
78+
| `TotalArrangeCount` | `int` | Total arrange operations |
79+
| `MeasureCountsByType` | `Dictionary<string, int>` | Measure count per element type |
80+
| `ArrangeCountsByType` | `Dictionary<string, int>` | Arrange count per element type |
81+
| `MeasureInstancesByType` | `Dictionary<string, HashSet<Guid>>` | Unique instances seen per type (measure) |
82+
| `ArrangeInstancesByType` | `Dictionary<string, HashSet<Guid>>` | Unique instances seen per type (arrange) |
83+
| `Warnings` | `List<string>` | Types exceeding avg 3 passes per instance |
84+
85+
| Method | Returns | Description |
86+
|---|---|---|
87+
| `ToCompactString()` | `string` | One-line summary: `M:120 A:60 ⚠️2` |
88+
| `ToDetailedString()` | `string` | Multi-line report with top types and warnings |
89+
| `ToJson()` | `string` | Full JSON export with per-type counts, instances, and warnings |
90+
91+
### LayoutDiagnosticsPage (not included — build your own)
92+
93+
There is no built-in page. Use the API above to build a diagnostics page suited to your app. See `src/app/Components/DiagnosticsSamples/LayoutDiagnosticsPage.xaml` or `src/app/Playground/DiagnosticsSamples/LayoutDiagnosticsPage.xaml` for a reference implementation.
94+
95+
### Example: Subscribe to snapshots programmatically
96+
97+
```csharp
98+
LayoutDiagnosticsService.SnapshotCompleted += snapshot =>
99+
{
100+
if (snapshot.Warnings.Count > 0)
101+
{
102+
Debug.WriteLine(snapshot.ToDetailedString());
103+
}
104+
};
105+
```
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
name: design-system-usage
3+
description: Rules and patterns for using DIPS.Mobile.UI design tokens, styles, and Layout effect in C# code. Apply when building UI components or views.
4+
---
5+
6+
# Design System Usage
7+
8+
When building UI elements in C# code, always use the DIPS design system instead of hardcoded values.
9+
10+
## Trigger Phrases
11+
12+
- Building a new component or view in C#
13+
- Creating UI elements with colors, sizes, fonts, or corner radius
14+
- Refactoring hardcoded values to design tokens
15+
- "Use design tokens" / "Use design system"
16+
17+
## Rules
18+
19+
### 1. Colors — Never Hardcode
20+
21+
```csharp
22+
using DIPS.Mobile.UI.Resources.Colors;
23+
using Colors = DIPS.Mobile.UI.Resources.Colors.Colors;
24+
25+
// ✅ Correct — use semantic color tokens from ColorName enum
26+
label.TextColor = Colors.GetColor(ColorName.color_text_default);
27+
grid.BackgroundColor = Colors.GetColor(ColorName.color_surface_backdrop);
28+
29+
// ❌ Wrong — hardcoded hex or Color values
30+
label.TextColor = Color.FromArgb("#FFFFFF");
31+
grid.BackgroundColor = Color.FromArgb("#DD1A1A2E");
32+
```
33+
34+
**Always verify** the color name exists in `src/library/DIPS.Mobile.UI/Resources/Colors/ColorName.cs` before using it.
35+
36+
Common color token categories:
37+
| Purpose | Token pattern |
38+
|---------|--------------|
39+
| Page/card backgrounds | `color_surface_*`, `color_background_default` |
40+
| Text | `color_text_default`, `color_text_subtle`, `color_text_on_button`, `color_text_danger` |
41+
| Buttons | `color_fill_button`, `color_fill_button_danger`, `color_fill_success` |
42+
| Icons | `color_icon_default`, `color_icon_danger`, `color_icon_success` |
43+
| Borders | `color_border_default`, `color_border_subtle` |
44+
| Overlays | `color_surface_backdrop` |
45+
| Disabled | `color_surface_disabled`, `color_text_disabled`, `color_fill_disabled` |
46+
47+
### 2. Sizes — Never Hardcode Dimensions
48+
49+
```csharp
50+
using DIPS.Mobile.UI.Resources.Sizes;
51+
52+
// ✅ Correct — use size tokens from SizeName enum
53+
Margin = new Thickness(Sizes.GetSize(SizeName.content_margin_small)); // 8
54+
WidthRequest = Sizes.GetSize(SizeName.size_8); // 32
55+
CornerRadius = (int)Sizes.GetSize(SizeName.radius_large); // 16
56+
57+
// ❌ Wrong — magic numbers
58+
Margin = new Thickness(8);
59+
WidthRequest = 32;
60+
CornerRadius = 16;
61+
```
62+
63+
Key size token groups:
64+
| Purpose | Tokens | Values |
65+
|---------|--------|--------|
66+
| Base sizes | `size_1` to `size_25` | 4px increments (4–100) |
67+
| Small fractions | `size_half` | 2 |
68+
| Content padding/margins | `content_margin_xsmall` to `content_margin_xlarge` | 4, 8, 12, 20, 28 |
69+
| Page margins | `page_margin_xsmall` to `page_margin_xlarge` | 12, 16, 24, 32 |
70+
| Corner radius | `radius_xsmall` to `radius_xlarge` | 4, 8, 12, 16, 32 |
71+
| Strokes | `stroke_small` to `stroke_xlarge` | 0.5, 1, 2, 5 |
72+
73+
### 3. Label Styles — Never Set FontSize/FontFamily Manually
74+
75+
```csharp
76+
using DIPS.Mobile.UI.Resources.Styles;
77+
using DIPS.Mobile.UI.Resources.Styles.Label;
78+
79+
// ✅ Correct — use label styles
80+
label.Style = Styles.GetLabelStyle(LabelStyle.UI200);
81+
82+
// ❌ Wrong — manual font properties
83+
label.FontSize = 14;
84+
label.FontFamily = "UI";
85+
```
86+
87+
Available label styles:
88+
| Style | Font Family | Font Size |
89+
|-------|-------------|-----------|
90+
| `UI100` | UI | 12 |
91+
| `UI200` | UI | 14 |
92+
| `UI300` | UI | 16 |
93+
| `UI400` | UI | 18 |
94+
| `Body100` | Body | 12 |
95+
| `Body200` | Body | 14 |
96+
| `Body300` | Body | 16 |
97+
| `Body400` | Body | 18 |
98+
| `SectionHeader` | UI | 18 |
99+
| `Header500``Header1000` | Header | 20–64 |
100+
101+
**Never** set `WidthRequest`/`HeightRequest` on labels — let them size naturally.
102+
103+
### 4. Button Styles
104+
105+
```csharp
106+
using DIPS.Mobile.UI.Resources.Styles;
107+
using DIPS.Mobile.UI.Resources.Styles.Button;
108+
109+
// ✅ Standard buttons
110+
button.Style = Styles.GetButtonStyle(ButtonStyle.DefaultSmall);
111+
112+
// ✅ Close/dismiss buttons
113+
closeButton.Style = Styles.GetButtonStyle(ButtonStyle.CloseIconSmall);
114+
```
115+
116+
Available: `DefaultLarge/Small`, `CallToActionLarge/Small`, `GhostLarge/Small`, `CloseIconSmall`, `DefaultIconSmall/Large`, `GhostIconSmall/Large`, `CallToActionIconSmall/Large`, `DefaultFloatingIconLarge/Large`.
117+
118+
### 5. Corner Radius — Use Layout Effect, Not Border
119+
120+
```csharp
121+
using LayoutEffect = DIPS.Mobile.UI.Effects.Layout.Layout;
122+
123+
// ✅ Correct — Layout effect with radius token
124+
var container = new Grid { BackgroundColor = Colors.GetColor(ColorName.color_surface_default) };
125+
LayoutEffect.SetCornerRadius(container, new CornerRadius(Sizes.GetSize(SizeName.radius_large)));
126+
127+
// ❌ Wrong — Border wrapper with StrokeShape
128+
var border = new Border
129+
{
130+
StrokeShape = new RoundRectangle { CornerRadius = 16 },
131+
Content = container
132+
};
133+
```
134+
135+
**Important**: Inside classes extending `VisualElement` (e.g., `Grid`, `ContentView`), `Layout` resolves to the `VisualElement.Layout(Rect)` method. Use a `using` alias:
136+
137+
```csharp
138+
using LayoutEffect = DIPS.Mobile.UI.Effects.Layout.Layout;
139+
140+
// Then use:
141+
LayoutEffect.SetCornerRadius(view, new CornerRadius(Sizes.GetSize(SizeName.radius_medium)));
142+
```
143+
144+
### 6. Font Families
145+
146+
Only three valid font families: `"Body"`, `"UI"`, `"Header"`.
147+
148+
**Never** use `"monospace"` or system fonts.
149+
150+
Prefer label styles over manual `FontFamily` assignment. Manual `FontFamily` is acceptable on `Button` when no matching `ButtonStyle` exists.
151+
152+
## Verification Checklist
153+
154+
Before finishing, verify:
155+
- [ ] No hardcoded hex colors — all use `Colors.GetColor(ColorName.*)`
156+
- [ ] No hardcoded pixel values for spacing/sizing — all use `Sizes.GetSize(SizeName.*)`
157+
- [ ] No manual `FontSize`/`FontFamily` on labels — all use `Styles.GetLabelStyle()`
158+
- [ ] No `Border` used purely for corner radius — use `LayoutEffect.SetCornerRadius()`
159+
- [ ] No `Shadow` on views — it causes platform-specific issues (Android elevation shadows)
160+
- [ ] Color/size names verified against `ColorName.cs` / `SizeName.cs`

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## [57.1.0]
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.
3+
14
## [57.0.0]
25
- Use SourceGen compilation.
36

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "10.0.100"
3+
"version": "10.0.200"
44
}
55
}

src/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<PackageVersion Include="DotNetMeteor.HotReload.Plugin" Version="3.3.0" />
1111
<PackageVersion Include="LightInject" Version="6.6.4" />
1212
<PackageVersion Include="Microsoft.Maui.Controls" Version="10.0.51" />
13+
<PackageVersion Include="Microsoft.Extensions.Diagnostics" Version="10.0.0" />
1314
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
1415
<PackageVersion Include="SkiaSharp" Version="3.119.1" />
1516
<PackageVersion Include="SkiaSharp.Extended.UI.Maui" Version="3.0.0-preview.18" />

src/app/Components/App.xaml.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Components.Resources.LocalizedStrings;
1+
using Components.DiagnosticsSamples;
2+
using Components.Resources.LocalizedStrings;
23
using DIPS.Mobile.UI.API.Library;
34
using Enum = System.Enum;
45

@@ -46,6 +47,19 @@ protected override Window CreateWindow(IActivationState? activationState)
4647

4748
shell.Items.Add(tabBar);
4849

50+
var diagnosticsTab = new Tab
51+
{
52+
Title = "Diagnostics",
53+
Items =
54+
{
55+
new ShellContent
56+
{
57+
ContentTemplate = new DataTemplate(() => new LayoutDiagnosticsPage())
58+
}
59+
}
60+
};
61+
tabBar.Items.Add(diagnosticsTab);
62+
4963
if (Current != null)
5064
{
5165
//Support dark mode if its enabled in the OS

src/app/Components/Components.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
</ItemGroup>
7171
<ItemGroup>
7272
<PackageReference Include="BitMiracle.LibTiff.NET" />
73+
<PackageReference Include="Microsoft.Extensions.Diagnostics" />
7374
<PackageReference Include="Microsoft.Maui.Controls" />
7475
<PackageReference Include="Microsoft.Extensions.Logging.Debug" />
7576
<PackageReference Include="Newtonsoft.Json" />
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<dui:ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
5+
xmlns:dui="http://dips.com/mobile.ui"
6+
x:Class="Components.DiagnosticsSamples.LayoutDiagnosticsPage"
7+
Title="Diagnostics">
8+
<Grid RowDefinitions="Auto,*">
9+
10+
<!-- Controls -->
11+
<dui:VerticalStackLayout Grid.Row="0"
12+
Spacing="0"
13+
Padding="{dui:Thickness UniformSize=content_margin_medium}">
14+
<dui:Button x:Name="OverlayButton"
15+
Style="{dui:Styles Button=DefaultSmall}"
16+
HorizontalOptions="Center"
17+
Margin="{dui:Margin Bottom=size_4}"
18+
Clicked="OnOverlayButtonClicked" />
19+
20+
<dui:Label x:Name="StatusLabel"
21+
Style="{dui:Styles Label=UI300}"
22+
FontAttributes="Bold"
23+
HorizontalTextAlignment="Center"
24+
Margin="{dui:Margin Bottom=size_2}" />
25+
26+
<dui:Button x:Name="ToggleButton"
27+
Style="{dui:Styles Button=DefaultSmall}"
28+
HorizontalOptions="Center"
29+
Clicked="OnToggleClicked" />
30+
31+
<Grid ColumnDefinitions="*,Auto"
32+
Margin="{dui:Margin Top=size_4}">
33+
<dui:Label x:Name="SnapshotCountLabel"
34+
Grid.Column="0"
35+
Style="{dui:Styles Label=UI100}"
36+
TextColor="{dui:Colors color_text_subtle}" />
37+
<dui:Button Grid.Column="1"
38+
Text="Clear snapshots"
39+
Style="{dui:Styles Button=GhostSmall}"
40+
Clicked="OnClearClicked" />
41+
</Grid>
42+
</dui:VerticalStackLayout>
43+
44+
<!-- Snapshot list -->
45+
<dui:ScrollView Grid.Row="1"
46+
Padding="{dui:Padding Left=content_margin_medium, Right=content_margin_medium}">
47+
<dui:VerticalStackLayout x:Name="SnapshotList"
48+
Spacing="{dui:Sizes content_margin_small}" />
49+
</dui:ScrollView>
50+
51+
</Grid>
52+
</dui:ContentPage>

0 commit comments

Comments
 (0)