aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/hsdbg_trace/hsdbg_trace.c68
-rw-r--r--ext/hsdbg_trace/hsdbg_trace.h51
-rw-r--r--src/debugger/debugger.cpp519
-rw-r--r--src/debugger/debugger.h97
-rw-r--r--src/debugger/types.h21
-rw-r--r--src/ui/macro_expander.cpp847
-rw-r--r--src/ui/macro_expander.h100
-rw-r--r--src/ui/source_view.cpp141
-rw-r--r--src/ui/source_view.h28
-rw-r--r--src/ui/ui.cpp286
-rw-r--r--src/ui/ui.h11
11 files changed, 2132 insertions, 37 deletions
diff --git a/ext/hsdbg_trace/hsdbg_trace.c b/ext/hsdbg_trace/hsdbg_trace.c
new file mode 100644
index 0000000..cf87ffc
--- /dev/null
+++ b/ext/hsdbg_trace/hsdbg_trace.c
@@ -0,0 +1,68 @@
+#include "hsdbg_trace.h"
+
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+// the hooks and their helpers must never instrument themselves, or every record
+// would recurse into another record
+#define NO_INSTR __attribute__((no_instrument_function))
+
+struct HsdbgTraceRecord hsdbg_trace_records[HSDBG_TRACE_CAPACITY];
+const uint64_t hsdbg_trace_capacity = HSDBG_TRACE_CAPACITY;
+uint64_t hsdbg_trace_head = 0;
+
+// slots are hedged out with a private atomic so concurrent threads never claim
+// the same one; hsdbg_trace_head is the separate, reader-facing high-water mark
+static _Atomic uint64_t g_next = 0;
+
+static NO_INSTR uint64_t now_ns(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
+}
+
+static NO_INSTR uint64_t current_thread(void)
+{
+ uint64_t id = 0;
+ pthread_threadid_np(NULL, &id);
+ return id;
+}
+
+static NO_INSTR void record(uint32_t kind, void* function)
+{
+ const uint64_t slot = atomic_fetch_add_explicit(&g_next, 1, memory_order_relaxed);
+
+ struct HsdbgTraceRecord* entry = &hsdbg_trace_records[slot % HSDBG_TRACE_CAPACITY];
+ entry->timestamp_ns = now_ns();
+ entry->function = (uint64_t)(uintptr_t)function;
+ entry->thread_id = current_thread();
+ entry->kind = kind;
+ entry->reserved = 0;
+
+ // publish only once the record is complete, so a reader that sees the new
+ // head always finds fully written fields
+ atomic_store_explicit((_Atomic uint64_t*)&hsdbg_trace_head, slot + 1, memory_order_release);
+}
+
+NO_INSTR void __cyg_profile_func_enter(void* this_fn, void* call_site)
+{
+ (void)call_site;
+ record(HSDBG_TRACE_ENTER, this_fn);
+}
+
+NO_INSTR void __cyg_profile_func_exit(void* this_fn, void* call_site)
+{
+ (void)call_site;
+ record(HSDBG_TRACE_EXIT, this_fn);
+}
+
+// opt-in sanity check when running the target on its own, outside hsdbg
+__attribute__((destructor)) NO_INSTR static void hsdbg_trace_report(void)
+{
+ if (getenv("HSDBG_TRACE_DEBUG"))
+ fprintf(stderr, "[hsdbg_trace] %llu records written\n", (unsigned long long)hsdbg_trace_head);
+}
diff --git a/ext/hsdbg_trace/hsdbg_trace.h b/ext/hsdbg_trace/hsdbg_trace.h
new file mode 100644
index 0000000..a819f9c
--- /dev/null
+++ b/ext/hsdbg_trace/hsdbg_trace.h
@@ -0,0 +1,51 @@
+// hsdbg function-trace runtime.
+//
+// Build your program with -finstrument-functions and link this in; the compiler
+// then calls the enter/exit hooks below on every function, and they append a
+// timestamped record to a fixed ring buffer. hsdbg reads that buffer straight
+// out of the live process and turns it into the flame-chart timeline, so no
+// breakpoints and no hand-picking of functions are needed.
+//
+// clang -finstrument-functions -g your_prog.c hsdbg_trace.c -o your_prog
+//
+// The record layout and the three exported symbols are the contract hsdbg reads
+// against; keep them in sync with the reader on the debugger side.
+#pragma once
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ enum
+ {
+ HSDBG_TRACE_ENTER = 0,
+ HSDBG_TRACE_EXIT = 1,
+
+ // 1,048,576 records ~= 32 MB; plenty for a short profiled run
+ HSDBG_TRACE_CAPACITY = 1u << 20,
+ };
+
+ // 32 bytes, naturally aligned so hsdbg can read the array with one memory
+ // fetch and cast without worrying about padding differences
+ struct HsdbgTraceRecord
+ {
+ uint64_t timestamp_ns; // CLOCK_MONOTONIC nanoseconds
+ uint64_t function; // address of the entered/exited function
+ uint64_t thread_id; // pthread_threadid_np of the running thread
+ uint32_t kind; // HSDBG_TRACE_ENTER or HSDBG_TRACE_EXIT
+ uint32_t reserved;
+ };
+
+ // the ring buffer and how far it has been filled. hsdbg reads hsdbg_trace_head
+ // first, then the records below it; head is published only after a record's
+ // fields are fully written
+ extern struct HsdbgTraceRecord hsdbg_trace_records[HSDBG_TRACE_CAPACITY];
+ extern const uint64_t hsdbg_trace_capacity;
+ extern uint64_t hsdbg_trace_head; // total records ever written (monotonic)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp
index 392dc19..d5dccee 100644
--- a/src/debugger/debugger.cpp
+++ b/src/debugger/debugger.cpp
@@ -742,11 +742,33 @@ 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
+ // each run is timed from its own start, so drop whatever the last run left,
+ // including return breakpoints whose addresses die with the old process
m_trace_epoch = std::chrono::steady_clock::now();
+ clear_return_breakpoints();
+ m_timeline.clear();
+
+ // instrumentation lives in the process, so a fresh run re-resolves it and
+ // starts its record count and per-thread stacks over
+ m_instr_checked = false;
+ m_instr_available = false;
+ m_instr_read_count = 0;
+ m_instr_base_set = false;
+ m_instr_functions.clear();
+ m_instr_names.clear();
+ m_instr_stacks.clear();
+
+ m_sample_pending = false;
+ m_sample_functions.clear();
+ m_sample_stacks.clear();
+
for (FunctionTrace& trace : m_traces)
{
trace.call_count = 0;
+ trace.completed_count = 0;
+ trace.total_time = 0.0;
+ trace.min_time = 0.0;
+ trace.max_time = 0.0;
trace.calls.clear();
}
@@ -1263,6 +1285,7 @@ namespace Hsdbg
pump_events();
sync_breakpoints();
sample_process_stats();
+ maybe_request_sample();
}
auto Debugger::sample_process_stats() -> void
@@ -1359,6 +1382,34 @@ namespace Hsdbg
m_traces.clear();
}
+ auto Debugger::ensure_return_breakpoint(uint64_t address) -> int32_t
+ {
+ if (const auto entry = m_return_breakpoints.find(address); entry != m_return_breakpoints.end())
+ return entry->second;
+
+ lldb::SBBreakpoint created = m_session->target.BreakpointCreateByAddress(address);
+ if (!created.IsValid())
+ return 0;
+
+ created.SetEnabled(true);
+
+ const int32_t id = created.GetID();
+ m_return_breakpoints.emplace(address, id);
+ return id;
+ }
+
+ auto Debugger::clear_return_breakpoints() -> void
+ {
+ if (m_session->target.IsValid())
+ {
+ for (const auto& [address, id] : m_return_breakpoints)
+ m_session->target.BreakpointDelete(id);
+ }
+
+ m_return_breakpoints.clear();
+ m_pending_calls.clear();
+ }
+
auto Debugger::handle_trace_stop() -> bool
{
if (m_traces.empty())
@@ -1377,39 +1428,44 @@ namespace Hsdbg
const lldb::StopReason reason = thread.GetStopReason();
- if (reason == lldb::eStopReasonBreakpoint)
+ if (reason != lldb::eStopReasonBreakpoint)
{
- // the stop reason carries (breakpoint id, location id) pairs
- const size_t pairs = thread.GetStopReasonDataCount() / 2;
+ // a signal, exception or completed step: a genuine stop
+ if (reason != lldb::eStopReasonNone && reason != lldb::eStopReasonInvalid)
+ any_other = true;
- for (size_t pair = 0; pair < pairs; ++pair)
- {
- const auto backend_id =
- static_cast<int32_t>(thread.GetStopReasonDataAtIndex(pair * 2));
+ continue;
+ }
- const auto trace = std::ranges::find(m_traces, backend_id,
- &FunctionTrace::entry_backend_id);
+ // the stop reason carries (breakpoint id, location id) pairs
+ const size_t pairs = thread.GetStopReasonDataCount() / 2;
- if (trace != m_traces.end() && backend_id != 0)
- {
- trace->call_count += 1;
+ for (size_t pair = 0; pair < pairs; ++pair)
+ {
+ const auto backend_id =
+ static_cast<int32_t>(thread.GetStopReasonDataAtIndex(pair * 2));
- if (trace->calls.size() < MAX_TRACE_CALLS)
- trace->calls.push_back(TraceCall{ trace_now(), 0.0 });
+ const auto trace = std::ranges::find(m_traces, backend_id,
+ &FunctionTrace::entry_backend_id);
- any_trace = true;
- }
- else
- {
- // a real breakpoint the user set
- any_other = true;
- }
+ const bool is_return = std::ranges::any_of(m_return_breakpoints,
+ [&](const auto& kv) { return kv.second == backend_id; });
+
+ if (trace != m_traces.end() && backend_id != 0)
+ {
+ record_trace_entry(*trace, thread);
+ any_trace = true;
+ }
+ else if (is_return)
+ {
+ record_trace_return(thread);
+ 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;
}
}
@@ -1423,6 +1479,402 @@ namespace Hsdbg
return false;
}
+ auto Debugger::record_trace_entry(FunctionTrace& trace, lldb::SBThread& thread) -> void
+ {
+ const double start = trace_now();
+
+ trace.call_count += 1;
+
+ size_t call_index = MAX_TRACE_CALLS;
+ if (trace.calls.size() < MAX_TRACE_CALLS)
+ {
+ call_index = trace.calls.size();
+ trace.calls.push_back(TraceCall{ start, 0.0 });
+ }
+
+ // the breakpoint sits past the prologue, so frame 1 is a settled caller
+ // whose pc is the address this call will return to
+ lldb::SBFrame caller = thread.GetFrameAtIndex(1);
+ if (!caller.IsValid())
+ return;
+
+ const uint64_t thread_id = thread.GetThreadID();
+
+ // nesting is how many traced calls are already open on this thread; the
+ // outermost one sits on row zero and children stack above it
+ const auto depth = static_cast<uint32_t>(std::ranges::count(
+ m_pending_calls, thread_id, &PendingCall::thread_id));
+
+ size_t span_index = MAX_TRACE_CALLS;
+ if (m_timeline.size() < MAX_TRACE_CALLS)
+ {
+ span_index = m_timeline.size();
+ m_timeline.push_back(TimelineSpan{ trace.id, thread_id, start, 0.0, depth });
+ }
+
+ PendingCall pending;
+ pending.trace_id = trace.id;
+ pending.thread_id = thread_id;
+ pending.return_pc = caller.GetPC();
+ pending.frame_sp = caller.GetSP();
+ pending.start = start;
+ pending.call_index = call_index;
+ pending.span_index = span_index;
+
+ m_pending_calls.push_back(pending);
+ ensure_return_breakpoint(pending.return_pc);
+ }
+
+ auto Debugger::record_trace_return(lldb::SBThread& thread) -> void
+ {
+ lldb::SBFrame frame = thread.GetFrameAtIndex(0);
+ if (!frame.IsValid())
+ return;
+
+ const uint64_t pc = frame.GetPC();
+ const uint64_t sp = frame.GetSP();
+ const uint64_t thread_id = thread.GetThreadID();
+ const double now = trace_now();
+
+ // walk newest first so a recursive call matches its own activation
+ for (auto entry = m_pending_calls.rbegin(); entry != m_pending_calls.rend(); ++entry)
+ {
+ if (entry->thread_id != thread_id || entry->return_pc != pc || entry->frame_sp != sp)
+ continue;
+
+ const double duration = now - entry->start;
+
+ if (const auto trace = std::ranges::find(m_traces, entry->trace_id, &FunctionTrace::id);
+ trace != m_traces.end())
+ {
+ if (entry->call_index < trace->calls.size())
+ trace->calls[entry->call_index].duration = duration;
+
+ if (entry->span_index < m_timeline.size())
+ m_timeline[entry->span_index].duration = duration;
+
+ trace->completed_count += 1;
+ trace->total_time += duration;
+ trace->max_time = std::max(trace->max_time, duration);
+ trace->min_time = trace->completed_count == 1 ? duration
+ : std::min(trace->min_time, duration);
+ }
+
+ m_pending_calls.erase(std::next(entry).base());
+ break;
+ }
+ }
+
+ // mirrors HsdbgTraceRecord in ext/hsdbg_trace/hsdbg_trace.h; that header is the
+ // source of truth for this 32-byte layout
+ struct Debugger::InstrRecord
+ {
+ uint64_t timestamp_ns;
+ uint64_t function;
+ uint64_t thread_id;
+ uint32_t kind;
+ uint32_t reserved;
+ };
+
+ auto Debugger::symbol_load_address(const char* name) -> uint64_t
+ {
+ lldb::SBSymbolContextList list = m_session->target.FindSymbols(name);
+
+ for (uint32_t index = 0; index < list.GetSize(); ++index)
+ {
+ lldb::SBSymbol symbol = list.GetContextAtIndex(index).GetSymbol();
+ if (!symbol.IsValid())
+ continue;
+
+ const uint64_t load = symbol.GetStartAddress().GetLoadAddress(m_session->target);
+ if (load != LLDB_INVALID_ADDRESS && load != 0)
+ return load;
+ }
+
+ return 0;
+ }
+
+ auto Debugger::resolve_instrumentation() -> void
+ {
+ const uint64_t head = symbol_load_address("hsdbg_trace_head");
+ const uint64_t records = symbol_load_address("hsdbg_trace_records");
+ const uint64_t capacity_addr = symbol_load_address("hsdbg_trace_capacity");
+
+ if (head == 0 || records == 0 || capacity_addr == 0)
+ {
+ // the target simply was not built with the trace runtime
+ m_instr_checked = true;
+ m_instr_available = false;
+ return;
+ }
+
+ lldb::SBError error;
+ uint64_t capacity = 0;
+ m_session->process.ReadMemory(capacity_addr, &capacity, sizeof(capacity), error);
+
+ if (!error.Success() || capacity == 0)
+ return; // memory not readable yet, try again on the next stop
+
+ m_instr_head_addr = head;
+ m_instr_records_addr = records;
+ m_instr_capacity = capacity;
+ m_instr_available = true;
+ m_instr_checked = true;
+
+ Log::info("debugger: function instrumentation active ({} record buffer)", capacity);
+ }
+
+ auto Debugger::read_instrumentation() -> void
+ {
+ if (!is_alive(m_session->process))
+ return;
+
+ if (!m_instr_checked)
+ resolve_instrumentation();
+
+ if (!m_instr_available)
+ return;
+
+ lldb::SBError error;
+ uint64_t head = 0;
+ m_session->process.ReadMemory(m_instr_head_addr, &head, sizeof(head), error);
+
+ if (!error.Success() || head <= m_instr_read_count)
+ return;
+
+ const uint64_t capacity = m_instr_capacity;
+
+ // if we fell further behind than the ring holds, only the newest survive
+ uint64_t start = m_instr_read_count;
+ if (head - start > capacity)
+ start = head - capacity;
+
+ const uint64_t total = head - start;
+ std::vector<InstrRecord> buffer(total);
+
+ // the live records wrap around the ring, so read up to two flat chunks
+ uint64_t read = 0;
+ while (read < total)
+ {
+ const uint64_t position = (start + read) % capacity;
+ const uint64_t chunk = std::min(total - read, capacity - position);
+ const uint64_t address = m_instr_records_addr + position * sizeof(InstrRecord);
+
+ lldb::SBError chunk_error;
+ const size_t got = m_session->process.ReadMemory(
+ address, buffer.data() + read, chunk * sizeof(InstrRecord), chunk_error);
+
+ if (!chunk_error.Success() || got != chunk * sizeof(InstrRecord))
+ return; // leave m_instr_read_count untouched and retry next stop
+
+ read += chunk;
+ }
+
+ for (const InstrRecord& record : buffer)
+ apply_instr_record(record);
+
+ m_instr_read_count = head;
+ }
+
+ auto Debugger::apply_instr_record(const InstrRecord& record) -> void
+ {
+ if (!m_instr_base_set)
+ {
+ m_instr_base_ns = record.timestamp_ns;
+ m_instr_base_set = true;
+ }
+
+ const double start = static_cast<double>(record.timestamp_ns - m_instr_base_ns) / 1.0e9;
+ const uint32_t id = intern_instr_function(record.function);
+
+ std::vector<InstrOpenCall>& stack = m_instr_stacks[record.thread_id];
+
+ if (record.kind == 0 /* enter */)
+ {
+ const auto depth = static_cast<uint32_t>(stack.size());
+
+ size_t span_index = m_timeline.size();
+ if (m_timeline.size() < MAX_TRACE_CALLS)
+ m_timeline.push_back(TimelineSpan{ id, record.thread_id, start, 0.0, depth });
+
+ stack.push_back(InstrOpenCall{ id, record.timestamp_ns, span_index });
+ }
+ else if (!stack.empty())
+ {
+ const InstrOpenCall open = stack.back();
+ stack.pop_back();
+
+ const double duration =
+ static_cast<double>(record.timestamp_ns - open.start_ns) / 1.0e9;
+
+ if (open.span_index < m_timeline.size())
+ m_timeline[open.span_index].duration = duration;
+ }
+ }
+
+ auto Debugger::intern_instr_function(uint64_t address) -> uint32_t
+ {
+ if (const auto entry = m_instr_functions.find(address); entry != m_instr_functions.end())
+ return entry->second;
+
+ std::string name;
+ lldb::SBAddress resolved = m_session->target.ResolveLoadAddress(address);
+
+ if (resolved.IsValid())
+ {
+ if (lldb::SBFunction function = resolved.GetFunction();
+ function.IsValid() && function.GetName() != nullptr)
+ {
+ name = function.GetName();
+ }
+ else if (lldb::SBSymbol symbol = resolved.GetSymbol();
+ symbol.IsValid() && symbol.GetName() != nullptr)
+ {
+ name = symbol.GetName();
+ }
+ }
+
+ if (name.empty())
+ name = std::format("{:#x}", address);
+
+ const uint32_t id = m_next_trace_id++;
+ m_instr_functions.emplace(address, id);
+ m_instr_names.emplace(id, std::move(name));
+ return id;
+ }
+
+ auto Debugger::span_label(uint32_t trace_id) const -> const char*
+ {
+ if (const auto entry = m_instr_names.find(trace_id); entry != m_instr_names.end())
+ return entry->second.c_str();
+
+ const auto trace = std::ranges::find(m_traces, trace_id, &FunctionTrace::id);
+ if (trace != m_traces.end())
+ return trace->function.c_str();
+
+ return "?";
+ }
+
+ auto Debugger::intern_named_function(std::string_view name) -> uint32_t
+ {
+ std::string key(name);
+
+ if (const auto entry = m_sample_functions.find(key); entry != m_sample_functions.end())
+ return entry->second;
+
+ const uint32_t id = m_next_trace_id++;
+ m_sample_functions.emplace(key, id);
+ m_instr_names.emplace(id, std::move(key)); // shared id -> name store used by span_label
+ return id;
+ }
+
+ auto Debugger::maybe_request_sample() -> void
+ {
+ // instrumentation, when present, is the exact source and wins
+ if (!m_sampling_enabled || m_instr_available)
+ return;
+
+ if (!is_alive(m_session->process) || m_session->process.GetState() != lldb::eStateRunning)
+ return;
+
+ if (m_sample_pending)
+ return;
+
+ // update() runs once a frame, so this caps out near the frame rate; a
+ // short interval just means "as often as we can"
+ const auto now = std::chrono::steady_clock::now();
+ if (now - m_sample_last < std::chrono::milliseconds(5))
+ return;
+
+ m_sample_last = now;
+ m_session->process.Stop();
+ m_sample_pending = true;
+ }
+
+ auto Debugger::take_sample_and_resume() -> bool
+ {
+ // a real breakpoint landing at the same time is a genuine stop, not a sample
+ const uint32_t threads = m_session->process.GetNumThreads();
+ for (uint32_t index = 0; index < threads; ++index)
+ {
+ lldb::SBThread thread = m_session->process.GetThreadAtIndex(index);
+ if (thread.IsValid() && thread.GetStopReason() == lldb::eStopReasonBreakpoint)
+ return false;
+ }
+
+ take_sample();
+ m_session->process.Continue();
+ return true;
+ }
+
+ auto Debugger::take_sample() -> void
+ {
+ const double now = trace_now();
+ const uint32_t threads = m_session->process.GetNumThreads();
+
+ std::vector<uint32_t> stack;
+
+ for (uint32_t index = 0; index < threads; ++index)
+ {
+ lldb::SBThread thread = m_session->process.GetThreadAtIndex(index);
+ if (!thread.IsValid())
+ continue;
+
+ stack.clear();
+
+ // lldb numbers frames innermost-first, so walk from the top down to put
+ // the outermost call (main) at depth zero
+ const uint32_t frames = std::min(thread.GetNumFrames(), 64u);
+ for (int frame_index = static_cast<int>(frames) - 1; frame_index >= 0; --frame_index)
+ {
+ lldb::SBFrame frame = thread.GetFrameAtIndex(static_cast<uint32_t>(frame_index));
+ if (frame.IsValid())
+ stack.push_back(intern_named_function(name_of(frame)));
+ }
+
+ fold_sample(thread.GetThreadID(), stack, now);
+ }
+ }
+
+ auto Debugger::fold_sample(uint64_t thread_id, const std::vector<uint32_t>& stack, double now) -> void
+ {
+ std::vector<OpenSample>& open = m_sample_stacks[thread_id];
+
+ // how deep the new stack still matches the bars already open
+ size_t match = 0;
+ while (match < open.size() && match < stack.size() && open[match].trace_id == stack[match])
+ ++match;
+
+ // close every bar below the divergence point, deepest first
+ for (size_t depth = open.size(); depth-- > match;)
+ {
+ const size_t span = open[depth].span_index;
+ if (span < m_timeline.size())
+ m_timeline[span].duration = now - m_timeline[span].start;
+ }
+ open.resize(match);
+
+ // open a fresh bar for each newly seen frame
+ for (size_t depth = match; depth < stack.size(); ++depth)
+ {
+ size_t span_index = m_timeline.size();
+ if (m_timeline.size() < MAX_TRACE_CALLS)
+ {
+ m_timeline.push_back(TimelineSpan{ stack[depth], thread_id, now, 0.0,
+ static_cast<uint32_t>(depth) });
+ }
+
+ open.push_back(OpenSample{ stack[depth], span_index });
+ }
+
+ // let still-open bars grow up to the current sample so they render live
+ for (const OpenSample& entry : open)
+ {
+ if (entry.span_index < m_timeline.size())
+ m_timeline[entry.span_index].duration = now - m_timeline[entry.span_index].start;
+ }
+ }
+
auto Debugger::require_stopped() const -> Result<void>
{
if (!is_alive(m_session->process))
@@ -1495,11 +1947,20 @@ namespace Hsdbg
case lldb::eStateStopped:
if (m_state != TargetState::Stopped)
{
+ // our own Stop() for a sample resolves here; clear the flag
+ // up front so a coincident breakpoint cannot strand it
+ const bool was_sampling = m_sample_pending;
+ m_sample_pending = false;
+
// a trace-only stop records the call and resumes without
// ever surfacing to the user as a stop
if (handle_trace_stop())
break;
+ // likewise a sampling stop grabs the stacks and resumes
+ if (was_sampling && take_sample_and_resume())
+ break;
+
on_stopped();
}
@@ -1611,6 +2072,10 @@ namespace Hsdbg
set_state(TargetState::Stopped);
+ // the process is settled, so drain whatever the instrumentation buffer has
+ // gathered since the last stop into the timeline
+ read_instrumentation();
+
++m_stop_count;
}
diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h
index 7faa3a1..e2f4c62 100644
--- a/src/debugger/debugger.h
+++ b/src/debugger/debugger.h
@@ -6,12 +6,18 @@
#include <chrono>
#include <cstdint>
#include <filesystem>
+#include <unordered_map>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <vector>
+namespace lldb
+{
+ class SBThread;
+}
+
namespace Hsdbg
{
class Debugger
@@ -60,6 +66,22 @@ namespace Hsdbg
auto clear_traces() -> void;
auto traces() const -> std::span<const FunctionTrace> { return m_traces; }
+ // time-ordered call spans for the flame chart, spanning every traced thread
+ auto timeline() const -> std::span<const TimelineSpan> { return m_timeline; }
+
+ // name behind a span's trace id, whether it came from a manual trace or
+ // from the automatic instrumentation buffer
+ auto span_label(uint32_t trace_id) const -> const char*;
+
+ // whether the running target was built with the hsdbg trace runtime, so
+ // every function is being timed automatically
+ auto instrumentation_active() const -> bool { return m_instr_available; }
+
+ // sampling profiler: no build changes, works on any binary. periodically
+ // pauses the running target and folds its call stacks into the timeline
+ auto set_sampling_enabled(bool enabled) -> void { m_sampling_enabled = enabled; }
+ auto sampling_enabled() const -> bool { return m_sampling_enabled; }
+
// inspection
auto threads() const -> std::span<const Thread> { return m_threads; }
auto call_stack() const -> std::span<const StackFrame> { return m_call_stack; }
@@ -129,8 +151,58 @@ namespace Hsdbg
// 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 record_trace_entry(FunctionTrace& trace, lldb::SBThread& thread) -> void;
+ auto record_trace_return(lldb::SBThread& thread) -> void;
auto trace_now() const -> double;
+ // automatic tracing: read the target's instrumentation ring buffer and turn
+ // its enter/exit records into timeline spans
+ struct InstrRecord;
+ auto resolve_instrumentation() -> void;
+ auto read_instrumentation() -> void;
+ auto apply_instr_record(const InstrRecord& record) -> void;
+ auto intern_instr_function(uint64_t address) -> uint32_t;
+ auto symbol_load_address(const char* name) -> uint64_t;
+
+ // one instrumented call still on a thread's stack, waiting for its exit
+ struct InstrOpenCall
+ {
+ uint32_t trace_id = 0;
+ uint64_t start_ns = 0;
+ size_t span_index = 0;
+ };
+
+ // sampling profiler internals
+ auto maybe_request_sample() -> void;
+ auto take_sample_and_resume() -> bool;
+ auto take_sample() -> void;
+ auto fold_sample(uint64_t thread_id, const std::vector<uint32_t>& stack, double now) -> void;
+ auto intern_named_function(std::string_view name) -> uint32_t;
+
+ // one frame currently open on a thread while folding samples into bars
+ struct OpenSample
+ {
+ uint32_t trace_id = 0;
+ size_t span_index = 0;
+ };
+
+ // a temporary breakpoint at a call's return address, so the matching exit
+ // can be timed; created lazily and torn down when the process restarts
+ auto ensure_return_breakpoint(uint64_t address) -> int32_t;
+ auto clear_return_breakpoints() -> void;
+
+ // one in-flight call waiting for its return to be seen
+ struct PendingCall
+ {
+ uint32_t trace_id = 0;
+ uint64_t thread_id = 0;
+ uint64_t return_pc = 0;
+ uint64_t frame_sp = 0;
+ double start = 0.0;
+ size_t call_index = 0;
+ size_t span_index = 0;
+ };
+
std::unique_ptr<Session> m_session;
TargetState m_state = TargetState::NoTarget;
@@ -144,6 +216,11 @@ namespace Hsdbg
std::vector<FunctionTrace> m_traces;
uint32_t m_next_trace_id = 1;
+ std::vector<PendingCall> m_pending_calls;
+ std::vector<TimelineSpan> m_timeline;
+
+ // return address -> lldb breakpoint id, so call sites share one breakpoint
+ std::unordered_map<uint64_t, int32_t> m_return_breakpoints;
std::vector<Thread> m_threads;
std::vector<StackFrame> m_call_stack;
@@ -162,5 +239,25 @@ namespace Hsdbg
uint64_t m_resident_memory = 0;
std::chrono::steady_clock::time_point m_trace_epoch = std::chrono::steady_clock::now();
+
+ // automatic instrumentation reader state, all rebuilt each run
+ bool m_instr_checked = false;
+ bool m_instr_available = false;
+ uint64_t m_instr_head_addr = 0;
+ uint64_t m_instr_records_addr = 0;
+ uint64_t m_instr_capacity = 0;
+ uint64_t m_instr_read_count = 0;
+ uint64_t m_instr_base_ns = 0;
+ bool m_instr_base_set = false;
+ std::unordered_map<uint64_t, uint32_t> m_instr_functions; // address -> trace id
+ std::unordered_map<uint32_t, std::string> m_instr_names; // trace id -> name
+ std::unordered_map<uint64_t, std::vector<InstrOpenCall>> m_instr_stacks; // thread -> open calls
+
+ // sampling profiler state, also rebuilt each run
+ bool m_sampling_enabled = false;
+ bool m_sample_pending = false;
+ std::chrono::steady_clock::time_point m_sample_last{};
+ std::unordered_map<std::string, uint32_t> m_sample_functions; // name -> trace id
+ std::unordered_map<uint64_t, std::vector<OpenSample>> m_sample_stacks; // thread -> open frames
};
}
diff --git a/src/debugger/types.h b/src/debugger/types.h
index cda3dbd..0c4e95a 100644
--- a/src/debugger/types.h
+++ b/src/debugger/types.h
@@ -167,6 +167,17 @@ namespace Hsdbg
double duration = 0.0; // seconds spent inside the call
};
+ // one call laid out for the flame chart: a horizontal bar whose x is the
+ // start time, width is the duration and row is how deeply it was nested
+ struct TimelineSpan
+ {
+ uint32_t trace_id = 0; // which traced function, also picks the colour
+ uint64_t thread_id = 0;
+ double start = 0.0; // seconds since tracing began
+ double duration = 0.0; // seconds; zero while the call is still running
+ uint32_t depth = 0; // 0 is the outermost call on its thread
+ };
+
// 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
@@ -174,7 +185,15 @@ namespace Hsdbg
uint32_t id = 0;
std::string function;
int32_t entry_backend_id = 0;
- uint64_t call_count = 0;
+
+ uint64_t call_count = 0; // entries seen
+ uint64_t completed_count = 0; // calls whose return was matched
+
+ // seconds, aggregated over completed calls
+ double total_time = 0.0;
+ double min_time = 0.0;
+ double max_time = 0.0;
+
std::vector<TraceCall> calls;
};
}
diff --git a/src/ui/macro_expander.cpp b/src/ui/macro_expander.cpp
new file mode 100644
index 0000000..f4588c3
--- /dev/null
+++ b/src/ui/macro_expander.cpp
@@ -0,0 +1,847 @@
+#include "ui/macro_expander.h"
+
+#include <algorithm>
+#include <cctype>
+#include <fstream>
+
+namespace Hsdbg
+{
+ namespace
+ {
+ // hard ceilings so a runaway macro can never take the whole ui with it
+ constexpr size_t MAX_TOKENS = 200000;
+ constexpr int MAX_INCLUDE_DEPTH = 8;
+
+ auto is_ident_start(char c) -> bool
+ {
+ return std::isalpha(static_cast<unsigned char>(c)) != 0 || c == '_';
+ }
+
+ auto is_ident(char c) -> bool
+ {
+ return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
+ }
+
+ // pull one preprocessing token out of text starting at index. strings and
+ // character literals are kept whole, and multi-character punctuators the
+ // expander actually reasons about (##, ::, ->) are recognised so they do
+ // not split. everything else falls back to a single-character punctuator
+ auto lex_one(std::string_view text, size_t& at) -> PpToken
+ {
+ while (at < text.size() && std::isspace(static_cast<unsigned char>(text[at])) != 0)
+ ++at;
+
+ PpToken token;
+
+ if (at >= text.size())
+ return token;
+
+ const char c = text[at];
+
+ if (is_ident_start(c))
+ {
+ const size_t start = at;
+ while (at < text.size() && is_ident(text[at]))
+ ++at;
+
+ token.kind = PpKind::Identifier;
+ token.text = std::string(text.substr(start, at - start));
+ return token;
+ }
+
+ if (std::isdigit(static_cast<unsigned char>(c)) != 0 ||
+ (c == '.' && at + 1 < text.size() &&
+ std::isdigit(static_cast<unsigned char>(text[at + 1])) != 0))
+ {
+ const size_t start = at++;
+
+ // a pp-number swallows digits, letters, dots and the exponent
+ // signs, which is loose but matches how the standard lexes them
+ while (at < text.size())
+ {
+ const char d = text[at];
+
+ if ((d == 'e' || d == 'E' || d == 'p' || d == 'P') && at + 1 < text.size() &&
+ (text[at + 1] == '+' || text[at + 1] == '-'))
+ {
+ at += 2;
+ continue;
+ }
+
+ if (is_ident(d) || d == '.')
+ ++at;
+ else
+ break;
+ }
+
+ token.kind = PpKind::Number;
+ token.text = std::string(text.substr(start, at - start));
+ return token;
+ }
+
+ if (c == '"' || c == '\'')
+ {
+ const size_t start = at++;
+
+ while (at < text.size())
+ {
+ if (text[at] == '\\' && at + 1 < text.size())
+ {
+ at += 2;
+ continue;
+ }
+
+ if (text[at] == c)
+ {
+ ++at;
+ break;
+ }
+
+ ++at;
+ }
+
+ token.kind = c == '"' ? PpKind::String : PpKind::Char;
+ token.text = std::string(text.substr(start, at - start));
+ return token;
+ }
+
+ static constexpr std::string_view multis[] = {
+ "...", "<<=", ">>=", "->*", "::", "->", "##", "<<", ">>", "<=", ">=",
+ "==", "!=", "&&", "||", "+=", "-=", "*=", "/=", "%=", "&=", "|=",
+ "^=", "++", "--", ".*",
+ };
+
+ for (const std::string_view op : multis)
+ {
+ if (text.compare(at, op.size(), op) == 0)
+ {
+ at += op.size();
+ token.kind = PpKind::Punct;
+ token.text = std::string(op);
+ return token;
+ }
+ }
+
+ token.kind = PpKind::Punct;
+ token.text = std::string(1, c);
+ ++at;
+ return token;
+ }
+
+ auto tokenize(std::string_view text) -> std::vector<PpToken>
+ {
+ std::vector<PpToken> tokens;
+ size_t at = 0;
+
+ while (at < text.size() && tokens.size() < MAX_TOKENS)
+ {
+ PpToken token = lex_one(text, at);
+
+ if (token.text.empty())
+ break;
+
+ tokens.push_back(std::move(token));
+ }
+
+ return tokens;
+ }
+
+ auto hidden_by(const PpToken& token, const std::string& name) -> bool
+ {
+ return std::ranges::find(token.hide, name) != token.hide.end();
+ }
+
+ // whether a space belongs between two adjacent tokens when rendering back
+ // to text; purely cosmetic, aimed at readable c-ish output
+ auto needs_space(const PpToken& left, const PpToken& right) -> bool
+ {
+ const std::string& l = left.text;
+ const std::string& r = right.text;
+
+ if (r == ")" || r == "]" || r == "," || r == ";" || r == "::" ||
+ r == "." || r == "->")
+ return false;
+
+ if (l == "(" || l == "[" || l == "::" || l == "." || l == "->")
+ return false;
+
+ if (r == "(" && (left.kind == PpKind::Identifier || l == ")" || l == "]"))
+ return false;
+
+ return true;
+ }
+
+ auto render(const std::vector<PpToken>& tokens) -> std::string
+ {
+ std::string out;
+
+ for (size_t i = 0; i < tokens.size(); ++i)
+ {
+ if (i != 0 && needs_space(tokens[i - 1], tokens[i]))
+ out += ' ';
+
+ out += tokens[i].text;
+ }
+
+ return out;
+ }
+
+ auto param_index(const MacroDef& def, const std::string& name) -> int
+ {
+ for (size_t i = 0; i < def.params.size(); ++i)
+ {
+ if (def.params[i] == name)
+ return static_cast<int>(i);
+ }
+
+ return -1;
+ }
+
+ // build a string literal token out of an argument's spelling, escaping the
+ // way # is required to: backslashes and quotes inside the text survive
+ auto stringize(const std::vector<PpToken>& arg) -> PpToken
+ {
+ std::string inner = render(arg);
+ std::string escaped;
+
+ for (const char c : inner)
+ {
+ if (c == '\\' || c == '"')
+ escaped += '\\';
+
+ escaped += c;
+ }
+
+ return PpToken{ PpKind::String, "\"" + escaped + "\"", {} };
+ }
+
+ auto paste_token(const std::string& text) -> PpToken
+ {
+ std::vector<PpToken> lexed = tokenize(text);
+
+ if (lexed.size() == 1)
+ return lexed.front();
+
+ // the paste did not form a single clean token; keep the joined
+ // spelling so the user still sees what ## produced
+ return PpToken{ PpKind::Identifier, text, {} };
+ }
+
+ // the arguments handed to a function-like macro. an object-like macro just
+ // passes an empty list through
+ using ArgList = std::vector<std::vector<PpToken>>;
+
+ // run every expansion layer on tokens until nothing changes, used to
+ // fully expand an argument before it is pasted into a macro body
+ auto expand_full(const MacroTable& table, std::vector<PpToken> tokens)
+ -> std::vector<PpToken>;
+
+ // raw holds each argument exactly as written (what # and ## must see);
+ // expanded holds the same arguments after full macro expansion (what a
+ // plain parameter reference is replaced with, per the standard)
+ auto substitute(const MacroDef& def, const ArgList& raw, const ArgList& expanded)
+ -> std::vector<PpToken>
+ {
+ std::vector<PpToken> out;
+
+ const auto raw_tokens = [&](int index) -> std::vector<PpToken>
+ {
+ if (index >= 0 && index < static_cast<int>(raw.size()))
+ return raw[static_cast<size_t>(index)];
+
+ return {};
+ };
+
+ const auto expanded_tokens = [&](int index) -> std::vector<PpToken>
+ {
+ if (index >= 0 && index < static_cast<int>(expanded.size()))
+ return expanded[static_cast<size_t>(index)];
+
+ return {};
+ };
+
+ // __VA_ARGS__ from the raw arguments, for # and ##
+ const auto raw_varargs = [&]() -> std::vector<PpToken>
+ {
+ std::vector<PpToken> joined;
+
+ for (size_t i = def.params.size(); i < raw.size(); ++i)
+ {
+ if (i != def.params.size())
+ joined.push_back(PpToken{ PpKind::Punct, ",", {} });
+
+ joined.insert(joined.end(), raw[i].begin(), raw[i].end());
+ }
+
+ return joined;
+ };
+
+ const auto expanded_varargs = [&]() -> std::vector<PpToken>
+ {
+ std::vector<PpToken> joined;
+
+ for (size_t i = def.params.size(); i < expanded.size(); ++i)
+ {
+ if (i != def.params.size())
+ joined.push_back(PpToken{ PpKind::Punct, ",", {} });
+
+ joined.insert(joined.end(), expanded[i].begin(), expanded[i].end());
+ }
+
+ return joined;
+ };
+
+ const auto& body = def.body;
+
+ for (size_t i = 0; i < body.size(); ++i)
+ {
+ const PpToken& tok = body[i];
+
+ if (tok.text == "#" && def.function_like && i + 1 < body.size() &&
+ body[i + 1].kind == PpKind::Identifier)
+ {
+ if (const int index = param_index(def, body[i + 1].text); index >= 0)
+ {
+ out.push_back(stringize(raw_tokens(index)));
+ ++i;
+ continue;
+ }
+
+ if (def.variadic && body[i + 1].text == "__VA_ARGS__")
+ {
+ out.push_back(stringize(raw_varargs()));
+ ++i;
+ continue;
+ }
+ }
+
+ if (tok.text == "##" && !out.empty() && i + 1 < body.size())
+ {
+ const PpToken& rhs = body[i + 1];
+ std::vector<PpToken> pieces;
+
+ if (const int index = param_index(def, rhs.text); index >= 0)
+ pieces = raw_tokens(index);
+ else if (def.variadic && rhs.text == "__VA_ARGS__")
+ pieces = raw_varargs();
+ else
+ pieces = { rhs };
+
+ if (!pieces.empty())
+ {
+ out.back() = paste_token(out.back().text + pieces.front().text);
+ out.insert(out.end(), pieces.begin() + 1, pieces.end());
+ }
+
+ ++i;
+ continue;
+ }
+
+ // a parameter next to ## on either side keeps its raw argument;
+ // everywhere else it takes the fully expanded one
+ const bool paste_operand =
+ (i + 1 < body.size() && body[i + 1].text == "##") ||
+ (i > 0 && body[i - 1].text == "##");
+
+ if (const int index = param_index(def, tok.text); index >= 0)
+ {
+ const std::vector<PpToken> value =
+ paste_operand ? raw_tokens(index) : expanded_tokens(index);
+ out.insert(out.end(), value.begin(), value.end());
+ continue;
+ }
+
+ if (def.variadic && tok.text == "__VA_ARGS__")
+ {
+ const std::vector<PpToken> value =
+ paste_operand ? raw_varargs() : expanded_varargs();
+ out.insert(out.end(), value.begin(), value.end());
+ continue;
+ }
+
+ out.push_back(tok);
+ }
+
+ return out;
+ }
+
+ // add the just-expanded macro's name to the hide set of every produced
+ // token, folding in the hide set the invocation itself carried
+ auto paint(std::vector<PpToken>& tokens, const std::vector<std::string>& carried,
+ const std::string& name) -> void
+ {
+ for (PpToken& token : tokens)
+ {
+ for (const std::string& hidden : carried)
+ {
+ if (!hidden_by(token, hidden))
+ token.hide.push_back(hidden);
+ }
+
+ if (!hidden_by(token, name))
+ token.hide.push_back(name);
+ }
+ }
+
+ // gather the arguments of a function-like call. open is the index of '(';
+ // on success end is set past the matching ')'. returns false if the parens
+ // never balance, in which case the name is left as a plain identifier
+ auto collect_args(const std::vector<PpToken>& tokens, size_t open, ArgList& args,
+ size_t& end) -> bool
+ {
+ int depth = 0;
+ std::vector<PpToken> current;
+ bool any = false;
+
+ for (size_t i = open; i < tokens.size(); ++i)
+ {
+ const PpToken& tok = tokens[i];
+
+ if (tok.text == "(")
+ {
+ ++depth;
+
+ if (depth == 1)
+ continue;
+ }
+ else if (tok.text == ")")
+ {
+ --depth;
+
+ if (depth == 0)
+ {
+ if (any || !current.empty())
+ args.push_back(std::move(current));
+
+ end = i + 1;
+ return true;
+ }
+ }
+ else if (tok.text == "," && depth == 1)
+ {
+ args.push_back(std::move(current));
+ current.clear();
+ any = true;
+ continue;
+ }
+
+ current.push_back(tok);
+ any = true;
+ }
+
+ return false;
+ }
+
+ // whether a call supplies enough arguments for a definition. a zero-arg
+ // macro invoked as NAME() lexes as a single empty argument, so treat that
+ // as a match
+ auto arity_ok(const MacroDef& def, const ArgList& args) -> bool
+ {
+ const size_t provided =
+ args.size() == 1 && args.front().empty() ? 0 : args.size();
+
+ if (def.variadic)
+ return provided >= def.params.size();
+
+ return provided == def.params.size();
+ }
+
+ // one expansion layer: every eligible macro name in the stream is replaced
+ // once, and the tokens it produced are left for the next layer to rescan.
+ // that "one layer per pass" rule is what the level slider steps through
+ auto expand_pass(const MacroTable& table, const std::vector<PpToken>& in,
+ std::vector<PpToken>& out, std::vector<std::string>& expanded) -> bool
+ {
+ bool changed = false;
+ size_t i = 0;
+
+ while (i < in.size())
+ {
+ const PpToken& tok = in[i];
+
+ if (tok.kind != PpKind::Identifier || hidden_by(tok, tok.text))
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ const MacroDef* def = table.find(tok.text);
+
+ if (def == nullptr)
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ if (!def->function_like)
+ {
+ std::vector<PpToken> repl = substitute(*def, {}, {});
+ paint(repl, tok.hide, def->name);
+ out.insert(out.end(), repl.begin(), repl.end());
+ expanded.push_back(def->name);
+ changed = true;
+ ++i;
+ continue;
+ }
+
+ const size_t paren = i + 1;
+
+ if (paren >= in.size() || in[paren].text != "(")
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ ArgList args;
+ size_t end = 0;
+
+ if (!collect_args(in, paren, args, end) || !arity_ok(*def, args))
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ ArgList expanded_args;
+ expanded_args.reserve(args.size());
+
+ for (const std::vector<PpToken>& arg : args)
+ expanded_args.push_back(expand_full(table, arg));
+
+ std::vector<PpToken> repl = substitute(*def, args, expanded_args);
+
+ std::vector<std::string> carried = tok.hide;
+ const std::vector<std::string>& close_hide = in[end - 1].hide;
+
+ // function-like painting keeps only the names hidden at both the
+ // name and the closing paren, then adds this macro
+ std::erase_if(carried, [&](const std::string& name)
+ {
+ return std::ranges::find(close_hide, name) == close_hide.end();
+ });
+
+ paint(repl, carried, def->name);
+ out.insert(out.end(), repl.begin(), repl.end());
+ expanded.push_back(def->name);
+ changed = true;
+ i = end;
+ }
+
+ return changed && out.size() < MAX_TOKENS;
+ }
+
+ auto expand_full(const MacroTable& table, std::vector<PpToken> tokens)
+ -> std::vector<PpToken>
+ {
+ for (int level = 0; level < 64; ++level)
+ {
+ std::vector<PpToken> next;
+ std::vector<std::string> expanded;
+
+ if (!expand_pass(table, tokens, next, expanded) || expanded.empty())
+ break;
+
+ tokens = std::move(next);
+
+ if (tokens.size() >= MAX_TOKENS)
+ break;
+ }
+
+ return tokens;
+ }
+ }
+
+ auto MacroTable::clear() -> void
+ {
+ m_macros.clear();
+ ++m_revision;
+ }
+
+ auto MacroTable::find(std::string_view name) const -> const MacroDef*
+ {
+ const auto it = m_macros.find(std::string(name));
+ return it == m_macros.end() ? nullptr : &it->second;
+ }
+
+ auto MacroTable::build(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir) -> void
+ {
+ m_macros.clear();
+
+ std::vector<std::filesystem::path> seen;
+ scan_lines(lines, base_dir, 0, seen);
+
+ ++m_revision;
+ }
+
+ namespace
+ {
+ // stitch backslash-newline continuations back into single logical lines so
+ // a multi-line #define parses as one
+ auto splice_lines(const std::vector<std::string>& lines) -> std::vector<std::string>
+ {
+ std::vector<std::string> out;
+ std::string pending;
+ bool continuing = false;
+
+ for (const std::string& line : lines)
+ {
+ std::string text = line;
+
+ const bool cont = !text.empty() && text.back() == '\\';
+
+ if (cont)
+ text.pop_back();
+
+ if (continuing)
+ pending += text;
+ else
+ pending = text;
+
+ if (cont)
+ {
+ continuing = true;
+ continue;
+ }
+
+ out.push_back(std::move(pending));
+ pending.clear();
+ continuing = false;
+ }
+
+ if (continuing)
+ out.push_back(std::move(pending));
+
+ return out;
+ }
+
+ auto directive_of(const std::string& line, std::string& rest) -> std::string
+ {
+ size_t at = 0;
+
+ while (at < line.size() && std::isspace(static_cast<unsigned char>(line[at])) != 0)
+ ++at;
+
+ if (at >= line.size() || line[at] != '#')
+ return {};
+
+ ++at;
+
+ while (at < line.size() && std::isspace(static_cast<unsigned char>(line[at])) != 0)
+ ++at;
+
+ const size_t start = at;
+
+ while (at < line.size() && is_ident(line[at]))
+ ++at;
+
+ std::string word = line.substr(start, at - start);
+ rest = line.substr(at);
+ return word;
+ }
+
+ auto quote_include(const std::string& rest) -> std::string
+ {
+ const size_t open = rest.find('"');
+
+ if (open == std::string::npos)
+ return {};
+
+ const size_t close = rest.find('"', open + 1);
+
+ if (close == std::string::npos)
+ return {};
+
+ return rest.substr(open + 1, close - open - 1);
+ }
+
+ auto parse_define(const std::string& rest, MacroDef& def) -> bool
+ {
+ size_t at = 0;
+
+ while (at < rest.size() && std::isspace(static_cast<unsigned char>(rest[at])) != 0)
+ ++at;
+
+ if (at >= rest.size() || !is_ident_start(rest[at]))
+ return false;
+
+ const size_t name_start = at;
+
+ while (at < rest.size() && is_ident(rest[at]))
+ ++at;
+
+ def.name = rest.substr(name_start, at - name_start);
+
+ // a '(' touching the name with no space makes it function-like; a
+ // space means the paren is part of the body
+ if (at < rest.size() && rest[at] == '(')
+ {
+ def.function_like = true;
+ ++at;
+
+ while (true)
+ {
+ while (at < rest.size() &&
+ std::isspace(static_cast<unsigned char>(rest[at])) != 0)
+ ++at;
+
+ if (at < rest.size() && rest[at] == ')')
+ {
+ ++at;
+ break;
+ }
+
+ if (rest.compare(at, 3, "...") == 0)
+ {
+ def.variadic = true;
+ at += 3;
+ continue;
+ }
+
+ if (at < rest.size() && is_ident_start(rest[at]))
+ {
+ const size_t p = at;
+
+ while (at < rest.size() && is_ident(rest[at]))
+ ++at;
+
+ std::string param = rest.substr(p, at - p);
+
+ // a named variadic (GNU args...) collapses onto the
+ // standard __VA_ARGS__ handling
+ if (rest.compare(at, 3, "...") == 0)
+ {
+ def.variadic = true;
+ at += 3;
+ }
+ else
+ {
+ def.params.push_back(std::move(param));
+ }
+
+ continue;
+ }
+
+ if (at < rest.size() && rest[at] == ',')
+ {
+ ++at;
+ continue;
+ }
+
+ // something unexpected in the parameter list; give up on this
+ // definition rather than guess
+ return false;
+ }
+ }
+
+ def.body = tokenize(std::string_view(rest).substr(at));
+ return true;
+ }
+ }
+
+ auto MacroTable::scan_lines(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir, int depth,
+ std::vector<std::filesystem::path>& seen) -> void
+ {
+ const std::vector<std::string> logical = splice_lines(lines);
+
+ for (const std::string& line : logical)
+ {
+ std::string rest;
+ const std::string directive = directive_of(line, rest);
+
+ if (directive == "define")
+ {
+ MacroDef def;
+
+ if (parse_define(rest, def))
+ m_macros[def.name] = std::move(def);
+ }
+ else if (directive == "undef")
+ {
+ MacroDef tmp;
+
+ if (parse_define(rest, tmp))
+ m_macros.erase(tmp.name);
+ }
+ else if (directive == "include" && depth < MAX_INCLUDE_DEPTH && !base_dir.empty())
+ {
+ const std::string name = quote_include(rest);
+
+ if (!name.empty())
+ scan_path(base_dir / name, depth + 1, seen);
+ }
+ }
+ }
+
+ auto MacroTable::scan_path(const std::filesystem::path& path, int depth,
+ std::vector<std::filesystem::path>& seen) -> void
+ {
+ std::error_code error;
+ const std::filesystem::path canonical = std::filesystem::weakly_canonical(path, error);
+ const std::filesystem::path key = error ? path : canonical;
+
+ if (std::ranges::find(seen, key) != seen.end())
+ return;
+
+ seen.push_back(key);
+
+ std::ifstream file(path);
+
+ if (!file.is_open())
+ return;
+
+ std::vector<std::string> lines;
+ std::string line;
+
+ while (std::getline(file, line))
+ {
+ if (!line.empty() && line.back() == '\r')
+ line.pop_back();
+
+ lines.push_back(std::move(line));
+ }
+
+ scan_lines(lines, path.parent_path(), depth, seen);
+ }
+
+ auto expand_stages(const MacroTable& table, std::string_view input, int max_levels)
+ -> MacroExpansion
+ {
+ MacroExpansion result;
+
+ std::vector<PpToken> tokens = tokenize(input);
+
+ result.levels.push_back(render(tokens));
+ result.expanded.emplace_back();
+
+ for (int level = 0; level < max_levels; ++level)
+ {
+ std::vector<PpToken> next;
+ std::vector<std::string> expanded;
+
+ if (!expand_pass(table, tokens, next, expanded) || expanded.empty())
+ {
+ result.m_reached_fixpoint = true;
+ return result;
+ }
+
+ tokens = std::move(next);
+ result.levels.push_back(render(tokens));
+ result.expanded.push_back(std::move(expanded));
+
+ if (tokens.size() >= MAX_TOKENS)
+ {
+ result.m_reached_fixpoint = false;
+ return result;
+ }
+ }
+
+ // ran out of levels before settling; the last one may still expand further
+ result.m_reached_fixpoint = false;
+ return result;
+ }
+}
diff --git a/src/ui/macro_expander.h b/src/ui/macro_expander.h
new file mode 100644
index 0000000..cf87d86
--- /dev/null
+++ b/src/ui/macro_expander.h
@@ -0,0 +1,100 @@
+#pragma once
+
+#include <cstdint>
+#include <filesystem>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace Hsdbg
+{
+ // one preprocessing token. whitespace is dropped during tokenising, so a run
+ // of tokens is rendered back to text with a small spacing heuristic. the hide
+ // set is the classic preprocessor "blue paint": the names of macros that must
+ // not expand this token again, which is what keeps a self-referential macro
+ // from unrolling forever
+ enum class PpKind : uint8_t
+ {
+ Identifier,
+ Number,
+ String,
+ Char,
+ Punct,
+ };
+
+ struct PpToken
+ {
+ PpKind kind = PpKind::Identifier;
+ std::string text;
+ std::vector<std::string> hide;
+ };
+
+ // a single #define, object-like (BUFFER) or function-like (MAX(a, b)). the
+ // body is the replacement list as significant tokens, with #, ## and the
+ // parameter names left in place for substitution time
+ struct MacroDef
+ {
+ std::string name;
+ bool function_like = false;
+ bool variadic = false;
+ std::vector<std::string> params;
+ std::vector<PpToken> body;
+ };
+
+ // the set of #defines visible in a source file. built by scanning the file
+ // for #define / #undef directives in order, optionally following local
+ // "quote" includes one project deep. system <...> includes are ignored on
+ // purpose: this is meant for a project's own macros, not libc's
+ class MacroTable
+ {
+ public:
+ auto clear() -> void;
+
+ // scan already-loaded lines as the primary file, then chase local quote
+ // includes relative to base_dir off disk. either argument may be empty
+ auto build(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir) -> void;
+
+ auto find(std::string_view name) const -> const MacroDef*;
+
+ auto empty() const -> bool { return m_macros.empty(); }
+ auto size() const -> size_t { return m_macros.size(); }
+
+ // bumped every rebuild, so a consumer can tell its cached expansion is
+ // stale without comparing the whole table
+ auto revision() const -> uint64_t { return m_revision; }
+
+ private:
+ auto scan_lines(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir, int depth,
+ std::vector<std::filesystem::path>& seen) -> void;
+ auto scan_path(const std::filesystem::path& path, int depth,
+ std::vector<std::filesystem::path>& seen) -> void;
+
+ std::unordered_map<std::string, MacroDef> m_macros;
+ uint64_t m_revision = 0;
+ };
+
+ // the result of unrolling an expression one macro layer at a time. levels[0]
+ // is the input as written, levels[i] is the text after i rounds of expansion,
+ // and levels.back() is the fixpoint where nothing expands any further
+ struct MacroExpansion
+ {
+ std::vector<std::string> levels;
+
+ // names of macros that were actually expanded going into each level, so
+ // levels_expanded[i] describes what turned levels[i-1] into levels[i].
+ // index 0 is always empty
+ std::vector<std::vector<std::string>> expanded;
+
+ auto fully_expanded() const -> bool { return m_reached_fixpoint; }
+
+ bool m_reached_fixpoint = true;
+ };
+
+ // unroll input against the table, capping the number of layers so a
+ // pathological macro cannot hang the ui
+ auto expand_stages(const MacroTable& table, std::string_view input,
+ int max_levels = 64) -> MacroExpansion;
+}
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp
index b998e3f..7638f17 100644
--- a/src/ui/source_view.cpp
+++ b/src/ui/source_view.cpp
@@ -103,7 +103,8 @@ namespace Hsdbg
// splits every line into contiguous coloured spans. the whole file is
// walked in order so a block comment opened on one line stays open on the
// next, which a per-line pass could not know
- auto highlight_lines(const std::vector<std::string>& lines, Language language)
+ auto highlight_lines(const std::vector<std::string>& lines, Language language,
+ const MacroTable& macros)
-> std::vector<std::vector<SourceSpan>>
{
std::vector<std::vector<SourceSpan>> out(lines.size());
@@ -265,9 +266,17 @@ namespace Hsdbg
while (at < size && is_word(line[at]))
++at;
- if (const SyntaxKind kind = classify_word(
- std::string_view(line).substr(start, at - start), language);
- kind != SyntaxKind::Default)
+ const std::string_view word =
+ std::string_view(line).substr(start, at - start);
+ SyntaxKind kind = classify_word(word, language);
+
+ // a plain identifier that names a #define is coloured as a
+ // macro so it reads as expandable in the source view
+ if (kind == SyntaxKind::Default && language == Language::Cpp &&
+ macros.find(word) != nullptr)
+ kind = SyntaxKind::Macro;
+
+ if (kind != SyntaxKind::Default)
{
flush_default(start);
emit(start, at - start, kind);
@@ -321,6 +330,46 @@ namespace Hsdbg
{
return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x;
}
+
+ // grab a macro invocation out of a line starting at the name: the name
+ // alone for an object-like use, or the name plus a balanced argument list
+ // for a function-like one. a call that runs off the end of the line falls
+ // back to the bare name rather than guessing where it closes
+ auto capture_invocation(const std::string& line, uint32_t start) -> std::string
+ {
+ const size_t size = line.size();
+ size_t at = start;
+
+ while (at < size && is_word(line[at]))
+ ++at;
+
+ size_t paren = at;
+
+ while (paren < size && std::isspace(static_cast<unsigned char>(line[paren])) != 0)
+ ++paren;
+
+ if (paren < size && line[paren] == '(')
+ {
+ int depth = 0;
+
+ for (size_t i = paren; i < size; ++i)
+ {
+ if (line[i] == '(')
+ {
+ ++depth;
+ }
+ else if (line[i] == ')')
+ {
+ --depth;
+
+ if (depth == 0)
+ return line.substr(start, i + 1 - start);
+ }
+ }
+ }
+
+ return line.substr(start, at - start);
+ }
}
auto SourceView::open(const std::filesystem::path& path) -> Result<void>
@@ -345,7 +394,15 @@ namespace Hsdbg
m_path = path;
m_lines = std::move(lines);
m_highlight = language.has_value();
- m_spans = language ? highlight_lines(m_lines, *language)
+
+ // gather the file's #defines (chasing local quote includes) so macro
+ // names highlight and the macros panel has something to expand
+ m_macros.clear();
+
+ if (language == Language::Cpp)
+ m_macros.build(m_lines, path.parent_path());
+
+ m_spans = language ? highlight_lines(m_lines, *language, m_macros)
: std::vector<std::vector<SourceSpan>>{};
m_path_input = path.string();
m_error.clear();
@@ -361,11 +418,20 @@ namespace Hsdbg
m_path.clear();
m_lines.clear();
m_spans.clear();
+ m_macros.clear();
+ m_macro_request.reset();
m_highlight = false;
m_error.clear();
m_highlighted_line = 0;
}
+ auto SourceView::take_macro_request() -> std::optional<std::string>
+ {
+ std::optional<std::string> request = std::move(m_macro_request);
+ m_macro_request.reset();
+ return request;
+ }
+
auto SourceView::set_highlighted_line(uint32_t line) -> void
{
m_highlighted_line = line;
@@ -541,6 +607,10 @@ namespace Hsdbg
{
ImGui::TextUnformatted(begin, end);
}
+ else if (span.kind == SyntaxKind::Macro)
+ {
+ draw_macro_span(draw_list, text, span);
+ }
else
{
ImGui::PushStyleColor(ImGuiCol_Text,
@@ -557,4 +627,65 @@ namespace Hsdbg
ImGui::EndChild();
}
+
+ auto SourceView::draw_macro_span(ImDrawList* draw_list, const std::string& line,
+ const SourceSpan& span) -> void
+ {
+ const char* const begin = line.c_str() + span.start;
+ const char* const end = begin + span.length;
+ const ImU32 color = m_syntax_colors[static_cast<size_t>(SyntaxKind::Macro)];
+
+ ImGui::PushStyleColor(ImGuiCol_Text, color);
+ ImGui::TextUnformatted(begin, end);
+ ImGui::PopStyleColor();
+
+ if (!ImGui::IsItemHovered())
+ return;
+
+ // underline on hover so the name reads as a link
+ const ImVec2 rect_min = ImGui::GetItemRectMin();
+ const ImVec2 rect_max = ImGui::GetItemRectMax();
+ draw_list->AddLine(ImVec2(rect_min.x, rect_max.y - 1.0f),
+ ImVec2(rect_max.x, rect_max.y - 1.0f), color);
+
+ const std::string invocation = capture_invocation(line, span.start);
+ const MacroExpansion expansion = expand_stages(m_macros, invocation);
+
+ ImGui::BeginTooltip();
+
+ ImGui::PushStyleColor(ImGuiCol_Text, color);
+ ImGui::TextUnformatted(invocation.c_str());
+ ImGui::PopStyleColor();
+
+ ImGui::Separator();
+
+ std::string preview = expansion.levels.back();
+
+ if (constexpr size_t limit = 240; preview.size() > limit)
+ {
+ preview.resize(limit);
+ preview += " ...";
+ }
+
+ ImGui::PushTextWrapPos(ImGui::GetFontSize() * 30.0f);
+ ImGui::TextUnformatted(preview.c_str());
+ ImGui::PopTextWrapPos();
+
+ ImGui::Spacing();
+
+ const size_t steps = expansion.levels.size() - 1;
+
+ if (steps == 0)
+ ImGui::TextDisabled("no expansion");
+ else if (!expansion.fully_expanded())
+ ImGui::TextDisabled("%zu+ levels — click to step through", steps);
+ else
+ ImGui::TextDisabled("%zu level%s — click to step through", steps,
+ steps == 1 ? "" : "s");
+
+ ImGui::EndTooltip();
+
+ if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
+ m_macro_request = invocation;
+ }
}
diff --git a/src/ui/source_view.h b/src/ui/source_view.h
index 2e1a20f..c8a9a21 100644
--- a/src/ui/source_view.h
+++ b/src/ui/source_view.h
@@ -1,12 +1,16 @@
#pragma once
#include "core/result.h"
+#include "ui/macro_expander.h"
#include <cstdint>
#include <filesystem>
+#include <optional>
#include <string>
#include <vector>
+struct ImDrawList;
+
namespace Hsdbg
{
class Debugger;
@@ -22,6 +26,7 @@ namespace Hsdbg
Number,
Comment,
Preprocessor,
+ Macro,
};
struct SourceSpan
@@ -52,21 +57,40 @@ namespace Hsdbg
auto line_count() const -> size_t { return m_lines.size(); }
auto is_open() const -> bool { return !m_lines.empty(); }
+ // the #defines visible in the open file, so a panel can expand the same
+ // macros the source view is highlighting
+ auto macros() const -> const MacroTable& { return m_macros; }
+
+ // when the user clicks a highlighted macro, the invocation as written
+ // (name plus any argument list) is stashed here for the ui to pick up and
+ // hand to the macros panel; cleared once taken
+ auto take_macro_request() -> std::optional<std::string>;
+
private:
auto draw_open_bar() -> void;
auto draw_lines(Debugger& debugger) -> void;
+ // renders a highlighted macro name: hovering previews its expansion,
+ // clicking records the invocation for the macros panel to open
+ auto draw_macro_span(ImDrawList* draw_list, const std::string& line,
+ const SourceSpan& span) -> void;
+
std::filesystem::path m_path;
std::vector<std::string> m_lines;
std::vector<std::vector<SourceSpan>> m_spans;
+ MacroTable m_macros;
+ std::optional<std::string> m_macro_request;
bool m_highlight = false;
bool m_highlighting_enabled = true;
bool m_line_numbers = true;
bool m_highlight_current_line = true;
- // indexed by SyntaxKind; the defaults match the built-in dark palette
- unsigned int m_syntax_colors[7] = {
+ // indexed by SyntaxKind; the defaults match the built-in dark palette.
+ // the macro colour (last) is warm on purpose so expandable names read as
+ // clickable against the rest of the syntax
+ unsigned int m_syntax_colors[8] = {
0, 0xFFD69C56, 0xFFB0C94E, 0xFF7891CE, 0xFFA8CEB5, 0xFF55996A, 0xFFC086C5,
+ 0xFF4FA3E0,
};
unsigned int m_current_line_color = 0xFF2E483A;
std::string m_path_input;
diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp
index e1a2f9d..f39b95f 100644
--- a/src/ui/ui.cpp
+++ b/src/ui/ui.cpp
@@ -16,6 +16,7 @@
#include <cfloat>
#include <filesystem>
#include <format>
+#include <optional>
#include <span>
#include <vector>
@@ -38,6 +39,8 @@ namespace Hsdbg
constexpr const char* PANEL_DISASSEMBLY = "disassembly";
constexpr const char* PANEL_CONSOLE = "console";
constexpr const char* PANEL_PROFILER = "profiler";
+ constexpr const char* PANEL_TIMELINE = "timeline";
+ constexpr const char* PANEL_MACROS = "macros";
constexpr const char* LOAD_TARGET_POPUP = "load target";
@@ -614,6 +617,17 @@ namespace Hsdbg
ImGui::End();
draw_source_panel(debugger);
+
+ // a click on a highlighted macro in the source view loads it into the
+ // macros panel, brings the panel up and jumps its focus there
+ if (std::optional<std::string> request = m_source_view.take_macro_request())
+ {
+ m_macro_input = std::move(*request);
+ m_macro_level = 0;
+ m_visible.macros = true;
+ m_focus_macros = true;
+ }
+
draw_breakpoints_panel(debugger);
draw_call_stack_panel(debugger);
draw_source_tree_panel(debugger);
@@ -624,6 +638,8 @@ namespace Hsdbg
draw_disassembly_panel(debugger);
draw_console_panel(debugger);
draw_profiler_panel(debugger);
+ draw_timeline_panel(debugger);
+ draw_macros_panel(debugger);
if (m_visible.demo)
ImGui::ShowDemoWindow(&m_visible.demo);
@@ -657,6 +673,12 @@ namespace Hsdbg
ImGui::SetWindowFocus(PANEL_BREAKPOINTS);
m_focus_breakpoints = false;
}
+
+ if (m_focus_macros)
+ {
+ ImGui::SetWindowFocus(PANEL_MACROS);
+ m_focus_macros = false;
+ }
}
auto Ui::build_default_layout(uint32_t dockspace_id) -> void
@@ -678,6 +700,7 @@ namespace Hsdbg
ImGui::DockBuilderDockWindow(PANEL_SOURCE, center_id);
ImGui::DockBuilderDockWindow(PANEL_DISASSEMBLY, center_id);
+ ImGui::DockBuilderDockWindow(PANEL_TIMELINE, center_id);
ImGui::DockBuilderDockWindow(PANEL_SOURCE_TREE, left_top_id);
ImGui::DockBuilderDockWindow(PANEL_THREADS, left_top_id);
ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_bottom_id);
@@ -687,6 +710,7 @@ namespace Hsdbg
ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_PROFILER, bottom_id);
+ ImGui::DockBuilderDockWindow(PANEL_MACROS, bottom_id);
ImGui::DockBuilderFinish(dockspace_id);
}
@@ -769,6 +793,8 @@ namespace Hsdbg
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::MenuItem(PANEL_TIMELINE, nullptr, &m_visible.timeline);
+ ImGui::MenuItem(PANEL_MACROS, nullptr, &m_visible.macros);
ImGui::Separator();
@@ -1770,10 +1796,13 @@ namespace Hsdbg
constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingStretchProp;
- if (ImGui::BeginTable("##traces", 3, flags))
+ if (ImGui::BeginTable("##traces", 6, flags))
{
ImGui::TableSetupColumn("function");
- ImGui::TableSetupColumn("calls", ImGuiTableColumnFlags_WidthFixed, 70.0f);
+ ImGui::TableSetupColumn("calls", ImGuiTableColumnFlags_WidthFixed, 52.0f);
+ ImGui::TableSetupColumn("avg", ImGuiTableColumnFlags_WidthFixed, 72.0f);
+ ImGui::TableSetupColumn("min", ImGuiTableColumnFlags_WidthFixed, 72.0f);
+ ImGui::TableSetupColumn("max", ImGuiTableColumnFlags_WidthFixed, 72.0f);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f);
ImGui::TableHeadersRow();
@@ -1781,6 +1810,11 @@ namespace Hsdbg
for (const FunctionTrace& trace : traces)
{
+ // milliseconds read easier than the seconds we store
+ const double avg_ms = trace.completed_count > 0
+ ? trace.total_time / static_cast<double>(trace.completed_count) * 1000.0
+ : 0.0;
+
ImGui::TableNextRow();
ImGui::TableNextColumn();
@@ -1790,6 +1824,24 @@ namespace Hsdbg
ImGui::Text("%llu", static_cast<unsigned long long>(trace.call_count));
ImGui::TableNextColumn();
+ if (trace.completed_count > 0)
+ ImGui::Text("%.3f ms", avg_ms);
+ else
+ ImGui::TextDisabled("-");
+
+ ImGui::TableNextColumn();
+ if (trace.completed_count > 0)
+ ImGui::Text("%.3f ms", trace.min_time * 1000.0);
+ else
+ ImGui::TextDisabled("-");
+
+ ImGui::TableNextColumn();
+ if (trace.completed_count > 0)
+ ImGui::Text("%.3f ms", trace.max_time * 1000.0);
+ else
+ ImGui::TextDisabled("-");
+
+ ImGui::TableNextColumn();
ImGui::PushID(static_cast<int>(trace.id));
if (ImGui::SmallButton("x"))
remove_id = trace.id;
@@ -1828,6 +1880,236 @@ namespace Hsdbg
ImGui::End();
}
+ auto Ui::draw_timeline_panel(Debugger& debugger) -> void
+ {
+ if (!m_visible.timeline)
+ return;
+
+ if (ImGui::Begin(PANEL_TIMELINE, &m_visible.timeline))
+ {
+ const std::span<const TimelineSpan> spans = debugger.timeline();
+
+ if (debugger.instrumentation_active())
+ {
+ ImGui::TextDisabled("instrumented: every function traced automatically");
+ }
+ else
+ {
+ bool sampling = debugger.sampling_enabled();
+ if (ImGui::Checkbox("sample while running", &sampling))
+ debugger.set_sampling_enabled(sampling);
+
+ ImGui::SameLine();
+ ImGui::TextDisabled("(any binary, approximate)");
+ }
+
+ if (spans.empty())
+ {
+ ImGui::TextDisabled("run an instrumented target, or trace functions, then stop to see calls");
+ }
+ else
+ {
+ // resolve a trace id to its function name for labels and tooltips
+ const auto name_of = [&](uint32_t trace_id) -> const char*
+ {
+ return debugger.span_label(trace_id);
+ };
+
+ // the time range to fit and how tall the call stack gets
+ double t_min = spans.front().start;
+ double t_max = t_min;
+ uint32_t max_depth = 0;
+
+ for (const TimelineSpan& span : spans)
+ {
+ t_min = std::min(t_min, span.start);
+ t_max = std::max(t_max, span.start + span.duration);
+ max_depth = std::max(max_depth, span.depth);
+ }
+
+ const double range = std::max(t_max - t_min, 1.0e-6);
+
+ ImGui::Text("%.3f ms total %zu calls", range * 1000.0, spans.size());
+
+ constexpr float row_height = 20.0f;
+ const float rows = static_cast<float>(max_depth + 1);
+
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
+ const ImVec2 avail = ImGui::GetContentRegionAvail();
+
+ const float canvas_w = std::max(avail.x, 1.0f);
+ const float canvas_h = std::max(avail.y, rows * row_height + 4.0f);
+
+ // claim the region so hovering resolves against it
+ ImGui::InvisibleButton("##timeline_canvas", ImVec2(canvas_w, canvas_h));
+ const bool canvas_hovered = ImGui::IsItemHovered();
+
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ draw->PushClipRect(origin, ImVec2(origin.x + canvas_w, origin.y + canvas_h), true);
+
+ const float scale = canvas_w / static_cast<float>(range); // pixels per second
+ const float baseline = origin.y + canvas_h; // row 0 rests on the bottom
+
+ const ImVec2 mouse = ImGui::GetMousePos();
+
+ for (const TimelineSpan& span : spans)
+ {
+ const float x0 = origin.x + static_cast<float>((span.start - t_min) * scale);
+ const float width = std::max(1.0f, static_cast<float>(span.duration * scale));
+ const float y1 = baseline - static_cast<float>(span.depth) * row_height;
+ const float y0 = y1 - (row_height - 2.0f);
+
+ const ImU32 fill = ImColor::HSV(span.trace_id * 0.13f, 0.55f, 0.78f);
+
+ draw->AddRectFilled(ImVec2(x0, y0), ImVec2(x0 + width, y1), fill, 2.0f);
+ draw->AddRect(ImVec2(x0, y0), ImVec2(x0 + width, y1), IM_COL32(0, 0, 0, 90), 2.0f);
+
+ if (width > 24.0f)
+ {
+ draw->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x0 + width - 2.0f, y1), true);
+ draw->AddText(ImVec2(x0 + 4.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255),
+ name_of(span.trace_id));
+ draw->PopClipRect();
+ }
+
+ const bool over = canvas_hovered && mouse.x >= x0 && mouse.x <= x0 + width &&
+ mouse.y >= y0 && mouse.y <= y1;
+
+ if (over)
+ {
+ ImGui::BeginTooltip();
+ ImGui::TextUnformatted(name_of(span.trace_id));
+ ImGui::Text("start %.3f ms", (span.start - t_min) * 1000.0);
+ if (span.duration > 0.0)
+ ImGui::Text("duration %.3f ms", span.duration * 1000.0);
+ else
+ ImGui::TextDisabled("running...");
+ ImGui::EndTooltip();
+ }
+ }
+
+ draw->PopClipRect();
+ }
+ }
+
+ ImGui::End();
+ }
+
+ auto Ui::draw_macros_panel(Debugger& /*debugger*/) -> void
+ {
+ if (!m_visible.macros)
+ return;
+
+ if (ImGui::Begin(PANEL_MACROS, &m_visible.macros))
+ {
+ const MacroTable& table = m_source_view.macros();
+
+ ImGui::TextDisabled("unroll a #define one layer per level");
+
+ ImGui::SetNextItemWidth(-FLT_MIN);
+
+ if (ImGui::InputTextWithHint("##macro_input",
+ "click a macro in source, or type one like MAX(a, b)",
+ &m_macro_input))
+ m_macro_level = 0;
+
+ if (table.empty())
+ ImGui::TextDisabled("no #define macros found in the open source file");
+
+ if (m_macro_input.empty())
+ {
+ ImGui::End();
+ return;
+ }
+
+ const MacroExpansion expansion = expand_stages(table, m_macro_input);
+ const int max_level = static_cast<int>(expansion.levels.size()) - 1;
+
+ m_macro_level = std::clamp(m_macro_level, 0, max_level);
+
+ ImGui::BeginDisabled(m_macro_level <= 0);
+ if (ImGui::ArrowButton("##macro_prev", ImGuiDir_Left))
+ --m_macro_level;
+ ImGui::EndDisabled();
+
+ ImGui::SameLine();
+
+ ImGui::BeginDisabled(m_macro_level >= max_level);
+ if (ImGui::ArrowButton("##macro_next", ImGuiDir_Right))
+ ++m_macro_level;
+ ImGui::EndDisabled();
+
+ ImGui::SameLine();
+
+ if (max_level > 0)
+ {
+ ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x -
+ ImGui::CalcTextSize("full reset").x -
+ ImGui::GetStyle().FramePadding.x * 6.0f -
+ ImGui::GetStyle().ItemSpacing.x * 2.0f);
+ ImGui::SliderInt("##macro_level", &m_macro_level, 0, max_level, "level %d");
+ }
+ else
+ {
+ ImGui::TextDisabled("nothing to expand here");
+ ImGui::SameLine();
+ }
+
+ ImGui::SameLine();
+
+ if (ImGui::SmallButton("full"))
+ m_macro_level = max_level;
+
+ ImGui::SameLine();
+
+ if (ImGui::SmallButton("reset"))
+ m_macro_level = 0;
+
+ // status: where we are, and whether the tail is truly the fixpoint
+ if (max_level == 0)
+ ImGui::TextDisabled("already fully expanded");
+ else if (m_macro_level == max_level && expansion.fully_expanded())
+ ImGui::TextDisabled("level %d of %d — fully expanded", m_macro_level, max_level);
+ else if (m_macro_level == max_level)
+ ImGui::TextDisabled("level %d — stopped at the expansion cap", m_macro_level);
+ else
+ ImGui::TextDisabled("level %d of %d", m_macro_level, max_level);
+
+ // which macros the next layer will unroll, so the step reads ahead
+ if (m_macro_level < max_level)
+ {
+ const std::vector<std::string>& next =
+ expansion.expanded[static_cast<size_t>(m_macro_level) + 1];
+
+ if (!next.empty())
+ {
+ std::string names;
+
+ for (const std::string& name : next)
+ {
+ if (!names.empty())
+ names += ", ";
+
+ names += name;
+ }
+
+ ImGui::SameLine();
+ ImGui::TextDisabled("| next: %s", names.c_str());
+ }
+ }
+
+ ImGui::Separator();
+
+ m_macro_output = expansion.levels[static_cast<size_t>(m_macro_level)];
+
+ ImGui::InputTextMultiline("##macro_output", &m_macro_output,
+ ImGui::GetContentRegionAvail(),
+ ImGuiInputTextFlags_ReadOnly);
+ }
+
+ 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 5730059..70cf691 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -51,6 +51,8 @@ namespace Hsdbg
bool disassembly = true;
bool console = true;
bool profiler = true;
+ bool timeline = true;
+ bool macros = true;
bool demo = false;
};
@@ -75,6 +77,8 @@ namespace Hsdbg
auto draw_disassembly_panel(Debugger& debugger) -> void;
auto draw_console_panel(Debugger& debugger) -> void;
auto draw_profiler_panel(Debugger& debugger) -> void;
+ auto draw_timeline_panel(Debugger& debugger) -> void;
+ auto draw_macros_panel(Debugger& debugger) -> void;
auto push_console(std::string line) -> void;
auto report(const Result<void>& result, std::string_view action) -> void;
@@ -115,6 +119,12 @@ namespace Hsdbg
std::string m_source_filter;
std::string m_trace_input;
+ // macros panel: the invocation being unrolled, the current unroll depth,
+ // and the read-only buffer that shows the tokens at that depth
+ std::string m_macro_input;
+ std::string m_macro_output;
+ int m_macro_level = 0;
+
uint64_t m_followed_stop = 0;
std::filesystem::path m_followed_target;
@@ -123,6 +133,7 @@ namespace Hsdbg
bool m_console_scroll_pending = false;
bool m_load_target_pending = false;
bool m_focus_breakpoints = false;
+ bool m_focus_macros = false;
bool m_focus_symbols = false;
bool m_focus_disassembly = false;
bool m_scroll_to_program_counter = false;