Skip to main content

bench_transfer/
bench_transfer.rs

1use std::{
2    env,
3    error::Error,
4    process,
5    time::{Duration, Instant},
6};
7use vlfd_rs::{Board, IoConfig, TransferStageProfile, TransportConfig};
8
9const WORDS_PER_CYCLE: usize = 4;
10
11fn main() {
12    if let Err(err) = real_main() {
13        eprintln!("error: {err}");
14        process::exit(1);
15    }
16}
17
18fn real_main() -> Result<(), Box<dyn Error>> {
19    let options = Options::parse(env::args().skip(1))?;
20    match options.mode {
21        RunMode::Cpu => run_cpu_bench(&options),
22        RunMode::Device => run_device_bench(&options),
23    }
24}
25
26#[derive(Debug, Clone, Copy)]
27enum RunMode {
28    Cpu,
29    Device,
30}
31
32#[derive(Debug, Clone, Copy)]
33struct Options {
34    mode: RunMode,
35    iterations: usize,
36    words: usize,
37    window: usize,
38    profile_stages: bool,
39    clock_high_delay: u16,
40    clock_low_delay: u16,
41    transport: TransportConfig,
42}
43
44impl Default for Options {
45    fn default() -> Self {
46        Self {
47            mode: RunMode::Cpu,
48            iterations: 100_000,
49            words: 512,
50            window: 16,
51            profile_stages: false,
52            clock_high_delay: 11,
53            clock_low_delay: 11,
54            transport: TransportConfig::default(),
55        }
56    }
57}
58
59impl Options {
60    fn parse<I>(args: I) -> Result<Self, Box<dyn Error>>
61    where
62        I: IntoIterator<Item = String>,
63    {
64        let mut options = Self::default();
65        let mut args = args.into_iter();
66
67        let Some(mode) = args.next() else {
68            print_usage();
69            return Err("missing mode (cpu|device)".into());
70        };
71        options.mode = match mode.as_str() {
72            "cpu" => RunMode::Cpu,
73            "device" => RunMode::Device,
74            _ => {
75                print_usage();
76                return Err(format!("unknown mode `{mode}`").into());
77            }
78        };
79
80        while let Some(flag) = args.next() {
81            match flag.as_str() {
82                "--iterations" => {
83                    options.iterations = next_value(&mut args, "--iterations")?.parse()?
84                }
85                "--words" => options.words = next_value(&mut args, "--words")?.parse()?,
86                "--window" => options.window = next_value(&mut args, "--window")?.parse()?,
87                "--profile-stages" => options.profile_stages = true,
88                "--clock-high" => {
89                    options.clock_high_delay = next_value(&mut args, "--clock-high")?.parse()?
90                }
91                "--clock-low" => {
92                    options.clock_low_delay = next_value(&mut args, "--clock-low")?.parse()?
93                }
94                "--usb-timeout-ms" => {
95                    let value: u64 = next_value(&mut args, "--usb-timeout-ms")?.parse()?;
96                    options.transport.usb_timeout = Duration::from_millis(value);
97                }
98                "--sync-timeout-ms" => {
99                    let value: u64 = next_value(&mut args, "--sync-timeout-ms")?.parse()?;
100                    options.transport.sync_timeout = Duration::from_millis(value);
101                }
102                "--reset-on-open" => options.transport.reset_on_open = true,
103                "--no-clear-halt" => options.transport.clear_halt_on_open = false,
104                "--help" | "-h" => {
105                    print_usage();
106                    process::exit(0);
107                }
108                other => return Err(format!("unknown flag `{other}`").into()),
109            }
110        }
111
112        Ok(options)
113    }
114}
115
116fn next_value<I>(args: &mut I, flag: &str) -> Result<String, Box<dyn Error>>
117where
118    I: Iterator<Item = String>,
119{
120    args.next()
121        .ok_or_else(|| format!("missing value for `{flag}`").into())
122}
123
124fn print_usage() {
125    eprintln!(
126        "Usage:\n  cargo run --example bench_transfer -- cpu [--words N] [--iterations N]\n  cargo run --example bench_transfer -- device [--words N] [--iterations N] [--window N] [--profile-stages] [--clock-high N] [--clock-low N] [--usb-timeout-ms N] [--sync-timeout-ms N] [--reset-on-open] [--no-clear-halt]"
127    );
128}
129
130fn run_cpu_bench(options: &Options) -> Result<(), Box<dyn Error>> {
131    let template = vec![0x1234u16; options.words];
132    let mut scratch = Vec::with_capacity(options.words);
133    let key = [0x55aau16; 16];
134
135    let started = Instant::now();
136    for _ in 0..options.iterations {
137        scratch.clear();
138        scratch.extend_from_slice(&template);
139        xor_words(&mut scratch, &key);
140    }
141    let elapsed = started.elapsed();
142
143    print_summary("cpu", options.words, options.iterations, elapsed, None);
144    Ok(())
145}
146
147fn run_device_bench(options: &Options) -> Result<(), Box<dyn Error>> {
148    let mut board = Board::open_with_transport(options.transport)?;
149    let max_cycles_per_transfer = usize::from(board.config().fifo_size_words()) / WORDS_PER_CYCLE;
150    let mut io = board.configure_io(&IoConfig {
151        clock_high_delay: options.clock_high_delay,
152        clock_low_delay: options.clock_low_delay,
153        ..IoConfig::default()
154    })?;
155
156    if options.window == 0 {
157        return Err("window must be at least 1".into());
158    }
159
160    let template = vec![0x1234u16; options.words];
161    let mut rx = vec![0u16; options.words];
162    let mut outputs = vec![vec![0u16; options.words]; options.window];
163    let mut stage_profile = TransferStageProfile::default();
164
165    let started = Instant::now();
166    if options.window == 1 {
167        if options.profile_stages {
168            for _ in 0..options.iterations {
169                let profile = io.transfer_profiled_into(&template, &mut rx)?;
170                stage_profile.merge(&profile);
171            }
172        } else {
173            for _ in 0..options.iterations {
174                io.transfer(&template, &mut rx)?;
175            }
176        }
177    } else {
178        let mut window = io.transfer_window(options.words, options.window)?;
179        let initial = options.iterations.min(options.window);
180        for _ in 0..initial {
181            if options.profile_stages {
182                let profile = window.submit_profiled(&template)?;
183                stage_profile.merge(&profile);
184            } else {
185                window.submit(&template)?;
186            }
187        }
188
189        let mut submitted = initial;
190        let mut completed = 0usize;
191        while completed < options.iterations {
192            let output = outputs[completed % options.window].as_mut_slice();
193            if options.profile_stages {
194                let profile = window.receive_into_profiled(output)?;
195                stage_profile.merge(&profile);
196            } else {
197                window.receive_into(output)?;
198            }
199            completed += 1;
200
201            if submitted < options.iterations {
202                if options.profile_stages {
203                    let profile = window.submit_profiled(&template)?;
204                    stage_profile.merge(&profile);
205                } else {
206                    window.submit(&template)?;
207                }
208                submitted += 1;
209            }
210        }
211    }
212    let elapsed = started.elapsed();
213    io.finish()?;
214
215    print_summary(
216        "device",
217        options.words,
218        options.iterations,
219        elapsed,
220        Some(max_cycles_per_transfer),
221    );
222    if options.profile_stages {
223        print_stage_profile(&stage_profile, elapsed);
224    }
225    Ok(())
226}
227
228fn xor_words(buffer: &mut [u16], key: &[u16; 16]) {
229    let mut index = 0usize;
230    for word in buffer {
231        *word ^= key[index];
232        index = (index + 1) & 0x0f;
233    }
234}
235
236fn print_summary(
237    mode: &str,
238    words: usize,
239    iterations: usize,
240    elapsed: Duration,
241    max_cycles_per_transfer: Option<usize>,
242) {
243    let seconds = elapsed.as_secs_f64();
244    let transfers_per_sec = iterations as f64 / seconds.max(f64::MIN_POSITIVE);
245    let words_per_sec = (iterations * words) as f64 / seconds.max(f64::MIN_POSITIVE);
246    let cycles_per_transfer = words / WORDS_PER_CYCLE;
247    let cycles_per_sec = words_per_sec / WORDS_PER_CYCLE as f64;
248    let max_cycles_per_transfer = max_cycles_per_transfer
249        .map(|value| value.to_string())
250        .unwrap_or_else(|| "n/a".to_string());
251
252    println!(
253        "mode={mode} words={words} cycles_per_transfer={cycles_per_transfer} max_cycles_per_transfer={max_cycles_per_transfer} iterations={iterations} elapsed={elapsed:?} transfers_per_sec={transfers_per_sec:.3} words_per_sec={words_per_sec:.3} cycles_per_sec={cycles_per_sec:.3}"
254    );
255}
256
257fn print_stage_profile(profile: &TransferStageProfile, elapsed: Duration) {
258    let accounted = profile.total_duration();
259    let elapsed_secs = elapsed.as_secs_f64().max(f64::MIN_POSITIVE);
260    let accounted_secs = accounted.as_secs_f64().max(f64::MIN_POSITIVE);
261    let transfers = profile.transfers.max(1);
262
263    println!(
264        "profile calls={} transfers={} accounted={:?} wall={:?} unaccounted={:?}",
265        profile.calls,
266        profile.transfers,
267        accounted,
268        elapsed,
269        elapsed.saturating_sub(accounted),
270    );
271
272    for (stage, duration) in [
273        ("validation", profile.validation),
274        ("setup", profile.setup),
275        ("submit", profile.submit),
276        ("wait_write", profile.wait_write),
277        ("wait_read", profile.wait_read),
278        ("decode_copy", profile.decode_copy),
279    ] {
280        let pct_of_accounted = duration.as_secs_f64() * 100.0 / accounted_secs;
281        let pct_of_wall = duration.as_secs_f64() * 100.0 / elapsed_secs;
282        let avg_us_per_transfer = duration.as_secs_f64() * 1_000_000.0 / transfers as f64;
283        println!(
284            "stage={stage} duration={duration:?} pct_accounted={pct_of_accounted:.2} pct_wall={pct_of_wall:.2} avg_us_per_transfer={avg_us_per_transfer:.3}"
285        );
286    }
287}