fix: apply a metadata cache image without copying the file
apply_cache_image returned a copy of the whole file with the image's
entries written in, and File (mmap by default), MmapFile and LazyFile
used that copy for every read: opening a 1 GiB sparse file with an image
needed 2 GB of memory, and an 8 GiB one aborted the process, where
de2a53f (which ignored the image) opened them in a few MB.
The metadata parsers read one contiguous slice, so the image still has
to be laid over the file's bytes; it is now laid over a private copy
that costs only the pages it touches:
- clawhdf5_format::superblock_ext::CacheImage decodes the image into an
entry list (address, offset in the block, length) and applies it to
any destination; cache_image_state tells an opener whether the file
has no image, a loadable one, or one libhdf5 cannot load;
apply_cache_image_in_place is for readers that own their buffer.
apply_cache_image and metadata_view (which copied) are gone.
- clawhdf5_io::HDF5Read::private_copy returns a writable private copy
of a reader's bytes: MmapReader gives a MAP_PRIVATE copy-on-write
mapping (memmap2 map_copy), so only the pages the entries land on are
copied; the default copies the bytes (in-memory readers).
- File, MmapFile and LazyFile write the image into that mapping
(crate::cache_image). File::from_bytes / open_buffered patch their own
buffer in place, copying only the image block, as libhdf5 does. A
file without an image is read straight from the mapping, unchanged.
An image entry that runs past the end of file is now refused: libhdf5
checks only that it starts inside the file, and the images libhdf5
writes never do this, but those bytes have nowhere to go in a view of
the file.
Tests: tests/cache_image_memory.rs has libhdf5 (through ctypes) add an
image to a 1 GiB sparse file and bounds resident-memory growth for all
three openers at 256 MiB; it fails on the previous commit (File::open
grew 2,148,720,640 bytes). reader.rs zero_copy_tests check that a file
without an image is read from the mapping itself and that an image goes
into a copy-on-write mapping, not a heap copy; clawhdf5-io checks that
private_copy writes never reach the reader or the file.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
+13
-3
@@ -13,9 +13,19 @@ ZFP filters; the 2 mismatches are the known h5py big-endian VL bug.
|
|||||||
bytes; in `h5clear_mdc_image.h5` the root group exists only there, and
|
bytes; in `h5clear_mdc_image.h5` the root group exists only there, and
|
||||||
every reader failed with `InvalidObjectHeaderVersion(0)`. `File`,
|
every reader failed with `InvalidObjectHeaderVersion(0)`. `File`,
|
||||||
`MmapFile` and `LazyFile` (and `h5rs`) now apply the image at open
|
`MmapFile` and `LazyFile` (and `h5rs`) now apply the image at open
|
||||||
(`clawhdf5_format::superblock_ext`), with libhdf5's checks. A file whose
|
(`clawhdf5_format::superblock_ext::CacheImage`), with libhdf5's checks.
|
||||||
image libhdf5 cannot load opens in libhdf5 but nothing in it can be read;
|
The file is not copied to do it: a mapped file gets the image's entries
|
||||||
`File::open` refuses it.
|
written into a private copy-on-write mapping
|
||||||
|
(`clawhdf5_io::HDF5Read::private_copy`, `MAP_PRIVATE`), so only the pages
|
||||||
|
they land on are copied, and a buffer the opener owns (`File::from_bytes`,
|
||||||
|
`open_buffered`) is patched in place; files without an image are read
|
||||||
|
from the mapping exactly as before. (An interim version copied the whole
|
||||||
|
file onto the heap: 2 GB of memory to open a 1 GiB sparse file with an
|
||||||
|
image, and an abort for an 8 GiB one; `tests/cache_image_memory.rs`
|
||||||
|
guards it.) An image entry that runs past the end of file is refused
|
||||||
|
(libhdf5 checks only its start; the images it writes never do this). A
|
||||||
|
file whose image libhdf5 cannot load opens in libhdf5 but nothing in it
|
||||||
|
can be read; `File::open` refuses it.
|
||||||
- **The superblock extension is decoded at open, as libhdf5 does:** a File
|
- **The superblock extension is decoded at open, as libhdf5 does:** a File
|
||||||
Space Info or Metadata Cache Image message libhdf5 cannot decode makes the
|
Space Info or Metadata Cache Image message libhdf5 cannot decode makes the
|
||||||
open fail (`cve-2020-10810`, `cve-2020-10812` were opened).
|
open fail (`cve-2020-10810`, `cve-2020-10812` were opened).
|
||||||
|
|||||||
@@ -718,8 +718,8 @@ fn main() {
|
|||||||
// root group — so a file whose image it cannot load still opens and
|
// root group — so a file whose image it cannot load still opens and
|
||||||
// every object fails; File::open refuses such a file outright. The
|
// every object fails; File::open refuses such a file outright. The
|
||||||
// probe records the image's error where libhdf5 reports it.
|
// probe records the image's error where libhdf5 reports it.
|
||||||
use clawhdf5_format::superblock_ext;
|
use clawhdf5_format::superblock_ext::{self, CacheImageState};
|
||||||
let ext = match guarded(|| superblock_ext::read_superblock_extension(hdf5, &sb).map_err(e)) {
|
let state = match guarded(|| superblock_ext::cache_image_state(hdf5, &sb).map_err(e)) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
top.insert("open_error".into(), Value::String(msg));
|
top.insert("open_error".into(), Value::String(msg));
|
||||||
@@ -728,18 +728,22 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut image_error = None;
|
let mut image_error = None;
|
||||||
let view = match ext.and_then(|x| x.cache_image) {
|
let view = match state {
|
||||||
None => None,
|
CacheImageState::Absent => None,
|
||||||
Some(loc) => match guarded(|| {
|
CacheImageState::Unloadable(err) => {
|
||||||
superblock_ext::apply_cache_image(hdf5, loc, &sb)
|
image_error = Some(e(err));
|
||||||
.map_err(e)
|
|
||||||
}) {
|
|
||||||
Ok(v) => Some(v),
|
|
||||||
Err(msg) => {
|
|
||||||
image_error = Some(msg);
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
CacheImageState::Loaded(image) => {
|
||||||
|
let mut v = hdf5.to_vec();
|
||||||
|
match image.block(hdf5).and_then(|b| image.apply(b, &mut v)) {
|
||||||
|
Ok(()) => Some(v),
|
||||||
|
Err(err) => {
|
||||||
|
image_error = Some(e(err));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let hdf5: &[u8] = view.as_deref().unwrap_or(hdf5);
|
let hdf5: &[u8] = view.as_deref().unwrap_or(hdf5);
|
||||||
top.insert("superblock_version".into(), json!(sb.version));
|
top.insert("superblock_version".into(), json!(sb.version));
|
||||||
|
|||||||
@@ -14,9 +14,11 @@
|
|||||||
//! `H5C__reconstruct_cache_contents`), and the entries take the place of
|
//! `H5C__reconstruct_cache_contents`), and the entries take the place of
|
||||||
//! the file's bytes at their addresses: the file itself may hold stale or
|
//! the file's bytes at their addresses: the file itself may hold stale or
|
||||||
//! no metadata there (in `h5clear_mdc_image.h5` the root group's header is
|
//! no metadata there (in `h5clear_mdc_image.h5` the root group's header is
|
||||||
//! only in the image). [`apply_cache_image`] does the same with bytes: it
|
//! only in the image). [`CacheImage::apply`] does the same with bytes: it
|
||||||
//! returns a copy of the file with every entry written at its address, so
|
//! writes every entry at its address, so every parser reads what libhdf5
|
||||||
//! every parser reads what libhdf5 reads.
|
//! reads. It writes into whatever the opener gives it — a private
|
||||||
|
//! copy-on-write mapping of the file, or a buffer the opener owns — so the
|
||||||
|
//! file is never copied whole.
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{collections::BTreeSet, vec::Vec};
|
use alloc::{collections::BTreeSet, vec::Vec};
|
||||||
@@ -284,36 +286,64 @@ fn decode_fsinfo(data: &[u8], os: u8, ls: u8) -> Result<FileSpaceInfo, FormatErr
|
|||||||
|
|
||||||
/// One entry of a metadata cache image: `len` bytes at `image_offset` in
|
/// One entry of a metadata cache image: `len` bytes at `image_offset` in
|
||||||
/// the image block, belonging at file address `address`.
|
/// the image block, belonging at file address `address`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
struct ImageEntry {
|
struct ImageEntry {
|
||||||
address: u64,
|
address: u64,
|
||||||
image_offset: usize,
|
image_offset: usize,
|
||||||
len: usize,
|
len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode the metadata cache image at `location` in `data` (the file from
|
/// A decoded metadata cache image: where its block is, and the entries it
|
||||||
/// the superblock on, up to its recorded end of file) and return a copy of
|
/// holds. [`CacheImage::apply`] writes the entries over a file's bytes.
|
||||||
/// `data` with every cached entry written at its address — the metadata
|
|
||||||
/// libhdf5 reads for this file. The image is checked as libhdf5 checks it
|
|
||||||
/// (`H5C__decode_cache_image_header`, `H5C__reconstruct_cache_entry`):
|
|
||||||
/// signature and version, the image length it records, entry types, rings
|
|
||||||
/// and ages in range, entry addresses inside the file and not repeated,
|
|
||||||
/// flush-dependency parents already in the cache.
|
|
||||||
///
|
///
|
||||||
/// libhdf5 does not verify the block's trailing checksum when it loads an
|
/// Only the entry list is kept, never a copy of the file: an opener that
|
||||||
/// image, so neither does this.
|
/// maps the file applies the image to a private copy-on-write mapping, so
|
||||||
pub fn apply_cache_image(
|
/// only the pages the entries land on are copied.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CacheImage {
|
||||||
|
location: CacheImageLocation,
|
||||||
|
entries: Vec<ImageEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What an opener must do about a file's metadata cache image.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CacheImageState {
|
||||||
|
/// The file has no image: its bytes are its metadata.
|
||||||
|
Absent,
|
||||||
|
/// The file has an image that loads: apply it with [`CacheImage::apply`].
|
||||||
|
Loaded(CacheImage),
|
||||||
|
/// The file has an image libhdf5 fails to load. libhdf5 still opens the
|
||||||
|
/// file (the image loads at the first metadata read), and that read
|
||||||
|
/// fails with this error.
|
||||||
|
Unloadable(FormatError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheImage {
|
||||||
|
/// Decode the metadata cache image at `location` in `data` (the file
|
||||||
|
/// from the superblock on, up to its recorded end of file). The image is
|
||||||
|
/// checked as libhdf5 checks it (`H5C__decode_cache_image_header`,
|
||||||
|
/// `H5C__reconstruct_cache_entry`): signature and version, the image
|
||||||
|
/// length it records, entry types, rings and ages in range, entry
|
||||||
|
/// addresses inside the file and not repeated, flush-dependency parents
|
||||||
|
/// already in the cache.
|
||||||
|
///
|
||||||
|
/// One check is stricter than libhdf5's: an entry must end inside the
|
||||||
|
/// file. libhdf5 checks only that it starts there, and serves the rest
|
||||||
|
/// from the image; the images libhdf5 writes never do this (every entry
|
||||||
|
/// lies below the image block, which is written last), and the bytes an
|
||||||
|
/// entry would put past the end of file have nowhere to go in a view of
|
||||||
|
/// the file.
|
||||||
|
///
|
||||||
|
/// libhdf5 does not verify the block's trailing checksum when it loads
|
||||||
|
/// an image, so neither does this.
|
||||||
|
pub fn decode(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
location: CacheImageLocation,
|
location: CacheImageLocation,
|
||||||
sb: &Superblock,
|
sb: &Superblock,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Self, FormatError> {
|
||||||
let (offset_size, length_size) = (sb.offset_size, sb.length_size);
|
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 block = image_block(data, location)?;
|
||||||
let len = usize::try_from(location.length).map_err(|_| bad("length out of range"))?;
|
|
||||||
let block = start
|
|
||||||
.checked_add(len)
|
|
||||||
.and_then(|end| data.get(start..end))
|
|
||||||
.ok_or(bad("image block extends past the end of the file"))?;
|
|
||||||
let eoa = data.len() as u64;
|
let eoa = data.len() as u64;
|
||||||
let mut c = Cursor::new(block, bad(RAN_OFF));
|
let mut c = Cursor::new(block, bad(RAN_OFF));
|
||||||
|
|
||||||
@@ -337,11 +367,12 @@ 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
|
// 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).
|
// and the superblock extension's object header (read to find the
|
||||||
// Each entry's flush-dependency parents are looked up in the cache as
|
// image). Each entry's flush-dependency parents are looked up in the
|
||||||
// the entry is inserted (`H5C__reconstruct_cache_contents` searches the
|
// cache as the entry is inserted (`H5C__reconstruct_cache_contents`
|
||||||
// index inside the loop that inserts the entries, in HDF5 1.14.6 and
|
// searches the index inside the loop that inserts the entries, in
|
||||||
// 2.0.0 alike), so a parent must be one of those or an earlier entry.
|
// 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();
|
let mut cached = BTreeSet::new();
|
||||||
cached.insert(0);
|
cached.insert(0);
|
||||||
if let Some(ext) = sb.superblock_extension_address {
|
if let Some(ext) = sb.superblock_extension_address {
|
||||||
@@ -362,8 +393,8 @@ pub fn apply_cache_image(
|
|||||||
}
|
}
|
||||||
let children = c.uint(2)?;
|
let children = c.uint(2)?;
|
||||||
// libhdf5 checks the parent flag against the child count only in
|
// libhdf5 checks the parent flag against the child count only in
|
||||||
// debug builds (release builds refuse any entry with children); the
|
// debug builds (release builds refuse any entry with children);
|
||||||
// image format's own rule is checked here.
|
// the image format's own rule is checked here.
|
||||||
if (flags & MDCI_ENTRY_IS_FD_PARENT != 0) != (children > 0) {
|
if (flags & MDCI_ENTRY_IS_FD_PARENT != 0) != (children > 0) {
|
||||||
return Err(bad("flush dependency parent flag and child count disagree"));
|
return Err(bad("flush dependency parent flag and child count disagree"));
|
||||||
}
|
}
|
||||||
@@ -392,6 +423,9 @@ pub fn apply_cache_image(
|
|||||||
let len = usize::try_from(size).map_err(|_| bad(RAN_OFF))?;
|
let len = usize::try_from(size).map_err(|_| bad(RAN_OFF))?;
|
||||||
let image_offset = c.pos;
|
let image_offset = c.pos;
|
||||||
c.take(len)?;
|
c.take(len)?;
|
||||||
|
if address.checked_add(size).is_none_or(|end| end > eoa) {
|
||||||
|
return Err(bad("entry extends past the end of file"));
|
||||||
|
}
|
||||||
if !seen.insert(address) {
|
if !seen.insert(address) {
|
||||||
return Err(bad("duplicate addresses in cache"));
|
return Err(bad("duplicate addresses in cache"));
|
||||||
}
|
}
|
||||||
@@ -401,33 +435,94 @@ pub fn apply_cache_image(
|
|||||||
len,
|
len,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Ok(CacheImage { location, entries })
|
||||||
|
}
|
||||||
|
|
||||||
let mut out = data.to_vec();
|
/// Where the image block is.
|
||||||
for e in &entries {
|
pub fn location(&self) -> CacheImageLocation {
|
||||||
// address < eoa <= usize::MAX, and the entry's bytes came from the
|
self.location
|
||||||
// block, so neither conversion nor the sum can fail.
|
|
||||||
let at = e.address as usize;
|
|
||||||
let end = at + e.len;
|
|
||||||
if end > out.len() {
|
|
||||||
out.resize(end, 0);
|
|
||||||
}
|
}
|
||||||
out[at..end].copy_from_slice(&block[e.image_offset..e.image_offset + e.len]);
|
|
||||||
|
/// The number of entries in the image.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.entries.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the image has no entries (a decoded image always has some).
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.entries.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file ranges (address, length) the image's entries replace.
|
||||||
|
pub fn entry_ranges(&self) -> impl Iterator<Item = (u64, usize)> + '_ {
|
||||||
|
self.entries.iter().map(|e| (e.address, e.len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The image block in `data`, the bytes [`Self::decode`] read it from.
|
||||||
|
pub fn block<'a>(&self, data: &'a [u8]) -> Result<&'a [u8], FormatError> {
|
||||||
|
image_block(data, self.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write every entry over `dst`, the file's bytes from the superblock
|
||||||
|
/// on (as long as the `data` the image was decoded from), taking the
|
||||||
|
/// entries from `block` (the image block, see [`Self::block`]). `block`
|
||||||
|
/// must not alias `dst`: an entry may land on the block itself.
|
||||||
|
pub fn apply(&self, block: &[u8], dst: &mut [u8]) -> Result<(), FormatError> {
|
||||||
|
let short = || FormatError::InvalidCacheImage("image applied to the wrong file");
|
||||||
|
for e in &self.entries {
|
||||||
|
let src = block
|
||||||
|
.get(e.image_offset..e.image_offset + e.len)
|
||||||
|
.ok_or_else(short)?;
|
||||||
|
let at = usize::try_from(e.address).map_err(|_| short())?;
|
||||||
|
dst.get_mut(at..at + e.len)
|
||||||
|
.ok_or_else(short)?
|
||||||
|
.copy_from_slice(src);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a reader must do before reading a file's metadata, in one call:
|
fn image_block(data: &[u8], location: CacheImageLocation) -> Result<&[u8], FormatError> {
|
||||||
/// check the superblock extension ([`read_superblock_extension`]) and,
|
let bad = FormatError::InvalidCacheImage;
|
||||||
/// when the file has a metadata cache image, return the file's bytes with
|
let start = usize::try_from(location.address).map_err(|_| bad("address out of range"))?;
|
||||||
/// the image applied ([`apply_cache_image`]). `Ok(None)` means read `data`
|
let len = usize::try_from(location.length).map_err(|_| bad("length out of range"))?;
|
||||||
/// as it is.
|
start
|
||||||
pub fn metadata_view(data: &[u8], sb: &Superblock) -> Result<Option<Vec<u8>>, FormatError> {
|
.checked_add(len)
|
||||||
|
.and_then(|end| data.get(start..end))
|
||||||
|
.ok_or(bad("image block extends past the end of the file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What an opener must do before reading a file's metadata: check the
|
||||||
|
/// superblock extension ([`read_superblock_extension`]; an error means
|
||||||
|
/// libhdf5 refuses to open the file) and decode any metadata cache image
|
||||||
|
/// ([`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<CacheImageState, FormatError> {
|
||||||
match read_superblock_extension(data, sb)? {
|
match read_superblock_extension(data, sb)? {
|
||||||
Some(SuperblockExtension {
|
Some(SuperblockExtension {
|
||||||
cache_image: Some(location),
|
cache_image: Some(location),
|
||||||
..
|
..
|
||||||
}) => apply_cache_image(data, location, sb).map(Some),
|
}) => Ok(match CacheImage::decode(data, location, sb) {
|
||||||
_ => Ok(None),
|
Ok(image) => CacheImageState::Loaded(image),
|
||||||
|
Err(e) => CacheImageState::Unloadable(e),
|
||||||
|
}),
|
||||||
|
_ => Ok(CacheImageState::Absent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`cache_image_state`] for a reader that holds the file's bytes in a
|
||||||
|
/// buffer of its own: check the superblock extension and write any cache
|
||||||
|
/// image over `data` in place (only the image block is copied). An image
|
||||||
|
/// libhdf5 cannot load is an error here: such a reader has no way to open
|
||||||
|
/// the file and fail each object instead.
|
||||||
|
pub fn apply_cache_image_in_place(data: &mut [u8], sb: &Superblock) -> Result<(), FormatError> {
|
||||||
|
match cache_image_state(data, sb)? {
|
||||||
|
CacheImageState::Absent => Ok(()),
|
||||||
|
CacheImageState::Unloadable(e) => Err(e),
|
||||||
|
CacheImageState::Loaded(image) => {
|
||||||
|
let block = image.block(data)?.to_vec();
|
||||||
|
image.apply(&block, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +530,18 @@ pub fn metadata_view(data: &[u8], sb: &Superblock) -> Result<Option<Vec<u8>>, Fo
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// The file's bytes with the image at `loc` applied.
|
||||||
|
fn apply_cache_image(
|
||||||
|
data: &[u8],
|
||||||
|
loc: CacheImageLocation,
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
let image = CacheImage::decode(data, loc, sb)?;
|
||||||
|
let mut out = data.to_vec();
|
||||||
|
image.apply(image.block(data)?, &mut out)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
fn sb_v2(ext: u64) -> Superblock {
|
fn sb_v2(ext: u64) -> Superblock {
|
||||||
Superblock {
|
Superblock {
|
||||||
version: 2,
|
version: 2,
|
||||||
@@ -651,6 +758,12 @@ mod tests {
|
|||||||
let mut len = image(&[(16, b"x")]);
|
let mut len = image(&[(16, b"x")]);
|
||||||
len[6] ^= 1;
|
len[6] ^= 1;
|
||||||
assert!(matches!(bad(len), FormatError::InvalidCacheImage(_)));
|
assert!(matches!(bad(len), FormatError::InvalidCacheImage(_)));
|
||||||
|
// An entry that starts inside the file (64 bytes, then a 60-byte
|
||||||
|
// image) but runs past its end.
|
||||||
|
assert!(matches!(
|
||||||
|
bad(image(&[(123, b"8 bytes!")])),
|
||||||
|
FormatError::InvalidCacheImage("entry extends past the end of file")
|
||||||
|
));
|
||||||
let mut cut = image(&[(16, b"abcdef")]);
|
let mut cut = image(&[(16, b"abcdef")]);
|
||||||
let n = cut.len() as u64 - 8;
|
let n = cut.len() as u64 - 8;
|
||||||
cut.truncate(cut.len() - 8);
|
cut.truncate(cut.len() - 8);
|
||||||
|
|||||||
@@ -41,6 +41,65 @@ pub trait HDF5Read {
|
|||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.as_bytes().is_empty()
|
self.as_bytes().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A private, writable copy of [`Self::as_bytes`]: writes to it stay in
|
||||||
|
/// this process and never reach the underlying storage.
|
||||||
|
///
|
||||||
|
/// Readers use it to lay a file's metadata cache image over the file's
|
||||||
|
/// own metadata. The default copies the bytes; a memory-mapped reader
|
||||||
|
/// returns a copy-on-write mapping instead, so only the pages written to
|
||||||
|
/// are copied and the rest stay shared with the page cache.
|
||||||
|
fn private_copy(&self) -> io::Result<PrivateCopy> {
|
||||||
|
Ok(PrivateCopy::Owned(self.as_bytes().to_vec()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A private, writable copy of a file's bytes (see
|
||||||
|
/// [`HDF5Read::private_copy`]).
|
||||||
|
pub enum PrivateCopy {
|
||||||
|
/// The bytes copied onto the heap.
|
||||||
|
Owned(Vec<u8>),
|
||||||
|
/// A copy-on-write mapping of the file: pages are copied only when
|
||||||
|
/// written to.
|
||||||
|
#[cfg(feature = "mmap")]
|
||||||
|
Mapped(memmap2::MmapMut),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrivateCopy {
|
||||||
|
/// Whether this is a copy-on-write mapping rather than a heap copy.
|
||||||
|
pub fn is_mapped(&self) -> bool {
|
||||||
|
!matches!(self, PrivateCopy::Owned(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PrivateCopy {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("PrivateCopy")
|
||||||
|
.field("len", &self.len())
|
||||||
|
.field("mapped", &self.is_mapped())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for PrivateCopy {
|
||||||
|
type Target = [u8];
|
||||||
|
fn deref(&self) -> &[u8] {
|
||||||
|
match self {
|
||||||
|
PrivateCopy::Owned(v) => v,
|
||||||
|
#[cfg(feature = "mmap")]
|
||||||
|
PrivateCopy::Mapped(m) => m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for PrivateCopy {
|
||||||
|
fn deref_mut(&mut self) -> &mut [u8] {
|
||||||
|
match self {
|
||||||
|
PrivateCopy::Owned(v) => v,
|
||||||
|
#[cfg(feature = "mmap")]
|
||||||
|
PrivateCopy::Mapped(m) => m,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read-write access to HDF5 data.
|
/// Read-write access to HDF5 data.
|
||||||
|
|||||||
@@ -87,6 +87,19 @@ impl HDF5Read for MmapReader {
|
|||||||
fn as_bytes(&self) -> &[u8] {
|
fn as_bytes(&self) -> &[u8] {
|
||||||
&self.mmap
|
&self.mmap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A private copy-on-write mapping of the file (`MAP_PRIVATE`): only the
|
||||||
|
/// pages written to are copied.
|
||||||
|
fn private_copy(&self) -> io::Result<crate::PrivateCopy> {
|
||||||
|
if self.mmap.is_empty() {
|
||||||
|
return Ok(crate::PrivateCopy::Owned(Vec::new()));
|
||||||
|
}
|
||||||
|
// SAFETY: as for `open`: the caller keeps the file from being
|
||||||
|
// modified while the mapping is alive. Writes to a private mapping
|
||||||
|
// never reach the file.
|
||||||
|
let map = unsafe { memmap2::MmapOptions::new().map_copy(&self._file)? };
|
||||||
|
Ok(crate::PrivateCopy::Mapped(map))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writable memory-mapped file for read-write HDF5 access.
|
/// Writable memory-mapped file for read-write HDF5 access.
|
||||||
@@ -218,6 +231,22 @@ mod tests {
|
|||||||
fs::remove_file(&path).ok();
|
fs::remove_file(&path).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_copy_is_a_copy_on_write_mapping() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("cow.bin");
|
||||||
|
fs::write(&path, [1u8, 2, 3, 4]).unwrap();
|
||||||
|
let reader = MmapReader::open(&path).unwrap();
|
||||||
|
let mut copy = reader.private_copy().unwrap();
|
||||||
|
assert!(copy.is_mapped());
|
||||||
|
copy[1] = 99;
|
||||||
|
assert_eq!(©[..], &[1, 99, 3, 4]);
|
||||||
|
// Neither the reader's mapping nor the file sees the write.
|
||||||
|
assert_eq!(reader.as_bytes(), &[1, 2, 3, 4]);
|
||||||
|
drop(copy);
|
||||||
|
assert_eq!(fs::read(&path).unwrap(), [1, 2, 3, 4]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mmap_reader_read_at() {
|
fn mmap_reader_read_at() {
|
||||||
let dir = std::env::temp_dir();
|
let dir = std::env::temp_dir();
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ impl<R: HDF5Read> HDF5Read for PrefetchReader<R> {
|
|||||||
fn as_bytes(&self) -> &[u8] {
|
fn as_bytes(&self) -> &[u8] {
|
||||||
self.inner.as_bytes()
|
self.inner.as_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn private_copy(&self) -> std::io::Result<crate::PrivateCopy> {
|
||||||
|
self.inner.private_copy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//! Metadata cache images, as the file openers apply them.
|
||||||
|
//!
|
||||||
|
//! A file written with a metadata cache image keeps metadata cache entries
|
||||||
|
//! (object headers, B-tree nodes, heaps) in an image block, and libhdf5
|
||||||
|
//! reads those entries in place of the file's own bytes at their addresses
|
||||||
|
//! (see `clawhdf5_format::superblock_ext`). The metadata parsers read one
|
||||||
|
//! contiguous byte slice, so the image has to be laid over the file's bytes
|
||||||
|
//! — without copying the file:
|
||||||
|
//!
|
||||||
|
//! - an opener that holds the file in a buffer it owns writes the entries
|
||||||
|
//! into that buffer (only the image block is copied, as libhdf5 copies
|
||||||
|
//! it);
|
||||||
|
//! - an opener that maps the file ([`File::open`](crate::File::open),
|
||||||
|
//! [`MmapFile`](crate::MmapFile), [`LazyFile::open_mmap`]
|
||||||
|
//! (crate::LazyFile::open_mmap)) writes them into a private copy-on-write
|
||||||
|
//! mapping of the file ([`clawhdf5_io::HDF5Read::private_copy`]): only the
|
||||||
|
//! pages the entries land on are copied, and the rest of the file stays
|
||||||
|
//! shared with the page cache;
|
||||||
|
//! - a file without an image is read from the original bytes, as before.
|
||||||
|
|
||||||
|
use clawhdf5_format::error::FormatError;
|
||||||
|
use clawhdf5_format::superblock::Superblock;
|
||||||
|
use clawhdf5_format::superblock_ext::{self, CacheImageState};
|
||||||
|
use clawhdf5_io::PrivateCopy;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
/// A file's metadata, as an opener must read it.
|
||||||
|
pub(crate) enum ImageView {
|
||||||
|
/// Read the opener's bytes: the file has no cache image, or it was
|
||||||
|
/// written into a buffer the opener owns.
|
||||||
|
Plain,
|
||||||
|
/// The file with its cache image written in: a private copy of the
|
||||||
|
/// whole file (the HDF5 data at the same offsets as in the file).
|
||||||
|
Patched(PrivateCopy),
|
||||||
|
/// The file has a cache image libhdf5 cannot load.
|
||||||
|
Unloadable(FormatError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the superblock extension of the file whose bytes are `whole` (the
|
||||||
|
/// HDF5 data in `base..end`) and lay any cache image over a private copy
|
||||||
|
/// of the file made by `copy`. An error means libhdf5 refuses the file.
|
||||||
|
pub(crate) fn private_view(
|
||||||
|
whole: &[u8],
|
||||||
|
base: usize,
|
||||||
|
end: usize,
|
||||||
|
sb: &Superblock,
|
||||||
|
copy: impl FnOnce() -> std::io::Result<PrivateCopy>,
|
||||||
|
) -> Result<ImageView, Error> {
|
||||||
|
let data = &whole[base..end];
|
||||||
|
Ok(match superblock_ext::cache_image_state(data, sb)? {
|
||||||
|
CacheImageState::Absent => ImageView::Plain,
|
||||||
|
CacheImageState::Unloadable(e) => ImageView::Unloadable(e),
|
||||||
|
CacheImageState::Loaded(image) => {
|
||||||
|
let mut view = copy().map_err(Error::Io)?;
|
||||||
|
let dst =
|
||||||
|
view.get_mut(base..end)
|
||||||
|
.ok_or(Error::Format(FormatError::InvalidCacheImage(
|
||||||
|
"the file changed while it was opened",
|
||||||
|
)))?;
|
||||||
|
image.apply(image.block(data)?, dst)?;
|
||||||
|
ImageView::Patched(view)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`private_view`] for a file held in `whole`, a buffer the opener owns:
|
||||||
|
/// the image is written into it in place.
|
||||||
|
pub(crate) fn in_place(
|
||||||
|
whole: &mut [u8],
|
||||||
|
base: usize,
|
||||||
|
end: usize,
|
||||||
|
sb: &Superblock,
|
||||||
|
) -> Result<ImageView, Error> {
|
||||||
|
let data = &mut whole[base..end];
|
||||||
|
Ok(match superblock_ext::cache_image_state(data, sb)? {
|
||||||
|
CacheImageState::Absent => ImageView::Plain,
|
||||||
|
CacheImageState::Unloadable(e) => ImageView::Unloadable(e),
|
||||||
|
CacheImageState::Loaded(image) => {
|
||||||
|
let block = image.block(data)?.to_vec();
|
||||||
|
image.apply(&block, data)?;
|
||||||
|
ImageView::Plain
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+23
-11
@@ -46,9 +46,13 @@ pub struct LazyFile<R: HDF5Read> {
|
|||||||
/// End of the HDF5 data (`Superblock::data_end`, absolute).
|
/// End of the HDF5 data (`Superblock::data_end`, absolute).
|
||||||
end: usize,
|
end: usize,
|
||||||
superblock: Superblock,
|
superblock: Superblock,
|
||||||
/// The metadata as libhdf5 reads it when the file holds a metadata
|
/// A file that holds a metadata cache image, with the image written in:
|
||||||
/// cache image (see `superblock_ext::metadata_view`); `None` otherwise.
|
/// the reader's [`HDF5Read::private_copy`] of the whole file (a
|
||||||
overlay: Option<Vec<u8>>,
|
/// copy-on-write mapping for [`clawhdf5_io::MmapReader`], so only the
|
||||||
|
/// pages the image's entries land on are copied; see
|
||||||
|
/// `crate::cache_image`). `None` for a file without an image, read
|
||||||
|
/// straight from the reader.
|
||||||
|
patched: Option<clawhdf5_io::PrivateCopy>,
|
||||||
root_header: ObjectHeader,
|
root_header: ObjectHeader,
|
||||||
/// Cache of parsed object headers, keyed by address.
|
/// Cache of parsed object headers, keyed by address.
|
||||||
header_cache: RefCell<HashMap<u64, ObjectHeader>>,
|
header_cache: RefCell<HashMap<u64, ObjectHeader>>,
|
||||||
@@ -87,11 +91,19 @@ impl<R: HDF5Read> LazyFile<R> {
|
|||||||
let end = base + superblock.data_end(base as u64, whole_len)? as usize;
|
let end = base + superblock.data_end(base as u64, whole_len)? as usize;
|
||||||
// Decode the superblock extension as libhdf5 does at open, and load
|
// Decode the superblock extension as libhdf5 does at open, and load
|
||||||
// a metadata cache image over the file's metadata.
|
// a metadata cache image over the file's metadata.
|
||||||
let overlay = clawhdf5_format::superblock_ext::metadata_view(
|
let view =
|
||||||
&reader.as_bytes()[base..end],
|
crate::cache_image::private_view(reader.as_bytes(), base, end, &superblock, || {
|
||||||
&superblock,
|
reader.private_copy()
|
||||||
)?;
|
})?;
|
||||||
let data = overlay.as_deref().unwrap_or(&reader.as_bytes()[base..end]);
|
let patched = match view {
|
||||||
|
crate::cache_image::ImageView::Plain => None,
|
||||||
|
crate::cache_image::ImageView::Patched(p) => Some(p),
|
||||||
|
crate::cache_image::ImageView::Unloadable(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
let data = match &patched {
|
||||||
|
Some(p) => &p[base..end],
|
||||||
|
None => &reader.as_bytes()[base..end],
|
||||||
|
};
|
||||||
let root_header = ObjectHeader::parse(
|
let root_header = ObjectHeader::parse(
|
||||||
data,
|
data,
|
||||||
superblock.root_group_address as usize,
|
superblock.root_group_address as usize,
|
||||||
@@ -103,7 +115,7 @@ impl<R: HDF5Read> LazyFile<R> {
|
|||||||
base,
|
base,
|
||||||
end,
|
end,
|
||||||
superblock,
|
superblock,
|
||||||
overlay,
|
patched,
|
||||||
root_header,
|
root_header,
|
||||||
header_cache: RefCell::new(HashMap::new()),
|
header_cache: RefCell::new(HashMap::new()),
|
||||||
})
|
})
|
||||||
@@ -121,8 +133,8 @@ impl<R: HDF5Read> LazyFile<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn hdf5_bytes(&self) -> &[u8] {
|
fn hdf5_bytes(&self) -> &[u8] {
|
||||||
match &self.overlay {
|
match &self.patched {
|
||||||
Some(v) => v,
|
Some(p) => &p[self.base..self.end],
|
||||||
None => &self.reader.as_bytes()[self.base..self.end],
|
None => &self.reader.as_bytes()[self.base..self.end],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
//! builder.write("output.h5").unwrap();
|
//! builder.write("output.h5").unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
mod cache_image;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod lazy;
|
pub mod lazy;
|
||||||
#[cfg(feature = "mmap")]
|
#[cfg(feature = "mmap")]
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ pub struct MmapFile {
|
|||||||
/// End of the HDF5 data (`Superblock::data_end`, absolute).
|
/// End of the HDF5 data (`Superblock::data_end`, absolute).
|
||||||
end: usize,
|
end: usize,
|
||||||
superblock: Superblock,
|
superblock: Superblock,
|
||||||
/// The metadata as libhdf5 reads it when the file holds a metadata
|
/// A file that holds a metadata cache image, with the image written in:
|
||||||
/// cache image (see `superblock_ext::metadata_view`); `None` otherwise.
|
/// a private copy-on-write mapping of the whole file, so only the pages
|
||||||
overlay: Option<Vec<u8>>,
|
/// the image's entries land on are copied (see `crate::cache_image`).
|
||||||
|
/// `None` for a file without an image, read straight from the mapping.
|
||||||
|
patched: Option<clawhdf5_io::PrivateCopy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MmapFile {
|
impl MmapFile {
|
||||||
@@ -55,24 +57,29 @@ impl MmapFile {
|
|||||||
let end = base + superblock.data_end(base as u64, whole_len)? as usize;
|
let end = base + superblock.data_end(base as u64, whole_len)? as usize;
|
||||||
// Decode the superblock extension as libhdf5 does at open, and load
|
// Decode the superblock extension as libhdf5 does at open, and load
|
||||||
// a metadata cache image over the file's metadata.
|
// a metadata cache image over the file's metadata.
|
||||||
let overlay = clawhdf5_format::superblock_ext::metadata_view(
|
let view =
|
||||||
&reader.as_bytes()[base..end],
|
crate::cache_image::private_view(reader.as_bytes(), base, end, &superblock, || {
|
||||||
&superblock,
|
clawhdf5_io::HDF5Read::private_copy(&reader)
|
||||||
)?;
|
})?;
|
||||||
|
let patched = match view {
|
||||||
|
crate::cache_image::ImageView::Plain => None,
|
||||||
|
crate::cache_image::ImageView::Patched(p) => Some(p),
|
||||||
|
crate::cache_image::ImageView::Unloadable(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
reader,
|
reader,
|
||||||
base,
|
base,
|
||||||
end,
|
end,
|
||||||
superblock,
|
superblock,
|
||||||
overlay,
|
patched,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The file's bytes from the superblock on — the space HDF5 addresses
|
/// The file's bytes from the superblock on — the space HDF5 addresses
|
||||||
/// index into.
|
/// index into.
|
||||||
fn hdf5_bytes(&self) -> &[u8] {
|
fn hdf5_bytes(&self) -> &[u8] {
|
||||||
match &self.overlay {
|
match &self.patched {
|
||||||
Some(v) => v,
|
Some(p) => &p[self.base..self.end],
|
||||||
None => &self.reader.as_bytes()[self.base..self.end],
|
None => &self.reader.as_bytes()[self.base..self.end],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ use clawhdf5_format::message_type::MessageType;
|
|||||||
use clawhdf5_format::object_header::ObjectHeader;
|
use clawhdf5_format::object_header::ObjectHeader;
|
||||||
use clawhdf5_format::signature;
|
use clawhdf5_format::signature;
|
||||||
use clawhdf5_format::superblock::Superblock;
|
use clawhdf5_format::superblock::Superblock;
|
||||||
use clawhdf5_format::superblock_ext;
|
|
||||||
|
|
||||||
|
use crate::cache_image::{self, ImageView};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::types::{AttrValue, DType, classify_datatype, read_attrs};
|
use crate::types::{AttrValue, DType, classify_datatype, read_attrs};
|
||||||
|
|
||||||
@@ -56,17 +56,19 @@ struct FileData {
|
|||||||
base: usize,
|
base: usize,
|
||||||
/// End of the HDF5 data in the file (`Superblock::data_end`, absolute).
|
/// End of the HDF5 data in the file (`Superblock::data_end`, absolute).
|
||||||
end: usize,
|
end: usize,
|
||||||
/// The file's metadata as libhdf5 reads it when the file holds a
|
/// A mapped file that holds a metadata cache image, with the image
|
||||||
/// metadata cache image: the bytes from the superblock to the end of
|
/// written in: a private copy-on-write mapping of the whole file, so
|
||||||
/// file with the image's entries written in
|
/// only the pages the image's entries land on are copied (see
|
||||||
/// ([`superblock_ext::metadata_view`]). `None` for every other file.
|
/// `crate::cache_image`). `None` for every other file: a file without
|
||||||
overlay: Option<Vec<u8>>,
|
/// an image is read straight from the mapping, and an owned buffer has
|
||||||
|
/// the image written into it in place.
|
||||||
|
patched: Option<clawhdf5_io::PrivateCopy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileData {
|
impl FileData {
|
||||||
/// Locate the superblock and parse it. A truncated file is refused, and
|
/// Locate the superblock and parse it. A truncated file is refused, and
|
||||||
/// bytes past the recorded end of file are not read, as in libhdf5.
|
/// bytes past the recorded end of file are not read, as in libhdf5.
|
||||||
fn new(backing: Backing) -> Result<(Self, Superblock), Error> {
|
fn new(mut backing: Backing) -> Result<(Self, Superblock), Error> {
|
||||||
let whole = backing.whole_file();
|
let whole = backing.whole_file();
|
||||||
let (user_block, hdf5) = signature::split_user_block(whole)?;
|
let (user_block, hdf5) = signature::split_user_block(whole)?;
|
||||||
let base = user_block.len();
|
let base = user_block.len();
|
||||||
@@ -77,21 +79,34 @@ impl FileData {
|
|||||||
// libhdf5 decodes the superblock extension at open (a message it
|
// libhdf5 decodes the superblock extension at open (a message it
|
||||||
// cannot decode fails the open) and loads a metadata cache image
|
// cannot decode fails the open) and loads a metadata cache image
|
||||||
// over the file's own metadata.
|
// over the file's own metadata.
|
||||||
let overlay = superblock_ext::metadata_view(&whole[base..end], &superblock)?;
|
let view = match &mut backing {
|
||||||
|
Backing::Owned(v) => cache_image::in_place(v, base, end, &superblock)?,
|
||||||
|
#[cfg(feature = "mmap")]
|
||||||
|
Backing::Mmap(r) => {
|
||||||
|
cache_image::private_view(r.as_bytes(), base, end, &superblock, || {
|
||||||
|
clawhdf5_io::HDF5Read::private_copy(r)
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let patched = match view {
|
||||||
|
ImageView::Plain => None,
|
||||||
|
ImageView::Patched(p) => Some(p),
|
||||||
|
ImageView::Unloadable(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
Ok((
|
Ok((
|
||||||
Self {
|
Self {
|
||||||
backing,
|
backing,
|
||||||
base,
|
base,
|
||||||
end,
|
end,
|
||||||
overlay,
|
patched,
|
||||||
},
|
},
|
||||||
superblock,
|
superblock,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_bytes(&self) -> &[u8] {
|
fn as_bytes(&self) -> &[u8] {
|
||||||
match &self.overlay {
|
match &self.patched {
|
||||||
Some(v) => v,
|
Some(p) => &p[self.base..self.end],
|
||||||
None => &self.backing.whole_file()[self.base..self.end],
|
None => &self.backing.whole_file()[self.base..self.end],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1282,3 +1297,44 @@ mod sibling_file_name_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "mmap"))]
|
||||||
|
mod zero_copy_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Where `File::open` reads metadata from: `Some(true)` for the file's
|
||||||
|
/// own mapping, `Some(false)` for a private copy-on-write mapping.
|
||||||
|
fn reads_from_the_mapping(f: &File) -> Option<bool> {
|
||||||
|
let Backing::Mmap(r) = &f.data.backing else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mapped = r.as_bytes()[f.data.base..].as_ptr();
|
||||||
|
match &f.data.patched {
|
||||||
|
None => Some(std::ptr::eq(f.as_bytes().as_ptr(), mapped)),
|
||||||
|
Some(p) => {
|
||||||
|
assert!(p.is_mapped(), "the image went into a heap copy of the file");
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_file_without_a_cache_image_is_read_from_the_mapping() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("plain.h5");
|
||||||
|
let mut b = crate::FileBuilder::new();
|
||||||
|
b.create_dataset("d").with_f64_data(&[1.0, 2.0]);
|
||||||
|
b.write(&path).unwrap();
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
assert_eq!(reads_from_the_mapping(&f), Some(true));
|
||||||
|
assert_eq!(f.dataset("d").unwrap().read_f64().unwrap(), [1.0, 2.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cache_image_goes_into_a_copy_on_write_mapping() {
|
||||||
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests/fixtures/h5clear_mdc_image.h5");
|
||||||
|
let f = File::open(path).unwrap();
|
||||||
|
assert_eq!(reads_from_the_mapping(&f), Some(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
//! Opening a file with a metadata cache image must not copy the file.
|
||||||
|
//!
|
||||||
|
//! The image's entries are laid over the file's bytes in a private
|
||||||
|
//! copy-on-write mapping (`File::open`, `MmapFile::open`,
|
||||||
|
//! `LazyFile::open_mmap`), so only the pages they land on are copied. The
|
||||||
|
//! first implementation copied the whole file onto the heap at open: a
|
||||||
|
//! 1 GiB file that takes a few KB on disk needed 2 GB of memory, and an
|
||||||
|
//! 8 GiB one aborted the process. Here libhdf5 itself (the library h5py
|
||||||
|
//! bundles, through ctypes: `H5Pset_mdc_image_config`) adds an image to a
|
||||||
|
//! 1 GiB sparse file, and the process's resident memory must stay far below
|
||||||
|
//! the file's size while each opener lists the file and reads its small
|
||||||
|
//! dataset.
|
||||||
|
//!
|
||||||
|
//! One test in its own binary, so no other test's allocations land in the
|
||||||
|
//! measurement. Linux only (it reads `VmRSS` from `/proc/self/status`).
|
||||||
|
//! Skipped when python3 with h5py is unavailable, unless
|
||||||
|
//! `CLAWHDF5_REQUIRE_INTEROP=1`.
|
||||||
|
|
||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use clawhdf5::{File, LazyFile, MmapFile};
|
||||||
|
|
||||||
|
fn python() -> String {
|
||||||
|
std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn python_available() -> bool {
|
||||||
|
Command::new(python())
|
||||||
|
.args(["-c", "import h5py, numpy"])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rss_bytes() -> u64 {
|
||||||
|
let status = std::fs::read_to_string("/proc/self/status").unwrap();
|
||||||
|
let line = status.lines().find(|l| l.starts_with("VmRSS:")).unwrap();
|
||||||
|
let kb: u64 = line.split_whitespace().nth(1).unwrap().parse().unwrap();
|
||||||
|
kb * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 1 GiB sparse file: `/big`, 2^27 `f8` with only its last element
|
||||||
|
/// written, `/small` = 0..10, with a metadata cache image added by libhdf5.
|
||||||
|
fn make_file(path: &Path) {
|
||||||
|
let script = format!(
|
||||||
|
r#"
|
||||||
|
import ctypes, glob, os, h5py, numpy as np
|
||||||
|
path = "{path}"
|
||||||
|
libs = glob.glob(os.path.join(os.path.dirname(h5py.__file__), os.pardir, "h5py.libs", "libhdf5-*.so*"))
|
||||||
|
assert libs, "no libhdf5 bundled with h5py"
|
||||||
|
lib = ctypes.CDLL(libs[0])
|
||||||
|
class Cfg(ctypes.Structure):
|
||||||
|
_fields_ = [("version", ctypes.c_int), ("generate_image", ctypes.c_bool),
|
||||||
|
("save_resize_status", ctypes.c_bool), ("entry_ageout", ctypes.c_int)]
|
||||||
|
with h5py.File(path, "w", libver="latest") as f:
|
||||||
|
d = f.create_dataset("big", shape=(2**27,), dtype="f8")
|
||||||
|
d[-1] = 7.5
|
||||||
|
f.create_dataset("small", data=np.arange(10, dtype="<i4"))
|
||||||
|
fapl = h5py.h5p.create(h5py.h5p.FILE_ACCESS)
|
||||||
|
fapl.set_libver_bounds(h5py.h5f.LIBVER_LATEST, h5py.h5f.LIBVER_LATEST)
|
||||||
|
cfg = Cfg(1, True, False, -1)
|
||||||
|
assert lib.H5Pset_mdc_image_config(ctypes.c_int64(fapl.id), ctypes.byref(cfg)) >= 0
|
||||||
|
f = h5py.File(h5py.h5f.open(path.encode(), h5py.h5f.ACC_RDWR, fapl=fapl))
|
||||||
|
f["small"][()]; f["big"].shape
|
||||||
|
f.close()
|
||||||
|
assert os.path.getsize(path) >= 2**30
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
fh.seek(-(1 << 20), 2)
|
||||||
|
assert b"MDCI" in fh.read(), "libhdf5 wrote no cache image"
|
||||||
|
with h5py.File(path, "r") as f:
|
||||||
|
assert list(f["small"][()]) == list(range(10))
|
||||||
|
"#,
|
||||||
|
path = path.display()
|
||||||
|
);
|
||||||
|
let out = Command::new(python())
|
||||||
|
.args(["-c", &script])
|
||||||
|
.output()
|
||||||
|
.expect("failed to run python");
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"python failed:\n{}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cache_image_does_not_copy_the_file() {
|
||||||
|
if !python_available() {
|
||||||
|
assert!(
|
||||||
|
!std::env::var("CLAWHDF5_REQUIRE_INTEROP").is_ok_and(|v| v == "1"),
|
||||||
|
"CLAWHDF5_REQUIRE_INTEROP=1 but python3 with h5py is not available"
|
||||||
|
);
|
||||||
|
eprintln!("SKIP: python3 with h5py not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("sparse_image.h5");
|
||||||
|
make_file(&path);
|
||||||
|
|
||||||
|
const LIMIT: u64 = 256 << 20;
|
||||||
|
let small: Vec<i32> = (0..10).collect();
|
||||||
|
let before = rss_bytes();
|
||||||
|
{
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
let mut names = f.root().datasets().unwrap();
|
||||||
|
names.sort();
|
||||||
|
assert_eq!(names, ["big", "small"]);
|
||||||
|
assert_eq!(f.dataset("small").unwrap().read_i32().unwrap(), small);
|
||||||
|
assert_eq!(f.dataset("big").unwrap().shape().unwrap(), [1 << 27]);
|
||||||
|
let grew = rss_bytes().saturating_sub(before);
|
||||||
|
assert!(
|
||||||
|
grew < LIMIT,
|
||||||
|
"File::open: resident memory grew {grew} bytes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let before = rss_bytes();
|
||||||
|
{
|
||||||
|
let f = MmapFile::open(&path).unwrap();
|
||||||
|
assert_eq!(f.dataset("small").unwrap().read_i32().unwrap(), small);
|
||||||
|
let grew = rss_bytes().saturating_sub(before);
|
||||||
|
assert!(
|
||||||
|
grew < LIMIT,
|
||||||
|
"MmapFile::open: resident memory grew {grew} bytes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let before = rss_bytes();
|
||||||
|
{
|
||||||
|
let f = LazyFile::open_mmap(&path).unwrap();
|
||||||
|
assert_eq!(f.dataset("small").unwrap().read_i32().unwrap(), small);
|
||||||
|
let grew = rss_bytes().saturating_sub(before);
|
||||||
|
assert!(
|
||||||
|
grew < LIMIT,
|
||||||
|
"LazyFile::open_mmap: resident memory grew {grew} bytes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user