PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
owned_storage.hpp
Go to the documentation of this file.
1#pragma once
2#include <algorithm>
3#include <array>
4#include <atomic>
5#include <cstddef>
6#include <cstdint>
7#include <deque>
8#include <exception>
9#include <limits>
10#include <memory>
11#include <new>
12#include <optional>
13#include <string>
14#include <tuple>
15#include <type_traits>
16#include <unordered_map>
17#include <utility>
18#include <vector>
19
20namespace pineforge {
21
22// Allocation observations count requested storage, including unused capacity,
23// not allocator-internal rounding or RSS. Inline objects are counted by their
24// containing owner; the hooks below add ONLY out-of-object ownership. One walk
25// must cover every root to deduplicate checkpoint/shared allocations.
38
40 std::uint64_t resident_metadata_bytes = 0;
41 std::uint64_t resident_payload_bytes = 0;
42 std::uint64_t backing_bytes = 0; // owned file extent/backing reservation
43 std::uint64_t logical_payload_bytes = 0; // extent, NEVER charged as capacity
44 std::uint64_t workspace_bytes = 0; // largest sequential transfer window
45 std::uint64_t observer_workspace_bytes = 0; // temporary identity directory
46 std::uint64_t unsupported = 0;
47 bool overflow = false;
48
49 static std::uint64_t sum(std::uint64_t a, std::uint64_t b) noexcept {
50 const auto max = std::numeric_limits<std::uint64_t>::max();
51 return b > max - a ? max : a + b;
52 }
53 bool complete() const noexcept { return !unsupported && !overflow; }
54 std::uint64_t resident_bytes() const noexcept {
56 }
57 // An incomplete observation has no usable scalar total. Known categories
58 // remain visible as lower bounds; a hard budget must refuse an incomplete
59 // observation explicitly, rather than report invented used/needed bytes.
60 std::uint64_t storage_bytes() const noexcept {
62 : std::numeric_limits<std::uint64_t>::max();
63 }
64};
65
67 struct Identity {
68 const void* address;
69 const void* kind; // null = allocation; otherwise the traversed type
70 };
71 OwnedStorageUsage usage_;
72 std::vector<Identity> identities_;
73 void add(std::uint64_t& target, std::uint64_t bytes) noexcept {
74 if (bytes > std::numeric_limits<std::uint64_t>::max() - target)
75 usage_.overflow = true;
76 target = OwnedStorageUsage::sum(target, bytes);
77 }
78 bool claim_identity(const void* address, const void* kind) noexcept {
79 if (!address) return false;
80 for (const auto& identity : identities_)
81 if (identity.address == address && identity.kind == kind) return false;
82 try { identities_.push_back({address, kind}); }
84 return true;
85 }
86public:
87 void incomplete(OwnedStorageUnsupported reason) noexcept {
88 usage_.unsupported |= static_cast<std::uint64_t>(reason);
89 }
90 std::uint64_t product(std::uint64_t count, std::uint64_t width) noexcept {
91 if (width && count > std::numeric_limits<std::uint64_t>::max() / width) {
92 usage_.overflow = true;
93 return std::numeric_limits<std::uint64_t>::max();
94 }
95 return count * width;
96 }
97 bool claim(const void* identity) noexcept {
98 return claim_identity(identity, nullptr);
99 }
100 template<class T> bool visit(const T& object) noexcept {
101 // A vector's first element may have exactly its allocation address.
102 // A containing object and its first field may also share an address.
103 // Neither relationship makes their external allocations identical.
104 static const char kind = 0;
105 return claim_identity(&object, &kind);
106 }
107 void metadata(std::uint64_t bytes) noexcept { add(usage_.resident_metadata_bytes, bytes); }
108 void payload(std::uint64_t bytes) noexcept { add(usage_.resident_payload_bytes, bytes); }
109 void backing(std::uint64_t bytes) noexcept { add(usage_.backing_bytes, bytes); }
110 void logical(std::uint64_t bytes) noexcept { add(usage_.logical_payload_bytes, bytes); }
111 void workspace(std::uint64_t bytes) noexcept {
112 usage_.workspace_bytes = std::max(usage_.workspace_bytes, bytes);
113 }
114 OwnedStorageUsage usage() const noexcept {
115 auto result = usage_;
116 result.observer_workspace_bytes = identities_.capacity() * sizeof(Identity);
117 // Detect overflow between categories as well as inside each category.
118 const auto max = std::numeric_limits<std::uint64_t>::max();
119 result.overflow |= result.resident_payload_bytes > max - result.resident_metadata_bytes;
120 result.overflow |= result.backing_bytes > max - result.resident_bytes();
121 return result;
122 }
123};
124
125namespace detail {
127 void* address = nullptr;
128 std::size_t bytes = 0;
129 bool payload = false;
130 bool active = false;
131};
133 std::atomic<std::size_t> references{1};
134 std::vector<OwnedAllocation> allocations;
135};
136} // namespace detail
137
138// Rebound STL allocations are observed at allocate(n), never inferred from
139// implementation-specific node/block sizes. The ledger owns a reusable record
140// pool, so record storage grows with peak live allocations and is reused after
141// deallocation; the ledger and pool backing are real, counted allocations.
142// Container copies get separate ledgers; allocator rebinds and allocate_shared's
143// control block retain the actual ownership id.
144// Allocation/mutation and observation follow the owner's serialized execution.
145template<class T, class Element = T>
147 template<class, class> friend class OwnedAllocator;
149 void retain() noexcept { ledger_->references.fetch_add(1, std::memory_order_relaxed); }
150 void release() noexcept {
151 if (ledger_->references.fetch_sub(1, std::memory_order_acq_rel) == 1) delete ledger_;
152 }
153public:
154 using value_type = T;
157 using propagate_on_container_swap = std::true_type;
158 using is_always_equal = std::false_type;
159 template<class U> struct rebind { using other = OwnedAllocator<U, Element>; };
160 OwnedAllocator() : ledger_(new detail::OwnedAllocationLedger) {}
161 OwnedAllocator(const OwnedAllocator& other) noexcept : ledger_(other.ledger_) { retain(); }
162 template<class U>
163 OwnedAllocator(const OwnedAllocator<U, Element>& other) noexcept : ledger_(other.ledger_) {
164 retain();
165 }
166 OwnedAllocator& operator=(const OwnedAllocator& other) noexcept {
167 if (ledger_ != other.ledger_) {
168 other.ledger_->references.fetch_add(1, std::memory_order_relaxed);
169 release();
170 ledger_ = other.ledger_;
171 }
172 return *this;
173 }
174 ~OwnedAllocator() { release(); }
176 T* allocate(std::size_t count) {
177 if (count > std::numeric_limits<std::size_t>::max() / sizeof(T))
178 throw std::bad_array_new_length();
179 T* data = std::allocator<T>{}.allocate(count);
180 const detail::OwnedAllocation entry{
181 data, count * sizeof(T), std::is_same_v<T, Element>, true};
182 try {
183 auto record = std::find_if(
184 ledger_->allocations.begin(), ledger_->allocations.end(),
185 [](const detail::OwnedAllocation& candidate) {
186 return !candidate.active;
187 });
188 if (record != ledger_->allocations.end()) {
189 *record = entry;
190 } else {
191 ledger_->allocations.push_back(entry);
192 }
193 } catch (...) { std::allocator<T>{}.deallocate(data, count); throw; }
194 return data;
195 }
196 void deallocate(T* data, std::size_t count) noexcept {
197 for (auto& record : ledger_->allocations) {
198 if (!record.active || record.address != data) continue;
199 record = detail::OwnedAllocation{};
200 std::allocator<T>{}.deallocate(data, count);
201 return;
202 }
203 std::terminate(); // a real allocator ownership violation
204 }
205 void observe_owned_storage(OwnedStorage& out) const noexcept {
206 if (!out.claim(ledger_)) return;
207 out.metadata(sizeof(*ledger_));
208 // The reusable records live in one ledger-owned vector allocation.
209 out.metadata(out.product(ledger_->allocations.capacity(),
210 sizeof(detail::OwnedAllocation)));
211 for (const auto& record : ledger_->allocations) {
212 if (!record.active) continue;
213 // Rebound nodes/maps include their inline elements in metadata;
214 // un-rebound element buffers are payload. No node layout guess.
215 if (record.payload) out.payload(record.bytes);
216 else out.metadata(record.bytes);
217 }
218 }
219 // Verify a tracked shared owner against real request bounds, without
220 // depending on where allocate_shared places its object/control fields.
221 // A copied/moved object outside this domain cannot claim full coverage.
222 bool contains(const void* object, std::size_t bytes) const noexcept {
223 const auto address = reinterpret_cast<std::uintptr_t>(object);
224 for (const auto& record : ledger_->allocations) {
225 if (!record.active) continue;
226 const auto base = reinterpret_cast<std::uintptr_t>(record.address);
227 if (address >= base && address - base <= record.bytes
228 && bytes <= record.bytes - (address - base)) return true;
229 }
230 return false;
231 }
232 template<class U>
233 bool operator==(const OwnedAllocator<U, Element>& other) const noexcept {
234 return ledger_ == other.ledger_;
235 }
236 template<class U>
237 bool operator!=(const OwnedAllocator<U, Element>& other) const noexcept {
238 return !(*this == other);
239 }
240};
241
242template<class T, class = void>
244 static void observe(OwnedStorage& out, const T&) noexcept {
245 if constexpr (!std::is_arithmetic_v<T> && !std::is_enum_v<T>
246 && !std::is_same_v<T, std::nullptr_t>)
248 }
249};
250template<class T>
251struct OwnedStorageTraits<T, std::void_t<decltype(
252 std::declval<const T&>().observe_owned_storage(std::declval<OwnedStorage&>()))>> {
253 static void observe(OwnedStorage& out, const T& value) noexcept {
254 try { value.observe_owned_storage(out); }
255 catch (...) { out.incomplete(OwnedStorageUnsupported::opaque_type); }
256 }
257};
258template<class T>
259void observe_owned_storage(OwnedStorage& out, const T& value) noexcept {
261}
262template<class... T>
263void observe_owned_members(OwnedStorage& out, const T&... values) noexcept {
264 (observe_owned_storage(out, values), ...);
265}
266template<>
268 static void observe(OwnedStorage& out, const std::string& value) noexcept {
269 const auto object = reinterpret_cast<std::uintptr_t>(&value);
270 const auto data = reinterpret_cast<std::uintptr_t>(value.data());
271 // Public data/capacity identify inline storage without knowing an STL
272 // layout or assuming a particular small-string threshold.
273 if (data >= object && data - object < sizeof(value)) return;
274 if (out.claim(value.data())) {
275 out.payload(out.product(value.capacity(), sizeof(char)));
276 out.payload(1); // the allocated terminator
277 }
278 }
279};
280template<class A> struct IsOwnedAllocator : std::false_type {};
281template<class T, class E> struct IsOwnedAllocator<OwnedAllocator<T, E>> : std::true_type {};
282template<class T, class A>
283struct OwnedStorageTraits<std::vector<T, A>> {
284 static void observe(OwnedStorage& out, const std::vector<T, A>& value) noexcept {
285 if (!out.visit(value)) return;
286 if constexpr (IsOwnedAllocator<A>::value) {
287 value.get_allocator().observe_owned_storage(out);
288 } else if constexpr (std::is_same_v<A, std::allocator<T>>) {
289 if constexpr (std::is_same_v<T, bool>) {
290 constexpr auto bits = std::numeric_limits<unsigned char>::digits;
291 out.payload(value.capacity() / bits + (value.capacity() % bits != 0));
292 } else if (out.claim(value.data())) {
293 out.payload(out.product(value.capacity(), sizeof(T)));
294 }
295 } else { out.incomplete(OwnedStorageUnsupported::allocator); }
296 if constexpr (!std::is_arithmetic_v<T> && !std::is_enum_v<T>)
297 for (const auto& element : value) observe_owned_storage(out, element);
298 }
299};
300template<class T, class A>
301struct OwnedStorageTraits<std::deque<T, A>> {
302 static void observe(OwnedStorage& out, const std::deque<T, A>& value) noexcept {
303 if (!out.visit(value)) return;
304 if constexpr (IsOwnedAllocator<A>::value)
305 value.get_allocator().observe_owned_storage(out);
306 else out.incomplete(OwnedStorageUnsupported::allocator);
307 if constexpr (!std::is_arithmetic_v<T> && !std::is_enum_v<T>)
308 for (const auto& element : value) observe_owned_storage(out, element);
309 }
310};
311template<class K, class V, class H, class E, class A>
312struct OwnedStorageTraits<std::unordered_map<K, V, H, E, A>> {
313 static void observe(OwnedStorage& out, const std::unordered_map<K, V, H, E, A>& value) noexcept {
314 if (!out.visit(value)) return;
315 if constexpr (IsOwnedAllocator<A>::value)
316 value.get_allocator().observe_owned_storage(out);
317 else out.incomplete(OwnedStorageUnsupported::allocator);
318 for (const auto& element : value) observe_owned_members(out, element.first, element.second);
319 }
320};
321template<class... T>
322struct OwnedStorageTraits<std::tuple<T...>> {
323 static void observe(OwnedStorage& out, const std::tuple<T...>& value) noexcept {
324 std::apply([&](const auto&... elements) { observe_owned_members(out, elements...); }, value);
325 }
326};
327template<class A, class B>
328struct OwnedStorageTraits<std::pair<A, B>> {
329 static void observe(OwnedStorage& out, const std::pair<A, B>& value) noexcept {
330 observe_owned_members(out, value.first, value.second);
331 }
332};
333template<class T, std::size_t N>
334struct OwnedStorageTraits<std::array<T, N>> {
335 static void observe(OwnedStorage& out, const std::array<T, N>& value) noexcept {
336 for (const auto& element : value) observe_owned_storage(out, element);
337 }
338};
339template<class T>
340struct OwnedStorageTraits<std::optional<T>> {
341 static void observe(OwnedStorage& out, const std::optional<T>& value) noexcept {
342 if (value) observe_owned_storage(out, *value);
343 }
344};
345
346// One allocation domain per container, including copy/move assignment. The
347// explicit-allocator move constructors prevent a moved-from container from
348// retaining an allocator that observes the destination's live allocations.
349// Ordinary container capacities, clear/erase and element ordering still come
350// from the STL; no block, node or bucket geometry is reimplemented.
351template<class T>
352class OwnedDeque : public std::deque<T, OwnedAllocator<T>> {
353 using Base = std::deque<T, OwnedAllocator<T>>;
354public:
355 OwnedDeque() = default;
356 explicit OwnedDeque(std::size_t count) : Base(count) {}
357 OwnedDeque(std::size_t count, const T& value) : Base(count, value) {}
358 OwnedDeque(const OwnedDeque& other) : Base(other, OwnedAllocator<T>{}) {}
359 OwnedDeque(OwnedDeque&& other) : Base(std::move(other), OwnedAllocator<T>{}) {}
361 Base::operator=(other);
362 return *this;
363 }
365 if (this != &other) Base::operator=(std::move(other));
366 return *this;
367 }
368 void observe_owned_storage(OwnedStorage& out) const noexcept {
370 }
371};
372template<class K, class V>
373class OwnedUnorderedMap : public std::unordered_map<K, V, std::hash<K>, std::equal_to<K>,
374 OwnedAllocator<std::pair<const K, V>>> {
375 using Allocator = OwnedAllocator<std::pair<const K, V>>;
376 using Base = std::unordered_map<K, V, std::hash<K>, std::equal_to<K>, Allocator>;
377public:
378 OwnedUnorderedMap() = default;
379 OwnedUnorderedMap(const OwnedUnorderedMap& other) : Base(other, Allocator{}) {}
380 OwnedUnorderedMap(OwnedUnorderedMap&& other) : Base(std::move(other), Allocator{}) {}
382 Base::operator=(other);
383 return *this;
384 }
386 if (this != &other) Base::operator=(std::move(other));
387 return *this;
388 }
389 void observe_owned_storage(OwnedStorage& out) const noexcept {
391 }
392};
393
394} // namespace pineforge
OwnedAllocator(const OwnedAllocator &other) noexcept
std::false_type is_always_equal
std::true_type propagate_on_container_swap
void observe_owned_storage(OwnedStorage &out) const noexcept
OwnedAllocator select_on_container_copy_construction() const
bool operator!=(const OwnedAllocator< U, Element > &other) const noexcept
bool contains(const void *object, std::size_t bytes) const noexcept
void deallocate(T *data, std::size_t count) noexcept
T * allocate(std::size_t count)
std::false_type propagate_on_container_move_assignment
bool operator==(const OwnedAllocator< U, Element > &other) const noexcept
OwnedAllocator & operator=(const OwnedAllocator &other) noexcept
OwnedAllocator(const OwnedAllocator< U, Element > &other) noexcept
std::false_type propagate_on_container_copy_assignment
OwnedDeque(OwnedDeque &&other)
OwnedDeque & operator=(const OwnedDeque &other)
OwnedDeque(std::size_t count)
OwnedDeque(std::size_t count, const T &value)
void observe_owned_storage(OwnedStorage &out) const noexcept
OwnedDeque & operator=(OwnedDeque &&other)
OwnedDeque(const OwnedDeque &other)
void logical(std::uint64_t bytes) noexcept
bool visit(const T &object) noexcept
void backing(std::uint64_t bytes) noexcept
void incomplete(OwnedStorageUnsupported reason) noexcept
OwnedStorageUsage usage() const noexcept
void metadata(std::uint64_t bytes) noexcept
void workspace(std::uint64_t bytes) noexcept
std::uint64_t product(std::uint64_t count, std::uint64_t width) noexcept
void payload(std::uint64_t bytes) noexcept
bool claim(const void *identity) noexcept
OwnedUnorderedMap & operator=(OwnedUnorderedMap &&other)
OwnedUnorderedMap & operator=(const OwnedUnorderedMap &other)
void observe_owned_storage(OwnedStorage &out) const noexcept
OwnedUnorderedMap(OwnedUnorderedMap &&other)
OwnedUnorderedMap(const OwnedUnorderedMap &other)
void observe_owned_storage(OwnedStorage &out, const T &value) noexcept
void observe_owned_members(OwnedStorage &out, const T &... values) noexcept
OwnedAllocator< U, Element > other
static void observe(OwnedStorage &out, const std::array< T, N > &value) noexcept
static void observe(OwnedStorage &out, const std::deque< T, A > &value) noexcept
static void observe(OwnedStorage &out, const std::optional< T > &value) noexcept
static void observe(OwnedStorage &out, const std::pair< A, B > &value) noexcept
static void observe(OwnedStorage &out, const std::string &value) noexcept
static void observe(OwnedStorage &out, const std::tuple< T... > &value) noexcept
static void observe(OwnedStorage &out, const std::unordered_map< K, V, H, E, A > &value) noexcept
static void observe(OwnedStorage &out, const std::vector< T, A > &value) noexcept
static void observe(OwnedStorage &out, const T &) noexcept
std::uint64_t storage_bytes() const noexcept
static std::uint64_t sum(std::uint64_t a, std::uint64_t b) noexcept
std::uint64_t resident_bytes() const noexcept
bool complete() const noexcept
std::vector< OwnedAllocation > allocations