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 events older than cutoff (or not newer, if Inclusive).
78 template <bool Inclusive>
79 void emit_pending_until(typename NumericTraits::abstime_type cutoff) {
80 auto emit_if_before_cutoff = [&](auto const &e) {
81 bool const p =
82 Inclusive ? e.abstime <= cutoff : e.abstime < cutoff;
83 if (p)
84 downstream.handle(e);
85 return p;
86 };
87 while (!pending.empty() && visit_variant_or_single_event(
88 emit_if_before_cutoff, pending.front()))
89 pending.pop();
90 }
91
92 void emit_all_pending() {
93 auto emit = [&](auto const &e) { downstream.handle(e); };
94 while (!pending.empty()) {
95 visit_variant_or_single_event(emit, pending.front());
96 pending.pop();
97 }
98 }
99
100 public:
101 explicit merge_impl(arg::max_buffered<std::size_t> max_buffered,
102 Downstream downstream)
103 : max_buffered(max_buffered.value), downstream(std::move(downstream)) {
104 }
105
106 merge_impl(merge_impl const &) = delete;
107 auto operator=(merge_impl const &) = delete;
108 merge_impl(merge_impl &&) = delete;
109 auto operator=(merge_impl &&) = delete;
110 ~merge_impl() = default;
111
112 [[nodiscard]] auto introspect_node() const -> processor_info {
113 return processor_info(this, "merge_impl");
114 }
115
116 [[nodiscard]] auto introspect_graph() const -> processor_graph {
117 return downstream.introspect_graph().push_entry_point(this);
118 }
119
120 template <unsigned InputChannel, typename Event>
121 void handle(Event const &event) {
122 static_assert(convertible_to_type_list_member<Event, EventList>);
123 static_assert(std::is_same_v<decltype(event.abstime),
124 typename NumericTraits::abstime_type>);
125 if (ended_with_exception)
126 return;
127 try {
128 if (is_pending_on_other<InputChannel>()) {
129 // Emit any older events pending on the other input.
130 // Emit events from input 0 before events from input 1 when
131 // they have equal abstime.
132 emit_pending_until<InputChannel == 1>(event.abstime);
133
134 // If events still pending on the other input, they are newer
135 // (or not older), so we can emit the current event first.
136 if (not pending.empty())
137 return downstream.handle(event);
138
139 // If we are still here, we have no more events pending from
140 // the other input, but will now enqueue the current event on
141 // this input.
142 set_pending_on<InputChannel>();
143 }
144 // If we got here, no events from the other input are pending. If
145 // the other input is also flushed, we have no need to buffer.
146 if (is_other_flushed<InputChannel>()) {
147 assert(pending.empty());
148 return downstream.handle(event);
149 }
150 if (pending.size() == max_buffered)
151 throw buffer_overflow_error("merge buffer capacity exceeded");
152 pending.push(event);
153 } catch (std::exception const &) {
154 ended_with_exception = true;
155 throw;
156 }
157 }
158
159 template <unsigned InputChannel> void flush() {
160 input_flushed[InputChannel] = true;
161 if (ended_with_exception)
162 return;
163 if (is_other_flushed<InputChannel>()) {
164 // Since the other input was flushed, events on this input have not
165 // been buffered. But there may still be events pending on the
166 // other input.
167 emit_all_pending();
168 downstream.flush();
169 } else if (is_pending_on_other<InputChannel>()) {
170 // Since this input won't have any more events, no need to buffer
171 // the other any more.
172 emit_all_pending();
173 }
174 }
175};
176
177template <unsigned InputChannel, typename EventList, typename NumericTraits,
178 typename Downstream>
179class merge_input {
180 std::shared_ptr<merge_impl<EventList, NumericTraits, Downstream>> impl;
181
182 public:
183 explicit merge_input(
184 std::shared_ptr<merge_impl<EventList, NumericTraits, Downstream>> impl)
185 : impl(std::move(impl)) {}
186
187 // Move-constructible but not copyable or move-assignable
188 merge_input(merge_input const &) = delete;
189 auto operator=(merge_input const &) = delete;
190 merge_input(merge_input &&) noexcept = default;
191 auto operator=(merge_input &&) = delete;
192 ~merge_input() = default;
193
194 [[nodiscard]] auto introspect_node() const -> processor_info {
195 return processor_info(this, "merge_input");
196 }
197
198 [[nodiscard]] auto introspect_graph() const -> processor_graph {
199 return impl->introspect_graph().push_entry_point(this);
200 }
201
202 template <typename Event>
203 requires convertible_to_type_list_member<std::remove_cvref_t<Event>,
204 EventList>
205 void handle(Event &&event) {
206 static_assert(std::is_same_v<decltype(event.abstime),
207 typename NumericTraits::abstime_type>);
208 impl->template handle<InputChannel>(std::forward<Event>(event));
209 }
210
211 void flush() { impl->template flush<InputChannel>(); }
212};
213
214} // namespace internal
215
256template <typename EventList, typename NumericTraits = default_numeric_traits,
257 typename Downstream>
259 Downstream downstream) {
260 auto p = std::make_shared<
261 internal::merge_impl<EventList, NumericTraits, Downstream>>(
262 max_buffered, std::move(downstream));
263 return std::pair{
264 internal::merge_input<0, EventList, NumericTraits, Downstream>(p),
265 internal::merge_input<1, EventList, NumericTraits, Downstream>(p)};
266}
267
318template <std::size_t N, typename EventList,
319 typename NumericTraits = default_numeric_traits, typename Downstream>
321 Downstream downstream) {
322 if constexpr (N == 0) {
323 return std::tuple{};
324 } else if constexpr (N == 1) {
325 return std::tuple{std::move(downstream)};
326 } else {
327 auto [final_in0, final_in1] = merge<EventList, NumericTraits>(
328 max_buffered, std::move(downstream));
329
330 std::size_t const left = N / 2;
331 std::size_t const right = N - left;
332 if constexpr (left == 1) {
333 if constexpr (right == 1) {
334 return std::tuple{std::move(final_in0), std::move(final_in1)};
335 } else {
336 return std::tuple_cat(std::tuple{std::move(final_in0)},
338 max_buffered, std::move(final_in1)));
339 }
340 } else {
341 return std::tuple_cat(merge_n<left, EventList, NumericTraits>(
342 max_buffered, std::move(final_in0)),
344 max_buffered, std::move(final_in1)));
345 }
346 }
347}
348
349namespace internal {
350
351// Internal implementation of N-way unsorted merge processor. This processor is
352// owned by the N input processors via shared_ptr.
353template <std::size_t N, typename Downstream>
354 requires processor<Downstream>
355class merge_unsorted_impl {
356 Downstream downstream;
357
358 // Cold data.
359 bool ended_with_exception = false;
360 std::array<bool, N> input_flushed{};
361
362 public:
363 explicit merge_unsorted_impl(Downstream downstream)
364 : downstream(std::move(downstream)) {}
365
366 merge_unsorted_impl(merge_unsorted_impl const &) = delete;
367 auto operator=(merge_unsorted_impl const &) = delete;
368 merge_unsorted_impl(merge_unsorted_impl &&) = delete;
369 auto operator=(merge_unsorted_impl &&) = delete;
370 ~merge_unsorted_impl() = default;
371
372 [[nodiscard]] auto introspect_node() const -> processor_info {
373 return processor_info(this, "merge_unsorted_impl");
374 }
375
376 [[nodiscard]] auto introspect_graph() const -> processor_graph {
377 return downstream.introspect_graph().push_entry_point(this);
378 }
379
380 template <typename Event>
381 requires handler_for<Downstream, std::remove_cvref_t<Event>>
382 void handle(Event &&event) {
383 if (ended_with_exception)
384 return;
385 try {
386 downstream.handle(std::forward<Event>(event));
387 } catch (std::exception const &) {
388 ended_with_exception = true;
389 throw;
390 }
391 }
392
393 void flush(std::size_t input_channel) {
394 input_flushed[input_channel] = true;
395 if (ended_with_exception)
396 return;
397 if (std::all_of(input_flushed.begin(), input_flushed.end(),
398 [](auto f) { return f; }))
399 downstream.flush();
400 }
401};
402
403template <std::size_t N, typename Downstream> class merge_unsorted_input {
404 std::shared_ptr<merge_unsorted_impl<N, Downstream>> impl;
405
406 // Cold data.
407 std::size_t chan;
408
409 public:
410 explicit merge_unsorted_input(
411 std::shared_ptr<merge_unsorted_impl<N, Downstream>> impl,
412 std::size_t channel)
413 : impl(std::move(impl)), chan(channel) {}
414
415 // Move-constructible but not copyable or move-assignable
416 merge_unsorted_input(merge_unsorted_input const &) = delete;
417 auto operator=(merge_unsorted_input const &) = delete;
418 merge_unsorted_input(merge_unsorted_input &&) noexcept = default;
419 auto operator=(merge_unsorted_input &&) = delete;
420 ~merge_unsorted_input() = default;
421
422 [[nodiscard]] auto introspect_node() const -> processor_info {
423 return processor_info(this, "merge_unsorted_input");
424 }
425
426 [[nodiscard]] auto introspect_graph() const -> processor_graph {
427 return impl->introspect_graph().push_entry_point(this);
428 }
429
430 template <typename Event>
431 requires handler_for<Downstream, std::remove_cvref_t<Event>>
432 void handle(Event &&event) {
433 impl->handle(std::forward<Event>(event));
434 }
435
436 void flush() { impl->flush(chan); }
437};
438
439template <std::size_t N, typename Downstream, std::size_t... Indices>
440auto make_merge_unsorted_inputs(
441 std::shared_ptr<merge_unsorted_impl<N, Downstream>> impl,
442 std::index_sequence<Indices...> /* indices */) {
443 using input_type = merge_unsorted_input<N, Downstream>;
444 return std::array<input_type, N>{(input_type(impl, Indices))...};
445}
446
447} // namespace internal
448
475template <std::size_t N = 2, typename Downstream>
476auto merge_n_unsorted(Downstream downstream) {
477 auto impl = std::make_shared<internal::merge_unsorted_impl<N, Downstream>>(
478 std::move(downstream));
479 return internal::make_merge_unsorted_inputs(std::move(impl),
480 std::make_index_sequence<N>());
481}
482
483} // 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:476
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:320
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:258
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