Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
flamegraph.cpp
Go to the documentation of this file.
1#include <pch.hpp>
2
4
5#include <helios/assert.hpp>
7
8#include <chrono>
9#include <concepts>
10#include <cstddef>
11#include <cstdint>
12#include <ostream>
13#include <source_location>
14#include <span>
15#include <string_view>
16#include <thread>
17#include <variant>
18
19namespace helios::profile {
20
21namespace {
22
23[[nodiscard]] constexpr size_t AlignUp(size_t value,
24 size_t alignment) noexcept {
25 return (value + alignment - 1) & ~(alignment - 1);
26}
27
28} // namespace
29
30size_t FlamegraphBackend::ZoneStorageSize() const noexcept {
31 static_assert(AlignUp(sizeof(ZoneState), alignof(std::max_align_t)) <=
33 "FlamegraphBackend zone state exceeds kZoneStorageBytes!");
34 return AlignUp(sizeof(ZoneState), alignof(std::max_align_t));
35}
36
38 output_file_.open(config_.output_path, std::ios::out | std::ios::trunc);
39 if (!output_file_.is_open()) {
40 return;
41 }
42 file_open_ = true;
43 output_file_ << "[\n";
44 started_ = true;
45}
46
48 if (!started_) {
49 return;
50 }
51 started_ = false;
52 FlushAll();
53}
54
56 std::span<std::byte> storage) noexcept {
57 auto* state = AsZoneState(storage);
58 std::construct_at(state);
59 state->name = spec.name.empty() ? spec.loc.function_name() : spec.name;
60 state->start_ts = NowMicros();
61}
62
63void FlamegraphBackend::EndZone(std::span<std::byte> storage) noexcept {
64 auto* state = AsZoneState(storage);
65 const uint64_t end_ts = NowMicros();
66
67 CompleteEvent event;
68 event.name = std::string{state->name};
69 event.start_ts = state->start_ts;
70 event.end_ts = end_ts;
71 event.tid = ThreadId();
72 event.has_text = state->has_text;
73 event.has_value = state->has_value;
74 if (state->has_text) {
75 event.text = std::string{state->text};
76 }
77 if (state->has_value) {
78 event.value = state->value;
79 }
80
81 std::destroy_at(state);
82
83 event_queue_.enqueue(std::move(event));
84 MaybeFlush();
85}
86
87void FlamegraphBackend::ZoneText(std::span<std::byte> storage,
88 std::string_view text) noexcept {
89 auto* state = AsZoneState(storage);
90 state->text = text;
91 state->has_text = true;
92}
93
94void FlamegraphBackend::ZoneValue(std::span<std::byte> storage,
95 uint64_t value) noexcept {
96 auto* state = AsZoneState(storage);
97 state->value = value;
98 state->has_value = true;
99}
100
101void FlamegraphBackend::ZoneName(std::span<std::byte> storage,
102 std::string_view name) noexcept {
103 auto* state = AsZoneState(storage);
104 state->name = name;
105}
106
108 InstantEvent event;
109 event.name = "Frame";
110 event.ts = NowMicros();
111 event.tid = ThreadId();
112 event.scope = "t";
113 event_queue_.enqueue(std::move(event));
114 MaybeFlush();
115}
116
118 InstantEvent event;
119 event.name = std::string{name.View()};
120 event.ts = NowMicros();
121 event.tid = ThreadId();
122 event.scope = "t";
123 event_queue_.enqueue(std::move(event));
124 MaybeFlush();
125}
126
128 InstantEvent event;
129 event.name = std::string{name.View()};
130 event.ts = NowMicros();
131 event.tid = ThreadId();
132 event.scope = "t";
133 event_queue_.enqueue(std::move(event));
134 MaybeFlush();
135}
136
138 InstantEvent event;
139 event.name = std::string{name.View()};
140 event.ts = NowMicros();
141 event.tid = ThreadId();
142 event.scope = "t";
143 event_queue_.enqueue(std::move(event));
144 MaybeFlush();
145}
146
147void FlamegraphBackend::Message(std::string_view text,
148 uint32_t /*color*/) noexcept {
149 InstantEvent event;
150 event.name = std::string{text};
151 event.ts = NowMicros();
152 event.tid = ThreadId();
153 event.scope = "t";
154 event_queue_.enqueue(std::move(event));
155 MaybeFlush();
156}
157
158void FlamegraphBackend::Plot(CStringView name, double value) noexcept {
159 CounterEvent event;
160 event.name = std::string{name.View()};
161 event.ts = NowMicros();
162 event.value = value;
163 event_queue_.enqueue(std::move(event));
164 MaybeFlush();
165}
166
167uint64_t FlamegraphBackend::NowMicros() noexcept {
168 const auto now = std::chrono::high_resolution_clock::now();
169 return static_cast<uint64_t>(
170 std::chrono::time_point_cast<std::chrono::microseconds>(now)
171 .time_since_epoch()
172 .count());
173}
174
175uint64_t FlamegraphBackend::ThreadId() noexcept {
176 return std::hash<std::thread::id>{}(std::this_thread::get_id());
177}
178
179void FlamegraphBackend::WriteJsonString(std::ostream& os,
180 std::string_view str) noexcept {
181 os << '"';
182 for (const char ch : str) {
183 switch (ch) {
184 case '"':
185 os << "\\\"";
186 break;
187 case '\\':
188 os << "\\\\";
189 break;
190 case '\b':
191 os << "\\b";
192 break;
193 case '\f':
194 os << "\\f";
195 break;
196 case '\n':
197 os << "\\n";
198 break;
199 case '\r':
200 os << "\\r";
201 break;
202 case '\t':
203 os << "\\t";
204 break;
205 default:
206 os << ch;
207 break;
208 }
209 }
210 os << '"';
211}
212
213void FlamegraphBackend::WriteEvent(const FlamegraphEvent& event) noexcept {
214 if (first_event_written_) {
215 output_file_ << ",\n";
216 }
217 first_event_written_ = true;
218
219 std::visit(
220 [this](const auto& e) {
221 using T = std::decay_t<decltype(e)>;
222
223 if constexpr (std::same_as<T, CompleteEvent>) {
224 output_file_ << " {";
225 output_file_ << R"("name":)";
226 WriteJsonString(output_file_, e.name);
227 output_file_ << R"(,"ph":"X","ts":)" << e.start_ts;
228 output_file_ << R"(,"dur":)" << (e.end_ts - e.start_ts);
229 output_file_ << R"(,"pid":0,"tid":)" << e.tid;
230
231 if (e.has_text || e.has_value) {
232 output_file_ << R"(,"args":{)";
233 bool first_arg = true;
234 if (e.has_text) {
235 output_file_ << R"("text":)";
236 WriteJsonString(output_file_, e.text);
237 first_arg = false;
238 }
239 if (e.has_value) {
240 if (!first_arg) {
241 output_file_ << ',';
242 }
243 output_file_ << R"("value":)" << e.value;
244 }
245 output_file_ << '}';
246 }
247
248 output_file_ << '}';
249 } else if constexpr (std::same_as<T, InstantEvent>) {
250 output_file_ << " {";
251 output_file_ << R"("name":)";
252 WriteJsonString(output_file_, e.name);
253 output_file_ << R"(,"ph":"i","ts":)" << e.ts;
254 output_file_ << R"(,"pid":0,"tid":)" << e.tid;
255 output_file_ << R"(,"s":")" << e.scope << '"';
256 output_file_ << '}';
257 } else if constexpr (std::same_as<T, CounterEvent>) {
258 output_file_ << " {";
259 output_file_ << R"("name":)";
260 WriteJsonString(output_file_, e.name);
261 output_file_ << R"(,"ph":"C","ts":)" << e.ts;
262 output_file_ << R"(,"pid":0,"args":{)";
263 WriteJsonString(output_file_, e.name);
264 output_file_ << ':' << e.value;
265 output_file_ << "}}";
266 }
267 },
268 event);
269}
270
271void FlamegraphBackend::FlushBatch(size_t count) noexcept {
272 if (!file_open_ || count == 0) {
273 return;
274 }
275
276 if (flush_lock_.test_and_set(std::memory_order_acquire)) {
277 return;
278 }
279
280 FlamegraphEvent event{std::in_place_type<CompleteEvent>};
281 size_t written = 0;
282 while (written < count && event_queue_.try_dequeue(event)) {
283 WriteEvent(event);
284 ++written;
285 }
286
287 flush_lock_.clear(std::memory_order_release);
288}
289
290void FlamegraphBackend::FlushAll() noexcept {
291 if (!file_open_) {
292 return;
293 }
294
295 while (flush_lock_.test_and_set(std::memory_order_acquire)) {
296 }
297
298 FlamegraphEvent event{std::in_place_type<CompleteEvent>};
299 while (event_queue_.try_dequeue(event)) {
300 WriteEvent(event);
301 }
302
303 output_file_ << "\n]\n";
304 output_file_.close();
305 file_open_ = false;
306
307 flush_lock_.clear(std::memory_order_release);
308}
309
310void FlamegraphBackend::MaybeFlush() noexcept {
311 if (config_.flush_threshold == 0 || !file_open_) {
312 return;
313 }
314
315 if (event_queue_.size_approx() >= config_.flush_threshold) {
316 FlushBatch(config_.flush_threshold);
317 }
318}
319
320} // namespace helios::profile
void ZoneText(std::span< std::byte > storage, std::string_view text) noexcept override
Attaches a text annotation to the active zone.
void Shutdown() noexcept override
Flushes all remaining events, writes the JSON array epilogue, and closes the output file.
size_t ZoneStorageSize() const noexcept override
Returns the per-zone storage size in bytes.
void FrameMark() noexcept override
Enqueues an unnamed frame-mark instant event.
void ZoneName(std::span< std::byte > storage, std::string_view name) noexcept override
Overrides the name of the active zone.
void ZoneValue(std::span< std::byte > storage, uint64_t value) noexcept override
Attaches a numeric value to the active zone.
void EndZone(std::span< std::byte > storage) noexcept override
Records the end of an instrumented zone and enqueues a completed event.
void Startup() noexcept override
Opens the output file and writes the JSON array prologue.
void BeginZone(const ZoneSpec &spec, std::span< std::byte > storage) noexcept override
Records the start of an instrumented zone.
void Plot(CStringView name, double value) noexcept override
Enqueues a counter (plot) event with the given value.
void FrameMarkStart(CStringView name) noexcept override
Enqueues a frame-region-start instant event.
void FrameMarkEnd(CStringView name) noexcept override
Enqueues a frame-region-end instant event.
void Message(std::string_view text, uint32_t color) noexcept override
Enqueues a message as an instant event.
constexpr size_t AlignUp(size_t value, size_t alignment) noexcept
constexpr size_t kZoneStorageBytes
Definition config.hpp:12
BasicCStringView< char > CStringView
A view of a null-terminated C string.
Immutable zone description for a single instrumentation site.
Definition common.hpp:22