Two things every worker thread of an offline run did inside the global HDF5 mutex, per image. It opened /entry/data/data and asked it for its dataspace, its datatype and its creation plist, then asked those for the rank, the dimensions, the chunking and the compression. All of that is a property of the file and identical for all of its images, so it is now resolved once when the file is first touched. And it read the pixels - megabytes of them, with the lock held, which is what turned a worker per hardware thread into a queue. HDF5 can say where a chunk lives instead - address and byte count, a lookup in the chunk index with no read attached - so that is all it is asked for now, and the bytes are fetched after the lock is dropped, with a positional read that any number of threads can make through one handle at once. Chunk addresses count from the end of the user block, so its size is added; zero for anything this project writes, not for every file. A file that is not one chunk per image, or a chunk that was never written and exists only as a fill value, still goes the old way - only HDF5 knows what those read as. On a 16 Mpx rotation dataset with the process file being written, the per-image loop at 48 workers goes 12.4 s -> 6.8 s, and stops getting slower as workers are added: 8 workers were faster than 48 before, and are not now. Where no process file is written the same loop only improves ~1%, because this machine has 1.5 TB of RAM and held the whole 7 GB test set in page cache - the read was never the expensive part here. It is where the cache is cold or the filesystem is remote. Battery 9m45s, space group 21/24, no failures, unchanged. The Windows path uses ReadFile with an OVERLAPPED offset for the same reason pread is used elsewhere: it takes the offset as an argument rather than moving a shared file position, so the viewer keeps building under MSVC and gets the same concurrency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
4.9 KiB
C++
105 lines
4.9 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#include "HDF5ImageLocator.h"
|
|
#include "../common/CompressedImage.h"
|
|
|
|
// Raw-pixel side of the reader. Turns a global image number into a CompressedImage, using
|
|
// HDF5ImageLocator to find the file (with its open-file cache). This is the part whose "links
|
|
// to files stay" constant: switching which master the per-image metadata is read from never
|
|
// touches it. Caller must hold the global hdf5_mutex (HDF5 is not thread-safe).
|
|
class HDF5ImageSource {
|
|
public:
|
|
// Plain positional-read handle on a data file, opened alongside the HDF5 one. Owns the handle.
|
|
class RawFile {
|
|
public:
|
|
explicit RawFile(const std::string &path);
|
|
~RawFile();
|
|
RawFile(const RawFile &) = delete;
|
|
RawFile &operator=(const RawFile &) = delete;
|
|
|
|
bool IsOpen() const { return handle_ != -1; }
|
|
// Read `size` bytes from byte `address`. Positional and stateless, so any number of threads
|
|
// may call it on the same handle at once. Throws on a short read.
|
|
void ReadAt(void *dst, size_t size, uint64_t address) const;
|
|
|
|
private:
|
|
intptr_t handle_ = -1; // a file descriptor on POSIX, a HANDLE on Windows
|
|
};
|
|
|
|
// Where the bytes of one image are, and what they decode to. Everything needed to read an image
|
|
// without calling HDF5 again.
|
|
struct DirectChunk {
|
|
const RawFile *file = nullptr;
|
|
uint64_t address = 0;
|
|
uint32_t size = 0;
|
|
hsize_t width = 0;
|
|
hsize_t height = 0;
|
|
CompressedImageMode mode{};
|
|
CompressionAlgorithm algorithm = CompressionAlgorithm::NO_COMPRESSION;
|
|
};
|
|
|
|
void Configure(HDF5ImageLocator::Layout layout);
|
|
void Clear();
|
|
|
|
// Where image `global` physically lives. Also used by the metadata source to find the data
|
|
// file that holds a legacy/VDS image's per-image metadata.
|
|
HDF5ImageLocator::Location Resolve(int64_t global) const;
|
|
|
|
// Read the pixels at a resolved location into a CompressedImage backed by `buffer`.
|
|
CompressedImage ReadImageAt(std::vector<uint8_t> &buffer, const HDF5ImageLocator::Location &loc) const;
|
|
|
|
// Ask HDF5 where image `loc` is in the file rather than asking it for the image. This is a
|
|
// lookup in the chunk index and nothing else - no read - so the mutex is held for a fraction of
|
|
// what an actual read costs, and the read itself then happens on any number of threads at once
|
|
// through ReadDirect(). Caller must hold hdf5_mutex.
|
|
//
|
|
// Empty when this file cannot be served that way: one chunk per image is what makes an image a
|
|
// single contiguous run of bytes, and a chunk that has never been written has no address at all.
|
|
// The caller falls back to ReadImageAt() then.
|
|
std::optional<DirectChunk> PrepareDirectRead(const HDF5ImageLocator::Location &loc) const;
|
|
|
|
// Read what PrepareDirectRead() found. Touches no HDF5 and no shared state, so it needs no
|
|
// mutex; this is the whole point of the two-step split.
|
|
static CompressedImage ReadDirect(std::vector<uint8_t> &buffer, const DirectChunk &chunk);
|
|
|
|
std::vector<HDF5DataSourceMessage> GetSourceMapping(uint64_t first_image,
|
|
std::optional<uint64_t> image_count,
|
|
uint64_t total_images,
|
|
uint64_t stride = 1) const;
|
|
|
|
private:
|
|
HDF5ImageLocator locator_;
|
|
|
|
// /entry/data/data and everything asked of it here - its rank and dimensions, its element type,
|
|
// its chunking, its compression - are properties of the file, identical for every image in it.
|
|
// They used to be looked up again for each image: four HDF5 object opens per frame, inside the
|
|
// global hdf5_mutex that every worker thread queues on. Resolve them once per file instead.
|
|
//
|
|
// The entry keeps the file alive, so the pointer it is keyed by cannot be recycled underneath it
|
|
// and the dataset handle cannot outlive the file it belongs to.
|
|
struct OpenDataset {
|
|
std::shared_ptr<HDF5ReadOnlyFile> file;
|
|
std::unique_ptr<HDF5DataSet> dataset;
|
|
std::unique_ptr<RawFile> raw;
|
|
// HDF5 addresses count from the end of the user block, so they are file offsets only once
|
|
// its size is added. Zero for everything this project writes, but not for every file.
|
|
uint64_t user_block = 0;
|
|
hsize_t width = 0;
|
|
hsize_t height = 0;
|
|
CompressedImageMode mode{};
|
|
CompressionAlgorithm algorithm = CompressionAlgorithm::NO_COMPRESSION;
|
|
bool direct_chunk = false;
|
|
};
|
|
mutable std::map<const HDF5ReadOnlyFile *, OpenDataset> dataset_cache_;
|
|
const OpenDataset &GetDataset(const HDF5ImageLocator::Location &loc) const;
|
|
};
|