#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use core::hint::black_box;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use polyplug::Runtime;
use polyplug_abi::{
AbiError, AbiErrorCode, DispatchType, GuestContractHandle, GuestContractInstance,
GuestContractInterface,
};
use polyplug_utils::{BundleId, GuestContractId};
#[path = "common/mod.rs"]
mod common;
use common::TestNativeLoader;
const TEST_PLUGIN_DIR: &str = env!("TEST_PLUGIN_DIR");
const TEST_BUNDLE_NAME: &str = "test_plugin";
const DEFAULT_SOAK_ITERS: u64 = 8;
const DEFAULT_SAMPLE_EVERY: u64 = 1;
const DISPATCHES_PER_CYCLE: u32 = 4;
type NativeDispatchFn =
unsafe extern "C" fn(GuestContractInstance, *const (), *mut (), *mut AbiError);
#[repr(C)]
struct AddArgs {
a: u32,
b: u32,
}
fn current_rss_kib() -> Option<u64> {
let status: String = std::fs::read_to_string("/proc/self/status").ok()?;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
return rest.split_whitespace().next()?.parse::<u64>().ok();
}
}
None
}
fn env_u64(name: &str, default: u64) -> u64 {
match std::env::var(name) {
Ok(raw) => raw.trim().parse::<u64>().unwrap_or(default).max(1),
Err(_) => default,
}
}
fn resolve_add_dispatch(runtime: &Runtime) -> NativeDispatchFn {
let contract_id: u64 = GuestContractId::new("test.add", 1).id();
let handle: GuestContractHandle = runtime
.find_guest_contract(contract_id, 0)
.expect("test.add must be registered after load");
let interface_ptr: *const GuestContractInterface = runtime
.resolve_guest_contract(handle)
.expect("handle must resolve");
let interface: &GuestContractInterface = unsafe { &*interface_ptr };
assert_eq!(
interface.dispatch_type,
DispatchType::Native,
"test_plugin is a native bundle"
);
let fn_ptr: *const () = unsafe { *interface.dispatch.native.functions.add(0) };
unsafe { core::mem::transmute::<*const (), NativeDispatchFn>(fn_ptr) }
}
fn run_one_cycle() {
let runtime: Arc<Runtime> = Runtime::builder()
.loader(TestNativeLoader::new())
.build()
.expect("runtime build must succeed");
runtime
.load_bundle(Path::new(TEST_PLUGIN_DIR))
.expect("load_bundle must succeed");
let dispatch_fn: NativeDispatchFn = resolve_add_dispatch(&runtime);
for i in 0..DISPATCHES_PER_CYCLE {
let args: AddArgs = AddArgs { a: i, b: 1 };
let mut out: u32 = u32::MAX;
let mut rc: AbiError = AbiError::ok();
unsafe {
dispatch_fn(
GuestContractInstance::null(),
&args as *const AddArgs as *const (),
&mut out as *mut u32 as *mut (),
&mut rc as *mut AbiError,
)
};
assert_eq!(rc.code, AbiErrorCode::Ok as u32, "dispatch must succeed");
assert_eq!(out, i.wrapping_add(1), "add(i, 1) must equal i + 1");
black_box(out);
}
runtime
.unload_bundle(BundleId::new(TEST_BUNDLE_NAME))
.expect("unload_bundle must succeed");
drop(runtime);
}
#[test]
fn soak_load_unload_churn() {
let iters: u64 = env_u64("POLYPLUG_SOAK_ITERS", DEFAULT_SOAK_ITERS);
let sample_every: u64 = env_u64("POLYPLUG_SOAK_SAMPLE_EVERY", DEFAULT_SAMPLE_EVERY);
let out_path: Option<String> = std::env::var("POLYPLUG_SOAK_OUT").ok();
let mut series: Vec<(u64, u64)> = Vec::new();
if let Some(rss) = current_rss_kib() {
series.push((0, rss));
}
let start: Instant = Instant::now();
for cycle in 1..=iters {
run_one_cycle();
if cycle % sample_every == 0 {
if let Some(rss) = current_rss_kib() {
series.push((cycle, rss));
}
}
}
let elapsed: core::time::Duration = start.elapsed();
let cycles_per_sec: f64 = iters as f64 / elapsed.as_secs_f64();
println!("[soak] cycles={iters} elapsed={elapsed:?} churn={cycles_per_sec:.0} cycles/sec");
if let Some(rss) = current_rss_kib() {
println!("[soak] final RSS = {rss} KiB");
}
if !series.is_empty() {
let first: (u64, u64) = series[0];
let mid: (u64, u64) = series[series.len() / 2];
let last: (u64, u64) = series[series.len() - 1];
println!(
"[soak] RSS series (KiB): first=({},{}) mid=({},{}) last=({},{}) samples={}",
first.0,
first.1,
mid.0,
mid.1,
last.0,
last.1,
series.len()
);
let q: usize = (series.len() / 4).max(1);
let mid_slice: &[(u64, u64)] = &series[q..(2 * q).min(series.len())];
let tail_slice: &[(u64, u64)] = &series[series.len().saturating_sub(q)..];
let mean = |s: &[(u64, u64)]| -> f64 {
if s.is_empty() {
0.0
} else {
s.iter().map(|(_, r)| *r as f64).sum::<f64>() / s.len() as f64
}
};
let mid_mean: f64 = mean(mid_slice);
let tail_mean: f64 = mean(tail_slice);
let growth_pct: f64 = if mid_mean > 0.0 {
(tail_mean - mid_mean) / mid_mean * 100.0
} else {
0.0
};
println!(
"[soak] steady-state: mid_mean={mid_mean:.0} KiB tail_mean={tail_mean:.0} KiB drift={growth_pct:+.1}%"
);
}
if let Some(path) = out_path {
if let Some(parent) = Path::new(&path).parent() {
let _ = std::fs::create_dir_all(parent);
}
let mut body: String =
String::from("# cycle rss_kib — polyplug load/unload soak (RSS over time)\n");
for (cycle, rss) in &series {
body.push_str(&format!("{cycle} {rss}\n"));
}
std::fs::write(&path, body).expect("write soak RSS series");
println!("[soak] wrote RSS series to {path}");
}
}