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;
}
}
}