using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Glass.Editing; using Glass.Typesetting; namespace Glass.PerformanceTests { /// /// Times counting the lines of a text as it grows, and the questions and edits a scroll bar needs from the count. /// public static class IndexBenchmark { public const string Argument = "--index"; public const string OneLineArgument = "--one-line"; public const string NoWrapArgument = "--no-wrap"; public const string Usage = "Usage: Glass.PerformanceTests --index [text sizes in chars, comma separated] " + "[--one-line] [--no-wrap]"; public static readonly int[] DefaultSizes = { 1000000, 10000000, 100000000 }; public const int ViewWidth = 300; public const int SliceChars = 65536; public const double SlowSliceMilliseconds = 10; [System.Runtime.InteropServices.DllImport("kernel32.dll")] private static extern bool QueryThreadCycleTime(IntPtr thread, out ulong cycles); [System.Runtime.InteropServices.DllImport("kernel32.dll")] private static extern IntPtr GetCurrentThread(); // The cycles the calling thread has run, which a slice that waited for a core or a page doesn't spend. public static ulong ThreadCycles() { QueryThreadCycleTime(GetCurrentThread(), out ulong cycles); return cycles; } public struct SliceTime { public double Milliseconds; // The cycles the thread ran during the slice. public ulong Cycles; // How far the work had got before the slice began, in chars or frames. public int CountedBefore; // How many collections of the youngest generation and of the oldest ran during the slice. public int Gen0; public int Gen2; } // Says how the slices were spread, and what the slowest ones ran into. public static void ReportSlices(List sliceTimes, string what = "slices", string unit = "chars") { if (sliceTimes.Count == 0) { return; } var sorted = new List(sliceTimes); sorted.Sort((left, right) => right.Milliseconds.CompareTo(left.Milliseconds)); int slow = 0; int slowWithCollections = 0; foreach (SliceTime slice in sorted) { if (slice.Milliseconds > SlowSliceMilliseconds) { ++slow; if (slice.Gen0 > 0) { ++slowWithCollections; } } } Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " {5}: median {0:F2} ms, 99th {1:F2} ms, {2:N0} over {3} ms ({4:N0} of them with a collection " + "running)", sorted[sorted.Count / 2].Milliseconds, sorted[sorted.Count / 100].Milliseconds, slow, SlowSliceMilliseconds, slowWithCollections, what)); // The median slice sets how many cycles a millisecond of running takes, so a slow slice that ran far fewer // says it waited. SliceTime median = sorted[sorted.Count / 2]; double cyclesPerMillisecond = (double)median.Cycles / median.Milliseconds; for (int i = 0; i < Math.Min(5, sorted.Count); ++i) { Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " slowest {0}: {1:F2} ms ({2:F2} ms of it running) after {3:N0} {6}, {4} young and {5} full " + "collections", i + 1, sorted[i].Milliseconds, sorted[i].Cycles / cyclesPerMillisecond, sorted[i].CountedBefore, sorted[i].Gen0, sorted[i].Gen2, unit)); } } public static int Run(string[] args) { var sizes = new List(DefaultSizes); bool oneLine = false; bool noWrap = false; string sizesArgument = null; for (int i = 1; i < args.Length; ++i) { if (args[i] == OneLineArgument) { oneLine = true; } else if (args[i] == NoWrapArgument) { noWrap = true; } else if (sizesArgument == null) { sizesArgument = args[i]; } else { Console.Error.WriteLine(Usage); return 1; } } if (sizesArgument != null) { sizes.Clear(); foreach (string part in sizesArgument.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"; Console.WriteLine($"VisualLineIndex, {bitness}, {VisualLineIndex.DefaultBlockChars}-char blocks, " + $"{SliceChars:N0}-char slices, {ViewWidth} wide."); if (noWrap) { Console.WriteLine("Lines don't wrap, and their widths are kept."); } string chunk = TypingBenchmark.TextOfAbout(PagedBenchmark.ChunkSize); if (oneLine) { chunk = chunk.Replace('\r', ' ').Replace('\n', ' '); Console.WriteLine("One hard line."); } foreach (int size in sizes) { Console.WriteLine($"--- {size:N0} chars"); var paged = new PagedText(); while (paged.Length < size) { paged.Replace(paged.Length, 0, chunk); } var index = new VisualLineIndex(); var font = SampleFonts.Philosopher18; index.Configure(font, LineLayout.HeightFor(font, 1) + font.LineGap, noWrap ? int.MaxValue : ViewWidth, TrailingSpaceMode.Hang, measureWidths: noWrap); double slowest = 0; int slices = 0; var sliceTimes = new List(); var watch = Stopwatch.StartNew(); while (true) { int collections0 = GC.CollectionCount(0); int collections2 = GC.CollectionCount(2); int countedBefore = index.CountedChars(paged); ulong cyclesBefore = ThreadCycles(); long start = Stopwatch.GetTimestamp(); bool more = index.CountMore(paged, SliceChars); double milliseconds = (Stopwatch.GetTimestamp() - start) * 1000.0 / Stopwatch.Frequency; ulong cycles = ThreadCycles() - cyclesBefore; slowest = Math.Max(slowest, milliseconds); if (!more) { break; } sliceTimes.Add(new SliceTime { Milliseconds = milliseconds, Cycles = cycles, CountedBefore = countedBefore, Gen0 = GC.CollectionCount(0) - collections0, Gen2 = GC.CollectionCount(2) - collections2, }); ++slices; } watch.Stop(); ReportSlices(sliceTimes); Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " counted {0:N0} chars into {1:N0} blocks and {2:N0} rows in {3:F2} s ({4:F1} M chars/s), " + "{5:N0} slices, the slowest {6:F2} ms", paged.Length, index.BlockCount, index.TotalRows(paged), watch.Elapsed.TotalSeconds, paged.Length / watch.Elapsed.TotalSeconds / 1000000, slices, slowest)); int middle = paged.Length / 2; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("total rows", () => index.TotalRows(paged)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("row of the middle", () => index.RowOf(paged, middle, atLineEnd: false)))); int row = index.RowOf(paged, middle, atLineEnd: false); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("index of a row", () => index.IndexOfRow(paged, row)))); if (noWrap) { long across = index.WidthBefore(paged, middle); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("widest line", () => index.WidestLine(paged)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("width before the middle", () => index.WidthBefore(paged, middle)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("index at a width", () => index.IndexAtWidth(paged, middle, across)))); Console.WriteLine($" widest line {index.WidestLine(paged):N0} px, middle at {across:N0} px"); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("key + widest + width", () => { paged.Replace(middle, 0, "x"); index.NoteEdit(paged, middle, 0, 1); index.WidestLine(paged); index.WidthBefore(paged, middle); }))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("key + delete + widest", () => { paged.Replace(middle, 0, "x"); index.NoteEdit(paged, middle, 0, 1); paged.Replace(middle, 1, ""); index.NoteEdit(paged, middle, 1, 0); index.WidestLine(paged); }))); } Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("key + total + row", () => { paged.Replace(middle, 0, "x"); index.NoteEdit(paged, middle, 0, 1); index.TotalRows(paged); index.RowOf(paged, middle, atLineEnd: false); }))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("key + delete", () => { paged.Replace(middle, 0, "x"); index.NoteEdit(paged, middle, 0, 1); index.TotalRows(paged); paged.Replace(middle, 1, ""); index.NoteEdit(paged, middle, 1, 0); index.TotalRows(paged); }))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("newline in a line", () => { paged.Replace(middle, 0, "\n"); index.NoteEdit(paged, middle, 0, 1); index.TotalRows(paged); paged.Replace(middle, 1, ""); index.NoteEdit(paged, middle, 1, 0); index.TotalRows(paged); }))); string manyBlocks = TypingBenchmark.TextOfAbout(4 * VisualLineIndex.DefaultBlockChars); foreach (var place in new[] { ("start", 0), ("middle", middle), ("end", paged.Length) }) { int at = place.Item2; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure( "block count changes, " + place.Item1, () => { paged.Replace(at, 0, manyBlocks); index.NoteEdit(paged, at, 0, manyBlocks.Length); index.TotalRows(paged); paged.Replace(at, manyBlocks.Length, ""); index.NoteEdit(paged, at, manyBlocks.Length, 0); index.TotalRows(paged); if (noWrap) { index.WidestLine(paged); } }))); } watch.Restart(); int removed = paged.Length / 2; int removedAt = paged.Length / 4; paged.Replace(removedAt, removed, ""); double deleted = watch.Elapsed.TotalMilliseconds; index.NoteEdit(paged, removedAt, removed, 0); double noted = watch.Elapsed.TotalMilliseconds; int total = index.TotalRows(paged); watch.Stop(); Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " deleting half the text once: {0:F1} ms ({1:F1} in the text, {2:F1} noting it, {3:F1} " + "reckoning), {4:N0} chars still counted, {5:N0} rows reckoned", watch.Elapsed.TotalMilliseconds, deleted, noted - deleted, watch.Elapsed.TotalMilliseconds - noted, index.CountedChars(paged), total)); } return 0; } } }