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