NavigationPerformanceMonitorHelper.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using Avalonia;
  5. using Avalonia.Controls;
  6. using Avalonia.Layout;
  7. using Avalonia.Media;
  8. using Avalonia.Threading;
  9. namespace ControlCatalog.Pages
  10. {
  11. /// <summary>
  12. /// Shared helpers for the performance-monitor demo pages
  13. /// (NavigationPage, TabbedPage, DrawerPage, ContentPage).
  14. /// </summary>
  15. internal sealed class NavigationPerformanceMonitorHelper
  16. {
  17. internal static readonly IBrush PositiveDeltaBrush = new SolidColorBrush(Color.Parse("#D32F2F"));
  18. internal static readonly IBrush NegativeDeltaBrush = new SolidColorBrush(Color.Parse("#388E3C"));
  19. internal static readonly IBrush ZeroDeltaBrush = new SolidColorBrush(Color.Parse("#757575"));
  20. internal static readonly IBrush CurrentBorderBrush = new SolidColorBrush(Color.Parse("#0078D4"));
  21. internal static readonly IBrush DefaultBorderBrush = new SolidColorBrush(Color.Parse("#CCCCCC"));
  22. private readonly List<WeakReference<Page>> _trackedPages = new();
  23. private double _previousHeapMB;
  24. private DispatcherTimer? _autoRefreshTimer;
  25. internal readonly Stopwatch OpStopwatch = new();
  26. internal int TotalCreated;
  27. /// <summary>
  28. /// Track a newly-created page via WeakReference and increment TotalCreated.
  29. /// </summary>
  30. internal void TrackPage(Page page)
  31. {
  32. TotalCreated++;
  33. _trackedPages.Add(new WeakReference<Page>(page));
  34. }
  35. /// <summary>
  36. /// Count live (not yet GC'd) tracked page instances.
  37. /// </summary>
  38. internal int CountLiveInstances()
  39. {
  40. int alive = 0;
  41. for (int i = _trackedPages.Count - 1; i >= 0; i--)
  42. {
  43. if (_trackedPages[i].TryGetTarget(out _))
  44. alive++;
  45. else
  46. _trackedPages.RemoveAt(i);
  47. }
  48. return alive;
  49. }
  50. /// <summary>
  51. /// Update heap and delta text blocks. Call from RefreshAll().
  52. /// </summary>
  53. internal void UpdateHeapDelta(TextBlock heapText, TextBlock deltaText)
  54. {
  55. var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
  56. heapText.Text = $"Managed Heap: {heapMB:##0.0} MB";
  57. var delta = heapMB - _previousHeapMB;
  58. if (Math.Abs(delta) < 0.05)
  59. {
  60. deltaText.Text = "(no change)";
  61. deltaText.Foreground = ZeroDeltaBrush;
  62. }
  63. else
  64. {
  65. var sign = delta > 0 ? "+" : "";
  66. deltaText.Text = $"({sign}{delta:0.0} MB)";
  67. deltaText.Foreground = delta > 0 ? PositiveDeltaBrush : NegativeDeltaBrush;
  68. }
  69. _previousHeapMB = heapMB;
  70. }
  71. /// <summary>
  72. /// Initialize previous heap baseline.
  73. /// </summary>
  74. internal void InitHeap()
  75. {
  76. _previousHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
  77. }
  78. /// <summary>
  79. /// Stop the stopwatch and write elapsed ms to the given TextBlock.
  80. /// </summary>
  81. internal void StopMetrics(TextBlock lastOpText)
  82. {
  83. if (!OpStopwatch.IsRunning) return;
  84. OpStopwatch.Stop();
  85. lastOpText.Text = $"Last Op: {OpStopwatch.ElapsedMilliseconds} ms";
  86. }
  87. /// <summary>
  88. /// Force full GC, then invoke the refresh callback.
  89. /// </summary>
  90. internal void ForceGC(Action refresh)
  91. {
  92. GC.Collect();
  93. GC.WaitForPendingFinalizers();
  94. GC.Collect();
  95. refresh();
  96. }
  97. /// <summary>
  98. /// Start a 2-second auto-refresh timer.
  99. /// </summary>
  100. internal void StartAutoRefresh(Action refresh)
  101. {
  102. if (_autoRefreshTimer != null) return;
  103. _autoRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
  104. _autoRefreshTimer.Tick += (_, _) => refresh();
  105. _autoRefreshTimer.Start();
  106. }
  107. /// <summary>
  108. /// Stop the auto-refresh timer.
  109. /// </summary>
  110. internal void StopAutoRefresh()
  111. {
  112. _autoRefreshTimer?.Stop();
  113. _autoRefreshTimer = null;
  114. }
  115. /// <summary>
  116. /// Toggle auto-refresh based on a CheckBox.
  117. /// </summary>
  118. internal void OnAutoRefreshChanged(CheckBox check, Action refresh)
  119. {
  120. if (check.IsChecked == true)
  121. StartAutoRefresh(refresh);
  122. else
  123. StopAutoRefresh();
  124. }
  125. /// <summary>
  126. /// Append a timestamped log entry to a StackPanel inside a ScrollViewer.
  127. /// </summary>
  128. internal void LogOperation(string action, string detail,
  129. StackPanel logPanel, ScrollViewer logScroll, string? extraInfo = null)
  130. {
  131. var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
  132. var timing = OpStopwatch.ElapsedMilliseconds;
  133. var extra = extraInfo != null ? $" {extraInfo}," : "";
  134. logPanel.Children.Add(new TextBlock
  135. {
  136. Text = $"{DateTime.Now:HH:mm:ss} [{action}] {detail} —{extra} heap {heapMB:##0.0} MB, {timing} ms",
  137. FontSize = 10,
  138. FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"),
  139. Padding = new Thickness(6, 2),
  140. TextTrimming = TextTrimming.CharacterEllipsis,
  141. });
  142. logScroll.ScrollToEnd();
  143. }
  144. /// <summary>
  145. /// Build a tracked ContentPage with a 50 KB dummy allocation.
  146. /// </summary>
  147. internal ContentPage BuildTrackedPage(string title, int index, int allocBytes = 51200)
  148. {
  149. var page = NavigationDemoHelper.MakePage(title,
  150. $"Stack position #{index}\nPush more pages ...", index);
  151. page.Tag = new byte[allocBytes];
  152. TrackPage(page);
  153. return page;
  154. }
  155. /// <summary>
  156. /// Create a reusable stack/history row (badge + title + label).
  157. /// </summary>
  158. internal static (Border Container, Border Badge, TextBlock IndexText,
  159. TextBlock TitleText, TextBlock BadgeText) CreateStackRow()
  160. {
  161. var indexText = new TextBlock
  162. {
  163. FontSize = 10, FontWeight = FontWeight.SemiBold,
  164. HorizontalAlignment = HorizontalAlignment.Center,
  165. VerticalAlignment = VerticalAlignment.Center,
  166. };
  167. var badge = new Border
  168. {
  169. Width = 22, Height = 22,
  170. CornerRadius = new CornerRadius(11),
  171. VerticalAlignment = VerticalAlignment.Center,
  172. Child = indexText,
  173. };
  174. var titleText = new TextBlock
  175. {
  176. VerticalAlignment = VerticalAlignment.Center,
  177. TextTrimming = TextTrimming.CharacterEllipsis,
  178. Margin = new Thickness(6, 0, 0, 0),
  179. };
  180. var badgeText = new TextBlock
  181. {
  182. FontSize = 10, Opacity = 0.5,
  183. VerticalAlignment = VerticalAlignment.Center,
  184. Margin = new Thickness(4, 0, 0, 0),
  185. IsVisible = false,
  186. };
  187. var row = new DockPanel();
  188. row.Children.Add(badge);
  189. row.Children.Add(titleText);
  190. row.Children.Add(badgeText);
  191. var container = new Border
  192. {
  193. CornerRadius = new CornerRadius(6),
  194. Padding = new Thickness(8, 6),
  195. Child = row,
  196. };
  197. return (container, badge, indexText, titleText, badgeText);
  198. }
  199. /// <summary>
  200. /// Update a stack row with page data.
  201. /// </summary>
  202. internal static void UpdateStackRow(
  203. (Border Container, Border Badge, TextBlock IndexText,
  204. TextBlock TitleText, TextBlock BadgeText) row,
  205. int stackIndex, string title, bool isCurrent, bool isRoot)
  206. {
  207. row.Badge.Background = NavigationDemoHelper.GetPageBrush(stackIndex);
  208. row.IndexText.Text = (stackIndex + 1).ToString();
  209. row.TitleText.Text = title;
  210. row.TitleText.FontWeight = isCurrent ? FontWeight.SemiBold : FontWeight.Normal;
  211. string? label = isCurrent ? "current" : (isRoot ? "root" : null);
  212. row.BadgeText.Text = label ?? "";
  213. row.BadgeText.IsVisible = label != null;
  214. row.Container.BorderBrush = isCurrent ? CurrentBorderBrush : DefaultBorderBrush;
  215. row.Container.BorderThickness = new Thickness(isCurrent ? 2 : 1);
  216. }
  217. /// <summary>
  218. /// Sync a StackPanel of stack rows with data, growing/shrinking the row cache as needed.
  219. /// </summary>
  220. internal static void RefreshStackPanel(
  221. StackPanel panel,
  222. List<(Border Container, Border Badge, TextBlock IndexText,
  223. TextBlock TitleText, TextBlock BadgeText)> rowCache,
  224. IReadOnlyList<Page> stack, Page? currentPage)
  225. {
  226. int count = stack.Count;
  227. while (rowCache.Count < count)
  228. rowCache.Add(CreateStackRow());
  229. while (panel.Children.Count > count)
  230. panel.Children.RemoveAt(panel.Children.Count - 1);
  231. while (panel.Children.Count < count)
  232. panel.Children.Add(rowCache[panel.Children.Count].Container);
  233. for (int displayIdx = 0; displayIdx < count; displayIdx++)
  234. {
  235. int stackIdx = count - 1 - displayIdx;
  236. var page = stack[stackIdx];
  237. bool isCurrent = ReferenceEquals(page, currentPage);
  238. bool isRoot = stackIdx == 0;
  239. var row = rowCache[displayIdx];
  240. if (!ReferenceEquals(panel.Children[displayIdx], row.Container))
  241. panel.Children[displayIdx] = row.Container;
  242. UpdateStackRow(row, stackIdx, page.Header?.ToString() ?? "(untitled)", isCurrent, isRoot);
  243. }
  244. }
  245. }
  246. }