using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Text; using Annulus; using Azimuth; namespace Glass.PerformanceTests { /// /// Times a keystroke in a multiline TextBox as its text grows, beside a control that only copies a string of the /// same size, so what the box adds to the copy every edit must make shows. /// public static class TypingBenchmark { public const string Argument = "--typing"; public const string OneLineArgument = "oneline"; public const string Usage = "Usage: Glass.PerformanceTests --typing [text sizes in chars, comma separated [oneline]]"; public const string Sentence = "The quick brown fox jumps over the lazy dog, and then keeps running.\n"; public static readonly int[] DefaultSizes = { 10000, 40000, 100000, 400000, 1000000, 4000000 }; public const double BatchMilliseconds = 20; public const int Rounds = 11; public sealed record FixedSizeMaterial : Material { private readonly Vector size; public FixedSizeMaterial(Vector size) => this.size = size; protected override Vector PreferredSizeInModelSpace(Constraints constraints) => size; } public struct Row { public string Name; public double MedianMilliseconds; public double MinimumMilliseconds; public double KilobytesPerKey; public double Gen0PerHundred; public double Gen1PerHundred; public double Gen2PerHundred; } public static string OneLineOfAbout(int chars) { return TextOfAbout(chars).Replace(Sentence[Sentence.Length - 1], ' '); } public static string TextOfAbout(int chars) { var returnMe = new StringBuilder(chars + Sentence.Length); while (returnMe.Length < chars) { returnMe.Append(Sentence); } return returnMe.ToString(); } private static ScrollBarMaterial BarLook() { ButtonMaterial arrow = new ButtonMaterial(new FixedSizeMaterial(new Vector(14, 14))); return new ScrollBarMaterial { Background = default, Track = new Material(), DecrementButton = arrow, IncrementButton = arrow, Thumb = arrow, }; } public static TextBox NewBox(string text, bool withScrollBar) { var material = new TextBoxMaterial { Scale = default, Background = default, InactiveSelection = default, Composition = default, CompositionThickness = default, CompositionTarget = default, CompositionTargetThickness = default, ScrollBar = default, HorizontalScrollBar = default, Text = new TextMaterialStyle { Font = SampleFonts.Philosopher18, HorizontalMargin = 0, VerticalMargin = 0, }, Selection = new SolidColorMaterial(), Caret = new SolidColorMaterial(), CaretWidth = 2, }; if (withScrollBar) { material.ScrollBar = BarLook(); } var box = new TextBox(new TextBoxBehavior { Multiline = true }) { TextBoxMaterial = material }; Scalar fourLines = 4 * SampleFonts.Philosopher18.LineHeight; box.Bounds = Rect.FromPositionAndSize(Vector.Zero, new Vector(300, fourLines)); box.TextChunks = new Amphora.StringChunks(text); box.Buffer.ClearHistory(); box.Buffer.MoveTo(text.Length / 2, extend: false); box.Update(Amphora.Duration.Zero, new UpdateState()); return box; } private static double Median(List samples) { samples.Sort(); return samples[samples.Count / 2]; } /// /// Times `keystroke` in batches sized to take about , and reports the typical /// batch per key along with what it allocated and how often the collector ran. /// public static Row Measure(string name, Action keystroke) { for (int i = 0; i < 3; ++i) { keystroke(); } if (WarmStart.IsWarming) { return new Row { Name = name }; } long start = Stopwatch.GetTimestamp(); keystroke(); double firstMilliseconds = (Stopwatch.GetTimestamp() - start) * 1000.0 / Stopwatch.Frequency; int keysPerBatch = Math.Max(1, (int)(BatchMilliseconds / Math.Max(firstMilliseconds, 0.001))); GC.Collect(); int gen0 = GC.CollectionCount(0); int gen1 = GC.CollectionCount(1); int gen2 = GC.CollectionCount(2); long bytesBefore = GC.GetAllocatedBytesForCurrentThread(); var samples = new List(Rounds); for (int round = 0; round < Rounds; ++round) { long batchStart = Stopwatch.GetTimestamp(); for (int key = 0; key < keysPerBatch; ++key) { keystroke(); } samples.Add((Stopwatch.GetTimestamp() - batchStart) * 1000.0 / Stopwatch.Frequency / keysPerBatch); } double keys = (double)keysPerBatch * Rounds; double minimum = double.MaxValue; foreach (double sample in samples) { minimum = Math.Min(minimum, sample); } return new Row { Name = name, MedianMilliseconds = Median(samples), MinimumMilliseconds = minimum, KilobytesPerKey = (GC.GetAllocatedBytesForCurrentThread() - bytesBefore) / keys / 1024, Gen0PerHundred = (GC.CollectionCount(0) - gen0) * 100 / keys, Gen1PerHundred = (GC.CollectionCount(1) - gen1) * 100 / keys, Gen2PerHundred = (GC.CollectionCount(2) - gen2) * 100 / keys, }; } public static string Format(Row row) { return string.Format(CultureInfo.InvariantCulture, " {0,-18} {1,9:F3} ms (min {2,8:F3}) {3,9:F1} KB/key GCs per 100 keys: " + "{4,5:F1} / {5,5:F1} / {6,5:F1}", row.Name, row.MedianMilliseconds, row.MinimumMilliseconds, row.KilobytesPerKey, row.Gen0PerHundred, row.Gen1PerHundred, row.Gen2PerHundred); } public static Row MeasureBox(string name, string text, bool withScrollBar) { TextBox box = NewBox(text, withScrollBar); var keys = new KeyboardManager(); return Measure(name, () => box.TextInput("x", keys)); } public static Row MeasureBoxFrame(string name, string text) { TextBox box = NewBox(text, withScrollBar: true); var keys = new KeyboardManager(); return Measure(name, () => { box.TextInput("x", keys); box.Update(Amphora.Duration.Zero, new UpdateState()); }); } /// /// Updates a fresh box with a scroll bar, a frame at a time, until it has counted the lines of its text, and /// reports how many frames that took and how long the slowest was. /// public static string MeasureCounting(string text) { TextBox box = NewBox(text, withScrollBar: true); int frames = 0; double slowest = 0; var watch = Stopwatch.StartNew(); while (!box.AllLinesCounted) { long start = Stopwatch.GetTimestamp(); box.Update(Amphora.Duration.Zero, new UpdateState()); slowest = Math.Max(slowest, (Stopwatch.GetTimestamp() - start) * 1000.0 / Stopwatch.Frequency); ++frames; } return string.Format(CultureInfo.InvariantCulture, " counting the lines {0,6:N0} frames, {1,8:F1} ms in all, the slowest frame {2:F2} ms", frames, watch.Elapsed.TotalMilliseconds, slowest); } public static Row MeasureControl(string text) { var scratch = new StringBuilder(text); string kept = null; Row returnMe = Measure("control (copy only)", () => { scratch.Insert(scratch.Length / 2, 'x'); kept = scratch.ToString(); }); GC.KeepAlive(kept); return returnMe; } public static int Run(string[] args) { var sizes = new List(DefaultSizes); if (args.Length > 3 || (args.Length == 3 && args[2] != OneLineArgument)) { Console.Error.WriteLine(Usage); return 1; } bool oneLine = args.Length == 3; if (args.Length >= 2) { sizes.Clear(); foreach (string part in args[1].Split(',')) { if (!int.TryParse(part, NumberStyles.None, CultureInfo.InvariantCulture, out int size) || size < 1) { Console.Error.WriteLine(Usage); return 1; } sizes.Add(size); } } string bitness = Environment.Is64BitProcess ? "x64" : "x86"; string shape = oneLine ? "text of one enormous line" : "text of many lines"; Console.WriteLine($"Keystroke into the middle of a multiline TextBox, {bitness}, {shape}."); Console.WriteLine("GCs per 100 keys are generation 0 / 1 / 2. The control copies a string the same size."); foreach (int size in sizes) { string text = oneLine ? OneLineOfAbout(size) : TextOfAbout(size); Console.WriteLine($"--- {text.Length:N0} chars"); Console.WriteLine(Format(MeasureControl(text))); Console.WriteLine(Format(MeasureBox("box", text, withScrollBar: false))); Console.WriteLine(Format(MeasureBox("box + scroll bar", text, withScrollBar: true))); Console.WriteLine(Format(MeasureBoxFrame("key + frame", text))); Console.WriteLine(MeasureCounting(text)); } return 0; } } }