PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
disk_resident_id_table.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <cassert>
8#include <cstddef>
9#include <cstdint>
10#include <iterator>
11#include <limits>
12#include <map>
13#include <memory>
14#include <string>
15#include <string_view>
16#include <type_traits>
17#include <utility>
18
19namespace pineforge::detail {
20
21// An id index plus the rows that still own live data. Value supplies a fixed
22// Record, retired_record(), restore_record(Record), and spillable(). Retired
23// scalar history lives on disk; live rosters stay resident. Iterators pin a
24// row across nested lookups. No destructor does I/O: eviction writes before
25// erasing, at the next lookup or explicit spill(), where failures can throw.
26//
27// begin()/end() visit RESIDENT rows, newest first. A spilled row
28// has no live members, so source decisions over live members can omit it.
29// resident_rows() visits them in ascending original offset order, including
30// rows restored from disk. Empty rows can await an implicit spill: readers
31// must use live members, never physical residency, as a decision predicate.
32// all_sorted() visits EVERY logical row, including retired ones, for hashing.
33// size() is the logical count, resident_size() the physical count.
34// Values with an opening/live-unit view additionally supply held(). Their
35// writers call refresh_held() after changing that predicate; admission and
36// restore register it automatically. held_rows() walks exactly that subset.
37template<class Value>
40 using Offset = typename Index::Offset;
41 using Row = std::pair<const std::string, Value>;
42 struct ResidentProbe {
43 std::string_view key;
44 std::uint64_t* comparisons;
45 };
46 struct ResidentKeyLess {
47 using is_transparent = void;
48 bool operator()(std::string_view a, std::string_view b) const noexcept { return a < b; }
49 bool operator()(const ResidentProbe& a, std::string_view b) const noexcept {
50 ++*a.comparisons;
51 return a.key < b;
52 }
53 bool operator()(std::string_view a, const ResidentProbe& b) const noexcept {
54 ++*b.comparisons;
55 return a < b.key;
56 }
57 };
58 // Borrowed stable keys, worst-case O(log R), no arena reads. The probe
59 // overloads count actual comparisons, not a hard-coded cost per call.
60 using ResidentKeys = std::map<std::string_view, Offset, ResidentKeyLess>;
61 struct ResidentRow {
62 std::shared_ptr<Row> owner;
63 typename ResidentKeys::iterator key;
64 };
65 using Residents = std::map<Offset, ResidentRow>;
66 // Derived ordered set of offsets, with a row pin for O(1) dereference.
67 // Only rows whose Value::held() predicate is true belong here. Pending
68 // origins alone do not qualify. One map node per held cohort, O(H) space.
69 using Held = std::map<Offset, std::shared_ptr<Row>>;
70
71public:
72 // Physical work only; not logical broker state and never hashed.
73 // A6 counters are single-threaded per run; mutable members are written
74 // from const methods because reads may restore, spill or instrument rows.
75 struct WorkCounters {
76 std::uint64_t rows_visited_by_spill = 0;
77 std::uint64_t find_resident_probes = 0;
78 std::uint64_t resident_rows_visited = 0;
79 std::uint64_t held_rows_visited = 0;
80 std::uint64_t iterated_rows = 0;
81 std::uint64_t rows_visited_by_capacity = 0;
82 std::uint64_t rows_spilled = 0, rows_restored = 0;
83 };
84 const WorkCounters& work_counters() const noexcept { return work_; }
85 static constexpr std::size_t resident_row_bytes() noexcept { return sizeof(Row); }
86 std::size_t resident_directory_heap_estimate() const noexcept {
87 // Portable estimate, not allocator accounting: two map nodes'
88 // links/color and the shared-row control block.
89 return resident_.size() * (sizeof(typename Residents::value_type) + 4 * sizeof(void*)
90 + sizeof(typename ResidentKeys::value_type) + 4 * sizeof(void*) + 2 * sizeof(void*))
91 + held_.size() * (sizeof(typename Held::value_type) + 4 * sizeof(void*));
92 }
93
94 template<bool Const, bool Sorted = false, bool Insertion = false>
95 class Iterator {
96 friend class DiskResidentIdTable;
97 using Owner = std::conditional_t<Const, const DiskResidentIdTable, DiskResidentIdTable>;
98 Owner* owner_ = nullptr;
99 Offset at_ = 0;
100 std::shared_ptr<Row> row_;
101
102 Iterator(Owner* owner, Offset at, std::shared_ptr<Row> row) noexcept
103 : owner_(owner), at_(at), row_(std::move(row)) {}
104 public:
105 using iterator_category = std::forward_iterator_tag;
106 using value_type = Row;
107 using difference_type = std::ptrdiff_t;
108 using reference = std::conditional_t<Const, const Row&, Row&>;
109 using pointer = std::conditional_t<Const, const Row*, Row*>;
110 Iterator() = default;
111 reference operator*() const noexcept {
112 ++owner_->work_.iterated_rows;
113 return *row_;
114 }
115 pointer operator->() const noexcept {
116 ++owner_->work_.iterated_rows;
117 return row_.get();
118 }
119 Iterator& operator++() {
120 ++owner_->work_.iterated_rows;
121 if constexpr (Sorted) {
122 const auto next = owner_->index_.next_sorted(at_);
123 row_.reset();
124 owner_->spill_implicitly();
125 at_ = next;
126 if (at_ != 0) row_ = owner_->load(at_);
127 } else if constexpr (Insertion) {
128 // Nested lookups can erase other empty rows. Re-seek by
129 // offset rather than keeping an invalidatable map iterator.
130 const auto next = owner_->resident_.upper_bound(at_);
131 at_ = next == owner_->resident_.end() ? 0 : next->first;
132 row_ = at_ == 0 ? nullptr : next->second.owner;
133 } else {
134 auto next = owner_->resident_.lower_bound(at_);
135 if (next == owner_->resident_.begin()) {
136 at_ = 0;
137 row_.reset();
138 } else {
139 --next;
140 at_ = next->first;
141 row_ = next->second.owner;
142 }
143 }
144 return *this;
145 }
146 Iterator operator++(int) { auto old = *this; ++*this; return old; }
147 bool operator==(const Iterator& other) const noexcept {
148 return owner_ == other.owner_ && at_ == other.at_;
149 }
150 bool operator!=(const Iterator& other) const noexcept { return !(*this == other); }
151 };
157
158 // Pin the CURRENT row while walking map nodes directly. A nested lookup
159 // may spill other rows, but cannot erase this node before ++ advances.
160 // This keeps an ascending walk O(R), rather than re-seeking R times.
161 // As with ordinary container ranges, clear/move during a walk is invalid.
162 template<bool Const> class ResidentIterator {
164 using Owner = std::conditional_t<Const, const DiskResidentIdTable, DiskResidentIdTable>;
165 Owner* owner_ = nullptr;
166 typename Residents::const_iterator at_{};
167 std::shared_ptr<Row> row_;
168 ResidentIterator(Owner* owner, typename Residents::const_iterator at) noexcept
169 : owner_(owner), at_(at),
170 row_(at == owner->resident_.end() ? nullptr : at->second.owner) {}
171 public:
172 using iterator_category = std::forward_iterator_tag;
173 using value_type = Row;
174 using difference_type = std::ptrdiff_t;
175 using reference = std::conditional_t<Const, const Row&, Row&>;
176 using pointer = std::conditional_t<Const, const Row*, Row*>;
177 ResidentIterator() = default;
178 reference operator*() const noexcept {
179 ++owner_->work_.resident_rows_visited;
180 return *row_;
181 }
182 pointer operator->() const noexcept {
183 ++owner_->work_.resident_rows_visited;
184 return row_.get();
185 }
186 ResidentIterator& operator++() noexcept {
187 ++at_; // The current node is still pinned.
188 row_ = at_ == owner_->resident_.end() ? nullptr : at_->second.owner;
189 return *this;
190 }
191 ResidentIterator operator++(int) noexcept { auto old = *this; ++*this; return old; }
192 bool operator==(const ResidentIterator& other) const noexcept {
193 return owner_ == other.owner_ && (!owner_ || at_ == other.at_);
194 }
195 bool operator!=(const ResidentIterator& other) const noexcept { return !(*this == other); }
196 };
197
198 // Ascending offsets, like resident_rows(), but only openings/live units.
199 // Membership changes must use refresh_held(). When removing the current
200 // member during a walk, advance the iterator before changing that row.
201 template<bool Const> class HeldIterator {
203 using Owner = std::conditional_t<Const, const DiskResidentIdTable, DiskResidentIdTable>;
204 Owner* owner_ = nullptr;
205 typename Held::const_iterator at_{};
206 Offset offset_ = 0;
207 std::shared_ptr<Row> row_;
208 HeldIterator(Owner* owner, typename Held::const_iterator at) noexcept
209 : owner_(owner), at_(at),
210 offset_(at == owner->held_.end() ? 0 : at->first),
211 row_(at == owner->held_.end() ? nullptr : at->second) {}
212 public:
213 using iterator_category = std::forward_iterator_tag;
214 using value_type = Row;
215 using difference_type = std::ptrdiff_t;
216 using reference = std::conditional_t<Const, const Row&, Row&>;
217 using pointer = std::conditional_t<Const, const Row*, Row*>;
218 HeldIterator() = default;
219 reference operator*() const noexcept {
220 ++owner_->work_.held_rows_visited;
221 return *row_;
222 }
223 pointer operator->() const noexcept {
224 ++owner_->work_.held_rows_visited;
225 return row_.get();
226 }
227 HeldIterator& operator++() noexcept {
228 ++at_;
229 offset_ = at_ == owner_->held_.end() ? 0 : at_->first;
230 row_ = at_ == owner_->held_.end() ? nullptr : at_->second;
231 return *this;
232 }
233 HeldIterator operator++(int) noexcept { auto old = *this; ++*this; return old; }
234 bool operator==(const HeldIterator& other) const noexcept {
235 return owner_ == other.owner_ && (!owner_ || at_ == other.at_);
236 }
237 bool operator!=(const HeldIterator& other) const noexcept { return !(*this == other); }
238 };
239
240 template<class Iter> struct Range {
241 Iter first, last;
242 Iter begin() const { return first; }
243 Iter end() const { return last; }
244 };
245 template<class Iter> class KeyIterator {
246 Iter at_;
247 public:
248 using iterator_category = std::forward_iterator_tag;
249 using value_type = std::string;
250 using difference_type = std::ptrdiff_t;
251 using reference = const std::string&;
252 using pointer = const std::string*;
253 KeyIterator() = default;
254 explicit KeyIterator(Iter at) : at_(std::move(at)) {}
255 reference operator*() const noexcept { return at_->first; }
256 pointer operator->() const noexcept { return &at_->first; }
257 KeyIterator& operator++() { ++at_; return *this; }
258 KeyIterator operator++(int) { auto old = *this; ++*this; return old; }
259 bool operator==(const KeyIterator& other) const noexcept { return at_ == other.at_; }
260 bool operator!=(const KeyIterator& other) const noexcept { return !(*this == other); }
261 };
262
266 DiskResidentIdTable(DiskResidentIdTable&& other) noexcept { *this = std::move(other); }
268 if (this != &other) {
269 clear(); // Drop borrowed keys before dropping their row strings.
270 index_ = std::move(other.index_);
271 resident_ = std::move(other.resident_);
272 resident_by_key_ = std::move(other.resident_by_key_);
273 held_ = std::move(other.held_);
274 work_ = std::exchange(other.work_, {});
275 spill_threshold_ = std::exchange(other.spill_threshold_, kSpillFloor);
276 spill_interval_ = std::exchange(other.spill_interval_, kSpillFloor);
277 calls_since_spill_ = std::exchange(other.calls_since_spill_, 0);
278 fixture_spill_interval_ = std::exchange(other.fixture_spill_interval_, 0);
279 fixture_spill_predicate_ = std::exchange(other.fixture_spill_predicate_, nullptr);
280 other.resident_by_key_.clear();
281 other.resident_.clear();
282 other.held_.clear();
283 assert(held_rows_exact() && other.held_rows_exact());
284 }
285 return *this;
286 }
287
288 void set_store(ScratchStore store) noexcept { index_.set_store(store); }
289 void set_directory(const std::string& directory) { index_.set_directory(directory); }
290 void bind_scratch(const std::shared_ptr<ScratchSlab>& slab, ScratchResource resource) {
291 index_.bind_scratch(slab, resource);
292 }
293 // A ticket anticipates making this id live, including a retired id that
294 // is about to reopen. It is read-only even when a dirty empty row awaits
295 // writeback: refusal must precede table AND kernel mutation.
296 void require_insert(const std::string& key) const {
297 index_.require_insert(key);
298 if (!index_.bounded_cohorts()) return;
299 const auto found = find_resident(key);
300 if (found == end() || found->second.spillable()) require_live_growth(1);
301 }
302 template<class Enumerate> void require_inserts(Enumerate&& enumerate) const {
303 index_.require_inserts(std::forward<Enumerate>(enumerate));
304 if (!index_.bounded_cohorts()) return;
305 std::size_t ordinal = 0, extra = 0;
306 enumerate([&](const std::string& key) {
307 const auto current = ordinal++;
308 const auto found = find_resident(key);
309 if (found != end() && !found->second.spillable()) return;
310 bool repeated = false;
311 std::size_t prior = 0;
312 enumerate([&](const std::string& other) {
313 if (prior++ < current && other == key) repeated = true;
314 });
315 if (!repeated) {
316 if (extra == kMaxResidentCohorts) require_live_growth(extra + 1);
317 ++extra;
318 }
319 });
320 require_live_growth(extra); // At most one dry resident pass for the whole ticket.
321 }
322 void clear() noexcept {
323 held_.clear();
324 resident_by_key_.clear();
325 resident_.clear();
326 index_.clear();
327 work_ = {};
328 spill_threshold_ = spill_interval_ = kSpillFloor;
329 calls_since_spill_ = 0;
330 assert(held_rows_exact());
331 }
332 std::uint64_t size() const noexcept { return index_.size(); }
333 std::size_t resident_size() const noexcept { return resident_.size(); }
334 std::size_t held_size() const noexcept { return held_.size(); }
335 iterator begin() noexcept { return resident_begin<false>(this); }
336 iterator end() noexcept { return {this, 0, {}}; }
337 const_iterator begin() const noexcept { return resident_begin<true>(this); }
338 const_iterator end() const noexcept { return {this, 0, {}}; }
339 auto resident_rows() noexcept {
340 return Range<ResidentIterator<false>>{
341 {this, resident_.begin()}, {this, resident_.end()}};
342 }
343 auto resident_rows() const noexcept {
344 return Range<ResidentIterator<true>>{
345 {this, resident_.begin()}, {this, resident_.end()}};
346 }
347 auto held_rows() noexcept {
348 assert(held_rows_exact());
349 return Range<HeldIterator<false>>{{this, held_.begin()}, {this, held_.end()}};
350 }
351 auto held_rows() const noexcept {
352 assert(held_rows_exact());
353 return Range<HeldIterator<true>>{{this, held_.begin()}, {this, held_.end()}};
354 }
355 // O(log H) update using the row's already-known offset and pin. No id
356 // search or full resident walk on the Release mutation path.
357 void refresh_held(const iterator& row) const {
358 assert(row.owner_ == this && row.row_);
359 refresh_held(row.at_, row.row_);
360 }
361 template<bool Const> void refresh_held(const HeldIterator<Const>& row) const {
362 assert(row.owner_ == this && row.row_);
363 refresh_held(row.offset_, row.row_);
364 }
365 // Independent, allocation-free oracles. They deliberately do not charge
366 // production work counters; assert-enabled builds still pay O(R).
367 bool held_rows_exact() const noexcept {
368 auto next = held_.begin();
369 for (const auto& row : resident_) {
370 if (!is_held(row.second.owner->second, 0)) continue;
371 if (next == held_.end() || next->first != row.first
372 || next->second != row.second.owner) return false;
373 ++next;
374 }
375 return next == held_.end();
376 }
377 std::size_t resident_opened_slot_count() const noexcept {
378 std::size_t total = 0;
379 for (const auto& row : resident_) total += row.second.owner->second.opened.size();
380 return total;
381 }
382 auto resident_ids() const noexcept {
383 const auto at = resident_.begin();
384 const insertion_iterator first{this, at == resident_.end() ? 0 : at->first,
385 at == resident_.end() ? nullptr : at->second.owner};
386 return Range<KeyIterator<insertion_iterator>>{
387 KeyIterator<insertion_iterator>(first),
388 KeyIterator<insertion_iterator>(insertion_iterator{this, 0, {}})};
389 }
390 auto all_sorted() {
391 const auto at = index_.first_sorted();
392 return Range<sorted_iterator>{{this, at, at ? load(at) : nullptr}, {this, 0, {}}};
393 }
394 auto all_sorted() const {
395 const auto at = index_.first_sorted();
396 return Range<const_sorted_iterator>{{this, at, at ? load(at) : nullptr}, {this, 0, {}}};
397 }
398 // Full insertion-order replay without a growing vector of keys.
399 template<class F> void for_each_id(F&& visit) const {
400 for (auto at = index_.first_inserted(); at != 0; at = index_.next_inserted(at))
401 visit(index_.key(at));
402 }
403 // Decisions over historical ids must not depend on row residency.
404 // Walk keys alone, without restoring facts or retaining a key vector.
405 template<class F> void for_each_sorted_id(F&& visit) const {
406 for (auto at = index_.first_sorted(); at != 0; at = index_.next_sorted(at))
407 visit(index_.key(at));
408 }
409 iterator find(const std::string& key) {
411 const auto at = index_.find(key);
412 return at ? iterator{this, *at, load(*at)} : end();
413 }
414 const_iterator find(const std::string& key) const {
416 const auto at = index_.find(key);
417 return at ? const_iterator{this, *at, load(*at)} : end();
418 }
419 // A retired row cannot carry exposure/opened handles. These reads stay
420 // allocation-free and noexcept even when the disk is unavailable.
421 iterator find_resident(const std::string& key) noexcept {
422 const auto at = resident_by_key_.find(ResidentProbe{key, &work_.find_resident_probes});
423 if (at == resident_by_key_.end()) return end();
424 const auto row = resident_.find(at->second);
425 return {this, row->first, row->second.owner};
426 }
427 const_iterator find_resident(const std::string& key) const noexcept {
428 const auto at = resident_by_key_.find(ResidentProbe{key, &work_.find_resident_probes});
429 if (at == resident_by_key_.end()) return end();
430 const auto row = resident_.find(at->second);
431 return {this, row->first, row->second.owner};
432 }
433 std::pair<iterator, bool> emplace(const std::string& key, Value value) {
434 // A refused new logical row must not first overwrite/evict an eligible
435 // resident row. This search reads index bytes only, without restoring.
436 index_.require_insert(key);
437 std::size_t extra = 0;
438 if (index_.bounded_cohorts() && !value.spillable() && !index_.find(key)) {
439 extra = 1; // An existing logical row ignores the incoming value.
440 require_live_growth(extra);
441 }
442 spill_before_admission(extra);
444 const auto placed = index_.insert(key, value.retired_record());
445 if (!placed.second) return {{this, placed.first, load(placed.first)}, false};
446 auto row = std::make_shared<Row>(key, std::move(value));
447 insert_resident(placed.first, row);
448 return {{this, placed.first, std::move(row)}, true};
449 }
450 void spill() const {
451 for (auto at = resident_.begin(); at != resident_.end();) {
452 ++work_.rows_visited_by_spill;
453 if (at->second.owner.use_count() == 1 && at->second.owner->second.spillable()
454 && (fixture_spill_predicate_ == nullptr
455 || fixture_spill_predicate_(at->second.owner->first))
456 ) {
457 index_.set(at->first, at->second.owner->second.retired_record());
458 ++work_.rows_spilled;
459 assert(held_.find(at->first) == held_.end());
460 resident_by_key_.erase(at->second.key); // Amortised O(1), no key search.
461 at = resident_.erase(at);
462 } else ++at;
463 }
464 const auto count = resident_.size();
465 const auto maximum = std::numeric_limits<std::size_t>::max();
466 spill_threshold_ = std::max(kSpillFloor, count > maximum / 2 ? maximum : count * 2);
467 spill_interval_ = std::max(kSpillFloor, count);
468 calls_since_spill_ = 0;
469 }
470 // Growth and a call budget both trigger a pass. The floor handles a pass
471 // that left zero/one rows; the budget also drains rows closed in place
472 // without further growth. A full pass costs O(rows at the previous pass
473 // + new rows), amortised over that many implicit calls or insertions.
474 void spill_implicitly() const {
475 const auto interval = fixture_spill_interval_ ? fixture_spill_interval_ : spill_interval_;
476 if (++calls_since_spill_ >= interval || resident_.size() >= spill_threshold_) spill();
477 }
478 // Test-only schedule control: 0 selects the production policy, 1 the old
479 // eager policy. The growth guard remains in force for every schedule.
480 void fixture_implicit_spill_interval(std::size_t calls) noexcept {
481 fixture_spill_interval_ = calls;
482 }
483 // Test-only observation hook. Production leaves this null, so the hot
484 // spill path has one predictable null check per candidate row.
485 void fixture_spill_predicate(bool (*predicate)(const std::string&)) noexcept {
486 fixture_spill_predicate_ = predicate;
487 }
488
489private:
490 void require_live_growth(std::size_t extra) const {
491 if (extra == 0 || !index_.bounded_cohorts()) return;
492 if (resident_.size() <= kMaxResidentCohorts
493 && extra <= kMaxResidentCohorts - resident_.size()) return;
494 // Admission's O(R) dry pass must finish before spill can write any
495 // bytes. It is at most one such pass per spill, never a per-ticket
496 // scan: callers batch the ticket's extra rows before reaching here.
497 // Counting physical rows here would falsely refuse deferred empties
498 // (including empty rows pinned by an enclosing iterator).
499 std::size_t live = 0;
500 for (const auto& row : resident_) {
502 if (!row.second.owner->second.spillable()) ++live;
503 }
504 if (live > kMaxResidentCohorts || extra > kMaxResidentCohorts - live)
505 throw ScratchCapacityError(ScratchResource::cohort,
507 }
508 void spill_before_admission(std::size_t extra) const {
509 if (extra != 0 && index_.bounded_cohorts() && resident_.size() >= kMaxResidentCohorts)
510 spill(); // The read-only ticket has already proved this can fit.
511 }
512 void insert_resident(Offset at, const std::shared_ptr<Row>& row) const {
513 // Views borrow the immutable key of a heap-pinned row, never the
514 // caller's string. Erase a view before its last row owner is released.
515 const auto key = std::string_view(row->first);
516 const auto indexed = resident_by_key_.emplace(key, at);
517 try {
518 resident_.emplace(at, ResidentRow{row, indexed.first});
519 if (is_held(row->second, 0)) held_.emplace(at, row);
520 } catch (...) {
521 resident_.erase(at);
522 resident_by_key_.erase(indexed.first);
523 throw;
524 }
525 assert(held_rows_exact());
526 }
527 template<class T>
528 static auto is_held(const T& value, int) noexcept -> decltype(value.held()) {
529 return value.held();
530 }
531 // Other users of the generic resident table have no opening/live-unit
532 // view and need no held index.
533 static bool is_held(const Value&, long) noexcept { return false; }
534 // May throw (std::bad_alloc from the node insert) AFTER the caller changed
535 // the row: the exception must end the run, as the other allocation
536 // failures on these paths do; a caller that continued would leave held_
537 // missing a row and later held walks would skip it silently.
538 void refresh_held(Offset at, const std::shared_ptr<Row>& row) const {
539 const auto found = held_.lower_bound(at);
540 if (is_held(row->second, 0)) {
541 if (found == held_.end() || found->first != at) held_.emplace_hint(found, at, row);
542 } else if (found != held_.end() && found->first == at) {
543 held_.erase(found);
544 }
545 assert(held_rows_exact());
546 }
547 // Codecs that validate stored bytes may accept their physical origin.
548 // Keep the original one-argument codec contract for other value types.
549 template<class T = Value>
550 auto restore_record(const typename T::Record& record, int) const
551 -> decltype(T::restore_record(record, bool{})) {
552 return T::restore_record(record, index_.arena_backed());
553 }
554 Value restore_record(const typename Value::Record& record, long) const {
555 return Value::restore_record(record);
556 }
557 template<bool Const, class Owner>
558 static Iterator<Const> resident_begin(Owner* owner) noexcept {
559 const auto at = owner->resident_.rbegin();
560 return {owner, at == owner->resident_.rend() ? 0 : at->first,
561 at == owner->resident_.rend() ? nullptr : at->second.owner};
562 }
563 std::shared_ptr<Row> load(Offset at) const {
564 const auto found = resident_.find(at);
565 if (found != resident_.end()) return found->second.owner;
566 auto value = restore_record(index_.get(at), 0);
567 const std::size_t extra = value.spillable() ? 0 : 1;
568 require_live_growth(extra);
569 spill_before_admission(extra);
570 auto row = std::make_shared<Row>(index_.key(at), std::move(value));
571 insert_resident(at, row);
572 ++work_.rows_restored;
573 return row;
574 }
575 static constexpr std::size_t kSpillFloor = 64;
576 mutable Index index_;
577 mutable Residents resident_;
578 mutable ResidentKeys resident_by_key_; // One borrowed key per resident row.
579 mutable Held held_;
580 mutable WorkCounters work_{};
581 mutable std::size_t spill_threshold_ = kSpillFloor, spill_interval_ = kSpillFloor;
582 mutable std::size_t calls_since_spill_ = 0;
583 std::size_t fixture_spill_interval_ = 0;
584 bool (*fixture_spill_predicate_)(const std::string&) = nullptr;
585};
586} // namespace pineforge::detail
#define F(name)
bool bounded_cohorts() const noexcept
bool operator==(const HeldIterator &other) const noexcept
std::conditional_t< Const, const Row *, Row * > pointer
std::conditional_t< Const, const Row &, Row & > reference
bool operator!=(const HeldIterator &other) const noexcept
std::conditional_t< Const, const Row *, Row * > pointer
bool operator==(const Iterator &other) const noexcept
bool operator!=(const Iterator &other) const noexcept
std::conditional_t< Const, const Row &, Row & > reference
bool operator==(const KeyIterator &other) const noexcept
bool operator!=(const KeyIterator &other) const noexcept
bool operator==(const ResidentIterator &other) const noexcept
std::conditional_t< Const, const Row &, Row & > reference
std::conditional_t< Const, const Row *, Row * > pointer
bool operator!=(const ResidentIterator &other) const noexcept
std::size_t resident_opened_slot_count() const noexcept
void require_inserts(Enumerate &&enumerate) const
void set_store(ScratchStore store) noexcept
const_iterator find_resident(const std::string &key) const noexcept
void require_insert(const std::string &key) const
void fixture_implicit_spill_interval(std::size_t calls) noexcept
DiskResidentIdTable & operator=(DiskResidentIdTable &&other) noexcept
const_iterator find(const std::string &key) const
void refresh_held(const iterator &row) const
std::pair< iterator, bool > emplace(const std::string &key, Value value)
void refresh_held(const HeldIterator< Const > &row) const
DiskResidentIdTable & operator=(const DiskResidentIdTable &)=delete
iterator find_resident(const std::string &key) noexcept
void bind_scratch(const std::shared_ptr< ScratchSlab > &slab, ScratchResource resource)
DiskResidentIdTable(DiskResidentIdTable &&other) noexcept
const WorkCounters & work_counters() const noexcept
static constexpr std::size_t resident_row_bytes() noexcept
void set_directory(const std::string &directory)
DiskResidentIdTable(const DiskResidentIdTable &)=delete
std::size_t resident_directory_heap_estimate() const noexcept
void fixture_spill_predicate(bool(*predicate)(const std::string &)) noexcept
Iterator< true, false, true > insertion_iterator
constexpr std::size_t kMaxResidentCohorts
std::string scratch_resident_cohort_limit_message(std::uint64_t limit)