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:
-
on_device → open_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.)
-
Cmd::Batch { file: Option<PathBuf> }, routed in main() alongside the
other commands that manage their own connection. Omit the path to read stdin.
-
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:#}");
},
}
}
...
}
Every
tonepushinvocation opens a fresh USB session, and on an HX Effects thatcosts ~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
batchcommand locally and it takes the same 100 commands to11.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_updrains the endpoint(3 consecutive 150 ms silent reads minimum), handshakes, opens the services, then
does a
preset_info()liveness probe because, as the comment inbring_upsays,the device ignores a reconnecting client on roughly every other attempt. When
that probe times out,
DeviceHandle::openruns 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:
tonepush batchHow I implemented it
The dispatch was already shaped for this —
on_deviceopened a session and ranone
match cmdagainst&mut Session. The change is mostly a split:on_device→open_session()+dispatch(&mut Session, Cmd). The bigmatch moves into
dispatchverbatim;on_devicebecomes open-then-dispatch.(Two arms referenced the outer
sessionbinding rather than thesalias andneeded renaming; nothing else changed.)
Cmd::Batch { file: Option<PathBuf> }, routed inmain()alongside theother commands that manage their own connection. Omit the path to read stdin.
run_batchreads the file, parses every line into aCmdviaCli::try_parse_from(once("tonepush").chain(words)), then opens onesession and calls
dispatchfor each.Design decisions worth flagging, since they are the parts I would expect you to
have opinions about:
discovered with the pedal already half-rewritten.
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.
batchnested inside a batch is refused rather than opening a secondsession against a claimed interface.
"1200 Hz","Scream 808"), avoiding a new dependency.sleep <ms>, and why it is not optionalSome 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
chainand were wrong on the pedal.So
sleep <ms>is a batch directive (handled before clap, as aStep::Pauserather than a
Cmd). My preset builder pauses 600 ms either side of everysnapshot switch.
If you would rather this were implicit, the honest version is a per-command
settle time inside
dispatchfor the operations that need it — I left itexplicit because I only characterised snapshot switching, and I would rather not
guess at which others are affected.
Sketch