tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
event_stream.h
1#pragma once
2
3#include <tess/core/assert.h>
4#include <tess/core/fail_fast.h>
5#include <tess/diagnostics/diagnostics.h>
6
7#include <algorithm>
8#include <cstddef>
9#include <cstdint>
10#include <span>
11#include <utility>
12#include <vector>
13
14namespace tess {
15
17template <typename T>
19 std::uint64_t tick = 0;
20 std::uint64_t sequence = 0;
21 T value{};
22};
23
32template <typename T>
33class EventStream {
34 public:
35 using event_type = TickStampedEvent<T>;
36
37 void reserve_events(std::size_t count) {
38 if (!events_.empty()) {
39 detail::fail_fast("EventStream::reserve_events requires an empty stream");
40 }
41 max_events_ = count;
42 events_.reserve(count);
43 }
44
45 [[nodiscard]] auto publish(std::uint64_t tick, const T& value) -> bool {
46 if (events_.size() >= max_events_) {
47 ++rejected_events_;
48 account_publish(false);
49 return false;
50 }
51 events_.push_back(event_type{tick, next_sequence_, value});
52 ++next_sequence_;
53 account_publish(true);
54 return true;
55 }
56
57 [[nodiscard]] auto publish(std::uint64_t tick, T&& value) -> bool {
58 if (events_.size() >= max_events_) {
59 ++rejected_events_;
60 account_publish(false);
61 return false;
62 }
63 events_.push_back(event_type{tick, next_sequence_, std::move(value)});
64 ++next_sequence_;
65 account_publish(true);
66 return true;
67 }
68
69 [[nodiscard]] auto events() const noexcept -> std::span<const event_type> {
70 return {events_.data(), events_.size()};
71 }
72
73 [[nodiscard]] auto size() const noexcept -> std::size_t {
74 return events_.size();
75 }
76
77 [[nodiscard]] bool empty() const noexcept { return events_.empty(); }
78
79 [[nodiscard]] auto rejected_events() const noexcept -> std::uint64_t {
80 return rejected_events_;
81 }
82
90 void clear() noexcept { retire_batch(false); }
91
93 void consume_all() noexcept { retire_batch(true); }
94
96 void discard_all() noexcept { retire_batch(false); }
97
106 if (!events_.empty()) {
107 detail::fail_fast(
108 "EventStream::set_flow_accounting requires an empty stream");
109 }
110 accounting_ = accounting;
111 }
112
119 void observe_flow_tick(std::uint64_t tick) noexcept {
120 if (accounting_ == nullptr) {
121 return;
122 }
123 accounting_->observe_tick(tick);
124 const auto now = accounting_->last_observed_tick;
125 accounting_->counters.oldest_outstanding_age_ticks =
126 events_.empty() ? 0 : now - oldest_published_tick_;
127 }
128
129 EventStream() = default;
130
131 // An attached accountant tracks exactly one stream: copies start
132 // unattached with cleared residence stamps, moves transfer the
133 // attachment. Assigning over an instrumented, non-empty stream would
134 // orphan outstanding inventory, so the destination must be empty or
135 // unattached.
136 EventStream(const EventStream& other)
137 : events_{other.events_},
138 max_events_{other.max_events_},
139 next_sequence_{other.next_sequence_},
140 rejected_events_{other.rejected_events_} {}
141 auto operator=(const EventStream& other) -> EventStream& {
142 if (this != &other) {
143 if (accounting_ != nullptr && !events_.empty()) {
144 detail::fail_fast(
145 "EventStream copy assignment would orphan outstanding "
146 "accounting");
147 }
148 events_ = other.events_;
149 max_events_ = other.max_events_;
150 next_sequence_ = other.next_sequence_;
151 rejected_events_ = other.rejected_events_;
152 oldest_published_tick_ = 0;
153 published_tick_total_ = 0;
154 accounting_ = nullptr;
155 }
156 return *this;
157 }
158 EventStream(EventStream&& other) noexcept
159 : events_{std::move(other.events_)},
160 max_events_{other.max_events_},
161 next_sequence_{other.next_sequence_},
162 rejected_events_{other.rejected_events_},
163 oldest_published_tick_{other.oldest_published_tick_},
164 published_tick_total_{other.published_tick_total_},
165 accounting_{other.accounting_} {
166 other.accounting_ = nullptr;
167 other.oldest_published_tick_ = 0;
168 other.published_tick_total_ = 0;
169 }
170 auto operator=(EventStream&& other) noexcept -> EventStream& {
171 if (this != &other) {
172 if (accounting_ != nullptr && !events_.empty()) {
173 detail::fail_fast(
174 "EventStream move assignment would orphan outstanding "
175 "accounting");
176 }
177 events_ = std::move(other.events_);
178 max_events_ = other.max_events_;
179 next_sequence_ = other.next_sequence_;
180 rejected_events_ = other.rejected_events_;
181 oldest_published_tick_ = other.oldest_published_tick_;
182 published_tick_total_ = other.published_tick_total_;
183 accounting_ = other.accounting_;
184 other.accounting_ = nullptr;
185 other.oldest_published_tick_ = 0;
186 other.published_tick_total_ = 0;
187 }
188 return *this;
189 }
190 ~EventStream() = default;
191
192 private:
193 void retire_batch(bool consumed) noexcept {
194 if (accounting_ != nullptr) {
195 auto& counters = accounting_->counters;
196 const auto count = static_cast<std::uint64_t>(events_.size());
197 (consumed ? counters.completed : counters.dropped_after_admission) +=
198 count;
199 // Clamped, matching `FlowAccounting::record_left_outstanding` and
200 // every other terminalization site. A shared accountant whose other
201 // flow terminalized first, or a `counters.reset()` between publish
202 // and retire, would otherwise wrap this unsigned counter and take
203 // `inventory_tick_weighted` and the retention identity with it.
204 counters.outstanding_current -=
205 std::min(counters.outstanding_current, count);
206 counters.residence_ticks_accumulated +=
207 accounting_->last_observed_tick * count - published_tick_total_;
208 published_tick_total_ = 0;
209 counters.oldest_outstanding_age_ticks = 0;
210 }
211 events_.clear();
212 }
213
214 void account_publish(bool admitted) noexcept {
215 if (accounting_ == nullptr) {
216 return;
217 }
218 ++accounting_->counters.offered;
219 if (!admitted) {
220 ++accounting_->counters.rejected;
221 return;
222 }
223 accounting_->record_admitted();
224 if (events_.size() == 1) {
225 oldest_published_tick_ = accounting_->last_observed_tick;
226 }
227 published_tick_total_ += accounting_->last_observed_tick;
228 }
229
230 std::vector<event_type> events_;
231 std::size_t max_events_ = 0;
232 std::uint64_t next_sequence_ = 0;
233 std::uint64_t rejected_events_ = 0;
234 std::uint64_t oldest_published_tick_ = 0;
235 std::uint64_t published_tick_total_ = 0;
236 diagnostics::FlowAccounting* accounting_ = nullptr;
237};
238
239} // namespace tess
Definition event_stream.h:33
void observe_flow_tick(std::uint64_t tick) noexcept
Definition event_stream.h:119
void discard_all() noexcept
Retires the batch unread: every event counts dropped.
Definition event_stream.h:96
void set_flow_accounting(diagnostics::FlowAccounting *accounting) noexcept
Definition event_stream.h:105
void consume_all() noexcept
Retires the batch as read: every event counts completed.
Definition event_stream.h:93
void clear() noexcept
Definition event_stream.h:90
One exact event payload with deterministic simulation and stream stamps.
Definition event_stream.h:18
Definition diagnostics.h:505