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