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