As a follow up of D103588, I'm reinitiating the discussion with a new proposal for traversing instructions in a trace which uses the feedback gotten in that diff. See the embedded documentation in TraceCursor for more information. The idea is to offer an OOP way to traverse instructions exposing a minimal interface that makes no assumptions on: - the number of instructions in the trace (i.e. having indices for instructions might be impractical for gigantic intel-pt traces, as it would require to decode the entire trace). This renders the use of indices to point to instructions impractical. Traces are big and expensive, and the consumer should try to do look linear lookups (forwards and/or backwards) and avoid random accesses (the API could be extended though, but for now I want to dicard that funcionality and leave the API extensible if needed). - the way the instructions are represented internally by each Trace plug-in. They could be mmap'ed from a file, exist in plain vector or generated on the fly as the user requests the data. - the actual data structure used internally for each plug-in. Ideas like having a struct TraceInstruction have been discarded because that would make the plug-in follow a certain data type, which might be costly. Instead, the user can ask the cursor for each independent property of the instruction it's pointing at. The way to get a cursor is to ask Trace.h for the end or being cursor or a thread's trace. There are some benefits of this approach: - there's little cost to create a cursor, and this allows for lazily decoding a trace as the user requests data. - each trace plug-in could decide how to cache the instructions it generates. For example, if a trace is small, it might decide to keep everything in memory, or if the trace is massive, it might decide to keep around the last thousands of instructions to speed up local searches. - a cursor can outlive a stop point, which makes trace comparison for live processes feasible. An application of this is to compare profiling data of two runs of the same function, which should be doable with intel pt. Differential Revision: https://reviews.llvm.org/D104422
481 lines
16 KiB
C++
481 lines
16 KiB
C++
//===-- Trace.cpp ---------------------------------------------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "lldb/Target/Trace.h"
|
|
|
|
#include "llvm/Support/Format.h"
|
|
|
|
#include "lldb/Core/Module.h"
|
|
#include "lldb/Core/PluginManager.h"
|
|
#include "lldb/Symbol/Function.h"
|
|
#include "lldb/Target/ExecutionContext.h"
|
|
#include "lldb/Target/Process.h"
|
|
#include "lldb/Target/SectionLoadList.h"
|
|
#include "lldb/Target/Thread.h"
|
|
#include "lldb/Target/ThreadPostMortemTrace.h"
|
|
#include "lldb/Utility/Stream.h"
|
|
|
|
using namespace lldb;
|
|
using namespace lldb_private;
|
|
using namespace llvm;
|
|
|
|
// Helper structs used to extract the type of a trace session json without
|
|
// having to parse the entire object.
|
|
|
|
struct JSONSimplePluginSettings {
|
|
std::string type;
|
|
};
|
|
|
|
struct JSONSimpleTraceSession {
|
|
JSONSimplePluginSettings trace;
|
|
};
|
|
|
|
namespace llvm {
|
|
namespace json {
|
|
|
|
bool fromJSON(const Value &value, JSONSimplePluginSettings &plugin_settings,
|
|
Path path) {
|
|
json::ObjectMapper o(value, path);
|
|
return o && o.map("type", plugin_settings.type);
|
|
}
|
|
|
|
bool fromJSON(const Value &value, JSONSimpleTraceSession &session, Path path) {
|
|
json::ObjectMapper o(value, path);
|
|
return o && o.map("trace", session.trace);
|
|
}
|
|
|
|
} // namespace json
|
|
} // namespace llvm
|
|
|
|
static Error createInvalidPlugInError(StringRef plugin_name) {
|
|
return createStringError(
|
|
std::errc::invalid_argument,
|
|
"no trace plug-in matches the specified type: \"%s\"",
|
|
plugin_name.data());
|
|
}
|
|
|
|
Expected<lldb::TraceSP>
|
|
Trace::FindPluginForPostMortemProcess(Debugger &debugger,
|
|
const json::Value &trace_session_file,
|
|
StringRef session_file_dir) {
|
|
JSONSimpleTraceSession json_session;
|
|
json::Path::Root root("traceSession");
|
|
if (!json::fromJSON(trace_session_file, json_session, root))
|
|
return root.getError();
|
|
|
|
ConstString plugin_name(json_session.trace.type);
|
|
if (auto create_callback = PluginManager::GetTraceCreateCallback(plugin_name))
|
|
return create_callback(trace_session_file, session_file_dir, debugger);
|
|
|
|
return createInvalidPlugInError(json_session.trace.type);
|
|
}
|
|
|
|
Expected<lldb::TraceSP>
|
|
Trace::FindPluginForLiveProcess(llvm::StringRef plugin_name, Process &process) {
|
|
if (!process.IsLiveDebugSession())
|
|
return createStringError(inconvertibleErrorCode(),
|
|
"Can't trace non-live processes");
|
|
|
|
ConstString name(plugin_name);
|
|
if (auto create_callback =
|
|
PluginManager::GetTraceCreateCallbackForLiveProcess(name))
|
|
return create_callback(process);
|
|
|
|
return createInvalidPlugInError(plugin_name);
|
|
}
|
|
|
|
Expected<StringRef> Trace::FindPluginSchema(StringRef name) {
|
|
ConstString plugin_name(name);
|
|
StringRef schema = PluginManager::GetTraceSchema(plugin_name);
|
|
if (!schema.empty())
|
|
return schema;
|
|
|
|
return createInvalidPlugInError(name);
|
|
}
|
|
|
|
static int GetNumberOfDigits(size_t num) {
|
|
return num == 0 ? 1 : static_cast<int>(log10(num)) + 1;
|
|
}
|
|
|
|
/// \return
|
|
/// \b true if the provided line entries match line, column and source file.
|
|
/// This function assumes that the line entries are valid.
|
|
static bool FileLineAndColumnMatches(const LineEntry &a, const LineEntry &b) {
|
|
if (a.line != b.line)
|
|
return false;
|
|
if (a.column != b.column)
|
|
return false;
|
|
return a.file == b.file;
|
|
}
|
|
|
|
// This custom LineEntry validator is neded because some line_entries have
|
|
// 0 as line, which is meaningless. Notice that LineEntry::IsValid only
|
|
// checks that line is not LLDB_INVALID_LINE_NUMBER, i.e. UINT32_MAX.
|
|
static bool IsLineEntryValid(const LineEntry &line_entry) {
|
|
return line_entry.IsValid() && line_entry.line > 0;
|
|
}
|
|
|
|
/// Helper structure for \a TraverseInstructionsWithSymbolInfo.
|
|
struct InstructionSymbolInfo {
|
|
SymbolContext sc;
|
|
Address address;
|
|
lldb::addr_t load_address;
|
|
lldb::DisassemblerSP disassembler;
|
|
lldb::InstructionSP instruction;
|
|
lldb_private::ExecutionContext exe_ctx;
|
|
};
|
|
|
|
/// InstructionSymbolInfo object with symbol information for the given
|
|
/// instruction, calculated efficiently.
|
|
///
|
|
/// \param[in] symbol_scope
|
|
/// If not \b 0, then the \a InstructionSymbolInfo will have its
|
|
/// SymbolContext calculated up to that level.
|
|
///
|
|
/// \param[in] include_disassembler
|
|
/// If \b true, then the \a InstructionSymbolInfo will have the
|
|
/// \a disassembler and \a instruction objects calculated.
|
|
static void TraverseInstructionsWithSymbolInfo(
|
|
Trace &trace, Thread &thread, size_t position,
|
|
Trace::TraceDirection direction, SymbolContextItem symbol_scope,
|
|
bool include_disassembler,
|
|
std::function<bool(size_t index, Expected<InstructionSymbolInfo> insn)>
|
|
callback) {
|
|
InstructionSymbolInfo prev_insn;
|
|
|
|
Target &target = thread.GetProcess()->GetTarget();
|
|
ExecutionContext exe_ctx;
|
|
target.CalculateExecutionContext(exe_ctx);
|
|
const ArchSpec &arch = target.GetArchitecture();
|
|
|
|
// Find the symbol context for the given address reusing the previous
|
|
// instruction's symbol context when possible.
|
|
auto calculate_symbol_context = [&](const Address &address) {
|
|
AddressRange range;
|
|
if (prev_insn.sc.GetAddressRange(symbol_scope, 0,
|
|
/*inline_block_range*/ false, range) &&
|
|
range.Contains(address))
|
|
return prev_insn.sc;
|
|
|
|
SymbolContext sc;
|
|
address.CalculateSymbolContext(&sc, symbol_scope);
|
|
return sc;
|
|
};
|
|
|
|
// Find the disassembler for the given address reusing the previous
|
|
// instruction's disassembler when possible.
|
|
auto calculate_disass = [&](const Address &address, const SymbolContext &sc) {
|
|
if (prev_insn.disassembler) {
|
|
if (InstructionSP instruction =
|
|
prev_insn.disassembler->GetInstructionList()
|
|
.GetInstructionAtAddress(address))
|
|
return std::make_tuple(prev_insn.disassembler, instruction);
|
|
}
|
|
|
|
if (sc.function) {
|
|
if (DisassemblerSP disassembler =
|
|
sc.function->GetInstructions(exe_ctx, nullptr)) {
|
|
if (InstructionSP instruction =
|
|
disassembler->GetInstructionList().GetInstructionAtAddress(
|
|
address))
|
|
return std::make_tuple(disassembler, instruction);
|
|
}
|
|
}
|
|
// We fallback to a single instruction disassembler
|
|
AddressRange range(address, arch.GetMaximumOpcodeByteSize());
|
|
DisassemblerSP disassembler =
|
|
Disassembler::DisassembleRange(arch, /*plugin_name*/ nullptr,
|
|
/*flavor*/ nullptr, target, range);
|
|
return std::make_tuple(disassembler,
|
|
disassembler ? disassembler->GetInstructionList()
|
|
.GetInstructionAtAddress(address)
|
|
: InstructionSP());
|
|
};
|
|
|
|
trace.TraverseInstructions(
|
|
thread, position, direction,
|
|
[&](size_t index, Expected<lldb::addr_t> load_address) -> bool {
|
|
if (!load_address)
|
|
return callback(index, load_address.takeError());
|
|
|
|
InstructionSymbolInfo insn;
|
|
insn.load_address = *load_address;
|
|
insn.exe_ctx = exe_ctx;
|
|
insn.address.SetLoadAddress(*load_address, &target);
|
|
if (symbol_scope != 0)
|
|
insn.sc = calculate_symbol_context(insn.address);
|
|
if (include_disassembler)
|
|
std::tie(insn.disassembler, insn.instruction) =
|
|
calculate_disass(insn.address, insn.sc);
|
|
prev_insn = insn;
|
|
return callback(index, insn);
|
|
});
|
|
}
|
|
|
|
/// Compare the symbol contexts of the provided \a InstructionSymbolInfo
|
|
/// objects.
|
|
///
|
|
/// \return
|
|
/// \a true if both instructions belong to the same scope level analized
|
|
/// in the following order:
|
|
/// - module
|
|
/// - symbol
|
|
/// - function
|
|
/// - line
|
|
static bool
|
|
IsSameInstructionSymbolContext(const InstructionSymbolInfo &prev_insn,
|
|
const InstructionSymbolInfo &insn) {
|
|
// module checks
|
|
if (insn.sc.module_sp != prev_insn.sc.module_sp)
|
|
return false;
|
|
|
|
// symbol checks
|
|
if (insn.sc.symbol != prev_insn.sc.symbol)
|
|
return false;
|
|
|
|
// function checks
|
|
if (!insn.sc.function && !prev_insn.sc.function)
|
|
return true;
|
|
else if (insn.sc.function != prev_insn.sc.function)
|
|
return false;
|
|
|
|
// line entry checks
|
|
const bool curr_line_valid = IsLineEntryValid(insn.sc.line_entry);
|
|
const bool prev_line_valid = IsLineEntryValid(prev_insn.sc.line_entry);
|
|
if (curr_line_valid && prev_line_valid)
|
|
return FileLineAndColumnMatches(insn.sc.line_entry,
|
|
prev_insn.sc.line_entry);
|
|
return curr_line_valid == prev_line_valid;
|
|
}
|
|
|
|
/// Dump the symbol context of the given instruction address if it's different
|
|
/// from the symbol context of the previous instruction in the trace.
|
|
///
|
|
/// \param[in] prev_sc
|
|
/// The symbol context of the previous instruction in the trace.
|
|
///
|
|
/// \param[in] address
|
|
/// The address whose symbol information will be dumped.
|
|
///
|
|
/// \return
|
|
/// The symbol context of the current address, which might differ from the
|
|
/// previous one.
|
|
static void
|
|
DumpInstructionSymbolContext(Stream &s,
|
|
Optional<InstructionSymbolInfo> prev_insn,
|
|
InstructionSymbolInfo &insn) {
|
|
if (prev_insn && IsSameInstructionSymbolContext(*prev_insn, insn))
|
|
return;
|
|
|
|
s.Printf(" ");
|
|
|
|
if (!insn.sc.module_sp)
|
|
s.Printf("(none)");
|
|
else if (!insn.sc.function && !insn.sc.symbol)
|
|
s.Printf("%s`(none)",
|
|
insn.sc.module_sp->GetFileSpec().GetFilename().AsCString());
|
|
else
|
|
insn.sc.DumpStopContext(&s, insn.exe_ctx.GetTargetPtr(), insn.address,
|
|
/*show_fullpath*/ false,
|
|
/*show_module*/ true, /*show_inlined_frames*/ false,
|
|
/*show_function_arguments*/ true,
|
|
/*show_function_name*/ true);
|
|
s.Printf("\n");
|
|
}
|
|
|
|
static void DumpInstructionDisassembly(Stream &s, InstructionSymbolInfo &insn) {
|
|
if (!insn.instruction)
|
|
return;
|
|
s.Printf(" ");
|
|
insn.instruction->Dump(&s, /*show_address*/ false, /*show_bytes*/ false,
|
|
/*max_opcode_byte_size*/ 0, &insn.exe_ctx, &insn.sc,
|
|
/*prev_sym_ctx*/ nullptr,
|
|
/*disassembly_addr_format*/ nullptr,
|
|
/*max_address_text_size*/ 0);
|
|
}
|
|
|
|
void Trace::DumpTraceInstructions(Thread &thread, Stream &s, size_t count,
|
|
size_t end_position, bool raw) {
|
|
Optional<size_t> instructions_count = GetInstructionCount(thread);
|
|
if (!instructions_count) {
|
|
s.Printf("thread #%u: tid = %" PRIu64 ", not traced\n", thread.GetIndexID(),
|
|
thread.GetID());
|
|
return;
|
|
}
|
|
|
|
s.Printf("thread #%u: tid = %" PRIu64 ", total instructions = %zu\n",
|
|
thread.GetIndexID(), thread.GetID(), *instructions_count);
|
|
|
|
if (count == 0 || end_position >= *instructions_count)
|
|
return;
|
|
|
|
int digits_count = GetNumberOfDigits(end_position);
|
|
size_t start_position =
|
|
end_position + 1 < count ? 0 : end_position + 1 - count;
|
|
auto printInstructionIndex = [&](size_t index) {
|
|
s.Printf(" [%*zu] ", digits_count, index);
|
|
};
|
|
|
|
bool was_prev_instruction_an_error = false;
|
|
Optional<InstructionSymbolInfo> prev_insn;
|
|
|
|
TraverseInstructionsWithSymbolInfo(
|
|
*this, thread, start_position, TraceDirection::Forwards,
|
|
eSymbolContextEverything, /*disassembler*/ true,
|
|
[&](size_t index, Expected<InstructionSymbolInfo> insn) -> bool {
|
|
if (!insn) {
|
|
printInstructionIndex(index);
|
|
s << toString(insn.takeError());
|
|
|
|
prev_insn = None;
|
|
was_prev_instruction_an_error = true;
|
|
} else {
|
|
if (was_prev_instruction_an_error)
|
|
s.Printf(" ...missing instructions\n");
|
|
|
|
if (!raw)
|
|
DumpInstructionSymbolContext(s, prev_insn, *insn);
|
|
|
|
printInstructionIndex(index);
|
|
s.Printf("0x%016" PRIx64, insn->load_address);
|
|
|
|
if (!raw)
|
|
DumpInstructionDisassembly(s, *insn);
|
|
|
|
prev_insn = *insn;
|
|
was_prev_instruction_an_error = false;
|
|
}
|
|
|
|
s.Printf("\n");
|
|
return index < end_position;
|
|
});
|
|
}
|
|
|
|
Error Trace::Start(const llvm::json::Value &request) {
|
|
if (!m_live_process)
|
|
return createStringError(inconvertibleErrorCode(),
|
|
"Tracing requires a live process.");
|
|
return m_live_process->TraceStart(request);
|
|
}
|
|
|
|
Error Trace::Stop() {
|
|
if (!m_live_process)
|
|
return createStringError(inconvertibleErrorCode(),
|
|
"Tracing requires a live process.");
|
|
return m_live_process->TraceStop(
|
|
TraceStopRequest(GetPluginName().AsCString()));
|
|
}
|
|
|
|
Error Trace::Stop(llvm::ArrayRef<lldb::tid_t> tids) {
|
|
if (!m_live_process)
|
|
return createStringError(inconvertibleErrorCode(),
|
|
"Tracing requires a live process.");
|
|
return m_live_process->TraceStop(
|
|
TraceStopRequest(GetPluginName().AsCString(), tids));
|
|
}
|
|
|
|
Expected<std::string> Trace::GetLiveProcessState() {
|
|
if (!m_live_process)
|
|
return createStringError(inconvertibleErrorCode(),
|
|
"Tracing requires a live process.");
|
|
return m_live_process->TraceGetState(GetPluginName().AsCString());
|
|
}
|
|
|
|
Optional<size_t> Trace::GetLiveThreadBinaryDataSize(lldb::tid_t tid,
|
|
llvm::StringRef kind) {
|
|
auto it = m_live_thread_data.find(tid);
|
|
if (it == m_live_thread_data.end())
|
|
return None;
|
|
std::unordered_map<std::string, size_t> &single_thread_data = it->second;
|
|
auto single_thread_data_it = single_thread_data.find(kind.str());
|
|
if (single_thread_data_it == single_thread_data.end())
|
|
return None;
|
|
return single_thread_data_it->second;
|
|
}
|
|
|
|
Optional<size_t> Trace::GetLiveProcessBinaryDataSize(llvm::StringRef kind) {
|
|
auto data_it = m_live_process_data.find(kind.str());
|
|
if (data_it == m_live_process_data.end())
|
|
return None;
|
|
return data_it->second;
|
|
}
|
|
|
|
Expected<ArrayRef<uint8_t>>
|
|
Trace::GetLiveThreadBinaryData(lldb::tid_t tid, llvm::StringRef kind) {
|
|
if (!m_live_process)
|
|
return createStringError(inconvertibleErrorCode(),
|
|
"Tracing requires a live process.");
|
|
llvm::Optional<size_t> size = GetLiveThreadBinaryDataSize(tid, kind);
|
|
if (!size)
|
|
return createStringError(
|
|
inconvertibleErrorCode(),
|
|
"Tracing data \"%s\" is not available for thread %" PRIu64 ".",
|
|
kind.data(), tid);
|
|
|
|
TraceGetBinaryDataRequest request{GetPluginName().AsCString(), kind.str(),
|
|
static_cast<int64_t>(tid), 0,
|
|
static_cast<int64_t>(*size)};
|
|
return m_live_process->TraceGetBinaryData(request);
|
|
}
|
|
|
|
Expected<ArrayRef<uint8_t>>
|
|
Trace::GetLiveProcessBinaryData(llvm::StringRef kind) {
|
|
if (!m_live_process)
|
|
return createStringError(inconvertibleErrorCode(),
|
|
"Tracing requires a live process.");
|
|
llvm::Optional<size_t> size = GetLiveProcessBinaryDataSize(kind);
|
|
if (!size)
|
|
return createStringError(
|
|
inconvertibleErrorCode(),
|
|
"Tracing data \"%s\" is not available for the process.", kind.data());
|
|
|
|
TraceGetBinaryDataRequest request{GetPluginName().AsCString(), kind.str(),
|
|
None, 0, static_cast<int64_t>(*size)};
|
|
return m_live_process->TraceGetBinaryData(request);
|
|
}
|
|
|
|
void Trace::RefreshLiveProcessState() {
|
|
if (!m_live_process)
|
|
return;
|
|
|
|
uint32_t new_stop_id = m_live_process->GetStopID();
|
|
if (new_stop_id == m_stop_id)
|
|
return;
|
|
|
|
m_stop_id = new_stop_id;
|
|
m_live_thread_data.clear();
|
|
|
|
Expected<std::string> json_string = GetLiveProcessState();
|
|
if (!json_string) {
|
|
DoRefreshLiveProcessState(json_string.takeError());
|
|
return;
|
|
}
|
|
Expected<TraceGetStateResponse> live_process_state =
|
|
json::parse<TraceGetStateResponse>(*json_string, "TraceGetStateResponse");
|
|
if (!live_process_state) {
|
|
DoRefreshLiveProcessState(live_process_state.takeError());
|
|
return;
|
|
}
|
|
|
|
for (const TraceThreadState &thread_state :
|
|
live_process_state->tracedThreads) {
|
|
for (const TraceBinaryData &item : thread_state.binaryData)
|
|
m_live_thread_data[thread_state.tid][item.kind] = item.size;
|
|
}
|
|
|
|
for (const TraceBinaryData &item : live_process_state->processBinaryData)
|
|
m_live_process_data[item.kind] = item.size;
|
|
|
|
DoRefreshLiveProcessState(std::move(live_process_state));
|
|
}
|
|
|
|
uint32_t Trace::GetStopID() {
|
|
RefreshLiveProcessState();
|
|
return m_stop_id;
|
|
}
|