Вступ: чому моніторинг продуктивності — це не розкіш
Мобільні додатки працюють у досить жорсткому середовищі: обмежена оперативна пам'ять, різна потужність процесорів, нестабільний інтернет і, що найголовніше, користувачі, які не пробачають навіть секундної затримки. За різними дослідженнями, понад 53% людей просто закривають додаток, якщо він вантажиться довше трьох секунд. Три секунди — і все, ви втратили користувача.
Саме тому моніторинг продуктивності — це не щось "на потім", а обов'язковий елемент будь-якого серйозного мобільного проєкту.
З випуском .NET MAUI 10 команда Microsoft зробила, чесно кажучи, величезний крок уперед у сфері діагностики та спостережуваності (observability). Уперше розробники отримали вбудовану систему метрик та трейсів, побудовану на стандартних API System.Diagnostics, що інтегрується з OpenTelemetry, .NET Aspire та іншими інструментами моніторингу — і все це без жодних сторонніх бібліотек. Тепер можна бачити, скільки часу займає кожний цикл Measure/Arrange, відстежувати повільні макети й виявляти проблеми з рендерингом ще до того, як їх помітить користувач.
У цьому посібнику ми детально розберемо архітектуру нової системи діагностики, всі доступні метрики, інтеграцію з .NET Aspire та OpenTelemetry, а також покроково налаштуємо моніторинг з нуля. Крім діагностики, охопимо й інші ключові покращення .NET MAUI 10: генерацію XAML на етапі компіляції, оновлення CollectionView, оптимізації для Android, нові асинхронні API та багато іншого.
Огляд архітектури діагностики в .NET MAUI 10
Нова система діагностики побудована на двох стовпах стандартного простору імен System.Diagnostics: ActivitySource для розподілених трейсів та Meter для метрик. Обидва компоненти зареєстровані під єдиним ім'ям "Microsoft.Maui", що суттєво спрощує їх підключення та фільтрацію.
ActivitySource для трейсів
ActivitySource з іменем "Microsoft.Maui" генерує трейси (spans), що дозволяють відстежувати виконання конкретних операцій у часі. Кожний трейс містить інформацію про початок, тривалість та контекст операції. Це особливо зручно для аналізу послідовності подій при завантаженні сторінки або при складних переходах між екранами.
Meter для метрик
Meter з тим самим ім'ям "Microsoft.Maui" надає числові метрики у вигляді лічильників (Counter) та гістограм (Histogram). Метрики агрегуються і споживають мінімум ресурсів, тож їх цілком можна використовувати навіть у production.
Feature Switch для керування
Для повного контролю над системою метрик передбачено перемикач System.Diagnostics.Metrics.Meter.IsSupported. Якщо встановити його в false, усі виклики, пов'язані з метриками, видаляються тримером під час AOT-компіляції — нульовий вплив на розмір збірки та продуктивність:
<PropertyGroup>
<!-- Повністю вимкнути метрики для production-збірки -->
<MetricsSupported>false</MetricsSupported>
</PropertyGroup>
Дизайн з нульовою алокацією
Одне з найважливіших архітектурних рішень — zero-allocation design. Система використовує структури (structs) замість класів для тегів метрик, а також патерн using для автоматичного завершення вимірювань. Простіше кажучи, збір метрик не створює додаткового тиску на збирач сміття (GC), а для мобільних додатків це критично — кожна зайва алокація може спричинити мікрозатримку.
// Внутрішня реалізація використовує struct-based підхід
// Приклад концептуального патерну, використаного в MAUI 10:
using (var measurement = DiagnosticScope.BeginMeasure(element))
{
// Виконання операції Measure або Arrange
// При Dispose() автоматично записується тривалість
}
Такий підхід гарантує, що навіть при активному збиранні метрик накладні витрати залишаються мінімальними — порядку наносекунд на одне вимірювання. Для мобільного додатка це, по суті, безкоштовно.
Метрики продуктивності макетів
Система макетів (layout system) — серце будь-якого UI-фреймворку. У .NET MAUI кожний кадр проходить два етапи: Measure (вимірювання бажаного розміру елемента) та Arrange (позиціонування елемента у виділеному просторі). Якщо ці етапи працюють повільно або повторюються занадто часто — користувач побачить "смикання" інтерфейсу і затримки.
.NET MAUI 10 дає чотири основні метрики для моніторингу продуктивності макетів:
Лічильники операцій
maui.layout.measure_count(Counter) — підраховує кількість викликів Measure. Різке зростання цього лічильника зазвичай вказує на зайві перемірювання через неоптимальну ієрархію вкладених макетів.maui.layout.arrange_count(Counter) — кількість викликів Arrange. Аналогічно, високе значення сигналізує про проблеми з компонуванням.
Гістограми тривалості
maui.layout.measure_duration(Histogram, наносекунди) — розподіл тривалості операцій Measure. Гістограма показує не лише середнє значення, а й процентилі (p50, p95, p99), що важливо для виявлення рідких, але критичних затримок.maui.layout.arrange_duration(Histogram, наносекунди) — розподіл тривалості операцій Arrange.
Що таке Measure та Arrange?
Для тих, хто не знайомий з внутрішньою кухнею: Measure — це перший прохід системи макетів, під час якого кожний елемент визначає свій бажаний розмір на основі вмісту, обмежень батьківського елемента та власних налаштувань (WidthRequest, HeightRequest, Padding, Margin тощо). Виклик проходить рекурсивно від кореня до листків дерева елементів.
Arrange — другий прохід, де батьківський елемент розподіляє доступний простір між дочірніми елементами та визначає їхні фінальні позиції. Теж рекурсивний.
Якщо ваш макет має глибоку ієрархію вкладених StackLayout або Grid, кількість операцій Measure/Arrange зростає експоненційно — і це одразу відобразиться у метриках.
Діагностичні теги
Кожне вимірювання супроводжується набором тегів для ідентифікації конкретного елемента:
element.type— тип елемента (наприклад,Label,Grid,StackLayout)element.id— значення властивостіx:NameабоIdelement.automation_id—AutomationId, якщо встановленийelement.class_id—ClassIdелементаelement.style_id—StyleIdелементаelement.class— CSS-клас елементаelement.frame— фінальна рамка (frame) елемента після Arrange
Завдяки цим тегам можна відфільтрувати метрики за конкретним елементом і зрозуміти, який саме компонент гальмує рендеринг:
// Приклад XAML з ідентифікаторами для діагностики
<VerticalStackLayout x:Name="MainStack" AutomationId="main_stack">
<Label x:Name="TitleLabel"
AutomationId="title_label"
ClassId="header"
Text="Заголовок сторінки" />
<CollectionView x:Name="ItemsList"
AutomationId="items_list"
ItemsSource="{Binding Items}" />
</VerticalStackLayout>
У метриках ви побачите записи на кшталт: element.type=CollectionView, element.automation_id=items_list, maui.layout.measure_duration=450000 (450 мікросекунд).
Генерація XAML на етапі компіляції
Чесно кажучи, це одне з найочікуваніших нововведень .NET MAUI 10 — XAML Source Generation. Ідея проста: замість інтерпретації XAML у runtime через рефлексію, новий генератор перетворює XAML-розмітку на строго типізований C#-код прямо під час збірки проєкту.
Як увімкнути
Для активації додайте одну властивість до файлу .csproj:
<Project Sdk="Microsoft.NET.Sdk.Maui">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<!-- Увімкнення генерації XAML на етапі компіляції -->
<MauiXamlInflator>SourceGen</MauiXamlInflator>
</PropertyGroup>
</Project>
Переваги
- Приблизно 25% швидший рендеринг UI — завдяки усуненню рефлексії та парсингу XML у runtime побудова дерева елементів відбувається значно швидше.
- Покращена підтримка інструментів — генерований код доступний для аналізу, що покращує навігацію в IDE, автодоповнення та рефакторинг.
- Строго типізований код — помилки в прив'язках та типах виявляються на етапі компіляції, а не під час виконання. Менше runtime-крешів, пов'язаних з XAML — і це приємно.
- Кращі діагностичні повідомлення — оскільки код генерується, будь-які помилки мають точні посилання на рядки у XAML-файлі.
Різниця особливо відчутна на складних сторінках із великою кількістю елементів, де виграш у часі завантаження може сягати сотень мілісекунд.
Глобальні простори імен XAML
Ще одне суттєве ергономічне покращення в .NET MAUI 10 — глобальні простори імен XAML. Якщо ви коли-небудь втомлювалися від копіювання десятків xmlns на кожну сторінку, то зрозумієте, наскільки це приємна зміна.
Старий підхід
Раніше на кожній XAML-сторінці доводилося повторювати всі необхідні простори імен:
<!-- Старий підхід: повторення xmlns на кожній сторінці -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp.Views"
xmlns:vm="clr-namespace:MyApp.ViewModels"
xmlns:controls="clr-namespace:MyApp.Controls"
xmlns:converters="clr-namespace:MyApp.Converters"
x:Class="MyApp.Views.MainPage">
<!-- Контент сторінки -->
</ContentPage>
Новий підхід з GlobalXmlns
Тепер можна створити файл GlobalXmlns.cs у корені проєкту та зареєструвати простори імен глобально:
// GlobalXmlns.cs
using Microsoft.Maui.Controls;
[assembly: XmlnsDefinition("http://schemas.myapp.com/2025/maui", "MyApp.Views")]
[assembly: XmlnsDefinition("http://schemas.myapp.com/2025/maui", "MyApp.ViewModels")]
[assembly: XmlnsDefinition("http://schemas.myapp.com/2025/maui", "MyApp.Controls")]
[assembly: XmlnsDefinition("http://schemas.myapp.com/2025/maui", "MyApp.Converters")]
[assembly: XmlnsPrefix("http://schemas.myapp.com/2025/maui", "app")]
Неявні простори імен
А є ще більш елегантний варіант — увімкнення неявних оголошень XAML через налаштування MauiAllowImplicitXmlnsDeclaration у файлі проєкту:
<PropertyGroup>
<MauiAllowImplicitXmlnsDeclaration>true</MauiAllowImplicitXmlnsDeclaration>
</PropertyGroup>
Після цього всі типи з поточного проєкту та підключених бібліотек стають доступними в XAML без явного оголошення xmlns. Сторінки стають набагато чистішими:
<!-- Новий підхід: чистий XAML без зайвих xmlns -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Views.MainPage">
<!-- Використання MyApp.Controls.CustomButton без xmlns -->
<CustomButton Text="Натисніть мене" />
<!-- Використання MyApp.Converters.BoolToColorConverter -->
<Label Text="{Binding Name}"
TextColor="{Binding IsActive, Converter={BoolToColorConverter}}" />
</ContentPage>
Це значно зменшує "шум" у XAML-файлах та робить їх читабельнішими — особливо у великих проєктах з десятками простірів імен.
Інтеграція з .NET Aspire та OpenTelemetry
Однією з найпотужніших можливостей нової діагностики є безшовна інтеграція з .NET Aspire — оркестратором розподілених додатків від Microsoft. Оскільки метрики та трейси .NET MAUI 10 побудовані на стандартних API System.Diagnostics, вони автоматично підхоплюються OpenTelemetry SDK і відображаються на дашборді Aspire. Нічого додатково вигадувати не потрібно.
Налаштування Service Defaults
Перший крок — підключення стандартних сервісів Aspire через AddServiceDefaults(), який налаштовує OpenTelemetry з усіма необхідними експортерами:
// MauiProgram.cs — інтеграція з .NET Aspire
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Підключення Aspire Service Defaults
builder.AddServiceDefaults();
// Додаткове налаштування OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
// Підписка на метрики MAUI
metrics.AddMeter("Microsoft.Maui");
// Додавання стандартних метрик .NET runtime
metrics.AddRuntimeInstrumentation();
// Експорт у OTLP (Aspire Dashboard)
metrics.AddOtlpExporter();
})
.WithTracing(tracing =>
{
// Підписка на трейси MAUI
tracing.AddSource("Microsoft.Maui");
// Підписка на HTTP-трейси для API-запитів
tracing.AddHttpClientInstrumentation();
// Експорт у OTLP
tracing.AddOtlpExporter();
});
// Service Discovery для бекенд-сервісів
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddServiceDiscovery();
});
return builder.Build();
}
}
Що показує дашборд Aspire
Після підключення на дашборді Aspire ви побачите:
- Трейси — повний ланцюжок операцій від натискання кнопки до отримання відповіді від сервера, включно з часом на рендеринг макету.
- Метрики макетів — графіки
maui.layout.measure_durationтаmaui.layout.arrange_durationз можливістю фільтрації за типом елемента. - Журнали — структуровані логи з контекстом трейсів, що дозволяє швидко знаходити причину проблем.
- Service Map — візуалізація залежностей між мобільним додатком і бекенд-сервісами.
Інтеграція з Aspire особливо цінна для команд, які розробляють повний стек: мобільний додаток, API-сервери та фонові сервіси. Тепер можна бачити повну картину — від натискання кнопки користувачем до обробки запиту на бекенді.
Оновлення CollectionView та CarouselView
У .NET MAUI 10 серйозно переробили два найпопулярніші компоненти для відображення списків: CollectionView та CarouselView.
Нові стандартні обробники для iOS та Mac Catalyst
На платформах iOS та Mac Catalyst тепер використовуються нові обробники (handlers), побудовані на сучасному UICollectionViewCompositionalLayout замість застарілого UICollectionViewFlowLayout. Раніше їх треба було вмикати вручну, а тепер вони працюють за замовчуванням.
Покращення продуктивності
- Плавніший скролінг — нові обробники забезпечують стабільнішу частоту кадрів при прокрутці великих списків завдяки покращеному перевикористанню комірок.
- Ефективніше використання пам'яті — зменшено кількість одночасно створених елементів поза видимою областю (що критично для списків з тисячами елементів).
- Коректна підтримка різних макетів — LinearItemsLayout, GridItemsLayout та групування тепер працюють без відомих артефактів рендерингу.
Для більшості додатків це покращення працює автоматично — достатньо просто оновити цільовий фреймворк на .NET 10. А якщо з якихось причин новий обробник спричиняє регресії, його завжди можна вимкнути через Feature Switch.
Оптимізації для Android
Android залишається найпопулярнішою мобільною платформою, і .NET MAUI 10 приносить декілька суттєвих оптимізацій саме для неї.
Оновлення API рівнів
За замовчуванням цільовим API тепер є API 36 (Android 16), а мінімальний підтримуваний рівень — API 24 (Android 7.0). Це дозволяє використовувати сучасні API платформи та зменшує обсяг коду для сумісності зі старими версіями.
Marshal Methods для швидшого запуску
Marshal methods тепер увімкнені за замовчуванням. Ця технологія замінює повільний механізм JNI-зворотних викликів на прямі виклики методів, що значно скорочує час холодного запуску. У середньому виграш — 10-20% швидшого старту. Для додатка, який раніше запускався 2 секунди, це відчутна різниця.
Експериментальний CoreCLR Runtime
Для тих, хто любить експериментувати, доступний новий CoreCLR runtime замість стандартного Mono:
<PropertyGroup>
<!-- Перехід на експериментальний CoreCLR замість Mono -->
<UseMonoRuntime>false</UseMonoRuntime>
</PropertyGroup>
CoreCLR обіцяє покращену JIT-компіляцію, кращу підтримку сучасних оптимізацій .NET та уніфікацію runtime між серверними й мобільними додатками. Але зверніть увагу: це поки що експериментальна функція, і в production її використовувати не варто.
Підтримка dotnet run
Тепер можна запускати Android-додатки безпосередньо через dotnet run, вказавши цільовий пристрій через параметр AdbTarget:
# Запуск на підключеному пристрої
dotnet run -f net10.0-android -p:AdbTarget=-d
# Запуск на емуляторі
dotnet run -f net10.0-android -p:AdbTarget=-e
# Запуск на конкретному пристрої
dotnet run -f net10.0-android -p:AdbTarget="-s emulator-5554"
Швидша збірка з System.IO.Compression
Процес збірки Android-додатків тепер використовує оптимізований System.IO.Compression замість попередньої реалізації для роботи з APK/AAB-архівами. Збірка стає швидшою, особливо для великих проєктів з великою кількістю ресурсів.
Нові асинхронні API
.NET MAUI 10 вводить набір нових асинхронних API для анімацій та діалогів. Нарешті можна писати чистіший і безпечніший код з async/await та підтримкою CancellationToken.
Асинхронні анімації
Нові методи анімацій підтримують скасування і поводяться значно передбачуваніше:
// Нові асинхронні анімації з підтримкою CancellationToken
public async Task AnimateEntrance(View element, CancellationToken ct = default)
{
// Початковий стан: невидимий, зменшений, зміщений
element.Opacity = 0;
element.Scale = 0.5;
element.TranslationY = 100;
// Паралельні анімації з можливістю скасування
await Task.WhenAll(
element.FadeToAsync(1.0, 500, Easing.CubicOut, ct),
element.ScaleToAsync(1.0, 500, Easing.SpringOut, ct),
element.TranslateToAsync(0, 0, 500, Easing.CubicOut, ct)
);
}
// Анімація обертання
public async Task AnimateLoading(View spinner, CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await spinner.RotateToAsync(360, 1000, Easing.Linear, ct);
spinner.Rotation = 0;
}
}
Асинхронні діалоги
Оновлені методи діалогів теж підтримують CancellationToken, що дозволяє скасовувати їх програмно:
// Асинхронний діалог з таймаутом
public async Task<bool> ConfirmActionWithTimeout(Page page)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
bool result = await page.DisplayAlertAsync(
"Підтвердження",
"Ви впевнені, що хочете видалити цей елемент?",
"Так", "Ні",
cts.Token);
return result;
}
catch (OperationCanceledException)
{
// Таймаут — повертаємо false за замовчуванням
return false;
}
}
// Асинхронний вибір з листа дій
public async Task<string> SelectCategoryAsync(Page page, CancellationToken ct)
{
string result = await page.DisplayActionSheetAsync(
"Оберіть категорію",
"Скасувати",
null,
ct,
"Технології", "Наука", "Мистецтво", "Спорт");
return result ?? "Не обрано";
}
Ці API вирішують поширену проблему з "витоком" діалогів — раніше вони могли залишатися відкритими після навігації зі сторінки. Тепер їх можна прив'язати до життєвого циклу сторінки через CancellationToken, і ця проблема зникає.
Нові можливості елементів керування
.NET MAUI 10 додає десятки покращень до існуючих елементів керування. Ось найцікавіші з них.
Nullable DatePicker та TimePicker
Нарешті! Розробники отримали можливість встановлювати null як значення для DatePicker та TimePicker. Для форм, де дата або час — необов'язкові поля, це було просто необхідно:
<!-- Nullable DatePicker — користувач може не обирати дату -->
<DatePicker x:Name="BirthDatePicker"
Date="{Binding BirthDate, Mode=TwoWay}"
Format="dd.MM.yyyy"
PlaceholderText="Оберіть дату народження" />
<!-- Nullable TimePicker -->
<TimePicker x:Name="ReminderTimePicker"
Time="{Binding ReminderTime, Mode=TwoWay}"
Format="HH:mm"
PlaceholderText="Оберіть час нагадування" />
// ViewModel
public partial class ProfileViewModel : ObservableObject
{
[ObservableProperty]
private DateTime? birthDate; // null = не вказано
[ObservableProperty]
private TimeSpan? reminderTime; // null = без нагадування
}
Switch OffColor та SearchBar SearchIconColor
Тепер можна налаштовувати колір перемикача у вимкненому стані та колір іконки пошуку — дрібниця, але часто саме таких дрібниць і не вистачало:
<!-- Switch з кастомним кольором у вимкненому стані -->
<Switch IsToggled="{Binding IsNotificationsEnabled}"
OnColor="#4CAF50"
OffColor="#E0E0E0"
ThumbColor="#FFFFFF" />
<!-- SearchBar з кольоровою іконкою пошуку -->
<SearchBar Placeholder="Пошук..."
SearchIconColor="#1976D2"
TextColor="#333333"
PlaceholderColor="#999999" />
Picker з програмним відкриттям та закриттям
Нові методи Open() та Close() дозволяють програмно керувати станом Picker:
// Програмне відкриття Picker з коду
private void OnFilterButtonClicked(object sender, EventArgs e)
{
CategoryPicker.Open();
}
// Закриття Picker після вибору
private void OnCategorySelected(object sender, EventArgs e)
{
CategoryPicker.Close();
ApplyFilter();
}
Покращення HybridWebView
HybridWebView отримав дві корисні нові можливості. Подія WebResourceRequested дозволяє перехоплювати запити до ресурсів і повертати кастомні відповіді, а метод InvokeJavaScriptAsync спрощує виклик JavaScript-функцій із C#:
// Перехоплення запитів до веб-ресурсів
hybridWebView.WebResourceRequested += (sender, args) =>
{
if (args.Url.Contains("/api/local-data"))
{
var jsonData = JsonSerializer.Serialize(localDataService.GetCachedData());
args.SetResponse("application/json", jsonData);
}
};
// Виклик JavaScript з C#
var result = await hybridWebView.InvokeJavaScriptAsync<ChartData>(
"getChartData",
new object[] { "sales", 2025 });
SafeAreaEdges
Новий enum SafeAreaEdges дозволяє точно контролювати, які краї екрана повинні враховувати безпечну зону (notch, Dynamic Island тощо):
<!-- Контент враховує безпечну зону -->
<ContentPage SafeAreaEdges="Container">
<Grid>
<!-- Контент, який не перекривається системними елементами -->
</Grid>
</ContentPage>
<!-- Скролінг від краю до краю -->
<ScrollView SafeAreaEdges="None">
<!-- Контент на повний екран -->
</ScrollView>
<!-- Сітка враховує клавіатуру -->
<Grid SafeAreaEdges="SoftInput">
<Entry Placeholder="Введіть повідомлення..." />
</Grid>
Практичний посібник: налаштування моніторингу з нуля
Ну що, давайте об'єднаємо всю теорію і налаштуємо повноцінну систему моніторингу для .NET MAUI 10 проєкту — крок за кроком.
Крок 1: Конфігурація файлу проєкту
Почнемо з налаштування .csproj з усіма необхідними параметрами:
<Project Sdk="Microsoft.NET.Sdk.Maui">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<!-- XAML Source Generation для швидшого рендерингу -->
<MauiXamlInflator>SourceGen</MauiXamlInflator>
<!-- Глобальні XAML простори імен -->
<MauiAllowImplicitXmlnsDeclaration>true</MauiAllowImplicitXmlnsDeclaration>
<!-- Метрики увімкнені (за замовчуванням true) -->
<MetricsSupported>true</MetricsSupported>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.0" />
</ItemGroup>
</Project>
Крок 2: Налаштування MauiProgram.cs
Далі — головна точка входу додатка з повною конфігурацією діагностики:
// MauiProgram.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using OpenTelemetry.Resources;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// Налаштування ресурсу OpenTelemetry
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(
serviceName: "MyMauiApp",
serviceVersion: "1.0.0",
serviceInstanceId: DeviceInfo.Current.Idiom.ToString());
// Налаштування OpenTelemetry
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("MyMauiApp"))
.WithMetrics(metrics =>
{
metrics
.SetResourceBuilder(resourceBuilder)
.AddMeter("Microsoft.Maui")
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://localhost:4317");
});
})
.WithTracing(tracing =>
{
tracing
.SetResourceBuilder(resourceBuilder)
.AddSource("Microsoft.Maui")
.AddHttpClientInstrumentation()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://localhost:4317");
});
});
// Реєстрація кастомного слухача діагностики
builder.Services.AddSingleton<LayoutDiagnosticsListener>();
builder.Services.AddHostedService(sp =>
sp.GetRequiredService<LayoutDiagnosticsListener>());
#if DEBUG
builder.Logging.AddDebug();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
#endif
return builder.Build();
}
}
Крок 3: Створення кастомного слухача діагностики
Для більш глибокого аналізу створимо власний слухач, який збиратиме та аналізуватиме метрики макетів у реальному часі:
// Services/LayoutDiagnosticsListener.cs
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class LayoutDiagnosticsListener : IHostedService, IDisposable
{
private readonly ILogger<LayoutDiagnosticsListener> _logger;
private MeterListener? _meterListener;
private ActivityListener? _activityListener;
// Порогові значення для попереджень (у наносекундах)
private const long MeasureWarningThreshold = 1_000_000; // 1 мс
private const long ArrangeWarningThreshold = 2_000_000; // 2 мс
private const int ExcessiveMeasureCount = 100;
private long _totalMeasureCount;
private long _totalArrangeCount;
public LayoutDiagnosticsListener(
ILogger<LayoutDiagnosticsListener> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// Налаштування слухача метрик
_meterListener = new MeterListener();
_meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name == "Microsoft.Maui")
{
listener.EnableMeasurementEvents(instrument);
_logger.LogInformation(
"Підписано на метрику: {Name} ({Type})",
instrument.Name, instrument.GetType().Name);
}
};
_meterListener.SetMeasurementEventCallback<long>(OnMeasurement);
_meterListener.SetMeasurementEventCallback<double>(OnMeasurementDouble);
_meterListener.Start();
// Налаштування слухача Activity (трейсів)
_activityListener = new ActivityListener
{
ShouldListenTo = source =>
source.Name == "Microsoft.Maui",
Sample = (ref ActivityCreationOptions<ActivityContext> _)
=> ActivitySamplingResult.AllData,
ActivityStopped = OnActivityStopped
};
ActivitySource.AddActivityListener(_activityListener);
_logger.LogInformation(
"Діагностичний слухач MAUI запущено");
return Task.CompletedTask;
}
private void OnMeasurement(
Instrument instrument,
long measurement,
ReadOnlySpan<KeyValuePair<string, object?>> tags,
object? state)
{
switch (instrument.Name)
{
case "maui.layout.measure_count":
Interlocked.Add(ref _totalMeasureCount, measurement);
CheckExcessiveMeasures();
break;
case "maui.layout.measure_duration":
if (measurement > MeasureWarningThreshold)
{
var elementType = GetTag(tags, "element.type");
var elementId = GetTag(tags, "element.automation_id");
_logger.LogWarning(
"Повільний Measure: {Type} (id={Id}) — {Dur:N0} нс",
elementType, elementId, measurement);
}
break;
case "maui.layout.arrange_duration":
if (measurement > ArrangeWarningThreshold)
{
var elementType = GetTag(tags, "element.type");
_logger.LogWarning(
"Повільний Arrange: {Type} — {Dur:N0} нс",
elementType, measurement);
}
break;
}
}
private void OnMeasurementDouble(
Instrument instrument,
double measurement,
ReadOnlySpan<KeyValuePair<string, object?>> tags,
object? state)
{ }
private void OnActivityStopped(Activity activity)
{
if (activity.Duration.TotalMilliseconds > 16)
{
_logger.LogWarning(
"Операція '{Op}' зайняла {Dur:F2} мс " +
"(перевищує бюджет кадру 60 FPS)",
activity.OperationName,
activity.Duration.TotalMilliseconds);
}
}
private void CheckExcessiveMeasures()
{
if (_totalMeasureCount > ExcessiveMeasureCount)
{
_logger.LogWarning(
"Надмірна кількість Measure: {Count}. " +
"Перевірте вкладеність макетів.",
_totalMeasureCount);
}
}
private static string GetTag(
ReadOnlySpan<KeyValuePair<string, object?>> tags,
string key)
{
foreach (var tag in tags)
if (tag.Key == key)
return tag.Value?.ToString() ?? "N/A";
return "N/A";
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation(
"Підсумок: Measure={M}, Arrange={A}",
_totalMeasureCount, _totalArrangeCount);
Dispose();
return Task.CompletedTask;
}
public void Dispose()
{
_meterListener?.Dispose();
_activityListener?.Dispose();
}
}
Крок 4: Інтерпретація даних метрик
Після налаштування моніторингу важливо правильно читати отримані дані. Ось на що варто звертати увагу:
- maui.layout.measure_duration > 1 мс (1 000 000 нс) — елемент вимірюється занадто довго. Якщо це трапляється для простих елементів (Label, Button), проблема, найімовірніше, у глибокій вкладеності макетів або в складних прив'язках даних.
- maui.layout.measure_count > 100 за один кадр — забагато операцій вимірювання. Це класичний симптом Layout Thrashing, коли зміна розміру одного елемента каскадно спричиняє перемірювання всього дерева.
- maui.layout.arrange_duration > 2 мс — елемент компонується занадто довго. Зверніть увагу на складні Grid-макети з великою кількістю рядків та стовпців.
- Теги element.type показують "VerticalStackLayout" з великою тривалістю — варто розглянути заміну на CollectionView для віртуалізації елементів.
Рекомендації з оптимізації на основі метрик
Зібравши діагностичні дані, застосуйте ці стратегії:
- Зменшіть глибину ієрархії макетів — замінюйте вкладені StackLayout на плоский Grid. Кожний рівень вкладеності множить кількість операцій Measure.
- Використовуйте BindableLayout або CollectionView замість динамічного створення елементів — ці компоненти підтримують віртуалізацію та перевикористання комірок.
- Уникайте Measure-тригерів у прив'язках — зміна Text, FontSize чи Margin спричиняє повторне вимірювання. Групуйте такі зміни за допомогою BatchBegin()/BatchCommit().
- Увімкніть XAML Source Generation — це, по суті, безкоштовне покращення швидкості інфляції XAML на ~25%.
- Профілюйте на реальному пристрої — емулятор може маскувати проблеми з продуктивністю через потужність хост-машини. Я б навіть сказав, що емулятор може створити хибне відчуття безпеки.
Висновок
.NET MAUI 10 — це, без перебільшення, визначний реліз з точки зору спостережуваності та продуктивності мобільних додатків. Вбудована діагностика з підтримкою ActivitySource та Meter дає розробникам можливість вперше побачити детальну картину того, що відбувається в системі макетів — від кількості та тривалості Measure/Arrange до ідентифікації конкретних проблемних елементів через діагностичні теги.
Інтеграція з OpenTelemetry та .NET Aspire виводить моніторинг на промисловий рівень. Ті самі інструменти, що і для серверних мікросервісів — тепер для мобільних додатків.
Генерація XAML на етапі компіляції, оновлені CollectionView та CarouselView, оптимізації для Android із Marshal methods та експериментальним CoreCLR, нові асинхронні API, покращення елементів керування — кожна з цих функцій сама по собі є вагомою причиною для оновлення. А разом вони формують платформу, яка не лише стала швидшою, але й дає інструменти для того, щоб зробити ваші додатки ще швидшими.
Якщо ви ще не перейшли на .NET MAUI 10 — зараз саме час. Починайте з увімкнення метрик у існуючому проєкті, аналізуйте дані, оптимізуйте вузькі місця — і ваші користувачі це відчують. Моніторинг продуктивності — це не одноразова дія, а безперервний процес, і .NET MAUI 10 нарешті дає всі необхідні інструменти, щоб робити це правильно.