use std::cell::Cell;
use std::collections::HashMap;
use std::io;
use std::sync::{Arc, Mutex, OnceLock};
use super::{Archive, ArchiveError};
use crate::{ar, tf};
#[derive(Clone)]
pub struct Package(Arc<Mutex<Archive>>);
impl Package {
pub fn with<T>(&self, read: impl FnOnce(&mut Archive) -> T) -> T {
read(&mut tf::lock(&self.0))
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error(transparent)]
pub struct PackageError(Arc<ArchiveError>);
impl PackageError {
pub fn archive_error(&self) -> &ArchiveError {
&self.0
}
}
impl From<ArchiveError> for PackageError {
fn from(error: ArchiveError) -> Self {
PackageError(Arc::new(error))
}
}
impl From<PackageError> for io::Error {
fn from(error: PackageError) -> Self {
io::Error::new(error.0.io_kind().unwrap_or(io::ErrorKind::Other), error)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PackageStats {
pub opens: u64,
pub directory_parses: u64,
pub cache_hits: u64,
}
#[derive(Default)]
struct PackageCache {
slots: Mutex<HashMap<u64, HashMap<String, PackageSlot>>>,
}
type PackageSlot = Arc<OnceLock<Result<Package, PackageError>>>;
pub fn open_package(resolver: &dyn ar::Resolver, package: &ar::ResolvedPath) -> Result<Package, PackageError> {
let path = package.to_string_lossy();
let open = || -> Result<Package, PackageError> {
count(|stats| {
if !ar::is_package_relative_path(&path) {
stats.opens += 1;
}
stats.directory_parses += 1;
});
let asset = resolver
.open_asset(package)
.map_err(|error| match error.downcast::<PackageError>() {
Ok(outer) => outer,
Err(error) => ArchiveError::from(error).into(),
})?;
Ok(Package(Arc::new(Mutex::new(Archive::from_asset(asset)?))))
};
let (Some(scope), Some(resolver_id)) = (ar::CacheScope::current(), resolver.cache_id()) else {
return open();
};
let cache = scope.cache::<PackageCache>();
let slot = {
let mut slots = tf::lock(&cache.slots);
let of_resolver = slots.entry(resolver_id).or_default();
match of_resolver.get(path.as_ref()) {
Some(slot) => Arc::clone(slot),
None => Arc::clone(of_resolver.entry(path.to_string()).or_default()),
}
};
if let Some(found) = slot.get() {
count(|stats| stats.cache_hits += 1);
return found.clone();
}
slot.get_or_init(open).clone()
}
pub fn package_contains(resolver: &dyn ar::Resolver, package: &str, inner: &str) -> bool {
let Ok(found) = open_package(resolver, &ar::ResolvedPath::new(package)) else {
return true;
};
let Some((nested, rest)) = ar::split_package_relative_path_outer(inner) else {
return found.with(|archive| archive.contains(inner));
};
found.with(|archive| archive.contains(&nested))
&& package_contains(resolver, &ar::nest_packaged_path(package, &nested), &rest)
}
pub fn package_entry(resolver: &dyn ar::Resolver, package: &str, inner: &str) -> Result<ar::AssetBuffer, PackageError> {
match ar::split_package_relative_path_outer(inner) {
Some((nested, rest)) => package_entry(resolver, &ar::nest_packaged_path(package, &nested), &rest),
None => Ok(open_package(resolver, &ar::ResolvedPath::new(package))?.with(|archive| archive.entry(inner))?),
}
}
pub fn package_stats() -> PackageStats {
STATS.get()
}
fn count(update: impl FnOnce(&mut PackageStats)) {
let mut stats = STATS.get();
update(&mut stats);
STATS.set(stats);
}
thread_local! {
static STATS: Cell<PackageStats> = const {
Cell::new(PackageStats {
opens: 0,
directory_parses: 0,
cache_hits: 0,
})
};
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io;
use std::path::Path;
use std::sync::Barrier;
use std::thread;
use super::*;
use crate::ar::Resolver as _;
use crate::usd;
use crate::usdz::tests::package;
fn canonical(path: &Path) -> String {
path.canonicalize().unwrap().to_string_lossy().into_owned()
}
fn packaged_dir() -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pkg.usdz");
fs::write(
&path,
package(&[("a.usda", b"#usda 1.0\n"), ("b.usda", b"#usda 1.0\n")]),
)
.unwrap();
let path = canonical(&path);
(dir, path)
}
fn nested_dir() -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let inner = package(&[("a.usda", b"#usda 1.0\n")]);
let path = dir.path().join("outer.usdz");
fs::write(&path, package(&[("root.usda", b"#usda 1.0\n"), ("inner.usdz", &inner)])).unwrap();
let path = canonical(&path);
(dir, path)
}
fn work() -> (u64, u64) {
let stats = package_stats();
(stats.opens, stats.directory_parses)
}
fn read_entry(resolver: &ar::DefaultResolver, package: &str, name: &str) -> io::Result<Vec<u8>> {
let path = ar::join_package_relative_path(package, name);
let resolved = resolver
.resolve(&path)
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?;
resolver.open_asset(&resolved)?.read_all()
}
#[test]
fn scope_opens_once() {
let (_dir, pkg) = packaged_dir();
let resolver = ar::DefaultResolver::new();
{
let _scope = ar::CacheScope::begin();
read_entry(&resolver, &pkg, "a.usda").unwrap();
read_entry(&resolver, &pkg, "b.usda").unwrap();
let missing = ar::join_package_relative_path(&pkg, "missing.usda");
assert!(resolver.resolve(&missing).is_none());
{
let _nested = ar::CacheScope::begin();
read_entry(&resolver, &pkg, "a.usda").unwrap();
}
assert_eq!(work(), (1, 1));
assert!(package_stats().cache_hits > 0);
}
let _sibling = ar::CacheScope::begin();
read_entry(&resolver, &pkg, "a.usda").unwrap();
assert_eq!(work(), (2, 2));
}
#[test]
fn no_scope_no_cache() {
let (_dir, pkg) = packaged_dir();
let resolver = ar::DefaultResolver::new();
let path = ar::join_package_relative_path(&pkg, "a.usda");
resolver.resolve(&path).unwrap();
resolver.resolve(&path).unwrap();
assert_eq!(work(), (2, 2));
}
#[test]
fn nested_levels_once() {
let (_dir, outer) = nested_dir();
let resolver = ar::DefaultResolver::new();
let _scope = ar::CacheScope::begin();
for _ in 0..2 {
assert_eq!(
read_entry(&resolver, &outer, "inner.usdz[a.usda]").unwrap(),
b"#usda 1.0\n"
);
}
read_entry(&resolver, &outer, "root.usda").unwrap();
assert_eq!(work(), (1, 2));
}
#[test]
fn nested_unscoped_opens_once() {
let (_dir, outer) = nested_dir();
let resolver = ar::DefaultResolver::new();
let nested = ar::join_package_relative_path(&outer, "inner.usdz[a.usda]");
let resolved = resolver.resolve(&nested).expect("the entry exists");
assert_eq!(work(), (1, 2), "the resolve");
resolver.open_asset(&resolved).unwrap();
assert_eq!(work(), (2, 4), "the open");
}
#[test]
fn bracketed_entry_name() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pkg.usdz");
fs::write(
&path,
package(&[("root.usda", b"#usda 1.0\n"), ("textures/tile[0].png", b"pixels")]),
)
.unwrap();
let resolver = ar::DefaultResolver::new();
assert_eq!(
read_entry(&resolver, &canonical(&path), "textures/tile[0].png").unwrap(),
b"pixels"
);
}
#[test]
fn concurrent_miss_one_open() {
let (_dir, pkg) = packaged_dir();
let resolver = ar::DefaultResolver::new();
let scope = ar::CacheScope::begin();
let handle = scope.handle();
let start = Barrier::new(8);
let opens: u64 = thread::scope(|threads| {
let workers: Vec<_> = (0..8)
.map(|_| {
threads.spawn(|| {
let _shared = ar::CacheScope::begin_shared(&handle);
start.wait();
read_entry(&resolver, &pkg, "a.usda").unwrap();
package_stats().opens
})
})
.collect();
workers.into_iter().map(|worker| worker.join().unwrap()).sum()
});
assert_eq!(opens, 1);
}
#[test]
fn cache_keyed_by_resolver() {
let (_dir, pkg) = packaged_dir();
let (first, second) = (ar::DefaultResolver::new(), ar::DefaultResolver::new());
let _scope = ar::CacheScope::begin();
read_entry(&first, &pkg, "a.usda").unwrap();
read_entry(&first, &pkg, "b.usda").unwrap();
assert_eq!(work(), (1, 1));
read_entry(&second, &pkg, "a.usda").unwrap();
assert_eq!(work(), (2, 2));
}
#[test]
fn unnamed_resolver_not_cached() {
let (_dir, pkg) = packaged_dir();
let bytes = fs::read(&pkg).unwrap();
let resolver = ar::tests::TestResolver(move || -> io::Result<Box<dyn ar::Asset>> {
Ok(Box::new(io::Cursor::new(bytes.clone())))
});
let _scope = ar::CacheScope::begin();
let package = ar::ResolvedPath::new(&pkg);
for _ in 0..2 {
assert!(
open_package(&resolver, &package)
.unwrap()
.with(|archive| archive.contains("a.usda"))
);
}
assert_eq!(work(), (2, 2));
}
#[test]
fn nested_failure_keeps_kind() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("outer.usdz");
fs::write(&path, b"not a package").unwrap();
let nested = ar::ResolvedPath::new(ar::join_package_relative_path(&path.to_string_lossy(), "inner.usdz"));
let error = open_package(&ar::DefaultResolver::new(), &nested)
.err()
.expect("an unreadable package");
assert!(
!matches!(error.archive_error(), ArchiveError::Io(_)),
"{:?}",
error.archive_error()
);
}
#[test]
fn replaced_between_scopes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pkg.usdz");
fs::write(&path, package(&[("a.usda", b"#usda 1.0\n# old\n")])).unwrap();
let pkg = canonical(&path);
let resolver = ar::DefaultResolver::new();
{
let _scope = ar::CacheScope::begin();
assert_eq!(read_entry(&resolver, &pkg, "a.usda").unwrap(), b"#usda 1.0\n# old\n");
}
fs::write(&path, package(&[("a.usda", b"#usda 1.0\n# new\n")])).unwrap();
let _scope = ar::CacheScope::begin();
assert_eq!(read_entry(&resolver, &pkg, "a.usda").unwrap(), b"#usda 1.0\n# new\n");
}
#[test]
fn failed_open_keeps_error() {
let dir = tempfile::tempdir().unwrap();
let corrupt = dir.path().join("corrupt.usdz");
fs::write(&corrupt, b"not a package").unwrap();
let missing = dir.path().join("missing.usdz");
let mut attempts = 0;
for (package, kind) in [(corrupt, io::ErrorKind::Other), (missing, io::ErrorKind::NotFound)] {
let resolver = ar::DefaultResolver::new();
let entry = ar::ResolvedPath::new(ar::join_package_relative_path(&package.to_string_lossy(), "a.usda"));
let failure = |resolver: &ar::DefaultResolver| {
let error = resolver.open_asset(&entry).err().expect("an unreadable package");
(error.kind(), error.to_string())
};
let hits = package_stats().cache_hits;
{
let _scope = ar::CacheScope::begin();
let first = failure(&resolver);
assert_eq!(first.0, kind, "{}", first.1);
assert!(!first.1.is_empty());
assert_eq!(failure(&resolver), first);
assert_eq!(package_stats().cache_hits, hits + 1, "one attempt serves both");
}
let _scope = ar::CacheScope::begin();
failure(&resolver);
assert_eq!(package_stats().cache_hits, hits + 1, "the next scope tries again");
attempts += 1;
}
assert_eq!(attempts, 2);
}
#[test]
fn stage_opens_package_once() -> crate::Result<()> {
let dir = tempfile::tempdir().unwrap();
let root = b"#usda 1.0\ndef \"A\" (references = @./b.usda@</B>) {}\n";
let leaf = b"#usda 1.0\ndef \"B\" { def \"Child\" {} }\n";
let path = dir.path().join("pkg.usdz");
fs::write(&path, package(&[("root.usda", root), ("b.usda", leaf)])).unwrap();
let path = path.to_string_lossy().into_owned();
let stage = usd::Stage::open(&path)?;
assert_eq!(work(), (1, 1));
drop(stage);
let _scope = ar::CacheScope::begin();
let stage = usd::Stage::open(&path)?;
assert!(stage.prim("/A/Child")?.is_valid()?);
assert_eq!(work(), (2, 2), "one more open for the second stage and its traversal");
Ok(())
}
#[test]
fn stage_opens_nested_once() -> crate::Result<()> {
let dir = tempfile::tempdir().unwrap();
let inner = package(&[("b.usda", b"#usda 1.0\ndef \"B\" { def \"Child\" {} }\n")]);
let root = b"#usda 1.0\ndef \"A\" (references = @./inner.usdz@</B>) {}\n";
let path = dir.path().join("pkg.usdz");
fs::write(&path, package(&[("root.usda", root), ("inner.usdz", &inner)])).unwrap();
let path = path.to_string_lossy().into_owned();
let _scope = ar::CacheScope::begin();
let stage = usd::Stage::open(&path)?;
assert!(stage.prim("/A/Child")?.is_valid()?);
assert_eq!(work(), (1, 2));
Ok(())
}
}