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}