libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
buffer.hpp
1/*
2 * This file is part of libtcspc
3 * Copyright 2019-2026 Board of Regents of the University of Wisconsin System
4 * SPDX-License-Identifier: MIT
5 */
6
7#pragma once
8
9#include "arg_wrappers.hpp"
10#include "common.hpp"
11#include "context.hpp"
12#include "errors.hpp"
13#include "introspect.hpp"
14#include "processor.hpp"
15#include "vector_queue.hpp"
16
17#include <algorithm>
18#include <chrono>
19#include <concepts>
20#include <condition_variable>
21#include <cstddef>
22#include <functional>
23#include <mutex>
24#include <stdexcept>
25#include <type_traits>
26#include <utility>
27
28namespace tcspc {
29
30namespace internal {
31
32// Avoid std::hardware_destructive_interference_size, because it suffers from
33// ABI compatibility requirements and therefore may not have the best value
34// (for example, it seems to be 256 on Linux/aarch64). Instead, just default to
35// 64 (correct for most x86_64 and many ARM and RISC-V processors) except in
36// known cases where a larger value is appropriate.
37#if (defined(__APPLE__) && defined(__arm64__)) || \
38 (defined(__powerpc64__) || defined(__ppc64__) || defined(__PPC64__))
39inline constexpr std::size_t destructive_interference_size = 128;
40#else
41inline constexpr std::size_t destructive_interference_size = 64;
42#endif
43
44} // namespace internal
45
52class buffer_accessor {
53 std::function<void()> halt_fn;
54 std::function<void()> pump_fn;
55
56 public:
58 template <typename HaltFunc, typename PumpFunc>
59 explicit buffer_accessor(HaltFunc halt_func, PumpFunc pump_func)
60 : halt_fn(halt_func), pump_fn(pump_func) {}
61
84 void halt() noexcept { halt_fn(); } // NOLINT(bugprone-exception-escape)
85
109 void pump() { pump_fn(); }
110};
111
112namespace internal {
113
114template <typename Event, bool LatencyLimited, typename Downstream>
115 requires processor<Downstream, Event>
116class buffer {
117 static_assert(std::move_constructible<Event>,
118 "buffer requires Event to be move-constructible (events are "
119 "stored in an internal queue)");
120
121 using clock_type = std::chrono::steady_clock;
122 using queue_type = vector_queue<Event>;
123
124 std::size_t threshold;
125 clock_type::duration max_latency;
126
127 std::mutex mutex;
128 std::condition_variable has_data_condition;
129 queue_type shared_queue;
130 clock_type::time_point oldest_enqueued_time;
131 bool upstream_flushed = false;
132 bool upstream_halted = false;
133 bool downstream_threw = false;
134
135#ifdef _MSC_VER
136#pragma warning(push)
137#pragma warning(disable : 4324) // Structure padded due to alignment specifier
138#endif
139
140 // To reduce lock contention on the shared_queue, we use a second queue
141 // that is accessed only by the emitting thread and is not protected by the
142 // mutex. Events in the shared_queue are transferred in bulk to the
143 // emit_queue while the mutex is held.
144 // This means that the mutex does not need to be acquired between every
145 // event emitted, so the producer will be less likely to block when the
146 // data rate is momentarily high, and the consumer will be less likely to
147 // block while catching up on buffered events.
148 // Furthermore, we ensure that the emit_queue and downstream do not share a
149 // CPU cache line with the shared_queue, to prevent false sharing.
150 alignas(destructive_interference_size) queue_type emit_queue;
151
152#ifdef _MSC_VER
153#pragma warning(pop)
154#endif
155
156 Downstream downstream;
157
158 // Cold data after downstream.
159 bool pumped = false;
160 access_tracker<buffer_accessor> trk;
161
162 void halt() noexcept {
163 {
164 auto const lock = std::lock_guard(mutex);
165 upstream_halted = true;
166 }
167 has_data_condition.notify_one();
168 }
169
170 void pump() {
171 try {
172 auto lock = std::unique_lock(mutex);
173 if (pumped) {
174 throw std::logic_error(
175 "buffer may not be pumped a second time");
176 }
177 pumped = true;
178
179 for (;;) {
180 if constexpr (LatencyLimited) {
181 has_data_condition.wait(lock, [&] {
182 return not shared_queue.empty() || upstream_flushed ||
183 upstream_halted;
184 });
185 // Won't overflow due to 24 h limit on max_latency:
186 auto const deadline = oldest_enqueued_time + max_latency;
187 has_data_condition.wait_until(lock, deadline, [&] {
188 return shared_queue.size() >= threshold ||
189 upstream_flushed || upstream_halted;
190 });
191 } else {
192 has_data_condition.wait(lock, [&] {
193 return shared_queue.size() >= threshold ||
194 upstream_flushed || upstream_halted;
195 });
196 }
197
198 if (not upstream_flushed && upstream_halted)
199 throw source_halted();
200 if (shared_queue.empty() && upstream_flushed) {
201 lock.unlock();
202 return downstream.flush();
203 }
204
205 emit_queue.swap(shared_queue);
206 lock.unlock();
207 while (!emit_queue.empty()) {
208 downstream.handle(std::move(emit_queue.front()));
209 emit_queue.pop();
210 }
211 lock.lock();
212 }
213 } catch (source_halted const &) {
214 throw;
215 } catch (...) {
216 auto const lock = std::lock_guard(mutex);
217 downstream_threw = true;
218 throw;
219 }
220 }
221
222 public:
223 template <typename Rep, typename Period>
224 explicit buffer(arg::threshold<std::size_t> threshold,
225 std::chrono::duration<Rep, Period> latency_limit,
226 access_tracker<buffer_accessor> &&tracker,
227 Downstream downstream)
228 : threshold(threshold.value >= 0 ? threshold.value : 1),
229 max_latency(
230 std::chrono::duration_cast<clock_type::duration>(latency_limit)),
231 downstream(std::move(downstream)), trk(std::move(tracker)) {
232 // Limit to avoid integer overflow.
233 if (max_latency > std::chrono::hours(24)) {
234 throw std::invalid_argument(
235 "buffer latency limit must not be greater than 24 h");
236 }
237
238 trk.register_accessor_factory([](auto &tracker) {
239 auto *self = LIBTCSPC_OBJECT_FROM_TRACKER(buffer, trk, tracker);
240 return buffer_accessor([self] { self->halt(); },
241 [self] { self->pump(); });
242 });
243 }
244
245 // NOLINTBEGIN(cppcoreguidelines-pro-type-member-init)
246 explicit buffer(arg::threshold<std::size_t> threshold,
247 access_tracker<buffer_accessor> &&tracker,
248 Downstream downstream)
249 : buffer(threshold, std::chrono::hours(24), std::move(tracker),
250 std::move(downstream)) {}
251 // NOLINTEND(cppcoreguidelines-pro-type-member-init)
252
253 // Custom move ctor because we have a mutex. Move only works when not
254 // running.
255 ~buffer() = default;
256
257 buffer(buffer const &) = delete;
258 auto operator=(buffer const &) = delete;
259
260 buffer(buffer &&other) noexcept
261 : threshold(other.threshold), max_latency(other.max_latency),
262 shared_queue(std::move(other.shared_queue)),
263 oldest_enqueued_time(other.oldest_enqueued_time),
264 upstream_flushed(other.upstream_flushed),
265 upstream_halted(other.upstream_halted),
266 downstream_threw(other.downstream_threw),
267 emit_queue(std::move(other.emit_queue)),
268 downstream(std::move(other.downstream)), pumped(other.pumped),
269 trk(std::move(other.trk)) {}
270
271 auto operator=(buffer &&) = delete;
272
273 [[nodiscard]] auto introspect_node() const -> processor_info {
274 return processor_info(this, "buffer");
275 }
276
277 [[nodiscard]] auto introspect_graph() const -> processor_graph {
278 return downstream.introspect_graph().push_entry_point(this);
279 }
280
281 template <typename E>
282 requires std::convertible_to<std::remove_cvref_t<E>, Event>
283 void handle(E &&event) {
284 bool should_notify{};
285 {
286 auto const lock = std::lock_guard(mutex);
287 if (downstream_threw)
288 throw end_of_processing(
289 "ending upstream of buffer upon end of downstream processing");
290
291 shared_queue.push(std::forward<E>(event));
292 should_notify = shared_queue.size() == threshold;
293 if constexpr (LatencyLimited) {
294 if (shared_queue.size() == 1) {
295 oldest_enqueued_time = clock_type::now();
296 should_notify = true; // Wake up once to set deadline.
297 }
298 }
299 }
300 if (should_notify)
301 has_data_condition.notify_one();
302 }
303
304 void flush() {
305 {
306 auto const lock = std::lock_guard(mutex);
307 if (downstream_threw)
308 throw end_of_processing(
309 "ending upstream of buffer upon end of downstream processing");
310 upstream_flushed = true;
311 }
312 has_data_condition.notify_one();
313 }
314};
315
316} // namespace internal
317
365template <typename Event, typename Downstream>
367 access_tracker<buffer_accessor> &&tracker, Downstream downstream) {
368 return internal::buffer<Event, false, Downstream>(
369 threshold, std::move(tracker), std::move(downstream));
370}
371
428template <typename Event, typename Rep, typename Period, typename Downstream>
430 std::chrono::duration<Rep, Period> latency_limit,
432 Downstream downstream) {
433 return internal::buffer<Event, true, Downstream>(
434 threshold, latency_limit, std::move(tracker), std::move(downstream));
435}
436
437} // namespace tcspc
Tracker that mediates access to objects via a tcspc::context.
Definition context.hpp:39
void pump()
Pump buffered events downstream.
Definition buffer.hpp:109
void halt() noexcept
Halt pumping of the buffer.
Definition buffer.hpp:84
#define LIBTCSPC_OBJECT_FROM_TRACKER(obj_type, tracker_field_name, tracker)
Recover the object address from a tcspc::access_tracker embedded in the object.
Definition context.hpp:255
auto buffer(arg::threshold< std::size_t > threshold, access_tracker< buffer_accessor > &&tracker, Downstream downstream)
Create a processor that buffers events and emits them on a different thread.
Definition buffer.hpp:366
auto real_time_buffer(arg::threshold< std::size_t > threshold, std::chrono::duration< Rep, Period > latency_limit, access_tracker< buffer_accessor > &&tracker, Downstream downstream)
Create a processor that buffers events and emits them on a different thread, with limited latency.
Definition buffer.hpp:429
libtcspc namespace.
Definition acquire.hpp:30
Function argument wrapper for threshold parameter.
Definition arg_wrappers.hpp:397