Remote reads (range-read M2/M3: open_storage, HTTP/S3) and near-complete in-place editing #18
@@ -113,7 +113,8 @@ impl Model {
|
|||||||
.fold(0u64, |a, (&x, &d)| a * d + x) as usize
|
.fold(0u64, |a, (&x, &d)| a * d + x) as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Grow to `shape`, new elements `fill`.
|
/// Change the extent to `shape`: elements inside both keep their
|
||||||
|
/// values, new ones are `fill`.
|
||||||
fn resize(&mut self, shape: &[u64], fill: i32) {
|
fn resize(&mut self, shape: &[u64], fill: i32) {
|
||||||
let old = self.clone();
|
let old = self.clone();
|
||||||
*self = Self::new(shape, |_| fill);
|
*self = Self::new(shape, |_| fill);
|
||||||
@@ -125,10 +126,12 @@ impl Model {
|
|||||||
c[d] = r % old.shape[d];
|
c[d] = r % old.shape[d];
|
||||||
r /= old.shape[d];
|
r /= old.shape[d];
|
||||||
}
|
}
|
||||||
|
if c.iter().zip(shape).all(|(x, s)| x < s) {
|
||||||
let i = self.index(&c);
|
let i = self.index(&c);
|
||||||
self.data[i] = old.data[flat as usize];
|
self.data[i] = old.data[flat as usize];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a hyperslab write of `vals` (row-major over the block).
|
/// Apply a hyperslab write of `vals` (row-major over the block).
|
||||||
fn write_block(&mut self, start: &[u64], count: &[u64], vals: &[i32]) {
|
fn write_block(&mut self, start: &[u64], count: &[u64], vals: &[i32]) {
|
||||||
@@ -293,9 +296,24 @@ fn append_many_gzip() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Random operations — grow, hyperslab writes, point writes, attributes —
|
/// A random attribute value: a scalar, an int64 array, a short string or
|
||||||
/// on a 2-D dataset with one unlimited dimension, checked against a model
|
/// one larger than a heap's managed-object limit (a huge heap object once
|
||||||
/// after every few operations.
|
/// the attributes are in dense storage).
|
||||||
|
fn random_attr(rng: &mut Rng) -> AttrValue {
|
||||||
|
match rng.below(8) {
|
||||||
|
0..=2 => AttrValue::I64(rng.next() as i64 >> 3),
|
||||||
|
3..=4 => AttrValue::I64Array((0..1 + rng.below(40)).map(|k| k as i64 * 7).collect()),
|
||||||
|
5..=6 => AttrValue::String("s".repeat(1 + rng.below(200) as usize)),
|
||||||
|
_ => AttrValue::String("h".repeat(5000 + rng.below(100) as usize)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Random operations — growth and shrinking along any dimension, hyperslab
|
||||||
|
/// and point writes, attributes (enough names to move them to dense storage
|
||||||
|
/// on version-2 object headers, replaced with values of any size) — on a
|
||||||
|
/// 2-D dataset with one unlimited dimension and one with two (a version-2
|
||||||
|
/// B-tree chunk index under `v114`/`latest`), checked against a model (and
|
||||||
|
/// through h5py, numpy) after every few operations.
|
||||||
fn random_ops(libver: &str, h5dump: bool, extra: &str, tag: &str, seed: u64) {
|
fn random_ops(libver: &str, h5dump: bool, extra: &str, tag: &str, seed: u64) {
|
||||||
let dir = tmpdir();
|
let dir = tmpdir();
|
||||||
let path = dir.path().join(format!("rand_{tag}.h5"));
|
let path = dir.path().join(format!("rand_{tag}.h5"));
|
||||||
@@ -304,106 +322,159 @@ fn random_ops(libver: &str, h5dump: bool, extra: &str, tag: &str, seed: u64) {
|
|||||||
with h5py.File({p:?}, 'w', libver={libver}) as f:\n\
|
with h5py.File({p:?}, 'w', libver={libver}) as f:\n\
|
||||||
\x20 f.create_dataset('m', shape=(4, 7), maxshape=(None, 7), chunks=(3, 4), \
|
\x20 f.create_dataset('m', shape=(4, 7), maxshape=(None, 7), chunks=(3, 4), \
|
||||||
dtype='<i4', fillvalue=-9{extra})\n\
|
dtype='<i4', fillvalue=-9{extra})\n\
|
||||||
\x20 f['m'][1:3, 2:6] = 5\n",
|
\x20 f['m'][1:3, 2:6] = 5\n\
|
||||||
|
\x20 f.create_dataset('b', shape=(5, 6), maxshape=(None, None), chunks=(2, 4), \
|
||||||
|
dtype='<i4', fillvalue=3{extra})\n\
|
||||||
|
\x20 f['b'][0:4, 1:5] = 8\n",
|
||||||
p = path.to_str().unwrap()
|
p = path.to_str().unwrap()
|
||||||
));
|
));
|
||||||
let mut m = Model::new(&[4, 7], |_| -9);
|
let mut models = [Model::new(&[4, 7], |_| -9), Model::new(&[5, 6], |_| 3)];
|
||||||
m.write_block(&[1, 2], &[2, 4], &[5; 8]);
|
models[0].write_block(&[1, 2], &[2, 4], &[5; 8]);
|
||||||
let mut attrs: Vec<(String, i64)> = Vec::new();
|
models[1].write_block(&[0, 1], &[4, 4], &[8; 16]);
|
||||||
|
let fills = [-9, 3];
|
||||||
|
let names = ["m", "b"];
|
||||||
|
let mut attrs: Vec<(String, AttrValue)> = Vec::new();
|
||||||
let mut rng = Rng(seed);
|
let mut rng = Rng(seed);
|
||||||
let mut ed = FileEditor::open(&path).unwrap();
|
let mut ed = FileEditor::open(&path).unwrap();
|
||||||
for step in 0..120 {
|
for step in 0..160 {
|
||||||
match rng.below(10) {
|
let d = rng.below(2) as usize;
|
||||||
0..=1 => {
|
let name = names[d];
|
||||||
let rows = m.shape[0] + 1 + rng.below(5);
|
let m = &mut models[d];
|
||||||
ed.resize("m", &[rows, 7]).unwrap();
|
match rng.below(12) {
|
||||||
m.resize(&[rows, 7], -9);
|
0..=2 => {
|
||||||
|
// Grow or shrink: dimension 1 of "m" is fixed at 7.
|
||||||
|
let rows = rng.below(m.shape[0] + 6);
|
||||||
|
let cols = if d == 0 { 7 } else { rng.below(m.shape[1] + 5) };
|
||||||
|
ed.resize(name, &[rows, cols]).unwrap();
|
||||||
|
m.resize(&[rows, cols], fills[d]);
|
||||||
}
|
}
|
||||||
2..=6 => {
|
3..=7 if m.shape.iter().all(|&s| s > 0) => {
|
||||||
let r0 = rng.below(m.shape[0]);
|
let r0 = rng.below(m.shape[0]);
|
||||||
let c0 = rng.below(7);
|
let c0 = rng.below(m.shape[1]);
|
||||||
let cnt = [
|
let cnt = [
|
||||||
1 + rng.below((m.shape[0] - r0).min(6)),
|
1 + rng.below((m.shape[0] - r0).min(6)),
|
||||||
1 + rng.below(7 - c0),
|
1 + rng.below((m.shape[1] - c0).min(6)),
|
||||||
];
|
];
|
||||||
let n = cnt[0] * cnt[1];
|
let n = cnt[0] * cnt[1];
|
||||||
let vals: Vec<i32> = (0..n).map(|_| (rng.next() % 100_000) as i32).collect();
|
let vals: Vec<i32> = (0..n).map(|_| (rng.next() % 100_000) as i32).collect();
|
||||||
ed.write_values("m", &block(&[r0, c0], &cnt), &vals)
|
ed.write_values(name, &block(&[r0, c0], &cnt), &vals)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
m.write_block(&[r0, c0], &cnt, &vals);
|
m.write_block(&[r0, c0], &cnt, &vals);
|
||||||
}
|
}
|
||||||
7 => {
|
8 if m.shape.iter().all(|&s| s > 0) => {
|
||||||
let pts: Vec<Vec<u64>> = (0..1 + rng.below(4))
|
let pts: Vec<Vec<u64>> = (0..1 + rng.below(4))
|
||||||
.map(|_| vec![rng.below(m.shape[0]), rng.below(7)])
|
.map(|_| vec![rng.below(m.shape[0]), rng.below(m.shape[1])])
|
||||||
.collect();
|
.collect();
|
||||||
let vals: Vec<i32> = pts.iter().map(|_| rng.next() as i32).collect();
|
let vals: Vec<i32> = pts.iter().map(|_| rng.next() as i32).collect();
|
||||||
ed.write_values("m", &Selection::Points(pts.clone()), &vals)
|
ed.write_values(name, &Selection::Points(pts.clone()), &vals)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for (p, v) in pts.iter().zip(&vals) {
|
for (p, v) in pts.iter().zip(&vals) {
|
||||||
let i = m.index(p);
|
let i = m.index(p);
|
||||||
m.data[i] = *v;
|
m.data[i] = *v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
9..=11 => {
|
||||||
let k = rng.below(6);
|
let k = rng.below(20);
|
||||||
let name = format!("a{k}");
|
let aname = format!("a{k}");
|
||||||
let v = rng.next() as i64;
|
let v = random_attr(&mut rng);
|
||||||
match ed.set_attr("m", &name, &AttrValue::I64(v)) {
|
match ed.set_attr("m", &aname, &v) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
attrs.retain(|(n, _)| *n != name);
|
attrs.retain(|(n, _)| *n != aname);
|
||||||
attrs.push((name, v));
|
attrs.push((aname, v));
|
||||||
}
|
}
|
||||||
Err(e) => panic!("set_attr {name}: {e}"),
|
// Replacing the only attribute in a heap block with one
|
||||||
|
// of another size would have libhdf5 free the block.
|
||||||
|
Err(Error::Unsupported(msg)) if msg.contains("last object") => {}
|
||||||
|
Err(e) => panic!("set_attr {aname}: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
if step % 30 == 29 {
|
if step % 40 == 39 {
|
||||||
drop(ed);
|
drop(ed);
|
||||||
verify(&path, "m", &m);
|
for (n, m) in names.iter().zip(&models) {
|
||||||
|
verify(&path, n, m);
|
||||||
|
}
|
||||||
check_tools(&path, h5dump);
|
check_tools(&path, h5dump);
|
||||||
check_attrs(&path, "m", &attrs);
|
check_attrs(&path, "m", &attrs);
|
||||||
ed = FileEditor::open(&path).unwrap();
|
ed = FileEditor::open(&path).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(ed);
|
drop(ed);
|
||||||
verify(&path, "m", &m);
|
for (n, m) in names.iter().zip(&models) {
|
||||||
|
verify(&path, n, m);
|
||||||
|
}
|
||||||
check_attrs(&path, "m", &attrs);
|
check_attrs(&path, "m", &attrs);
|
||||||
py(&format!(
|
py(&format!(
|
||||||
"import h5py, numpy as np\n\
|
"import h5py, numpy as np\n\
|
||||||
with h5py.File({p:?}, 'r+') as f:\n\
|
with h5py.File({p:?}, 'r+') as f:\n\
|
||||||
\x20 d = f['m']\n\
|
\x20 for name, cols in (('m', 7), ('b', None)):\n\
|
||||||
\x20 n = d.shape[0]\n\
|
\x20 d = f[name]\n\
|
||||||
\x20 d.resize((n + 3, 7))\n\
|
\x20 n, c = d.shape\n\
|
||||||
|
\x20 d.resize((n + 3, cols or c + 2))\n\
|
||||||
\x20 d[n:, :] = 42\n\
|
\x20 d[n:, :] = 42\n\
|
||||||
\x20 d.attrs['from_h5py'] = 1.5\n",
|
\x20 f['m'].attrs['from_h5py'] = 1.5\n",
|
||||||
p = path.to_str().unwrap()
|
p = path.to_str().unwrap()
|
||||||
));
|
));
|
||||||
let n = m.shape[0];
|
for (d, m) in models.iter_mut().enumerate() {
|
||||||
m.resize(&[n + 3, 7], -9);
|
let (n, c) = (m.shape[0], m.shape[1]);
|
||||||
m.write_block(&[n, 0], &[3, 7], &[42; 21]);
|
let c2 = if d == 0 { 7 } else { c + 2 };
|
||||||
verify(&path, "m", &m);
|
m.resize(&[n + 3, c2], fills[d]);
|
||||||
|
m.write_block(&[n, 0], &[3, c2], &vec![42; (3 * c2) as usize]);
|
||||||
|
}
|
||||||
|
for (n, m) in names.iter().zip(&models) {
|
||||||
|
verify(&path, n, m);
|
||||||
|
}
|
||||||
check_tools(&path, h5dump);
|
check_tools(&path, h5dump);
|
||||||
|
check_attrs(&path, "m", &attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_attrs(path: &Path, obj: &str, attrs: &[(String, i64)]) {
|
/// Our reader and h5py see `attrs` on dataset `obj` (and h5py's count of
|
||||||
|
/// its attributes agrees with libhdf5's object info).
|
||||||
|
fn check_attrs(path: &Path, obj: &str, attrs: &[(String, AttrValue)]) {
|
||||||
let f = File::open(path).unwrap();
|
let f = File::open(path).unwrap();
|
||||||
let got = f.dataset(obj).unwrap().attrs().unwrap();
|
let got = f.dataset(obj).unwrap().attrs().unwrap();
|
||||||
for (n, v) in attrs {
|
for (n, v) in attrs {
|
||||||
match got.get(n) {
|
let g = got
|
||||||
Some(AttrValue::I64(g)) => assert_eq!(g, v, "attribute {n}"),
|
.get(n)
|
||||||
other => panic!("attribute {n}: {other:?}"),
|
.unwrap_or_else(|| panic!("attribute {n} missing"));
|
||||||
|
// Our reader reports a one-element array as a scalar.
|
||||||
|
let v = match v {
|
||||||
|
AttrValue::I64Array(a) if a.len() == 1 => &AttrValue::I64(a[0]),
|
||||||
|
v => v,
|
||||||
|
};
|
||||||
|
assert_eq!(format!("{g:?}"), format!("{v:?}"), "attribute {n}");
|
||||||
}
|
}
|
||||||
}
|
let want: Vec<String> = attrs
|
||||||
let want: Vec<String> = attrs.iter().map(|(n, v)| format!("{n:?}: {v}")).collect();
|
.iter()
|
||||||
py(&format!(
|
.map(|(n, v)| {
|
||||||
|
let pv = match v {
|
||||||
|
AttrValue::I64(x) => format!("{x}"),
|
||||||
|
AttrValue::I64Array(a) => format!("{a:?}"),
|
||||||
|
AttrValue::String(s) => format!("{s:?}"),
|
||||||
|
other => panic!("{other:?}"),
|
||||||
|
};
|
||||||
|
format!("{n:?}: {pv}")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let script = format!(
|
||||||
"import h5py\n\
|
"import h5py\n\
|
||||||
f = h5py.File({p:?}, 'r')\n\
|
f = h5py.File({p:?}, 'r')\n\
|
||||||
want = {{{w}}}\n\
|
want = {{{w}}}\n\
|
||||||
got = {{k: int(v) for k, v in f[{obj:?}].attrs.items() if k in want}}\n\
|
a = f[{obj:?}].attrs\n\
|
||||||
assert got == want, (got, want)\n",
|
def norm(v):\n\
|
||||||
|
\x20 v = v.decode() if isinstance(v, bytes) else v\n\
|
||||||
|
\x20 return v.tolist() if hasattr(v, 'tolist') else v\n\
|
||||||
|
got = {{k: norm(v) for k, v in a.items() if k in want}}\n\
|
||||||
|
assert got == want, sorted(set(want) ^ set(got))\n\
|
||||||
|
assert len(a) == h5py.h5o.get_info(f[{obj:?}].id).num_attrs == len(list(a))\n",
|
||||||
p = path.to_str().unwrap(),
|
p = path.to_str().unwrap(),
|
||||||
w = want.join(", ")
|
w = want.join(", ")
|
||||||
));
|
);
|
||||||
|
let sp = path.with_extension("attrs.py");
|
||||||
|
std::fs::write(&sp, script).unwrap();
|
||||||
|
let o = Command::new(python()).arg(&sp).output().unwrap();
|
||||||
|
assert!(o.status.success(), "attribute check failed:\n{}", text(&o));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -411,7 +482,11 @@ fn random_operations_match_a_model() {
|
|||||||
if !tools_ok() {
|
if !tools_ok() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut seed = 1;
|
// CLAWHDF5_EDIT_SEED runs the same workloads with other random choices.
|
||||||
|
let mut seed = std::env::var("CLAWHDF5_EDIT_SEED")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
|
.unwrap_or(1);
|
||||||
for (i, (lv, dump)) in LIBVERS.iter().enumerate() {
|
for (i, (lv, dump)) in LIBVERS.iter().enumerate() {
|
||||||
// h5dump has no LZF decoder (h5py's own filter).
|
// h5dump has no LZF decoder (h5py's own filter).
|
||||||
for (j, (extra, lzf)) in [
|
for (j, (extra, lzf)) in [
|
||||||
|
|||||||
Reference in New Issue
Block a user