1use crate::stress::MemoryPressureStats;
6use anyhow::Context as _;
7use argh::FromArgs;
8use fidl_fuchsia_io as fio;
9use fuchsia_trace as trace;
10use fuchsiaperf::{Direction, FuchsiaPerfBenchmarkResult, Unit};
11use rand::RngExt as _;
12use std::fs::OpenOptions;
13use std::path::Path;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::thread;
17use std::time::{Duration, Instant};
18
19pub const KB: u64 = 1024;
20pub const MB: u64 = 1024 * KB;
21pub const PAGE_SIZE: u64 = 4096;
22
23pub const BURST_IO_PREALLOC_SIZE: u64 = 55 * MB;
24
25pub const NS_PER_MS: f64 = 1_000_000.0;
26
27pub const RANDOM_IO_WRITE_PATTERN: u8 = 0xBB;
28pub const BURST_IO_WRITE_PATTERN: u8 = 0xCC;
29
30#[derive(Default, Debug, Clone)]
31pub struct Metrics {
32 pub read_ops: u64,
33 pub write_ops: u64,
34 pub fsync_ops: u64,
35 pub read_bytes: u64,
36 pub write_bytes: u64,
37 pub max_read_latency_ns: u64,
38 pub max_write_latency_ns: u64,
39 pub max_fsync_latency_ns: u64,
40 pub total_write_latency_ns: u64,
41 pub total_read_latency_ns: u64,
42 pub total_fsync_latency_ns: u64,
43 pub op_latencies_ns: Vec<u64>,
44 pub fsync_latencies_ns: Vec<u64>,
45}
46
47impl Metrics {
48 pub fn record_read(&mut self, bytes_read: u64, elapsed_ns: u64) {
49 self.read_ops += 1;
50 self.read_bytes += bytes_read;
51 self.max_read_latency_ns = std::cmp::max(self.max_read_latency_ns, elapsed_ns);
52 self.total_read_latency_ns += elapsed_ns;
53 self.op_latencies_ns.push(elapsed_ns);
54 }
55
56 pub fn record_write(&mut self, bytes_written: u64, elapsed_ns: u64) {
57 self.write_ops += 1;
58 self.write_bytes += bytes_written;
59 self.max_write_latency_ns = std::cmp::max(self.max_write_latency_ns, elapsed_ns);
60 self.total_write_latency_ns += elapsed_ns;
61 self.op_latencies_ns.push(elapsed_ns);
62 }
63
64 pub fn record_fsync(&mut self, elapsed_ns: u64) {
65 self.fsync_ops += 1;
66 self.max_fsync_latency_ns = std::cmp::max(self.max_fsync_latency_ns, elapsed_ns);
67 self.total_fsync_latency_ns += elapsed_ns;
68 self.fsync_latencies_ns.push(elapsed_ns);
69 }
70
71 pub fn merge(&mut self, other: &Metrics) {
72 self.read_ops += other.read_ops;
73 self.write_ops += other.write_ops;
74 self.fsync_ops += other.fsync_ops;
75 self.read_bytes += other.read_bytes;
76 self.write_bytes += other.write_bytes;
77 self.max_read_latency_ns =
78 std::cmp::max(self.max_read_latency_ns, other.max_read_latency_ns);
79 self.max_write_latency_ns =
80 std::cmp::max(self.max_write_latency_ns, other.max_write_latency_ns);
81 self.max_fsync_latency_ns =
82 std::cmp::max(self.max_fsync_latency_ns, other.max_fsync_latency_ns);
83 self.total_write_latency_ns += other.total_write_latency_ns;
84 self.total_read_latency_ns += other.total_read_latency_ns;
85 self.total_fsync_latency_ns += other.total_fsync_latency_ns;
86 self.op_latencies_ns.extend_from_slice(&other.op_latencies_ns);
87 self.fsync_latencies_ns.extend_from_slice(&other.fsync_latencies_ns);
88 }
89}
90
91struct Timer {
92 start: zx::MonotonicInstant,
93}
94
95impl Timer {
96 fn start() -> Self {
97 Self { start: zx::MonotonicInstant::get() }
98 }
99
100 fn elapsed_ns(&self) -> u64 {
101 (zx::MonotonicInstant::get() - self.start).into_nanos() as u64
102 }
103}
104
105fn do_fsync(file_proxy: &fio::FileSynchronousProxy, metrics: &mut Metrics) -> anyhow::Result<()> {
106 let timer = Timer::start();
107 trace::duration!("benchmark", "fsync");
108 let deadline = zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(30));
109 file_proxy.sync(deadline).context("FIDL error on fsync")?.map_err(|status| {
110 anyhow::anyhow!("fsync status: {:?}", zx::Status::err_from_raw(status))
111 })?;
112 metrics.record_fsync(timer.elapsed_ns());
113 Ok(())
114}
115
116struct RateLimiter {
117 start_run: Instant,
118 rate_mibs: u64,
119}
120
121impl RateLimiter {
122 fn new(rate_mibs: u64) -> Self {
123 Self { start_run: Instant::now(), rate_mibs }
124 }
125
126 fn sleep_if_needed(&mut self, total_bytes_processed: u64) {
127 if self.rate_mibs > 0 {
128 let target_duration = Duration::from_secs_f64(
129 total_bytes_processed as f64 / (self.rate_mibs * 1024 * 1024) as f64,
130 );
131 let elapsed = self.start_run.elapsed();
132 if elapsed < target_duration {
133 thread::sleep(target_duration - elapsed);
134 } else if elapsed > target_duration + Duration::from_millis(50) {
135 self.start_run = Instant::now() - target_duration;
136 }
137 }
138 }
139}
140
141#[allow(clippy::too_many_arguments)]
142pub fn run_random(
143 vmo: zx::Vmo,
144 file_proxy: &fio::FileSynchronousProxy,
145 op_size_bytes: usize,
146 file_size_bytes: u64,
147 read_percentage: u32,
148 fsync_every_n_ops: u64,
149 rate_mibs: u64,
150 seed: u64,
151 stop_signal: Arc<AtomicBool>,
152 metrics: &mut Metrics,
153) -> anyhow::Result<()> {
154 use rand::SeedableRng as _;
155 let mut rng = if seed == 0 {
156 rand::rngs::StdRng::from_rng(&mut rand::rng())
157 } else {
158 rand::rngs::StdRng::seed_from_u64(seed)
159 };
160 let mut op_count = 0;
161 let mut rate_limiter = RateLimiter::new(rate_mibs);
162 let mut total_bytes_processed = 0u64;
163 let mut io_buffer = vec![0u8; op_size_bytes];
164
165 while !stop_signal.load(Ordering::Relaxed) {
166 let range = file_size_bytes.saturating_sub(op_size_bytes as u64);
167 let max_page_index = range / PAGE_SIZE;
168 let mut offset =
169 if max_page_index == 0 { 0 } else { rng.random_range(0..=max_page_index) * PAGE_SIZE };
170 let is_read = rng.random_range(0..100) < read_percentage;
171 let buf_slice = &mut io_buffer[..op_size_bytes];
172
173 if offset + op_size_bytes as u64 > file_size_bytes {
174 offset = 0;
175 }
176
177 let timer = Timer::start();
178 if is_read {
179 trace::duration!(
180 "benchmark",
181 "vmo_read",
182 "bytes" => op_size_bytes as u64,
183 "offset" => offset
184 );
185 vmo.read(buf_slice, offset).context("VMO read failed")?;
186 metrics.record_read(op_size_bytes as u64, timer.elapsed_ns());
187 } else {
188 buf_slice.fill(RANDOM_IO_WRITE_PATTERN);
189 trace::duration!(
190 "benchmark",
191 "vmo_write",
192 "bytes" => op_size_bytes as u64,
193 "offset" => offset
194 );
195 vmo.write(buf_slice, offset).context("VMO write failed")?;
196 metrics.record_write(op_size_bytes as u64, timer.elapsed_ns());
197 }
198
199 let is_fsync = !is_read && fsync_every_n_ops > 0 && op_count % fsync_every_n_ops == 0;
200 if is_fsync {
201 do_fsync(file_proxy, metrics)?;
202 }
203
204 if !is_read {
205 op_count += 1;
206 }
207
208 total_bytes_processed += op_size_bytes as u64;
209 rate_limiter.sleep_if_needed(total_bytes_processed);
210 }
211
212 if fsync_every_n_ops > 0 {
213 do_fsync(file_proxy, metrics)?;
214 }
215
216 Ok(())
217}
218
219#[allow(clippy::too_many_arguments)]
220pub fn run_sequential(
221 zx_vmo: zx::Vmo,
222 file_proxy: &fio::FileSynchronousProxy,
223 op_size_bytes: usize,
224 file_size_bytes: u64,
225 rate_mibs: u64,
226 fsync_every_n_ops: u64,
227 read: bool,
228 stop_signal: Arc<AtomicBool>,
229 metrics: &mut Metrics,
230) -> anyhow::Result<()> {
231 let mut io_buffer = vec![0u8; op_size_bytes];
232 if !read {
233 io_buffer.fill(0x99);
234 }
235
236 let mut offset = 0;
237 let mut op_count = 0;
238 let mut rate_limiter = RateLimiter::new(rate_mibs);
239 let mut total_bytes_processed = 0u64;
240
241 while !stop_signal.load(Ordering::Relaxed) {
242 if offset + op_size_bytes as u64 > file_size_bytes {
243 if !read && fsync_every_n_ops == 0 {
244 do_fsync(file_proxy, metrics)?;
245 }
246 offset = 0;
247 }
248
249 let timer = Timer::start();
250 if read {
251 trace::duration!(
252 "benchmark",
253 "vmo_read",
254 "bytes" => op_size_bytes as u64,
255 "offset" => offset
256 );
257 zx_vmo.read(&mut io_buffer, offset).context("Sequential VMO read failed")?;
258 metrics.record_read(op_size_bytes as u64, timer.elapsed_ns());
259 } else {
260 trace::duration!(
261 "benchmark",
262 "vmo_write",
263 "bytes" => op_size_bytes as u64,
264 "offset" => offset
265 );
266 zx_vmo.write(&io_buffer, offset).context("Sequential VMO write failed")?;
267 metrics.record_write(op_size_bytes as u64, timer.elapsed_ns());
268 }
269
270 let is_fsync = !read && fsync_every_n_ops > 0 && op_count % fsync_every_n_ops == 0;
271 if is_fsync {
272 do_fsync(file_proxy, metrics)?;
273 }
274
275 if !read {
276 op_count += 1;
277 }
278
279 offset += op_size_bytes as u64;
280 total_bytes_processed += op_size_bytes as u64;
281 rate_limiter.sleep_if_needed(total_bytes_processed);
282 }
283
284 if !read {
285 do_fsync(file_proxy, metrics)?;
286 }
287
288 Ok(())
289}
290
291#[allow(clippy::too_many_arguments)]
292pub fn run_burst(
293 vmo: zx::Vmo,
294 file_proxy: &fio::FileSynchronousProxy,
295 op_size_bytes: usize,
296 burst_ops_count: usize,
297 sleep_between_bursts_ms: u64,
298 periodic_fsync_ms: u64,
299 read: bool,
300 rate_mibs: u64,
301 stop_signal: Arc<AtomicBool>,
302 metrics: &mut Metrics,
303) -> anyhow::Result<()> {
304 let mut io_buffer = vec![0u8; op_size_bytes];
305 let buf_slice = &mut io_buffer[..op_size_bytes];
306 if !read {
307 buf_slice.fill(BURST_IO_WRITE_PATTERN);
308 }
309 let mut offset = 0;
310 let mut last_fsync = Instant::now();
311 let mut rate_limiter = RateLimiter::new(rate_mibs);
312 let mut total_bytes_processed = 0u64;
313
314 while !stop_signal.load(Ordering::Relaxed) {
315 for _ in 0..burst_ops_count {
316 if offset + op_size_bytes as u64 > BURST_IO_PREALLOC_SIZE {
317 offset = 0;
318 }
319 let timer = Timer::start();
320 if read {
321 trace::duration!(
322 "benchmark",
323 "vmo_read",
324 "bytes" => op_size_bytes as u64,
325 "offset" => offset
326 );
327 vmo.read(buf_slice, offset).context("VMO burst read failed")?;
328 metrics.record_read(op_size_bytes as u64, timer.elapsed_ns());
329 } else {
330 trace::duration!(
331 "benchmark",
332 "vmo_write",
333 "bytes" => op_size_bytes as u64,
334 "offset" => offset
335 );
336 vmo.write(buf_slice, offset).context("VMO burst write failed")?;
337 metrics.record_write(op_size_bytes as u64, timer.elapsed_ns());
338 }
339 offset += op_size_bytes as u64;
340 total_bytes_processed += op_size_bytes as u64;
341 }
342
343 if !read {
344 let is_fsync = periodic_fsync_ms > 0
345 && last_fsync.elapsed().as_millis() >= periodic_fsync_ms as u128;
346 if is_fsync {
347 do_fsync(file_proxy, metrics)?;
348 last_fsync = Instant::now();
349 }
350 }
351
352 rate_limiter.sleep_if_needed(total_bytes_processed);
353 thread::sleep(Duration::from_millis(sleep_between_bursts_ms));
354 }
355
356 if !read {
357 do_fsync(file_proxy, metrics)?;
358 }
359
360 Ok(())
361}
362
363#[allow(clippy::too_many_arguments)]
364pub fn run_transfer(
365 source_vmo: zx::Vmo,
366 dest_vmo: zx::Vmo,
367 dest_proxy: &fio::FileSynchronousProxy,
368 op_size_bytes: usize,
369 file_size_bytes: u64,
370 xor_transform: bool,
371 rate_mibs: u64,
372 fsync_every_n_ops: u64,
373 stop_signal: Arc<AtomicBool>,
374 metrics: &mut Metrics,
375) -> anyhow::Result<()> {
376 let mut op_count = 0;
377 let mut rate_limiter = RateLimiter::new(rate_mibs);
378 let mut total_bytes_processed = 0u64;
379 let mut io_buffer = vec![0u8; op_size_bytes];
380
381 while !stop_signal.load(Ordering::Relaxed) {
382 let offset = (op_count as u64 * op_size_bytes as u64) % file_size_bytes;
383
384 let read_timer = Timer::start();
385 source_vmo.read(&mut io_buffer, offset).context("Failed to read from source VMO")?;
386 metrics.record_read(op_size_bytes as u64, read_timer.elapsed_ns());
387
388 if xor_transform {
389 for b in &mut io_buffer {
390 *b ^= 0x55;
391 }
392 }
393
394 let write_timer = Timer::start();
395 dest_vmo.write(&io_buffer, offset).context("Failed to write to dest VMO")?;
396 metrics.record_write(op_size_bytes as u64, write_timer.elapsed_ns());
397
398 op_count += 1;
399 total_bytes_processed += op_size_bytes as u64;
400 rate_limiter.sleep_if_needed(total_bytes_processed);
401
402 if fsync_every_n_ops > 0 && op_count % fsync_every_n_ops == 0 {
403 do_fsync(dest_proxy, metrics)?;
404 }
405 }
406
407 if fsync_every_n_ops > 0 {
408 do_fsync(dest_proxy, metrics)?;
409 }
410
411 Ok(())
412}
413
414#[derive(FromArgs, Debug, Clone)]
415#[argh(subcommand, name = "random")]
416pub struct RandomArgs {
418 #[argh(option, default = "4096")]
420 pub op_size_bytes: u64,
421
422 #[argh(option, default = "67108864")]
424 pub file_size_bytes: u64,
425
426 #[argh(option, default = "50")]
428 pub read_percentage: u32,
429
430 #[argh(option, default = "0")]
432 pub fsync_every_n_ops: u64,
433
434 #[argh(option, default = "0")]
436 pub rate_mibs: u64,
437
438 #[argh(option, default = "0")]
440 pub seed: u64,
441}
442
443#[derive(FromArgs, Debug, Clone)]
444#[argh(subcommand, name = "sequential")]
445pub struct SequentialArgs {
447 #[argh(option, default = "131072")]
449 pub op_size_bytes: u64,
450
451 #[argh(option, default = "1073741824")]
453 pub file_size_bytes: u64,
454
455 #[argh(switch)]
457 pub read: bool,
458
459 #[argh(option, default = "0")]
461 pub fsync_every_n_ops: u64,
462
463 #[argh(option, default = "0")]
465 pub rate_mibs: u64,
466}
467
468#[derive(FromArgs, Debug, Clone)]
469#[argh(subcommand, name = "burst")]
470pub struct BurstArgs {
472 #[argh(option, default = "4096")]
474 pub op_size_bytes: u64,
475
476 #[argh(option, default = "1000")]
478 pub burst_ops_count: u64,
479
480 #[argh(option, default = "100")]
482 pub sleep_between_bursts_ms: u64,
483
484 #[argh(option, default = "1000")]
486 pub periodic_fsync_ms: u64,
487
488 #[argh(switch)]
490 pub read: bool,
491
492 #[argh(option, default = "0")]
494 pub rate_mibs: u64,
495}
496
497#[derive(FromArgs, Debug, Clone)]
498#[argh(subcommand, name = "transfer")]
499pub struct TransferArgs {
501 #[argh(option, default = "131072")]
503 pub op_size_bytes: u64,
504
505 #[argh(option, default = "67108864")]
507 pub file_size_bytes: u64,
508
509 #[argh(switch)]
511 pub xor_transform: bool,
512
513 #[argh(option, default = "0")]
515 pub fsync_every_n_ops: u64,
516
517 #[argh(option, default = "0")]
519 pub rate_mibs: u64,
520}
521
522#[derive(FromArgs, Debug, Clone)]
523#[argh(subcommand)]
524pub enum WorkloadSubcommand {
525 Random(RandomArgs),
526 Sequential(SequentialArgs),
527 Burst(BurstArgs),
528 Transfer(TransferArgs),
529}
530
531impl WorkloadSubcommand {
532 pub fn name(&self) -> &'static str {
533 match self {
534 WorkloadSubcommand::Random(_) => "random",
535 WorkloadSubcommand::Sequential(_) => "sequential",
536 WorkloadSubcommand::Burst(_) => "burst",
537 WorkloadSubcommand::Transfer(_) => "transfer",
538 }
539 }
540
541 pub fn persona(&self) -> &'static str {
542 match self {
543 WorkloadSubcommand::Random(args) => {
544 if args.rate_mibs == 0 {
545 "AppLaunch"
546 } else {
547 "Database"
548 }
549 }
550 WorkloadSubcommand::Sequential(args) => {
551 if args.fsync_every_n_ops > 0 {
552 "Media"
553 } else {
554 "Download"
555 }
556 }
557 WorkloadSubcommand::Transfer(args) => {
558 if args.xor_transform {
559 "Compress"
560 } else {
561 "Copy"
562 }
563 }
564 WorkloadSubcommand::Burst(_) => "Media",
565 }
566 }
567}
568
569pub fn get_effective_op_size(op_size_bytes: u64) -> u64 {
570 if op_size_bytes == 0 { PAGE_SIZE } else { op_size_bytes }
571}
572
573pub fn setup_backing_vmo(
574 file_path: &Path,
575 size_bytes: u64,
576) -> anyhow::Result<(fio::FileSynchronousProxy, zx::Vmo)> {
577 let file = OpenOptions::new()
578 .read(true)
579 .write(true)
580 .create(true)
581 .truncate(true)
582 .open(file_path)
583 .context("Failed to open or create target file under /data")?;
584 if size_bytes > 0 {
585 file.set_len(size_bytes).context("Failed to set file size limit")?;
586 }
587
588 let zx_channel = fdio::clone_channel(&file).context("Failed to clone fdio channel")?;
589 let file_proxy = fio::FileSynchronousProxy::new(zx_channel);
590 let vmo_flags = fio::VmoFlags::READ | fio::VmoFlags::WRITE | fio::VmoFlags::SHARED_BUFFER;
591 let deadline = zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(30));
592 let vmo_handle = file_proxy
593 .get_backing_memory(vmo_flags, deadline)
594 .context("Failed to get backing memory via FIDL")?
595 .map_err(|status| {
596 anyhow::anyhow!("get_backing_memory status: {:?}", zx::Status::err_from_raw(status))
597 })?;
598 Ok((file_proxy, zx::Vmo::from(vmo_handle)))
599}
600
601pub struct StopSignalGuard(pub Arc<AtomicBool>);
602impl Drop for StopSignalGuard {
603 fn drop(&mut self) {
604 self.0.store(true, Ordering::Relaxed);
605 }
606}
607
608pub struct FileCleanupGuard {
609 pub paths: Vec<String>,
610}
611impl Drop for FileCleanupGuard {
612 fn drop(&mut self) {
613 for path in &self.paths {
614 let _ = std::fs::remove_file(path);
615 }
616 }
617}
618
619fn calc_percentile(mut vals: Vec<u64>, pct: f64) -> f64 {
620 if vals.is_empty() {
621 return 0.0;
622 }
623 vals.sort_unstable();
624 let idx = ((vals.len() as f64 - 1.0) * pct).round() as usize;
625 vals[std::cmp::min(idx, vals.len() - 1)] as f64
626}
627
628pub async fn run_workloads(
629 run_name: &str,
630 subcommands: Vec<WorkloadSubcommand>,
631 duration_secs: u64,
632 mem_stats: Arc<MemoryPressureStats>,
633) -> anyhow::Result<Vec<FuchsiaPerfBenchmarkResult>> {
634 let stop_signal = Arc::new(AtomicBool::new(false));
635 let _stop_guard = StopSignalGuard(stop_signal.clone());
636 let mut created_file_paths = Vec::new();
637
638 let _timer_task = if duration_secs > 0 {
639 let stop_signal_clone = stop_signal.clone();
640 Some(fuchsia_async::Task::spawn(async move {
641 fuchsia_async::Timer::new(Duration::from_secs(duration_secs)).await;
642 log::info!("Configured global duration reached, signalling stop to all threads.");
643 stop_signal_clone.store(true, Ordering::Relaxed);
644 }))
645 } else {
646 None
647 };
648
649 let mut thread_handles = Vec::new();
650 let mut persona_counters = std::collections::HashMap::new();
651
652 for sub in subcommands {
653 let sub_clone = sub.clone();
654 let stop_signal = stop_signal.clone();
655 let persona = sub.persona();
656
657 let entry = persona_counters.entry(persona).or_insert(0);
658 *entry += 1;
659 let display_name = format!("{}_{}", persona, entry);
660
661 let handle = match &sub_clone {
662 WorkloadSubcommand::Transfer(args) => {
663 let src_path_str = format!("/data/stress_target_{}_src", display_name);
664 let dest_path_str = format!("/data/stress_target_{}_dest", display_name);
665 created_file_paths.push(src_path_str.clone());
666 created_file_paths.push(dest_path_str.clone());
667
668 let args_clone = args.clone();
669 thread::Builder::new()
670 .name(display_name.clone())
671 .spawn(move || -> anyhow::Result<Metrics> {
672 let mut metrics = Metrics::default();
673 let op_size = get_effective_op_size(args_clone.op_size_bytes);
674 let (_src_proxy, src_vmo) = setup_backing_vmo(
675 Path::new(&src_path_str),
676 args_clone.file_size_bytes,
677 )?;
678 let (dest_proxy, dest_vmo) = setup_backing_vmo(
679 Path::new(&dest_path_str),
680 args_clone.file_size_bytes,
681 )?;
682
683 run_transfer(
684 src_vmo,
685 dest_vmo,
686 &dest_proxy,
687 op_size as usize,
688 args_clone.file_size_bytes,
689 args_clone.xor_transform,
690 args_clone.rate_mibs,
691 args_clone.fsync_every_n_ops,
692 stop_signal,
693 &mut metrics,
694 )?;
695 Ok(metrics)
696 })
697 .context("Failed to spawn thread")?
698 }
699 WorkloadSubcommand::Random(args) => {
700 let file_path_str = format!("/data/stress_target_{}", display_name);
701 created_file_paths.push(file_path_str.clone());
702
703 let args_clone = args.clone();
704 thread::Builder::new()
705 .name(display_name.clone())
706 .spawn(move || -> anyhow::Result<Metrics> {
707 let mut metrics = Metrics::default();
708 let file_path = Path::new(&file_path_str);
709 let op_size = get_effective_op_size(args_clone.op_size_bytes);
710 let (file_proxy, zx_vmo) =
711 setup_backing_vmo(&file_path, args_clone.file_size_bytes)?;
712 run_random(
713 zx_vmo,
714 &file_proxy,
715 op_size as usize,
716 args_clone.file_size_bytes,
717 args_clone.read_percentage,
718 args_clone.fsync_every_n_ops,
719 args_clone.rate_mibs,
720 args_clone.seed,
721 stop_signal,
722 &mut metrics,
723 )?;
724 Ok(metrics)
725 })
726 .context("Failed to spawn thread")?
727 }
728 WorkloadSubcommand::Sequential(args) => {
729 let file_path_str = format!("/data/stress_target_{}", display_name);
730 created_file_paths.push(file_path_str.clone());
731
732 let args_clone = args.clone();
733 thread::Builder::new()
734 .name(display_name.clone())
735 .spawn(move || -> anyhow::Result<Metrics> {
736 let mut metrics = Metrics::default();
737 let file_path = Path::new(&file_path_str);
738 let op_size = get_effective_op_size(args_clone.op_size_bytes);
739 let (file_proxy, zx_vmo) =
740 setup_backing_vmo(&file_path, args_clone.file_size_bytes)?;
741 run_sequential(
742 zx_vmo,
743 &file_proxy,
744 op_size as usize,
745 args_clone.file_size_bytes,
746 args_clone.rate_mibs,
747 args_clone.fsync_every_n_ops,
748 args_clone.read,
749 stop_signal,
750 &mut metrics,
751 )?;
752 Ok(metrics)
753 })
754 .context("Failed to spawn thread")?
755 }
756 WorkloadSubcommand::Burst(args) => {
757 let file_path_str = format!("/data/stress_target_{}", display_name);
758 created_file_paths.push(file_path_str.clone());
759
760 let args_clone = args.clone();
761 thread::Builder::new()
762 .name(display_name.clone())
763 .spawn(move || -> anyhow::Result<Metrics> {
764 let mut metrics = Metrics::default();
765 let file_path = Path::new(&file_path_str);
766 let op_size = get_effective_op_size(args_clone.op_size_bytes);
767 let (file_proxy, zx_vmo) =
768 setup_backing_vmo(&file_path, BURST_IO_PREALLOC_SIZE)?;
769 run_burst(
770 zx_vmo,
771 &file_proxy,
772 op_size as usize,
773 args_clone.burst_ops_count as usize,
774 args_clone.sleep_between_bursts_ms,
775 args_clone.periodic_fsync_ms,
776 args_clone.read,
777 args_clone.rate_mibs,
778 stop_signal,
779 &mut metrics,
780 )?;
781 Ok(metrics)
782 })
783 .context("Failed to spawn thread")?
784 }
785 };
786
787 thread_handles.push((persona, handle));
788 }
789
790 let _file_guard = FileCleanupGuard { paths: created_file_paths };
791
792 let start_time = Instant::now();
793 let mut results: Vec<(&'static str, Metrics)> = Vec::new();
794 let mut first_error = None;
795
796 for (persona, handle) in thread_handles {
797 let join_result = fuchsia_async::unblock(move || handle.join()).await;
798 match join_result {
799 Ok(Ok(metrics)) => results.push((persona, metrics)),
800 Ok(Err(e)) => {
801 stop_signal.store(true, Ordering::Relaxed);
802 if first_error.is_none() {
803 first_error = Some(e.context(format!("Workload '{}' returned error", persona)));
804 }
805 }
806 Err(_) => {
807 stop_signal.store(true, Ordering::Relaxed);
808 if first_error.is_none() {
809 first_error = Some(anyhow::anyhow!("Workload '{}' panicked", persona));
810 }
811 }
812 }
813 }
814 if let Some(err) = first_error {
815 return Err(err);
816 }
817
818 let elapsed_secs = start_time.elapsed().as_secs_f64();
819
820 let mut persona_map: std::collections::BTreeMap<&'static str, Metrics> =
821 std::collections::BTreeMap::new();
822 for (persona, m) in results {
823 let entry = persona_map.entry(persona).or_default();
824 entry.merge(&m);
825 }
826
827 let mut total_bytes = 0;
828 let mut perf_results = Vec::new();
829
830 for (persona, m) in &persona_map {
831 total_bytes += m.write_bytes + m.read_bytes;
832 let p95_op = calc_percentile(m.op_latencies_ns.clone(), 0.95);
833 let p99_op = calc_percentile(m.op_latencies_ns.clone(), 0.99);
834 let throughput = if elapsed_secs > 0.0 {
835 (m.write_bytes + m.read_bytes) as f64 / elapsed_secs
836 } else {
837 0.0
838 };
839
840 perf_results.push(FuchsiaPerfBenchmarkResult {
841 label: format!("{}/{}/p95_op_latency", run_name, persona),
842 test_suite: "fuchsia.io_stress".to_string(),
843 unit: Unit::Nanoseconds,
844 direction: Direction::SmallerBetter,
845 values: vec![p95_op],
846 });
847 perf_results.push(FuchsiaPerfBenchmarkResult {
848 label: format!("{}/{}/p99_op_latency", run_name, persona),
849 test_suite: "fuchsia.io_stress".to_string(),
850 unit: Unit::Nanoseconds,
851 direction: Direction::SmallerBetter,
852 values: vec![p99_op],
853 });
854 perf_results.push(FuchsiaPerfBenchmarkResult {
855 label: format!("{}/{}/throughput", run_name, persona),
856 test_suite: "fuchsia.io_stress".to_string(),
857 unit: Unit::BytesPerSecond,
858 direction: Direction::BiggerBetter,
859 values: vec![throughput],
860 });
861 if m.fsync_ops > 0 {
862 let p95_fsync = calc_percentile(m.fsync_latencies_ns.clone(), 0.95);
863 let p99_fsync = calc_percentile(m.fsync_latencies_ns.clone(), 0.99);
864 perf_results.push(FuchsiaPerfBenchmarkResult {
865 label: format!("{}/{}/p95_fsync_latency", run_name, persona),
866 test_suite: "fuchsia.io_stress".to_string(),
867 unit: Unit::Nanoseconds,
868 direction: Direction::SmallerBetter,
869 values: vec![p95_fsync],
870 });
871 perf_results.push(FuchsiaPerfBenchmarkResult {
872 label: format!("{}/{}/p99_fsync_latency", run_name, persona),
873 test_suite: "fuchsia.io_stress".to_string(),
874 unit: Unit::Nanoseconds,
875 direction: Direction::SmallerBetter,
876 values: vec![p99_fsync],
877 });
878 }
879 }
880
881 let total_throughput_bps =
882 if elapsed_secs > 0.0 { total_bytes as f64 / elapsed_secs } else { 0.0 };
883 let mem_score = match mem_stats.max_level() {
884 fidl_fuchsia_memorypressure::Level::Normal => 0.0,
885 fidl_fuchsia_memorypressure::Level::Warning => 1.0,
886 fidl_fuchsia_memorypressure::Level::Critical => 2.0,
887 };
888
889 perf_results.push(FuchsiaPerfBenchmarkResult {
890 label: format!("{}/total_throughput", run_name),
891 test_suite: "fuchsia.io_stress".to_string(),
892 unit: Unit::BytesPerSecond,
893 direction: Direction::BiggerBetter,
894 values: vec![total_throughput_bps],
895 });
896 perf_results.push(FuchsiaPerfBenchmarkResult {
897 label: format!("{}/max_memory_pressure_score", run_name),
898 test_suite: "fuchsia.io_stress".to_string(),
899 unit: Unit::Count,
900 direction: Direction::SmallerBetter,
901 values: vec![mem_score],
902 });
903
904 perf_results.sort_by(|a, b| a.label.cmp(&b.label));
905
906 Ok(perf_results)
907}