use std::alloc::{GlobalAlloc, Layout, System};
#[cfg(not(feature = "mmap"))]
use std::io;
use std::mem;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use std::{env, fs};
use openusd::usd::{PrimPredicate, Stage, TimeCode};
use openusd::{Error, Result, ar, sdf, usdz};
fn main() -> Result<()> {
let Some(args) = Args::parse() else {
eprintln!(
"usage: open_bench [--time <t>] [--save] [--no-arrays] [--proxies | --all] [--mmap] \
[--until <phase>] <root.usd[a|c|z]>"
);
process::exit(2);
};
let copy = if args.save {
Some(copy_to_temp(&args.root)?)
} else {
None
};
let root = copy.as_deref().unwrap_or(&args.root).to_str().expect("a UTF-8 path");
let resolver = resolver(args.mmap)?;
let phase = Phase::start("open");
let stage = Stage::builder().resolver(resolver).open(root)?;
phase.end("");
let mut prims = Vec::new();
if args.runs("metadata") {
let phase = Phase::start("metadata");
let _scope = ar::CacheScope::begin();
stage.traverse(args.predicate, |path| prims.push(path.clone()))?;
for path in &prims {
stage.prim(path.clone())?.type_name()?;
}
phase.end(&format!("prims: {}", prims.len()));
}
if !args.no_arrays && args.runs("arrays") {
let phase = Phase::start("arrays");
let _scope = ar::CacheScope::begin();
let time = args.time.map(TimeCode::new);
let (mut arrays, mut decoded) = (0usize, 0usize);
for path in &prims {
for attribute in stage.prim(path.clone())?.attributes()? {
if let Some(value) = attribute.get_at::<sdf::Value>(time)?
&& let Some(bytes) = array_bytes(&value)
{
arrays += 1;
decoded += bytes;
}
}
}
phase.end(&format!("arrays: {arrays} decoded: {}", mib(decoded)));
}
if args.save && args.runs("save") {
let phase = Phase::start("save");
let prim = prims.first().cloned().unwrap_or_else(sdf::Path::abs_root);
stage.edit(|edit| -> Result<()> {
let path = prim.append_property("openBench").map_err(Error::from)?;
edit.attribute_builder(path, "int").set(1).build()?;
Ok(())
})?;
let identifier = stage.root_layer().identifier().to_owned();
stage.layer_mut(&identifier).expect("the root layer is live").save()?;
phase.end("");
}
eprintln!("heap peak: {}", mib(PEAK.load(Ordering::Relaxed)));
if let Some(memory) = Memory::read() {
eprintln!("working set peak: {}", mib(memory.peak_working_set));
}
let packages = usdz::package_stats();
eprintln!(
"packages: opens {} directory parses {} cache hits {}",
packages.opens, packages.directory_parses, packages.cache_hits,
);
drop(stage);
if let Some(copy) = copy {
fs::remove_file(copy)?;
}
Ok(())
}
struct Args {
root: PathBuf,
time: Option<f64>,
save: bool,
no_arrays: bool,
predicate: PrimPredicate,
mmap: bool,
until: Option<String>,
}
impl Args {
fn parse() -> Option<Self> {
let mut args = env::args().skip(1);
let mut parsed = Args {
root: PathBuf::new(),
time: None,
save: false,
no_arrays: false,
predicate: PrimPredicate::DEFAULT,
mmap: false,
until: None,
};
while let Some(arg) = args.next() {
match arg.as_str() {
"--time" => parsed.time = Some(args.next()?.parse().ok()?),
"--save" => parsed.save = true,
"--no-arrays" => parsed.no_arrays = true,
"--proxies" => parsed.predicate = PrimPredicate::DEFAULT_PROXIES,
"--all" => parsed.predicate = PrimPredicate::ALL,
"--mmap" => parsed.mmap = true,
"--until" => parsed.until = Some(args.next().filter(|phase| PHASES.contains(&phase.as_str()))?),
_ => parsed.root = PathBuf::from(arg),
}
}
(!parsed.root.as_os_str().is_empty()).then_some(parsed)
}
fn runs(&self, phase: &str) -> bool {
let position = |name: &str| PHASES.iter().position(|known| *known == name);
match &self.until {
Some(until) => position(phase) <= position(until),
None => true,
}
}
}
const PHASES: [&str; 4] = ["open", "metadata", "arrays", "save"];
#[cfg(feature = "mmap")]
#[allow(unsafe_code)]
fn resolver(mmap: bool) -> Result<ar::DefaultResolver> {
if !mmap {
return Ok(ar::DefaultResolver::new());
}
Ok(unsafe { ar::DefaultResolver::new().map_files() })
}
#[cfg(not(feature = "mmap"))]
fn resolver(mmap: bool) -> Result<ar::DefaultResolver> {
if mmap {
return Err(io::Error::other("--mmap needs the mmap feature").into());
}
Ok(ar::DefaultResolver::new())
}
fn copy_to_temp(root: &Path) -> Result<PathBuf> {
let name = root.file_name().expect("a root with a file name");
let copy = env::temp_dir().join(format!("open_bench-{}-{}", process::id(), name.to_string_lossy()));
fs::copy(root, ©)?;
#[cfg(windows)]
#[allow(clippy::permissions_set_readonly_false)]
{
let mut permissions = fs::metadata(©)?.permissions();
permissions.set_readonly(false);
fs::set_permissions(©, permissions)?;
}
Ok(copy)
}
struct Phase {
name: &'static str,
start: Instant,
live: usize,
memory: Option<Memory>,
}
impl Phase {
fn start(name: &'static str) -> Self {
let live = LIVE.load(Ordering::Relaxed);
PHASE_PEAK.store(live, Ordering::Relaxed);
Phase {
name,
memory: Memory::read(),
start: Instant::now(),
live,
}
}
fn end(self, detail: &str) {
let elapsed = self.start.elapsed().as_secs_f64();
let live = LIVE.load(Ordering::Relaxed);
let peak = PHASE_PEAK.load(Ordering::Relaxed).saturating_sub(self.live);
let (sign, retained) = if live >= self.live {
("+", live - self.live)
} else {
("-", self.live - live)
};
let process = match (self.memory, Memory::read()) {
(Some(before), Some(after)) => {
let hard = match (before.hard_faults, after.hard_faults) {
(Some(before), Some(after)) => format!(" (hard +{})", after - before),
_ => String::new(),
};
format!(
"ws {:>10} faults +{}{hard} ",
mib(after.working_set),
after.faults - before.faults
)
}
_ => String::new(),
};
eprintln!(
"{:<9} {elapsed:>8.3}s retained {sign}{:>10} peak {:>10} {process}{detail}",
self.name,
mib(retained),
mib(peak),
);
}
}
#[derive(Clone, Copy)]
struct Memory {
working_set: usize,
peak_working_set: usize,
faults: u64,
hard_faults: Option<u64>,
}
impl Memory {
#[cfg(windows)]
#[allow(unsafe_code)]
fn read() -> Option<Self> {
let mut counters = win32::ProcessMemoryCounters::default();
let size = mem::size_of::<win32::ProcessMemoryCounters>() as u32;
let read = unsafe { win32::K32GetProcessMemoryInfo(win32::GetCurrentProcess(), &raw mut counters, size) };
(read != 0).then_some(Memory {
working_set: counters.working_set_size,
peak_working_set: counters.peak_working_set_size,
faults: u64::from(counters.page_fault_count),
hard_faults: None,
})
}
#[cfg(target_os = "linux")]
fn read() -> Option<Self> {
let stat = fs::read_to_string("/proc/self/stat").ok()?;
let mut fields = stat.rsplit_once(')')?.1.split_whitespace();
let faults = fields.nth(7)?.parse().ok()?;
let hard_faults = fields.nth(1)?.parse().ok()?;
let status = fs::read_to_string("/proc/self/status").ok()?;
let kib = |key: &str| -> Option<usize> {
let line = status.lines().find(|line| line.starts_with(key))?;
line.split_whitespace().nth(1)?.parse().ok()
};
Some(Memory {
working_set: kib("VmRSS:")? * 1024,
peak_working_set: kib("VmHWM:")? * 1024,
faults,
hard_faults: Some(hard_faults),
})
}
#[cfg(not(any(windows, target_os = "linux")))]
fn read() -> Option<Self> {
None
}
}
#[cfg(windows)]
#[allow(unsafe_code)]
mod win32 {
use std::ffi::c_void;
#[repr(C)]
#[derive(Default)]
pub struct ProcessMemoryCounters {
pub cb: u32,
pub page_fault_count: u32,
pub peak_working_set_size: usize,
pub working_set_size: usize,
pub quota_peak_paged_pool_usage: usize,
pub quota_paged_pool_usage: usize,
pub quota_peak_non_paged_pool_usage: usize,
pub quota_non_paged_pool_usage: usize,
pub pagefile_usage: usize,
pub peak_pagefile_usage: usize,
}
#[link(name = "kernel32")]
unsafe extern "system" {
pub fn GetCurrentProcess() -> *mut c_void;
pub fn K32GetProcessMemoryInfo(process: *mut c_void, counters: *mut ProcessMemoryCounters, size: u32) -> i32;
}
}
fn mib(bytes: usize) -> String {
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
}
fn array_bytes(value: &sdf::Value) -> Option<usize> {
use sdf::Value as V;
let bytes = match value {
V::BoolVec(v) => mem::size_of_val(v.as_slice()),
V::UcharVec(v) => mem::size_of_val(v.as_slice()),
V::IntVec(v) => mem::size_of_val(v.as_slice()),
V::UintVec(v) => mem::size_of_val(v.as_slice()),
V::Int64Vec(v) => mem::size_of_val(v.as_slice()),
V::Uint64Vec(v) => mem::size_of_val(v.as_slice()),
V::HalfVec(v) => mem::size_of_val(v.as_slice()),
V::FloatVec(v) => mem::size_of_val(v.as_slice()),
V::DoubleVec(v) => mem::size_of_val(v.as_slice()),
V::StringVec(v) => mem::size_of_val(v.as_slice()),
V::TokenVec(v) => mem::size_of_val(v.as_slice()),
V::AssetPathVec(v) => mem::size_of_val(v.as_slice()),
V::QuathVec(v) => mem::size_of_val(v.as_slice()),
V::QuatfVec(v) => mem::size_of_val(v.as_slice()),
V::QuatdVec(v) => mem::size_of_val(v.as_slice()),
V::Vec2hVec(v) => mem::size_of_val(v.as_slice()),
V::Vec2fVec(v) => mem::size_of_val(v.as_slice()),
V::Vec2dVec(v) => mem::size_of_val(v.as_slice()),
V::Vec2iVec(v) => mem::size_of_val(v.as_slice()),
V::Vec3hVec(v) => mem::size_of_val(v.as_slice()),
V::Vec3fVec(v) => mem::size_of_val(v.as_slice()),
V::Vec3dVec(v) => mem::size_of_val(v.as_slice()),
V::Vec3iVec(v) => mem::size_of_val(v.as_slice()),
V::Vec4hVec(v) => mem::size_of_val(v.as_slice()),
V::Vec4fVec(v) => mem::size_of_val(v.as_slice()),
V::Vec4dVec(v) => mem::size_of_val(v.as_slice()),
V::Vec4iVec(v) => mem::size_of_val(v.as_slice()),
V::Matrix2dVec(v) => mem::size_of_val(v.as_slice()),
V::Matrix3dVec(v) => mem::size_of_val(v.as_slice()),
V::Matrix4dVec(v) => mem::size_of_val(v.as_slice()),
V::PathVec(v) => mem::size_of_val(v.as_slice()),
V::LayerOffsetVec(v) => mem::size_of_val(v.as_slice()),
V::ValueVec(v) => mem::size_of_val(v.as_slice()),
V::TimeCodeVec(v) => mem::size_of_val(v.as_slice()),
V::PathExpressionVec(v) => mem::size_of_val(v.as_slice()),
_ => return None,
};
Some(bytes)
}
struct Tracking;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
static PHASE_PEAK: AtomicUsize = AtomicUsize::new(0);
impl Tracking {
fn add(delta: usize) {
let live = LIVE.fetch_add(delta, Ordering::Relaxed) + delta;
for mark in [&PEAK, &PHASE_PEAK] {
if live > mark.load(Ordering::Relaxed) {
mark.fetch_max(live, Ordering::Relaxed);
}
}
}
}
#[allow(unsafe_code)]
unsafe impl GlobalAlloc for Tracking {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
Tracking::add(layout.size());
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) };
LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
if !new_ptr.is_null() {
if new_size >= layout.size() {
Tracking::add(new_size - layout.size());
} else {
LIVE.fetch_sub(layout.size() - new_size, Ordering::Relaxed);
}
}
new_ptr
}
}
#[global_allocator]
static ALLOCATOR: Tracking = Tracking;