using System.Diagnostics; using System.Text; using TinyTUI.Components; using TinyTUI.Overlay; using TinyTUI.Rendering; using TinyTUI.Stdout; using TinyTUI.Terminal.Images; using TinyTUI.Text; namespace TinyTUI.Benchmarks; internal static class Program { private const int Width = 100; private const int Rows = 28; private static readonly TerminalSize Size = new(Width, Rows); private static int Main() { Console.OutputEncoding = Encoding.UTF8; var scenarios = new[] { RunLargeTextMeasureAndTruncate(), RunMarkdownRender(), RunOverlayCompose(), RunDifferentialRefresh(), RunImageLineGuard(), }; Console.WriteLine("TinyTUI benchmark pressure scenarios"); Console.WriteLine($"runtime: {Environment.Version}"); Console.WriteLine($"size: {Width}x{Rows}"); Console.WriteLine(); foreach (var scenario in scenarios) Console.WriteLine(scenario); return scenarios.Any(static scenario => scenario.Failed) ? 1 : 0; } /// /// 压测 ANSI 宽度测量和截断路径 /// private static ScenarioResult RunLargeTextMeasureAndTruncate() { var measurer = new TerminalTextMeasurer(); var lines = BuildLargeTextLines(lineCount: 2_000); var totalWidth = 0; var truncatedWidth = 0; return Measure("large text measure/truncate", lines.Count, () => { foreach (var line in lines) { totalWidth += measurer.GetWidth(line); truncatedWidth += measurer.GetWidth(measurer.Truncate(line, Width)); } }, () => $"totalWidth={totalWidth}, truncatedWidth={truncatedWidth}"); } /// /// 压测 Markdown 组件的轻量转换和宽度截断路径 /// private static ScenarioResult RunMarkdownRender() { var markdown = new Markdown(BuildMarkdownDocument(sectionCount: 240)); var renderedLineCount = 0; var checksum = 0; return Measure("markdown render", 60, () => { for (var iteration = 0; iteration < 60; iteration++) { var lines = markdown.Render(Width); renderedLineCount = lines.Count; checksum += lines[iteration % lines.Count].Length; } }, () => $"lines={renderedLineCount}, checksum={checksum}"); } /// /// 压测 overlay 合成中的按列切片和 reset 隔离路径 /// private static ScenarioResult RunOverlayCompose() { var manager = new OverlayManager(); var baseLines = BuildLargeTextLines(lineCount: Rows + 40); manager.Show( new StaticComponent(BuildOverlayLines()), new OverlayOptions { Anchor = OverlayAnchor.TopRight, Width = OverlaySize.Columns(42), MaxHeight = OverlaySize.Columns(10), Margin = new OverlayMargin(1), }); var composedLineCount = 0; var sampleWidth = 0; return Measure("overlay compose", 800, () => { for (var iteration = 0; iteration < 800; iteration++) { var composed = manager.Compose(baseLines, Size); composedLineCount = composed.Count; sampleWidth += composed[iteration % composed.Count].Length; } }, () => $"lines={composedLineCount}, sampledLength={sampleWidth}"); } /// /// 压测差分渲染频繁刷新但只写内存输出 /// private static ScenarioResult RunDifferentialRefresh() { var output = new MemoryTerminalOutput(); var renderer = new DifferentialRenderer( output, new TerminalTextMeasurer(), new RenderPipelineOptions { UseSynchronizedOutput = false }); var lines = BuildLargeTextLines(lineCount: Rows); return Measure("differential refresh", 1_000, () => { for (var frame = 0; frame < 1_000; frame++) { lines[Rows / 2] = $"tick={frame:0000} Δ refresh path \e[36mchanged\e[39m"; renderer.Render(lines, Size); } }, () => $"full={renderer.FullRedrawCount}, diff={renderer.DifferentialRedrawCount}, writes={output.WriteCount}, bytes={output.WrittenBytes}"); } /// /// 压测图像行检测保护 避免大段协议内容进入普通测宽截断路径 /// private static ScenarioResult RunImageLineGuard() { var output = new MemoryTerminalOutput(); var renderer = new DifferentialRenderer( output, new TerminalTextMeasurer(), new RenderPipelineOptions { UseSynchronizedOutput = false, ThrowOnWidthOverflow = true, }); var imageLine = BuildKittyImageLine(); var guarded = false; var ids = 0; return Measure("image line guard", 120, () => { for (var iteration = 0; iteration < 120; iteration++) { guarded |= TerminalImageService.IsImageLine(imageLine); ids += TerminalImageService.ExtractKittyImageIds(imageLine).Count(); renderer.Render([$"frame={iteration}", imageLine, "after-image"], new TerminalSize(32, 6)); } }, () => $"guarded={guarded}, extractedIds={ids}, bytes={output.WrittenBytes}"); } /// /// 统一执行场景并把异常转换为可读输出 /// private static ScenarioResult Measure(string name, int operations, Action action, Func getDetails) { try { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); var before = GC.GetTotalAllocatedBytes(precise: true); var stopwatch = Stopwatch.StartNew(); action(); stopwatch.Stop(); var allocated = GC.GetTotalAllocatedBytes(precise: true) - before; return ScenarioResult.Passed(name, operations, stopwatch.Elapsed, allocated, getDetails()); } catch (Exception exception) { return ScenarioResult.FailedResult(name, exception); } } /// /// 构造混合 ANSI 中文 emoji 和 tab 的长文本输入 /// private static List BuildLargeTextLines(int lineCount) { var lines = new List(lineCount); for (var index = 0; index < lineCount; index++) { // 混合不可见 ANSI 和宽字符 让测宽路径比 ASCII-only 更接近真实 TUI 输出 lines.Add($"\e[3{index % 7 + 1}mrow-{index:0000}\e[39m\t中文列 {index % 11} emoji 😀 link \e]8;;https://example.local/{index}\aword-{index}\e]8;;\a trailing text"); } return lines; } /// /// 构造重复 Markdown 文档用于观察轻量渲染成本 /// private static string BuildMarkdownDocument(int sectionCount) { var builder = new StringBuilder(); for (var section = 0; section < sectionCount; section++) { builder.AppendLine($"## Section {section}"); builder.AppendLine("- **item** with `code` and 中文内容"); builder.AppendLine("> quoted line with wide emoji 😀"); builder.AppendLine("plain paragraph that should be truncated by the component renderer when the viewport is narrow"); } return builder.ToString(); } /// /// 构造 overlay 内容并包含 ANSI 样式用于观察 reset 隔离 /// private static IReadOnlyList BuildOverlayLines() => [ "\e[1mBenchmark Overlay\e[22m", "status: \e[32mrunning\e[39m", "wide: 中文 😀 boundary", "short", "long value should be clipped at the overlay boundary", ]; /// /// 构造足够长的 Kitty 图像序列 模拟协议行保护场景 /// private static string BuildKittyImageLine() { var payload = Convert.ToBase64String(Enumerable.Range(0, 24_000).Select(static value => (byte)(value % 251)).ToArray()); return $"prefix \e_Ga=T,f=100,i=4242,q=2,c=20,r=8;{payload}\e\\ suffix"; } } internal sealed record ScenarioResult(string Name, int Operations, TimeSpan Elapsed, long AllocatedBytes, string Details, Exception? Exception) { public bool Failed => Exception is not null; public static ScenarioResult Passed(string name, int operations, TimeSpan elapsed, long allocatedBytes, string details) => new(name, operations, elapsed, allocatedBytes, details, null); public static ScenarioResult FailedResult(string name, Exception exception) => new(name, 0, TimeSpan.Zero, 0, string.Empty, exception); public override string ToString() { if (Exception is not null) return $"[fail] {Name}: {Exception.GetType().Name}: {Exception.Message}"; var perOperation = Operations <= 0 ? 0 : Elapsed.TotalMilliseconds / Operations; return $"[pass] {Name}: {Operations} ops, {Elapsed.TotalMilliseconds:N1} ms, {perOperation:N3} ms/op, {AllocatedBytes / 1024.0:N1} KiB allocated, {Details}"; } } internal sealed class StaticComponent(IReadOnlyList lines) : IComponent { public IReadOnlyList Render(int width) => lines; } internal sealed class MemoryTerminalOutput : ITerminalOutput { public int WriteCount { get; private set; } public long WrittenBytes { get; private set; } public void Write(string value) { WriteCount++; WrittenBytes += Encoding.UTF8.GetByteCount(value); } public void Flush() { } public void ClearScreen() => Write("\e[2J\e[H"); public void HideCursor() => Write("\e[?25l"); public void ShowCursor() => Write("\e[?25h"); public void MoveCursorTo(int row, int column) => Write($"\e[{row};{column}H"); }