From 052098bf36940b3eacc4e9b3742133cec75d2342 Mon Sep 17 00:00:00 2001 From: osobh Date: Sat, 26 Sep 2026 14:22:37 -0500 Subject: [PATCH] format: monomorphise the Storage parsers so local files stay as fast Every `*_in` core and the read helpers take `file: &S` with `S: Storage + ?Sized` instead of `&dyn Storage`, and the `&[u8]` wrappers pass the slice itself, so they compile to a `[u8]` instance: `as_contiguous()` inlines to `Some(self)` and each structure read is the slice code's bounds check again, with no indirect call. `&dyn Storage` still works (`S = dyn Storage`); there is one parser implementation. Also, so the structure reads cost no more than the slice checks did: - ObjectHeader::parse_in reads the prefix once (signature included) instead of the signature and then the prefix: two reads for a one-chunk header instead of three on a range backend; - the symbol-table node and group B-tree (v1) loops walk their entries with chunks_exact over the bytes read, and the node's redundant second bounds check is gone (the entries' read is the check, same error); - a version-1 header's message list is sized from its (capped) count. Same results and errors; the unit and equivalence tests are unchanged. New Criterion bench `clawhdf5/benches/local_metadata_bench.rs` over a 400-group version-1 file written by h5py (new fixture `v1_groups_400.h5`): ObjectHeader::parse, symbol-table nodes, the group B-tree walk and a facade listing, using only APIs that exist at f2ff2c4 so it builds there for an A/B. Provisional A/B against f2ff2c4 (busy machine, not for docs): both builds linked into one binary and timed in alternation, 200 rounds; median ratio new/old: facade listing -0.5% to -3.5% (was +14%), ObjectHeader::parse +1% to +2% (was +25%), symbol-table nodes -18%, group B-tree walk -18%, local-heap names and resolve_group_children within +-1.5%. An old-vs-old-copy run shows +-2% from code layout alone. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/clawhdf5-format/src/attribute.rs | 44 ++++----- crates/clawhdf5-format/src/btree_v1.rs | 40 ++++---- crates/clawhdf5-format/src/data_layout.rs | 6 +- .../clawhdf5-format/src/extensible_array.rs | 18 ++-- crates/clawhdf5-format/src/fixed_array.rs | 10 +- crates/clawhdf5-format/src/fractal_heap.rs | 34 ++++--- crates/clawhdf5-format/src/global_heap.rs | 16 +-- crates/clawhdf5-format/src/local_heap.rs | 18 ++-- crates/clawhdf5-format/src/object_header.rs | 75 +++++++-------- crates/clawhdf5-format/src/shared_message.rs | 48 ++++----- crates/clawhdf5-format/src/signature.rs | 2 +- crates/clawhdf5-format/src/storage.rs | 37 ++++--- crates/clawhdf5-format/src/superblock.rs | 13 ++- crates/clawhdf5-format/src/superblock_ext.rs | 27 +++--- crates/clawhdf5-format/src/symbol_table.rs | 51 +++------- .../tests/fixtures/v1_groups_400.h5 | Bin 0 -> 355840 bytes crates/clawhdf5/Cargo.toml | 4 + .../clawhdf5/benches/local_metadata_bench.rs | 91 ++++++++++++++++++ 18 files changed, 315 insertions(+), 219 deletions(-) create mode 100644 crates/clawhdf5-format/tests/fixtures/v1_groups_400.h5 create mode 100644 crates/clawhdf5/benches/local_metadata_bench.rs diff --git a/crates/clawhdf5-format/src/attribute.rs b/crates/clawhdf5-format/src/attribute.rs index 815719e..8724d7e 100644 --- a/crates/clawhdf5-format/src/attribute.rs +++ b/crates/clawhdf5-format/src/attribute.rs @@ -51,7 +51,7 @@ impl AttributeMessage { /// /// `length_size` is needed for dataspace dimension parsing. pub fn parse(data: &[u8], length_size: u8) -> Result { - Self::parse_impl(data, length_size, None) + Self::parse_impl(data, length_size, None::<(&[u8], u8)>) } /// [`AttributeMessage::parse`] with access to the rest of the file, which @@ -66,24 +66,24 @@ impl AttributeMessage { offset_size: u8, length_size: u8, ) -> Result { - Self::parse_in_storage(data, &file_data, offset_size, length_size) + Self::parse_in_storage(data, file_data, offset_size, length_size) } /// [`AttributeMessage::parse_in_file`] with the file behind any /// [`Storage`]. - pub fn parse_in_storage( + pub fn parse_in_storage( data: &[u8], - file: &dyn Storage, + file: &S, offset_size: u8, length_size: u8, ) -> Result { Self::parse_impl(data, length_size, Some((file, offset_size))) } - fn parse_impl( + fn parse_impl( data: &[u8], length_size: u8, - file: Option<(&dyn Storage, u8)>, + file: Option<(&S, u8)>, ) -> Result { ensure_len(data, 0, 2)?; let version = data[0]; @@ -98,12 +98,12 @@ impl AttributeMessage { /// The bytes of an embedded datatype/dataspace message, following the /// shared-message reference when `shared` is set. - fn embedded_message<'a>( + fn embedded_message<'a, S: Storage + ?Sized>( bytes: &'a [u8], shared: bool, msg_type: MessageType, length_size: u8, - file: Option<(&dyn Storage, u8)>, + file: Option<(&S, u8)>, ) -> Result, FormatError> { if !shared { return Ok(Cow::Borrowed(bytes)); @@ -155,10 +155,10 @@ impl AttributeMessage { }) } - fn parse_v2( + fn parse_v2( data: &[u8], length_size: u8, - file: Option<(&dyn Storage, u8)>, + file: Option<(&S, u8)>, ) -> Result { // Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared. let flags = data.get(1).copied().unwrap_or(0); @@ -209,10 +209,10 @@ impl AttributeMessage { }) } - fn parse_v3( + fn parse_v3( data: &[u8], length_size: u8, - file: Option<(&dyn Storage, u8)>, + file: Option<(&S, u8)>, ) -> Result { // Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared. let flags = data.get(1).copied().unwrap_or(0); @@ -427,15 +427,15 @@ pub fn extract_attributes_full( offset_size: u8, length_size: u8, ) -> Result, FormatError> { - extract_attributes_full_in(&file_data, header, offset_size, length_size) + extract_attributes_full_in(file_data, header, offset_size, length_size) } /// [`extract_attributes_full`] over any [`Storage`]. Dense attribute /// storage is indexed by a v2 B-tree, which is not read over [`Storage`] /// yet: on a backend without the whole file in memory an object with dense /// attributes is [`FormatError::ContiguousStorageRequired`]. -pub fn extract_attributes_full_in( - file: &dyn Storage, +pub fn extract_attributes_full_in( + file: &S, header: &ObjectHeader, offset_size: u8, length_size: u8, @@ -457,13 +457,13 @@ pub fn extract_attributes_tolerant( offset_size: u8, length_size: u8, ) -> Result<(Vec, Vec), FormatError> { - extract_attributes_tolerant_in(&file_data, header, offset_size, length_size) + extract_attributes_tolerant_in(file_data, header, offset_size, length_size) } /// [`extract_attributes_tolerant`] over any [`Storage`] (see /// [`extract_attributes_full_in`] for dense storage). -pub fn extract_attributes_tolerant_in( - file_data: &dyn Storage, +pub fn extract_attributes_tolerant_in( + file_data: &S, header: &ObjectHeader, offset_size: u8, length_size: u8, @@ -478,8 +478,8 @@ pub fn extract_attributes_tolerant_in( /// Read every attribute; each one that fails goes to `on_error`, which /// either stops the read (returns the error) or skips that attribute. -fn extract_attributes_with( - file_data: &dyn Storage, +fn extract_attributes_with( + file_data: &S, header: &ObjectHeader, offset_size: u8, length_size: u8, @@ -572,8 +572,8 @@ fn find_attribute_info( /// Extract attributes from dense storage (fractal heap + B-tree v2), and /// each one's creation order into `orders`. #[allow(clippy::too_many_arguments)] -fn extract_dense_attributes( - file_data: &dyn Storage, +fn extract_dense_attributes( + file_data: &S, attr_info: &AttributeInfoMessage, fh_addr: u64, offset_size: u8, diff --git a/crates/clawhdf5-format/src/btree_v1.rs b/crates/clawhdf5-format/src/btree_v1.rs index 3378131..4853ca1 100644 --- a/crates/clawhdf5-format/src/btree_v1.rs +++ b/crates/clawhdf5-format/src/btree_v1.rs @@ -77,13 +77,13 @@ impl BTreeV1Node { offset_size: u8, length_size: u8, ) -> Result { - Self::parse_in(&file_data, offset as u64, offset_size, length_size) + Self::parse_in(file_data, offset as u64, offset_size, length_size) } /// [`Self::parse`] over any [`Storage`]: one read of the node's header, /// one of its keys and children. - pub fn parse_in( - file: &dyn Storage, + pub fn parse_in( + file: &S, offset: u64, offset_size: u8, _length_size: u8, @@ -126,24 +126,22 @@ impl BTreeV1Node { let needed = eu * (key_size + os) + key_size; // eu children + (eu+1) keys let body = read_exact_at(file, body_start, needed)?; let file_data: &[u8] = &body; - let mut pos = 0usize; let mut keys = Vec::with_capacity(eu + 1); let mut children = Vec::with_capacity(eu); - for _i in 0..eu { - // key[i] - let key = read_offset(file_data, pos, offset_size)?; - keys.push(key); - pos += key_size; - // child[i] - let child = read_offset(file_data, pos, offset_size)?; - children.push(child); - pos += os; + if os == 0 { + // What reading the first key reports (and keeps `chunks_exact` + // below from being given a zero size). + return Err(FormatError::InvalidOffsetSize(offset_size)); } - // final key - let key = read_offset(file_data, pos, offset_size)?; - keys.push(key); + // `needed` bytes: key[0], child[0], ..., child[eu - 1], key[eu]. + let (pairs, last) = file_data.split_at(eu * (key_size + os)); + for pair in pairs.chunks_exact(key_size + os) { + keys.push(read_offset(pair, 0, offset_size)?); + children.push(read_offset(pair, key_size, offset_size)?); + } + keys.push(read_offset(last, 0, offset_size)?); Ok(BTreeV1Node { node_type, @@ -167,12 +165,12 @@ pub fn collect_symbol_table_nodes( offset_size: u8, length_size: u8, ) -> Result, FormatError> { - collect_symbol_table_nodes_in(&file_data, btree_address, offset_size, length_size) + collect_symbol_table_nodes_in(file_data, btree_address, offset_size, length_size) } /// [`collect_symbol_table_nodes`] over any [`Storage`]: two reads per node. -pub fn collect_symbol_table_nodes_in( - file: &dyn Storage, +pub fn collect_symbol_table_nodes_in( + file: &S, btree_address: u64, offset_size: u8, length_size: u8, @@ -180,8 +178,8 @@ pub fn collect_symbol_table_nodes_in( collect_symbol_table_nodes_inner(file, btree_address, offset_size, length_size, 0) } -fn collect_symbol_table_nodes_inner( - file: &dyn Storage, +fn collect_symbol_table_nodes_inner( + file: &S, btree_address: u64, offset_size: u8, length_size: u8, diff --git a/crates/clawhdf5-format/src/data_layout.rs b/crates/clawhdf5-format/src/data_layout.rs index 480f459..e4873c5 100644 --- a/crates/clawhdf5-format/src/data_layout.rs +++ b/crates/clawhdf5-format/src/data_layout.rs @@ -311,14 +311,14 @@ impl DataLayout { file_data: &[u8], length_size: u8, ) -> Result<(), FormatError> { - self.resolve_vds_mappings_in(&file_data, length_size) + self.resolve_vds_mappings_in(file_data, length_size) } /// [`Self::resolve_vds_mappings`] over any [`Storage`]: one read of the /// global heap collection holding the mappings. - pub fn resolve_vds_mappings_in( + pub fn resolve_vds_mappings_in( &mut self, - file_data: &dyn Storage, + file_data: &S, length_size: u8, ) -> Result<(), FormatError> { if let DataLayout::Virtual { diff --git a/crates/clawhdf5-format/src/extensible_array.rs b/crates/clawhdf5-format/src/extensible_array.rs index 529955d..a95c124 100644 --- a/crates/clawhdf5-format/src/extensible_array.rs +++ b/crates/clawhdf5-format/src/extensible_array.rs @@ -121,12 +121,12 @@ impl ExtensibleArrayHeader { offset_size: u8, length_size: u8, ) -> Result { - Self::parse_in(&file_data, offset as u64, offset_size, length_size) + Self::parse_in(file_data, offset as u64, offset_size, length_size) } /// [`Self::parse`] over any [`Storage`]: one read of the header. - pub fn parse_in( - file: &dyn Storage, + pub fn parse_in( + file: &S, offset: u64, offset_size: u8, length_size: u8, @@ -304,8 +304,8 @@ fn page_nelmts(header: &ExtensibleArrayHeader) -> Option { /// paged. The bitmap lives in the super block, not here — a paged data block /// stores only its prefix, then one slot per page. #[allow(clippy::too_many_arguments)] -fn read_data_block_elements( - file: &dyn Storage, +fn read_data_block_elements( + file: &S, db_offset: u64, nelmts: usize, header: &ExtensibleArrayHeader, @@ -448,8 +448,8 @@ pub fn read_extensible_array_chunks( /// index block's prefix, one of the whole index block, and the same for /// every super block and data block it references. #[allow(clippy::too_many_arguments)] -pub fn read_extensible_array_chunks_in( - file: &dyn Storage, +pub fn read_extensible_array_chunks_in( + file: &S, header: &ExtensibleArrayHeader, dataset_dims: &[u64], max_dims: Option<&[u64]>, @@ -642,8 +642,8 @@ pub fn read_extensible_array_chunks_in( /// + block offset + the page-init bitmap for every data block it owns /// + one address per data block + checksum. #[allow(clippy::too_many_arguments)] -fn read_super_block( - file: &dyn Storage, +fn read_super_block( + file: &S, sb_offset: u64, ndblks: usize, dblk_nelmts: usize, diff --git a/crates/clawhdf5-format/src/fixed_array.rs b/crates/clawhdf5-format/src/fixed_array.rs index de936a7..e6be63b 100644 --- a/crates/clawhdf5-format/src/fixed_array.rs +++ b/crates/clawhdf5-format/src/fixed_array.rs @@ -92,12 +92,12 @@ impl FixedArrayHeader { offset_size: u8, length_size: u8, ) -> Result { - Self::parse_in(&file_data, offset as u64, offset_size, length_size) + Self::parse_in(file_data, offset as u64, offset_size, length_size) } /// [`Self::parse`] over any [`Storage`]: one read of the header. - pub fn parse_in( - file: &dyn Storage, + pub fn parse_in( + file: &S, offset: u64, offset_size: u8, length_size: u8, @@ -174,8 +174,8 @@ pub fn read_fixed_array_chunks( /// [`read_fixed_array_chunks`] over any [`Storage`]: one read of the data /// block's prefix, one of the whole data block (pages included). #[allow(clippy::too_many_arguments)] -pub fn read_fixed_array_chunks_in( - file: &dyn Storage, +pub fn read_fixed_array_chunks_in( + file: &S, header: &FixedArrayHeader, dataset_dims: &[u64], max_dims: Option<&[u64]>, diff --git a/crates/clawhdf5-format/src/fractal_heap.rs b/crates/clawhdf5-format/src/fractal_heap.rs index 1c6a77c..e47c2c9 100644 --- a/crates/clawhdf5-format/src/fractal_heap.rs +++ b/crates/clawhdf5-format/src/fractal_heap.rs @@ -139,13 +139,13 @@ impl FractalHeapHeader { offset_size: u8, length_size: u8, ) -> Result { - Self::parse_in(&file_data, offset as u64, offset_size, length_size) + Self::parse_in(file_data, offset as u64, offset_size, length_size) } /// [`Self::parse`] over any [`Storage`]: one read of the header (two /// when it holds an I/O filter pipeline). - pub fn parse_in( - file: &dyn Storage, + pub fn parse_in( + file: &S, offset: u64, offset_size: u8, length_size: u8, @@ -382,15 +382,15 @@ impl FractalHeapHeader { id_bytes: &[u8], offset_size: u8, ) -> Result, FormatError> { - self.read_managed_object_in(&file_data, id_bytes, offset_size) + self.read_managed_object_in(file_data, id_bytes, offset_size) } /// [`Self::read_managed_object`] over any [`Storage`]. A huge object /// found through the huge-object v2 B-tree still needs the whole file /// in memory ([`FormatError::ContiguousStorageRequired`] otherwise). - pub fn read_managed_object_in( + pub fn read_managed_object_in( &self, - file_data: &dyn Storage, + file_data: &S, id_bytes: &[u8], offset_size: u8, ) -> Result, FormatError> { @@ -423,7 +423,11 @@ impl FractalHeapHeader { } /// Read a huge object (heap ID type 1). - fn read_huge_object(&self, file: &dyn Storage, id: &[u8]) -> Result, FormatError> { + fn read_huge_object( + &self, + file: &S, + id: &[u8], + ) -> Result, FormatError> { let os = usize::from(self.offset_size); let ls = usize::from(self.length_size); // (address, stored length, filter mask, decoded length); the last two @@ -475,9 +479,9 @@ impl FractalHeapHeader { /// Look up huge object `key` in the huge-object v2 B-tree, returning /// (address, stored length, filter mask, decoded length). - fn find_huge_record( + fn find_huge_record( &self, - file: &dyn Storage, + file: &S, key: u64, ) -> Result<(u64, u64, u32, u64), FormatError> { if is_undefined(self.huge_btree_address, self.offset_size) { @@ -554,9 +558,9 @@ impl FractalHeapHeader { } /// Read a managed object (heap ID type 0). - fn read_heap_managed( + fn read_heap_managed( &self, - file_data: &dyn Storage, + file_data: &S, id_bytes: &[u8], offset_size: u8, ) -> Result, FormatError> { @@ -604,9 +608,9 @@ impl FractalHeapHeader { /// header), so we just add it to the block address minus the block's heap /// offset. A filtered heap stores each direct block (header included) /// through its filter pipeline, so the block is decoded first. - fn read_from_direct_block( + fn read_from_direct_block( &self, - file: &dyn Storage, + file: &S, block: DirectBlock, target_offset: u64, length: usize, @@ -645,9 +649,9 @@ impl FractalHeapHeader { /// Read an object by traversing an indirect block to find the right direct block. #[allow(clippy::too_many_arguments)] - fn read_from_indirect_block( + fn read_from_indirect_block( &self, - file: &dyn Storage, + file: &S, iblock_addr: usize, nrows: u16, iblock_heap_offset: u64, diff --git a/crates/clawhdf5-format/src/global_heap.rs b/crates/clawhdf5-format/src/global_heap.rs index e635d4f..9861e8f 100644 --- a/crates/clawhdf5-format/src/global_heap.rs +++ b/crates/clawhdf5-format/src/global_heap.rs @@ -99,13 +99,13 @@ impl GlobalHeapCollection { offset: usize, length_size: u8, ) -> Result { - Self::parse_in(&file_data, offset as u64, length_size) + Self::parse_in(file_data, offset as u64, length_size) } /// [`Self::parse`] over any [`Storage`]: one read of the header, one of /// the collection. - pub fn parse_in( - file: &dyn Storage, + pub fn parse_in( + file: &S, offset: u64, length_size: u8, ) -> Result { @@ -136,13 +136,13 @@ impl GlobalHeapCollection { offset: usize, length_size: u8, ) -> Result { - Self::parse_index_in(&file_data, offset as u64, length_size) + Self::parse_index_in(file_data, offset as u64, length_size) } /// [`Self::parse_index`] over any [`Storage`]: one read of the header, /// one of the collection. The object offsets are file offsets. - pub fn parse_index_in( - file: &dyn Storage, + pub fn parse_index_in( + file: &S, offset: u64, length_size: u8, ) -> Result { @@ -152,8 +152,8 @@ impl GlobalHeapCollection { /// Read the collection at `offset` and index its objects: the /// collection's bytes, its offset as a `usize`, and the index (with /// file offsets). - fn read_collection( - file: &dyn Storage, + fn read_collection( + file: &S, offset: u64, length_size: u8, ) -> Result<(Cow<'_, [u8]>, usize, GlobalHeapIndex), FormatError> { diff --git a/crates/clawhdf5-format/src/local_heap.rs b/crates/clawhdf5-format/src/local_heap.rs index a4e3480..952a9d4 100644 --- a/crates/clawhdf5-format/src/local_heap.rs +++ b/crates/clawhdf5-format/src/local_heap.rs @@ -44,12 +44,12 @@ impl LocalHeap { offset_size: u8, length_size: u8, ) -> Result { - Self::parse_in(&file_data, offset as u64, offset_size, length_size) + Self::parse_in(file_data, offset as u64, offset_size, length_size) } /// [`Self::parse`] over any [`Storage`]: one read of the header. - pub fn parse_in( - file: &dyn Storage, + pub fn parse_in( + file: &S, offset: u64, offset_size: u8, length_size: u8, @@ -97,14 +97,14 @@ impl LocalHeap { /// The end of the list is `H5HL_FREE_NULL` (1); an all-ones value (the /// undefined address) is accepted as "no free list" too. pub fn validate_free_list(&self, file_data: &[u8], length_size: u8) -> Result<(), FormatError> { - self.validate_free_list_in(&file_data, length_size) + self.validate_free_list_in(file_data, length_size) } /// [`Self::validate_free_list`] over any [`Storage`]: two small reads /// per free block. - pub fn validate_free_list_in( + pub fn validate_free_list_in( &self, - file: &dyn Storage, + file: &S, length_size: u8, ) -> Result<(), FormatError> { const FREE_NULL: u64 = 1; @@ -149,14 +149,14 @@ impl LocalHeap { /// Read a null-terminated string from the heap's data segment at the given byte offset. pub fn read_string(&self, file_data: &[u8], string_offset: u64) -> Result { - self.read_string_in(&file_data, string_offset) + self.read_string_in(file_data, string_offset) } /// [`Self::read_string`] over any [`Storage`]: one read, from the /// string to the end of the data segment. - pub fn read_string_in( + pub fn read_string_in( &self, - file: &dyn Storage, + file: &S, string_offset: u64, ) -> Result { let file_len = len_usize(file); diff --git a/crates/clawhdf5-format/src/object_header.rs b/crates/clawhdf5-format/src/object_header.rs index 1fa4b41..4c21eb1 100644 --- a/crates/clawhdf5-format/src/object_header.rs +++ b/crates/clawhdf5-format/src/object_header.rs @@ -7,7 +7,7 @@ use byteorder::{ByteOrder, LittleEndian}; use crate::error::FormatError; use crate::message_type::MessageType; -use crate::storage::{Storage, len_usize, read_exact_at, read_upto}; +use crate::storage::{Storage, Window, len_usize, read_exact_at}; /// OHDR signature for v2 object headers. const OHDR_SIGNATURE: [u8; 4] = *b"OHDR"; @@ -119,36 +119,43 @@ impl ObjectHeader { offset_size: u8, length_size: u8, ) -> Result { - Self::parse_in(&data, offset as u64, offset_size, length_size) + Self::parse_in(data, offset as u64, offset_size, length_size) } /// [`Self::parse`] over any [`Storage`]. /// - /// Reads the signature, the prefix (at most [`V2_PREFIX_MAX`] bytes), - /// then each chunk as one bounded read, continuation chunks included. - pub fn parse_in( - file: &dyn Storage, + /// Reads the prefix (at most [`V2_PREFIX_MAX`] bytes, signature + /// included), then each chunk as one bounded read, continuation chunks + /// included. + pub fn parse_in( + file: &S, offset: u64, offset_size: u8, length_size: u8, ) -> Result { - let sig = read_exact_at(file, offset, 4)?; - if *sig == OHDR_SIGNATURE { - Self::parse_v2(file, offset, offset_size, length_size) + // The longest prefix of either version, in one read. It holds the + // whole prefix or ends at the end of the file, so its bounds checks + // are the whole-file ones. + let prefix = Window::read(file, offset, V2_PREFIX_MAX)?; + prefix.ensure(0, 4)?; + if prefix.bytes[..4] == OHDR_SIGNATURE { + Self::parse_v2(file, offset, &prefix, offset_size, length_size) } else { - Self::parse_v1(file, offset, offset_size, length_size) + Self::parse_v1(file, offset, &prefix, offset_size, length_size) } } - fn parse_v1( - file: &dyn Storage, + fn parse_v1( + file: &S, offset: u64, + prefix: &Window<'_>, offset_size: u8, length_size: u8, ) -> Result { // version(1) + reserved(1) + num_messages(2) + ref_count(4) + header_size(4) = 12 // then pad to 8-byte alignment from start of header - let prefix = read_exact_at(file, offset, 12)?; + prefix.ensure(0, 12)?; + let prefix = &prefix.bytes[..12]; let version = prefix[0]; if version != 1 { @@ -179,7 +186,9 @@ impl ObjectHeader { })?; // parse_v1_chunk reads the chunk, with the bounds check that was here. - let mut messages = Vec::new(); + // The prefix's count (NIL messages included, capped: it is untrusted) + // sizes the list once instead of growing it message by message. + let mut messages = Vec::with_capacity(num_messages.min(64)); let chunk0_count = Self::parse_v1_chunk( file, msg_start, @@ -221,8 +230,8 @@ impl ObjectHeader { /// end of the chunk, or leftover bytes too few for a message header (a /// "gap", which only version 2 allows). #[allow(clippy::too_many_arguments)] - fn parse_v1_chunk( - file: &dyn Storage, + fn parse_v1_chunk( + file: &S, offset: u64, length: usize, offset_size: u8, @@ -295,31 +304,21 @@ impl ObjectHeader { Ok(count) } - fn parse_v2( - file: &dyn Storage, + fn parse_v2( + file: &S, offset: u64, + prefix: &Window<'_>, offset_size: u8, length_size: u8, ) -> Result { - // The prefix, read as one window. The window holds the whole prefix - // or ends at the end of the file, so a position past the window is - // past the end of the file: `ensure_len` checks positions relative - // to the header against it and reports them as the whole-file check - // did, with absolute positions and the file's length. - let window = read_upto(file, offset, V2_PREFIX_MAX)?; - let data: &[u8] = &window; + // `ensure_len` checks positions relative to the header against the + // prefix window and reports them as the whole-file check did, with + // absolute positions and the file's length. + let data: &[u8] = &prefix.bytes; let file_len = len_usize(file); let base = usize::try_from(offset).unwrap_or(usize::MAX); let abs = |rel: usize| base.saturating_add(rel); - let ensure_len = |_: &[u8], rel: usize, needed: usize| -> Result<(), FormatError> { - match rel.checked_add(needed) { - Some(end) if end <= data.len() => Ok(()), - _ => Err(FormatError::UnexpectedEof { - expected: abs(rel).saturating_add(needed), - available: file_len, - }), - } - }; + let ensure_len = |_: &[u8], rel: usize, needed: usize| prefix.ensure(rel, needed); let offset = 0usize; // signature(4) + version(1) + flags(1) = 6 ensure_len(data, offset, 6)?; @@ -533,8 +532,8 @@ impl ObjectHeader { } #[allow(clippy::too_many_arguments)] - fn parse_v2_continuation( - file: &dyn Storage, + fn parse_v2_continuation( + file: &S, offset: u64, length: usize, has_creation_order: bool, @@ -1328,7 +1327,7 @@ mod tests { /// Every header, and every truncation of it, parses to the same result /// (or the same error) through a `read_at`-only storage as from a slice; - /// a header in one chunk takes three reads (signature, prefix, chunk). + /// a header in one chunk takes two reads (prefix, chunk). #[test] fn parse_in_matches_slice_parse() { use crate::storage::CountingStorage; @@ -1375,6 +1374,6 @@ mod tests { let one_chunk = build_v2_header(0x00, &[(0x01, &[42], 0)], None); let storage = CountingStorage::new(one_chunk); ObjectHeader::parse_in(&storage, 0, 8, 8).unwrap(); - assert_eq!(storage.reads(), 3); + assert_eq!(storage.reads(), 2); } } diff --git a/crates/clawhdf5-format/src/shared_message.rs b/crates/clawhdf5-format/src/shared_message.rs index 5c8b1cb..05ee649 100644 --- a/crates/clawhdf5-format/src/shared_message.rs +++ b/crates/clawhdf5-format/src/shared_message.rs @@ -254,13 +254,13 @@ pub fn parse_sohm_table( nindexes: u8, offset_size: u8, ) -> Result { - parse_sohm_table_in(&file_data, table_addr as u64, nindexes, offset_size) + parse_sohm_table_in(file_data, table_addr as u64, nindexes, offset_size) } /// [`parse_sohm_table`] over any [`Storage`]: one read of the signature, /// one of every index entry. -pub fn parse_sohm_table_in( - file: &dyn Storage, +pub fn parse_sohm_table_in( + file: &S, table_addr: u64, nindexes: u8, offset_size: u8, @@ -384,13 +384,13 @@ pub fn parse_sohm_list( num_messages: u16, offset_size: u8, ) -> Result, FormatError> { - parse_sohm_list_in(&file_data, list_addr as u64, num_messages, offset_size) + parse_sohm_list_in(file_data, list_addr as u64, num_messages, offset_size) } /// [`parse_sohm_list`] over any [`Storage`]: one read of the signature, one /// of every entry. -pub fn parse_sohm_list_in( - file: &dyn Storage, +pub fn parse_sohm_list_in( + file: &S, list_addr: u64, num_messages: u16, offset_size: u8, @@ -420,14 +420,14 @@ pub fn parse_sohm_btree_entries( offset_size: u8, length_size: u8, ) -> Result, FormatError> { - parse_sohm_btree_entries_in(&file_data, btree_addr as u64, offset_size, length_size) + parse_sohm_btree_entries_in(file_data, btree_addr as u64, offset_size, length_size) } /// [`parse_sohm_btree_entries`] over any [`Storage`]. The v2 B-tree is not /// read over [`Storage`] yet, so this needs the whole file in memory /// ([`FormatError::ContiguousStorageRequired`] otherwise). -pub fn parse_sohm_btree_entries_in( - file: &dyn Storage, +pub fn parse_sohm_btree_entries_in( + file: &S, btree_addr: u64, offset_size: u8, length_size: u8, @@ -455,12 +455,12 @@ pub fn load_sohm_table( offset_size: u8, length_size: u8, ) -> Result, FormatError> { - load_sohm_table_in(&file_data, offset_size, length_size) + load_sohm_table_in(file_data, offset_size, length_size) } /// [`load_sohm_table`] over any [`Storage`]. -pub fn load_sohm_table_in( - file_data: &dyn Storage, +pub fn load_sohm_table_in( + file_data: &S, offset_size: u8, length_size: u8, ) -> Result, FormatError> { @@ -498,12 +498,12 @@ pub fn message_data_with_sohm<'a>( offset_size: u8, length_size: u8, ) -> Result, FormatError> { - message_data_with_sohm_in(&file_data, msg, offset_size, length_size) + message_data_with_sohm_in(file_data, msg, offset_size, length_size) } /// [`message_data_with_sohm`] over any [`Storage`]. -pub fn message_data_with_sohm_in<'a>( - file_data: &dyn Storage, +pub fn message_data_with_sohm_in<'a, S: Storage + ?Sized>( + file_data: &S, msg: &'a crate::object_header::HeaderMessage, offset_size: u8, length_size: u8, @@ -568,8 +568,8 @@ pub fn resolve_sohm_message( } /// [`resolve_sohm_message`] over any [`Storage`]. -pub fn resolve_sohm_message_in( - file_data: &dyn Storage, +pub fn resolve_sohm_message_in( + file_data: &S, heap_id: &[u8; FHEAP_ID_LEN], sohm_table: &SohmTable, target_msg_type: MessageType, @@ -603,12 +603,12 @@ pub fn message_data<'a>( offset_size: u8, length_size: u8, ) -> Result, FormatError> { - message_data_in(&file_data, msg, offset_size, length_size) + message_data_in(file_data, msg, offset_size, length_size) } /// [`message_data`] over any [`Storage`]. -pub fn message_data_in<'a>( - file_data: &dyn Storage, +pub fn message_data_in<'a, S: Storage + ?Sized>( + file_data: &S, msg: &'a crate::object_header::HeaderMessage, offset_size: u8, length_size: u8, @@ -650,8 +650,8 @@ pub fn resolve_shared_message( } /// [`resolve_shared_message`] over any [`Storage`]. -pub fn resolve_shared_message_in( - file_data: &dyn Storage, +pub fn resolve_shared_message_in( + file_data: &S, shared_ref: &SharedMessageRef, target_msg_type: MessageType, offset_size: u8, @@ -692,8 +692,8 @@ pub fn resolve_shared_message_with_sohm( } /// [`resolve_shared_message_with_sohm`] over any [`Storage`]. -pub fn resolve_shared_message_with_sohm_in( - file_data: &dyn Storage, +pub fn resolve_shared_message_with_sohm_in( + file_data: &S, shared_ref: &SharedMessageRef, target_msg_type: MessageType, offset_size: u8, diff --git a/crates/clawhdf5-format/src/signature.rs b/crates/clawhdf5-format/src/signature.rs index 4b152ee..3a2e5a1 100644 --- a/crates/clawhdf5-format/src/signature.rs +++ b/crates/clawhdf5-format/src/signature.rs @@ -42,7 +42,7 @@ pub fn find_signature(data: &[u8]) -> Result { /// [`find_signature`] over any [`Storage`]: one 8-byte read per candidate /// offset. -pub fn find_signature_in(file: &dyn Storage) -> Result { +pub fn find_signature_in(file: &S) -> Result { let len = file.len(); let mut offset = 0u64; while offset.checked_add(8).is_some_and(|end| end <= len) { diff --git a/crates/clawhdf5-format/src/storage.rs b/crates/clawhdf5-format/src/storage.rs index 7d68eeb..6d09e7e 100644 --- a/crates/clawhdf5-format/src/storage.rs +++ b/crates/clawhdf5-format/src/storage.rs @@ -10,8 +10,15 @@ //! `impl Storage for [u8]` serves the in-memory case with no copy, and //! [`Storage::as_contiguous`] lets a hot loop borrow the whole file at once //! when the backend has it. Modules are converted one at a time: a converted -//! parser has an `*_in(file: &dyn Storage, ..)` core and keeps its old -//! `&[u8]` signature as a thin wrapper, so callers do not change. +//! parser has an `*_in(file: &S, ..)` core and keeps +//! its old `&[u8]` signature as a thin wrapper, so callers do not change. +//! +//! The cores are generic rather than taking `&dyn Storage` so that the +//! wrappers monomorphise for `[u8]`: the bounds check of each structure read +//! inlines to what the slice code did, with no indirect call and no copy, +//! which keeps local files as fast as before the migration. A `&dyn Storage` +//! still works (`S = dyn Storage`), and a remote backend pays one indirect +//! call per structure read. //! //! The trait is synchronous and `no_std`: parsing is CPU work, and a remote //! backend bridges to its own I/O. @@ -176,7 +183,7 @@ impl Storage for std::sync::Arc { /// `storage.len()` as the `usize` the parsers' end-of-file errors report /// (saturating on targets where the file is larger than the address space). #[inline] -pub(crate) fn len_usize(file: &dyn Storage) -> usize { +pub(crate) fn len_usize(file: &S) -> usize { usize::try_from(file.len()).unwrap_or(usize::MAX) } @@ -187,8 +194,8 @@ pub(crate) fn len_usize(file: &dyn Storage) -> usize { /// `available = storage length` — the error the `&[u8]` parsers give for /// the same bounds check (`offset + len > file_data.len()`). #[inline] -pub fn read_exact_at( - file: &dyn Storage, +pub fn read_exact_at( + file: &S, offset: u64, len: usize, ) -> Result, FormatError> { @@ -198,7 +205,8 @@ pub fn read_exact_at( .saturating_add(len), available: len_usize(file), }; - // In-memory fast path: one dynamic call, then plain slicing. + // In-memory fast path: plain slicing (for `S = [u8]` this inlines to + // the slice code's bounds check). if let Some(all) = file.as_contiguous() { return usize::try_from(offset) .ok() @@ -219,6 +227,8 @@ pub fn read_exact_at( Ok(bytes) } +#[cold] +#[inline(never)] fn short_read() -> FormatError { FormatError::Storage( "short read inside the file (the storage shrank or the backend failed)".into(), @@ -240,7 +250,11 @@ pub(crate) struct Window<'a> { impl<'a> Window<'a> { /// Read up to `max` bytes at `base`. - pub fn read(file: &'a dyn Storage, base: u64, max: usize) -> Result { + pub fn read( + file: &'a S, + base: u64, + max: usize, + ) -> Result { Ok(Window { bytes: read_upto(file, base, max)?, base: usize::try_from(base).unwrap_or(usize::MAX), @@ -259,6 +273,7 @@ impl<'a> Window<'a> { } /// Check that `[rel, rel + needed)` (relative to `base`) is in the file. + #[inline] pub fn ensure(&self, rel: usize, needed: usize) -> Result<(), FormatError> { match rel.checked_add(needed) { Some(end) if end <= self.bytes.len() => Ok(()), @@ -274,8 +289,8 @@ impl<'a> Window<'a> { /// storage. For structures whose size is only known once their prefix has /// been parsed and whose parsers bound-check what they are given. #[inline] -pub fn read_upto( - file: &dyn Storage, +pub fn read_upto( + file: &S, offset: u64, max: usize, ) -> Result, FormatError> { @@ -297,8 +312,8 @@ pub fn read_upto( /// [`Storage`] yet. On a backend without a contiguous view this is the /// clean [`FormatError::ContiguousStorageRequired`] error, never a guess. #[inline] -pub fn require_contiguous<'a>( - file: &'a dyn Storage, +pub fn require_contiguous<'a, S: Storage + ?Sized>( + file: &'a S, what: &'static str, ) -> Result<&'a [u8], FormatError> { file.as_contiguous() diff --git a/crates/clawhdf5-format/src/superblock.rs b/crates/clawhdf5-format/src/superblock.rs index 318c945..dc21f7c 100644 --- a/crates/clawhdf5-format/src/superblock.rs +++ b/crates/clawhdf5-format/src/superblock.rs @@ -166,13 +166,13 @@ impl Superblock { file_data: &[u8], signature_offset: usize, ) -> Result { - self.refresh_eof_in(&file_data, signature_offset as u64) + self.refresh_eof_in(file_data, signature_offset as u64) } /// [`Self::refresh_eof`] over any [`Storage`]. - pub fn refresh_eof_in( + pub fn refresh_eof_in( &mut self, - file: &dyn Storage, + file: &S, signature_offset: u64, ) -> Result { let refreshed = Superblock::parse_in(file, signature_offset)?; @@ -233,13 +233,16 @@ impl Superblock { /// [`FormatError::UserBlockNotStripped`] because the addresses in the /// returned superblock would otherwise be applied to the wrong bytes. pub fn parse(data: &[u8], signature_offset: usize) -> Result { - Self::parse_in(&data, signature_offset as u64) + Self::parse_in(data, signature_offset as u64) } /// [`Self::parse`] over any [`Storage`]: one read of the first /// [`SUPERBLOCK_READ_LEN`] bytes (fewer when the file is shorter, which /// is then refused with the same end-of-file errors as a short slice). - pub fn parse_in(file: &dyn Storage, signature_offset: u64) -> Result { + pub fn parse_in( + file: &S, + signature_offset: u64, + ) -> Result { if signature_offset != 0 { return Err(FormatError::UserBlockNotStripped(signature_offset)); } diff --git a/crates/clawhdf5-format/src/superblock_ext.rs b/crates/clawhdf5-format/src/superblock_ext.rs index 1a684f6..8f09aff 100644 --- a/crates/clawhdf5-format/src/superblock_ext.rs +++ b/crates/clawhdf5-format/src/superblock_ext.rs @@ -171,13 +171,13 @@ pub fn read_superblock_extension( data: &[u8], sb: &Superblock, ) -> Result, FormatError> { - read_superblock_extension_in(&data, sb) + read_superblock_extension_in(data, sb) } /// [`read_superblock_extension`] over any [`Storage`]; its length is the /// end of file. -pub fn read_superblock_extension_in( - file: &dyn Storage, +pub fn read_superblock_extension_in( + file: &S, sb: &Superblock, ) -> Result, FormatError> { let os = sb.offset_size; @@ -351,12 +351,12 @@ impl CacheImage { location: CacheImageLocation, sb: &Superblock, ) -> Result { - Self::decode_in(&data, location, sb) + Self::decode_in(data, location, sb) } /// [`Self::decode`] over any [`Storage`]: one read of the image block. - pub fn decode_in( - file: &dyn Storage, + pub fn decode_in( + file: &S, location: CacheImageLocation, sb: &Superblock, ) -> Result { @@ -483,7 +483,10 @@ impl CacheImage { } /// [`Self::block`] over any [`Storage`]. - pub fn block_in<'a>(&self, file: &'a dyn Storage) -> Result, FormatError> { + pub fn block_in<'a, S: Storage + ?Sized>( + &self, + file: &'a S, + ) -> Result, FormatError> { image_block_in(file, self.location) } @@ -511,8 +514,8 @@ fn image_block(data: &[u8], location: CacheImageLocation) -> Result<&[u8], Forma Ok(&data[start as usize..start as usize + len]) } -fn image_block_in( - file: &dyn Storage, +fn image_block_in( + file: &S, location: CacheImageLocation, ) -> Result, FormatError> { let (start, len) = image_block_range(file.len(), location)?; @@ -540,12 +543,12 @@ fn image_block_range( /// ([`CacheImage::decode`]). `data` is the file from the superblock on, up /// to its recorded end of file. pub fn cache_image_state(data: &[u8], sb: &Superblock) -> Result { - cache_image_state_in(&data, sb) + cache_image_state_in(data, sb) } /// [`cache_image_state`] over any [`Storage`]. -pub fn cache_image_state_in( - file: &dyn Storage, +pub fn cache_image_state_in( + file: &S, sb: &Superblock, ) -> Result { match read_superblock_extension_in(file, sb)? { diff --git a/crates/clawhdf5-format/src/symbol_table.rs b/crates/clawhdf5-format/src/symbol_table.rs index 8fb4453..467f43b 100644 --- a/crates/clawhdf5-format/src/symbol_table.rs +++ b/crates/clawhdf5-format/src/symbol_table.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use crate::error::FormatError; -use crate::storage::{Storage, len_usize, read_exact_at}; +use crate::storage::{Storage, read_exact_at}; /// Symbol Table message (type 0x0011) found in v1 group object headers. #[derive(Debug, Clone, PartialEq)] @@ -80,17 +80,16 @@ impl SymbolTableNode { offset: usize, offset_size: u8, ) -> Result { - Self::parse_in(&file_data, offset as u64, offset_size) + Self::parse_in(file_data, offset as u64, offset_size) } /// [`Self::parse`] over any [`Storage`]: one read of the node's header, /// one of its entries. - pub fn parse_in( - file: &dyn Storage, + pub fn parse_in( + file: &S, offset: u64, offset_size: u8, ) -> Result { - let file_len = len_usize(file); // signature(4) + version(1) + reserved(1) + number_of_symbols(2) = 8 let header = read_exact_at(file, offset, 8)?; @@ -108,42 +107,22 @@ impl SymbolTableNode { let os = offset_size as usize; // Each entry: link_name_offset(os) + obj_hdr_addr(os) + cache_type(4) + reserved(4) + scratch(16) let entry_size = os + os + 4 + 4 + 16; - // `offset + 8` fits: the header's read checked it. - let entries_start = offset as usize + 8; - let needed = entries_start.checked_add(num_symbols * entry_size).ok_or( - FormatError::UnexpectedEof { - expected: usize::MAX, - available: file_len, - }, - )?; - if needed > file_len { - return Err(FormatError::UnexpectedEof { - expected: needed, - available: file_len, - }); - } - let body = read_exact_at(file, entries_start as u64, num_symbols * entry_size)?; + // `offset + 8` fits: the header's read checked it. The entries' + // read is the bounds check (`offset + 8 + entries > file length`, + // which cannot overflow: at most 65535 entries of 40 bytes). + let body = read_exact_at(file, offset + 8, num_symbols * entry_size)?; let file_data: &[u8] = &body; let mut entries = Vec::with_capacity(num_symbols); - let mut pos = 0usize; - for _ in 0..num_symbols { - let link_name_offset = read_offset(file_data, pos, offset_size)?; - pos += os; - let object_header_address = read_offset(file_data, pos, offset_size)?; - pos += os; - let cache_type = u32::from_le_bytes([ - file_data[pos], - file_data[pos + 1], - file_data[pos + 2], - file_data[pos + 3], - ]); - pos += 4; + for entry in file_data.chunks_exact(entry_size) { + let link_name_offset = read_offset(entry, 0, offset_size)?; + let object_header_address = read_offset(entry, os, offset_size)?; + let pos = 2 * os; + let cache_type = + u32::from_le_bytes([entry[pos], entry[pos + 1], entry[pos + 2], entry[pos + 3]]); // reserved 4 bytes - pos += 4; let mut scratch_pad = [0u8; 16]; - scratch_pad.copy_from_slice(&file_data[pos..pos + 16]); - pos += 16; + scratch_pad.copy_from_slice(&entry[pos + 8..pos + 24]); entries.push(SymbolTableEntry { link_name_offset, diff --git a/crates/clawhdf5-format/tests/fixtures/v1_groups_400.h5 b/crates/clawhdf5-format/tests/fixtures/v1_groups_400.h5 new file mode 100644 index 0000000000000000000000000000000000000000..5237559583ebd35892389fba2a6cdfff0a87461b GIT binary patch literal 355840 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, objs: &mut usize) { + for name in g.datasets().unwrap_or_default() { + *objs += 1; + if let Ok(ds) = g.dataset(&name) { + let _ = black_box(ds.shape()); + let _ = black_box(ds.dtype()); + let _ = black_box(ds.attrs()); + } + } + for name in g.groups().unwrap_or_default() { + *objs += 1; + if let Ok(sub) = g.group(&name) { + walk(&sub, objs); + } + } +} + +fn bench_local_metadata(c: &mut Criterion) { + let bytes = std::fs::read(FIXTURE).unwrap(); + let (_, f) = clawhdf5_format::signature::split_user_block(&bytes).unwrap(); + let sb = Superblock::parse(f, 0).unwrap(); + let (os, ls) = (sb.offset_size, sb.length_size); + let root = ObjectHeader::parse(f, sb.root_group_address as usize, os, ls).unwrap(); + let stm = root + .messages + .iter() + .find(|m| m.msg_type == MessageType::SymbolTable) + .map(|m| SymbolTableMessage::parse(&m.data, os).unwrap()) + .unwrap(); + let nodes = collect_symbol_table_nodes(f, stm.btree_address, os, ls).unwrap(); + let headers: Vec = nodes + .iter() + .flat_map(|&a| SymbolTableNode::parse(f, a as usize, os).unwrap().entries) + .map(|e| e.object_header_address) + .collect(); + assert_eq!(headers.len(), 401); + + let mut g = c.benchmark_group("local_metadata"); + g.bench_function("object_header_parse_x401", |b| { + b.iter(|| { + for &a in &headers { + black_box(ObjectHeader::parse(f, a as usize, os, ls).unwrap()); + } + }) + }); + g.bench_function("snod_parse_all", |b| { + b.iter(|| { + for &a in &nodes { + black_box(SymbolTableNode::parse(f, a as usize, os).unwrap()); + } + }) + }); + g.bench_function("btree_v1_walk", |b| { + b.iter(|| black_box(collect_symbol_table_nodes(f, stm.btree_address, os, ls).unwrap())) + }); + let file = File::open(FIXTURE).unwrap(); + g.bench_function("facade_list_400_groups", |b| { + b.iter(|| { + let mut n = 0; + walk(&file.root(), &mut n); + assert_eq!(n, 401); + }) + }); + g.finish(); +} + +criterion_group!(benches, bench_local_metadata); +criterion_main!(benches);