Skip to content
This is the development version of the documentation. It may change before the next release. See 0.6.x for the latest release.

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.

On Windows, update the NIC driver to the latest version and then configure it as follows.

In Device Manager, open the adapter properties → Advanced

  • Interrupt Moderation → Disabled
  • Interrupt Throttle Rate → Disabled
  • DMA Coalescing → Disabled
  • Energy Efficient Ethernet → Disabled
  • Flow Control → Disabled

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.