libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
merge.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 "errors.hpp"
12#include "introspect.hpp"
13#include "numeric_traits.hpp"
14#include "processor.hpp"
15#include "type_list.hpp"
16#include "variant_event.hpp"
17#include "vector_queue.hpp"
18
19#include <algorithm>
20#include <array>
21#include <cassert>
22#include <cstddef>
23#include <exception>
24#include <memory>
25#include <tuple>
26#include <type_traits>
27#include <utility>
28
29namespace tcspc {
30
31namespace internal {
32
33// Internal implementation of merge processor. This processor is owned by the
34// two input processors via shared_ptr.
35template <typename EventList, typename NumericTraits, typename Downstream>
37class merge_impl {
38 static_assert(type_list_like<EventList>);
39 // merge copies events into its reorder buffer (the per-input handle()
40 // receives events by const reference, so even rvalues are copied). This is
41 // a deliberate simplicity choice: merge's use cases involve small,
42 // trivial, copyable events. A move-only merge, if ever needed, should be a
43 // separate processor.
44 static_assert(
45 is_copy_constructible_list_v<EventList>,
46 "merge requires every event in EventList to be copy-constructible "
47 "(events are copied into the reordering buffer)");
48
49 // When events have equal abstime, those originating from input 0 are
50 // emitted before those originating from input1. Within the same input, the
51 // order is preserved.
52 // As long as we follow that rule and also ensure never to buffer events
53 // that can be emitted, we only ever need to buffer events from one or the
54 // other input at any given time.
55 bool pending_on_1 = false; // Pending on input 0 if false
56 std::array<bool, 2> input_flushed{false, false};
57 bool ended_with_exception = false;
58 vector_queue<variant_or_single_event<EventList>> pending;
59 std::size_t max_buffered;
60
61 Downstream downstream;
62
63 template <unsigned InputChannel>
64 [[nodiscard]] auto is_other_flushed() const noexcept -> bool {
65 return input_flushed[1 - InputChannel];
66 }
67
68 template <unsigned InputChannel>
69 [[nodiscard]] auto is_pending_on_other() const noexcept -> bool {
70 return pending_on_1 == (InputChannel == 0);
71 }
72
73 template <unsigned InputChannel> void set_pending_on() noexcept {
74 pending_on_1 = (InputChannel == 1);
75 }
76
77 // Emit pending while predicate is true.
78 // Pred: bool(abstime_type const &)
79 template <typename Pred> void emit_pending(Pred predicate) {
80 auto emit_if_true = [&](auto const &e) {
81 bool p = predicate(e.abstime);
82 if (p)
83 downstream.handle(e);
84 return p;
85 };
86 while (!pending.empty() &&
87 visit_variant_or_single_event(emit_if_true, pending.front()))
88 pending.pop();
89 }
90
91 public:
92 explicit merge_impl(arg::max_buffered<std::size_t> max_buffered,
93 Downstream downstream)
94 : max_buffered(max_buffered.value), downstream(std::move(downstream)) {
95 }
96
97 merge_impl(merge_impl const &) = delete;
98 auto operator=(merge_impl const &) = delete;
99 merge_impl(merge_impl &&) = delete;
100 auto operator=(merge_impl &&) = delete;
101 ~merge_impl() = default;
102
103 [[nodiscard]] auto introspect_node() const -> processor_info {
104 return processor_info(this, "merge_impl");
105 }
106
107 [[nodiscard]] auto introspect_graph() const -> processor_graph {
108 return downstream.introspect_graph().push_entry_point(this);
109 }
110
111 template <unsigned InputChannel, typename Event>
112 void handle(Event const &event) {
113 static_assert(convertible_to_type_list_member<Event, EventList>);
114 static_assert(std::is_same_v<decltype(event.abstime),
115 typename NumericTraits::abstime_type>);
116 if (ended_with_exception)
117 return;
118 try {
119 if (is_pending_on_other<InputChannel>()) {
120 // Emit any older events pending on the other input.
121 auto cutoff = event.abstime;
122 // Emit events from input 0 before events from input 1 when
123 // they have equal abstime.
124 if constexpr (InputChannel == 0)
125 --cutoff;
126 emit_pending([=](auto t) { return t <= cutoff; });
127
128 // If events still pending on the other input, they are newer
129 // (or not older), so we can emit the current event first.
130 if (not pending.empty())
131 return downstream.handle(event);
132
133 // If we are still here, we have no more events pending from
134 // the other input, but will now enqueue the current event on
135 // this input.
136 set_pending_on<InputChannel>();
137 }
138 // If we got here, no events from the other input are pending. If
139 // the other input is also flushed, we have no need to buffer.
140 if (is_other_flushed<InputChannel>()) {
141 assert(pending.empty());
142 return downstream.handle(event);
143 }
144 if (pending.size() == max_buffered)
145 throw buffer_overflow_error("merge buffer capacity exceeded");
146 pending.push(event);
147 } catch (std::exception const &) {
148 ended_with_exception = true;
149 throw;
150 }
151 }
152
153 template <unsigned InputChannel> void flush() {
154 input_flushed[InputChannel] = true;
155 if (ended_with_exception)
156 return;
157 if (is_other_flushed<InputChannel>()) {
158 // Since the other input was flushed, events on this input have not
159 // been buffered. But there may still be events pending on the
160 // other input.
161 emit_pending([](auto /* t */) { return true; });
162 downstream.flush();
163 } else if (is_pending_on_other<InputChannel>()) {
164 // Since this input won't have any more events, no need to buffer
165 // the other any more.
166 emit_pending([](auto /* t */) { return true; });
167 }
168 }
169};
170
171template <unsigned InputChannel, typename EventList, typename NumericTraits,
172 typename Downstream>
173class merge_input {
174 std::shared_ptr<merge_impl<EventList, NumericTraits, Downstream>> impl;
175
176 public:
177 explicit merge_input(
178 std::shared_ptr<merge_impl<EventList, NumericTraits, Downstream>> impl)
179 : impl(std::move(impl)) {}
180
181 // Move-constructible but not copyable or move-assignable
182 merge_input(merge_input const &) = delete;
183 auto operator=(merge_input const &) = delete;
184 merge_input(merge_input &&) noexcept = default;
185 auto operator=(merge_input &&) = delete;
186 ~merge_input() = default;
187
188 [[nodiscard]] auto introspect_node() const -> processor_info {
189 return processor_info(this, "merge_input");
190 }
191
192 [[nodiscard]] auto introspect_graph() const -> processor_graph {
193 return impl->introspect_graph().push_entry_point(this);
194 }
195
196 template <typename Event>
197 requires convertible_to_type_list_member<std::remove_cvref_t<Event>,
198 EventList>
199 void handle(Event &&event) {
200 static_assert(std::is_same_v<decltype(event.abstime),
201 typename NumericTraits::abstime_type>);
202 impl->template handle<InputChannel>(std::forward<Event>(event));
203 }
204
205 void flush() { impl->template flush<InputChannel>(); }
206};
207
208} // namespace internal
209
250template <typename EventList, typename NumericTraits = default_numeric_traits,
251 typename Downstream>
253 Downstream downstream) {
254 auto p = std::make_shared<
255 internal::merge_impl<EventList, NumericTraits, Downstream>>(
256 max_buffered, std::move(downstream));
257 return std::pair{
258 internal::merge_input<0, EventList, NumericTraits, Downstream>(p),
259 internal::merge_input<1, EventList, NumericTraits, Downstream>(p)};
260}
261
312template <std::size_t N, typename EventList,
313 typename NumericTraits = default_numeric_traits, typename Downstream>
315 Downstream downstream) {
316 if constexpr (N == 0) {
317 return std::tuple{};
318 } else if constexpr (N == 1) {
319 return std::tuple{std::move(downstream)};
320 } else {
321 auto [final_in0, final_in1] = merge<EventList, NumericTraits>(
322 max_buffered, std::move(downstream));
323
324 std::size_t const left = N / 2;
325 std::size_t const right = N - left;
326 if constexpr (left == 1) {
327 if constexpr (right == 1) {
328 return std::tuple{std::move(final_in0), std::move(final_in1)};
329 } else {
330 return std::tuple_cat(std::tuple{std::move(final_in0)},
332 max_buffered, std::move(final_in1)));
333 }
334 } else {
335 return std::tuple_cat(merge_n<left, EventList, NumericTraits>(
336 max_buffered, std::move(final_in0)),
338 max_buffered, std::move(final_in1)));
339 }
340 }
341}
342
343namespace internal {
344
345// Internal implementation of N-way unsorted merge processor. This processor is
346// owned by the N input processors via shared_ptr.
347template <std::size_t N, typename Downstream>
348 requires processor<Downstream>
349class merge_unsorted_impl {
350 Downstream downstream;
351
352 // Cold data.
353 bool ended_with_exception = false;
354 std::array<bool, N> input_flushed{};
355
356 public:
357 explicit merge_unsorted_impl(Downstream downstream)
358 : downstream(std::move(downstream)) {}
359
360 merge_unsorted_impl(merge_unsorted_impl const &) = delete;
361 auto operator=(merge_unsorted_impl const &) = delete;
362 merge_unsorted_impl(merge_unsorted_impl &&) = delete;
363 auto operator=(merge_unsorted_impl &&) = delete;
364 ~merge_unsorted_impl() = default;
365
366 [[nodiscard]] auto introspect_node() const -> processor_info {
367 return processor_info(this, "merge_unsorted_impl");
368 }
369
370 [[nodiscard]] auto introspect_graph() const -> processor_graph {
371 return downstream.introspect_graph().push_entry_point(this);
372 }
373
374 template <typename Event>
375 requires handler_for<Downstream, std::remove_cvref_t<Event>>
376 void handle(Event &&event) {
377 if (ended_with_exception)
378 return;
379 try {
380 downstream.handle(std::forward<Event>(event));
381 } catch (std::exception const &) {
382 ended_with_exception = true;
383 throw;
384 }
385 }
386
387 void flush(std::size_t input_channel) {
388 input_flushed[input_channel] = true;
389 if (ended_with_exception)
390 return;
391 if (std::all_of(input_flushed.begin(), input_flushed.end(),
392 [](auto f) { return f; }))
393 downstream.flush();
394 }
395};
396
397template <std::size_t N, typename Downstream> class merge_unsorted_input {
398 std::shared_ptr<merge_unsorted_impl<N, Downstream>> impl;
399
400 // Cold data.
401 std::size_t chan;
402
403 public:
404 explicit merge_unsorted_input(
405 std::shared_ptr<merge_unsorted_impl<N, Downstream>> impl,
406 std::size_t channel)
407 : impl(std::move(impl)), chan(channel) {}
408
409 // Move-constructible but not copyable or move-assignable
410 merge_unsorted_input(merge_unsorted_input const &) = delete;
411 auto operator=(merge_unsorted_input const &) = delete;
412 merge_unsorted_input(merge_unsorted_input &&) noexcept = default;
413 auto operator=(merge_unsorted_input &&) = delete;
414 ~merge_unsorted_input() = default;
415
416 [[nodiscard]] auto introspect_node() const -> processor_info {
417 return processor_info(this, "merge_unsorted_input");
418 }
419
420 [[nodiscard]] auto introspect_graph() const -> processor_graph {
421 return impl->introspect_graph().push_entry_point(this);
422 }
423
424 template <typename Event>
425 requires handler_for<Downstream, std::remove_cvref_t<Event>>
426 void handle(Event &&event) {
427 impl->handle(std::forward<Event>(event));
428 }
429
430 void flush() { impl->flush(chan); }
431};
432
433template <std::size_t N, typename Downstream, std::size_t... Indices>
434auto make_merge_unsorted_inputs(
435 std::shared_ptr<merge_unsorted_impl<N, Downstream>> impl,
436 std::index_sequence<Indices...> /* indices */) {
437 using input_type = merge_unsorted_input<N, Downstream>;
438 return std::array<input_type, N>{(input_type(impl, Indices))...};
439}
440
441} // namespace internal
442
469template <std::size_t N = 2, typename Downstream>
470auto merge_n_unsorted(Downstream downstream) {
471 auto impl = std::make_shared<internal::merge_unsorted_impl<N, Downstream>>(
472 std::move(downstream));
473 return internal::make_merge_unsorted_inputs(std::move(impl),
474 std::make_index_sequence<N>());
475}
476
477} // namespace tcspc
constexpr bool is_processor_of_list_v
Trait variable to check whether a processor handles a list of event types and flush.
Definition processor.hpp:223
auto merge_n_unsorted(Downstream downstream)
Create a processor that merges a given number of event streams without sorting by abstime.
Definition merge.hpp:470
auto merge_n(arg::max_buffered< std::size_t > max_buffered, Downstream downstream)
Create a processor that merges a given number of event streams.
Definition merge.hpp:314
auto merge(arg::max_buffered< std::size_t > max_buffered, Downstream downstream)
Create a pair of processors that merge two event streams.
Definition merge.hpp:252
constexpr auto visit_variant_or_single_event(Visitor visitor, Event &&event)
Apply a visitor to an event that is not a tcspc::variant_event.
Definition variant_event.hpp:131
libtcspc namespace.
Definition acquire.hpp:30
Function argument wrapper for maximum buffered parameter.
Definition arg_wrappers.hpp:237
The default numeric traits.
Definition numeric_traits.hpp:27