Chunked reads beat an h5py process pool; unlimited writer B-trees; Blosc2; 599/697 conformance #16
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inventory of whole-file `&[u8]` parameters in the clawhdf5 workspace.
|
||||
|
||||
Used by docs/design/range-reads.md. Run from the repository root:
|
||||
|
||||
python3 docs/design/tools/inventory.py # per-file table
|
||||
python3 docs/design/tools/inventory.py --list # every signature
|
||||
python3 docs/design/tools/inventory.py --patterns # read patterns per crate
|
||||
|
||||
A function counts as taking "the whole file" when it has a parameter named
|
||||
`file_data: &[u8]` (the repository convention), or a `data` / `file` / `buf` /
|
||||
`bytes` / `mmap` parameter of type `&[u8]` *together with* a parameter whose
|
||||
name says it is a file address (`*address*`, `*addr*`, `*offset*` of an
|
||||
integer type). The second rule is a heuristic; `--list` prints every match so
|
||||
it can be checked by eye. Code after the first `#[cfg(test)] mod ... {` in a
|
||||
file is excluded (the repository keeps unit tests at the end of each file),
|
||||
as are the tests/ and benches/ directories.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
ROOT = os.getcwd()
|
||||
FN_RE = re.compile(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)\s*(<[^()]*?>)?\s*\(", re.S)
|
||||
PARAM_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\s*:\s*&(?:'[a-z_]+\s+)?\[u8\]")
|
||||
ADDR_RE = re.compile(r"\b([a-z_]*(?:address|addr|offset)[a-z_]*)\s*:\s*(?:u64|usize|u32)")
|
||||
WHOLE_NAMES = {"data", "file", "buf", "bytes", "file_bytes", "mmap"}
|
||||
|
||||
|
||||
def signature(src, start):
|
||||
depth, i = 0, start
|
||||
while i < len(src):
|
||||
c = src[i]
|
||||
if c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[start + 1 : i]
|
||||
i += 1
|
||||
return ""
|
||||
|
||||
|
||||
def non_test_source(src):
|
||||
m = re.search(r"#\[cfg\(test\)\]\s*mod\s+\w+\s*\{", src)
|
||||
return src[: m.start()] if m else src
|
||||
|
||||
|
||||
PATTERNS = [
|
||||
("`file_data` passed on (call sites)", re.compile(r"[(,]\s*&?file_data\s*[,)]")),
|
||||
("`file_data[..]` slicing", re.compile(r"\bfile_data\s*\[")),
|
||||
("open-ended `file_data[x..]`", re.compile(r"\bfile_data\s*\[[^\]]*\.\.\s*\]")),
|
||||
("`file_data.get(..)`", re.compile(r"\bfile_data\s*\.\s*get\s*\(")),
|
||||
("`file_data.len()`", re.compile(r"\bfile_data\s*\.\s*len\s*\(\)")),
|
||||
("address/offset `as usize` casts", re.compile(r"\b[a-z_]*(?:addr|address|offset)[a-z_]*\s+as\s+usize")),
|
||||
("`ObjectHeader::parse(` calls", re.compile(r"ObjectHeader::parse\s*\(")),
|
||||
("`.as_bytes()` on a file/reader", re.compile(r"\b(?:file|reader|data|self\.file|self\.data|self\.file\.data|root\.file)\s*\.\s*as_bytes\s*\(\)")),
|
||||
]
|
||||
|
||||
|
||||
def patterns():
|
||||
"""Per-crate counts of the read patterns (non-test code only)."""
|
||||
per_crate = defaultdict(lambda: [0] * len(PATTERNS))
|
||||
for crate in sorted(os.listdir(os.path.join(ROOT, "crates"))):
|
||||
srcdir = os.path.join(ROOT, "crates", crate, "src")
|
||||
for dp, _, fns in os.walk(srcdir):
|
||||
for fn in fns:
|
||||
if not fn.endswith(".rs"):
|
||||
continue
|
||||
with open(os.path.join(dp, fn), encoding="utf-8") as fh:
|
||||
src = non_test_source(fh.read())
|
||||
for i, (_, rx) in enumerate(PATTERNS):
|
||||
per_crate[crate][i] += len(rx.findall(src))
|
||||
print("| crate | " + " | ".join(n for n, _ in PATTERNS) + " |")
|
||||
print("|---|" + "---:|" * len(PATTERNS))
|
||||
tot = [0] * len(PATTERNS)
|
||||
for c, v in sorted(per_crate.items()):
|
||||
if any(v):
|
||||
print("| %s | %s |" % (c, " | ".join(str(x) for x in v)))
|
||||
tot = [a + b for a, b in zip(tot, v)]
|
||||
print("| **total** | %s |" % " | ".join("**%d**" % x for x in tot))
|
||||
|
||||
|
||||
def main():
|
||||
if "--patterns" in sys.argv:
|
||||
patterns()
|
||||
return
|
||||
per_file = defaultdict(lambda: [0, 0, 0]) # [file_data, heuristic, pub]
|
||||
rows = []
|
||||
for crate in sorted(os.listdir(os.path.join(ROOT, "crates"))):
|
||||
srcdir = os.path.join(ROOT, "crates", crate, "src")
|
||||
for dp, _, fns in os.walk(srcdir):
|
||||
for fn in sorted(fns):
|
||||
if not fn.endswith(".rs"):
|
||||
continue
|
||||
path = os.path.join(dp, fn)
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
src = non_test_source(fh.read())
|
||||
for m in FN_RE.finditer(src):
|
||||
sig = signature(src, m.end() - 1)
|
||||
params = PARAM_RE.findall(sig)
|
||||
kind = None
|
||||
if "file_data" in params:
|
||||
kind = "file_data"
|
||||
elif any(p in WHOLE_NAMES for p in params) and ADDR_RE.search(sig):
|
||||
kind = "heuristic"
|
||||
if not kind:
|
||||
continue
|
||||
rel = os.path.relpath(path, ROOT)
|
||||
line_start = src.rfind("\n", 0, m.start()) + 1
|
||||
is_pub = src[line_start : m.start()].strip().startswith("pub")
|
||||
per_file[rel][0 if kind == "file_data" else 1] += 1
|
||||
per_file[rel][2] += int(is_pub)
|
||||
line = src.count("\n", 0, m.start()) + 1
|
||||
rows.append((rel, line, m.group(1), kind, is_pub))
|
||||
if "--list" in sys.argv:
|
||||
for r in rows:
|
||||
print("%s:%d %s [%s%s]" % (r[0], r[1], r[2], r[3], ", pub" if r[4] else ""))
|
||||
return
|
||||
print("| file | `file_data` fns | other whole-slice fns (heuristic) | of which `pub` |")
|
||||
print("|---|---:|---:|---:|")
|
||||
tot = [0, 0, 0]
|
||||
for f, (a, b, p) in sorted(per_file.items(), key=lambda kv: (-(kv[1][0] + kv[1][1]), kv[0])):
|
||||
print("| %s | %d | %d | %d |" % (f, a, b, p))
|
||||
tot = [tot[0] + a, tot[1] + b, tot[2] + p]
|
||||
print("| **total** | **%d** | **%d** | **%d** |" % tuple(tot))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The libhdf5 side of the range-read measurement (docs/design/range-reads.md).
|
||||
|
||||
libhdf5_reads.py FILE DATASET # count libhdf5's reads
|
||||
libhdf5_reads.py FILE DATASET --extents # print DATASET's stored extents
|
||||
|
||||
Counting: the file is opened through h5py's `fileobj` driver with a Python
|
||||
file-like object that logs every `readinto`/`read` libhdf5 makes (this is how
|
||||
h5py + fsspec reads remote files: each call becomes a range request unless
|
||||
fsspec's own block cache absorbs it). The phases mirror range-trace: open,
|
||||
list (visit every object; shape and dtype of every dataset), read (the whole
|
||||
dataset). Default h5py settings (libhdf5's metadata cache, 64 KiB sieve
|
||||
buffer, 1 MiB raw chunk cache) apply.
|
||||
|
||||
--extents prints `offset size` lines for the dataset's stored data (the
|
||||
contiguous block, or every allocated chunk), the input range-trace uses to
|
||||
split its read phase into metadata and raw data.
|
||||
"""
|
||||
import sys
|
||||
|
||||
import h5py
|
||||
|
||||
|
||||
class LoggingFile:
|
||||
def __init__(self, path):
|
||||
self.f = open(path, "rb")
|
||||
self.pos = 0
|
||||
self.log = []
|
||||
|
||||
def seek(self, off, whence=0):
|
||||
self.pos = self.f.seek(off, whence)
|
||||
return self.pos
|
||||
|
||||
def tell(self):
|
||||
return self.pos
|
||||
|
||||
def readinto(self, b):
|
||||
n = self.f.readinto(b)
|
||||
self.log.append((self.pos, n))
|
||||
self.pos += n
|
||||
return n
|
||||
|
||||
def read(self, size=-1):
|
||||
data = self.f.read(size)
|
||||
self.log.append((self.pos, len(data)))
|
||||
self.pos += len(data)
|
||||
return data
|
||||
|
||||
|
||||
def extents(path, name):
|
||||
with h5py.File(path, "r") as f:
|
||||
ds = f[name]
|
||||
if ds.chunks is None:
|
||||
off = ds.id.get_offset()
|
||||
if off is not None:
|
||||
print(off, ds.id.get_storage_size())
|
||||
return
|
||||
for i in range(ds.id.get_num_chunks()):
|
||||
info = ds.id.get_chunk_info(i)
|
||||
print(info.byte_offset, info.size)
|
||||
|
||||
|
||||
def summarise(label, log):
|
||||
n = len(log)
|
||||
total = sum(s for _, s in log)
|
||||
distinct = len(set(log))
|
||||
print("| %s | %d | %d | %d |" % (label, n, distinct, total))
|
||||
|
||||
|
||||
def count(path, name):
|
||||
lf = LoggingFile(path)
|
||||
f = h5py.File(lf, "r")
|
||||
opened = list(lf.log)
|
||||
lf.log.clear()
|
||||
|
||||
def visit(_n, obj):
|
||||
if isinstance(obj, h5py.Dataset):
|
||||
obj.shape, obj.dtype
|
||||
|
||||
f.visititems(visit)
|
||||
listed = list(lf.log)
|
||||
lf.log.clear()
|
||||
f[name][()]
|
||||
readlog = list(lf.log)
|
||||
f.close()
|
||||
print("| phase | read calls | distinct (offset, len) | bytes |")
|
||||
print("|---|---:|---:|---:|")
|
||||
summarise("open", opened)
|
||||
summarise("list", listed)
|
||||
summarise("read", readlog)
|
||||
summarise("open+list+read", opened + listed + readlog)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) >= 4 and sys.argv[3] == "--extents":
|
||||
extents(sys.argv[1], sys.argv[2])
|
||||
else:
|
||||
count(sys.argv[1], sys.argv[2])
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "range-trace"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.92"
|
||||
publish = false
|
||||
description = "Records which byte ranges of an HDF5 file the clawhdf5 facade reads (docs/design/range-reads.md)"
|
||||
|
||||
# Outside the main workspace on purpose: a measurement tool for a design
|
||||
# document, never built by `cargo test --workspace`. x86-64 Linux only.
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
# Default features minus nothing: `parallel` is off by default, and the tracer
|
||||
# relies on every read happening on the main thread.
|
||||
clawhdf5 = { path = "../../../../crates/clawhdf5" }
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,375 @@
|
||||
//! range-trace: which bytes of an HDF5 file does clawhdf5 read?
|
||||
//!
|
||||
//! Measurement tool for `docs/design/range-reads.md` (x86-64 Linux only).
|
||||
//!
|
||||
//! The file is loaded into a page-aligned buffer and handed to
|
||||
//! `clawhdf5::File::from_bytes`, so the library parses it exactly as it does
|
||||
//! today. The buffer is then `mprotect`ed to `PROT_NONE`. Every load from it
|
||||
//! faults; the SIGSEGV handler logs the exact faulting address, makes that
|
||||
//! page readable and sets the x86 trap flag, so the CPU single-steps the one
|
||||
//! instruction and the SIGTRAP handler re-protects the page. Every load
|
||||
//! instruction that touches the file is therefore recorded (with its first
|
||||
//! byte; widths are not decoded, see `ACCESS_WIDTH`).
|
||||
//!
|
||||
//! Bulk copies (raw data) would fault once per load; a page that faults more
|
||||
//! than `BULK_THRESHOLD` times in one phase is left readable for the rest of
|
||||
//! that phase and counted as wholly read ("bulk page").
|
||||
//!
|
||||
//! Phases: `open` (superblock), `list` (walk every group; for every dataset
|
||||
//! its shape and dtype, i.e. what `h5ls -r -v` or a tree view needs), and
|
||||
//! `read` (read the named dataset in full through `read_selection(All)`).
|
||||
//!
|
||||
//! Usage: range-trace FILE DATASET_PATH [RAW_EXTENTS]
|
||||
//!
|
||||
//! RAW_EXTENTS (optional) lists the dataset's stored data as `offset size`
|
||||
//! lines (absolute file offsets; `libhdf5_reads.py --extents` writes it from
|
||||
//! h5py). With it the `read` phase is split into `read (metadata)`, the
|
||||
//! loads outside those extents, and `read (raw data)`, the loads inside.
|
||||
//!
|
||||
//! Every read must happen on this thread: build without the facade's
|
||||
//! `parallel` feature (off by default).
|
||||
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
|
||||
|
||||
use clawhdf5::{File, Group, Selection};
|
||||
|
||||
const PAGE: usize = 4096;
|
||||
const BULK_THRESHOLD: u32 = 4096;
|
||||
/// Bytes assumed read by one logged load (the widest scalar load the parsers
|
||||
/// issue; SIMD copies are bulk anyway).
|
||||
const ACCESS_WIDTH: u64 = 8;
|
||||
/// Two accesses closer than this belong to the same structure, i.e. would be
|
||||
/// one range request.
|
||||
const MERGE_GAP: u64 = 64;
|
||||
const LOG_CAP: usize = 64 << 20;
|
||||
|
||||
static BUF_START: AtomicUsize = AtomicUsize::new(0);
|
||||
static BUF_LEN: AtomicUsize = AtomicUsize::new(0);
|
||||
static LOG: AtomicPtr<u64> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static LOG_LEN: AtomicUsize = AtomicUsize::new(0);
|
||||
static HITS: AtomicPtr<u32> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static BULK: AtomicPtr<u8> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static PENDING: [AtomicUsize; 8] = [const { AtomicUsize::new(0) }; 8];
|
||||
static PENDING_N: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
const TF: i64 = 0x100;
|
||||
|
||||
extern "C" fn on_segv(_sig: libc::c_int, info: *mut libc::siginfo_t, ctx: *mut libc::c_void) {
|
||||
unsafe {
|
||||
let addr = (*info).si_addr() as usize;
|
||||
let start = BUF_START.load(Ordering::Relaxed);
|
||||
let len = BUF_LEN.load(Ordering::Relaxed);
|
||||
if addr < start || addr >= start + len {
|
||||
// A genuine crash: restore the default action and re-fault.
|
||||
libc::signal(libc::SIGSEGV, libc::SIG_DFL);
|
||||
return;
|
||||
}
|
||||
let off = addr - start;
|
||||
let n = LOG_LEN.load(Ordering::Relaxed);
|
||||
if n < LOG_CAP {
|
||||
*LOG.load(Ordering::Relaxed).add(n) = off as u64;
|
||||
LOG_LEN.store(n + 1, Ordering::Relaxed);
|
||||
}
|
||||
let page = off / PAGE;
|
||||
let hits = HITS.load(Ordering::Relaxed).add(page);
|
||||
*hits += 1;
|
||||
libc::mprotect(
|
||||
(start + page * PAGE) as *mut libc::c_void,
|
||||
PAGE,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
);
|
||||
if *hits >= BULK_THRESHOLD {
|
||||
*BULK.load(Ordering::Relaxed).add(page) = 1;
|
||||
} else {
|
||||
let p = PENDING_N.load(Ordering::Relaxed);
|
||||
if p < PENDING.len() {
|
||||
PENDING[p].store(page, Ordering::Relaxed);
|
||||
PENDING_N.store(p + 1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
let uc = ctx as *mut libc::ucontext_t;
|
||||
(*uc).uc_mcontext.gregs[libc::REG_EFL as usize] |= TF;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn on_trap(_sig: libc::c_int, _info: *mut libc::siginfo_t, ctx: *mut libc::c_void) {
|
||||
unsafe {
|
||||
let start = BUF_START.load(Ordering::Relaxed);
|
||||
let n = PENDING_N.load(Ordering::Relaxed);
|
||||
for p in PENDING.iter().take(n) {
|
||||
let page = p.load(Ordering::Relaxed);
|
||||
libc::mprotect(
|
||||
(start + page * PAGE) as *mut libc::c_void,
|
||||
PAGE,
|
||||
libc::PROT_NONE,
|
||||
);
|
||||
}
|
||||
PENDING_N.store(0, Ordering::Relaxed);
|
||||
let uc = ctx as *mut libc::ucontext_t;
|
||||
(*uc).uc_mcontext.gregs[libc::REG_EFL as usize] &= !TF;
|
||||
}
|
||||
}
|
||||
|
||||
fn install(
|
||||
sig: libc::c_int,
|
||||
h: extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void),
|
||||
) {
|
||||
unsafe {
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
sa.sa_sigaction = h as usize;
|
||||
sa.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER;
|
||||
libc::sigemptyset(&mut sa.sa_mask);
|
||||
assert_eq!(libc::sigaction(sig, &sa, std::ptr::null_mut()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn protect(prot: libc::c_int) {
|
||||
let start = BUF_START.load(Ordering::Relaxed);
|
||||
let len = BUF_LEN.load(Ordering::Relaxed);
|
||||
unsafe {
|
||||
assert_eq!(libc::mprotect(start as *mut libc::c_void, len, prot), 0);
|
||||
}
|
||||
}
|
||||
|
||||
struct Phase {
|
||||
name: &'static str,
|
||||
log: Vec<u64>,
|
||||
bulk_pages: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Start a phase: clear the per-page state and protect the buffer.
|
||||
fn begin() {
|
||||
let pages = BUF_LEN.load(Ordering::Relaxed) / PAGE;
|
||||
unsafe {
|
||||
std::ptr::write_bytes(HITS.load(Ordering::Relaxed), 0, pages);
|
||||
std::ptr::write_bytes(BULK.load(Ordering::Relaxed), 0, pages);
|
||||
}
|
||||
LOG_LEN.store(0, Ordering::Relaxed);
|
||||
protect(libc::PROT_NONE);
|
||||
}
|
||||
|
||||
fn end(name: &'static str) -> Phase {
|
||||
protect(libc::PROT_READ | libc::PROT_WRITE);
|
||||
let n = LOG_LEN.load(Ordering::Relaxed);
|
||||
let log = unsafe { std::slice::from_raw_parts(LOG.load(Ordering::Relaxed), n) }.to_vec();
|
||||
let pages = BUF_LEN.load(Ordering::Relaxed) / PAGE;
|
||||
let bulk = unsafe { std::slice::from_raw_parts(BULK.load(Ordering::Relaxed), pages) };
|
||||
let bulk_pages = (0..pages)
|
||||
.filter(|&p| bulk[p] != 0)
|
||||
.map(|p| p as u64)
|
||||
.collect();
|
||||
if n == LOG_CAP {
|
||||
eprintln!("warning: access log full in phase {name}");
|
||||
}
|
||||
Phase {
|
||||
name,
|
||||
log,
|
||||
bulk_pages,
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte intervals [lo, hi) read in a phase, merged when closer than `gap`.
|
||||
fn ranges(ph: &[&Phase], gap: u64) -> Vec<(u64, u64)> {
|
||||
let mut iv: Vec<(u64, u64)> = Vec::new();
|
||||
for p in ph {
|
||||
iv.extend(p.log.iter().map(|&o| (o, o + ACCESS_WIDTH)));
|
||||
iv.extend(
|
||||
p.bulk_pages
|
||||
.iter()
|
||||
.map(|&pg| (pg * PAGE as u64, (pg + 1) * PAGE as u64)),
|
||||
);
|
||||
}
|
||||
iv.sort_unstable();
|
||||
let mut out: Vec<(u64, u64)> = Vec::new();
|
||||
for (lo, hi) in iv {
|
||||
match out.last_mut() {
|
||||
Some(last) if lo <= last.1 + gap => last.1 = last.1.max(hi),
|
||||
_ => out.push((lo, hi)),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Requests a reader with no cache at all would make: a new request each
|
||||
/// time the access stream leaves the neighbourhood of the current run.
|
||||
fn uncached_requests(p: &Phase) -> usize {
|
||||
let mut n = 0;
|
||||
let (mut lo, mut hi) = (u64::MAX, 0u64);
|
||||
for &o in &p.log {
|
||||
if lo != u64::MAX && o + MERGE_GAP >= lo && o <= hi + MERGE_GAP {
|
||||
lo = lo.min(o);
|
||||
hi = hi.max(o + ACCESS_WIDTH);
|
||||
} else {
|
||||
n += 1;
|
||||
lo = o;
|
||||
hi = o + ACCESS_WIDTH;
|
||||
}
|
||||
}
|
||||
n + p.bulk_pages.len()
|
||||
}
|
||||
|
||||
fn blocks(ph: &[&Phase], block: u64) -> usize {
|
||||
let mut set = BTreeSet::new();
|
||||
for (lo, hi) in ranges(ph, 0) {
|
||||
for b in lo / block..=(hi - 1) / block {
|
||||
set.insert(b);
|
||||
}
|
||||
}
|
||||
set.len()
|
||||
}
|
||||
|
||||
fn read_extents(path: &str) -> Vec<(u64, u64)> {
|
||||
let text = std::fs::read_to_string(path).expect("read extents");
|
||||
let mut v: Vec<(u64, u64)> = text
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let mut it = l.split_whitespace().map(|t| t.parse::<u64>());
|
||||
match (it.next(), it.next()) {
|
||||
(Some(Ok(o)), Some(Ok(n))) => Some((o, o + n)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
|
||||
fn in_extents(ext: &[(u64, u64)], lo: u64, hi: u64) -> bool {
|
||||
let i = ext.partition_point(|e| e.1 <= lo);
|
||||
i < ext.len() && ext[i].0 < hi
|
||||
}
|
||||
|
||||
/// Split a phase into loads outside and inside the raw-data extents. A bulk
|
||||
/// page counts as raw data when it overlaps an extent.
|
||||
fn split_raw(p: &Phase, ext: &[(u64, u64)]) -> (Phase, Phase) {
|
||||
let (mut m, mut r) = (Vec::new(), Vec::new());
|
||||
for &o in &p.log {
|
||||
if in_extents(ext, o, o + 1) {
|
||||
r.push(o)
|
||||
} else {
|
||||
m.push(o)
|
||||
}
|
||||
}
|
||||
let pg = PAGE as u64;
|
||||
let (bm, br): (Vec<u64>, Vec<u64>) = p
|
||||
.bulk_pages
|
||||
.iter()
|
||||
.partition(|&&b| !in_extents(ext, b * pg, (b + 1) * pg));
|
||||
(
|
||||
Phase {
|
||||
name: "read (metadata)",
|
||||
log: m,
|
||||
bulk_pages: bm,
|
||||
},
|
||||
Phase {
|
||||
name: "read (raw data)",
|
||||
log: r,
|
||||
bulk_pages: br,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn walk(g: &Group<'_>, path: &str, objs: &mut usize) {
|
||||
for name in g.datasets().unwrap_or_default() {
|
||||
*objs += 1;
|
||||
if let Ok(ds) = g.dataset(&name) {
|
||||
let _ = ds.shape();
|
||||
let _ = ds.dtype();
|
||||
}
|
||||
}
|
||||
for name in g.groups().unwrap_or_default() {
|
||||
*objs += 1;
|
||||
if let Ok(sub) = g.group(&name) {
|
||||
walk(&sub, &format!("{path}/{name}"), objs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() != 3 && args.len() != 4 {
|
||||
eprintln!("usage: range-trace FILE DATASET_PATH [RAW_EXTENTS]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let bytes = std::fs::read(&args[1]).expect("read file");
|
||||
let len = bytes.len();
|
||||
let cap = len.div_ceil(PAGE).max(1) * PAGE;
|
||||
let pages = cap / PAGE;
|
||||
// Page-aligned buffer so that protection covers exactly the file. The
|
||||
// Vec is never dropped (its layout differs from Vec's own), see the end.
|
||||
let ptr = unsafe { alloc_zeroed(Layout::from_size_align(cap, PAGE).unwrap()) };
|
||||
unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len) };
|
||||
drop(bytes);
|
||||
let buf = unsafe { Vec::from_raw_parts(ptr, len, cap) };
|
||||
BUF_START.store(ptr as usize, Ordering::Relaxed);
|
||||
BUF_LEN.store(cap, Ordering::Relaxed);
|
||||
let mut log = vec![0u64; LOG_CAP];
|
||||
LOG.store(log.as_mut_ptr(), Ordering::Relaxed);
|
||||
let mut hits = vec![0u32; pages];
|
||||
HITS.store(hits.as_mut_ptr(), Ordering::Relaxed);
|
||||
let mut bulk = vec![0u8; pages];
|
||||
BULK.store(bulk.as_mut_ptr(), Ordering::Relaxed);
|
||||
install(libc::SIGSEGV, on_segv);
|
||||
install(libc::SIGTRAP, on_trap);
|
||||
|
||||
begin();
|
||||
let file = File::from_bytes(buf).expect("open");
|
||||
let p_open = end("open");
|
||||
|
||||
begin();
|
||||
let mut objs = 0;
|
||||
walk(&file.root(), "", &mut objs);
|
||||
let p_list = end("list");
|
||||
|
||||
begin();
|
||||
let out = file
|
||||
.dataset(&args[2])
|
||||
.and_then(|d| d.read_selection(&Selection::All))
|
||||
.expect("read dataset");
|
||||
let p_read = end("read");
|
||||
|
||||
println!("file: {} ({} bytes), objects listed: {objs}", args[1], len);
|
||||
println!("dataset: {} ({} bytes decoded)", args[2], out.len());
|
||||
println!(
|
||||
"| phase | loads logged | bulk 4K pages | uncached requests | distinct ranges (gap<{MERGE_GAP}B) | bytes in ranges | 4 KiB blocks | 64 KiB blocks | 1 MiB blocks |"
|
||||
);
|
||||
println!("|---|---:|---:|---:|---:|---:|---:|---:|---:|");
|
||||
let row = |label: String, ph: &[&Phase], uncached: usize| {
|
||||
let r = ranges(ph, MERGE_GAP);
|
||||
let bytes: u64 = r.iter().map(|(a, b)| b - a).sum();
|
||||
let loads: usize = ph.iter().map(|p| p.log.len()).sum();
|
||||
let bulk: usize = ph.iter().map(|p| p.bulk_pages.len()).sum();
|
||||
println!(
|
||||
"| {label} | {loads} | {bulk} | {uncached} | {} | {bytes} | {} | {} | {} |",
|
||||
r.len(),
|
||||
blocks(ph, 4 << 10),
|
||||
blocks(ph, 64 << 10),
|
||||
blocks(ph, 1 << 20)
|
||||
);
|
||||
};
|
||||
for p in [&p_open, &p_list, &p_read] {
|
||||
row(p.name.to_string(), &[p], uncached_requests(p));
|
||||
}
|
||||
if let Some(path) = args.get(3) {
|
||||
let (meta, raw) = split_raw(&p_read, &read_extents(path));
|
||||
for p in [&meta, &raw] {
|
||||
row(p.name.to_string(), &[p], uncached_requests(p));
|
||||
}
|
||||
let all = [&p_open, &p_list, &meta];
|
||||
let unc: usize = all.iter().map(|p| uncached_requests(p)).sum();
|
||||
row("all metadata".into(), &all, unc);
|
||||
}
|
||||
let all = [&p_open, &p_list, &p_read];
|
||||
let unc: usize = all.iter().map(|p| uncached_requests(p)).sum();
|
||||
row("open+list+read".into(), &all, unc);
|
||||
let meta = [&p_open, &p_list];
|
||||
let unc: usize = meta.iter().map(|p| uncached_requests(p)).sum();
|
||||
row("open+list".into(), &meta, unc);
|
||||
// The File owns a buffer whose layout Vec does not know; never drop it.
|
||||
std::mem::forget(file);
|
||||
std::mem::forget(log);
|
||||
std::mem::forget(hits);
|
||||
std::mem::forget(bulk);
|
||||
}
|
||||
Reference in New Issue
Block a user