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:
@@ -163,6 +163,12 @@ impl ObjectHeader {
|
||||
offset_size: u8,
|
||||
length_size: u8,
|
||||
) -> Result<ObjectHeader, FormatError> {
|
||||
// The first chunk is read once the prefix says how long it is: say
|
||||
// so (see `Storage::hint`), for a storage that fetches between
|
||||
// attempts.
|
||||
if file.as_contiguous().is_none() {
|
||||
file.hint(offset, OBJECT_HEADER_HINT_LEN);
|
||||
}
|
||||
// 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.
|
||||
@@ -279,10 +285,20 @@ impl ObjectHeader {
|
||||
let mut spans = ChunkSpans::new(file.len(), offset, length)?;
|
||||
let mut chunk0_count = 0usize;
|
||||
let mut next = 0usize;
|
||||
let hints = file.as_contiguous().is_none();
|
||||
while let Some((chunk_offset, chunk_length)) = spans.get(next) {
|
||||
let chunk = read_exact_at(file, chunk_offset, chunk_length)?;
|
||||
let known = spans.len;
|
||||
let count =
|
||||
Self::parse_v1_messages(&chunk, offset_size, length_size, messages, &mut spans)?;
|
||||
// The continuation chunks this one names are read next.
|
||||
if hints {
|
||||
for i in known..spans.len {
|
||||
if let Some((o, l)) = spans.get(i) {
|
||||
file.hint(o, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only the first chunk's messages are held to the prefix count.
|
||||
if next == 0 {
|
||||
chunk0_count = count;
|
||||
@@ -477,8 +493,17 @@ impl ObjectHeader {
|
||||
// whenever a message no longer fits), up to the same bound as a
|
||||
// version-1 header.
|
||||
let mut spans = ChunkSpans::new(file.len(), base as u64, chunk0_msg_end.saturating_add(4))?;
|
||||
// The continuation chunks a chunk names are read next (see
|
||||
// `Storage::hint`).
|
||||
let hints = file.as_contiguous().is_none();
|
||||
if hints {
|
||||
for &(o, l) in &continuations {
|
||||
file.hint(o as u64, l);
|
||||
}
|
||||
}
|
||||
while let Some((cont_offset, cont_length)) = continuations.pop() {
|
||||
spans.add(cont_offset as u64, cont_length)?;
|
||||
let known = continuations.len();
|
||||
Self::parse_v2_continuation(
|
||||
file,
|
||||
cont_offset as u64,
|
||||
@@ -489,6 +514,11 @@ impl ObjectHeader {
|
||||
&mut messages,
|
||||
&mut continuations,
|
||||
)?;
|
||||
if hints {
|
||||
for &(o, l) in &continuations[known..] {
|
||||
file.hint(o as u64, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ObjectHeader {
|
||||
@@ -634,6 +664,11 @@ impl ObjectHeader {
|
||||
}
|
||||
}
|
||||
|
||||
/// What an object header is hinted to take before its prefix is read (see
|
||||
/// [`Storage::hint`]): the first chunk of a typical dataset's header. A
|
||||
/// longer header is read all the same.
|
||||
pub(crate) const OBJECT_HEADER_HINT_LEN: usize = 512;
|
||||
|
||||
/// Longest version-2 object header prefix: signature(4) + version(1) +
|
||||
/// flags(1) + times(16) + attribute phase change(4) + chunk-0 size(8).
|
||||
const V2_PREFIX_MAX: usize = 34;
|
||||
|
||||
Reference in New Issue
Block a user