format: group walks go on past a failed node and hint what they read next
Listing a large group over openUrl still took 6-11 passes (network round
trips) for the reviewer's 3000-dataset h5py file: each pass only found
the structures the walk reached before its first miss.
- The v1 and v2 B-tree collectors descend into every child of a node
after one fails (they only read the siblings before, so a sibling's
subtree came a pass later), then return the first error: results and
errors unchanged. The v2 walk stops once its record budget is spent,
so a shared-subtree tree still cannot multiply the work.
- Hints (`Storage::hint`, a no-op for every backend but the lazy one):
a group B-tree node's and a symbol table node's body (read once their
header gives a length, a round trip later when the body is in the
next block), an object header's first chunk and its continuation
chunks, the symbol table nodes a B-tree leaf names, a dense group's
name index header and the heap's root block (both read right after
the heap header). A listing also hints every child's object header as
its entry is read, even after a failure, and every direct block of a
dense group's heap (reading the indirect blocks, at most 4096 entries
and 4 levels deep); a lookup does not.
- The fractal heap's indirect-block layout (entry sizes, where the first
n entries end) is one helper used by the object reads and the hints.
Measured with tests/lazy.rs listing_cost_of_a_given_file on an h5py file
like the reviewer's (3000 datasets of 64 KiB, 198 MB), list('/'),
passes/requests/bytes, before -> after:
earliest, 1 MiB: 6/73/192.5 MB -> 4/68/192.5 MB
earliest, 64 KiB: 8/531/35.2 MB -> 5/530/35.3 MB
latest, 1 MiB: 9/98/196.5 MB -> 5/86/196.5 MB
latest, 64 KiB: 11/452/29.6 MB -> 6/454/30.5 MB
listing_a_large_group_takes_a_few_passes (512-byte blocks), budgets
tightened to the new counts: FileBuilder 600 children 5 -> 4 passes,
h5py 2000 children earliest 8 -> 5, latest 11 -> 6.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -92,6 +92,8 @@ impl BTreeV1Node {
|
||||
// + left_sibling(offset_size) + right_sibling(offset_size)
|
||||
let os = offset_size as usize;
|
||||
let header_size = 8 + os * 2;
|
||||
// The body is read once the header says how long it is.
|
||||
file.hint(offset, NODE_HINT_LEN);
|
||||
let header = read_exact_at(file, offset, header_size)?;
|
||||
let file_data: &[u8] = &header;
|
||||
// The header's read checked that `offset + header_size` fits.
|
||||
@@ -158,6 +160,17 @@ impl BTreeV1Node {
|
||||
/// Maximum recursion depth for B-tree traversal (malformed data protection).
|
||||
const MAX_BTREE_DEPTH: usize = 64;
|
||||
|
||||
/// What a symbol table node takes with libhdf5's default group leaf K (4):
|
||||
/// its 8-byte header and 2K entries of 40 bytes (8-byte offsets). Hinted
|
||||
/// before one is read ([`Storage::hint`]); a node of another size is read
|
||||
/// all the same.
|
||||
const SNOD_HINT_LEN: usize = 8 + 8 * 40;
|
||||
|
||||
/// What a group B-tree node takes with libhdf5's default internal K (16):
|
||||
/// its header (24 bytes with 8-byte offsets), 2K + 1 keys and 2K children
|
||||
/// of 8 bytes. Hinted before one is read.
|
||||
const NODE_HINT_LEN: usize = 24 + (2 * 16 + 1 + 2 * 16) * 8;
|
||||
|
||||
/// Collect all leaf-level child addresses (SNOD addresses) by traversing the B-tree.
|
||||
pub fn collect_symbol_table_nodes(
|
||||
file_data: &[u8],
|
||||
@@ -196,20 +209,22 @@ fn collect_symbol_table_nodes_inner<S: Storage + ?Sized>(
|
||||
}
|
||||
|
||||
if node.node_level == 0 {
|
||||
// Leaf: children are SNOD addresses
|
||||
// Leaf: children are SNOD addresses, read next (see
|
||||
// `Storage::hint`).
|
||||
for &snod in &node.children {
|
||||
file.hint(snod, SNOD_HINT_LEN);
|
||||
}
|
||||
Ok(node.children)
|
||||
} else {
|
||||
// Internal: recurse into children. After the first child that
|
||||
// fails, the others are only read (as `storage::touch` does), not
|
||||
// descended into; that error is returned.
|
||||
// Internal: recurse into children. A child that fails does not
|
||||
// stop the walk: the others are still descended into (reading, not
|
||||
// using, what they hold), then the first error is returned. The
|
||||
// result and the error are those of stopping at the first failure;
|
||||
// a storage that records what it lacks (see `storage::touch`)
|
||||
// learns every node the walk can reach in one attempt.
|
||||
let mut result = Vec::new();
|
||||
let mut failed = None;
|
||||
for &child_addr in &node.children {
|
||||
if failed.is_some() {
|
||||
// Parsing reads the node's header, then its body.
|
||||
let _ = BTreeV1Node::parse_in(file, child_addr, offset_size, length_size);
|
||||
continue;
|
||||
}
|
||||
match collect_symbol_table_nodes_inner(
|
||||
file,
|
||||
child_addr,
|
||||
@@ -217,8 +232,11 @@ fn collect_symbol_table_nodes_inner<S: Storage + ?Sized>(
|
||||
length_size,
|
||||
depth + 1,
|
||||
) {
|
||||
Ok(child_snods) => result.extend(child_snods),
|
||||
Err(e) => failed = Some(e),
|
||||
Ok(child_snods) if failed.is_none() => result.extend(child_snods),
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
failed.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
match failed {
|
||||
|
||||
Reference in New Issue
Block a user