59 const auto representable =
static_cast<std::size_t
>(
60 std::numeric_limits<std::ptrdiff_t>::max())
78 line_bytes_ = other.line_bytes_;
79 slots_ = other.slots_;
80 capacity_bytes_ = other.capacity_bytes_;
89 throw std::invalid_argument(
"id read cache capacity must be page-aligned and at most 64 KiB");
93 constexpr auto maximum =
static_cast<std::size_t
>(
94 std::numeric_limits<std::ptrdiff_t>::max());
96 throw std::invalid_argument(
"id read cache line size must be positive");
98 if (policy.
line_bytes > maximum || !inclusive_line
99 || policy.
lines > maximum / inclusive_line)
100 throw std::invalid_argument(
"id read cache geometry is not representable");
103 slots_ = policy.
lines;
104 capacity_bytes_ = slots_ * inclusive_line;
107 if (cache_) (void)cache_->invalidate_all();
111 const auto page = offset / line_bytes_;
112 const auto shift = file_shift(file);
113 if (page > kMaximum - shift || page + shift > kMaximum / line_bytes_) {
117 const auto key = page_key(file, page);
118 (void)cache_->invalidate_from(
key.backing_identity,
key.generation,
119 key.page_number * line_bytes_);
124 wide_workspace_.reset();
128 std::uint64_t offset, std::size_t length)
const noexcept {
129 if (!cache_ || length == 0)
return;
130 if (offset > logical_end || length > logical_end - offset) {
136 const auto first = offset / line_bytes_;
137 const auto last = (offset + length - 1) / line_bytes_;
141 if (last - first >= slots_) {
145 for (
auto page = first;; ++page) {
146 (void)cache_->invalidate(page_key(file, page));
147 if (page == last)
break;
152 void read(
const File&
source,
bool plain_disk, std::uint64_t logical_end,
153 unsigned file, std::uint64_t offset,
void* destination, std::size_t length)
const {
154 if (!plain_disk || slots_ == 0 || length == 0
155 || offset >= logical_end || length > logical_end - offset) {
156 source.read(offset, destination, length);
160 if ((offset + length - 1) / line_bytes_ > kMaximum - file_shift(file)
162 source.read(offset, destination, length);
165 context_.source = &
source;
166 context_.read = [](
const void* owner, std::uint64_t at,
void* bytes, std::size_t size) {
167 static_cast<const File*
>(owner)->
read(at, bytes, size);
170 ReadContext& context;
172 context.source =
nullptr;
173 context.read =
nullptr;
174 context.failure =
nullptr;
178 read_cached(logical_end, file, offset, destination, length, wide_workspace_.get());
180 std::array<std::byte, kPageBytes> workspace;
181 read_cached(logical_end, file, offset, destination, length, workspace.data());
186 static constexpr auto kMaximum = std::numeric_limits<std::uint64_t>::max();
187 static constexpr std::uint64_t kIdentityMix = 0x9e3779b97f4a7c15ULL;
188 static constexpr std::uint64_t kGenerationMix = 0xbf58476d1ce4e5b9ULL;
189 static std::uint64_t file_shift(
unsigned file)
noexcept {
190 return static_cast<std::uint64_t
>(file) * 7;
192 static ReadThroughCacheKey page_key(
unsigned file, std::uint64_t page)
noexcept {
196 return {file * kGenerationMix, file * kIdentityMix, page + file_shift(file)};
198 struct StorageDeleter {
199 void operator()(
void* bytes)
const noexcept { ::operator
delete(bytes); }
201 using Storage = std::unique_ptr<void, StorageDeleter>;
203 const void* source =
nullptr;
204 void (*read)(
const void*, std::uint64_t,
void*, std::size_t) =
nullptr;
205 std::uint64_t offset = 0;
206 std::exception_ptr failure;
208 static ReadThroughCacheIoResult backing_read(
void* owner, ReadThroughCacheKey,
209 ScratchBuffer destination)
noexcept {
210 auto& context = *
static_cast<ReadContext*
>(owner);
212 context.read(context.source, context.offset, destination.data, destination.size);
215 context.failure = std::current_exception();
221 bool prepare() const noexcept {
222 if (cache_)
return true;
226 prepared_.reset(::operator
new(capacity_bytes_, std::nothrow));
227 if (!prepared_)
return false;
229 wide_workspace_.reset(
new (std::nothrow) std::byte[line_bytes_]);
230 if (!wide_workspace_) {
235 const ReadThroughCachePolicy policy{
238 const ReadThroughCacheBacking backing{&context_, &backing_read,
nullptr};
239 const ReadThroughCacheAllocator allocator{
241 [](
void* storage, std::size_t)
noexcept ->
void* {
242 return static_cast<Storage*
>(storage)->
release();
244 [](
void*,
void* bytes, std::size_t)
noexcept { ::operator
delete(bytes); }
246 cache_.emplace(policy, backing, allocator);
249 void read_cached(std::uint64_t logical_end,
unsigned file, std::uint64_t offset,
250 void* destination, std::size_t length, std::byte* workspace)
const {
251 auto* output =
static_cast<unsigned char*
>(destination);
252 while (length != 0) {
253 const auto page = offset / line_bytes_;
254 const auto base =
page * line_bytes_;
255 const auto within =
static_cast<std::size_t
>(offset - base);
256 const auto valid =
static_cast<std::size_t
>(
257 std::min<std::uint64_t>(line_bytes_, logical_end - base));
258 context_.offset = base;
259 const auto status = cache_->get(page_key(file, page), {workspace, valid});
261 if (context_.failure) std::rethrow_exception(context_.failure);
262 throw std::logic_error(
"id read cache callback contract failed");
264 const auto count = std::min(length, valid - within);
265 std::memcpy(output, workspace + within, count);
272 std::size_t slots_ = 0;
273 std::size_t capacity_bytes_ = 0;
274 mutable Storage prepared_;
275 mutable std::unique_ptr<std::byte[]> wide_workspace_;
276 mutable ReadContext context_;
277 mutable std::optional<ReadThroughCache> cache_;