A cold run on a spinning disk waited on the disk twice over. The CBF header
scan read 256 kB from every frame on eight threads that each strode through
their own share of the sweep, so they drifted apart and the scan became a
seek storm (34 s for 2400 frames here); and after it, the pre-scan and the
first-pass indexing touch a few hundred frames and leave the disk idle until
the first image loop reads everything at seek-bound rates.
- ReadAhead (reader/): once the dataset is open, rugnux starts eight threads
that read the data files - HDF5 data files (legacy, VDS or the integrated
master) or the per-frame CBF/marCCD/SMV files - in 4 MB pieces taken
strictly in order, into a throwaway buffer. One stream reads this disk at
125 MB/s, eight in-order streams at 190 MB/s, 32 at 157 MB/s. It never gets
more than a quarter of MemAvailable (GlobalMemoryStatusEx on Windows, 4 GiB
where there is no figure) ahead of what ReadRawImage has handed out, so a
dataset bigger than the cache does not evict its own start, and it stops
with the reader. Plain ifstream reads: portable, no POSIX calls.
- Header scans (CBF, marCCD, SMV) hand the files out in order from an atomic
counter (sweep::ForEachInOrder) instead of striding: 18 s -> 12 s for 2400
cold CBF headers. The CBF header is first read with a 16 kB probe and again
with the old 256 kB one only when the separator is not in it, so the parsed
header is exactly what it was: 12 s -> 6 s.
Output unchanged: p.hkl, p.mtz and p_unmerged.mtz md5-identical to the
rc173 baseline on 6toc (CBF, 2400 frames, 6.0 GB) and 9q41 (HDF5 VDS, 900
frames, 5.1 GB), and on 6z9g (HDF5, 12.8 GB) to the unmodified branch; myob
(p.hkl p.mtz p_P1.mtz p_unmerged.mtz) md5-identical to the reference.
Measured cold (files evicted with POSIX_FADV_DONTNEED before every run),
same code without this commit vs with it, on a shared box (load 20-70, other
agents reading the same disk, so single runs scatter by +-20 s):
6toc wall 61.7/62.7 -> 49.3/49.8 s (clean pairs); all data resident
after 62/51/50 -> 45/41/42 s
9q41 wall 67.3 -> 57.8 s (clean pair); resident after 58/46/43 -> 48/37/35 s
6z9g resident after 81 -> 69 s
The first image loop can look slower with this in CBF runs: the old 256 kB
header probes pulled ~70% of the data in as kernel readahead, so the old
loop started warm - after a 34 s header scan instead of 10 s.
Warm (myob, NVMe, cached): 19.35/20.00 s without, 19.76-20.16 s with; the
read-ahead then only copies 9.3 GB out of the page cache, 0.44 s wall and
3.4 CPU-s measured standalone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
209 lines
9.4 KiB
C++
209 lines
9.4 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "HDF5ImageLocator.h"
|
|
#include "../common/JFJochException.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace {
|
|
// Coalesce consecutive single-image mappings into one contiguous range when the source and
|
|
// virtual images stay contiguous in the same file/dataset.
|
|
void AppendOrExtendSourceMapping(std::vector<HDF5DataSourceMessage> &ret,
|
|
const std::string &filename,
|
|
const std::string &dataset,
|
|
uint64_t source_first_image,
|
|
uint64_t virtual_first_image,
|
|
uint64_t image_count,
|
|
std::optional<uint64_t> source_channel = {}) {
|
|
if (image_count == 0)
|
|
return;
|
|
|
|
if (!ret.empty()) {
|
|
auto &last = ret.back();
|
|
if (last.filename == filename
|
|
&& last.dataset == dataset
|
|
&& last.source_channel == source_channel
|
|
&& last.source_first_image + last.image_count == source_first_image
|
|
&& last.virtual_first_image + last.image_count == virtual_first_image) {
|
|
last.image_count += image_count;
|
|
return;
|
|
}
|
|
}
|
|
|
|
ret.push_back(HDF5DataSourceMessage{
|
|
.filename = filename,
|
|
.dataset = dataset,
|
|
.source_first_image = source_first_image,
|
|
.virtual_first_image = virtual_first_image,
|
|
.image_count = image_count,
|
|
.source_channel = source_channel
|
|
});
|
|
}
|
|
|
|
// A 4D mapping, [image, channel, y, x], may cover only some of the channels; the reader wants
|
|
// the first one.
|
|
bool CoversFirstChannel(const HDF5VirtualDatasetMapping &mapping) {
|
|
return mapping.virtual_start.size() != 4 || mapping.virtual_start[1] == 0;
|
|
}
|
|
|
|
std::optional<hsize_t> SourceChannel(const HDF5VirtualDatasetMapping &mapping) {
|
|
if (mapping.source_start.size() == 4)
|
|
return mapping.source_start[1];
|
|
return {};
|
|
}
|
|
}
|
|
|
|
void HDF5ImageLocator::Configure(Layout layout) {
|
|
file_cache_.clear();
|
|
layout_ = std::move(layout);
|
|
}
|
|
|
|
void HDF5ImageLocator::Clear() {
|
|
file_cache_.clear();
|
|
layout_ = Layout{};
|
|
}
|
|
|
|
std::shared_ptr<HDF5ReadOnlyFile> HDF5ImageLocator::OpenCached(const std::string &path) const {
|
|
auto it = file_cache_.find(path);
|
|
if (it != file_cache_.end())
|
|
return it->second;
|
|
auto file = std::make_shared<HDF5ReadOnlyFile>(path);
|
|
file_cache_[path] = file;
|
|
return file;
|
|
}
|
|
|
|
HDF5ImageLocator::Location HDF5ImageLocator::Resolve(int64_t global_image) const {
|
|
if (global_image < 0)
|
|
throw JFJochException(JFJochExceptionCategory::HDF5, "Image out of bounds");
|
|
|
|
if (layout_.format == FileWriterFormat::NXmxLegacy) {
|
|
const uint32_t file_id = global_image / layout_.images_per_file;
|
|
const uint32_t local_index = global_image % layout_.images_per_file;
|
|
const auto &data_file = layout_.legacy_files.at(file_id);
|
|
return {OpenCached(data_file.path), local_index, data_file.path, data_file.dataset};
|
|
}
|
|
|
|
if (layout_.format == FileWriterFormat::NXmxVDS
|
|
&& layout_.data_layout == HDF5DataSetLayout::VIRTUAL) {
|
|
const auto image = static_cast<hsize_t>(global_image);
|
|
for (const auto &mapping: layout_.vds_mappings) {
|
|
if (!CoversFirstChannel(mapping) || !mapping.ContainsVirtualImage(image))
|
|
continue;
|
|
return {OpenCached(mapping.filename), static_cast<uint32_t>(mapping.SourceImage(image)),
|
|
mapping.filename, mapping.dataset, SourceChannel(mapping).value_or(0)};
|
|
}
|
|
throw JFJochException(JFJochExceptionCategory::HDF5,
|
|
"Image not covered by /entry/data/data VDS mappings");
|
|
}
|
|
|
|
// Contiguous / integrated: pixels live in the master file at the global index.
|
|
if (!layout_.master_file)
|
|
throw JFJochException(JFJochExceptionCategory::HDF5, "Master file not loaded");
|
|
return {layout_.master_file, static_cast<uint32_t>(global_image), layout_.master_filename};
|
|
}
|
|
|
|
std::vector<std::string> HDF5ImageLocator::DataFiles() const {
|
|
std::vector<std::string> ret;
|
|
if (layout_.format == FileWriterFormat::NXmxLegacy) {
|
|
for (const auto &f: layout_.legacy_files)
|
|
ret.push_back(f.path);
|
|
} else if (layout_.format == FileWriterFormat::NXmxVDS
|
|
&& layout_.data_layout == HDF5DataSetLayout::VIRTUAL) {
|
|
for (const auto &mapping: layout_.vds_mappings)
|
|
if (CoversFirstChannel(mapping)
|
|
&& std::find(ret.begin(), ret.end(), mapping.filename) == ret.end())
|
|
ret.push_back(mapping.filename);
|
|
} else if (!layout_.master_filename.empty())
|
|
ret.push_back(layout_.master_filename);
|
|
return ret;
|
|
}
|
|
|
|
std::vector<HDF5DataSourceMessage> HDF5ImageLocator::GetSourceMapping(uint64_t first_image,
|
|
std::optional<uint64_t> image_count,
|
|
uint64_t total_images,
|
|
uint64_t stride) const {
|
|
if (stride == 0)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Image stride cannot be zero");
|
|
|
|
if (first_image > total_images)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"First image outside dataset range");
|
|
|
|
// With a stride the caller's count is the number of OUTPUT images, which spans
|
|
// (count - 1) * stride + 1 source images - not count of them.
|
|
const uint64_t requested_count =
|
|
image_count.value_or((total_images - first_image + stride - 1) / stride);
|
|
if (requested_count > 0 && first_image + (requested_count - 1) * stride >= total_images)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Requested image range outside dataset range");
|
|
|
|
std::vector<HDF5DataSourceMessage> ret;
|
|
if (requested_count == 0)
|
|
return ret;
|
|
|
|
// Integrated / contiguous source: link directly to the original master file.
|
|
if (layout_.format == FileWriterFormat::NXmxVDS && layout_.data_layout != HDF5DataSetLayout::VIRTUAL) {
|
|
// One extendable run when the images are consecutive; strided output needs one mapping per image,
|
|
// which AppendOrExtendSourceMapping leaves separate because the source indices are not adjacent.
|
|
for (uint64_t local_image = 0; local_image < requested_count; ++local_image)
|
|
AppendOrExtendSourceMapping(ret, layout_.master_filename, "/entry/data/data",
|
|
first_image + local_image * stride, local_image, 1);
|
|
return ret;
|
|
}
|
|
|
|
// VDS source: expand VDS mappings to original source files, not to the VDS master.
|
|
if (layout_.format == FileWriterFormat::NXmxVDS && layout_.data_layout == HDF5DataSetLayout::VIRTUAL) {
|
|
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
|
|
const hsize_t virtual_image = first_image + local_image * stride;
|
|
|
|
bool found = false;
|
|
for (const auto &mapping: layout_.vds_mappings) {
|
|
if (!CoversFirstChannel(mapping) || !mapping.ContainsVirtualImage(virtual_image))
|
|
continue;
|
|
|
|
const uint64_t source_image = mapping.SourceImage(virtual_image);
|
|
const std::string dataset = mapping.dataset.empty() ? "/entry/data/data" : mapping.dataset;
|
|
|
|
AppendOrExtendSourceMapping(ret, mapping.filename, dataset, source_image, local_image, 1,
|
|
SourceChannel(mapping));
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if (!found)
|
|
throw JFJochException(JFJochExceptionCategory::HDF5,
|
|
"Image not covered by /entry/data/data VDS mappings");
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// Legacy source: link directly to the linked data files.
|
|
if (layout_.format == FileWriterFormat::NXmxLegacy) {
|
|
if (layout_.images_per_file == 0)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Cannot generate HDF5 source mapping: images_per_file is zero");
|
|
|
|
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
|
|
const uint64_t source_global_image = first_image + local_image * stride;
|
|
const uint64_t file_id = source_global_image / layout_.images_per_file;
|
|
const uint64_t source_image = source_global_image % layout_.images_per_file;
|
|
|
|
if (file_id >= layout_.legacy_files.size())
|
|
throw JFJochException(JFJochExceptionCategory::HDF5,
|
|
"Legacy image source file missing");
|
|
|
|
const auto &data_file = layout_.legacy_files.at(file_id);
|
|
AppendOrExtendSourceMapping(ret, data_file.path, data_file.dataset,
|
|
source_image, local_image, 1);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Unsupported HDF5 file layout for source mapping");
|
|
}
|