Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
flamegraph.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <concurrentqueue/moodycamel/concurrentqueue.h>
8
9#include <atomic>
10#include <cstddef>
11#include <cstdint>
12#include <filesystem>
13#include <fstream>
14#include <ostream>
15#include <source_location>
16#include <span>
17#include <string>
18#include <string_view>
19#include <variant>
20
21namespace helios::profile {
22
23/**
24 * @brief Configuration for the FlamegraphBackend.
25 * @details Controls the output destination and flushing behavior of the
26 * Chrome Tracing JSON backend.
27 */
29 /// @brief Default number of events accumulated before flushing.
30 static constexpr size_t kDefaultFlushThreshold = 1024;
31
32 /// File path where the JSON trace is written.
33 std::filesystem::path output_path = "helios_flamegraph.json";
34
35 /// Number of events before flushing to disk.
36 /// A threshold of `0` holds all events in memory until shutdown.
38};
39
40/**
41 * @brief Profiling backend that emits a Chrome Tracing JSON file.
42 * @details Collects zone begin/end, instant, and counter events into a
43 * lock-free concurrent queue. Batched JSON output is written to disk
44 * periodically (controlled by `FlamegraphBackendConfig::flush_threshold`) or
45 * at shutdown. The resulting file is compatible with `about://tracing` in
46 * Chromium-based browsers and https://ui.perfetto.dev.
47 */
48class FlamegraphBackend final : public Backend {
49public:
50 /**
51 * @brief Constructs with a custom configuration.
52 * @param config Configuration for output path and flush threshold
53 */
54 explicit FlamegraphBackend(FlamegraphBackendConfig config = {}) noexcept
55 : config_(std::move(config)) {}
56
59 ~FlamegraphBackend() noexcept override = default;
60
61 FlamegraphBackend& operator=(const FlamegraphBackend&) = delete;
62 FlamegraphBackend& operator=(FlamegraphBackend&&) = delete;
63
64 /**
65 * @brief Returns the per-zone storage size in bytes.
66 * @return Byte count (aligned to `std::max_align_t`) needed for the internal
67 * ZoneState.
68 */
69 [[nodiscard]] size_t ZoneStorageSize() const noexcept override;
70
71 /**
72 * @brief Opens the output file and writes the JSON array prologue.
73 * @warning Not thread-safe.
74 * Call during single-threaded startup after `Finalize()`.
75 */
76 void Startup() noexcept override;
77
78 /**
79 * @brief Flushes all remaining events, writes the JSON array epilogue,
80 * and closes the output file.
81 * @warning Not thread-safe.
82 * Call during single-threaded shutdown.
83 */
84 void Shutdown() noexcept override;
85
86 /**
87 * @brief Records the start of an instrumented zone.
88 * @details Stores the zone name and start timestamp in the per-zone
89 * storage slice allocated by the Profiler.
90 * @param spec Zone specification (name, source location, etc.)
91 * @param storage Per-zone byte slice for this backend
92 */
93 void BeginZone(const ZoneSpec& spec,
94 std::span<std::byte> storage) noexcept override;
95
96 /**
97 * @brief Records the end of an instrumented zone and enqueues a
98 * completed event.
99 * @details Computes the duration, destroys the zone state, and pushes a
100 * `CompleteEvent` into the lock-free queue. May trigger a batch flush if
101 * the queue size exceeds `flush_threshold`.
102 * @param storage Per-zone byte slice for this backend
103 */
104 void EndZone(std::span<std::byte> storage) noexcept override;
105
106 /**
107 * @brief Attaches a text annotation to the active zone.
108 * @param storage Per-zone byte slice for this backend
109 * @param text Text to attach
110 */
111 void ZoneText(std::span<std::byte> storage,
112 std::string_view text) noexcept override;
113
114 /**
115 * @brief Attaches a numeric value to the active zone.
116 * @param storage Per-zone byte slice for this backend
117 * @param value Numeric value to attach
118 */
119 void ZoneValue(std::span<std::byte> storage,
120 uint64_t value) noexcept override;
121
122 /**
123 * @brief Overrides the name of the active zone.
124 * @details The new name is used in the emitted JSON event.
125 * @param storage Per-zone byte slice for this backend
126 * @param name New zone name
127 */
128 void ZoneName(std::span<std::byte> storage,
129 std::string_view name) noexcept override;
130
131 /// @brief Enqueues an unnamed frame-mark instant event.
132 void FrameMark() noexcept override;
133
134 /**
135 * @brief Enqueues a named frame-mark instant event.
136 * @param name Frame marker name
137 */
138 void FrameMark(CStringView name) noexcept override;
139
140 /**
141 * @brief Enqueues a frame-region-start instant event.
142 * @param name Frame region name
143 */
144 void FrameMarkStart(CStringView name) noexcept override;
145
146 /**
147 * @brief Enqueues a frame-region-end instant event.
148 * @param name Frame region name
149 */
150 void FrameMarkEnd(CStringView name) noexcept override;
151
152 /**
153 * @brief Enqueues a message as an instant event.
154 * @param text Message text
155 * @param color Display color (unused in JSON output)
156 */
157 void Message(std::string_view text, uint32_t color) noexcept override;
158
159 /**
160 * @brief Thread name events are not emitted in the JSON format.
161 * @param name Thread name
162 */
163 void SetThreadName(CStringView /*name*/) noexcept override {}
164
165 /**
166 * @brief Enqueues a counter (plot) event with the given value.
167 * @details Emitted as a Chrome Tracing counter event (`ph: "C"`).
168 * @param name Counter name
169 * @param value Numeric value
170 */
171 void Plot(CStringView name, double value) noexcept override;
172
173 /**
174 * @brief Plot configuration is not represented in the JSON format.
175 * @param name Counter name
176 * @param type Plot format type
177 * @param step Whether to use step interpolation
178 * @param fill Whether to fill the area
179 * @param color Display color
180 */
181 void PlotConfig(CStringView /*name*/, PlotFormat /*type*/, bool /*step*/,
182 bool /*fill*/, uint32_t /*color*/) noexcept override {}
183
184 /**
185 * @brief Memory allocation events are not emitted in the JSON format.
186 * @param ptr Pointer to allocated memory
187 * @param size Allocation size in bytes
188 * @param name Optional pool name
189 * @param depth Callstack depth
190 * @param loc Source location of the allocation
191 */
192 void Alloc(const void* /*ptr*/, size_t /*size*/,
193 std::optional<CStringView> /*name*/, int /*depth*/,
194 std::source_location /*loc*/) noexcept override {}
195
196 /**
197 * @brief Memory deallocation events are not emitted in the JSON format.
198 * @param ptr Pointer to freed memory
199 * @param name Optional pool name
200 * @param depth Callstack depth
201 * @param loc Source location of the deallocation
202 */
203 void Free(const void* /*ptr*/, std::optional<CStringView> /*name*/,
204 int /*depth*/, std::source_location /*loc*/) noexcept override {}
205
206 /**
207 * @brief Memory discard events are not emitted in the JSON format.
208 * @param name Pool name
209 */
210 void MemoryDiscard(CStringView /*name*/) noexcept override {}
211
212 /**
213 * @brief Memory discard events are not emitted in the JSON format.
214 * @param name Pool name
215 * @param depth Callstack depth
216 */
217 void MemoryDiscard(CStringView /*name*/, int /*depth*/) noexcept override {}
218
219 /**
220 * @brief Returns the backend identifier.
221 * @return `"flamegraph"`
222 */
223 [[nodiscard]] std::string_view Name() const noexcept override {
224 return "flamegraph";
225 }
226
227private:
228 /**
229 * @brief Per-zone state stored in the zone storage slice.
230 * @details Lives on the stack inside `ScopedZone::storage_` and is
231 * placement-constructed / destroyed by `BeginZone` / `EndZone`.
232 */
233 struct ZoneState {
234 std::string_view name;
235 uint64_t start_ts = 0;
236 std::string_view text;
237 uint64_t value = 0;
238 bool has_value = false;
239 bool has_text = false;
240 };
241
242 /**
243 * @brief A completed zone event ready for JSON emission.
244 * @details Emitted as a Chrome Tracing complete event (`ph: "X"`) with
245 * duration computed from `end_ts - start_ts`.
246 */
247 struct CompleteEvent {
248 std::string name;
249 uint64_t start_ts = 0;
250 uint64_t end_ts = 0;
251 uint64_t tid = 0;
252 std::string text;
253 uint64_t value = 0;
254 bool has_value = false;
255 bool has_text = false;
256 };
257
258 /**
259 * @brief An instant event (frame marks, messages).
260 * @details Emitted as a Chrome Tracing instant event (`ph: "i"`).
261 */
262 struct InstantEvent {
263 std::string name;
264 uint64_t ts = 0;
265 uint64_t tid = 0;
266 std::string scope;
267 };
268
269 /**
270 * @brief A counter event (plot samples).
271 * @details Emitted as a Chrome Tracing counter event (`ph: "C"`).
272 */
273 struct CounterEvent {
274 std::string name;
275 uint64_t ts = 0;
276 double value = 0.0;
277 };
278
279 /// @brief Variant covering all event types stored in the lock-free queue.
280 using FlamegraphEvent =
281 std::variant<CompleteEvent, InstantEvent, CounterEvent>;
282
283 /// @brief Lock-free concurrent queue type.
284 using EventQueue = moodycamel::ConcurrentQueue<FlamegraphEvent>;
285
286 /**
287 * @brief Reinterprets a zone storage slice as ZoneState.
288 * @param storage Byte span from the profiler
289 * @return Pointer to the zone state
290 */
291 [[nodiscard]] static ZoneState* AsZoneState(
292 std::span<std::byte> storage) noexcept {
293 return std::launder(reinterpret_cast<ZoneState*>(storage.data()));
294 }
295
296 /**
297 * @brief Returns the current time in microseconds since epoch.
298 * @return Microsecond timestamp
299 */
300 [[nodiscard]] static uint64_t NowMicros() noexcept;
301
302 /**
303 * @brief Returns a numeric hash of the current thread id.
304 * @return Thread identifier for JSON `tid` field
305 */
306 [[nodiscard]] static uint64_t ThreadId() noexcept;
307
308 /**
309 * @brief Writes a JSON-escaped string to the output stream.
310 * @param os Output stream
311 * @param str Raw string to escape and write
312 */
313 static void WriteJsonString(std::ostream& os, std::string_view str) noexcept;
314
315 /**
316 * @brief Writes a single event to the output file as a JSON object.
317 * @param event Event to write
318 */
319 void WriteEvent(const FlamegraphEvent& event) noexcept;
320
321 /**
322 * @brief Dequeues and writes up to `count` events from the queue.
323 * @details Acquires the flush lock to prevent concurrent writes to the
324 * output file. If another thread is already flushing, returns
325 * immediately.
326 * @param count Maximum number of events to flush
327 */
328 void FlushBatch(size_t count) noexcept;
329
330 /**
331 * @brief Dequeues and writes all remaining events, then writes the JSON
332 * array epilogue and closes the file.
333 * @details Spins on the flush lock to ensure all concurrent flushes
334 * complete before draining the queue. Only called from `Shutdown()`.
335 */
336 void FlushAll() noexcept;
337
338 /**
339 * @brief Called after each event enqueue to check if a flush is needed.
340 * @details If `flush_threshold_ > 0` and the approximate queue size
341 * reaches the threshold, triggers a batch flush.
342 */
343 void MaybeFlush() noexcept;
344
345 FlamegraphBackendConfig config_;
346 EventQueue event_queue_;
347 std::ofstream output_file_;
348 std::atomic_flag flush_lock_ = ATOMIC_FLAG_INIT;
349 bool first_event_written_ = false;
350 bool file_open_ = false;
351 bool started_ = false;
352};
353
354} // namespace helios::profile
Abstract profiler backend interface.
Definition backend.hpp:30
~FlamegraphBackend() noexcept override=default
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.
FlamegraphBackend(const FlamegraphBackend &)=delete
void Alloc(const void *, size_t, std::optional< CStringView >, int, std::source_location) noexcept override
Memory allocation events are not emitted in the JSON format.
void ZoneName(std::span< std::byte > storage, std::string_view name) noexcept override
Overrides the name of the active zone.
FlamegraphBackend(FlamegraphBackendConfig config={}) noexcept
Constructs with a custom configuration.
void Free(const void *, std::optional< CStringView >, int, std::source_location) noexcept override
Memory deallocation events are not emitted in the JSON format.
void MemoryDiscard(CStringView, int) noexcept override
Memory discard events are not emitted in the JSON format.
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 PlotConfig(CStringView, PlotFormat, bool, bool, uint32_t) noexcept override
Plot configuration is not represented in the JSON format.
void MemoryDiscard(CStringView) noexcept override
Memory discard events are not emitted in the JSON format.
std::string_view Name() const noexcept override
Returns the backend identifier.
void SetThreadName(CStringView) noexcept override
Thread name events are not emitted in the JSON format.
void BeginZone(const ZoneSpec &spec, std::span< std::byte > storage) noexcept override
Records the start of an instrumented zone.
FlamegraphBackend(FlamegraphBackend &&)=delete
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.
PlotFormat
Plot value format for timeline plots.
Definition common.hpp:10
BasicCStringView< char > CStringView
A view of a null-terminated C string.
STL namespace.
Configuration for the FlamegraphBackend.
std::filesystem::path output_path
File path where the JSON trace is written.
static constexpr size_t kDefaultFlushThreshold
Default number of events accumulated before flushing.
Immutable zone description for a single instrumentation site.
Definition common.hpp:22