123 private static readonly TimeSpan MinimumRefreshInterval = TimeSpan.FromMilliseconds(10);
124 private static readonly TimeSpan MaximumRefreshInterval = TimeSpan.FromMinutes(1);
125 private const int MetricSelectorHeight = 30;
126 private const int MetricSelectorWidth = 92;
127 private const int MetricSelectorMargin = 4;
129 private readonly Dictionary<string, bool> _sourceVisibility =
new(StringComparer.OrdinalIgnoreCase);
130 private readonly Dictionary<string, bool> _measurementVisibility =
new(StringComparer.Ordinal);
131 private readonly Dictionary<string, bool> _sourceMeasurementVisibility =
new(StringComparer.Ordinal);
132 private readonly Dictionary<string, string> _measurementDescriptions =
new(StringComparer.Ordinal);
133 private readonly Dictionary<int, string> _renderedMetricLines = [];
136 private TimeSpan _refreshInterval = TimeSpan.FromMilliseconds(250);
137 private long _lastRefreshTimestamp;
138 private IDisposable? _collectionRequest;
139 private bool _disposed;
140 private string _headerText =
"Gondwana Runtime Profiler";
142 private bool _measurementTooltipsEnabled =
true;
143 private bool _showMetricSelector =
true;
147 private readonly List<MetricSelectorRow> _metricSelectorRows = [];
148 private string? _metricSelectorSignature;
149 private bool _metricSelectorOpen;
150 private int _profilerZOrder;
151 private bool _showHeader =
true;
152 private bool _showSnapshotMetadata =
true;
153 private bool _showSourceHeaders =
true;
154 private bool _showUnavailableMeasurements;
167 string? nickname =
null)
171 ValidateBounds(bounds).Location,
174 ArgumentNullException.ThrowIfNull(view);
177 Rectangle displayBounds = GetDisplayBounds(bounds, showSelector:
true);
183 nickname: $
"{Nickname}.display")
186 VerticalScrollEndPaddingPx = 8f
190 .SetFont(SKTypeface.Default, 15f, minSize: 9f)
191 .SetColors(SKColors.White,
new SKColor(0, 0, 0, 102))
193 .SetPadding(12f, 10f)
194 .EnableWrapping(
false);
199 displayBounds.X - bounds.X,
200 displayBounds.Y - bounds.Y));
206 $
"{Nickname}.tooltip");
215 Display.PointerMove += OnProfilerPointerMove;
216 Display.PointerLeave += OnProfilerPointerLeave;
217 Display.MouseWheel += OnProfilerMouseWheel;
219 Display.SetText(
"Gondwana Runtime Profiler\nInactive. Call Show() to collect telemetry.");
236 return new Rectangle(
237 (
int)MathF.Round(position.X),
238 (
int)MathF.Round(position.Y),
267 get => _refreshInterval;
270 if (value < MinimumRefreshInterval || value > MaximumRefreshInterval)
272 throw new ArgumentOutOfRangeException(
275 "Profiler widget refresh interval must be between 10 ms and one minute.");
278 _refreshInterval = value;
287 get => _measurementVisibilityMode;
290 if (!Enum.IsDefined(value))
291 throw new ArgumentOutOfRangeException(nameof(value));
293 if (_measurementVisibilityMode == value)
296 _measurementVisibilityMode = value;
308 ArgumentException.ThrowIfNullOrWhiteSpace(value);
319 set => _showHeader = value;
327 get => _showSnapshotMetadata;
328 set => _showSnapshotMetadata = value;
336 get => _showSourceHeaders;
337 set => _showSourceHeaders = value;
346 get => _showUnavailableMeasurements;
349 if (_showUnavailableMeasurements == value)
352 _showUnavailableMeasurements = value;
361 get => _measurementTooltipsEnabled;
364 if (_measurementTooltipsEnabled == value)
367 _measurementTooltipsEnabled = value;
370 _measurementTooltip.HideTooltip();
384 get => _showMetricSelector;
387 if (_showMetricSelector == value)
390 _showMetricSelector = value;
391 _metricSelectorSignature =
null;
394 DisposeMetricSelector();
398 if (value && _latestSnapshot is not
null)
399 EnsureMetricSelector(_latestSnapshot);
440 ArgumentException.ThrowIfNullOrWhiteSpace(sourceName);
441 _sourceVisibility[sourceName] = visible;
453 ValidateMetricKey(metricKey);
454 _measurementVisibility[metricKey] = visible;
470 ArgumentException.ThrowIfNullOrWhiteSpace(sourceName);
471 ValidateMetricKey(metricKey);
472 _sourceMeasurementVisibility[CreateSourceMetricKey(sourceName, metricKey)] = visible;
482 _sourceVisibility.
Clear();
483 _measurementVisibility.
Clear();
484 _sourceMeasurementVisibility.
Clear();
496 ValidateMetricKey(metricKey);
498 if (
string.IsNullOrWhiteSpace(description))
499 _measurementDescriptions.Remove(metricKey);
501 _measurementDescriptions[metricKey] = description.Trim();
514 ArgumentException.ThrowIfNullOrWhiteSpace(sourceName);
515 ValidateMetricKey(metricKey);
527 Display.SetColors(foreground, background);
540 Display.SetFont(typeface, size, minSize);
551 _profilerZOrder = zOrder;
552 Display.SetLabelZOrder(zOrder);
553 _metricSelectorButton?.SetButtonZOrder(zOrder + 100);
554 _metricSelectorList?.SetListBoxZOrder(zOrder + 200);
555 _measurementTooltip.SetTooltipZOrder(zOrder + 1000);
569 _latestSnapshot = snapshot;
571 string text = FormatSnapshot(snapshot);
572 CaptureRenderedMetricLines(text);
575 if (snapshot is not
null)
576 EnsureMetricSelector(snapshot);
586 if (!_metricSelectorOpen)
587 _metricSelectorList?.Hide();
589 if (_collectionRequest is not
null)
593 _lastRefreshTimestamp = 0;
594 Engine.Instance.AfterBackgroundTasksExecute += OnAfterBackgroundTasksExecute;
601 _measurementTooltip.HideTooltip();
602 _metricSelectorOpen =
false;
603 _metricSelectorList?.Hide();
605 base.ProcessHidden();
618 Display.PointerMove -= OnProfilerPointerMove;
619 Display.PointerLeave -= OnProfilerPointerLeave;
620 Display.MouseWheel -= OnProfilerMouseWheel;
622 _measurementTooltip.HideTooltip();
623 _measurementTooltip.Dispose();
624 DisposeMetricSelector();
629 private void OnAfterBackgroundTasksExecute()
631 if (_collectionRequest is
null)
634 long now = Stopwatch.GetTimestamp();
635 if (_lastRefreshTimestamp != 0 &&
636 Stopwatch.GetElapsedTime(_lastRefreshTimestamp, now) <
RefreshInterval)
641 _lastRefreshTimestamp = now;
645 private void StopCollection()
647 Engine.Instance.AfterBackgroundTasksExecute -= OnAfterBackgroundTasksExecute;
648 _collectionRequest?.Dispose();
649 _collectionRequest =
null;
650 _lastRefreshTimestamp = 0;
655 var text =
new StringBuilder();
661 if (view is not
null)
663 var extensionContext =
new ProfilerWidgetExtensionContext(
668 AppendAdditionalLines(text, extensionContext);
669 AppendRuntimeContext(text, extensionContext);
672 if (snapshot is
null)
674 text.Append(
"Waiting for first completed telemetry window...");
675 return text.ToString().TrimEnd();
680 text.Append(
"Generation ")
681 .Append(snapshot.Generation)
682 .Append(
" | window ")
683 .Append((snapshot.ElapsedSeconds * 1000d).ToString(
"0.0"))
686 if (snapshot.Truncated)
687 text.Append(
" | detail truncated");
692 bool wroteSource =
false;
695 if (!IsSourceVisible(source.Name))
698 var metrics = source.Metrics
699 .Where(pair => ResolveMeasurementVisibility(
702 pair.Value.Availability))
703 .OrderBy(pair => pair.Key, StringComparer.Ordinal)
706 if (metrics.Length == 0)
714 text.Append(source.Name)
716 .Append(source.Backend)
720 foreach (KeyValuePair<string, TelemetrySummary> pair
in metrics)
725 .AppendLine(FormatSummary(pair.Value, snapshot.ElapsedSeconds));
732 text.Append(
"No measurements selected.");
734 return text.ToString().TrimEnd();
737 private void AppendAdditionalLines(
739 ProfilerWidgetExtensionContext context)
745 foreach (
string line
in lines)
746 text.AppendLine(line ??
string.Empty);
749 private void AppendRuntimeContext(
751 ProfilerWidgetExtensionContext context)
755 var scene = context.RenderSurfaceHost.Scene;
756 long gridCells = scene.SceneLayers.Sum(
757 static layer => (
long)layer.GridColumnCount * layer.GridRowCount);
759 text.Append(
"Animating tiles: ")
760 .AppendLine(Tile.TilesAnimating.Count.ToString(
"N0"));
761 text.Append(
"Layers / grid cells: ")
762 .Append(scene.SceneLayers.Count)
764 .AppendLine(gridCells.ToString(
"N0"));
767 Rectangle viewport = context.View.Viewport.TargetRectPx;
770 PointF camera = context.View.Camera.PositionPx;
771 text.Append(
"Camera: ")
772 .Append(camera.X.ToString(
"0.0"))
774 .Append(camera.Y.ToString(
"0.0"))
775 .Append(
" px; zoom: ")
776 .Append(context.View.Viewport.Zoom.ToString(
"0.000"))
782 if (includeViewport && includeBackbuffer)
784 var backbuffer = context.RenderSurfaceHost.Backbuffer;
785 text.Append(
"Viewport / backbuffer: ")
786 .Append(viewport.Width)
788 .Append(viewport.Height)
790 .Append(backbuffer.Width)
792 .AppendLine(backbuffer.Height.ToString());
794 else if (includeViewport)
796 text.Append(
"Viewport: ")
797 .Append(viewport.Width)
799 .AppendLine(viewport.Height.ToString());
801 else if (includeBackbuffer)
803 var backbuffer = context.RenderSurfaceHost.Backbuffer;
804 text.Append(
"Backbuffer: ")
805 .Append(backbuffer.Width)
807 .AppendLine(backbuffer.Height.ToString());
812 text.Append(
"Target FPS / VSync: ")
813 .Append(Engine.Instance.Configuration.TargetFPS)
815 .AppendLine(Engine.Instance.Configuration.VSync.ToString());
819 context.RenderSurfaceHost.Backbuffer is GpuBackbuffer gpu)
821 text.Append(
"MSAA requested / actual / max: ")
822 .Append(gpu.MsaaSampleCount)
824 .Append(gpu.ActualMsaaSampleCount)
826 .AppendLine(gpu.MaxSupportedMsaaSampleCount.ToString());
830 private void ApplyLayout()
838 displayBounds.X - bounds.X,
839 displayBounds.Y - bounds.Y));
840 Display.Size = displayBounds.Size;
842 if (_metricSelectorButton is not
null || _metricSelectorList is not
null)
844 DisposeMetricSelector();
845 _metricSelectorSignature =
null;
848 EnsureMetricSelector(_latestSnapshot);
852 private static Rectangle GetDisplayBounds(Rectangle bounds,
bool showSelector)
854 int toolbarHeight = showSelector
855 ? Math.Min(MetricSelectorHeight, Math.Max(0, bounds.Height - 1))
860 bounds.Top + toolbarHeight,
862 Math.Max(1, bounds.Height - toolbarHeight));
865 private static Rectangle GetMetricSelectorButtonBounds(Rectangle bounds)
867 int width = Math.Max(
871 Math.Max(1, bounds.Width - MetricSelectorMargin * 2)));
872 int height = Math.Max(
875 MetricSelectorHeight - MetricSelectorMargin * 2,
876 Math.Max(1, bounds.Height)));
879 bounds.Left + MetricSelectorMargin,
880 bounds.Top + MetricSelectorMargin,
885 private static Rectangle GetMetricSelectorListBounds(Rectangle bounds)
887 int width = Math.Max(24, Math.Min(460, Math.Max(24, bounds.Width - MetricSelectorMargin * 2)));
888 int availableHeight = Math.Max(
890 bounds.Height - MetricSelectorHeight - MetricSelectorMargin);
891 int height = Math.Max(28, Math.Min(420, availableHeight));
894 bounds.Left + MetricSelectorMargin,
895 bounds.Top + MetricSelectorHeight,
906 string signature = BuildMetricSelectorSignature(snapshot);
907 if (_metricSelectorButton is
null ||
908 _metricSelectorList is
null ||
909 !
string.Equals(_metricSelectorSignature, signature, StringComparison.Ordinal))
911 bool wasOpen = _metricSelectorOpen;
912 DisposeMetricSelector();
913 _metricSelectorOpen = wasOpen;
916 Rectangle buttonBounds = GetMetricSelectorButtonBounds(bounds);
917 Rectangle listBounds = GetMetricSelectorListBounds(bounds);
919 _metricSelectorButton =
new ButtonWidget(
924 $
"{Nickname}.metric-selector.button")
925 .SetButtonZOrder(_profilerZOrder + 100);
927 _metricSelectorButton.Clicked += ToggleMetricSelector;
929 _metricSelectorList =
new ListBoxWidget(
933 nickname: $
"{Nickname}.metric-selector.list")
936 VerticalScrollBarVisibility = ScrollBarVisibility.Auto
940 .SetListBoxZOrder(_profilerZOrder + 200)
941 .SetSelectionColor(Color.FromArgb(255, 70, 70, 84));
943 _metricSelectorList.SelectionCommitted += OnMetricSelectorSelectionCommitted;
946 _metricSelectorButton,
948 buttonBounds.X - bounds.X,
949 buttonBounds.Y - bounds.Y));
953 listBounds.X - bounds.X,
954 listBounds.Y - bounds.Y));
956 _metricSelectorSignature = signature;
958 if (!_metricSelectorOpen)
959 _metricSelectorList.Hide();
962 RefreshMetricSelectorRows(snapshot);
967 var signature =
new StringBuilder()
972 foreach (var sourceGroup
in snapshot.Sources
973 .GroupBy(source => source.Name, StringComparer.OrdinalIgnoreCase)
974 .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase))
976 signature.Append(
'|').Append(sourceGroup.Key);
978 foreach (
string backend
in sourceGroup
979 .Select(source => source.Backend)
980 .Distinct(StringComparer.Ordinal)
981 .OrderBy(value => value, StringComparer.Ordinal))
983 signature.Append(
'[').Append(backend).Append(
']');
986 foreach (
string metricKey
in sourceGroup
987 .SelectMany(source => source.Metrics.Keys)
988 .Distinct(StringComparer.Ordinal)
989 .OrderBy(value => value, StringComparer.Ordinal))
991 signature.Append(
';').Append(metricKey);
995 return signature.ToString();
1000 if (_metricSelectorList is
null)
1003 _metricSelectorRows.Clear();
1007 "Hover definitions",
1013 _metricSelectorRows.Add(
new MetricSelectorRow(
"Show all measurements", () =>
1018 _metricSelectorRows.Add(
new MetricSelectorRow(
"Hide all measurements", () =>
1023 _metricSelectorRows.Add(
new MetricSelectorRow(
"------------------------------",
null));
1025 foreach (var sourceGroup
in snapshot.Sources
1026 .GroupBy(source => source.Name, StringComparer.OrdinalIgnoreCase)
1027 .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase))
1029 string sourceName = sourceGroup.Key;
1030 string backend =
string.Join(
1033 .Select(source => source.Backend)
1034 .Distinct(StringComparer.Ordinal)
1035 .OrderBy(value => value, StringComparer.Ordinal));
1037 bool sourceVisible = IsSourceVisible(sourceName);
1040 $
"{sourceName} [{backend}]",
1043 foreach (
string metricKey
in sourceGroup
1044 .SelectMany(source => source.Metrics.Keys)
1045 .Distinct(StringComparer.Ordinal)
1046 .OrderBy(value => value, StringComparer.Ordinal))
1049 .Where(source => source.Metrics.ContainsKey(metricKey))
1050 .Select(source => source.Metrics[metricKey].Availability)
1053 bool metricVisible = ResolveMeasurementVisibility(
1057 string capturedMetricKey = metricKey;
1058 string capturedSourceName = sourceName;
1060 string availabilitySuffix = availability == TelemetryAvailability.Available
1062 : $
" [{FormatAvailability(availability)}]";
1066 $
" {metricKey}{availabilitySuffix}",
1070 !ResolveMeasurementVisibility(
1073 capturedAvailability)));
1077 int topIndex = _metricSelectorList.TopIndex;
1078 _metricSelectorList.SetItems(_metricSelectorRows.Select(row => row.Text));
1080 int maximumTopIndex = Math.Max(
1082 _metricSelectorRows.Count - _metricSelectorList.VisibleItemCount);
1083 _metricSelectorList.TopIndex = Math.Min(topIndex, maximumTopIndex);
1086 private void AddSelectorRow(
bool isChecked,
string text, Action action)
1088 _metricSelectorRows.Add(
1089 new MetricSelectorRow(
1090 $
"[{(isChecked ? 'x' : ' ')}] {text}",
1094 private void ToggleMetricSelector()
1096 if (_metricSelectorList is
null)
1099 _metricSelectorOpen = !_metricSelectorOpen;
1100 if (_metricSelectorOpen)
1102 _metricSelectorList.Show();
1103 _metricSelectorList.Activate();
1107 _metricSelectorList.Hide();
1111 private void OnMetricSelectorSelectionCommitted(
int index)
1113 if (index < 0 || index >= _metricSelectorRows.Count)
1116 MetricSelectorRow row = _metricSelectorRows[index];
1117 if (row.Action is
null)
1124 private void DisposeMetricSelector()
1126 if (_metricSelectorButton is not
null)
1128 _metricSelectorButton.Clicked -= ToggleMetricSelector;
1129 RemoveChild(_metricSelectorButton, dispose:
true);
1130 _metricSelectorButton =
null;
1133 if (_metricSelectorList is not
null)
1135 _metricSelectorList.SelectionCommitted -= OnMetricSelectorSelectionCommitted;
1137 _metricSelectorList =
null;
1140 _metricSelectorRows.Clear();
1141 _metricSelectorSignature =
null;
1144 private void CaptureRenderedMetricLines(
string text)
1146 _renderedMetricLines.Clear();
1148 string[] lines = text
1149 .Replace(
"\r\n",
"\n", StringComparison.Ordinal)
1152 for (
int index = 0; index < lines.Length; index++)
1154 string line = lines[index];
1155 if (!line.StartsWith(
" ", StringComparison.Ordinal))
1158 int colon = line.IndexOf(
':', 2);
1162 string metricKey = line[2..colon].Trim();
1163 if (!
string.IsNullOrWhiteSpace(metricKey))
1164 _renderedMetricLines[index] = metricKey;
1168 private void OnProfilerPointerMove(WidgetPointerEventArgs args)
1172 _measurementTooltip.HideTooltip();
1178 if (!displayBounds.Contains(pointer))
1180 _measurementTooltip.HideTooltip();
1184 float lineHeight =
Display.TextBlock.LineHeight;
1185 if (lineHeight <= 0f)
1187 _measurementTooltip.HideTooltip();
1192 args.ScreenPositionPx.Y -
1194 Display.TextBlock.VerticalPadding +
1195 Display.VerticalScrollOffsetPx;
1199 _measurementTooltip.HideTooltip();
1203 int lineIndex = (int)MathF.Floor(localY / lineHeight);
1204 if (!_renderedMetricLines.TryGetValue(lineIndex, out
string? metricKey))
1206 _measurementTooltip.HideTooltip();
1210 string? description = GetMeasurementDescription(metricKey);
1211 if (
string.IsNullOrWhiteSpace(description))
1213 _measurementTooltip.HideTooltip();
1217 _measurementTooltip.ShowTooltip(
1218 $
"{metricKey}\n{description}",
1222 private void OnProfilerPointerLeave(WidgetPointerEventArgs args)
1225 _measurementTooltip.HideTooltip();
1228 private void OnProfilerMouseWheel(WidgetMouseWheelEventArgs args)
1231 _measurementTooltip.HideTooltip();
1234 private string? GetMeasurementDescription(
string metricKey)
1236 if (_measurementDescriptions.TryGetValue(metricKey, out
string? overrideDescription))
1237 return overrideDescription;
1239 string normalizedKey = NormalizeLayerMetricKey(metricKey);
1240 string? description = normalizedKey
switch
1243 "CPU time for one complete Engine cycle. The sample rate is the Engine cycle rate (CPS).",
1244 "background.cpu.ms" =>
1245 "CPU time spent in background/simulation work for one Engine cycle.",
1246 "foreground.cpu.ms" =>
1247 "CPU time spent in foreground/render-preparation work. The sample rate is foreground frame production.",
1249 "Engine-thread time spent building the render frame or render snapshot.",
1251 "Time spent querying and culling visible drawables. Sorting is reported separately.",
1253 "Time spent sorting queried drawables into render order.",
1255 "Time spent recording render commands after query/sort.",
1257 "Time spent recording view/direct-drawing overlay work.",
1258 "visible.drawables" =>
1259 "Visible drawable instances observed during a render build, including repeated instances across views.",
1261 "Visible tile draw instances observed during a render build.",
1263 "Atlas-backed tile batches generated for a render build.",
1265 "Tiles emitted through atlas-backed batches for a render build.",
1267 "GPU presentation-thread time spent replaying a completed render snapshot.",
1269 "Time spent replaying the recorded Skia picture into the GPU backbuffer.",
1270 "backbuffer.flush.cpu.ms" =>
1271 "Time spent flushing GPU backbuffer drawing after replay.",
1272 "snapshot.cpu.ms" =>
1273 "Time spent snapshotting the current GPU backbuffer for presentation/inspection.",
1274 "snapshot.age.ms" =>
1275 "Age of the consumed render snapshot when GPU replay begins. Higher values indicate producer/consumer lag.",
1276 "mailbox.published.lifetime" =>
1277 "Lifetime count of render snapshots published to the producer/consumer mailbox.",
1278 "mailbox.dropped.lifetime" =>
1279 "Lifetime count of published snapshots superseded before they could be consumed.",
1281 "Current occupied snapshot-mailbox slots.",
1282 "snapshot.commands.approximate" =>
1283 "Approximate command count retained by the latest render snapshot.",
1284 "gate.wait.cpu.ms" =>
1285 "Time spent waiting to enter the shared live render-state gate. Current desktop snapshot replay does not enter this broad gate, so the desktop GPU source reports NotApplicable.",
1286 "gate.held.cpu.ms" =>
1287 "Time spent while holding the shared live render-state gate. Current desktop snapshot replay does not enter this broad gate, so the desktop GPU source reports NotApplicable.",
1288 "presentation.count" =>
1289 "Completed presentation callbacks. Its sample rate is the actual GPU presentation FPS for this render surface.",
1290 "presentation.cpu.ms" =>
1291 "Total CPU duration of one GPU presentation callback.",
1292 "render.snapshot.cpu.ms" =>
1293 "Combined render/snapshot CPU work measured by the GPU presentation path.",
1295 "Platform presentation time spent blitting the completed image.",
1297 "Platform presentation time spent performing the final graphics flush.",
1299 "Number of SceneLayers omitted from detailed per-layer telemetry because the retained layer-detail limit was reached.",
1300 "layer.query.cpu.ms" =>
1301 "Per-layer time spent querying/culling visible content; sorting is excluded.",
1302 "layer.record.cpu.ms" =>
1303 "Per-layer time spent recording render commands.",
1304 "layer.drawables" =>
1305 "Visible drawable instances contributed by this SceneLayer across views.",
1307 "Visible tile draw instances contributed by this SceneLayer.",
1308 "layer.transformed.tiles" =>
1309 "Current count of this layer's visible tiles requiring transform work.",
1310 "layer.tile.width.px" =>
1311 "Current native tile width for this SceneLayer.",
1312 "layer.tile.height.px" =>
1313 "Current native tile height for this SceneLayer.",
1315 "Current SceneLayer Z-order.",
1319 if (description is
null)
1322 if (_latestSnapshot is
null)
1326 .SelectMany(source => source.Metrics)
1327 .Where(pair =>
string.Equals(
1328 NormalizeLayerMetricKey(pair.Key),
1330 StringComparison.Ordinal))
1331 .Select(pair => pair.Value)
1334 return summary?.Kind == TelemetryMetricKind.Sample
1336 "\n\navg/min/max are values observed during the current telemetry window. " +
1337 "samples is how many times this metric was recorded; samples/s is that count " +
1338 "divided by the window duration, not the metric value per second."
1342 private static string NormalizeLayerMetricKey(
string metricKey)
1344 if (!metricKey.StartsWith(
"layer.", StringComparison.Ordinal))
1347 int firstSeparator = metricKey.IndexOf(
'.',
"layer.".Length);
1348 if (firstSeparator < 0 || firstSeparator + 1 >= metricKey.Length)
1351 return "layer." + metricKey[(firstSeparator + 1)..];
1354 private sealed record MetricSelectorRow(
string Text, Action? Action);
1356 private bool IsSourceVisible(
string sourceName)
1358 return !_sourceVisibility.TryGetValue(sourceName, out
bool visible) || visible;
1361 private bool ResolveMeasurementVisibility(
1366 if (!IsSourceVisible(sourceName))
1369 bool explicitlySelected =
false;
1372 if (_sourceMeasurementVisibility.TryGetValue(
1373 CreateSourceMetricKey(sourceName, metricKey),
1374 out
bool sourceMetricVisible))
1376 selected = sourceMetricVisible;
1377 explicitlySelected = sourceMetricVisible;
1379 else if (_measurementVisibility.TryGetValue(metricKey, out
bool metricVisible))
1381 selected = metricVisible;
1382 explicitlySelected = metricVisible;
1392 return availability == TelemetryAvailability.Available ||
1397 private static string FormatSummary(
TelemetrySummary summary,
double elapsedSeconds)
1400 return summary.Availability.ToString();
1402 return summary.Kind
switch
1404 TelemetryMetricKind.Gauge =>
1405 $
"last {FormatNumber(summary.Last)}",
1406 TelemetryMetricKind.LifetimeCounter =>
1407 summary.WindowDelta is { } delta
1408 ? $
"last {FormatNumber(summary.Last)}, delta {FormatNumber(delta)}"
1409 : $
"last {FormatNumber(summary.Last)}, delta n/a",
1411 $
"avg {FormatNumber(summary.Mean)}, min {FormatNumber(summary.Minimum)}, " +
1412 $
"max {FormatNumber(summary.Maximum)}, samples {summary.Count:N0} " +
1413 $
"({FormatRate(summary.Count, elapsedSeconds)} samples/s)"
1417 private static string FormatRate(
long count,
double elapsedSeconds)
1419 return elapsedSeconds > 0d
1420 ? (count / elapsedSeconds).ToString(
"0.0")
1424 private static string FormatNumber(
double? value)
1426 return value?.ToString(
"0.###") ??
"n/a";
1431 return availability
switch
1433 TelemetryAvailability.Unsupported =>
"unsupported",
1434 TelemetryAvailability.NotApplicable =>
"not applicable",
1435 TelemetryAvailability.NotYetSampled =>
"not yet sampled",
1440 private static string CreateSourceMetricKey(
string sourceName,
string metricKey)
1442 return $
"{sourceName.ToUpperInvariant()}\u001f{metricKey}";
1445 private static void ValidateMetricKey(
string metricKey)
1447 ArgumentException.ThrowIfNullOrWhiteSpace(metricKey);
1450 private static Rectangle ValidateBounds(Rectangle bounds)
1452 ValidateSize(bounds.Size);
1456 private static void ValidateSize(
Size size)
1458 if (size.Width <= 0 || size.Height <= 0)
1459 throw new ArgumentOutOfRangeException(nameof(size), size,
"Profiler widget size must be positive.");