Performance Tips
Guidelines for drawing out performance in situations that demand throughput, such as updating the focal point at high frequency.
Composite Commands and Low-Level Commands
Section titled “Composite Commands and Low-Level Commands”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?;}builder = client.datagram_builder()builder.push(SetSilencer.disable())builder.push( WritePatternBuffer( bank=PatternBank.B0, index=0, emissions=patterns, ))builder.push( ConfigPattern( bank=PatternBank.B0, config=SamplingConfig.FREQ_40K, size=1, loop_behavior=LoopBehavior.Infinite, ))for frame in builder.build(): await client.send_checked(frame)var builder = client.DatagramBuilder();builder.Push(SetSilencer.Disable());builder.Push(new WritePatternBuffer( bank: PatternBank.B0, index: 0, emissions: patterns));builder.Push(new ConfigPattern( bank: PatternBank.B0, config: SamplingConfig.Freq40k, size: 1, loopBehavior: LoopBehavior.Infinite));foreach (var frame in builder.Build()){ await client.SendCheckedAsync(frame);}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()?;}pending = collections.deque()for i in range(NUM_POINTS): theta = 2.0 * math.pi * i / NUM_POINTS target = center + np.array([RADIUS_MM * math.cos(theta), RADIUS_MM * math.sin(theta), 0.0]) pattern.focus( geometry, target, wavelength, pattern.FocusOption(), patterns, ) builder = client.datagram_builder() builder.push( WritePatternBuffer( bank=PatternBank.B0, index=0, emissions=patterns, ) ) for frame in builder.build(): if len(pending) >= MAX_INFLIGHT: (await pending.popleft()).check() pending.append(await client.send(frame))while pending: (await pending.popleft()).check()var pending = new Queue<ResponseToken>();for (var i = 0; i < NumPoints; i++){ var theta = 2.0f * MathF.PI * i / NumPoints; var target = center + new Vector3(RadiusMm * MathF.Cos(theta), RadiusMm * MathF.Sin(theta), 0.0f); Pattern.Focus( geometry, target, wavelength, new FocusOption(), patterns );
var hotBuilder = client.DatagramBuilder(); hotBuilder.Push(new WritePatternBuffer( bank: PatternBank.B0, index: 0, emissions: patterns )); foreach (var frame in hotBuilder.Build()) { if (pending.Count >= Client.MaxInflight) { (await pending.Dequeue()).Check(); } pending.Enqueue(await client.SendAsync(frame)); }}while (pending.Count > 0){ (await pending.Dequeue()).Check();}Streaming Transmission
Section titled “Streaming Transmission”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.
Buffer Reuse
Section titled “Buffer Reuse”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,
Framescan also be written into the same buffer withbuild_into, avoiding allocation on everybuild.build_intois not provided in Python / C#.
OS Tuning
Section titled “OS Tuning”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.
Windows Driver Settings
Section titled “Windows Driver Settings”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
Measurement Tools
Section titled “Measurement Tools”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.