PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
scratch_arena.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <cstddef>
6#include <cstdint>
7#include <memory>
8#include <optional>
9#include <system_error>
10#include <vector>
11
12namespace pineforge::detail {
13
14// Also used by the legacy ScratchFile backend. Keep errno and the typed run
15// failure together, so callers keep their existing std::system_error catch.
16class ReportScratchIoError final : public std::system_error, public RunFailureInfo {
17public:
18 ReportScratchIoError(int error, const char* operation,
19 const RunFailureArgs& args = {{"reason", "scratch_io"}})
20 : std::system_error(error, std::generic_category(), operation),
22};
23
24// Re-emit the original typed object, retaining errno, English and the validated
25// argument object. Status-only backings pass nullptr and keep their old raiser.
27 if (failure) throw *failure;
28}
29
30// Closed report_sink_failed.store vocabulary; never derive it from a path
31// or an open() prefix. The port owners label each physical ScratchFile.
47
48enum class ScratchArenaBackend : std::uint8_t {
51};
52
54 std::uint64_t bytes_total = 0; // Complete, allocatable chunks.
55 std::uint64_t bytes_used = 0; // Claimed chunks, including slack.
56 std::uint64_t chunks_total = 0;
57 std::uint64_t chunks_free = 0;
58 std::uint64_t windows_mapped_bytes = 0; // Current mappings, not dirty bytes.
59 std::uint64_t high_water = 0; // Peak bytes_used since attach.
61 std::uint64_t bytes_supplied = 0; // Exact size given, including tail.
62};
63
64class ScratchFile;
65
66// Explicit, process-wide scratch backing; ScratchFile::open never attaches.
67// Attach/detach belong to single-threaded startup/teardown. Once attached,
68// independent single-thread-owned ScratchFiles share a mutex-protected
69// allocator and LRU. Close all arena ScratchFiles before detach.
70//
71// The wrapper owns sizing: this class never grows, shrinks, writes with
72// write/pwrite, or changes seals on the backing fd. It owns a CLOEXEC dup;
73// the caller keeps ownership of the supplied fd. Both backends require
74// S_ISREG and exactly the supplied size. On Linux, F_GET_SEALS returning
75// exactly F_SEAL_SEAL (sealing disabled) or EINVAL selects regular_file.
76// A mask containing F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL selects
77// sealed_memfd; F_SEAL_EXEC and other non-write seals may be added (e.g.
78// MFD_NOEXEC_SEAL). A mask containing F_SEAL_WRITE or F_SEAL_FUTURE_WRITE
79// is refused with report_sink_failed/arena_backing in every profile.
80// Masks missing a required seal, except the regular-file cases, are refused.
81// PF_SCRATCH_ARENA_JAILED=1 additionally requires sealed_memfd at attachment;
82// direct attach(fd, bytes), unset or "0" may attach ordinary files.
83// strategy_require_report_scratch_arena_v1 itself requires sealed_memfd on
84// Linux regardless of PF_SCRATCH_ARENA_JAILED; off Linux it admits any
85// attached arena.
86// The wrapper preallocates regular files in a root-owned 0700 directory,
87// mode 0600, and passes O_RDWR. New mappings and short preads recheck size.
88// The one unrecoverable case is SIGBUS after a concurrent shrink invalidates
89// an already mapped window (including the fstat-to-access race). There is no
90// SIGBUS handler. With the wrapper retaining size until child exit, only the
91// child itself can cause this by shrinking its inherited regular-file fd;
92// the arena never shrinks it, and memfd SHRINK seals prevent it entirely.
93//
94// Successful attach deliberately sets process-wide SIGXFSZ to SIG_IGN, and
95// detach does not restore it: a later EFBIG must be a typed refusal instead
96// of killing the strategy child under RLIMIT_FSIZE=0.
97//
98// Capacity is checked under the allocator lock, before mapping or claiming
99// any chunk for a grow. Lowest-free-chunk reuse depends only on prior claims
100// and releases, never on LRU state, mapping addresses or hole-punch success.
101// A single-threaded operation script therefore exhausts at the same operation
102// index for a given arena size, across window caps, absent other I/O/resource
103// failures. Concurrent regions share the same capacity bound, but scheduling
104// decides which operation reaches it; no per-thread failing-index guarantee.
105// Exhaustion snapshots high_water_bytes, arena_bytes, claimed_bytes (all
106// currently owned chunks) and needed_bytes_lower_bound (the requesting
107// region's additional whole chunks, or a bounded caller's supplied arena-size
108// requirement), plus its store, under that same lock.
109// Other failure reasons carry no byte counts or store identity.
110class ScratchArena final {
111public:
112 static constexpr std::uint64_t kChunkBytes = 1024ULL * 1024ULL;
113 static constexpr std::uint64_t kDefaultWindowBytes = 32ULL * kChunkBytes;
114 static constexpr int kInheritedFd = 5;
115
116 // Same fd/bytes on an existing attachment is an idempotent no-op;
117 // replacing a live attachment is a std::logic_error.
118 // Invalid backing/configuration and I/O failures throw ReportScratchIoError.
119 static void attach(int fd, std::uint64_t bytes);
120
121 // No PF_SCRATCH_ARENA_FD => no-op (even if the size variable is present).
122 // Otherwise require FD=5 and a positive decimal PF_SCRATCH_ARENA_BYTES.
123 // PF_SCRATCH_ARENA_JAILED: exactly "1" requires sealed_memfd at attachment;
124 // unset or exactly "0" permits regular_file; all other values are refused.
125 // Write-sealed masks are refused in every profile. The attach-time sealed
126 // requirement also applies to an existing attachment. The admission export
127 // requires sealed_memfd on Linux regardless of this variable; off Linux
128 // it admits any attached arena.
129 // attach also reads PF_SCRATCH_WINDOW_BYTES: a total mapping cap, default
130 // 32 MiB, rounded down to whole pages; less than one page is refused.
132 static ScratchArena* attached() noexcept;
133 static void detach(); // std::logic_error if any arena file is still open.
134
136 ScratchArena(const ScratchArena&) = delete;
137 ScratchArena& operator=(const ScratchArena&) = delete;
139
140private:
141 friend class ScratchFile;
142 struct Region {
143 // Four bytes per claimed 1 MiB chunk, in logical-file order.
144 std::vector<std::uint32_t> chunks;
145 std::uint64_t size = 0;
146 // No value is the original unbounded report-region policy. A bounded
147 // region (including a zero-byte share) reserves its directory at open.
148 std::optional<std::uint64_t> max_bytes;
150 };
151 struct Impl;
152 ScratchArena(int fd, std::uint64_t bytes);
153 void open_region(Region& region, std::optional<std::uint64_t> max_bytes = std::nullopt);
154 void release_region(Region& region) noexcept;
155 void truncate(Region& region, std::uint64_t bytes);
156 void ensure_size(Region& region, std::uint64_t bytes,
157 std::uint64_t needed_bytes_lower_bound = 0);
158 void write(Region& region, std::uint64_t offset, const void* data, std::size_t length);
159 void read(const Region& region, std::uint64_t offset, void* data, std::size_t length) const;
160
161 std::unique_ptr<Impl> impl_;
162 static std::unique_ptr<ScratchArena> singleton_;
163};
164
165} // namespace pineforge::detail
The code carried beside a thrown exception.
RunFailureInfo(RunFailureCode code, const RunFailureArgs &args) noexcept
ReportScratchIoError(int error, const char *operation, const RunFailureArgs &args={{"reason", "scratch_io"}})
static constexpr int kInheritedFd
ScratchArena(const ScratchArena &)=delete
static void attach(int fd, std::uint64_t bytes)
static ScratchArena * attached() noexcept
static constexpr std::uint64_t kDefaultWindowBytes
ScratchArenaStats stats() const
static constexpr std::uint64_t kChunkBytes
void rethrow_carried_scratch_failure(const ReportScratchIoError *failure)
std::vector< RunFailureArg > RunFailureArgs