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