Skip to content

Add a batch command: many operations per USB session (12 min -> 11 s for a preset build) #8

Description

@AC6y86

Every tonepush invocation opens a fresh USB session, and on an HX Effects that
costs ~9 s — which is the entire runtime of any real work. Building one
preset here is ~100 commands: ~12 minutes, essentially all of it connecting.

I implemented a batch command locally and it takes the same 100 commands to
11.5 s. Filing in case you want it upstream; I'm glad to send a PR.

Found on v0.6.0 (ec7b02d), HX Effects, firmware 3.80.

Why it is 9 s

Not the operation — the connect. Session::bring_up drains the endpoint
(3 consecutive 150 ms silent reads minimum), handshakes, opens the services, then
does a preset_info() liveness probe because, as the comment in bring_up says,
the device ignores a reconnecting client on roughly every other attempt. When
that probe times out, DeviceHandle::open runs the whole thing a second time.
So a good invocation is ~9 s and an unlucky one closer to 17 s.

Measurement

Same 97-command preset build, same pedal, back to back:

separate invocations ~12 min
tonepush batch 11.5 s (0.1 s/command)

How I implemented it

The dispatch was already shaped for this — on_device opened a session and ran
one match cmd against &mut Session. The change is mostly a split:

  1. on_deviceopen_session() + dispatch(&mut Session, Cmd). The big
    match moves into dispatch verbatim; on_device becomes open-then-dispatch.
    (Two arms referenced the outer session binding rather than the s alias and
    needed renaming; nothing else changed.)

  2. Cmd::Batch { file: Option<PathBuf> }, routed in main() alongside the
    other commands that manage their own connection. Omit the path to read stdin.

  3. run_batch reads the file, parses every line into a Cmd via
    Cli::try_parse_from(once("tonepush").chain(words)), then opens one
    session and calls dispatch for each.

Design decisions worth flagging, since they are the parts I would expect you to
have opinions about:

  • Parse everything before touching USB. A typo on line 40 should not be
    discovered with the pedal already half-rewritten.
  • A failing line is reported and the batch continues, then the process exits
    non-zero with a count. A preset build would rather place eight of nine blocks
    and name the one it missed than stop halfway in an unknown state. Easy to make
    this configurable if you would rather it abort.
  • batch nested inside a batch is refused rather than opening a second
    session against a claimed interface.
  • Small hand-rolled word splitter so quoted values survive ("1200 Hz",
    "Scream 808"), avoiding a new dependency.

sleep <ms>, and why it is not optional

Some device operations commit asynchronously, and at batch speed the next
command beats them. Snapshot switching is the one that bit me: the pedal stores
a snapshot's block states as you leave it, so with commands 0.1 s apart every
snapshot ended up holding the previous one's states — silently, with no error.
The presets looked right in chain and were wrong on the pedal.

So sleep <ms> is a batch directive (handled before clap, as a Step::Pause
rather than a Cmd). My preset builder pauses 600 ms either side of every
snapshot switch.

If you would rather this were implicit, the honest version is a per-command
settle time inside dispatch for the operations that need it — I left it
explicit because I only characterised snapshot switching, and I would rather not
guess at which others are affected.

Sketch

enum Step { Run(Box<Cmd>), Pause(Duration) }

fn run_batch(source: Option<PathBuf>) -> Result<()> {
    let program = parse_all(read(source)?)?;   // before any USB
    let mut session = open_session()?;         // once
    for (n, line, step) in program {
        match step {
            Step::Pause(d) => std::thread::sleep(d),
            Step::Run(cmd) => if let Err(e) = dispatch(&mut session, *cmd) {
                failed += 1;
                eprintln!("line {n}: {line}\n    error: {e:#}");
            },
        }
    }
    ...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions