axbuild 0.4.14

An OS build lib toolkit used by arceos
Documentation
use std::{
    fs,
    fs::File,
    path::Path,
    process::{Command, ExitStatus},
    time::Instant,
};

use anyhow::{Context, bail};
use serde::{Deserialize, Serialize};

use super::{
    super::ArgsPerf,
    args_support::{effective_callchain, effective_max_depth, host_time_enabled},
    harness::QperfTools,
    metrics::{child_resource_usage, write_host_perf_unavailable, write_host_time_metrics},
    monitor::{
        PerfWindowReport, run_qemu_with_stdout_monitor, window_report_from_config,
        write_window_report,
    },
    outputs::{PerfOutputs, ensure_file},
    symbols::KernelTextRange,
    toolchain::find_executable,
};

pub(super) const QPERF_QUEUE_SIZE: usize = 4096;
pub(super) const DEFAULT_STARRY_SHELL_PREFIX: &str = "root@starry:";

#[derive(Deserialize, Serialize)]
pub(super) struct PerfQemuConfig {
    pub(super) args: Vec<String>,
    pub(super) uefi: bool,
    pub(super) to_bin: bool,
    pub(super) success_regex: Vec<String>,
    pub(super) fail_regex: Vec<String>,
    pub(super) shell_prefix: Option<String>,
    pub(super) shell_init_cmd: Option<String>,
    pub(super) timeout: Option<u64>,
    pub(super) start_marker: Option<String>,
    pub(super) stop_marker: Option<String>,
    pub(super) workload_timeout: Option<u64>,
}

pub(super) struct QemuRun {
    pub(super) status: ExitStatus,
    pub(super) window: PerfWindowReport,
}

pub(super) fn write_qemu_config(
    outputs: &PerfOutputs,
    tools: &QperfTools,
    args: &ArgsPerf,
    arch: &str,
    qemu_args: Vec<String>,
    text_range: Option<KernelTextRange>,
) -> anyhow::Result<()> {
    let mut perf_qemu_args = vec!["-plugin".to_string()];
    let mut plugin_params = format!(
        "{},freq={},max_depth={},queue_size={},mode={},callchain={},out={}",
        tools.plugin.display(),
        args.freq,
        effective_max_depth(args),
        QPERF_QUEUE_SIZE,
        args.mode,
        effective_callchain(args),
        outputs.raw.display()
    );
    plugin_params.push_str(&format!(
        ",filter_kernel={}",
        if args.kernel_filter { 1 } else { 0 }
    ));
    if let Some(range) = text_range {
        let start = range.virt.start;
        let end = range.virt.end;
        plugin_params.push_str(&format!(",filter_start=0x{start:x},filter_end=0x{end:x}"));
        if let Some(phys) = range.phys {
            let offset = range.virt.start.wrapping_sub(phys.start);
            plugin_params.push_str(&format!(
                ",filter_alias_start=0x{:x},filter_alias_end=0x{:x},filter_alias_offset=0x{:x}",
                phys.start, phys.end, offset
            ));
        }
    }
    perf_qemu_args.push(plugin_params);
    let mut qemu_args = direct_qemu_args(arch, qemu_args)?;
    qemu_args.extend(args.qemu_args.iter().cloned());
    if qemu_stdout_monitor_enabled(args) && !has_qemu_option(&qemu_args, "-qmp") {
        qemu_args.extend([
            "-qmp".to_string(),
            format!("unix:{},server=on,wait=off", outputs.qmp_socket.display()),
        ]);
    }
    perf_qemu_args.extend(qemu_args);

    let shell_init_cmd = args
        .shell_init_cmd
        .as_deref()
        .map(str::trim)
        .filter(|cmd| !cmd.is_empty())
        .map(str::to_string);
    let shell_prefix = shell_init_cmd.as_ref().map(|_| {
        args.shell_prefix
            .clone()
            .unwrap_or_else(|| DEFAULT_STARRY_SHELL_PREFIX.to_string())
    });

    let config = PerfQemuConfig {
        args: perf_qemu_args,
        uefi: false,
        to_bin: true,
        success_regex: Vec::new(),
        fail_regex: vec![r"(?i)\bpanic(?:ked)?\b".to_string()],
        shell_prefix,
        shell_init_cmd,
        timeout: (args.timeout > 0).then_some(args.timeout),
        start_marker: args.start_marker.clone(),
        stop_marker: args.stop_marker.clone(),
        workload_timeout: args.workload_timeout,
    };
    fs::write(&outputs.qemu_config, toml::to_string_pretty(&config)?)
        .with_context(|| format!("failed to write {}", outputs.qemu_config.display()))?;
    Ok(())
}

fn direct_qemu_args(arch: &str, mut args: Vec<String>) -> anyhow::Result<Vec<String>> {
    match arch {
        "riscv64" | "loongarch64" => {
            if !has_qemu_option(&args, "-machine") {
                args.splice(0..0, ["-machine".to_string(), "virt".to_string()]);
            }
        }
        _ => bail!("qperf currently supports StarryOS riscv64 and loongarch64 only"),
    }
    Ok(args)
}

fn has_qemu_option(args: &[String], option: &str) -> bool {
    args.iter().any(|arg| arg == option)
}

pub(super) fn run_qemu_direct(
    outputs: &PerfOutputs,
    args: &ArgsPerf,
    arch: &str,
    kernel_bin: &Path,
) -> anyhow::Result<QemuRun> {
    ensure_file(kernel_bin, "StarryOS kernel image")?;
    let qemu = qemu_executable(arch)?;
    let config = qemu_config_from_path(&outputs.qemu_config)?;
    let qemu_args = config.args.clone();
    let monitor_stdout = qemu_stdout_monitor_enabled(args);

    let mut command_args = if args.timeout > 0 && !monitor_stdout {
        vec![
            "timeout".to_string(),
            "--signal=INT".to_string(),
            "--kill-after=5s".to_string(),
            format!("{}s", args.timeout),
            qemu.to_string(),
        ]
    } else {
        vec![qemu.to_string()]
    };
    command_args.extend(qemu_args);
    command_args.push("-kernel".to_string());
    command_args.push(kernel_bin.display().to_string());

    if args.host_perf {
        if let Some(perf) = find_executable("perf") {
            let mut wrapped = vec![
                perf.display().to_string(),
                "stat".to_string(),
                "-x".to_string(),
                ",".to_string(),
                "-o".to_string(),
                outputs.host_perf.display().to_string(),
                "-e".to_string(),
                args.host_perf_events.clone(),
                "--".to_string(),
            ];
            wrapped.extend(command_args);
            command_args = wrapped;
        } else {
            write_host_perf_unavailable(&outputs.host_perf, "perf not found in PATH")?;
            eprintln!("qperf: --host-perf requested but `perf` was not found in PATH");
        }
    }

    let mut command = Command::new(&command_args[0]);
    command.args(&command_args[1..]);
    eprintln!("running qperf QEMU: {command:?}");
    let host_wall_start = Instant::now();
    let host_usage_start = child_resource_usage();
    let qemu_run = if monitor_stdout {
        run_qemu_with_stdout_monitor(command, &config, outputs, args.timeout)?
    } else {
        QemuRun {
            status: command.status().context("failed to spawn QEMU")?,
            window: window_report_from_config(&config),
        }
    };
    if host_time_enabled(args) {
        write_host_time_metrics(
            &outputs.host_time,
            host_wall_start.elapsed(),
            host_usage_start,
            child_resource_usage(),
            &qemu_run.status,
        )?;
    }
    write_window_report(&outputs.window, &qemu_run.window)?;
    if !outputs.profile_stdout.exists() {
        File::create(&outputs.profile_stdout)
            .with_context(|| format!("failed to create {}", outputs.profile_stdout.display()))?;
    }
    if !outputs.profile_stderr.exists() {
        File::create(&outputs.profile_stderr)
            .with_context(|| format!("failed to create {}", outputs.profile_stderr.display()))?;
    }
    Ok(qemu_run)
}

fn qemu_executable(arch: &str) -> anyhow::Result<&'static str> {
    let name = match arch {
        "riscv64" => "qemu-system-riscv64",
        "loongarch64" => "qemu-system-loongarch64",
        _ => bail!("qperf currently supports StarryOS riscv64 and loongarch64 only"),
    };
    if find_executable(name).is_none() {
        bail!(
            "qperf requires `{name}` in PATH; install the matching QEMU system emulator or run \
             the Docker-based harness perf-profile entrypoint"
        );
    }
    Ok(name)
}

fn qemu_config_from_path(path: &Path) -> anyhow::Result<PerfQemuConfig> {
    let text = fs::read_to_string(path)
        .with_context(|| format!("failed to read qperf QEMU config {}", path.display()))?;
    toml::from_str(&text)
        .with_context(|| format!("failed to parse qperf QEMU config {}", path.display()))
}

fn qemu_stdout_monitor_enabled(args: &ArgsPerf) -> bool {
    args.shell_init_cmd
        .as_deref()
        .is_some_and(|cmd| !cmd.trim().is_empty())
        || args.start_marker.is_some()
        || args.stop_marker.is_some()
        || args.workload_timeout.is_some()
}