diff options
| author | hachem <im@hachem.wtf> | 2026-09-01 16:58:48 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-01 16:58:48 +0200 |
| commit | e7e423c25754b0161c0d0d0273b03a57de6b8922 (patch) | |
| tree | eeccda3d6d20f397e5097566af4aa2dfeb40d8c2 | |
| parent | 37098d4db1195d3f18f396d839b4eda6942b1f8c (diff) | |
feat: attempt to add a profiler...didn't work
| -rw-r--r-- | src/debugger/debugger.cpp | 181 | ||||
| -rw-r--r-- | src/debugger/debugger.h | 26 | ||||
| -rw-r--r-- | src/debugger/types.h | 19 | ||||
| -rw-r--r-- | src/ui/profiler.cpp | 82 | ||||
| -rw-r--r-- | src/ui/profiler.h | 61 | ||||
| -rw-r--r-- | src/ui/ui.cpp | 127 | ||||
| -rw-r--r-- | src/ui/ui.h | 5 |
7 files changed, 501 insertions, 0 deletions
diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp index 97af0e1..392dc19 100644 --- a/src/debugger/debugger.cpp +++ b/src/debugger/debugger.cpp @@ -15,6 +15,9 @@ // no standard way to read or write the environment #include <cstdlib> +// resident memory of the debugged process, read straight from the kernel +#include <libproc.h> + namespace Hsdbg { namespace @@ -23,6 +26,10 @@ namespace Hsdbg #define HSDBG_LLVM_PREFIX "" #endif + // how many individual calls a single trace keeps before it stops growing; + // the counters keep climbing, only the per-call history is bounded + constexpr size_t MAX_TRACE_CALLS = 200000; + auto set_environment(const char* name, const std::string& value) -> void { #ifdef HSDBG_WINDOWS @@ -735,6 +742,14 @@ namespace Hsdbg m_process_id = process.GetProcessID(); m_stop_reason = StopReason::None; + // each run is timed from its own start, so drop whatever the last run left + m_trace_epoch = std::chrono::steady_clock::now(); + for (FunctionTrace& trace : m_traces) + { + trace.call_count = 0; + trace.calls.clear(); + } + Log::info("debugger: launched '{}' as pid {}", m_target_path.filename().string(), m_process_id); @@ -1247,6 +1262,165 @@ namespace Hsdbg { pump_events(); sync_breakpoints(); + sample_process_stats(); + } + + auto Debugger::sample_process_stats() -> void + { + if (!is_alive(m_session->process)) + { + m_resident_memory = 0; + return; + } + + rusage_info_v2 usage{}; + if (proc_pid_rusage(static_cast<int>(m_process_id), RUSAGE_INFO_V2, + reinterpret_cast<rusage_info_t*>(&usage)) == 0) + { + m_resident_memory = usage.ri_resident_size; + } + } + + auto Debugger::trace_now() const -> double + { + const auto elapsed = std::chrono::steady_clock::now() - m_trace_epoch; + return std::chrono::duration<double>(elapsed).count(); + } + + auto Debugger::add_trace(std::string_view function) -> uint32_t + { + const auto existing = std::ranges::find_if(m_traces, [&](const FunctionTrace& candidate) + { + return candidate.function == function; + }); + + if (existing != m_traces.end()) + return existing->id; + + FunctionTrace trace; + trace.id = m_next_trace_id++; + trace.function = function; + + m_traces.push_back(std::move(trace)); + resolve_trace(m_traces.back()); + + Log::info("debugger: tracing {}()", m_traces.back().function); + + return m_traces.back().id; + } + + auto Debugger::resolve_trace(FunctionTrace& trace) -> void + { + if (!m_session->target.IsValid()) + return; + + // scoped to the executable so a bare name does not also catch the same + // symbol pulled in from a shared library + lldb::SBBreakpoint created = m_session->target.BreakpointCreateByName( + trace.function.c_str(), m_session->target.GetExecutable().GetFilename()); + + if (!created.IsValid() || created.GetNumLocations() == 0) + { + Log::warn("debugger: could not trace {}()", trace.function); + return; + } + + created.SetEnabled(true); + trace.entry_backend_id = created.GetID(); + + Log::debug("debugger: trace on {}() ({} locations)", + trace.function, created.GetNumLocations()); + } + + auto Debugger::remove_trace(uint32_t id) -> bool + { + const auto entry = std::ranges::find(m_traces, id, &FunctionTrace::id); + if (entry == m_traces.end()) + return false; + + if (entry->entry_backend_id != 0 && m_session->target.IsValid()) + m_session->target.BreakpointDelete(entry->entry_backend_id); + + m_traces.erase(entry); + return true; + } + + auto Debugger::clear_traces() -> void + { + if (m_session->target.IsValid()) + { + for (const FunctionTrace& trace : m_traces) + { + if (trace.entry_backend_id != 0) + m_session->target.BreakpointDelete(trace.entry_backend_id); + } + } + + m_traces.clear(); + } + + auto Debugger::handle_trace_stop() -> bool + { + if (m_traces.empty()) + return false; + + bool any_trace = false; + bool any_other = false; + + const uint32_t thread_count = m_session->process.GetNumThreads(); + + for (uint32_t index = 0; index < thread_count; ++index) + { + lldb::SBThread thread = m_session->process.GetThreadAtIndex(index); + if (!thread.IsValid()) + continue; + + const lldb::StopReason reason = thread.GetStopReason(); + + if (reason == lldb::eStopReasonBreakpoint) + { + // the stop reason carries (breakpoint id, location id) pairs + const size_t pairs = thread.GetStopReasonDataCount() / 2; + + for (size_t pair = 0; pair < pairs; ++pair) + { + const auto backend_id = + static_cast<int32_t>(thread.GetStopReasonDataAtIndex(pair * 2)); + + const auto trace = std::ranges::find(m_traces, backend_id, + &FunctionTrace::entry_backend_id); + + if (trace != m_traces.end() && backend_id != 0) + { + trace->call_count += 1; + + if (trace->calls.size() < MAX_TRACE_CALLS) + trace->calls.push_back(TraceCall{ trace_now(), 0.0 }); + + any_trace = true; + } + else + { + // a real breakpoint the user set + any_other = true; + } + } + } + else if (reason != lldb::eStopReasonNone && reason != lldb::eStopReasonInvalid) + { + // a signal, exception or completed step: a genuine stop + any_other = true; + } + } + + // only slip the process back into motion when nothing but tracing happened + if (any_trace && !any_other) + { + m_session->process.Continue(); + return true; + } + + return false; } auto Debugger::require_stopped() const -> Result<void> @@ -1320,7 +1494,14 @@ namespace Hsdbg // else is lldb repeating one that was already handled case lldb::eStateStopped: if (m_state != TargetState::Stopped) + { + // a trace-only stop records the call and resumes without + // ever surfacing to the user as a stop + if (handle_trace_stop()) + break; + on_stopped(); + } break; diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h index e2277df..7faa3a1 100644 --- a/src/debugger/debugger.h +++ b/src/debugger/debugger.h @@ -3,6 +3,7 @@ #include "core/result.h" #include "debugger/types.h" +#include <chrono> #include <cstdint> #include <filesystem> #include <memory> @@ -52,6 +53,13 @@ namespace Hsdbg auto find_breakpoint(uint32_t id) -> Breakpoint*; auto breakpoints() const -> std::span<const Breakpoint> { return m_breakpoints; } + // function tracing: time how long each call of a named function takes by + // recording entry and return without stopping the ui + auto add_trace(std::string_view function) -> uint32_t; + auto remove_trace(uint32_t id) -> bool; + auto clear_traces() -> void; + auto traces() const -> std::span<const FunctionTrace> { return m_traces; } + // inspection auto threads() const -> std::span<const Thread> { return m_threads; } auto call_stack() const -> std::span<const StackFrame> { return m_call_stack; } @@ -65,6 +73,10 @@ namespace Hsdbg auto read_memory(uint64_t address, size_t size) -> Result<std::vector<uint8_t>>; auto console_output() const -> std::span<const std::string> { return m_console_output; } + // resident set size of the debugged process in bytes, refreshed once a + // frame while a target is alive and zero otherwise + auto resident_memory() const -> uint64_t { return m_resident_memory; } + // selection, what the ui is currently looking at auto select_thread(uint64_t thread_id) -> bool; auto select_frame(uint32_t frame_index) -> bool; @@ -110,6 +122,14 @@ namespace Hsdbg auto refresh_source_files() -> void; auto refresh_disassembly() -> void; auto load_disassembly(uint64_t file_address) -> void; + auto sample_process_stats() -> void; + + auto resolve_trace(FunctionTrace& trace) -> void; + + // called for a stop that trace breakpoints took part in; returns true when + // the stop was purely for tracing and the process was resumed + auto handle_trace_stop() -> bool; + auto trace_now() const -> double; std::unique_ptr<Session> m_session; @@ -122,6 +142,9 @@ namespace Hsdbg std::vector<Breakpoint> m_breakpoints; uint32_t m_next_breakpoint_id = 1; + std::vector<FunctionTrace> m_traces; + uint32_t m_next_trace_id = 1; + std::vector<Thread> m_threads; std::vector<StackFrame> m_call_stack; std::vector<Variable> m_locals; @@ -136,5 +159,8 @@ namespace Hsdbg uint32_t m_selected_frame = 0; uint64_t m_selected_symbol = 0; uint64_t m_stop_count = 0; + uint64_t m_resident_memory = 0; + + std::chrono::steady_clock::time_point m_trace_epoch = std::chrono::steady_clock::now(); }; } diff --git a/src/debugger/types.h b/src/debugger/types.h index e288f85..cda3dbd 100644 --- a/src/debugger/types.h +++ b/src/debugger/types.h @@ -158,4 +158,23 @@ namespace Hsdbg uint64_t address = 0; uint64_t size = 0; }; + + // one recorded activation of a traced function: when it was entered and how + // long it took. duration stays zero until the matching return is seen + struct TraceCall + { + double start = 0.0; // seconds since tracing began + double duration = 0.0; // seconds spent inside the call + }; + + // a function the user asked to time. the debugger sets an internal breakpoint + // on it that records each call without stopping the ui + struct FunctionTrace + { + uint32_t id = 0; + std::string function; + int32_t entry_backend_id = 0; + uint64_t call_count = 0; + std::vector<TraceCall> calls; + }; } diff --git a/src/ui/profiler.cpp b/src/ui/profiler.cpp new file mode 100644 index 0000000..df22a20 --- /dev/null +++ b/src/ui/profiler.cpp @@ -0,0 +1,82 @@ +#include "ui/profiler.h" + +namespace Hsdbg +{ + auto TimeSeries::push(float value) -> void + { + m_values[m_write] = value; + m_write = (m_write + 1) % CAPACITY; + + if (m_count < CAPACITY) + m_count += 1; + } + + auto TimeSeries::clear() -> void + { + m_values.fill(0.0f); + m_count = 0; + m_write = 0; + } + + auto TimeSeries::latest() const -> float + { + if (m_count == 0) + return 0.0f; + + const int last = (m_write - 1 + CAPACITY) % CAPACITY; + return m_values[last]; + } + + auto TimeSeries::average() const -> float + { + if (m_count == 0) + return 0.0f; + + float sum = 0.0f; + for (int i = 0; i < m_count; ++i) + sum += m_values[i]; + + return sum / static_cast<float>(m_count); + } + + auto TimeSeries::maximum() const -> float + { + float peak = 0.0f; + for (int i = 0; i < m_count; ++i) + { + if (m_values[i] > peak) + peak = m_values[i]; + } + + return peak; + } + + auto Profiler::sample_frame(float delta_seconds) -> void + { + if (m_paused) + return; + + const float frame_ms = delta_seconds * 1000.0f; + m_frame_ms.push(frame_ms); + m_fps.push(delta_seconds > 0.0f ? 1.0f / delta_seconds : 0.0f); + } + + auto Profiler::sample_target(uint64_t resident_bytes, bool alive) -> void + { + if (m_paused) + return; + + const float megabytes = alive + ? static_cast<float>(resident_bytes) / (1024.0f * 1024.0f) + : 0.0f; + + m_target_memory_mb.push(megabytes); + } + + auto Profiler::reset() -> void + { + m_frame_ms.clear(); + m_fps.clear(); + m_target_memory_mb.clear(); + } +} diff --git a/src/ui/profiler.h b/src/ui/profiler.h new file mode 100644 index 0000000..0ceb740 --- /dev/null +++ b/src/ui/profiler.h @@ -0,0 +1,61 @@ +#pragma once + +#include <array> +#include <cstdint> + +namespace Hsdbg +{ + // a small fixed-size ring buffer of floats, sized for a live scrolling + // graph; new samples overwrite the oldest once it fills up + class TimeSeries + { + public: + // roughly four seconds of history at 60 fps + static constexpr int CAPACITY = 240; + + auto push(float value) -> void; + auto clear() -> void; + + auto values() const -> const float* { return m_values.data(); } + auto count() const -> int { return m_count; } + + // where the oldest sample sits, so ImGui::PlotLines can unwrap the ring + auto offset() const -> int { return m_count == CAPACITY ? m_write : 0; } + + auto latest() const -> float; + auto average() const -> float; + auto maximum() const -> float; + + private: + std::array<float, CAPACITY> m_values{}; + int m_count = 0; + int m_write = 0; + }; + + // collects live performance samples for the profiler panel. this first pass + // only tracks the debugger's own frame timing; sampling the debugged target + // comes in a later step + class Profiler + { + public: + auto sample_frame(float delta_seconds) -> void; + + // resident set size of the target in bytes; alive says whether a process + // is actually running so the graph can hold flat while nothing is loaded + auto sample_target(uint64_t resident_bytes, bool alive) -> void; + auto reset() -> void; + + auto frame_times() const -> const TimeSeries& { return m_frame_ms; } + auto frame_rates() const -> const TimeSeries& { return m_fps; } + auto target_memory_mb() const -> const TimeSeries& { return m_target_memory_mb; } + + auto paused() const -> bool { return m_paused; } + auto set_paused(bool paused) -> void { m_paused = paused; } + + private: + TimeSeries m_frame_ms; + TimeSeries m_fps; + TimeSeries m_target_memory_mb; + bool m_paused = false; + }; +} diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 040f27b..e1a2f9d 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -13,6 +13,7 @@ #include <algorithm> #include <cctype> +#include <cfloat> #include <filesystem> #include <format> #include <span> @@ -36,6 +37,7 @@ namespace Hsdbg constexpr const char* PANEL_SYMBOLS = "symbols"; constexpr const char* PANEL_DISASSEMBLY = "disassembly"; constexpr const char* PANEL_CONSOLE = "console"; + constexpr const char* PANEL_PROFILER = "profiler"; constexpr const char* LOAD_TARGET_POPUP = "load target"; @@ -563,6 +565,9 @@ namespace Hsdbg { apply_preferences(); + m_profiler.sample_frame(ImGui::GetIO().DeltaTime); + m_profiler.sample_target(debugger.resident_memory(), debugger.has_target()); + const ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); @@ -618,6 +623,7 @@ namespace Hsdbg draw_symbols_panel(debugger); draw_disassembly_panel(debugger); draw_console_panel(debugger); + draw_profiler_panel(debugger); if (m_visible.demo) ImGui::ShowDemoWindow(&m_visible.demo); @@ -680,6 +686,7 @@ namespace Hsdbg ImGui::DockBuilderDockWindow(PANEL_REGISTERS, right_bottom_id); ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id); ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id); + ImGui::DockBuilderDockWindow(PANEL_PROFILER, bottom_id); ImGui::DockBuilderFinish(dockspace_id); } @@ -761,6 +768,7 @@ namespace Hsdbg ImGui::MenuItem(PANEL_SYMBOLS, nullptr, &m_visible.symbols); ImGui::MenuItem(PANEL_DISASSEMBLY, nullptr, &m_visible.disassembly); ImGui::MenuItem(PANEL_CONSOLE, nullptr, &m_visible.console); + ImGui::MenuItem(PANEL_PROFILER, nullptr, &m_visible.profiler); ImGui::Separator(); @@ -1701,6 +1709,125 @@ namespace Hsdbg ImGui::End(); } + auto Ui::draw_profiler_panel(Debugger& debugger) -> void + { + if (!m_visible.profiler) + return; + + if (ImGui::Begin(PANEL_PROFILER, &m_visible.profiler)) + { + bool paused = m_profiler.paused(); + if (ImGui::Checkbox("pause", &paused)) + m_profiler.set_paused(paused); + + ImGui::SameLine(); + if (ImGui::Button("reset")) + m_profiler.reset(); + + ImGui::Separator(); + + if (!debugger.has_target()) + { + ImGui::TextDisabled("no target: load and run a process to profile it"); + } + else + { + const TimeSeries& memory = m_profiler.target_memory_mb(); + + const std::string memory_overlay = + std::format("{:.1f} MB (peak {:.1f})", memory.latest(), memory.maximum()); + + ImGui::TextUnformatted("resident memory"); + ImGui::PlotLines("##target_memory", memory.values(), memory.count(), memory.offset(), + memory_overlay.c_str(), 0.0f, FLT_MAX, ImVec2(-1.0f, 90.0f)); + } + + ImGui::Separator(); + ImGui::TextUnformatted("function tracing"); + + ImGui::SetNextItemWidth(-70.0f); + const bool submitted = ImGui::InputTextWithHint("##trace_input", "function to time", + &m_trace_input, + ImGuiInputTextFlags_EnterReturnsTrue); + + ImGui::SameLine(); + const bool add_clicked = ImGui::Button("trace", ImVec2(-1.0f, 0.0f)); + + if ((submitted || add_clicked) && !m_trace_input.empty()) + { + debugger.add_trace(m_trace_input); + m_trace_input.clear(); + } + + const std::span<const FunctionTrace> traces = debugger.traces(); + + if (traces.empty()) + { + ImGui::TextDisabled("no traced functions yet"); + } + else + { + constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_SizingStretchProp; + + if (ImGui::BeginTable("##traces", 3, flags)) + { + ImGui::TableSetupColumn("function"); + ImGui::TableSetupColumn("calls", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f); + ImGui::TableHeadersRow(); + + uint32_t remove_id = 0; + + for (const FunctionTrace& trace : traces) + { + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(trace.function.c_str()); + + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast<unsigned long long>(trace.call_count)); + + ImGui::TableNextColumn(); + ImGui::PushID(static_cast<int>(trace.id)); + if (ImGui::SmallButton("x")) + remove_id = trace.id; + ImGui::PopID(); + } + + ImGui::EndTable(); + + if (remove_id != 0) + debugger.remove_trace(remove_id); + } + } + + if (ImGui::CollapsingHeader("debugger self-timing")) + { + const TimeSeries& frame_ms = m_profiler.frame_times(); + const TimeSeries& fps = m_profiler.frame_rates(); + + const std::string frame_overlay = + std::format("{:.2f} ms (avg {:.2f} peak {:.2f})", + frame_ms.latest(), frame_ms.average(), frame_ms.maximum()); + + ImGui::TextUnformatted("frame time"); + ImGui::PlotLines("##frame_ms", frame_ms.values(), frame_ms.count(), frame_ms.offset(), + frame_overlay.c_str(), 0.0f, FLT_MAX, ImVec2(-1.0f, 70.0f)); + + const std::string fps_overlay = + std::format("{:.0f} fps (avg {:.0f})", fps.latest(), fps.average()); + + ImGui::TextUnformatted("frame rate"); + ImGui::PlotLines("##fps", fps.values(), fps.count(), fps.offset(), + fps_overlay.c_str(), 0.0f, FLT_MAX, ImVec2(-1.0f, 70.0f)); + } + } + + ImGui::End(); + } + auto Ui::push_console(std::string line) -> void { m_console_lines.push_back(std::move(line)); diff --git a/src/ui/ui.h b/src/ui/ui.h index 436bfc4..5730059 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -4,6 +4,7 @@ #include "ui/animated_image.h" #include "ui/image_renderer.h" #include "ui/preferences.h" +#include "ui/profiler.h" #include "ui/source_view.h" #include <cstdint> @@ -49,6 +50,7 @@ namespace Hsdbg bool symbols = true; bool disassembly = true; bool console = true; + bool profiler = true; bool demo = false; }; @@ -72,6 +74,7 @@ namespace Hsdbg auto draw_symbols_panel(Debugger& debugger) -> void; auto draw_disassembly_panel(Debugger& debugger) -> void; auto draw_console_panel(Debugger& debugger) -> void; + auto draw_profiler_panel(Debugger& debugger) -> void; auto push_console(std::string line) -> void; auto report(const Result<void>& result, std::string_view action) -> void; @@ -82,6 +85,7 @@ namespace Hsdbg Window* m_window = nullptr; SourceView m_source_view; + Profiler m_profiler; PanelVisibility m_visible; Preferences m_preferences; @@ -109,6 +113,7 @@ namespace Hsdbg std::string m_target_input; std::string m_symbol_filter; std::string m_source_filter; + std::string m_trace_input; uint64_t m_followed_stop = 0; std::filesystem::path m_followed_target; |
