Chunked reads beat an h5py process pool; unlimited writer B-trees; Blosc2; 599/697 conformance #16

Merged
osobh merged 48 commits from feat/p2b-scale into main 2026-09-26 17:42:16 +00:00
2 changed files with 81 additions and 12 deletions
Showing only changes of commit a6ed3a5c7d - Show all commits
+1 -1
View File
@@ -731,7 +731,7 @@ fn main() {
let view = match ext.and_then(|x| x.cache_image) { let view = match ext.and_then(|x| x.cache_image) {
None => None, None => None,
Some(loc) => match guarded(|| { Some(loc) => match guarded(|| {
superblock_ext::apply_cache_image(hdf5, loc, sb.offset_size, sb.length_size) superblock_ext::apply_cache_image(hdf5, loc, &sb)
.map_err(e) .map_err(e)
}) { }) {
Ok(v) => Some(v), Ok(v) => Some(v),
+80 -11
View File
@@ -297,16 +297,16 @@ struct ImageEntry {
/// (`H5C__decode_cache_image_header`, `H5C__reconstruct_cache_entry`): /// (`H5C__decode_cache_image_header`, `H5C__reconstruct_cache_entry`):
/// signature and version, the image length it records, entry types, rings /// signature and version, the image length it records, entry types, rings
/// and ages in range, entry addresses inside the file and not repeated, /// and ages in range, entry addresses inside the file and not repeated,
/// flush-dependency parents that are earlier entries. /// flush-dependency parents already in the cache.
/// ///
/// libhdf5 does not verify the block's trailing checksum when it loads an /// libhdf5 does not verify the block's trailing checksum when it loads an
/// image, so neither does this. /// image, so neither does this.
pub fn apply_cache_image( pub fn apply_cache_image(
data: &[u8], data: &[u8],
location: CacheImageLocation, location: CacheImageLocation,
offset_size: u8, sb: &Superblock,
length_size: u8,
) -> Result<Vec<u8>, FormatError> { ) -> Result<Vec<u8>, FormatError> {
let (offset_size, length_size) = (sb.offset_size, sb.length_size);
let bad = FormatError::InvalidCacheImage; let bad = FormatError::InvalidCacheImage;
let start = usize::try_from(location.address).map_err(|_| bad("address out of range"))?; let start = usize::try_from(location.address).map_err(|_| bad("address out of range"))?;
let len = usize::try_from(location.length).map_err(|_| bad("length out of range"))?; let len = usize::try_from(location.length).map_err(|_| bad("length out of range"))?;
@@ -336,6 +336,17 @@ pub fn apply_cache_image(
} }
let mut entries = Vec::new(); let mut entries = Vec::new();
// What is in libhdf5's cache when it loads the image: the superblock
// and the superblock extension's object header (read to find the image).
// Each entry's flush-dependency parents are looked up in the cache as
// the entry is inserted (`H5C__reconstruct_cache_contents` searches the
// index inside the loop that inserts the entries, in HDF5 1.14.6 and
// 2.0.0 alike), so a parent must be one of those or an earlier entry.
let mut cached = BTreeSet::new();
cached.insert(0);
if let Some(ext) = sb.superblock_extension_address {
cached.insert(ext);
}
let mut seen = BTreeSet::new(); let mut seen = BTreeSet::new();
for _ in 0..n_entries { for _ in 0..n_entries {
let type_id = c.u8()?; let type_id = c.u8()?;
@@ -374,8 +385,8 @@ pub fn apply_cache_image(
let parent = c let parent = c
.addr(offset_size)? .addr(offset_size)?
.ok_or(bad("invalid flush dependency parent offset"))?; .ok_or(bad("invalid flush dependency parent offset"))?;
if !seen.contains(&parent) { if !seen.contains(&parent) && !cached.contains(&parent) {
return Err(bad("flush dependency parent not in the image")); return Err(bad("fd parent not in cache"));
} }
} }
let len = usize::try_from(size).map_err(|_| bad(RAN_OFF))?; let len = usize::try_from(size).map_err(|_| bad(RAN_OFF))?;
@@ -415,7 +426,7 @@ pub fn metadata_view(data: &[u8], sb: &Superblock) -> Result<Option<Vec<u8>>, Fo
Some(SuperblockExtension { Some(SuperblockExtension {
cache_image: Some(location), cache_image: Some(location),
.. ..
}) => apply_cache_image(data, location, sb.offset_size, sb.length_size).map(Some), }) => apply_cache_image(data, location, sb).map(Some),
_ => Ok(None), _ => Ok(None),
} }
} }
@@ -562,18 +573,37 @@ mod tests {
/// A cache image block with `entries` of (address, bytes). /// A cache image block with `entries` of (address, bytes).
fn image(entries: &[(u64, &[u8])]) -> Vec<u8> { fn image(entries: &[(u64, &[u8])]) -> Vec<u8> {
let with_deps: Vec<_> = entries.iter().map(|&(a, b)| (a, b, 0, None)).collect();
image_with_deps(&with_deps)
}
/// A cache image block with `entries` of (address, bytes, flush
/// dependency children, flush dependency parent).
fn image_with_deps(entries: &[(u64, &[u8], u16, Option<u64>)]) -> Vec<u8> {
let mut b = Vec::new(); let mut b = Vec::new();
b.extend_from_slice(MDCI_SIGNATURE); b.extend_from_slice(MDCI_SIGNATURE);
b.push(0); b.push(0);
b.push(0); b.push(0);
b.extend_from_slice(&0u64.to_le_bytes()); // length, patched below b.extend_from_slice(&0u64.to_le_bytes()); // length, patched below
b.extend_from_slice(&(entries.len() as u32).to_le_bytes()); b.extend_from_slice(&(entries.len() as u32).to_le_bytes());
for (addr, bytes) in entries { for &(addr, bytes, children, parent) in entries {
b.extend_from_slice(&[5, 0x02, 1, 0]); // type, flags (in LRU), ring, age let mut flags = 0x02; // in LRU
b.extend_from_slice(&[0; 6]); // children, dirty children, parents if children > 0 {
flags |= MDCI_ENTRY_IS_FD_PARENT;
}
if parent.is_some() {
flags |= MDCI_ENTRY_IS_FD_CHILD;
}
b.extend_from_slice(&[5, flags, 1, 0]); // type, flags, ring, age
b.extend_from_slice(&children.to_le_bytes());
b.extend_from_slice(&0u16.to_le_bytes()); // dirty children
b.extend_from_slice(&u16::from(parent.is_some()).to_le_bytes());
b.extend_from_slice(&0i32.to_le_bytes()); b.extend_from_slice(&0i32.to_le_bytes());
b.extend_from_slice(&addr.to_le_bytes()); b.extend_from_slice(&addr.to_le_bytes());
b.extend_from_slice(&(bytes.len() as u64).to_le_bytes()); b.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
if let Some(p) = parent {
b.extend_from_slice(&p.to_le_bytes());
}
b.extend_from_slice(bytes); b.extend_from_slice(bytes);
} }
b.extend_from_slice(&[0; 4]); // checksum (not verified, as in libhdf5) b.extend_from_slice(&[0; 4]); // checksum (not verified, as in libhdf5)
@@ -592,7 +622,7 @@ mod tests {
address: at, address: at,
length: img.len() as u64, length: img.len() as u64,
}; };
let out = apply_cache_image(&f, loc, 8, 8).unwrap(); let out = apply_cache_image(&f, loc, &sb_v2(u64::MAX)).unwrap();
assert_eq!(out.len(), f.len()); assert_eq!(out.len(), f.len());
assert_eq!(&out[16..22], b"HEADER"); assert_eq!(&out[16..22], b"HEADER");
assert_eq!(&out[40..44], b"NODE"); assert_eq!(&out[40..44], b"NODE");
@@ -605,7 +635,7 @@ mod tests {
address: 64, address: 64,
length: img.len() as u64, length: img.len() as u64,
}; };
apply_cache_image(&f, loc, 8, 8).unwrap_err() apply_cache_image(&f, loc, &sb_v2(u64::MAX)).unwrap_err()
}; };
let mut sig = image(&[(16, b"x")]); let mut sig = image(&[(16, b"x")]);
sig[0] = b'X'; sig[0] = b'X';
@@ -627,4 +657,43 @@ mod tests {
cut[6..14].copy_from_slice(&n.to_le_bytes()); cut[6..14].copy_from_slice(&n.to_le_bytes());
assert!(matches!(bad(cut), FormatError::InvalidCacheImage(_))); assert!(matches!(bad(cut), FormatError::InvalidCacheImage(_)));
} }
/// libhdf5 resolves an entry's flush-dependency parents as it inserts
/// the entry (`H5C__reconstruct_cache_contents`): a parent must be an
/// earlier entry, or the superblock or its extension's object header,
/// which are cached before the image loads. A parent listed after its
/// child fails ("fd parent not in cache?!?").
#[test]
fn flush_dependency_parents_must_already_be_cached() {
let load = |img: Vec<u8>| {
let mut f = vec![0u8; 64];
f.extend_from_slice(&img);
let loc = CacheImageLocation {
address: 64,
length: img.len() as u64,
};
apply_cache_image(&f, loc, &sb_v2(48))
};
// Parent first, as libhdf5 writes images.
assert!(
load(image_with_deps(&[
(16, b"P", 1, None),
(40, b"C", 0, Some(16))
]))
.is_ok()
);
// Child first: libhdf5 does not find the parent.
assert_eq!(
load(image_with_deps(&[
(40, b"C", 0, Some(16)),
(16, b"P", 1, None)
]))
.unwrap_err(),
FormatError::InvalidCacheImage("fd parent not in cache")
);
// The superblock extension's header (at 48 here) is in the cache.
assert!(load(image_with_deps(&[(40, b"C", 0, Some(48))])).is_ok());
// An entry cannot be its own parent.
assert!(load(image_with_deps(&[(40, b"C", 1, Some(40))])).is_err());
}
} }