Skip to content

  • Projects
  • Groups
  • Snippets
  • Help
  • This project
    • Loading...
  • Sign in / Register
T
Tidal
  • Overview
    • Overview
    • Details
    • Activity
    • Cycle Analytics
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
    • Charts
  • Issues 0
    • Issues 0
    • List
    • Board
    • Labels
    • Milestones
  • Merge Requests 0
    • Merge Requests 0
  • CI / CD
    • CI / CD
    • Pipelines
    • Jobs
    • Schedules
    • Charts
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Members
    • Members
  • Collapse sidebar
  • Activity
  • Graph
  • Charts
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
  • PLN
  • Tidal
  • Repository

Switch branch/tag
  • Tidal
  • tools
  • at
  • tests
  • test_lcxl_latency.py
Find file
BlameHistoryPermalink
  • PLN (Algolia)'s avatar
    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) authored Jul 28, 2026
    128de62d
test_lcxl_latency.py 8.17 KB
EditWeb IDE
×

Replace test_lcxl_latency.py

Attach a file by drag & drop or click to upload


Cancel
A new branch will be created in your fork and a new merge request will be started.