tess 0.4.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
trace.h
1#pragma once
2
3#include <tess/diagnostics/diagnostics.h>
4
5#include <array>
6#include <chrono>
7#include <cstddef>
8#include <cstdint>
9#include <span>
10#include <string_view>
11
12// Trace macros live here, next to the trace_event/TraceCategory they expand to,
13// so a translation unit that includes this header gets a self-contained macro:
14// TESS_DIAG_TRACE routes to the active trace buffer when diagnostics are on and
15// compiles to an empty statement (never naming its arguments) when off.
16#if TESS_DIAGNOSTICS_ENABLED
18#define TESS_DIAG_TRACE(category, label) \
19 do { \
20 ::tess::diagnostics::trace_event((category), (label), 0); \
21 } while (false)
23#define TESS_DIAG_TRACE_VALUE(category, label, value) \
24 do { \
25 ::tess::diagnostics::trace_event((category), (label), (value)); \
26 } while (false)
27#else
28#define TESS_DIAG_TRACE(category, label) \
29 do { \
30 } while (false)
31#define TESS_DIAG_TRACE_VALUE(category, label, value) \
32 do { \
33 } while (false)
34#endif
35
36namespace tess::diagnostics {
37
38#if TESS_DIAGNOSTICS_ENABLED
39
45enum class TraceCategory : std::uint8_t {
46 General,
47 Path,
48 Topology,
49 Queued,
50 Planner,
51 Scheduler,
52 Render,
53 Count,
54};
55
57inline constexpr std::size_t trace_category_count =
58 static_cast<std::size_t>(TraceCategory::Count);
59
67 TraceCategory category = TraceCategory::General;
68 std::string_view label;
69 std::uint64_t value = 0;
70 std::uint64_t sequence = 0;
71};
72
75 std::uint64_t samples = 0;
76 std::uint64_t total_ns = 0;
77 std::uint64_t min_ns = 0;
78 std::uint64_t max_ns = 0;
79
80 void reset() noexcept { *this = TraceCategoryStats{}; }
81};
82
90class TraceBuffer {
91 public:
92 explicit TraceBuffer(std::span<TraceRecord> storage) noexcept
93 : storage_{storage} {}
94
95 // Caller-owned and referenced by address: ScopedTrace/ScopedTimer capture a
96 // TraceBuffer* and the ring metadata plus timing accumulators live in the
97 // object while the records live in the shared backing span. Copying or moving
98 // would split that metadata from the storage, so a by-value copy would
99 // collect records the caller's original never sees. The buffer is therefore
100 // pinned to its storage -- construct it in place, pass it by reference.
101 TraceBuffer(const TraceBuffer&) = delete;
102 auto operator=(const TraceBuffer&) -> TraceBuffer& = delete;
103 TraceBuffer(TraceBuffer&&) = delete;
104 auto operator=(TraceBuffer&&) -> TraceBuffer& = delete;
105
106 // Append a structured trace record. When the ring is full the oldest record
107 // is overwritten and dropped() is bumped; sequence numbers keep advancing so
108 // a reader can see the gap. A record against the Count sentinel (or any
109 // out-of-range category) is rejected and counted as dropped, so the ring
110 // never carries a non-category value.
111 void record(TraceCategory category, std::string_view label,
112 std::uint64_t value) noexcept {
113 const auto seq = sequence_++;
114 if (storage_.empty() ||
115 static_cast<std::size_t>(category) >= trace_category_count) {
116 ++dropped_;
117 return;
118 }
119 if (count_ == storage_.size()) {
120 head_ = (head_ + 1) % storage_.size();
121 ++dropped_;
122 } else {
123 ++count_;
124 }
125 const auto slot = (head_ + count_ - 1) % storage_.size();
126 storage_[slot] = TraceRecord{category, label, value, seq};
127 }
128
129 // Fold one timing sample (nanoseconds) into a category's accumulator. Records
130 // against the Count sentinel or any out-of-range category are ignored.
131 // total_ns is a running sum that wraps only after ~584 years of accumulated
132 // time, so it is treated as unbounded in practice.
133 void record_timing(TraceCategory category, std::uint64_t nanos) noexcept {
134 const auto index = static_cast<std::size_t>(category);
135 if (index >= stats_.size()) {
136 return;
137 }
138 auto& stats = stats_[index];
139 if (stats.samples == 0) {
140 stats.min_ns = nanos;
141 stats.max_ns = nanos;
142 } else {
143 if (nanos < stats.min_ns) {
144 stats.min_ns = nanos;
145 }
146 if (nanos > stats.max_ns) {
147 stats.max_ns = nanos;
148 }
149 }
150 ++stats.samples;
151 stats.total_ns += nanos;
152 }
153
154 [[nodiscard]] auto size() const noexcept -> std::size_t { return count_; }
155
156 [[nodiscard]] auto capacity() const noexcept -> std::size_t {
157 return storage_.size();
158 }
159
160 [[nodiscard]] bool empty() const noexcept { return count_ == 0; }
161
162 [[nodiscard]] bool full() const noexcept { return count_ == storage_.size(); }
163
164 // Total records lost to ring overflow (or, for an empty span, every record).
165 [[nodiscard]] auto dropped() const noexcept -> std::uint64_t {
166 return dropped_;
167 }
168
169 // Oldest-first access: index 0 is the oldest retained record, size() - 1 the
170 // newest. Behavior is undefined for index >= size().
171 [[nodiscard]] auto operator[](std::size_t index) const noexcept
172 -> const TraceRecord& {
173 return storage_[(head_ + index) % storage_.size()];
174 }
175
176 // Timing accumulator for a category. Returns a zeroed reference for the Count
177 // sentinel or any out-of-range category.
178 [[nodiscard]] auto stats(TraceCategory category) const noexcept
179 -> const TraceCategoryStats& {
180 static constexpr TraceCategoryStats kZero{};
181 const auto index = static_cast<std::size_t>(category);
182 if (index >= stats_.size()) {
183 return kZero;
184 }
185 return stats_[index];
186 }
187
188 // Snapshot of every category's timing accumulator, indexed by
189 // static_cast<std::size_t>(TraceCategory).
190 [[nodiscard]] auto category_stats() const noexcept
191 -> const std::array<TraceCategoryStats, trace_category_count>& {
192 return stats_;
193 }
194
195 void clear() noexcept {
196 head_ = 0;
197 count_ = 0;
198 sequence_ = 0;
199 dropped_ = 0;
200 for (auto& stats : stats_) {
201 stats.reset();
202 }
203 }
204
205 private:
206 std::span<TraceRecord> storage_;
207 std::array<TraceCategoryStats, trace_category_count> stats_{};
208 std::size_t head_ = 0; // index of the oldest retained record
209 std::size_t count_ = 0; // retained records (<= storage_.size())
210 std::uint64_t sequence_ = 0; // next record ordinal
211 std::uint64_t dropped_ = 0;
212};
213
214// Thread-local active buffer, mirroring the counter sinks in diagnostics.h. The
215// TESS_DIAG_TRACE macros and trace_event route to whichever buffer is installed
216// on the current thread; worker threads do not feed the installer's buffer (the
217// same deliberate thread_local limit documented for the counters). Reading a
218// buffer -- including export.h's capture_timing/capture_diagnostics -- is
219// likewise unsynchronized: read on the recording thread, or externally
220// synchronize the read against all recording into that buffer.
221inline thread_local TraceBuffer* active_trace_buffer = nullptr;
222
228class ScopedTrace {
229 public:
230 explicit ScopedTrace(TraceBuffer& buffer) noexcept
231 : previous_{active_trace_buffer} {
232 active_trace_buffer = &buffer;
233 }
234
235 ScopedTrace(const ScopedTrace&) = delete;
236 auto operator=(const ScopedTrace&) -> ScopedTrace& = delete;
237
238 ~ScopedTrace() { active_trace_buffer = previous_; }
239
240 private:
241 TraceBuffer* previous_;
242};
243
245inline void trace_event(TraceCategory category, std::string_view label,
246 std::uint64_t value) noexcept {
247 if (active_trace_buffer != nullptr) {
248 active_trace_buffer->record(category, label, value);
249 }
250}
251
259class ScopedTimer {
260 public:
261 ScopedTimer(TraceCategory category, std::string_view label) noexcept
262 : target_{active_trace_buffer},
263 category_{category},
264 label_{label},
265 start_{std::chrono::steady_clock::now()} {}
266
267 ScopedTimer(const ScopedTimer&) = delete;
268 auto operator=(const ScopedTimer&) -> ScopedTimer& = delete;
269
270 ~ScopedTimer() {
271 if (target_ == nullptr) {
272 return;
273 }
274 const auto elapsed = std::chrono::steady_clock::now() - start_;
275 const auto ticks =
276 std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
277 const auto nanos =
278 ticks < 0 ? std::uint64_t{0} : static_cast<std::uint64_t>(ticks);
279 target_->record_timing(category_, nanos);
280 target_->record(category_, label_, nanos);
281 }
282
283 private:
284 TraceBuffer* target_;
285 TraceCategory category_;
286 std::string_view label_;
287 std::chrono::steady_clock::time_point start_;
288};
289
290#endif // TESS_DIAGNOSTICS_ENABLED
291
292} // namespace tess::diagnostics
Definition trace.h:90
Definition trace.h:66