| // Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "src/developer/debug/zxdb/client/tracepoint_capture.h" | |
| #include <algorithm> | |
| #include <memory> | |
| #include <ranges> | |
| #include "lib/fit/defer.h" | |
| #include "src/developer/debug/shared/register_info.h" | |
| #include "src/developer/debug/zxdb/client/frame.h" | |
| #include "src/developer/debug/zxdb/client/pretty_stack_manager.h" | |
| #include "src/developer/debug/zxdb/client/session.h" | |
| #include "src/developer/debug/zxdb/client/stack.h" | |
| #include "src/developer/debug/zxdb/client/system.h" | |
| #include "src/developer/debug/zxdb/client/thread.h" | |
| #include "src/developer/debug/zxdb/common/file_util.h" | |
| #include "src/developer/debug/zxdb/common/join_callbacks.h" | |
| #include "src/developer/debug/zxdb/expr/cast.h" | |
| #include "src/developer/debug/zxdb/expr/eval_context.h" | |
| #include "src/developer/debug/zxdb/expr/expr.h" | |
| #include "src/developer/debug/zxdb/expr/expr_parser.h" | |
| #include "src/developer/debug/zxdb/expr/format_node.h" | |
| #include "src/developer/debug/zxdb/expr/format_options.h" | |
| #include "src/developer/debug/zxdb/expr/parsed_identifier.h" | |
| #include "src/developer/debug/zxdb/format/format.h" | |
| #include "src/developer/debug/zxdb/symbols/function.h" | |
| #include "src/developer/debug/zxdb/symbols/location.h" | |
| #include "src/developer/debug/zxdb/symbols/variable.h" | |
| #include "src/lib/fxl/memory/ref_counted.h" | |
| namespace zxdb { | |
| namespace { | |
| bool MatchParsedIdentifier(const ParsedIdentifier& target, const ParsedIdentifier& candidate) { | |
| if (target.components().empty()) | |
| return true; | |
| if (target.components().size() > candidate.components().size()) | |
| return false; | |
| size_t offset = candidate.components().size() - target.components().size(); | |
| if (target.qualification() == IdentifierQualification::kGlobal && offset != 0) | |
| return false; | |
| for (size_t i = 0; i < target.components().size(); ++i) { | |
| const auto& tc = target.components()[i]; | |
| const auto& cc = candidate.components()[offset + i]; | |
| if (tc.special() != cc.special() || tc.name() != cc.name()) | |
| return false; | |
| if (tc.has_template()) { | |
| if (!cc.has_template() || tc.template_contents() != cc.template_contents()) | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool FrameMatchesFunction(const Frame* frame, const ParsedIdentifier& target_ident) { | |
| const Location& loc = frame->GetLocation(); | |
| if (!loc.has_symbols() || !loc.symbol()) | |
| return false; | |
| const Function* func = loc.symbol().Get()->As<Function>(); | |
| if (!func) | |
| return false; | |
| return MatchParsedIdentifier(target_ident, ToParsedIdentifier(func->GetIdentifier())); | |
| } | |
| bool FrameMatchesFileLine(const Frame* frame, const FileLine& target_file_line) { | |
| const Location& loc = frame->GetLocation(); | |
| if (!loc.file_line().is_valid()) | |
| return false; | |
| if (loc.file_line().line() != target_file_line.line()) | |
| return false; | |
| return PathEndsWith(loc.file_line().file(), target_file_line.file()); | |
| } | |
| // Ref-counted holder for a capture that async callbacks fill in. Each callback keeps a reference, | |
| // so the capture stays alive until the last callback finishes. | |
| struct CaptureState : public fxl::RefCountedThreadSafe<CaptureState> { | |
| TracepointFrameCapture capture; | |
| }; | |
| // Each AddCallback() token is wrapped in fit::defer_callback as soon as it is created. If an async | |
| // producer drops its callback without running it, the token still completes, so the joiner always | |
| // reaches its Ready callback and the stopped thread is not left blocked. | |
| void ExtractLocals(const fxl::RefPtr<JoinCallbacks<void>>& joiner, | |
| const fxl::RefPtr<CaptureState>& state, | |
| const fxl::RefPtr<EvalContext>& eval_context, Frame* frame) { | |
| state->capture.locals = std::map<std::string, TracepointValue>(); | |
| ErrOr<std::map<std::string, fxl::RefPtr<Variable>>> vars_or = frame->GetLocalVariables(); | |
| if (vars_or.has_error()) { | |
| state->capture.errors.push_back(vars_or.err().msg()); | |
| return; | |
| } | |
| for (const auto& [name, var] : vars_or.value()) { | |
| eval_context->GetVariableValue( | |
| var, [name = name, eval_context, state, | |
| done = fit::defer_callback(joiner->AddCallback())](ErrOrValue val_res) mutable { | |
| auto node = std::make_unique<FormatNode>(std::string(), val_res); | |
| auto* node_ptr = node.get(); | |
| FormatBufferOptions options; | |
| options.pointer_expand_depth = 0; | |
| DescribeFormatTreeNode(node_ptr, options, eval_context, | |
| fit::defer_callback([name, options, node = std::move(node), state, | |
| done = std::move(done)]() mutable { | |
| TracepointValue tv; | |
| if (node->err().has_error()) { | |
| tv.value = "<" + node->err().msg() + ">"; | |
| } else { | |
| tv.value = FormatTreeNode(*node, options).AsString(); | |
| tv.type = node->type(); | |
| } | |
| (*state->capture.locals)[name] = std::move(tv); | |
| done.call(); | |
| })); | |
| }); | |
| } | |
| } | |
| void ExtractExpressions(const fxl::RefPtr<JoinCallbacks<void>>& joiner, | |
| const fxl::RefPtr<CaptureState>& state, | |
| const fxl::RefPtr<EvalContext>& eval_context, | |
| const std::vector<std::string>& expressions) { | |
| if (expressions.empty()) | |
| return; | |
| state->capture.evaluations = std::map<std::string, TracepointEvaluation>(); | |
| for (const auto& expr : expressions) { | |
| auto node = std::make_unique<FormatNode>(std::string(), expr); | |
| auto* node_ptr = node.get(); | |
| FormatBufferOptions options; | |
| options.pointer_expand_depth = 0; | |
| DescribeFormatTreeNode( | |
| node_ptr, options, eval_context, | |
| fit::defer_callback([expr, options, node = std::move(node), state, | |
| done = fit::defer_callback(joiner->AddCallback())]() mutable { | |
| TracepointEvaluation te; | |
| if (node->err().has_error()) { | |
| te.error = node->err().msg(); | |
| state->capture.errors.push_back(node->err().msg()); | |
| } else { | |
| te.result = FormatTreeNode(*node, options).AsString(); | |
| te.type = node->type(); | |
| } | |
| state->capture.evaluations[expr] = std::move(te); | |
| done.call(); | |
| })); | |
| } | |
| } | |
| void ExtractRegisters(const fxl::RefPtr<JoinCallbacks<void>>& joiner, | |
| const fxl::RefPtr<CaptureState>& state, Frame* frame) { | |
| const auto* sync_regs = frame->GetRegisterCategorySync(debug::RegisterCategory::kGeneral); | |
| if (sync_regs) { | |
| state->capture.registers = FormatRegisterValues(*sync_regs); | |
| return; | |
| } | |
| frame->GetRegisterCategoryAsync( | |
| debug::RegisterCategory::kGeneral, false, | |
| [state, done = fit::defer_callback(joiner->AddCallback())]( | |
| const Err& err, const std::vector<debug::RegisterValue>& regs) mutable { | |
| if (err.has_error()) { | |
| state->capture.errors.push_back(err.msg()); | |
| } else { | |
| state->capture.registers = FormatRegisterValues(regs); | |
| } | |
| done.call(); | |
| }); | |
| } | |
| TracepointFrameRecord MakeFrameRecord(const Frame* f, size_t frame_index) { | |
| TracepointFrameRecord rec; | |
| rec.frame_index = frame_index; | |
| rec.ip = f->GetAddress(); | |
| rec.sp = f->GetStackPointer(); | |
| rec.trust = f->GetTrust(); | |
| const Location& loc = f->GetLocation(); | |
| if (loc.has_symbols() && loc.symbol()) { | |
| rec.name = loc.symbol().Get()->GetFullName(); | |
| } | |
| if (loc.file_line().is_valid()) { | |
| rec.file = loc.file_line().file(); | |
| rec.line = loc.file_line().line(); | |
| } | |
| rec.elided = false; | |
| return rec; | |
| } | |
| void DoCaptureFrameState(fxl::WeakPtr<Thread> thread, fxl::WeakPtr<Frame> frame, | |
| const TracepointSpec& spec, | |
| fit::callback<void(ErrOr<TracepointFrameCapture>)> cb) { | |
| if (!frame) { | |
| cb(Err("Frame destroyed during tracepoint capture.")); | |
| return; | |
| } | |
| auto state = fxl::MakeRefCounted<CaptureState>(); | |
| TracepointFrameCapture* capture = &state->capture; | |
| if (spec.frame) { | |
| TracepointFrameSummary summary; | |
| summary.ip = frame->GetAddress(); | |
| summary.sp = frame->GetStackPointer(); | |
| summary.trust = frame->GetTrust(); | |
| const Location& loc = frame->GetLocation(); | |
| if (loc.has_symbols() && loc.symbol()) { | |
| summary.name = loc.symbol().Get()->GetFullName(); | |
| } | |
| if (loc.file_line().is_valid()) { | |
| summary.file = loc.file_line().file(); | |
| summary.line = loc.file_line().line(); | |
| } | |
| capture->frame = std::move(summary); | |
| } | |
| if (spec.backtrace || spec.raw_backtrace) { | |
| if (thread) { | |
| const Frame* matched = frame.get(); | |
| capture->backtrace = CaptureThreadBacktrace(thread.get(), spec.raw_backtrace, {&matched, 1}); | |
| } else { | |
| capture->errors.push_back("Thread not available for backtrace capture."); | |
| } | |
| } | |
| auto joiner = fxl::MakeRefCounted<JoinCallbacks<void>>(); | |
| if (spec.registers) { | |
| ExtractRegisters(joiner, state, frame.get()); | |
| } | |
| auto eval_context = frame->GetEvalContext(); | |
| if (!eval_context && (spec.locals || !spec.expressions.empty())) { | |
| capture->errors.push_back("No eval context available for locals or expressions."); | |
| } else { | |
| if (spec.locals) | |
| ExtractLocals(joiner, state, eval_context, frame.get()); | |
| ExtractExpressions(joiner, state, eval_context, spec.expressions); | |
| } | |
| joiner->Ready([state, cb = std::move(cb)]() mutable { cb(std::move(state->capture)); }); | |
| } | |
| TracepointMatchResult MatchAddressFrames(Stack* stack, TargetPointer address, | |
| std::optional<uint32_t> max_matching_frames, | |
| FrameMatchMode mode) { | |
| TracepointMatchResult result; | |
| if (mode == FrameMatchMode::kLiveStopTopFrame) { | |
| for (size_t i = 0; i < stack->size(); ++i) { | |
| Frame* f = (*stack)[i]; | |
| if (f->GetAddress() != address) | |
| break; | |
| if (max_matching_frames.has_value() && result.frames.size() >= *max_matching_frames) { | |
| result.truncated = true; | |
| break; | |
| } | |
| result.frames.push_back(f); | |
| } | |
| return result; | |
| } | |
| for (size_t i = 0; i < stack->size(); ++i) { | |
| Frame* f = (*stack)[i]; | |
| if (f->GetAddress() == address) { | |
| if (max_matching_frames.has_value() && result.frames.size() >= *max_matching_frames) { | |
| result.truncated = true; | |
| break; | |
| } | |
| result.frames.push_back(f); | |
| } | |
| } | |
| return result; | |
| } | |
| template <typename Predicate> | |
| TracepointMatchResult MatchFramesWithPredicate(Stack* stack, | |
| std::optional<uint32_t> max_matching_frames, | |
| FrameMatchMode mode, Predicate pred) { | |
| TracepointMatchResult result; | |
| if (mode == FrameMatchMode::kLiveStopTopFrame) { | |
| TargetPointer top_ip = (*stack)[0]->GetAddress(); | |
| for (size_t i = 0; i < stack->size(); ++i) { | |
| Frame* f = (*stack)[i]; | |
| if (f->GetAddress() != top_ip) | |
| break; | |
| if (pred(f)) { | |
| if (max_matching_frames.has_value() && result.frames.size() >= *max_matching_frames) { | |
| result.truncated = true; | |
| break; | |
| } | |
| result.frames.push_back(f); | |
| } | |
| } | |
| if (result.frames.empty()) { | |
| result.frames.push_back((*stack)[0]); | |
| } | |
| return result; | |
| } | |
| for (size_t i = 0; i < stack->size(); ++i) { | |
| Frame* f = (*stack)[i]; | |
| if (pred(f)) { | |
| if (max_matching_frames.has_value() && result.frames.size() >= *max_matching_frames) { | |
| result.truncated = true; | |
| break; | |
| } | |
| result.frames.push_back(f); | |
| } | |
| } | |
| return result; | |
| } | |
| TracepointMatchResult MatchFileLineFrames(Stack* stack, const FileLine& file_line, | |
| std::optional<uint32_t> max_matching_frames, | |
| FrameMatchMode mode) { | |
| return MatchFramesWithPredicate(stack, max_matching_frames, mode, [&file_line](const Frame* f) { | |
| return FrameMatchesFileLine(f, file_line); | |
| }); | |
| } | |
| ErrOr<TracepointMatchResult> MatchFunctionFrames(Stack* stack, const std::string& function_name, | |
| std::optional<uint32_t> max_matching_frames, | |
| FrameMatchMode mode) { | |
| ParsedIdentifier target_ident; | |
| if (Err err = ExprParser::ParseIdentifier(function_name, &target_ident); err.has_error()) | |
| return err; | |
| return MatchFramesWithPredicate( | |
| stack, max_matching_frames, mode, | |
| [&target_ident](const Frame* f) { return FrameMatchesFunction(f, target_ident); }); | |
| } | |
| } // namespace | |
| Err TracepointSpec::Validate() const { | |
| if (max_matching_frames.has_value() && *max_matching_frames == 0) { | |
| return Err("max_matching_frames must be at least 1."); | |
| } | |
| size_t target_count = 0; | |
| if (function.has_value()) { | |
| if (function->empty()) { | |
| return Err("Function name cannot be empty for a tracepoint."); | |
| } | |
| target_count++; | |
| } | |
| if (file_line.has_value()) { | |
| if (!file_line->is_valid()) { | |
| return Err("Invalid file/line location specified for tracepoint."); | |
| } | |
| target_count++; | |
| } | |
| if (address.has_value()) { | |
| target_count++; | |
| } | |
| if (watch.has_value()) { | |
| if (!watch->address.has_value()) { | |
| return Err("Watchpoint tracepoints require a resolved target address."); | |
| } | |
| if (watch->access_type != debug_ipc::BreakpointType::kWrite && | |
| watch->access_type != debug_ipc::BreakpointType::kReadWrite) { | |
| return Err("Unsupported watchpoint access type (only write or read-write allowed)."); | |
| } | |
| target_count++; | |
| } | |
| if (target_count == 0) { | |
| return Err("A tracepoint must specify a function, file/line, address, or watch target."); | |
| } | |
| if (target_count > 1) { | |
| return Err( | |
| "A tracepoint can specify at most one of a function, file/line, address, or watch " | |
| "target."); | |
| } | |
| return Err(); | |
| } | |
| std::vector<TracepointFrameRecord> CaptureThreadBacktrace( | |
| const Thread* thread, bool raw_backtrace, std::span<const Frame* const> matched_frames) { | |
| std::vector<TracepointFrameRecord> records; | |
| if (!thread) | |
| return records; | |
| const Stack& stack = thread->GetStack(); | |
| records.reserve(stack.size()); | |
| if (raw_backtrace) { | |
| for (size_t i = 0; i < stack.size(); ++i) { | |
| records.push_back(MakeFrameRecord(stack[i], i)); | |
| } | |
| return records; | |
| } | |
| auto pretty_manager = thread->session()->system().pretty_stack_manager(); | |
| std::vector<PrettyStackManager::FrameEntry> entries; | |
| if (pretty_manager) { | |
| entries = pretty_manager->ProcessStack(stack); | |
| } | |
| if (entries.empty()) { | |
| for (size_t i = 0; i < stack.size(); ++i) { | |
| records.push_back(MakeFrameRecord(stack[i], i)); | |
| } | |
| return records; | |
| } | |
| for (const auto& entry : entries) { | |
| bool contains_matched = std::ranges::any_of(entry.frames, [&matched_frames](const Frame* f) { | |
| return std::ranges::find(matched_frames, f) != matched_frames.end(); | |
| }); | |
| for (size_t i = 0; i < entry.frames.size(); ++i) { | |
| TracepointFrameRecord rec = MakeFrameRecord(entry.frames[i], entry.begin_index + i); | |
| if (entry.match && !contains_matched) { | |
| rec.elided = (i > 0); | |
| rec.elide_description = entry.match.description; | |
| } | |
| records.push_back(std::move(rec)); | |
| } | |
| } | |
| return records; | |
| } | |
| ErrOr<TracepointMatchResult> MatchTracepointFrames(Stack* stack, const TracepointSpec& spec, | |
| FrameMatchMode mode) { | |
| if (Err err = spec.Validate(); err.has_error()) | |
| return err; | |
| if (mode == FrameMatchMode::kAllMatchingFrames && spec.watch.has_value()) { | |
| return Err("Watchpoint tracepoints do not support all-matching-frames mode."); | |
| } | |
| if (!stack || stack->empty()) | |
| return TracepointMatchResult(); | |
| if (spec.watch.has_value()) { | |
| return TracepointMatchResult{.frames = {(*stack)[0]}}; | |
| } | |
| if (spec.address.has_value()) { | |
| return MatchAddressFrames(stack, *spec.address, spec.max_matching_frames, mode); | |
| } | |
| if (spec.file_line.has_value()) { | |
| return MatchFileLineFrames(stack, *spec.file_line, spec.max_matching_frames, mode); | |
| } | |
| if (spec.function.has_value()) { | |
| return MatchFunctionFrames(stack, *spec.function, spec.max_matching_frames, mode); | |
| } | |
| // Validate() requires exactly one target, so this is not reachable. | |
| return Err("Tracepoint has no target."); | |
| } | |
| void CaptureTracepointFrame(fxl::WeakPtr<Thread> thread, fxl::WeakPtr<Frame> frame, | |
| const TracepointSpec& spec, | |
| fit::callback<void(ErrOr<TracepointFrameCapture>)> cb) { | |
| if (!frame) { | |
| cb(Err("Frame destroyed during tracepoint capture.")); | |
| return; | |
| } | |
| if (spec.condition.has_value() && !spec.condition->empty()) { | |
| auto eval_context = frame->GetEvalContext(); | |
| if (!eval_context) { | |
| cb(Err("No eval context available for condition evaluation.")); | |
| return; | |
| } | |
| EvalExpression(*spec.condition, eval_context, true, | |
| [thread, frame, spec, cb = std::move(cb)](ErrOrValue val_res) mutable { | |
| if (!frame) { | |
| cb(Err("Frame destroyed during condition evaluation.")); | |
| return; | |
| } | |
| if (val_res.has_error()) { | |
| TracepointFrameCapture capture; | |
| capture.matched = false; | |
| capture.errors.push_back(val_res.err().msg()); | |
| cb(capture); | |
| return; | |
| } | |
| auto bool_res = | |
| CastNumericExprValueToBool(frame->GetEvalContext(), val_res.value()); | |
| if (bool_res.has_error()) { | |
| TracepointFrameCapture capture; | |
| capture.matched = false; | |
| capture.errors.push_back(bool_res.err().msg()); | |
| cb(capture); | |
| return; | |
| } | |
| if (!bool_res.value()) { | |
| TracepointFrameCapture capture; | |
| capture.matched = false; | |
| cb(capture); | |
| return; | |
| } | |
| DoCaptureFrameState(thread, frame, spec, std::move(cb)); | |
| }); | |
| return; | |
| } | |
| DoCaptureFrameState(thread, frame, spec, std::move(cb)); | |
| } | |
| } // namespace zxdb |