tess 1.0.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
63enum class TraceRecordKind : std::uint8_t {
64 Event,
65 Duration,
66};
67
75 TraceCategory category = TraceCategory::General;
76 std::string_view label;
77 std::uint64_t value = 0;
78 std::uint64_t sequence = 0;
79 TraceRecordKind kind = TraceRecordKind::Event;
80 std::uint64_t allocation_bytes = 0;
81 std::uint64_t deallocation_bytes = 0;
82};
83
86 std::uint64_t samples = 0;
87 std::uint64_t total_ns = 0;
88 std::uint64_t min_ns = 0;
89 std::uint64_t max_ns = 0;
90
91 void reset() noexcept { *this = TraceCategoryStats{}; }
92};
93
101class TraceBuffer {
102 public:
103 explicit TraceBuffer(std::span<TraceRecord> storage) noexcept
104 : storage_{storage} {}
105
106 // Caller-owned and referenced by address: ScopedTrace/ScopedTimer capture a
107 // TraceBuffer* and the ring metadata plus timing accumulators live in the
108 // object while the records live in the shared backing span. Copying or moving
109 // would split that metadata from the storage, so a by-value copy would
110 // collect records the caller's original never sees. The buffer is therefore
111 // pinned to its storage -- construct it in place, pass it by reference.
112 TraceBuffer(const TraceBuffer&) = delete;
113 auto operator=(const TraceBuffer&) -> TraceBuffer& = delete;
114 TraceBuffer(TraceBuffer&&) = delete;
115 auto operator=(TraceBuffer&&) -> TraceBuffer& = delete;
116
117 // Append a structured trace record. When the ring is full the oldest record
118 // is overwritten and dropped() is bumped; sequence numbers keep advancing so
119 // a reader can see the gap. A record against the Count sentinel (or any
120 // out-of-range category) is rejected and counted as dropped, so the ring
121 // never carries a non-category value.
122 void record(TraceCategory category, std::string_view label,
123 std::uint64_t value) noexcept {
124 record_impl(category, label, value, TraceRecordKind::Event, 0, 0);
125 }
126
128 void record_span(TraceCategory category, std::string_view label,
129 std::uint64_t nanos, std::uint64_t allocation_bytes = 0,
130 std::uint64_t deallocation_bytes = 0) noexcept {
131 record_timing(category, nanos);
132 record_impl(category, label, nanos, TraceRecordKind::Duration,
133 allocation_bytes, deallocation_bytes);
134 }
135
136 private:
137 void record_impl(TraceCategory category, std::string_view label,
138 std::uint64_t value, TraceRecordKind kind,
139 std::uint64_t allocation_bytes,
140 std::uint64_t deallocation_bytes) noexcept {
141 const auto seq = sequence_++;
142 if (storage_.empty() ||
143 static_cast<std::size_t>(category) >= trace_category_count) {
144 ++dropped_;
145 return;
146 }
147 if (count_ == storage_.size()) {
148 head_ = (head_ + 1) % storage_.size();
149 ++dropped_;
150 } else {
151 ++count_;
152 }
153 const auto slot = (head_ + count_ - 1) % storage_.size();
154 storage_[slot] = TraceRecord{
155 category, label, value, seq, kind, allocation_bytes,
156 deallocation_bytes};
157 }
158
159 public:
160 // Fold one timing sample (nanoseconds) into a category's accumulator. Records
161 // against the Count sentinel or any out-of-range category are ignored.
162 // total_ns is a running sum that wraps only after ~584 years of accumulated
163 // time, so it is treated as unbounded in practice.
164 void record_timing(TraceCategory category, std::uint64_t nanos) noexcept {
165 const auto index = static_cast<std::size_t>(category);
166 if (index >= stats_.size()) {
167 return;
168 }
169 auto& stats = stats_[index];
170 if (stats.samples == 0) {
171 stats.min_ns = nanos;
172 stats.max_ns = nanos;
173 } else {
174 if (nanos < stats.min_ns) {
175 stats.min_ns = nanos;
176 }
177 if (nanos > stats.max_ns) {
178 stats.max_ns = nanos;
179 }
180 }
181 ++stats.samples;
182 stats.total_ns += nanos;
183 }
184
185 [[nodiscard]] auto size() const noexcept -> std::size_t { return count_; }
186
187 [[nodiscard]] auto capacity() const noexcept -> std::size_t {
188 return storage_.size();
189 }
190
191 [[nodiscard]] bool empty() const noexcept { return count_ == 0; }
192
193 [[nodiscard]] bool full() const noexcept { return count_ == storage_.size(); }
194
195 // Total records lost to ring overflow (or, for an empty span, every record).
196 [[nodiscard]] auto dropped() const noexcept -> std::uint64_t {
197 return dropped_;
198 }
199
200 // Oldest-first access: index 0 is the oldest retained record, size() - 1 the
201 // newest. Behavior is undefined for index >= size().
202 [[nodiscard]] auto operator[](std::size_t index) const noexcept
203 -> const TraceRecord& {
204 return storage_[(head_ + index) % storage_.size()];
205 }
206
207 // Timing accumulator for a category. Returns a zeroed reference for the Count
208 // sentinel or any out-of-range category.
209 [[nodiscard]] auto stats(TraceCategory category) const noexcept
210 -> const TraceCategoryStats& {
211 static constexpr TraceCategoryStats kZero{};
212 const auto index = static_cast<std::size_t>(category);
213 if (index >= stats_.size()) {
214 return kZero;
215 }
216 return stats_[index];
217 }
218
219 // Snapshot of every category's timing accumulator, indexed by
220 // static_cast<std::size_t>(TraceCategory).
221 [[nodiscard]] auto all_stats() const noexcept
222 -> const std::array<TraceCategoryStats, trace_category_count>& {
223 return stats_;
224 }
225
226 void clear() noexcept {
227 head_ = 0;
228 count_ = 0;
229 sequence_ = 0;
230 dropped_ = 0;
231 for (auto& stats : stats_) {
232 stats.reset();
233 }
234 }
235
236 private:
237 std::span<TraceRecord> storage_;
238 std::array<TraceCategoryStats, trace_category_count> stats_{};
239 std::size_t head_ = 0; // index of the oldest retained record
240 std::size_t count_ = 0; // retained records (<= storage_.size())
241 std::uint64_t sequence_ = 0; // next record ordinal
242 std::uint64_t dropped_ = 0;
243};
244
245// Thread-local active buffer, mirroring the counter sinks in diagnostics.h. The
246// TESS_DIAG_TRACE macros and trace_event route to whichever buffer is installed
247// on the current thread; worker threads do not feed the installer's buffer (the
248// same deliberate thread_local limit documented for the counters). Reading a
249// buffer -- including export.h's capture_timing/capture_diagnostics -- is
250// likewise unsynchronized: read on the recording thread, or externally
251// synchronize the read against all recording into that buffer.
252inline thread_local TraceBuffer* active_trace_buffer = nullptr;
253
259class ScopedTrace {
260 public:
261 explicit ScopedTrace(TraceBuffer& buffer) noexcept
262 : previous_{active_trace_buffer} {
263 active_trace_buffer = &buffer;
264 }
265
266 ScopedTrace(const ScopedTrace&) = delete;
267 auto operator=(const ScopedTrace&) -> ScopedTrace& = delete;
268
269 ~ScopedTrace() { active_trace_buffer = previous_; }
270
271 private:
272 TraceBuffer* previous_;
273};
274
276inline void trace_event(TraceCategory category, std::string_view label,
277 std::uint64_t value) noexcept {
278 if (active_trace_buffer != nullptr) {
279 active_trace_buffer->record(category, label, value);
280 }
281}
282
290class ScopedTimer {
291 public:
292 ScopedTimer(TraceCategory category, std::string_view label) noexcept
293 : target_{active_trace_buffer},
294 allocation_target_{active_allocation_counters},
295 allocation_scope_id_{active_allocation_scope_id},
296 category_{category},
297 label_{label},
298 start_{target_ == nullptr ? std::chrono::steady_clock::time_point{}
299 : std::chrono::steady_clock::now()},
300 allocation_bytes_at_start_{allocation_target_ == nullptr
301 ? 0
302 : allocation_target_->allocation_bytes},
303 deallocation_bytes_at_start_{
304 allocation_target_ == nullptr
305 ? 0
306 : allocation_target_->deallocation_bytes} {}
307
308 ScopedTimer(const ScopedTimer&) = delete;
309 auto operator=(const ScopedTimer&) -> ScopedTimer& = delete;
310
311 ~ScopedTimer() {
312 if (target_ == nullptr) {
313 return;
314 }
315 const auto elapsed = std::chrono::steady_clock::now() - start_;
316 const auto ticks =
317 std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
318 const auto nanos =
319 ticks < 0 ? std::uint64_t{0} : static_cast<std::uint64_t>(ticks);
320 const auto allocation_scope_is_active =
321 allocation_target_ != nullptr &&
322 allocation_target_ == active_allocation_counters &&
323 allocation_scope_id_ == active_allocation_scope_id;
324 const auto allocation_bytes =
325 !allocation_scope_is_active || allocation_target_->allocation_bytes <
326 allocation_bytes_at_start_
327 ? 0
328 : allocation_target_->allocation_bytes - allocation_bytes_at_start_;
329 const auto deallocation_bytes =
330 !allocation_scope_is_active || allocation_target_->deallocation_bytes <
331 deallocation_bytes_at_start_
332 ? 0
333 : allocation_target_->deallocation_bytes -
334 deallocation_bytes_at_start_;
335 target_->record_span(category_, label_, nanos, allocation_bytes,
336 deallocation_bytes);
337 }
338
339 private:
340 TraceBuffer* target_;
341 AllocationCounters* allocation_target_;
342 std::uint64_t allocation_scope_id_;
343 TraceCategory category_;
344 std::string_view label_;
345 std::chrono::steady_clock::time_point start_;
346 std::uint64_t allocation_bytes_at_start_;
347 std::uint64_t deallocation_bytes_at_start_;
348};
349
350#endif // TESS_DIAGNOSTICS_ENABLED
351
352} // namespace tess::diagnostics
Definition trace.h:101
void record_span(TraceCategory category, std::string_view label, std::uint64_t nanos, std::uint64_t allocation_bytes=0, std::uint64_t deallocation_bytes=0) noexcept
Definition trace.h:128
Definition diagnostics.h:103
Definition trace.h:74