Unravel Engine C++ Reference
Loading...
Searching...
No Matches
profiler.cpp
Go to the documentation of this file.
1#include "profiler.h"
2
4#include <graphics/graphics.h>
5#include <dotnetpp/dotnetpp.h>
6
7#include <algorithm>
8#include <cstring>
9#include <sstream>
10#include <thread>
11
12namespace unravel
13{
14
15namespace
16{
17
18constexpr const char* main_thread_lane_name = "Main Thread";
19constexpr const char* frame_loop_scope_name = "Frame Loop";
20
21struct main_thread_busy_metrics
22{
23 float busy_ms{0.0f};
24 float busy_ratio{1.0f};
25};
26
27auto find_main_thread_snapshot(const frame_snapshot& snap) -> const frame_snapshot::thread_snapshot*
28{
29 for(const auto& ts : snap.threads)
30 {
31 if(ts.name == main_thread_lane_name)
32 {
33 return &ts;
34 }
35 }
36 for(const auto& ts : snap.threads)
37 {
38 for(const auto& ev : ts.events)
39 {
40 if(ev.depth == 0 && std::strcmp(ev.name(), frame_loop_scope_name) == 0)
41 {
42 return &ts;
43 }
44 }
45 }
46 if(!snap.threads.empty())
47 {
48 return &snap.threads.front();
49 }
50 return nullptr;
51}
52
54auto compute_main_thread_busy_metrics(const frame_snapshot& snap) -> main_thread_busy_metrics
55{
56 const float wall_ms = snap.frame_wall_ms;
57 if(wall_ms <= 0.0f)
58 {
59 return {};
60 }
61 const frame_snapshot::thread_snapshot* main_ts = find_main_thread_snapshot(snap);
62 if(main_ts == nullptr)
63 {
64 return {wall_ms, 1.0f};
65 }
66 const profile_event* chosen = nullptr;
67 int64_t best_wall_ns = -1;
68 for(const auto& ev : main_ts->events)
69 {
70 if(ev.depth != 0 || ev.end_ns <= ev.start_ns)
71 {
72 continue;
73 }
74 if(std::strcmp(ev.name(), frame_loop_scope_name) == 0)
75 {
76 chosen = &ev;
77 break;
78 }
79 const int64_t wall_ns = ev.end_ns - ev.start_ns;
80 if(wall_ns > best_wall_ns)
81 {
82 best_wall_ns = wall_ns;
83 chosen = &ev;
84 }
85 }
86 if(chosen == nullptr)
87 {
88 return {wall_ms, 1.0f};
89 }
90 const float busy_ms = std::clamp(
91 static_cast<float>(std::max<int64_t>(0, chosen->cpu_end_ns - chosen->cpu_start_ns)) / 1'000'000.0f,
92 0.0f,
93 wall_ms);
94 return {busy_ms, busy_ms / wall_ms};
95}
96
97} // namespace
98
100{
101 static performance_profiler profiler;
102 return &profiler;
103}
104
105auto ensure_thread_registered(const char* thread_name) -> thread_profile_data*
106{
107 auto* profiler = get_app_profiler();
108 if(!profiler)
109 {
110 return nullptr;
111 }
112
113 if(thread_name != nullptr && thread_name[0] != '\0')
114 {
115 return profiler->register_thread(thread_name);
116 }
117
118 std::ostringstream oss;
119 oss << "Thread-" << std::this_thread::get_id();
120 return profiler->register_thread(oss.str());
121}
122
123auto performance_profiler::register_thread_unlocked(const std::string& name) -> thread_profile_data*
124{
125 auto thread_index = static_cast<uint16_t>(threads_.size());
126 auto data = std::make_unique<thread_profile_data>();
127 data->buffers[0].thread_index = thread_index;
128 data->buffers[1].thread_index = thread_index;
129
130 auto* raw_ptr = data.get();
131
132 threads_.push_back({name, std::move(data)});
133 return raw_ptr;
134}
135
137{
138 std::lock_guard lock(registration_mutex_);
139 return register_thread_unlocked(name);
140}
141
142void performance_profiler::add_record(hpp::string_view name, float time_ms)
143{
144 if(time_ms < 0.0f)
145 {
146 return;
147 }
148
149 const int64_t dur_ns = static_cast<int64_t>(time_ms * 1'000'000.0f);
150 const int64_t end_ns = get_time_ns();
151 const int64_t start_ns = end_ns - dur_ns;
152
153 const uint32_t idx = profile_begin_owned(name);
154 if(idx == UINT32_MAX)
155 {
156 return;
157 }
158
159 profile_end(idx);
160
161 auto* data = get_thread_profile_data();
162 if(!data)
163 {
164 return;
165 }
166
167 auto& ev = data->write_buffer().events[idx];
168 ev.start_ns = start_ns;
169 ev.end_ns = end_ns;
170 ev.cpu_start_ns = start_ns;
171 ev.cpu_end_ns = end_ns;
172}
173
175{
176 prev_frame_start_ns_ = frame_start_ns_;
177 prev_frame_end_ns_ = get_time_ns();
178
179 for(auto& [n, data] : aggregate_data_)
180 {
181 (void)n;
182 data.reset();
183 }
184
185 {
186 std::lock_guard lock(registration_mutex_);
187 for(auto& thread : threads_)
188 {
189 thread.data->flip();
190 }
191 }
192
193 {
194 std::lock_guard lock(registration_mutex_);
195 for(const auto& thread : threads_)
196 {
197 auto& buf = thread.data->read_buffer();
198 for(uint32_t i = 0; i < buf.count; ++i)
199 {
200 auto& ev = buf.events[i];
201 if(ev.end_ns <= ev.start_ns)
202 {
203 continue;
204 }
205
206 float ms = static_cast<float>(ev.end_ns - ev.start_ns) / 1'000'000.0f;
207 auto name_view = hpp::string_view(ev.name());
208 auto it = aggregate_data_.find(name_view);
209 if(it != aggregate_data_.end())
210 {
211 it->second.add_sample(ms);
212 }
213 else
214 {
215 per_frame_data pfd;
216 pfd.add_sample(ms);
217 aggregate_data_.emplace(std::string(ev.name()), pfd);
218 }
219 }
220 }
221 }
222
223 if(recording_state_ == recording_state::recording)
224 {
225 capture_frame_snapshot();
226 }
227
228 frame_start_ns_ = get_time_ns();
229}
230
231void performance_profiler::capture_frame_snapshot()
232{
233 frame_snapshot snapshot;
234 snapshot.frame_start_ns = prev_frame_start_ns_;
235 snapshot.frame_end_ns = prev_frame_end_ns_;
236
237 std::lock_guard lock(registration_mutex_);
238 snapshot.threads.reserve(threads_.size());
239
240 int64_t emin = INT64_MAX;
241 int64_t emax = INT64_MIN;
242
243 for(const auto& thread : threads_)
244 {
245 auto& buf = thread.data->read_buffer();
246 if(buf.count == 0)
247 {
248 continue;
249 }
250
251 frame_snapshot::thread_snapshot ts;
252 ts.thread_index = buf.thread_index;
253 ts.name = thread.name;
254 ts.events.reserve(buf.count);
255
256 for(uint32_t i = 0; i < buf.count; ++i)
257 {
258 const auto& src = buf.events[i];
259 ts.events.push_back(src);
260
261 if(src.end_ns > src.start_ns)
262 {
263 emin = std::min(emin, src.start_ns);
264 emax = std::max(emax, src.end_ns);
265 }
266 }
267
268 snapshot.threads.push_back(std::move(ts));
269 }
270
271 snapshot.event_min_ns = (emin <= emax) ? emin : snapshot.frame_start_ns;
272 snapshot.event_max_ns = (emin <= emax) ? emax : snapshot.frame_end_ns;
273
274 snapshot.cpu_heap_used_bytes = dotnet::gc_get_used_size();
276 if(const auto* st = gfx::get_stats())
277 {
278 snapshot.gpu_memory_used_bytes = st->gpuMemoryUsed;
279 }
280
281 if(snapshot.frame_start_ns <= 0 && emin <= emax)
282 {
283 snapshot.frame_start_ns = emin;
284 }
285
286 if(snapshot.frame_end_ns > snapshot.frame_start_ns)
287 {
288 snapshot.frame_wall_ms =
289 static_cast<float>(snapshot.frame_end_ns - snapshot.frame_start_ns) / 1'000'000.0f;
290 }
291 const main_thread_busy_metrics main_busy = compute_main_thread_busy_metrics(snapshot);
292 snapshot.frame_busy_ms = main_busy.busy_ms;
293 snapshot.frame_cpu_ratio = main_busy.busy_ratio;
294
295 if(frame_history_.size() < max_frame_history_)
296 {
297 frame_history_.push_back(std::move(snapshot));
298 history_count_ = static_cast<uint32_t>(frame_history_.size());
299 history_write_idx_ = history_count_ % max_frame_history_;
300 }
301 else
302 {
303 frame_history_[history_write_idx_] = std::move(snapshot);
304 history_write_idx_ = (history_write_idx_ + 1) % max_frame_history_;
305 history_count_ = max_frame_history_;
306 }
307}
308
310{
311 return aggregate_data_;
312}
313
314auto performance_profiler::get_threads() const -> const std::vector<thread_info>&
315{
316 return threads_;
317}
318
320{
321 return prev_frame_start_ns_;
322}
323
325{
326 return prev_frame_end_ns_;
327}
328
329void performance_profiler::sync_capture_active_to_threads()
330{
331 const uint8_t v = (recording_state_ == recording_state::recording) ? 1u : 0u;
333}
334
336{
337 if(state == recording_state::stopped)
338 {
340 }
341 recording_state_ = state;
342 std::lock_guard lock(registration_mutex_);
343 sync_capture_active_to_threads();
344}
345
347{
348 return recording_state_;
349}
350
352{
353 return history_count_;
354}
355
357{
358 if(index >= history_count_)
359 {
360 return nullptr;
361 }
362
363 if(history_count_ < max_frame_history_)
364 {
365 return &frame_history_[index];
366 }
367
368 uint32_t actual = (history_write_idx_ + index) % max_frame_history_;
369 return &frame_history_[actual];
370}
371
373{
374 selected_frame_ = index;
375}
376
378{
379 return selected_frame_;
380}
381
383{
384 return max_frame_history_;
385}
386
388{
390 if(capacity == max_frame_history_)
391 {
392 return;
393 }
394 const uint32_t keep = std::min(history_count_, capacity);
395 const uint32_t start = history_count_ - keep;
396 std::vector<frame_snapshot> kept;
397 kept.reserve(keep);
398 for(uint32_t i = 0; i < keep; ++i)
399 {
400 const frame_snapshot* snap = get_frame_snapshot(start + i);
401 if(snap != nullptr)
402 {
403 kept.push_back(*snap);
404 }
405 }
406 frame_history_ = std::move(kept);
407 history_count_ = static_cast<uint32_t>(frame_history_.size());
408 max_frame_history_ = capacity;
409 if(history_count_ >= max_frame_history_)
410 {
411 history_write_idx_ = 0;
412 }
413 else
414 {
415 history_write_idx_ = history_count_;
416 }
417 if(selected_frame_ >= 0)
418 {
419 selected_frame_ = std::min(selected_frame_, static_cast<int32_t>(history_count_) - 1);
420 }
421}
422
424{
425 frame_history_.clear();
426 history_write_idx_ = 0;
427 history_count_ = 0;
428 selected_frame_ = -1;
429}
430
431} // 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 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
uint16_t index
std::string name
Definition hub.cpp:33
const stats * get_stats()
Definition graphics.cpp:450
auto get_process_resident_set_bytes() -> int64_t
Hash specialization for batch_key to enable use in std::unordered_map.
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
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
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< math::vec3 > start
float busy_ms
Definition profiler.cpp:23
float busy_ratio
Definition profiler.cpp:24
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
Double-buffered per-thread profiling state. One buffer is being written by the owning thread while th...
Definition profiler.h:180