Unravel Engine C++ Reference
Loading...
Searching...
No Matches
profiler.h
Go to the documentation of this file.
1#pragma once
2#include <engine/engine_export.h>
3
4#include <chrono>
5#include <cstdint>
6#include <map>
7#include <array>
8#include <string>
9#include <vector>
10#include <memory>
11#include <mutex>
12#include <atomic>
13#include <algorithm>
14#include <type_traits>
15#include <hpp/string_view.hpp>
17
18namespace unravel
19{
20
21// ============================================================================
22// sample_data - Rolling statistics buffer
23// ============================================================================
24
26{
27public:
28 static constexpr uint32_t num_samples = 500;
29
31 {
32 reset(0.0f);
33 }
34
35 auto reset(float value) -> void
36 {
37 offset_ = 0;
38 std::fill(values_.begin(), values_.end(), value);
39 min_ = value;
40 max_ = value;
41 average_ = value;
42 sum_ = value * static_cast<float>(num_samples);
43 min_index_ = 0;
44 max_index_ = 0;
45 smart_init_samples_ = smart_init_samples_count;
46 }
47
48 auto push_sample(float value) -> void
49 {
50 if(smart_init_samples_ > 0 && offset_ > smart_init_samples_)
51 {
52 reset(value);
53 smart_init_samples_ = -1;
54 return;
55 }
56
57 const float old_value = values_[offset_];
58 const int32_t current_index = offset_;
59
60 sum_ = sum_ - old_value + value;
61 values_[offset_] = value;
62 offset_ = (offset_ + 1) % static_cast<int32_t>(num_samples);
63 average_ = sum_ / static_cast<float>(num_samples);
64
65 if(min_index_ == current_index)
66 {
67 min_index_ = 0;
68 min_ = values_[0];
69 for(int32_t i = 1; i < static_cast<int32_t>(num_samples); ++i)
70 {
71 if(values_[i] < min_)
72 {
73 min_ = values_[i];
74 min_index_ = i;
75 }
76 }
77 }
78 else if(value < min_)
79 {
80 min_ = value;
81 min_index_ = current_index;
82 }
83
84 if(max_index_ == current_index)
85 {
86 max_index_ = 0;
87 max_ = values_[0];
88 for(int32_t i = 1; i < static_cast<int32_t>(num_samples); ++i)
89 {
90 if(values_[i] > max_)
91 {
92 max_ = values_[i];
93 max_index_ = i;
94 }
95 }
96 }
97 else if(value > max_)
98 {
99 max_ = value;
100 max_index_ = current_index;
101 }
102 }
103
104 auto get_values() const -> const float* { return values_.data(); }
105 auto get_offset() const -> int32_t { return offset_; }
106 auto get_min() const -> float { return min_; }
107 auto get_max() const -> float { return max_; }
108 auto get_average() const -> float { return average_; }
109
110private:
111 static constexpr int32_t smart_init_samples_count = 20;
112
113 int32_t offset_{0};
114 std::array<float, num_samples> values_{};
115 float min_{0.0f};
116 float max_{0.0f};
117 float average_{0.0f};
118 float sum_{0.0f};
119 int32_t min_index_{0};
120 int32_t max_index_{0};
121 int32_t smart_init_samples_{-1};
122};
123
124// ============================================================================
125// Timeline profiler data structures
126// ============================================================================
127
128constexpr auto fnv1a_hash(const char* str) -> uint32_t
129{
130 uint32_t hash = 2166136261u;
131 for(; *str; ++str)
132 {
133 hash ^= static_cast<uint32_t>(*str);
134 hash *= 16777619u;
135 }
136 return hash;
137}
138
141{
143 const char* name_literal{};
144 std::string name_owned{};
145 int64_t start_ns{0};
146 int64_t end_ns{0};
147 int64_t cpu_start_ns{0};
148 int64_t cpu_end_ns{0};
149 uint16_t depth{0};
150 uint16_t thread_index{0};
151 uint32_t color_hash{0};
152
153 [[nodiscard]] auto name() const -> const char*
154 {
155 return name_literal ? name_literal : name_owned.c_str();
156 }
157};
158
161{
162 static constexpr uint32_t max_events = 32768;
163
164 std::array<profile_event, max_events> events{};
165 uint32_t count{0};
166 uint16_t depth{0};
167 uint16_t thread_index{0};
168
169 void reset()
170 {
171 count = 0;
172 depth = 0;
173 }
174};
175
180{
181 std::array<thread_profile_buffer, 2> buffers{};
182 std::atomic<uint32_t> write_idx{0};
183
184 auto write_buffer() -> thread_profile_buffer& { return buffers[write_idx.load(std::memory_order_relaxed)]; }
185 auto read_buffer() -> thread_profile_buffer& { return buffers[1 - write_idx.load(std::memory_order_acquire)]; }
186
187 void flip()
188 {
189 uint32_t old_idx = write_idx.load(std::memory_order_relaxed);
190 write_idx.store(1 - old_idx, std::memory_order_release);
191 write_buffer().reset();
192 }
193};
194
195// ============================================================================
196// Frame snapshot and recording state
197// ============================================================================
198
201{
202 int64_t frame_start_ns{0};
203 int64_t frame_end_ns{0};
204 int64_t event_min_ns{0};
205 int64_t event_max_ns{0};
213 float frame_wall_ms{0.0f};
215 float frame_busy_ms{0.0f};
217 float frame_cpu_ratio{1.0f};
218
220 {
221 uint16_t thread_index{0};
222 std::string name;
223 std::vector<profile_event> events;
224 };
225
226 std::vector<thread_snapshot> threads;
227};
228
229enum class recording_state : uint8_t
230{
231 stopped,
232 recording,
233 paused
234};
235
236// ============================================================================
237// performance_profiler
238// ============================================================================
239
241{
242public:
245 {
246 float time_since_swap = 0.0f;
247 uint32_t samples_since_swap = 0;
249
250 per_frame_data() = default;
255
256 operator float() const { return time_since_swap; }
257
258 auto operator+=(float t) -> per_frame_data&
259 {
260 time_since_swap += t;
261 return *this;
262 }
263
264 auto get_time_since_swap() const -> float { return time_since_swap; }
265 auto get_samples_since_swap() const -> uint32_t { return samples_since_swap; }
266 auto get_avg() const -> float { return history.get_average(); }
267 auto get_min() const -> float { return history.get_min(); }
268 auto get_max() const -> float { return history.get_max(); }
269 auto get_history() const -> const sample_data& { return history; }
270
271 void add_sample(float t)
272 {
273 time_since_swap += t;
275 }
276
277 void reset()
278 {
279 if(samples_since_swap > 0)
280 {
282 }
284 time_since_swap = 0.0f;
285 }
286 };
287
288 using record_data_t = std::map<std::string, per_frame_data, std::less<>>;
289
291 {
292 std::string name;
293 std::unique_ptr<thread_profile_data> data;
294 };
295
299 void add_record(hpp::string_view name, float time_ms);
300
302 void swap();
303
305 auto get_per_frame_data_read() const -> const record_data_t&;
306
309 auto register_thread(const std::string& name) -> thread_profile_data*;
310
312 auto get_threads() const -> const std::vector<thread_info>&;
313
315 auto get_frame_start_ns() const -> int64_t;
316 auto get_frame_end_ns() const -> int64_t;
317
320 auto get_recording_state() const -> recording_state;
321
323 auto get_frame_count() const -> uint32_t;
324
327 auto get_frame_snapshot(uint32_t index) const -> const frame_snapshot*;
328
330 void set_selected_frame(int32_t index);
331 auto get_selected_frame() const -> int32_t;
332
334 void clear_history();
335
337 static constexpr uint32_t max_frame_history_limit = 2000;
339 static constexpr uint32_t default_frame_history = 256;
341 static constexpr uint32_t min_frame_history = 64;
342
344 [[nodiscard]] auto get_max_frame_history() const -> uint32_t;
346 void set_max_frame_history(uint32_t capacity);
347
348private:
349 std::vector<thread_info> threads_;
350 std::mutex registration_mutex_;
351
352 record_data_t aggregate_data_;
353
354 int64_t frame_start_ns_{0};
355 int64_t frame_end_ns_{0};
356 int64_t prev_frame_start_ns_{0};
357 int64_t prev_frame_end_ns_{0};
358
359 std::vector<frame_snapshot> frame_history_;
360 uint32_t history_write_idx_{0};
361 uint32_t history_count_{0};
362 uint32_t max_frame_history_{default_frame_history};
364 int32_t selected_frame_{-1};
365
366 auto register_thread_unlocked(const std::string& name) -> thread_profile_data*;
367
369 void sync_capture_active_to_threads();
370
371 void capture_frame_snapshot();
372};
373
374// ============================================================================
375// Thread-local state and inline recording functions
376// ============================================================================
377
378inline thread_local thread_profile_data* t_profile_data = nullptr;
379
382[[nodiscard]] inline auto profiler_process_capture_gate_ref() -> std::atomic<uint8_t>&
383{
384 static std::atomic<uint8_t> process_capture_gate{0};
385 return process_capture_gate;
386}
387
388[[nodiscard]] inline auto profiler_process_capture_gate_load() -> bool
389{
390 return profiler_process_capture_gate_ref().load(std::memory_order_relaxed) != 0;
391}
392
394{
395 profiler_process_capture_gate_ref().store(v, std::memory_order_relaxed);
396}
397
398inline auto get_time_ns() -> int64_t
399{
400 using clock = std::chrono::high_resolution_clock;
401 return std::chrono::duration_cast<std::chrono::nanoseconds>(
402 clock::now().time_since_epoch())
403 .count();
404}
405
408auto ensure_thread_registered(const char* thread_name = nullptr) -> thread_profile_data*;
409
411{
412 if(!t_profile_data) [[unlikely]]
413 {
415 }
416 return t_profile_data;
417}
418
421inline auto get_thread_profile_data(const char* thread_name) -> thread_profile_data*
422{
423 if(!t_profile_data) [[unlikely]]
424 {
426 }
427 return t_profile_data;
428}
429
431inline auto profile_begin(const char* name) -> uint32_t
432{
433 if(!profiler_process_capture_gate_load()) [[likely]]
434 {
435 return UINT32_MAX;
436 }
437 auto* data = get_thread_profile_data();
438 if(!data) [[unlikely]]
439 {
440 return UINT32_MAX;
441 }
442
443 auto& buf = data->write_buffer();
444 if(buf.count >= thread_profile_buffer::max_events) [[unlikely]]
445 {
446 return UINT32_MAX;
447 }
448
449 uint32_t idx = buf.count++;
450 auto& ev = buf.events[idx];
451 ev.name_literal = name;
452 ev.name_owned.clear();
453 ev.start_ns = get_time_ns();
454 ev.cpu_start_ns = platform::get_thread_cpu_time_ns();
455 ev.end_ns = 0;
456 ev.cpu_end_ns = 0;
457 ev.depth = buf.depth++;
458 ev.thread_index = buf.thread_index;
459 ev.color_hash = fnv1a_hash(name);
460 return idx;
461}
462
465inline auto profile_begin(const char* name, const char* thread_name) -> uint32_t
466{
467 if(!profiler_process_capture_gate_load()) [[likely]]
468 {
469 return UINT32_MAX;
470 }
471 auto* data = get_thread_profile_data(thread_name);
472 if(!data) [[unlikely]]
473 {
474 return UINT32_MAX;
475 }
476
477 auto& buf = data->write_buffer();
478 if(buf.count >= thread_profile_buffer::max_events) [[unlikely]]
479 {
480 return UINT32_MAX;
481 }
482
483 uint32_t idx = buf.count++;
484 auto& ev = buf.events[idx];
485 ev.name_literal = name;
486 ev.name_owned.clear();
487 ev.start_ns = get_time_ns();
488 ev.cpu_start_ns = platform::get_thread_cpu_time_ns();
489 ev.end_ns = 0;
490 ev.cpu_end_ns = 0;
491 ev.depth = buf.depth++;
492 ev.thread_index = buf.thread_index;
493 ev.color_hash = fnv1a_hash(name);
494 return idx;
495}
496
498inline auto profile_begin_owned(hpp::string_view name) -> uint32_t
499{
500 if(!profiler_process_capture_gate_load()) [[likely]]
501 {
502 return UINT32_MAX;
503 }
504 auto* data = get_thread_profile_data();
505 if(!data) [[unlikely]]
506 {
507 return UINT32_MAX;
508 }
509
510 auto& buf = data->write_buffer();
511 if(buf.count >= thread_profile_buffer::max_events) [[unlikely]]
512 {
513 return UINT32_MAX;
514 }
515
516 uint32_t idx = buf.count++;
517 auto& ev = buf.events[idx];
518 ev.name_literal = nullptr;
519 ev.name_owned = std::string(name);
520 ev.start_ns = get_time_ns();
521 ev.cpu_start_ns = platform::get_thread_cpu_time_ns();
522 ev.end_ns = 0;
523 ev.cpu_end_ns = 0;
524 ev.depth = buf.depth++;
525 ev.thread_index = buf.thread_index;
526 ev.color_hash = fnv1a_hash(ev.name());
527 return idx;
528}
529
531inline auto profile_begin_owned(hpp::string_view name, const char* thread_name) -> uint32_t
532{
533 if(!profiler_process_capture_gate_load()) [[likely]]
534 {
535 return UINT32_MAX;
536 }
537 auto* data = get_thread_profile_data(thread_name);
538 if(!data) [[unlikely]]
539 {
540 return UINT32_MAX;
541 }
542
543 auto& buf = data->write_buffer();
544 if(buf.count >= thread_profile_buffer::max_events) [[unlikely]]
545 {
546 return UINT32_MAX;
547 }
548
549 uint32_t idx = buf.count++;
550 auto& ev = buf.events[idx];
551 ev.name_literal = nullptr;
552 ev.name_owned = std::string(name);
553 ev.start_ns = get_time_ns();
554 ev.cpu_start_ns = platform::get_thread_cpu_time_ns();
555 ev.end_ns = 0;
556 ev.cpu_end_ns = 0;
557 ev.depth = buf.depth++;
558 ev.thread_index = buf.thread_index;
559 ev.color_hash = fnv1a_hash(ev.name());
560 return idx;
561}
562
564inline void profile_end(uint32_t idx)
565{
566 if(idx == UINT32_MAX) [[likely]]
567 {
568 return;
569 }
570 auto* data = get_thread_profile_data();
571 if(!data) [[unlikely]]
572 {
573 return;
574 }
575
576 auto& buf = data->write_buffer();
577 buf.events[idx].end_ns = get_time_ns();
578 buf.events[idx].cpu_end_ns = platform::get_thread_cpu_time_ns();
579 buf.depth--;
580}
581
582// ============================================================================
583// RAII scope timer
584// ============================================================================
585
588template<typename T>
589concept string_literal = std::is_array_v<std::remove_reference_t<T>> &&
590 std::is_same_v<std::remove_const_t<std::remove_extent_t<std::remove_reference_t<T>>>, char>;
591
593{
594public:
595 template<typename T>
596 requires string_literal<T>
597 explicit scope_profile_timer(const T& name)
598 : index_(profile_begin(name))
599 {
600 }
601
603 {
604 profile_end(index_);
605 }
606
611
612private:
613 uint32_t index_;
614};
615
617{
618public:
619 explicit scope_profile_timer_owned(hpp::string_view name)
620 : index_(profile_begin_owned(name))
621 {
622 }
623
625 {
626 profile_end(index_);
627 }
628
633
634private:
635 uint32_t index_;
636};
637
639template<typename ScopeName, typename ThreadName>
642{
643public:
644 explicit scope_profile_timer_named_thread(const ScopeName& scope_name, const ThreadName& thread_name)
645 : index_(profile_begin(scope_name, thread_name))
646 {
647 }
648
653
658
659private:
660 uint32_t index_;
661};
662
663// ============================================================================
664// Global accessor and macros
665// ============================================================================
666
668
669#define APP_SCOPE_PERF_CONCATENATE_DETAIL(x, y) x##y
670#define APP_SCOPE_PERF_CONCATENATE(x, y) APP_SCOPE_PERF_CONCATENATE_DETAIL(x, y)
671#define APP_SCOPE_PERF_UNIQUE_VAR(prefix) APP_SCOPE_PERF_CONCATENATE(prefix, __LINE__)
672
675#define APP_SCOPE_PERF(name_literal) \
676 const ::unravel::scope_profile_timer APP_SCOPE_PERF_UNIQUE_VAR(timer)(name_literal)
677
680#define APP_SCOPE_PERF_OWNED(name) \
681 const ::unravel::scope_profile_timer_owned APP_SCOPE_PERF_UNIQUE_VAR(timer)(name)
682
684#define APP_SCOPE_PERF_THREAD(scope_name_literal, thread_name_literal) \
685 const ::unravel::scope_profile_timer_named_thread APP_SCOPE_PERF_UNIQUE_VAR(timer_thread)(scope_name_literal, \
686 thread_name_literal)
687
688} // namespace unravel
auto get_recording_state() const -> recording_state
Definition profiler.cpp:346
auto get_threads() const -> const std::vector< thread_info > &
All registered threads and their profiling data.
Definition profiler.cpp:314
auto get_frame_end_ns() const -> int64_t
Definition profiler.cpp:324
void swap()
End-of-frame: flip buffers, compute aggregates, advance history.
Definition profiler.cpp:174
static constexpr uint32_t default_frame_history
Default ring-buffer capacity (keeps histogram/UI cost bounded).
Definition profiler.h:339
static constexpr uint32_t min_frame_history
Minimum allowed capacity.
Definition profiler.h:341
auto get_max_frame_history() const -> uint32_t
Current frame-history ring capacity.
Definition profiler.cpp:382
auto get_per_frame_data_read() const -> const record_data_t &
Aggregate per-name data for the statistics panel.
Definition profiler.cpp:309
auto get_frame_start_ns() const -> int64_t
Frame boundary timestamps for the last completed frame.
Definition profiler.cpp:319
auto get_frame_count() const -> uint32_t
Number of captured frame snapshots available.
Definition profiler.cpp:351
void set_max_frame_history(uint32_t capacity)
Resize history capacity, keeping the newest frames when shrinking.
Definition profiler.cpp:387
auto register_thread(const std::string &name) -> thread_profile_data *
Register the calling thread for timeline profiling.
Definition profiler.cpp:136
std::map< std::string, per_frame_data, std::less<> > record_data_t
Definition profiler.h:288
void add_record(hpp::string_view name, float time_ms)
Script / external pre-measured span on the current thread: same as profile_begin_owned + profile_end,...
Definition profiler.cpp:142
void set_selected_frame(int32_t index)
Selected frame index for UI inspection (-1 = live/latest).
Definition profiler.cpp:372
void set_recording_state(recording_state state)
Recording control.
Definition profiler.cpp:335
static constexpr uint32_t max_frame_history_limit
Absolute upper bound for set_max_frame_history.
Definition profiler.h:337
auto get_frame_snapshot(uint32_t index) const -> const frame_snapshot *
Access a captured frame snapshot.
Definition profiler.cpp:356
auto get_selected_frame() const -> int32_t
Definition profiler.cpp:377
void clear_history()
Clear all captured frame history.
Definition profiler.cpp:423
auto get_offset() const -> int32_t
Definition profiler.h:105
static constexpr uint32_t num_samples
Definition profiler.h:28
auto reset(float value) -> void
Definition profiler.h:35
auto get_min() const -> float
Definition profiler.h:106
auto get_max() const -> float
Definition profiler.h:107
auto get_values() const -> const float *
Definition profiler.h:104
auto get_average() const -> float
Definition profiler.h:108
auto push_sample(float value) -> void
Definition profiler.h:48
Like scope_profile_timer but registers the current OS thread with ThreadName on first use.
Definition profiler.h:642
scope_profile_timer_named_thread(scope_profile_timer_named_thread &&)=delete
auto operator=(const scope_profile_timer_named_thread &) -> scope_profile_timer_named_thread &=delete
scope_profile_timer_named_thread(const ScopeName &scope_name, const ThreadName &thread_name)
Definition profiler.h:644
scope_profile_timer_named_thread(const scope_profile_timer_named_thread &)=delete
auto operator=(scope_profile_timer_named_thread &&) -> scope_profile_timer_named_thread &=delete
auto operator=(scope_profile_timer_owned &&) -> scope_profile_timer_owned &=delete
scope_profile_timer_owned(hpp::string_view name)
Definition profiler.h:619
auto operator=(const scope_profile_timer_owned &) -> scope_profile_timer_owned &=delete
scope_profile_timer_owned(scope_profile_timer_owned &&)=delete
scope_profile_timer_owned(const scope_profile_timer_owned &)=delete
auto operator=(scope_profile_timer &&) -> scope_profile_timer &=delete
auto operator=(const scope_profile_timer &) -> scope_profile_timer &=delete
scope_profile_timer(const T &name)
Definition profiler.h:597
scope_profile_timer(scope_profile_timer &&)=delete
scope_profile_timer(const scope_profile_timer &)=delete
Concept to ensure only string literals (char arrays) are accepted. With const T& parameter binding,...
Definition profiler.h:589
uint16_t index
std::string name
Definition hub.cpp:33
auto get_thread_cpu_time_ns() -> int64_t
Definition thread.hpp:164
Hash specialization for batch_key to enable use in std::unordered_map.
auto profile_begin(const char *name) -> uint32_t
Begin a profiling scope. Returns an event index for profile_end().
Definition profiler.h:431
auto get_app_profiler() -> performance_profiler *
Definition profiler.cpp:99
void profiler_process_capture_gate_store(uint8_t v)
Definition profiler.h:393
void profile_end(uint32_t idx)
End a profiling scope started by profile_begin().
Definition profiler.h:564
auto profiler_process_capture_gate_load() -> bool
Definition profiler.h:388
recording_state
Definition profiler.h:230
auto get_time_ns() -> int64_t
Definition profiler.h:398
auto ensure_thread_registered(const char *thread_name) -> thread_profile_data *
Register the current thread on first use. thread_name if non-null and non-empty is stored as the lane...
Definition profiler.cpp:105
auto get_thread_profile_data() -> thread_profile_data *
Definition profiler.h:410
constexpr auto fnv1a_hash(const char *str) -> uint32_t
Definition profiler.h:128
auto profiler_process_capture_gate_ref() -> std::atomic< uint8_t > &
Definition profiler.h:382
thread_local thread_profile_data * t_profile_data
Definition profiler.h:378
auto profile_begin_owned(hpp::string_view name) -> uint32_t
Like profile_begin() but copies the label into name_owned (script / dynamic names).
Definition profiler.h:498
uint32_t capacity
std::vector< profile_event > events
Definition profiler.h:223
Compacted copy of one frame's profiling data across all threads.
Definition profiler.h:201
float frame_cpu_ratio
frame_busy_ms / frame_wall_ms — fraction of the frame the main thread was running.
Definition profiler.h:217
float frame_busy_ms
Main-thread busy time in milliseconds (Frame Loop CPU, or longest main root).
Definition profiler.h:215
std::vector< thread_snapshot > threads
Definition profiler.h:226
int64_t gpu_memory_used_bytes
Total GPU memory reported by bgfx at capture; bytes.
Definition profiler.h:209
int64_t cpu_heap_used_bytes
Managed heap used (e.g. Mono GC) sampled at capture; bytes.
Definition profiler.h:207
int64_t process_resident_bytes
Process resident set (RSS / working set) at capture; bytes.
Definition profiler.h:211
float frame_wall_ms
Wall-clock frame duration in milliseconds (cached at capture for UI).
Definition profiler.h:213
Per-name aggregate timing data with rolling history.
Definition profiler.h:245
auto get_samples_since_swap() const -> uint32_t
Definition profiler.h:265
auto operator+=(float t) -> per_frame_data &
Definition profiler.h:258
auto get_history() const -> const sample_data &
Definition profiler.h:269
std::unique_ptr< thread_profile_data > data
Definition profiler.h:293
A single profiling event captured during a frame.
Definition profiler.h:141
std::string name_owned
Definition profiler.h:144
const char * name_literal
String literal from native scopes; null if the label is stored in name_owned.
Definition profiler.h:143
auto name() const -> const char *
Definition profiler.h:153
Fixed-size buffer of profile events for one frame on one thread.
Definition profiler.h:161
static constexpr uint32_t max_events
Definition profiler.h:162
Double-buffered per-thread profiling state. One buffer is being written by the owning thread while th...
Definition profiler.h:180
std::atomic< uint32_t > write_idx
Definition profiler.h:182
auto read_buffer() -> thread_profile_buffer &
Definition profiler.h:185
std::array< thread_profile_buffer, 2 > buffers
Definition profiler.h:181
auto write_buffer() -> thread_profile_buffer &
Definition profiler.h:184