5#include <dotnetpp/dotnetpp.h>
18constexpr const char* main_thread_lane_name =
"Main Thread";
19constexpr const char* frame_loop_scope_name =
"Frame Loop";
21struct main_thread_busy_metrics
27auto find_main_thread_snapshot(
const frame_snapshot& snap) ->
const frame_snapshot::thread_snapshot*
29 for(
const auto& ts : snap.threads)
31 if(ts.name == main_thread_lane_name)
36 for(
const auto& ts : snap.threads)
38 for(
const auto& ev : ts.events)
40 if(ev.depth == 0 && std::strcmp(ev.name(), frame_loop_scope_name) == 0)
46 if(!snap.threads.empty())
48 return &snap.threads.front();
54auto compute_main_thread_busy_metrics(
const frame_snapshot& snap) -> main_thread_busy_metrics
56 const float wall_ms = snap.frame_wall_ms;
61 const frame_snapshot::thread_snapshot* main_ts = find_main_thread_snapshot(snap);
62 if(main_ts ==
nullptr)
64 return {wall_ms, 1.0f};
66 const profile_event* chosen =
nullptr;
67 int64_t best_wall_ns = -1;
68 for(
const auto& ev : main_ts->events)
70 if(ev.depth != 0 || ev.end_ns <= ev.start_ns)
74 if(std::strcmp(ev.name(), frame_loop_scope_name) == 0)
79 const int64_t wall_ns = ev.end_ns - ev.start_ns;
80 if(wall_ns > best_wall_ns)
82 best_wall_ns = wall_ns;
88 return {wall_ms, 1.0f};
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,
113 if(thread_name !=
nullptr && thread_name[0] !=
'\0')
115 return profiler->register_thread(thread_name);
118 std::ostringstream oss;
119 oss <<
"Thread-" << std::this_thread::get_id();
120 return profiler->register_thread(oss.str());
123auto performance_profiler::register_thread_unlocked(
const std::string&
name) -> thread_profile_data*
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;
130 auto* raw_ptr = data.get();
132 threads_.push_back({
name, std::move(data)});
138 std::lock_guard lock(registration_mutex_);
139 return register_thread_unlocked(
name);
149 const int64_t dur_ns =
static_cast<int64_t
>(time_ms * 1'000'000.0f);
151 const int64_t start_ns = end_ns - dur_ns;
154 if(idx == UINT32_MAX)
167 auto& ev = data->write_buffer().events[idx];
168 ev.start_ns = start_ns;
170 ev.cpu_start_ns = start_ns;
171 ev.cpu_end_ns = end_ns;
176 prev_frame_start_ns_ = frame_start_ns_;
179 for(
auto& [n, data] : aggregate_data_)
186 std::lock_guard lock(registration_mutex_);
187 for(
auto& thread : threads_)
194 std::lock_guard lock(registration_mutex_);
195 for(
const auto& thread : threads_)
197 auto& buf = thread.data->read_buffer();
198 for(uint32_t i = 0; i < buf.count; ++i)
200 auto& ev = buf.events[i];
201 if(ev.end_ns <= ev.start_ns)
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())
211 it->second.add_sample(ms);
217 aggregate_data_.emplace(std::string(ev.name()), pfd);
225 capture_frame_snapshot();
231void performance_profiler::capture_frame_snapshot()
237 std::lock_guard lock(registration_mutex_);
238 snapshot.
threads.reserve(threads_.size());
240 int64_t emin = INT64_MAX;
241 int64_t emax = INT64_MIN;
243 for(
const auto& thread : threads_)
245 auto& buf = thread.data->read_buffer();
251 frame_snapshot::thread_snapshot ts;
252 ts.thread_index = buf.thread_index;
253 ts.name = thread.name;
254 ts.events.reserve(buf.count);
256 for(uint32_t i = 0; i < buf.count; ++i)
258 const auto& src = buf.events[i];
259 ts.events.push_back(src);
261 if(src.end_ns > src.start_ns)
263 emin = std::min(emin, src.start_ns);
264 emax = std::max(emax, src.end_ns);
268 snapshot.
threads.push_back(std::move(ts));
271 snapshot.
event_min_ns = (emin <= emax) ? emin : snapshot.frame_start_ns;
272 snapshot.
event_max_ns = (emin <= emax) ? emax : snapshot.frame_end_ns;
291 const main_thread_busy_metrics main_busy = compute_main_thread_busy_metrics(snapshot);
295 if(frame_history_.size() < max_frame_history_)
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_;
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_;
311 return aggregate_data_;
321 return prev_frame_start_ns_;
326 return prev_frame_end_ns_;
329void performance_profiler::sync_capture_active_to_threads()
341 recording_state_ = state;
342 std::lock_guard lock(registration_mutex_);
343 sync_capture_active_to_threads();
348 return recording_state_;
353 return history_count_;
358 if(
index >= history_count_)
363 if(history_count_ < max_frame_history_)
365 return &frame_history_[
index];
368 uint32_t actual = (history_write_idx_ +
index) % max_frame_history_;
369 return &frame_history_[actual];
374 selected_frame_ =
index;
379 return selected_frame_;
384 return max_frame_history_;
394 const uint32_t keep = std::min(history_count_,
capacity);
395 const uint32_t
start = history_count_ - keep;
396 std::vector<frame_snapshot> kept;
398 for(uint32_t i = 0; i < keep; ++i)
403 kept.push_back(*snap);
406 frame_history_ = std::move(kept);
407 history_count_ =
static_cast<uint32_t
>(frame_history_.size());
409 if(history_count_ >= max_frame_history_)
411 history_write_idx_ = 0;
415 history_write_idx_ = history_count_;
417 if(selected_frame_ >= 0)
419 selected_frame_ = std::min(selected_frame_,
static_cast<int32_t
>(history_count_) - 1);
425 frame_history_.clear();
426 history_write_idx_ = 0;
428 selected_frame_ = -1;
const stats * get_stats()
Hash specialization for batch_key to enable use in std::unordered_map.
auto get_app_profiler() -> performance_profiler *
void profiler_process_capture_gate_store(uint8_t v)
void profile_end(uint32_t idx)
End a profiling scope started by profile_begin().
auto get_time_ns() -> int64_t
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...
auto get_thread_profile_data() -> thread_profile_data *
auto profile_begin_owned(hpp::string_view name) -> uint32_t
Like profile_begin() but copies the label into name_owned (script / dynamic names).
std::vector< math::vec3 > start
Compacted copy of one frame's profiling data across all threads.
float frame_cpu_ratio
frame_busy_ms / frame_wall_ms — fraction of the frame the main thread was running.
float frame_busy_ms
Main-thread busy time in milliseconds (Frame Loop CPU, or longest main root).
std::vector< thread_snapshot > threads
int64_t gpu_memory_used_bytes
Total GPU memory reported by bgfx at capture; bytes.
int64_t cpu_heap_used_bytes
Managed heap used (e.g. Mono GC) sampled at capture; bytes.
int64_t process_resident_bytes
Process resident set (RSS / working set) at capture; bytes.
float frame_wall_ms
Wall-clock frame duration in milliseconds (cached at capture for UI).
Double-buffered per-thread profiling state. One buffer is being written by the owning thread while th...