PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
id_table_read_cache.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cstddef>
9#include <cstdint>
10#include <cstring>
11#include <exception>
12#include <limits>
13#include <memory>
14#include <new>
15#include <optional>
16#include <stdexcept>
17#include <utility>
18
19namespace pineforge::detail {
20
21// Copy-in/copy-out logical pages for the private disk id tables only.
22//
23// WRITE-THROUGH: this class never writes or delays a write. Owners invalidate
24// before the real write, including a write that might fail after a prefix.
25// Clear, move and backend/identity changes invalidate ALL lines. Growth
26// invalidates the old tail page onward in that file; in-place writes
27// invalidate every overlapping line, even when the write fails.
28// EOF: out-of-extent reads go straight to the original reader, preserving its
29// short-read/EIO/errno text and copied prefix. A fill stops at logical_end;
30// no zero padding and no bytes beyond that end are read or returned.
31// BYPASS: plain_disk must be false for EVERY slab/arena backend, including
32// unjailed regular-file arenas and inherited/sealed memfds. No allocation,
33// tag probe or copied page occurs on that path. No payload pointer escapes.
34//
35// Policy fixes line bytes and line count, independent of the id population.
36// The legacy byte constructor keeps its 4 KiB geometry and 64 KiB ceiling.
37// Default capacity is zero, with no allocation. Only the adapter opts its
38// four tables in: 64/64/32/32 KiB (pair data+index together), 192 KiB of page
39// bytes per adapter. ReadThroughCache owns every tag and payload line in one
40// optional allocation: lines * ReadThroughCache::line_bytes(line_bytes).
41// A 4 KiB stack transfer buffer is separate; wider policies lazily allocate
42// one optional line-sized transfer buffer. Either allocation may bypass.
43// One adapter owns each cache; no sharing across threads or per-id directory.
44// This is disposable physical state, not logical/durable broker state.
46public:
47 static constexpr std::size_t kPageBytes = 4096;
48 static constexpr std::size_t kCacheBytes = 64 * 1024;
49 static constexpr std::size_t kSlots = kCacheBytes / kPageBytes;
50 static constexpr std::size_t kPairCacheBytes = kCacheBytes / 2;
51 static constexpr std::size_t kAdapterCacheBytes = 3 * kCacheBytes;
52
53 struct Policy {
54 std::size_t line_bytes = kPageBytes;
55 std::size_t lines = 0; // Zero bypasses; neither field limits backing data.
56 };
57
58 static Policy policy_for_capacity(CacheCapacity capacity) noexcept {
59 const auto representable = static_cast<std::size_t>(
60 std::numeric_limits<std::ptrdiff_t>::max())
62 return Policy{kPageBytes, std::min(capacity.lines(kPageBytes), representable)};
63 }
64
65 explicit IdTableReadCache(std::size_t bytes = 0) { set_capacity(bytes); }
66 explicit IdTableReadCache(Policy policy) { set_capacity(policy); }
74 IdTableReadCache(IdTableReadCache&& other) noexcept { *this = std::move(other); }
76 if (this != &other) {
77 release();
78 line_bytes_ = other.line_bytes_;
79 slots_ = other.slots_;
80 capacity_bytes_ = other.capacity_bytes_;
81 // The shared cache is nonmovable and its callbacks borrow this
82 // owner. Both instances start fresh after an ownership change.
83 other.release();
84 }
85 return *this;
86 }
87 void set_capacity(std::size_t bytes) {
88 if (bytes > kCacheBytes || bytes % kPageBytes != 0)
89 throw std::invalid_argument("id read cache capacity must be page-aligned and at most 64 KiB");
91 }
92 void set_capacity(Policy policy) {
93 constexpr auto maximum = static_cast<std::size_t>(
94 std::numeric_limits<std::ptrdiff_t>::max());
95 if (policy.line_bytes == 0)
96 throw std::invalid_argument("id read cache line size must be positive");
97 const auto inclusive_line = ReadThroughCache::line_bytes(policy.line_bytes);
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");
101 release();
102 line_bytes_ = policy.line_bytes;
103 slots_ = policy.lines;
104 capacity_bytes_ = slots_ * inclusive_line;
105 }
106 void invalidate() const noexcept {
107 if (cache_) (void)cache_->invalidate_all();
108 }
109 void invalidate_from(unsigned file, std::uint64_t offset) const noexcept {
110 if (!cache_) return;
111 const auto page = offset / line_bytes_;
112 const auto shift = file_shift(file);
113 if (page > kMaximum - shift || page + shift > kMaximum / line_bytes_) {
114 invalidate();
115 return;
116 }
117 const auto key = page_key(file, page);
118 (void)cache_->invalidate_from(key.backing_identity, key.generation,
119 key.page_number * line_bytes_);
120 }
121 void release() noexcept {
122 cache_.reset();
123 prepared_.reset();
124 wide_workspace_.reset();
125 }
126
127 void before_write(unsigned file, std::uint64_t logical_end,
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) {
131 // Include any overwritten prefix when a write straddles the end.
132 // Pure appends retain all complete pages before the old tail.
133 invalidate_from(file, std::min(offset, logical_end));
134 return;
135 }
136 const auto first = offset / line_bytes_;
137 const auto last = (offset + length - 1) / line_bytes_;
138 // The public cache port has key and tail invalidation. Bound the key
139 // walk by configured slots; a wider write conservatively drops the
140 // same file's following pages too, without visiting backing records.
141 if (last - first >= slots_) {
142 invalidate_from(file, offset);
143 return;
144 }
145 for (auto page = first;; ++page) {
146 (void)cache_->invalidate(page_key(file, page));
147 if (page == last) break;
148 }
149 }
150
151 template<class File>
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);
157 return;
158 }
159 // A translated key must remain ordered for tail invalidation.
160 if ((offset + length - 1) / line_bytes_ > kMaximum - file_shift(file)
161 || !prepare()) {
162 source.read(offset, destination, length);
163 return;
164 }
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);
168 };
169 struct ActiveRead {
170 ReadContext& context;
171 ~ActiveRead() {
172 context.source = nullptr;
173 context.read = nullptr;
174 context.failure = nullptr;
175 }
176 } active{context_};
177 if (line_bytes_ > kPageBytes) {
178 read_cached(logical_end, file, offset, destination, length, wide_workspace_.get());
179 } else {
180 std::array<std::byte, kPageBytes> workspace;
181 read_cached(logical_end, file, offset, destination, length, workspace.data());
182 }
183 }
184
185private:
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;
191 }
192 static ReadThroughCacheKey page_key(unsigned file, std::uint64_t page) noexcept {
193 // The shared direct map XORs identity*I and generation*G. Exchanging
194 // I/G in these two salts cancels those products, leaving the legacy
195 // (page + file*7) placement while the full keys still distinguish files.
196 return {file * kGenerationMix, file * kIdentityMix, page + file_shift(file)};
197 }
198 struct StorageDeleter {
199 void operator()(void* bytes) const noexcept { ::operator delete(bytes); }
200 };
201 using Storage = std::unique_ptr<void, StorageDeleter>;
202 struct ReadContext {
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;
207 };
208 static ReadThroughCacheIoResult backing_read(void* owner, ReadThroughCacheKey,
209 ScratchBuffer destination) noexcept {
210 auto& context = *static_cast<ReadContext*>(owner);
211 try {
212 context.read(context.source, context.offset, destination.data, destination.size);
213 return {ScratchStatus::ok, destination.size, false, nullptr};
214 } catch (...) {
215 context.failure = std::current_exception();
216 // The throwing reader exposes no completed-byte count. Preserve
217 // its exact exception rather than inventing a partial-I/O count.
218 return {ScratchStatus::io_error, 0, false, nullptr};
219 }
220 }
221 bool prepare() const noexcept {
222 if (cache_) return true;
223 // Acquire the shared block before get(), so an allocation failure can
224 // use the ORIGINAL read span (not a speculative page fill). A later
225 // logical read retries; the shared instance itself never sees ENOMEM.
226 prepared_.reset(::operator new(capacity_bytes_, std::nothrow));
227 if (!prepared_) return false;
228 if (line_bytes_ > kPageBytes) {
229 wide_workspace_.reset(new (std::nothrow) std::byte[line_bytes_]);
230 if (!wide_workspace_) {
231 prepared_.reset();
232 return false;
233 }
234 }
235 const ReadThroughCachePolicy policy{
236 capacity_bytes_, line_bytes_, ReadThroughCacheReplacement::direct_mapped,
238 const ReadThroughCacheBacking backing{&context_, &backing_read, nullptr};
239 const ReadThroughCacheAllocator allocator{
240 &prepared_,
241 [](void* storage, std::size_t) noexcept -> void* {
242 return static_cast<Storage*>(storage)->release();
243 },
244 [](void*, void* bytes, std::size_t) noexcept { ::operator delete(bytes); }
245 };
246 cache_.emplace(policy, backing, allocator);
247 return true;
248 }
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});
260 if (status != ScratchStatus::ok) {
261 if (context_.failure) std::rethrow_exception(context_.failure);
262 throw std::logic_error("id read cache callback contract failed");
263 }
264 const auto count = std::min(length, valid - within);
265 std::memcpy(output, workspace + within, count);
266 output += count;
267 offset += count;
268 length -= count;
269 }
270 }
271 std::size_t line_bytes_ = kPageBytes;
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_;
278};
279} // namespace pineforge::detail
IdTableReadCache(const IdTableReadCache &)=delete
static constexpr std::size_t kCacheBytes
void read(const File &source, bool plain_disk, std::uint64_t logical_end, unsigned file, std::uint64_t offset, void *destination, std::size_t length) const
IdTableReadCache & operator=(IdTableReadCache &&other) noexcept
static Policy policy_for_capacity(CacheCapacity capacity) noexcept
static constexpr std::size_t kAdapterCacheBytes
IdTableReadCache(IdTableReadCache &&other) noexcept
void set_cache_capacity(CacheCapacity capacity)
static constexpr std::size_t kPairCacheBytes
void before_write(unsigned file, std::uint64_t logical_end, std::uint64_t offset, std::size_t length) const noexcept
static constexpr std::size_t kPageBytes
void invalidate_from(unsigned file, std::uint64_t offset) const noexcept
static constexpr std::size_t kSlots
IdTableReadCache & operator=(const IdTableReadCache &)=delete
static std::size_t line_bytes(std::size_t page_size) noexcept
std::uint64_t page(std::uint64_t bytes)