//
// Syd: rock-solid application kernel
// benches/sys/open.rs: open microbenchmarks
//
// Copyright (c) 2024 Ali Polatel <alip@chesswob.org>
// Based in part upon gVisor's open_benchmark.cc which is:
//   Copyright 2020 The gVisor Authors.
//   SPDX-License-Identifier: Apache-2.0
//
// SPDX-License-Identifier: GPL-3.0

// This benchmark approximates gVisor's open micro-benchmark, but includes
// three modes: O_RDONLY, O_WRONLY, and O_RDWR. We create a set of temporary
// files, then repeatedly open/close random files among them.

use std::{
    env,
    ffi::CString,
    fs::{self, File},
    path::{Path, PathBuf},
    time::SystemTime,
};

use brunch::{benches, Bench};
use libc::{close, open, O_RDONLY, O_RDWR, O_WRONLY};
use nix::unistd::unlink;

/// Simple XorShift32 RNG to replace `rand_r`.
struct XorShift32 {
    state: u32,
}

impl XorShift32 {
    fn new(seed: u32) -> Self {
        Self { state: seed }
    }
    fn next_u32(&mut self) -> u32 {
        let mut x = self.state;
        x ^= x << 13;
        x ^= x >> 17;
        x ^= x << 5;
        self.state = x;
        x
    }
}

/// Create `count` empty files in a unique temp directory, return `(dir, paths)`.
fn create_temp_files(count: usize) -> (PathBuf, Vec<PathBuf>) {
    let mut dir = env::temp_dir();
    let unique = format!(
        "syd_open_bench_{}_{}",
        count,
        SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    );
    dir.push(unique);
    fs::create_dir_all(&dir).unwrap_or_else(|_| panic!("Failed to create directory: {:?}", &dir));

    let mut paths = Vec::with_capacity(count);
    for i in 0..count {
        let file_path = dir.join(format!("file_{}", i));
        File::create(&file_path)
            .unwrap_or_else(|_| panic!("Failed to create file: {:?}", &file_path));
        paths.push(file_path);
    }
    (dir, paths)
}

/// Open a random file among `files` using `mode`, then close it.
fn open_random_file(files: &[PathBuf], mode: i32, rng: &mut XorShift32) {
    let chosen = (rng.next_u32() as usize) % files.len();
    let c_path = CString::new(files[chosen].to_string_lossy().as_bytes())
        .expect("Failed to convert path to CString");
    let fd = unsafe { open(c_path.as_ptr(), mode) };
    if fd < 0 {
        panic!("open() failed for {:?}", files[chosen]);
    }
    unsafe {
        close(fd);
    }
}

/// A single iteration of "open random file" for the given file set and mode.
fn bench_open(files: &[PathBuf], mode: i32) {
    // We seed the RNG with a fixed value (as gVisor uses rand_r & a seed).
    let mut rng = XorShift32::new(1);
    open_random_file(files, mode, &mut rng);
}

/// Clean up: remove all files and then remove the directory.
fn cleanup_temp_dir(dir: &Path, files: &[PathBuf]) {
    for f in files {
        let _ = unlink(f);
    }
    let _ = fs::remove_dir_all(dir);
}

fn main() {
    // We'll test these file counts (original: Range(1, 128)).
    let file_counts = [1, 8, 64, 128];
    // Prepare directories and file sets.
    let mut setups = Vec::new();
    for &count in &file_counts {
        let (dir, files) = create_temp_files(count);
        setups.push((count, dir, files));
    }

    benches!(
        inline:

        // O_RDONLY
        Bench::new("Open(O_RDONLY) file_count=1").run(|| {
            bench_open(&setups[0].2, O_RDONLY);
        }),
        Bench::new("Open(O_RDONLY) file_count=8").run(|| {
            bench_open(&setups[1].2, O_RDONLY);
        }),
        Bench::new("Open(O_RDONLY) file_count=64").run(|| {
            bench_open(&setups[2].2, O_RDONLY);
        }),
        Bench::new("Open(O_RDONLY) file_count=128").run(|| {
            bench_open(&setups[3].2, O_RDONLY);
        }),

        // O_WRONLY
        Bench::new("Open(O_WRONLY) file_count=1").run(|| {
            bench_open(&setups[0].2, O_WRONLY);
        }),
        Bench::new("Open(O_WRONLY) file_count=8").run(|| {
            bench_open(&setups[1].2, O_WRONLY);
        }),
        Bench::new("Open(O_WRONLY) file_count=64").run(|| {
            bench_open(&setups[2].2, O_WRONLY);
        }),
        Bench::new("Open(O_WRONLY) file_count=128").run(|| {
            bench_open(&setups[3].2, O_WRONLY);
        }),

        // O_RDWR
        Bench::new("Open(O_RDWR) file_count=1").run(|| {
            bench_open(&setups[0].2, O_RDWR);
        }),
        Bench::new("Open(O_RDWR) file_count=8").run(|| {
            bench_open(&setups[1].2, O_RDWR);
        }),
        Bench::new("Open(O_RDWR) file_count=64").run(|| {
            bench_open(&setups[2].2, O_RDWR);
        }),
        Bench::new("Open(O_RDWR) file_count=128").run(|| {
            bench_open(&setups[3].2, O_RDWR);
        }),
    );

    // Cleanup all directories/files after benchmarks.
    for (_, dir, files) in setups {
        cleanup_temp_dir(&dir, &files);
    }
}
