audio-plugin-bsd 0.1.1

Dynamic .so audio-plugin loader with ABI verification and FreeBSD Capsicum/pdfork per-process sandboxing for real-time audio in Rust
Documentation
//! Criterion micro-benchmarks for the non-RT plugin load/instantiate path.
//!
//! All measurements here run on a **setup** thread: `PluginLoader::load`
//! (`dlopen` + ABI verify + metadata copy), `instantiate` (adapter allocation),
//! and `unload` (`dlclose`) are never called from the RT audio thread. The
//! numbers quantify the one-time setup cost per plugin, not per-audio-block
//! overhead.
//!
//! # RT-safety contract
//!
//! Nothing in this file is RT-safe by construction — `load` / `instantiate` /
//! `unload` allocate, call into `libloading` (which calls `dlopen`), and may
//! block on the dynamic linker. The host must move the result of
//! `instantiate` onto the RT thread; the setup calls themselves stay here.

use std::hint::black_box;
use std::path::{Path, PathBuf};
use std::process::Command;

use audio_plugin_bsd::PluginLoader;
use criterion::{criterion_group, criterion_main, Criterion};

/// Build the `test_plugin` cdylib example and return its artifact path.
///
/// Mirrors `tests/common/mod.rs::ensure_test_plugin_built` — the `tests/`
/// helper module is not visible to the bench crate, so the small build helper
/// is duplicated here.
fn ensure_test_plugin_built() -> PathBuf {
    let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
    let status = Command::new(&cargo)
        .args(["build", "--example", "test_plugin"])
        .status()
        .unwrap_or_else(|e| panic!("failed to spawn `{cargo:?} build --example test_plugin`: {e}"));
    assert!(
        status.success(),
        "`cargo build --example test_plugin` failed (exit {:?})",
        status.code()
    );

    let (prefix, suffix) = platform_prefix_suffix();
    let file_name = format!("{prefix}test_plugin{suffix}");

    let target_dir = std::env::var_os("CARGO_TARGET_DIR")
        .map(PathBuf::from)
        .or_else(|| {
            std::env::var_os("CARGO_MANIFEST_DIR").map(|md| PathBuf::from(md).join("target"))
        })
        .expect("CARGO_MANIFEST_DIR or CARGO_TARGET_DIR must be set by the bench harness");

    // Match the running profile first, then fall back to the other.
    let profiles: [&str; 2] = if cfg!(debug_assertions) {
        ["debug", "release"]
    } else {
        ["release", "debug"]
    };
    for profile in profiles {
        let candidate = target_dir.join(profile).join("examples").join(&file_name);
        if candidate.is_file() {
            return candidate;
        }
    }
    panic!(
        "test_plugin artifact `{file_name}` not found under {target_dir:?} \
         (probed profiles: debug, release)"
    );
}

/// `(prefix, suffix)` for a cdylib artifact on the current platform.
fn platform_prefix_suffix() -> (&'static str, &'static str) {
    if cfg!(target_os = "windows") {
        ("", ".dll")
    } else if cfg!(target_os = "macos") {
        ("lib", ".dylib")
    } else {
        ("lib", ".so")
    }
}

fn plugin_benches(c: &mut Criterion) {
    let so = ensure_test_plugin_built();
    let so: &Path = &so;

    // Full non-RT lifecycle per iteration: dlopen → ABI verify → metadata →
    // instantiate → drop(node) → dlclose. Every stage that the 0.1.0 loader
    // exposes is inside the measured region.
    c.bench_function("plugin_load_full_lifecycle", |b| {
        b.iter(|| {
            let mut loader = PluginLoader::new();
            let id = loader.load(so).expect("plugin load");
            let _meta = loader.metadata(id).expect("metadata lookup");
            let node = loader.instantiate(id).expect("instantiate");
            drop(node);
            loader.unload(id).expect("unload");
        });
    });

    // Isolate the per-node allocation cost: `load` once (outside the measured
    // region), then repeatedly `instantiate` + drop. `unload` happens after
    // the measurement so the library stays mapped across iterations.
    c.bench_function("plugin_instantiate_only", |b| {
        let mut loader = PluginLoader::new();
        let id = loader.load(so).expect("plugin load for instantiate bench");
        b.iter(|| {
            let node = loader.instantiate(id).expect("instantiate");
            // Keep the allocation observable; the node is dropped at the `;`.
            black_box(node);
        });
        loader.unload(id).expect("cleanup unload");
    });
}

criterion_group!(benches, plugin_benches);
criterion_main!(benches);