using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Amphora; using Glass.Editing; namespace Glass.PerformanceTests { /// /// Times the operations a keystroke needs from PagedText as the text grows, from loading it to the middle of it /// being edited, and reports what it costs to hold. /// public static class PagedBenchmark { public const string Argument = "--paged"; public const string Usage = "Usage: Glass.PerformanceTests --paged [text sizes in chars, comma separated]"; public static readonly int[] DefaultSizes = { 1000000, 10000000, 100000000 }; public const int ChunkSize = 1000000; private static PagedText Loaded(string chunk, int size) { var returnMe = new PagedText(); while (returnMe.Length < size) { returnMe.Replace(returnMe.Length, 0, chunk); } return returnMe; } /// /// Times finding the ends of a line, and the run of plain pages, in a text that is one line of the size. /// public static void MeasureOneLine(int size) { PagedText line = Loaded(TypingBenchmark.OneLineOfAbout(ChunkSize), size); int middle = line.Length / 2; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("one line, ends of it", () => { line.StartOfLine(middle); line.EndOfLine(middle); }))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("one line, insert", () => line.Replace(middle, 0, "x")))); PagedText plain = Loaded(new string('a', ChunkSize), size); int plainMiddle = plain.Length / 2; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("plain run, forward", () => plain.PlainRunEnd(plainMiddle)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("plain run, back", () => plain.PlainRunStart(plainMiddle)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("plain line, ends of it", () => { plain.StartOfLine(plainMiddle); plain.EndOfLine(plainMiddle); }))); MeasureOtherScript("ASCII word", "a", size); MeasureOtherScript("Cyrillic word", ((char)0x436).ToString(), size); MeasureOtherScript("Arabic word with marks", ((char)0x628).ToString() + (char)0x64E, size); } // Loading a word of one unit repeated costs a scan of each page, and the run of it is crossed by pages. private static void MeasureOtherScript(string name, string unit, int size) { var chunk = new System.Text.StringBuilder().Insert(0, unit, ChunkSize / unit.Length).ToString(); var watch = Stopwatch.StartNew(); PagedText word = Loaded(chunk, size); watch.Stop(); Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " {0}: loaded in {1:F2} s", name, watch.Elapsed.TotalSeconds)); int middle = word.Length / 2; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure(name + ", run forward", () => word.PlainRunEnd(middle)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure(name + ", insert", () => word.Replace(middle, 0, unit)))); } public static int Run(string[] args) { var sizes = new List(DefaultSizes); if (args.Length > 2) { Console.Error.WriteLine(Usage); return 1; } 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"; Console.WriteLine($"PagedText, {bitness}, {PagedText.DefaultPageSize}-char pages."); string chunk = TypingBenchmark.TextOfAbout(ChunkSize); foreach (int size in sizes) { Console.WriteLine($"--- {size:N0} chars"); var paged = new PagedText(); GC.Collect(); long memoryBefore = GC.GetTotalMemory(forceFullCollection: true); var watch = Stopwatch.StartNew(); while (paged.Length < size) { paged.Replace(paged.Length, 0, chunk); } watch.Stop(); long memory = GC.GetTotalMemory(forceFullCollection: true) - memoryBefore; Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " loaded {0:N0} chars in {1:F2} s, {2:N0} pages, {3:F1} MB held ({4:F2} bytes a char), " + "{5:N0} lines", paged.Length, watch.Elapsed.TotalSeconds, paged.PageCount, memory / 1048576.0, (double)memory / paged.Length, paged.HardLineCount)); int middle = paged.Length / 2; char[] scratch = new char[ChunkSize]; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("insert, middle", () => paged.Replace(middle, 0, "x")))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("insert, start", () => paged.Replace(0, 0, "x")))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("insert, end", () => paged.Replace(paged.Length, 0, "x")))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("insert + delete", () => { paged.Replace(middle, 0, "x"); paged.Replace(middle, 1, ""); }))); string moreThanAPage = chunk.Substring(0, PagedText.DefaultPageSize + PagedText.DefaultPageSize / 2); foreach (var where in new[] { ("start", 0), ("middle", middle), ("end", paged.Length) }) { int at = where.Item2; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure( "page count changes, " + where.Item1, () => { paged.Replace(at, 0, moreThanAPage); paged.Replace(at, moreThanAPage.Length, ""); }))); } Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("line of the middle", () => paged.LineNumberOf(middle)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("start of a line", () => paged.StartOfLineNumber(paged.HardLineCount / 2)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("start+end of line", () => { paged.StartOfLine(middle); paged.EndOfLine(middle); }))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("slice 4096 chars", () => paged.Slice(middle, 4096, ref scratch)))); Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("paste 1M + delete", () => { paged.Replace(middle, 0, chunk); paged.Replace(middle, chunk.Length, ""); }))); var composition = new SplicedText(); string composed = "composing"; Console.WriteLine(TypingBenchmark.Format(TypingBenchmark.Measure("composition + line queries", () => { composition.Set(paged, middle, middle + 3, composed); composed = composed.Substring(1) + composed[0]; int line = composition.LineNumberOf(middle + 5); composition.StartOfLineNumber(Math.Min(composition.HardLineCount - 1, line + 1)); }))); int removed = paged.Length / 2; watch.Restart(); paged.Replace(paged.Length / 4, removed, ""); watch.Stop(); Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " deleting half the text once: {0:F1} ms, {1:N0} pages left", watch.Elapsed.TotalMilliseconds, paged.PageCount)); paged.Validate(); int takenStart = paged.Length / 4; const int Rounds = 9; var takeOutMilliseconds = new double[Rounds]; var putBackMilliseconds = new double[Rounds]; for (int round = 0; round < Rounds; ++round) { watch.Restart(); PagedText.PageRun run = paged.TakeOut(takenStart, paged.Length / 2); watch.Stop(); takeOutMilliseconds[round] = watch.Elapsed.TotalMilliseconds; watch.Restart(); paged.PutBack(takenStart, run); watch.Stop(); putBackMilliseconds[round] = watch.Elapsed.TotalMilliseconds; } Array.Sort(takeOutMilliseconds); Array.Sort(putBackMilliseconds); Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " taking out half the text, {0} times: {1:F1} ms (median {2:F1}), putting it back: {3:F1} ms " + "(median {4:F1})", Rounds, takeOutMilliseconds[0], takeOutMilliseconds[Rounds / 2], putBackMilliseconds[0], putBackMilliseconds[Rounds / 2])); paged.Validate(); MeasureOneLine(size); } return 0; } } }