libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
acquire.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 "bucket.hpp"
11#include "context.hpp"
12#include "core.hpp"
13#include "errors.hpp"
14#include "introspect.hpp"
15#include "processor.hpp"
16
17#include <chrono>
18#include <concepts>
19#include <condition_variable>
20#include <cstddef>
21#include <exception>
22#include <functional>
23#include <memory>
24#include <mutex>
25#include <optional>
26#include <span>
27#include <stdexcept>
28#include <utility>
29
30namespace tcspc {
31
41template <typename R, typename T>
43 std::move_constructible<R> && requires(R &r, std::span<T> buffer) {
44 { r(buffer) } -> std::same_as<std::optional<std::size_t>>;
45 };
46
52class acquire_accessor {
53 std::function<void()> halt_fn;
54
55 public:
57 template <typename Func>
58 explicit acquire_accessor(Func halt_func) : halt_fn(halt_func) {}
59
75 void halt() { halt_fn(); }
76};
77
78namespace internal {
79
80// The max sleep duration when a read doesn't fill a batch is chosen to be
81// short enough that (1) its effect is unnoticeable in a live display of the
82// data and (2) hardware buffers are unlikely to fill up if they started out
83// empty, given the buffer capacity and maximum count rates of typical devices.
84constexpr auto slow_acq_sleep = std::chrono::milliseconds(10);
85
86template <typename T, typename Reader, typename Downstream>
87 requires acquisition_reader<Reader, T> && processor<Downstream, bucket<T>>
88class acquire {
89 Reader reader;
90 std::shared_ptr<bucket_source<T>> bsource;
91 std::size_t bsize;
92
93 std::mutex halt_mutex;
94 std::condition_variable halt_cv;
95 bool halted = false;
96
97 Downstream downstream;
98
99 // Cold data after downstream.
100 access_tracker<acquire_accessor> trk;
101
102 void halt() {
103 {
104 auto const lock = std::lock_guard(halt_mutex);
105 halted = true;
106 }
107 halt_cv.notify_one();
108 }
109
110 public:
111 explicit acquire(Reader reader,
112 std::shared_ptr<bucket_source<T>> buffer_provider,
113 arg::batch_size<std::size_t> batch_size,
114 access_tracker<acquire_accessor> tracker,
115 Downstream downstream)
116 : reader(std::move(reader)), bsource(std::move(buffer_provider)),
117 bsize(batch_size.value), downstream(std::move(downstream)),
118 trk(std::move(tracker)) {
119 if (not bsource)
120 throw std::invalid_argument(
121 "acquire buffer_provider must not be null");
122 if (bsize == 0)
123 throw std::invalid_argument("acquire batch size must be positive");
124
125 trk.register_accessor_factory([](auto &tracker) {
126 auto *self = LIBTCSPC_OBJECT_FROM_TRACKER(acquire, trk, tracker);
127 return acquire_accessor([self] { self->halt(); });
128 });
129 }
130
131 // Custom move ctor because we have a mutex. Move only works when not
132 // running.
133 ~acquire() = default;
134
135 acquire(acquire const &) = delete;
136 auto operator=(acquire const &) = delete;
137
138 acquire(acquire &&other) noexcept
139 : reader(std::move(other.reader)), bsource(std::move(other.bsource)),
140 bsize(other.bsize), halted(other.halted),
141 downstream(std::move(other.downstream)), trk(std::move(other.trk)) {}
142
143 auto operator=(acquire &&) = delete;
144
145 [[nodiscard]] auto introspect_node() const -> processor_info {
146 return processor_info(this, "acquire");
147 }
148
149 [[nodiscard]] auto introspect_graph() const -> processor_graph {
150 return downstream.introspect_graph().push_entry_point(this);
151 }
152
153 void flush() {
154 bucket<T> b;
155 bool reached_end = false;
156 {
157 auto lock = std::unique_lock(halt_mutex);
158 while (not halted) {
159 lock.unlock();
160 auto const start_time = std::chrono::steady_clock::now();
161 if (b.empty())
162 b = bsource->bucket_of_size(bsize);
163 std::optional<std::size_t> const read = reader(std::span(b));
164 if (not read) {
165 reached_end = true;
166 break;
167 }
168 if (*read > 0) {
169 b.shrink(0, *read);
170 downstream.handle(std::move(b));
171 b = {};
172 }
173 lock.lock();
174 if (*read < bsize) { // Not enough data to fill the batch.
175 halt_cv.wait_until(lock, start_time + slow_acq_sleep,
176 [&] { return halted; });
177 }
178 }
179 }
180 if (reached_end)
181 downstream.flush();
182 else
183 throw acquisition_halted();
184 }
185};
186
187template <typename T, typename Reader, typename LiveDownstream,
188 typename BatchDownstream>
189 requires acquisition_reader<Reader, T> &&
190 processor<LiveDownstream, bucket<T const>> &&
191 processor<BatchDownstream, bucket<T>>
192class acquire_full_buckets {
193 Reader reader;
194 std::shared_ptr<bucket_source<T>> bsource;
195 std::size_t bsize;
196
197 std::mutex halt_mutex;
198 std::condition_variable halt_cv;
199 bool halted = false;
200
201 LiveDownstream live_downstream;
202 BatchDownstream batch_downstream;
203
204 // Cold data after downstream.
205 access_tracker<acquire_accessor> trk;
206
207 void halt() {
208 {
209 auto const lock = std::lock_guard(halt_mutex);
210 halted = true;
211 }
212 halt_cv.notify_one();
213 }
214
215 // Mutates 'b' only when throwing.
216 void emit_live(bucket<T> &b, std::size_t start, std::size_t count) {
217 if (count > 0) {
218 try {
219 auto v = bsource->shared_view_of(b);
220 v.shrink(start, count);
221 live_downstream.handle(std::move(v));
222 } catch (end_of_processing const &) {
223 b.shrink(0, start + count);
224 batch_downstream.handle(std::move(b));
225 batch_downstream.flush();
226 throw;
227 }
228 }
229 }
230
231 void emit_batch(bucket<T> &&b) {
232 try {
233 batch_downstream.handle(std::move(b));
234 } catch (end_of_processing const &) {
235 live_downstream.flush();
236 throw;
237 }
238 }
239
240 void flush_downstreams(bucket<T> &&b, std::size_t filled) {
241 std::exception_ptr end;
242 try {
243 live_downstream.flush();
244 } catch (end_of_processing const &) {
245 end = std::current_exception();
246 }
247 if (not b.empty() && filled > 0) {
248 b.shrink(0, filled);
249 batch_downstream.handle(std::move(b));
250 }
251 batch_downstream.flush();
252 if (end)
253 std::rethrow_exception(end);
254 }
255
256 public:
257 explicit acquire_full_buckets(
258 Reader reader, std::shared_ptr<bucket_source<T>> buffer_provider,
259 arg::batch_size<std::size_t> batch_size,
260 access_tracker<acquire_accessor> tracker,
261 LiveDownstream live_downstream, BatchDownstream batch_downstream)
262 : reader(std::move(reader)), bsource(std::move(buffer_provider)),
263 bsize(batch_size.value), live_downstream(std::move(live_downstream)),
264 batch_downstream(std::move(batch_downstream)),
265 trk(std::move(tracker)) {
266 if (not bsource)
267 throw std::invalid_argument(
268 "acquire_full_buckets buffer_provider must not be null");
269 if constexpr (not std::is_same_v<LiveDownstream, internal::sink_all>) {
270 if (not bsource->supports_shared_views())
271 throw std::invalid_argument(
272 "acquire_full_buckets buffer_provider must support shared views");
273 }
274 if (bsize == 0)
275 throw std::invalid_argument(
276 "acquire_full_buckets batch size must be positive");
277
278 trk.register_accessor_factory([](auto &tracker) {
279 auto *self = LIBTCSPC_OBJECT_FROM_TRACKER(acquire_full_buckets,
280 trk, tracker);
281 return acquire_accessor([self] { self->halt(); });
282 });
283 }
284
285 // Custom move ctor because we have a mutex. Move only works when not
286 // running.
287 ~acquire_full_buckets() = default;
288
289 acquire_full_buckets(acquire_full_buckets const &) = delete;
290 auto operator=(acquire_full_buckets const &) = delete;
291
292 acquire_full_buckets(acquire_full_buckets &&other) noexcept
293 : reader(std::move(other.reader)), bsource(std::move(other.bsource)),
294 bsize(other.bsize), halted(other.halted),
295 live_downstream(std::move(other.live_downstream)),
296 batch_downstream(std::move(other.batch_downstream)),
297 trk(std::move(other.trk)) {}
298
299 auto operator=(acquire_full_buckets &&) = delete;
300
301 [[nodiscard]] auto introspect_node() const -> processor_info {
302 return processor_info(this, "acquire_full_buckets");
303 }
304
305 [[nodiscard]] auto introspect_graph() const -> processor_graph {
307 live_downstream.introspect_graph().push_entry_point(this),
308 batch_downstream.introspect_graph().push_entry_point(this));
309 }
310
311 void flush() {
312 bucket<T> b;
313 std::size_t filled = 0;
314 {
315 auto lock = std::unique_lock(halt_mutex);
316 while (not halted) {
317 lock.unlock();
318 auto const start_time = std::chrono::steady_clock::now();
319 if (b.empty()) {
320 b = bsource->bucket_of_size(bsize);
321 filled = 0;
322 }
323 auto const unfilled = std::span(b).subspan(filled);
324 std::optional<std::size_t> const read = reader(unfilled);
325 if (not read)
326 return flush_downstreams(std::move(b), filled);
327 if constexpr (not std::is_same_v<LiveDownstream,
328 internal::sink_all>)
329 emit_live(b, filled, *read);
330 filled += *read;
331 if (filled == bsize) {
332 emit_batch(std::move(b));
333 b = {};
334 }
335 lock.lock();
336 if (filled < bsize) {
337 halt_cv.wait_until(lock, start_time + slow_acq_sleep,
338 [&] { return halted; });
339 }
340 }
341 }
342 throw acquisition_halted();
343 }
344};
345
346} // namespace internal
347
386template <typename T, typename Reader, typename Downstream>
387auto acquire(Reader reader, std::shared_ptr<bucket_source<T>> buffer_provider,
389 access_tracker<acquire_accessor> tracker, Downstream downstream) {
390 return internal::acquire<T, Reader, Downstream>(
391 std::move(reader), std::move(buffer_provider), batch_size,
392 std::move(tracker), std::move(downstream));
393}
394
451template <typename T, typename Reader, typename LiveDownstream,
452 typename BatchDownstream>
453auto acquire_full_buckets(Reader reader,
454 std::shared_ptr<bucket_source<T>> buffer_provider,
457 LiveDownstream live_downstream,
458 BatchDownstream batch_downstream) {
459 return internal::acquire_full_buckets<T, Reader, LiveDownstream,
460 BatchDownstream>(
461 std::move(reader), std::move(buffer_provider), batch_size,
462 std::move(tracker), std::move(live_downstream),
463 std::move(batch_downstream));
464}
465
471template <typename T> struct null_reader {
473 auto operator()(std::span<T> /* buffer */) -> std::optional<std::size_t> {
474 return std::nullopt;
475 }
476};
477
483template <typename T> struct stuck_reader {
485 auto operator()(std::span<T> /* buffer */) -> std::optional<std::size_t> {
486 return 0;
487 }
488};
489
490} // namespace tcspc
Tracker that mediates access to objects via a tcspc::context.
Definition context.hpp:39
void halt()
Halt the acquisition: stop reading further data.
Definition acquire.hpp:75
Concept that is satisfied when R conforms to the libtcspc acquisition reader interface for element ty...
Definition acquire.hpp:42
#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 merge_processor_graphs(processor_graph const &a, processor_graph const &b) -> processor_graph
Create a new processor graph by merging two existing ones.
Definition introspect.hpp:380
auto acquire(Reader reader, std::shared_ptr< bucket_source< T > > buffer_provider, arg::batch_size< std::size_t > batch_size, access_tracker< acquire_accessor > tracker, Downstream downstream)
Create a processor that acquires data into buckets.
Definition acquire.hpp:387
auto acquire_full_buckets(Reader reader, std::shared_ptr< bucket_source< T > > buffer_provider, arg::batch_size< std::size_t > batch_size, access_tracker< acquire_accessor > tracker, LiveDownstream live_downstream, BatchDownstream batch_downstream)
Create a processor that acquires data into buckets, ensuring that each bucket is filled to a fixed si...
Definition acquire.hpp:453
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 count(access_tracker< count_accessor > &&tracker, Downstream downstream)
Create a processor that counts events of a given type.
Definition count.hpp:312
libtcspc namespace.
Definition acquire.hpp:30
Function argument wrapper for batch size parameter.
Definition arg_wrappers.hpp:47
Abstract base class for polymorphic bucket sources.
Definition bucket.hpp:504
Acquisition reader that reads an empty stream.
Definition acquire.hpp:471
auto operator()(std::span< T >) -> std::optional< std::size_t >
Implements the acquisition reader requirement.
Definition acquire.hpp:473
Acquisition reader that waits indefinitely without producing data.
Definition acquire.hpp:483
auto operator()(std::span< T >) -> std::optional< std::size_t >
Implements the acquisition reader requirement.
Definition acquire.hpp:485