using System;
using System.Collections.Generic;
using System.Globalization;
namespace Clave.PerformanceTests
{
///
/// One way of filling a ChordArbiter, timed from chords that are already built.
///
public sealed class BuildScenario
{
public readonly string Name;
public readonly Action Build;
public BuildScenario(string name, Action build)
{
Name = name;
Build = build;
}
}
public static class ChordSetBuilding
{
public static readonly int[] ChordCounts = { 16, 32, 64, 128, 256, 512 };
public const int FocusChainLength = 4;
private static readonly KeyState Modifiers = KeyState.Shift | KeyState.Control | KeyState.Alt;
public static List TypingKeys()
{
var returnMe = new List();
foreach (KeyCode key in Enum.GetValues(typeof(KeyCode)))
{
if (key != KeyCode.None && !Modifiers.IsKeySet(key))
{
returnMe.Add(key);
}
}
return returnMe;
}
///
/// Press chords of every key with each combination of Ctrl, Shift and Alt, as an application's shortcuts are.
/// Plain Z is hidden by Ctrl+Z, and Ctrl+Z by Ctrl+Shift+Z.
///
public static List Shortcuts(int count, KeySpace space, int firstKey = 0)
{
List keys = TypingKeys();
var returnMe = new List(count);
for (int i = 0; returnMe.Count < count; ++i)
{
KeyCode key = keys[(firstKey + i / 8) % keys.Count];
Chord chord = Chord.Press(key, space);
if ((i & 1) != 0)
{
chord = chord.WithAnyHeld(KeyState.Control, space);
}
if ((i & 2) != 0)
{
chord = chord.WithAnyHeld(KeyState.Shift, space);
}
if ((i & 4) != 0)
{
chord = chord.WithAnyHeld(KeyState.Alt, space);
}
returnMe.Add(chord);
}
return returnMe;
}
// Shortcuts uses eight chords of a key, so this is how many keys a set of a chain takes.
private static int KeysPerChainSet(int count)
{
return Math.Max(1, count / FocusChainLength / 8);
}
private static ChordSet SetOf(List chords)
{
var builder = new ChordSetBuilder();
foreach (Chord chord in chords)
{
builder.Add(chord);
}
return builder.Build();
}
private static BuildScenario AddingOneByOne(string name, List chords, bool alternatePriorities)
{
return new BuildScenario(name, () =>
{
var set = new ChordArbiter();
ChordMount mount = set.Mount();
for (int i = 0; i < chords.Count; ++i)
{
set.Add(chords[i], mount, priority: alternatePriorities ? i & 1 : 0);
}
});
}
// A focus chain joins the sets of the focused control and its ancestors, each on its own mount. A set
// starts `keyStride` keys after the last, so a small stride has the sets share most of their keys.
private static BuildScenario AddingAFocusChain(string name, int count, KeySpace space, int keyStride)
{
var sets = new ChordSet[FocusChainLength];
for (int i = 0; i < sets.Length; ++i)
{
sets[i] = SetOf(Shortcuts(count / FocusChainLength, space, firstKey: i * keyStride));
}
return new BuildScenario(name, () =>
{
var arbiter = new ChordArbiter();
for (int i = 0; i < sets.Length; ++i)
{
arbiter.Mount(sets[i], priority: i);
}
});
}
///
/// The scenarios for a number of chords. They differ in which keyboard spaces the chords read.
///
public static List Scenarios(int count)
{
List physical = Shortcuts(count, KeySpace.Physical);
List layout = Shortcuts(count, KeySpace.Layout);
// Alternating spaces makes every other pair read two spaces, which can only be ranked by the fold.
var both = new List(count);
for (int i = 0; i < count; ++i)
{
both.Add(((i & 1) == 0 ? physical : layout)[i]);
}
return new List
{
AddingOneByOne("Add, physical", physical, alternatePriorities: false),
AddingOneByOne("Add, layout", layout, alternatePriorities: false),
AddingOneByOne("Add, both spaces, two priorities", both, alternatePriorities: true),
AddingAFocusChain("Mount, chain, own keys", count, KeySpace.Physical,
keyStride: KeysPerChainSet(count)),
AddingAFocusChain("Mount, chain, shared keys", count, KeySpace.Physical, keyStride: 3),
AddingAFocusChain("Mount, chain, own keys, layout", count, KeySpace.Layout,
keyStride: KeysPerChainSet(count)),
};
}
///
/// Matching chords against a snapshot, which no change to ChordArbiter touches.
///
public static Action Control(int count)
{
List chords = Shortcuts(count, KeySpace.Physical);
KeyboardSnapshot idle = default;
return () =>
{
int matched = 0;
for (int pass = 0; pass < 64; ++pass)
{
for (int i = 0; i < chords.Count; ++i)
{
if (chords[i].Matches(idle))
{
++matched;
}
}
}
GC.KeepAlive(matched);
};
}
private static string Format(double nanoseconds)
{
if (nanoseconds >= 1e6)
{
return (nanoseconds / 1e6).ToString("0.00", CultureInfo.InvariantCulture) + " ms";
}
return (nanoseconds / 1e3).ToString("0.0", CultureInfo.InvariantCulture) + " us";
}
public static void Run(string filter, int rounds)
{
Run("ChordArbiter building", Scenarios, filter, rounds);
}
///
/// Times the scenarios that makes for each count of chords, each beside the
/// control.
///
public static void Run(string title, Func> scenariosFor, string filter, int rounds)
{
Console.WriteLine(title + ". Times are the fastest of the rounds, and the ratio is over " +
"the control, which doesn't change.");
Console.WriteLine();
foreach (int count in ChordCounts)
{
var scenarios = new List();
foreach (BuildScenario scenario in scenariosFor(count))
{
if (scenario.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0)
{
scenarios.Add(scenario);
}
}
if (scenarios.Count == 0)
{
continue;
}
var calls = new List { Control(count) };
foreach (BuildScenario scenario in scenarios)
{
calls.Add(scenario.Build);
}
Timing[] timings = Measurement.TimeTakingTurns(calls, rounds);
double control = timings[0].MinimumNanoseconds;
Console.WriteLine($"{count} chords. Control {Format(control)}.");
for (int i = 0; i < scenarios.Count; ++i)
{
Timing timing = timings[i + 1];
string ratio = (timing.MinimumNanoseconds / control).ToString("0.0", CultureInfo.InvariantCulture);
string kilobytes = (timing.BytesPerCall / 1024).ToString("0", CultureInfo.InvariantCulture);
Console.WriteLine($" {scenarios[i].Name,-36} {Format(timing.MinimumNanoseconds),10} " +
$"{ratio,7}x control {kilobytes,6} KB");
}
Console.WriteLine();
}
}
}
}