-
perf(lcxl): the LEDs lagged because we deduped on VALUE, and colour is a step function · 128de62d
PLN, watching the board for the first time on real hardware: "its slow to track if i move all fders quickly i see the animations lagging a sec or two behind". Not a slow device. Three compounding faults, each of which alone would have been survivable: 1. THE BUG. The read loop repainted whenever a CC *value* changed. But colour is a STEP function of value -- six steps in value_ramp, seven in filter_colour. Sweeping one knob 0 -> 127 emits ~128 events and can change the board at most 6 times. We were asking the wire for ~20x the work that could possibly be visible. 2. Every send re-resolved the port from scratch: `aconnect -o` AND `amidi -l` AND then `aseqsend`. Three forks per LED, ~1.7 ms each, measured. 3. All of it ran INSIDE `for line in proc.stdout`, so a write in flight stopped us reading the next MIDI event. aseqdump's pipe QUEUES rather than drops, so nothing was lost -- everything just arrived later, and later, without bound. That unboundedness is why it read as "a second or two" rather than a constant delay. The fix, in payoff order. A new `Painter` thread owns the wire; the reader only touches the model and hands it colours, never blocking. The painter dedupes on the COLOUR the board will show, coalesces so only the last colour per index within a frame reaches the wire, and batches every dirty index into ONE SysEx (the Launchpad dialect takes (index, colour) pairs, so a whole-surface repaint is a single write). Port resolution is cached for 2 s and thrown away the instant a write fails -- evidence, not a timer -- which keeps the replug-recovery property that made it uncached in the first place. Rate limiting is a floor on the GAP between writes, not a fixed tick, so an isolated button press still goes out immediately. Measured, not claimed. `--bench` replays PLN's own complaint (8 knobs + 8 faders swept together, 500 events/s) against a transport modelled at its real cost, and runs the legacy path beside the new one: legacy 744 wire msgs wall 11.23s overrun +8.23s coalesced 48 wire msgs wall 3.00s overrun +0.00s 15.5x fewer messages; latency p50 7.9 ms, p99 20.2 ms Overrun IS the visible lag -- it is how far behind his hands the board finishes. On the real device the per-write cost also fell 7.70 ms -> 2.72 ms with ports cached, so the total wire work is down roughly 44x. Twelve regression tests, no hardware needed. Speed regresses silently -- nothing goes red, it just gets slow again -- so the assertions are numeric: wire rate bounded by the frame rate and not the input rate, reader never falls behind, p99 under 50 ms, a lone press not delayed by the frame boundary. The last one is the one that matters: fast and wrong beats nothing, so we decode every SysEx the coalescer emitted through the mock surface and assert the board that LANDS is exactly what a full `build_frame` recomputation would have produced. Also: verbose logging now fires only when the board actually changes. Printing 400 lines a second of "CC77 = 63" was itself I/O in the hot loop, and told nobody anything.PLN (Algolia) authored128de62d
×