using System; using System.Collections.Generic; using System.Diagnostics; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; namespace ControlCatalog.Pages { /// /// Shared helpers for the performance-monitor demo pages /// (NavigationPage, TabbedPage, DrawerPage, ContentPage). /// internal sealed class NavigationPerformanceMonitorHelper { internal static readonly IBrush PositiveDeltaBrush = new SolidColorBrush(Color.Parse("#D32F2F")); internal static readonly IBrush NegativeDeltaBrush = new SolidColorBrush(Color.Parse("#388E3C")); internal static readonly IBrush ZeroDeltaBrush = new SolidColorBrush(Color.Parse("#757575")); internal static readonly IBrush CurrentBorderBrush = new SolidColorBrush(Color.Parse("#0078D4")); internal static readonly IBrush DefaultBorderBrush = new SolidColorBrush(Color.Parse("#CCCCCC")); private readonly List> _trackedPages = new(); private double _previousHeapMB; private DispatcherTimer? _autoRefreshTimer; internal readonly Stopwatch OpStopwatch = new(); internal int TotalCreated; /// /// Track a newly-created page via WeakReference and increment TotalCreated. /// internal void TrackPage(Page page) { TotalCreated++; _trackedPages.Add(new WeakReference(page)); } /// /// Count live (not yet GC'd) tracked page instances. /// internal int CountLiveInstances() { int alive = 0; for (int i = _trackedPages.Count - 1; i >= 0; i--) { if (_trackedPages[i].TryGetTarget(out _)) alive++; else _trackedPages.RemoveAt(i); } return alive; } /// /// Update heap and delta text blocks. Call from RefreshAll(). /// internal void UpdateHeapDelta(TextBlock heapText, TextBlock deltaText) { var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); heapText.Text = $"Managed Heap: {heapMB:##0.0} MB"; var delta = heapMB - _previousHeapMB; if (Math.Abs(delta) < 0.05) { deltaText.Text = "(no change)"; deltaText.Foreground = ZeroDeltaBrush; } else { var sign = delta > 0 ? "+" : ""; deltaText.Text = $"({sign}{delta:0.0} MB)"; deltaText.Foreground = delta > 0 ? PositiveDeltaBrush : NegativeDeltaBrush; } _previousHeapMB = heapMB; } /// /// Initialize previous heap baseline. /// internal void InitHeap() { _previousHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); } /// /// Stop the stopwatch and write elapsed ms to the given TextBlock. /// internal void StopMetrics(TextBlock lastOpText) { if (!OpStopwatch.IsRunning) return; OpStopwatch.Stop(); lastOpText.Text = $"Last Op: {OpStopwatch.ElapsedMilliseconds} ms"; } /// /// Force full GC, then invoke the refresh callback. /// internal void ForceGC(Action refresh) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); refresh(); } /// /// Start a 2-second auto-refresh timer. /// internal void StartAutoRefresh(Action refresh) { if (_autoRefreshTimer != null) return; _autoRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; _autoRefreshTimer.Tick += (_, _) => refresh(); _autoRefreshTimer.Start(); } /// /// Stop the auto-refresh timer. /// internal void StopAutoRefresh() { _autoRefreshTimer?.Stop(); _autoRefreshTimer = null; } /// /// Toggle auto-refresh based on a CheckBox. /// internal void OnAutoRefreshChanged(CheckBox check, Action refresh) { if (check.IsChecked == true) StartAutoRefresh(refresh); else StopAutoRefresh(); } /// /// Append a timestamped log entry to a StackPanel inside a ScrollViewer. /// internal void LogOperation(string action, string detail, StackPanel logPanel, ScrollViewer logScroll, string? extraInfo = null) { var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); var timing = OpStopwatch.ElapsedMilliseconds; var extra = extraInfo != null ? $" {extraInfo}," : ""; logPanel.Children.Add(new TextBlock { Text = $"{DateTime.Now:HH:mm:ss} [{action}] {detail} —{extra} heap {heapMB:##0.0} MB, {timing} ms", FontSize = 10, FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"), Padding = new Thickness(6, 2), TextTrimming = TextTrimming.CharacterEllipsis, }); logScroll.ScrollToEnd(); } /// /// Build a tracked ContentPage with a 50 KB dummy allocation. /// internal ContentPage BuildTrackedPage(string title, int index, int allocBytes = 51200) { var page = NavigationDemoHelper.MakePage(title, $"Stack position #{index}\nPush more pages ...", index); page.Tag = new byte[allocBytes]; TrackPage(page); return page; } /// /// Create a reusable stack/history row (badge + title + label). /// internal static (Border Container, Border Badge, TextBlock IndexText, TextBlock TitleText, TextBlock BadgeText) CreateStackRow() { var indexText = new TextBlock { FontSize = 10, FontWeight = FontWeight.SemiBold, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; var badge = new Border { Width = 22, Height = 22, CornerRadius = new CornerRadius(11), VerticalAlignment = VerticalAlignment.Center, Child = indexText, }; var titleText = new TextBlock { VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(6, 0, 0, 0), }; var badgeText = new TextBlock { FontSize = 10, Opacity = 0.5, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 0, 0, 0), IsVisible = false, }; var row = new DockPanel(); row.Children.Add(badge); row.Children.Add(titleText); row.Children.Add(badgeText); var container = new Border { CornerRadius = new CornerRadius(6), Padding = new Thickness(8, 6), Child = row, }; return (container, badge, indexText, titleText, badgeText); } /// /// Update a stack row with page data. /// internal static void UpdateStackRow( (Border Container, Border Badge, TextBlock IndexText, TextBlock TitleText, TextBlock BadgeText) row, int stackIndex, string title, bool isCurrent, bool isRoot) { row.Badge.Background = NavigationDemoHelper.GetPageBrush(stackIndex); row.IndexText.Text = (stackIndex + 1).ToString(); row.TitleText.Text = title; row.TitleText.FontWeight = isCurrent ? FontWeight.SemiBold : FontWeight.Normal; string? label = isCurrent ? "current" : (isRoot ? "root" : null); row.BadgeText.Text = label ?? ""; row.BadgeText.IsVisible = label != null; row.Container.BorderBrush = isCurrent ? CurrentBorderBrush : DefaultBorderBrush; row.Container.BorderThickness = new Thickness(isCurrent ? 2 : 1); } /// /// Sync a StackPanel of stack rows with data, growing/shrinking the row cache as needed. /// internal static void RefreshStackPanel( StackPanel panel, List<(Border Container, Border Badge, TextBlock IndexText, TextBlock TitleText, TextBlock BadgeText)> rowCache, IReadOnlyList stack, Page? currentPage) { int count = stack.Count; while (rowCache.Count < count) rowCache.Add(CreateStackRow()); while (panel.Children.Count > count) panel.Children.RemoveAt(panel.Children.Count - 1); while (panel.Children.Count < count) panel.Children.Add(rowCache[panel.Children.Count].Container); for (int displayIdx = 0; displayIdx < count; displayIdx++) { int stackIdx = count - 1 - displayIdx; var page = stack[stackIdx]; bool isCurrent = ReferenceEquals(page, currentPage); bool isRoot = stackIdx == 0; var row = rowCache[displayIdx]; if (!ReferenceEquals(panel.Children[displayIdx], row.Container)) panel.Children[displayIdx] = row.Container; UpdateStackRow(row, stackIdx, page.Header?.ToString() ?? "(untitled)", isCurrent, isRoot); } } } }