Skip to content

Performance Tips

Guidelines for drawing out performance in situations that demand throughput, such as updating the focal point at high frequency.

Pattern is a composite command that performs the buffer write, the playback configuration and the bank switch together, but these are fused into a single frame. It therefore costs the same number of frames as one low-level command, so for a plain focal point update you may send Pattern itself at high frequency.

However, once the bank is set, all that is needed for a per-frame focal point update is rewriting the buffer, that is, only WritePatternBuffer. Sending only WritePatternBuffer skips the per-frame playback configuration and bank switch, making the per-frame work on the device lighter.

Set the bank once at the beginning. (You may use Pattern for the first time.)

let mut builder = client.datagram_builder();
builder
.push(SetSilencer::disable())
.push(WritePatternBuffer {
bank: PatternBank::B0,
index: 0,
emissions: &patterns,
})
.push(ConfigPattern {
bank: PatternBank::B0,
config: SamplingConfig::FREQ_40K,
size: 1,
loop_behavior: LoopBehavior::Infinite,
});
for frame in &builder.build()? {
client.send_checked(frame).await?;
}

In the subsequent hot loop, only WritePatternBuffer is sent.

let mut buf = Frames::default();
let mut pending: VecDeque<ResponseFuture> = VecDeque::with_capacity(MAX_INFLIGHT);
for i in 0..NUM_POINTS {
let theta = 2.0 * PI * i as f32 / NUM_POINTS as f32;
let target = center
+ offset(
RADIUS_MM * theta.cos() * mm,
RADIUS_MM * theta.sin() * mm,
0.0 * mm,
);
autd3_rs_pattern::focus(
&geometry,
target,
wavelength,
&autd3_rs_pattern::FocusOption::default(),
&mut patterns,
);
let mut builder = client.datagram_builder();
builder.push(WritePatternBuffer {
bank: PatternBank::B0,
index: 0,
emissions: &patterns,
});
builder.build_into(&mut buf)?;
for frame in &buf {
if pending.len() >= MAX_INFLIGHT {
pending.pop_front().expect("non-empty").await?.check()?;
}
pending.push_back(client.send(frame).await?);
}
}
while let Some(fut) = pending.pop_front() {
fut.await?.check()?;
}

The hot loop above uses streaming, which sends one after another without waiting for a response. send queues frames, and once MAX_INFLIGHT is exceeded, the oldest responses are collected. Since the round-trip latency is hidden and throughput improves further, choose streaming over stop-and-wait for high-frequency updates.

To avoid per-frame allocation, reuse buffers.

  • The buffer obtained via pattern_buffer() can be overwritten every frame as the output destination of the Pattern compute function.
  • In Rust, Frames can also be written into the same buffer with build_into, avoiding allocation on every build. build_into is not provided in Python / C#.

The real-time performance of EtherCAT also depends on the OS settings. By adjusting thread priority, CPU affinity, timer precision, and so on, jitter can be suppressed, potentially enabling more stable high-rate transmission.

These can be set as rt_priority / rt_policy / rt_affinity of ClientConfig, and sync0_period / sync0_shift of the Link Option. The appropriate values differ by environment, so it is best to measure and decide.

The repository provides tools for measuring and tuning performance.

  • cargo xtask tool perftest: measures throughput and latency.
  • cargo xtask tool synctune: measures synchronization stability and searches for the optimal parameters.

See each tool’s --help for details.