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