-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlimits.hxx
More file actions
333 lines (289 loc) · 10.8 KB
/
limits.hxx
File metadata and controls
333 lines (289 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#pragma once
/**
* @file limits.hxx
* @brief Cooperative time limits and memory limits.
* @version 3.0.1
*
* @details
* Two self-contained limiter classes — `TimeLimiter` and `MemoryLimiter` —
* each with an independent background polling thread.
*
* **Global limits** are a special case: convenience free functions
* (`set_time_limit`, `set_memory_limit`, …) operate on process-wide singleton
* instances whose callbacks write `global_limits::time_flag` /
* `global_limits::memory_flag`. Poll those flags cheaply with
* `global_limits::time_reached()` / `global_limits::memory_reached()` from anywhere.
*
* **Local limits** are plain stack objects. Give them a callback if you want
* a notification, or just poll `expired()` / `exceeded()`.
*
* @author Matteo Zanella <matteozanella2@gmail.com>
* Copyright 2026 Matteo Zanella
*
* Repository: https://github.com/Zanzibarr/cpp_utils
*
* SPDX-License-Identifier: MIT
*/
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <functional>
#include <mutex>
#include <thread>
#ifdef __APPLE__
#include <mach/mach.h>
#else
#include <unistd.h>
#include <fstream>
#endif
// ── TimeLimiter ───────────────────────────────────────────────────────────────
/**
* Cooperative time limiter backed by a single background thread.
*
* `expired()` is sticky: once true it never reverts, even if `cancel()` is
* called afterwards.
*
* Usage — local:
* @code
* TimeLimiter lim;
* lim.set(3s, []{ std::cerr << "time's up\n"; });
* while (!lim.expired()) { ... }
* @endcode
*
* Usage — global (via free functions):
* @code
* set_time_limit(5);
* while (!LIMITS_CHECK_STOP()) { ... }
* cancel_time_limit();
* @endcode
*/
class [[nodiscard]] TimeLimiter {
public:
using Clock = std::chrono::steady_clock;
using Callback = std::function<void()>;
TimeLimiter() = default;
~TimeLimiter() { cancel(); }
TimeLimiter(const TimeLimiter&) = delete;
auto operator=(const TimeLimiter&) -> TimeLimiter& = delete;
TimeLimiter(TimeLimiter&&) = delete;
auto operator=(TimeLimiter&&) -> TimeLimiter& = delete;
/**
* Arms the timer. Cancels any previously running timer first.
*
* @param duration How long until expiry.
* @param on_expire Called from the background thread on expiry (optional).
*/
void set(std::chrono::seconds duration, Callback on_expire = nullptr) {
cancel();
on_expire_ = std::move(on_expire);
thread_ = std::jthread{[this, deadline = Clock::now() + duration](std::stop_token stop) {
std::unique_lock lock{cv_mutex_};
cv_.wait_until(lock, stop, deadline, [] { return false; });
if (stop.stop_requested()) {
return;
}
expired_.store(true, std::memory_order_release);
if (on_expire_) {
on_expire_();
}
}};
}
/**
* Disarms the timer. Blocks until the background thread exits.
* Does NOT reset `expired_` — if the timer fired naturally that state
* is preserved and `expired()` will keep returning true.
*/
void cancel() {
if (thread_.joinable()) {
thread_.request_stop();
thread_.join();
}
}
/**
* Returns true if this limiter has expired. Sticky: never reverts to
* false after returning true.
*/
[[nodiscard]] auto expired() const noexcept -> bool { return expired_.load(std::memory_order_acquire); }
private:
std::atomic<bool> expired_{false};
std::jthread thread_;
Callback on_expire_;
std::condition_variable_any cv_;
std::mutex cv_mutex_;
};
// ── Memory helpers ────────────────────────────────────────────────────────────
namespace memlim {
constexpr std::size_t BYTES_PER_MB = 1024ULL * 1024ULL;
/**
* Returns the current RSS / physical footprint in bytes, or 0 on failure.
* Callers must treat 0 as "unknown" and skip threshold comparisons.
*/
[[nodiscard]] inline auto current_memory_bytes() noexcept -> std::size_t {
#ifdef __APPLE__
task_vm_info_data_t info{};
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast<task_info_t>(&info), &count) == KERN_SUCCESS) {
return static_cast<std::size_t>(info.phys_footprint);
}
return 0;
#else
std::ifstream f{"/proc/self/statm"};
long resident{};
if (long total{}; f >> total >> resident) {
return static_cast<std::size_t>(resident) * static_cast<std::size_t>(sysconf(_SC_PAGESIZE));
}
return 0;
#endif
}
/**
* Returns the current RSS in bytes, or -1 on failure.
*/
[[nodiscard]] inline auto current_memory_usage() noexcept -> std::ptrdiff_t {
const auto bytes = current_memory_bytes();
return bytes > 0 ? static_cast<std::ptrdiff_t>(bytes) : -1;
}
} // namespace memlim
// ── MemoryLimiter ─────────────────────────────────────────────────────────────
/**
* Cooperative RSS memory limiter backed by a single background thread.
*
* `exceeded()` is sticky: once true it never reverts.
*
* **Note on multi-threaded use:** the underlying measurement is process-wide
* RSS. There is no per-thread memory accounting at the OS level, so multiple
* concurrent `MemoryLimiter` instances all observe the same number.
*
* Usage — local:
* @code
* MemoryLimiter mlim;
* mlim.set(256, []{ std::cerr << "memory exceeded\n"; });
* while (!mlim.exceeded()) { ... }
* @endcode
*
* Usage — global (via free functions):
* @code
* set_memory_limit(512);
* while (!LIMITS_CHECK_MEMORY()) { ... }
* cancel_memory_limit();
* @endcode
*/
class [[nodiscard]] MemoryLimiter {
public:
using Callback = std::function<void()>;
static constexpr auto POLL_INTERVAL = std::chrono::milliseconds{100};
MemoryLimiter() = default;
~MemoryLimiter() { cancel(); }
MemoryLimiter(const MemoryLimiter&) = delete;
auto operator=(const MemoryLimiter&) -> MemoryLimiter& = delete;
MemoryLimiter(MemoryLimiter&&) = delete;
auto operator=(MemoryLimiter&&) -> MemoryLimiter& = delete;
/**
* Arms the monitor. Cancels any previously running monitor first.
*
* @param limit_mb Threshold in megabytes. Must be > 0.
* @param on_exceed Called from the polling thread on first breach (optional).
*/
void set(std::size_t limit_mb, Callback on_exceed = nullptr) {
cancel();
on_exceed_ = std::move(on_exceed);
const std::size_t limit_bytes = limit_mb * memlim::BYTES_PER_MB;
thread_ = std::jthread{[this, limit_bytes](std::stop_token stop) {
while (!stop.stop_requested()) {
{
std::unique_lock lock{cv_mutex_};
cv_.wait_for(lock, stop, POLL_INTERVAL, [] { return false; });
}
if (stop.stop_requested()) {
return;
}
const auto bytes = memlim::current_memory_bytes();
if (bytes == 0) {
continue; // sampling failure — skip
}
if (bytes >= limit_bytes) {
exceeded_.store(true, std::memory_order_release);
if (on_exceed_) {
on_exceed_();
}
return;
}
}
}};
}
/**
* Disarms the monitor. Blocks until the polling thread exits.
* Does NOT reset `exceeded_` — naturally-fired state is preserved.
*/
void cancel() {
if (thread_.joinable()) {
thread_.request_stop();
thread_.join();
}
}
/**
* Returns true if the RSS threshold has been breached. Sticky: never
* reverts to false after returning true.
*/
[[nodiscard]] auto exceeded() const noexcept -> bool { return exceeded_.load(std::memory_order_acquire); }
private:
std::atomic<bool> exceeded_{false};
std::jthread thread_;
Callback on_exceed_;
std::condition_variable_any cv_;
std::mutex cv_mutex_;
};
// ── Global singletons and free functions ──────────────────────────────────────
namespace global_limits {
/// Global flag atomics
inline std::atomic<bool> time_flag{false};
inline std::atomic<bool> memory_flag{false};
/// The global TimeLimiter: its callback writes global_limits::time_flag.
inline TimeLimiter global_time_limiter;
/// The global MemoryLimiter: its callback writes global_limits::memory_flag.
inline MemoryLimiter global_memory_limiter;
/// Cheap relaxed read of the global time flag. Use inside tight loops.
inline auto time_reached() { return global_limits::time_flag.load(std::memory_order_relaxed); }
/// Cheap relaxed read of the global memory flag. Use inside tight loops.
inline auto memory_reached() { return global_limits::memory_flag.load(std::memory_order_relaxed); }
} // namespace global_limits
/**
* Arms the process-wide time limit.
* On expiry, sets `global_limits::time_flag` (readable via `global_limits::time_reached()`).
*
* @param seconds Limit duration.
* @param on_expire Extra callback on expiry alongside the flag write (optional).
*/
inline void set_time_limit(unsigned int seconds, TimeLimiter::Callback on_expire = nullptr) {
global_limits::global_time_limiter.set(std::chrono::seconds{seconds}, [cb = std::move(on_expire)] {
global_limits::time_flag.store(true, std::memory_order_release);
if (cb) {
cb();
}
});
}
/**
* Disarms the process-wide time limit.
* Does NOT reset `global_limits::time_flag` if it already fired.
*/
inline void cancel_time_limit() { global_limits::global_time_limiter.cancel(); }
/**
* Arms the process-wide memory limit.
* On breach, sets `global_limits::memory_flag` (readable via `global_limits::memory_reached()`).
*
* @param limit_mb Threshold in megabytes.
* @param on_exceed Extra callback on breach alongside the flag write (optional).
*/
inline void set_memory_limit(std::size_t limit_mb, MemoryLimiter::Callback on_exceed = nullptr) {
global_limits::global_memory_limiter.set(limit_mb, [cb = std::move(on_exceed)] {
global_limits::memory_flag.store(true, std::memory_order_release);
if (cb) {
cb();
}
});
}
/**
* Disarms the process-wide memory limit.
* Does NOT reset `global_limits::memory_flag` if it already fired.
*/
inline void cancel_memory_limit() { global_limits::global_memory_limiter.cancel(); }