aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/debugger/debugger.cpp181
-rw-r--r--src/debugger/debugger.h26
-rw-r--r--src/debugger/types.h19
-rw-r--r--src/ui/profiler.cpp82
-rw-r--r--src/ui/profiler.h61
-rw-r--r--src/ui/ui.cpp127
-rw-r--r--src/ui/ui.h5
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;