Clave.PerformanceTests times how long Clave takes to build a ChordSet. Run Bin\Release\Clave.PerformanceTests.exe for the 32-bit build the games ship on, or Clave.PerformanceTests64.exe, which is built from the x64 folder, for the same code as 64-bit. Arguments: --quick for fewer rounds, and text to run only the rows with it in their name. It refuses to run under a debugger or from a Debug build. Every count of chords has a control that runs beside the rows, which is matching the chords against a snapshot and so doesn't change with ChordSet. Read the ratio over it and not the microseconds. Each time is the fastest of the rounds. The chords are built before the timing starts, so a row is the cost of Add and AddAll alone. The chords are shortcuts: a press of each key with every combination of Ctrl, Shift and Alt, so plain Z is hidden by Ctrl+Z and that by Ctrl+Shift+Z. The rows add them one at a time to a set that reads the physical keys, the layout keys, or both alternately at two priorities, and join four sets owned by different controls the way a focus chain is. Ratios over the control (2026-10-31, 32-bit and then 64-bit in brackets, at 128 and then 512 chords): Add, physical: 4.3 and 4.2 (7.8 and 8.4). The ratio stays flat as the count grows, so adding is linear. Add, layout: 10.7 and 12.0 (19.2 and 19.9). Folding each layout chord to the physical keys costs more per chord. Add, both spaces, two priorities: 9.7 and 13.5 (16.0 and 23.5). AddAll, chain, own keys: 0.2 and 0.2 (0.3 and 0.3). Sets that share no key are merged without ranking anything. AddAll, chain, own keys, layout: 0.2 and 0.2 (0.3 and 0.3). AddAll, chain, shared keys: 1.1 and 4.6 (2.0 and 10.6). This is the one row that still grows with the count, since Z and Ctrl+Z have to be compared with each other. The 64-bit control runs two to three times faster than the 32-bit one, so read each build against its own control. A second group of rows (ArbiterOperations) times what an arbiter does once it is built, with the same control. A row that handles four chords but grows with the arbiter's size is scanning all of it. The rows are CountOn, mounting and unmounting a set of four, Add and Remove, Add then Remove of four one by one or with RemoveAll, SetPriority of one chord or of a mount of four, asking about every chord with ShouldFire, the analysis each chord costs alone in an arbiter of one, and constructing chords with Press, PressOrRepeat, a chained WithAnyHeld and HoldAny of every typing key. The chord rows repeat `count` times, except HoldAny, which repeats a sixteenth as often. Results (2026-11-01, 32-bit, --quick), per chord at 512 chords unless said: CountOn on a mount of 4: 2 us in all, so 4 ns per chord of the arbiter. Not worth fixing on its own. Mount and unmount a set of 4: 6 us at 16 chords to 82 us at 512. Grows with the arbiter, but is small. Add and Remove one chord: 9 us to 27 us. Add 4 then Remove 4 one by one or with RemoveAll: both 128 us at 512. Analysing a chord alone: 9.5 us. An Add into the 512-chord arbiter costs 14.7 us, so about two thirds is analysis. Chord.Press: 4.6 us. PressOrRepeat: 13 us. Press.WithAnyHeld twice: 35 us. HoldAny(Typing): about 1 ms. The control scales with the count too, so the ratios over it stay flat for the chord rows: construction is linear. After KeyFormula.Any/All/None build their diagrams directly and KeySpaceSnapshot.IsConsistent went bitwise (2026-11-01): HoldAny(Typing): about 1 ms to 25 us. Chord.Press: 4.6 us to 2.3 us. PressOrRepeat: 13 us to 7 us. Press.WithAnyHeld twice: 35 us to 17 us. Then PressOrRepeat, PressOrRepeatAny, WithAnyHeld and WithAllHeld build one chord from formulas instead of one per step: PressOrRepeat 7 us to 5 us, Press.WithAnyHeld twice 17 us to 14 us. What remains is each diagram operation's fixed cost, not the number of chords. After BinaryDecisionDiagram numbers nodes with arrays and collects its variables with a sort instead of a Dictionary, Stack, List and SortedSet (2026-11-01, ratios over the control, since the machine's speed drifted between runs): Add, physical: 4.2x to 2.3x. Add, layout: 9.8x to 7.3x. Add, both spaces: 14.1x to 10.8x. Memory down by a third. Chord.Press, PressOrRepeat and Press.WithAnyHeld: about 10 to 15% faster, and a third less memory. After an entry stops redoing what its Chord worked out when it was built, the satisfiability and witness of a chord that reads no layout key (2026-11-01): "Analyse each chord, arbiter of one" 1.4x to 1.0x, Add, physical 2.3x to 2.0x. The "Analyse each chord, layout" row is the other half: a layout chord costs about three times as much, and nearly all of that is folding its formula to the physical keys. After the fold to the physical keys is one BinaryDecisionDiagram.WithVariablesMapped in a single builder, where it was four whole-diagram operations per node (2026-11-01): Add, layout 6.1x to 2.8x, with memory down by two thirds. A layout chord now costs about twice a physical one, 1.9x against 1.0x on "Analyse each chord". Add, both spaces is only 10.9x to 9.5x, since its cost is in ranking chords of different spaces against each other. Chords that pin no key (PressAny of several keys) are ranked against every other chord, so "Add chords that pin nothing" grows with the count squared. At 512 chords it was 417 ms. About 3% of the 261,000 ordered pairs pass the witness test and then cost a diagram operation or two. Checking for a counterexample before building the consistency diagram made it 100 ms, and walking the two diagrams together with BinaryDecisionDiagram.TryFindCounterexample, which builds nothing, made it 32 ms and 3.5 MB (2026-11-01). Then three more cuts, all in ChordArbiter: Two chords that read no key in common can't imply each other, unless one matches every consistent snapshot. A pair like that is ruled out with one mask, without a witness. The random-group row went from 32 ms to 11.6 ms. Chords whose diagrams are equal are equivalent, with no implication to work out. That mattered for the row of overlapping groups ("Add overlapping chords that pin nothing", 8 of 40 keys, which is closer to a text box). A chord that pins nothing keeps four witnesses that press different keys, so a partly overlapping pair is ruled out more often. Every pair of satisfiable chords is now compared through the fold, so they all use them. The overlapping row went from 101 ms to 50 ms at 512 chords. Eight witnesses were no better than four. A chord that pins nothing is ranked only against the entries listed under the keys it reads, plus those that match everything, instead of against every entry. The random-group row went from 11.6 ms to 9.4 ms. The overlapping row stayed at 49 ms, since those chords share a key with four fifths of the others. A fold that doesn't match an idle keyboard can't match a witness that touches none of the keys it reads, so most witness tests became one mask test. The overlapping row went from 54 ms to 32 ms. The extra witnesses come from a walk of the diagram that prefers the variables earlier witnesses left false, where each had been a diagram operation. Analysing an overlapping chord went from 28 us to 9 us, and the row from 32 ms to 21 ms, with memory down from 12.5 MB to 2.7 MB. After that (2026-11-01, 512 chords): random groups 4.8 ms, flat at about 2.5x control at every count, so linear. Overlapping groups 18.8 ms, 10.9x control, which still grows with the square of the count. A chord that pins nothing now finds its candidates with bitsets of entry ids (IdSet). Implying needs the implied chord to match an idle keyboard or to read a key that each witness of the implying chord touches, so the candidates are an AND over the witnesses and take a few words each. KeyState.ToKeyCodes replaced a loop that asked about all 128 keys. Random groups 3.2 ms (2.0x control at every count), overlapping groups 6.7 ms (2.3x control at 16 chords, 4.5x at 512), which is 1.1 ms at 128. Add, physical went from 2.0x to 1.2x control and Add, both spaces from 9.5x to 6.3x, from the cheaper key lists. KeyFormula.Key and the Any/All/None chains check their enum arguments against tables built once, where each had asked Enum.IsDefined, which is reflection (2026-11-02, 512 chords): Chord.Press 1.15 us per chord, 0.4x control, from about 2 us. Removing chords (2026-11-02, 32-bit): "Fill" adds n shortcuts to a new arbiter, and the Remove rows add the same teardown, so a row minus Fill is the removal. Removing one at a time costs 15 us at 16 chords, 105 us at 64, 331 us at 128, 1.3 ms at 256 and 4.9 ms at 512 (backwards, 6.6 ms), so the cost per chord doubles each time the count does and is quadratic. RemoveAll of the same chords is 0.28 ms at 512, flat at 1.2x control. Each entry now keeps the list of entries it hides, so removing one unlinks just those instead of scrubbing every entry (2026-11-02, 512 chords): removing in order 4.9 ms to 0.4 ms, backwards 6.6 ms to 1.6 ms. RemoveAll filters each surviving neighbour once, 0.28 ms to 0.24 ms. What remained backwards was List.Remove searching the per-key and edge index lists. Those two are now the IdSets of entry ids (the per-key one already existed), which remove in O(1): removing one at a time is 0.3 ms at 512 chords in either order, the same as RemoveAll, and the Add rows did not move. Each mount keeps its own set of entries, where CountOn, ChordsOn, Unmount and ConflictsOf had filtered every entry of the arbiter by mount (2026-11-03, 512 chords): CountOn 1.6 us to under 0.05 us, and mounting then unmounting a set of 4 from 24 us to 16 us. That row still grows with the arbiter, since the four chords are ranked against what is there. Asking an arbiter about each of its chords with ShouldFire, as a control does on a key event (2026-11-03, 512 chords): 54 us with one key pressed and 53 us idle, so about 100 ns a call and nothing allocated. The cost is linear in the chords asked about and independent of how many others the arbiter holds. An entry for a chord that reads no layout key finds its pinned keys and its need for an edge in one pass over the diagram, where each was a pass that allocated an array the size of the diagram (2026-11-04, 512 chords): each pass cost 0.25 us and 250 bytes. "Analyse each chord, arbiter of one" 0.8x to 0.7x control and 1481 KB to 1355 KB, and "Add, physical" 1.0x to 0.9x control and 1337 KB to 1237 KB. SetPriority takes a chord out and puts it back in with the analysis it already has, so it ranks the chord against the others again without redoing the diagrams (2026-11-05, 512 chords): one chord 0.7 us, against 2.4 us for an Add and a Remove, and a mount of four 21 us, which grows with the arbiter as mounting a set of four does. A disabled mount is a check in ShouldFire beside each chord's own enabled state. The "ShouldFire each chord" rows stayed at 2.6% of the control with it and 2.7% to 2.8% without. KeyState walks its set keys with a struct enumerator, and ToKeyCodes fills one array where it had built a list and then an array (2026-11-06, 512 chords): "Add, physical" 1237 KB to 1148 KB and "Analyse each chord, arbiter of one" 1355 KB to 1288 KB. The times stayed at 0.9x and 0.7x to 0.8x control, and "Add overlapping chords that pin nothing", which now collects the keys of its consistency check with the enumerator, read 4.2x to 4.3x control against 4.5x before. What the rows leave out: matching a snapshot against a set, which Chord.Matches times in its own right.