using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Text; using AtomUI.Controls.Primitives; using AtomUI.Desktop.Controls; using AtomUI.Theme; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Primitives; using Avalonia.Headless; using Avalonia.Interactivity; using Avalonia.Threading; using Avalonia.VisualTree; using AvaloniaButton = Avalonia.Controls.Button; using AtomTabItem = AtomUI.Desktop.Controls.TabItem; using TabStrip = AtomUI.Desktop.Controls.TabStrip; using TabStripItem = AtomUI.Desktop.Controls.TabStripItem; namespace AtomUI.TabOverflowPerformance; internal static class Program { private static readonly Size MeasureSize = new(1280, 4096); private static readonly Rect ArrangeRect = new(0, 0, 1280, 4096); [STAThread] private static int Main(string[] args) { var options = Options.Parse(args); AppBuilder.Configure() .UseHeadless(new AvaloniaHeadlessPlatformOptions()) .SetupWithLifetime(new ClassicDesktopStyleApplicationLifetime()); if (options.CreationOnly) { return RunCreationBenchmarks(options); } var results = new[] { Measure("TabControl.Line", CreateTabControl(card: false), options), Measure("TabControl.Card", CreateTabControl(card: true), options), Measure("TabStrip.Line", CreateTabStrip(card: false), options), Measure("TabStrip.Card", CreateTabStrip(card: true), options) }; var output = RenderTable(results); Console.WriteLine(output); if (options.MarkdownPath is { Length: > 0 } markdownPath) { var fullPath = Path.GetFullPath(markdownPath); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); File.WriteAllText(fullPath, RenderMarkdown(results, options), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); Console.WriteLine($"Wrote markdown result: {fullPath}"); } return 0; } private static int RunCreationBenchmarks(Options options) { var results = new[] { MeasureCreation("TabControl.Line", () => CreateTabControl(card: false), options.Count), MeasureCreation("TabControl.Card", () => CreateTabControl(card: true), options.Count), MeasureCreation("TabStrip.Line", () => CreateTabStrip(card: false), options.Count), MeasureCreation("TabStrip.Card", () => CreateTabStrip(card: true), options.Count) }; var output = RenderCreationTable(results); Console.WriteLine(output); if (options.MarkdownPath is { Length: > 0 } markdownPath) { var fullPath = Path.GetFullPath(markdownPath); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); File.WriteAllText(fullPath, RenderCreationMarkdown(results, options), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); Console.WriteLine($"Wrote markdown result: {fullPath}"); } return 0; } private static CreationResult MeasureCreation(string name, Func factory, int count) { using (var warmup = new RealizedControl(factory())) { } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); var allocatedBefore = GC.GetAllocatedBytesForCurrentThread(); var stopwatch = Stopwatch.StartNew(); var panel = new StackPanel { Spacing = 8 }; for (var i = 0; i < count; i++) { panel.Children.Add(factory()); } using var realized = new RealizedControl(panel); stopwatch.Stop(); return new CreationResult( name, count, stopwatch.Elapsed, GC.GetAllocatedBytesForCurrentThread() - allocatedBefore, panel.GetVisualDescendants().Count()); } private static Result Measure(string name, Control control, Options options) { using var realized = new RealizedControl(control); var scrollViewer = FindNamed(control, "PART_CardTabStripScrollViewer") ?? FindNamed(control, "PART_TabsContainer") ?? throw new InvalidOperationException($"{name} did not realize its tab scroll viewer."); var indicator = FindNamed(scrollViewer, "PART_ScrollMenuIndicator") ?? throw new InvalidOperationException($"{name} did not realize PART_ScrollMenuIndicator."); if (!indicator.IsVisible) { throw new InvalidOperationException($"{name} did not overflow at the benchmark viewport."); } var closeOverflow = CreateCloseOverflowAction(scrollViewer); OpenOverflow(indicator); realized.RefreshLayout(); realized.AssertPopupOpen(); closeOverflow(); realized.RefreshLayout(); for (var i = 0; i < options.WarmupCount; i++) { OpenOverflow(indicator); realized.RefreshLayout(); closeOverflow(); realized.RefreshLayout(); } var visualsAfterWarmup = realized.VisualCount; GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); var gen0Before = GC.CollectionCount(0); var allocatedBefore = GC.GetAllocatedBytesForCurrentThread(); var stopwatch = Stopwatch.StartNew(); for (var i = 0; i < options.Count; i++) { OpenOverflow(indicator); realized.RefreshLayout(); closeOverflow(); realized.RefreshLayout(); } stopwatch.Stop(); return new Result( name, options.Count, stopwatch.Elapsed, GC.GetAllocatedBytesForCurrentThread() - allocatedBefore, GC.CollectionCount(0) - gen0Before, visualsAfterWarmup, realized.VisualCount); } private static void OpenOverflow(IconButton indicator) { indicator.RaiseEvent(new RoutedEventArgs(AvaloniaButton.ClickEvent, indicator)); } private static Action CreateCloseOverflowAction(Control scrollViewer) { foreach (var methodName in new[] { "CloseOverflowPopup", "CloseForLifecycle" }) { for (var type = scrollViewer.GetType(); type is not null; type = type.BaseType) { var method = type.GetMethod( methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); if (method is not null) { return method.CreateDelegate(scrollViewer); } } } throw new InvalidOperationException( $"{scrollViewer.GetType().FullName} does not expose an ordinary close path or its legacy fallback."); } private static T? FindNamed(Control root, string name) where T : Control { if (root is T typedRoot && root.Name == name) { return typedRoot; } return root.GetVisualDescendants().OfType().FirstOrDefault(control => control.Name == name); } private static BaseTabControl CreateTabControl(bool card) { BaseTabControl control = card ? new CardTabControl() : new AtomUI.Desktop.Controls.TabControl(); control.Width = 260; for (var i = 0; i < 24; i++) { control.Items.Add(new AtomTabItem { Header = $"Long Tab Header {i + 1}", Content = $"Content {i + 1}", IsClosable = i % 3 == 0 }); } return control; } private static BaseTabStrip CreateTabStrip(bool card) { BaseTabStrip control = card ? new CardTabStrip() : new TabStrip(); control.Width = 260; for (var i = 0; i < 24; i++) { control.Items.Add(new TabStripItem { Content = $"Long Tab Header {i + 1}", IsClosable = i % 3 == 0 }); } return control; } private static string RenderTable(IReadOnlyList results) { var builder = new StringBuilder(); builder.AppendLine("Scenario Cycles Total ms us/cycle KB total bytes/cycle Gen0 Visual warm/end"); builder.AppendLine("-------------------------------------------------------------------------------------------"); foreach (var result in results) { builder.AppendLine(CultureInfo.InvariantCulture, $"{result.Name,-18}{result.Cycles,7}{result.Elapsed.TotalMilliseconds,10:0.00}{result.MicrosecondsPerCycle,10:0.00}{result.AllocatedBytes / 1024.0,10:0.0}{result.BytesPerCycle,13:0.0}{result.Gen0Collections,6}{result.VisualsAfterWarmup,8}/{result.VisualsAfterMeasurement}"); } return builder.ToString(); } private static string RenderMarkdown(IReadOnlyList results, Options options) { var builder = new StringBuilder(); builder.AppendLine("# Tab Overflow Interaction Benchmark"); builder.AppendLine(); builder.AppendLine($"- Date: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss zzz}"); builder.AppendLine($"- Configuration: {typeof(Program).Assembly.GetCustomAttribute()?.Configuration} / net10.0"); builder.AppendLine("- Runner: `tools/performances/AtomUI.TabOverflowPerformance`"); builder.AppendLine($"- Warmup: {options.WarmupCount} complete open/close cycles per control"); builder.AppendLine("- Operation: realized overflowing control -> click more indicator -> ordinary close (legacy lifecycle fallback when no ordinary close method exists), with layout and a compositor frame at both boundaries"); builder.AppendLine("- Host: real headless overlay popup; visible menu content is required. Open/close layout work is included and host errors fail the run."); builder.AppendLine(); builder.AppendLine("| Scenario | Cycles | Total ms | us/cycle | KB total | bytes/cycle | Gen0 | Visual warm/end |"); builder.AppendLine("| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"); foreach (var result in results) { builder.AppendLine(CultureInfo.InvariantCulture, $"| {result.Name} | {result.Cycles} | {result.Elapsed.TotalMilliseconds:0.00} | {result.MicrosecondsPerCycle:0.00} | {result.AllocatedBytes / 1024.0:0.0} | {result.BytesPerCycle:0.0} | {result.Gen0Collections} | {result.VisualsAfterWarmup}/{result.VisualsAfterMeasurement} |"); } return builder.ToString(); } private static string RenderCreationTable(IReadOnlyList results) { var builder = new StringBuilder(); builder.AppendLine("Scenario Count Total ms ms/item KB/item Visuals"); builder.AppendLine("----------------------------------------------------------------"); foreach (var result in results) { builder.AppendLine(CultureInfo.InvariantCulture, $"{result.Name,-18}{result.Count,6}{result.Elapsed.TotalMilliseconds,10:0.00}{result.MillisecondsPerItem,9:0.000}{result.KilobytesPerItem,9:0.0}{result.VisualCount,9}"); } return builder.ToString(); } private static string RenderCreationMarkdown(IReadOnlyList results, Options options) { var builder = new StringBuilder(); builder.AppendLine("# Tab Never-open Creation/Layout Benchmark"); builder.AppendLine(); builder.AppendLine($"- Date: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss zzz}"); builder.AppendLine($"- Configuration: {typeof(Program).Assembly.GetCustomAttribute()?.Configuration} / net10.0"); builder.AppendLine("- Runner: `tools/performances/AtomUI.TabOverflowPerformance --creation-only`"); builder.AppendLine($"- Controls per scenario: {options.Count}"); builder.AppendLine("- Operation: construct overflowing tab owners -> attach -> measure/arrange/update layout without opening the overflow popup"); builder.AppendLine(); builder.AppendLine("| Scenario | Count | Total ms | ms/item | KB/item | Visuals |"); builder.AppendLine("| --- | ---: | ---: | ---: | ---: | ---: |"); foreach (var result in results) { builder.AppendLine(CultureInfo.InvariantCulture, $"| {result.Name} | {result.Count} | {result.Elapsed.TotalMilliseconds:0.00} | {result.MillisecondsPerItem:0.000} | {result.KilobytesPerItem:0.0} | {result.VisualCount} |"); } return builder.ToString(); } private sealed record Options(int Count, int WarmupCount, string? MarkdownPath, bool CreationOnly) { public static Options Parse(IReadOnlyList args) { var count = 500; var warmup = 25; string? markdown = null; var creationOnly = false; for (var i = 0; i < args.Count; i++) { if (args[i] == "--count" && i + 1 < args.Count && int.TryParse(args[++i], out var parsedCount)) { count = Math.Max(1, parsedCount); } else if (args[i] == "--warmup" && i + 1 < args.Count && int.TryParse(args[++i], out var parsedWarmup)) { warmup = Math.Max(0, parsedWarmup); } else if (args[i] == "--markdown" && i + 1 < args.Count) { markdown = args[++i]; } else if (args[i] == "--creation-only") { creationOnly = true; } } return new Options(count, warmup, markdown, creationOnly); } } private sealed record Result( string Name, int Cycles, TimeSpan Elapsed, long AllocatedBytes, int Gen0Collections, int VisualsAfterWarmup, int VisualsAfterMeasurement) { public double MicrosecondsPerCycle => Elapsed.TotalMilliseconds * 1000 / Cycles; public double BytesPerCycle => AllocatedBytes / (double)Cycles; } private sealed record CreationResult( string Name, int Count, TimeSpan Elapsed, long AllocatedBytes, int VisualCount) { public double MillisecondsPerItem => Elapsed.TotalMilliseconds / Count; public double KilobytesPerItem => AllocatedBytes / 1024.0 / Count; } private sealed class RealizedControl : IDisposable { public RealizedControl(Control control) { var host = new ScopeAwareOverlayLayerPanel(); host.Children.Add(control); var layers = new VisualLayerManager { Child = host }; var overlayProperty = typeof(VisualLayerManager).GetProperty( "EnablePopupOverlayLayer", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("The headless benchmark requires a popup overlay layer."); overlayProperty.SetValue(layers, true); Window = new Avalonia.Controls.Window { Width = MeasureSize.Width, Height = 900, Content = layers, ShowInTaskbar = false }; Window.Show(); RefreshLayout(); } private Avalonia.Controls.Window Window { get; } public int VisualCount => Window.GetVisualDescendants().Count(); public void AssertPopupOpen() { if (!Window.GetVisualDescendants().OfType().Any(host => host.IsVisible && host.Bounds.Width > 0 && host.Bounds.Height > 0 && host.GetVisualDescendants().OfType().Any(item => item.GetType().Name.EndsWith("OverflowMenuItem", StringComparison.Ordinal) && item.IsVisible && item.Bounds.Width > 0 && item.Bounds.Height > 0))) { throw new InvalidOperationException("The overflow popup did not realize visible menu content."); } } public void Dispose() { Window.Close(); CompleteFrames(); } public void RefreshLayout() { Dispatcher.UIThread.RunJobs(); Window.Measure(MeasureSize); Window.Arrange(ArrangeRect); Window.UpdateLayout(); // Complete the same frame boundary for both implementations. Layout // alone can leave detached visuals queued for compositor serialization. CompleteFrames(); } private static void CompleteFrames() { var dispatcher = Dispatcher.UIThread; for (var frame = 0; frame < 10; frame++) { dispatcher.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); if (!dispatcher.HasJobsWithPriority(DispatcherPriority.SystemIdle)) { return; } } throw new InvalidOperationException("The headless benchmark did not settle within ten frames."); } } } public sealed class PerformanceApplication : Application { public override void Initialize() { this.UseAtomUI(builder => { builder.UseDesktopControls(); }); } }