153 changes: 130 additions & 23 deletions lldb/source/Commands/CommandObjectThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,131 @@ class CommandObjectTraceStop : public CommandObjectMultipleThreads {
}
};

static ThreadSP GetSingleThreadFromArgs(ExecutionContext &exe_ctx, Args &args,
CommandReturnObject &result) {
if (args.GetArgumentCount() == 0)
return exe_ctx.GetThreadSP();

const char *arg = args.GetArgumentAtIndex(0);
uint32_t thread_idx;

if (!llvm::to_integer(arg, thread_idx)) {
result.AppendErrorWithFormat("invalid thread specification: \"%s\"\n", arg);
return nullptr;
}
ThreadSP thread_sp =
exe_ctx.GetProcessRef().GetThreadList().FindThreadByIndexID(thread_idx);
if (!thread_sp)
result.AppendErrorWithFormat("no thread with index: \"%s\"\n", arg);
return thread_sp;
}

// CommandObjectTraceDumpFunctionCalls
#define LLDB_OPTIONS_thread_trace_dump_function_calls
#include "CommandOptions.inc"

class CommandObjectTraceDumpFunctionCalls : public CommandObjectParsed {
public:
class CommandOptions : public Options {
public:
CommandOptions() { OptionParsingStarting(nullptr); }

~CommandOptions() override = default;

Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
ExecutionContext *execution_context) override {
Status error;
const int short_option = m_getopt_table[option_idx].val;

switch (short_option) {
case 'j': {
m_dumper_options.json = true;
break;
}
case 'J': {
m_dumper_options.json = true;
m_dumper_options.pretty_print_json = true;
break;
}
case 'F': {
m_output_file.emplace(option_arg);
break;
}
default:
llvm_unreachable("Unimplemented option");
}
return error;
}

void OptionParsingStarting(ExecutionContext *execution_context) override {
m_dumper_options = {};
m_output_file = llvm::None;
}

llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
return llvm::makeArrayRef(g_thread_trace_dump_function_calls_options);
}

static const size_t kDefaultCount = 20;

// Instance variables to hold the values for command options.
TraceDumperOptions m_dumper_options;
llvm::Optional<FileSpec> m_output_file;
};

CommandObjectTraceDumpFunctionCalls(CommandInterpreter &interpreter)
: CommandObjectParsed(
interpreter, "thread trace dump function-calls",
"Dump the traced function-calls for one thread. If no "
"thread is specified, the current thread is used.",
nullptr,
eCommandRequiresProcess | eCommandRequiresThread |
eCommandTryTargetAPILock | eCommandProcessMustBeLaunched |
eCommandProcessMustBePaused | eCommandProcessMustBeTraced) {
CommandArgumentData thread_arg{eArgTypeThreadIndex, eArgRepeatOptional};
m_arguments.push_back({thread_arg});
}

~CommandObjectTraceDumpFunctionCalls() override = default;

Options *GetOptions() override { return &m_options; }

protected:
bool DoExecute(Args &args, CommandReturnObject &result) override {
ThreadSP thread_sp = GetSingleThreadFromArgs(m_exe_ctx, args, result);
if (!thread_sp) {
result.AppendError("invalid thread\n");
return false;
}

llvm::Expected<TraceCursorSP> cursor_or_error =
m_exe_ctx.GetTargetSP()->GetTrace()->CreateNewCursor(*thread_sp);

if (!cursor_or_error) {
result.AppendError(llvm::toString(cursor_or_error.takeError()));
return false;
}
TraceCursorSP &cursor_sp = *cursor_or_error;

llvm::Optional<StreamFile> out_file;
if (m_options.m_output_file) {
out_file.emplace(m_options.m_output_file->GetPath().c_str(),
File::eOpenOptionWriteOnly | File::eOpenOptionCanCreate);
}

m_options.m_dumper_options.forwards = true;

TraceDumper dumper(std::move(cursor_sp),
out_file ? *out_file : result.GetOutputStream(),
m_options.m_dumper_options);

dumper.DumpFunctionCalls();
return true;
}

CommandOptions m_options;
};

// CommandObjectTraceDumpInstructions
#define LLDB_OPTIONS_thread_trace_dump_instructions
#include "CommandOptions.inc"
Expand Down Expand Up @@ -2238,28 +2363,8 @@ class CommandObjectTraceDumpInstructions : public CommandObjectParsed {
}

protected:
ThreadSP GetThread(Args &args, CommandReturnObject &result) {
if (args.GetArgumentCount() == 0)
return m_exe_ctx.GetThreadSP();

const char *arg = args.GetArgumentAtIndex(0);
uint32_t thread_idx;

if (!llvm::to_integer(arg, thread_idx)) {
result.AppendErrorWithFormat("invalid thread specification: \"%s\"\n",
arg);
return nullptr;
}
ThreadSP thread_sp =
m_exe_ctx.GetProcessRef().GetThreadList().FindThreadByIndexID(
thread_idx);
if (!thread_sp)
result.AppendErrorWithFormat("no thread with index: \"%s\"\n", arg);
return thread_sp;
}

bool DoExecute(Args &args, CommandReturnObject &result) override {
ThreadSP thread_sp = GetThread(args, result);
ThreadSP thread_sp = GetSingleThreadFromArgs(m_exe_ctx, args, result);
if (!thread_sp) {
result.AppendError("invalid thread\n");
return false;
Expand Down Expand Up @@ -2290,8 +2395,7 @@ class CommandObjectTraceDumpInstructions : public CommandObjectParsed {
llvm::Optional<StreamFile> out_file;
if (m_options.m_output_file) {
out_file.emplace(m_options.m_output_file->GetPath().c_str(),
File::eOpenOptionWriteOnly | File::eOpenOptionCanCreate,
lldb::eFilePermissionsFileDefault);
File::eOpenOptionWriteOnly | File::eOpenOptionCanCreate);
}

if (m_options.m_continue && !m_last_id) {
Expand Down Expand Up @@ -2401,6 +2505,9 @@ class CommandObjectMultiwordTraceDump : public CommandObjectMultiword {
LoadSubCommand(
"instructions",
CommandObjectSP(new CommandObjectTraceDumpInstructions(interpreter)));
LoadSubCommand(
"function-calls",
CommandObjectSP(new CommandObjectTraceDumpFunctionCalls(interpreter)));
LoadSubCommand(
"info", CommandObjectSP(new CommandObjectTraceDumpInfo(interpreter)));
}
Expand Down
13 changes: 13 additions & 0 deletions lldb/source/Commands/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,19 @@ let Command = "thread plan list" in {
Desc<"Display thread plans for unreported threads">;
}

let Command = "thread trace dump function calls" in {
def thread_trace_dump_function_calls_file : Option<"file", "F">, Group<1>,
Arg<"Filename">,
Desc<"Dump the function calls to a file instead of the standard output.">;
def thread_trace_dump_function_calls_json: Option<"json", "j">,
Group<1>,
Desc<"Dump in simple JSON format.">;
def thread_trace_dump_function_calls_pretty_json: Option<"pretty-json", "J">,
Group<1>,
Desc<"Dump in JSON format but pretty printing the output for easier "
"readability.">;
}

let Command = "thread trace dump instructions" in {
def thread_trace_dump_instructions_forwards: Option<"forwards", "f">,
Group<1>,
Expand Down
504 changes: 483 additions & 21 deletions lldb/source/Target/TraceDumper.cpp

Large diffs are not rendered by default.

325 changes: 325 additions & 0 deletions lldb/test/API/commands/trace/TestTraceDumpFunctionCalls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
from intelpt_testcase import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test.decorators import *

class TestTraceDumpInfo(TraceIntelPTTestCaseBase):
def testDumpSimpleFunctionCalls(self):
self.expect("trace load -v " +
os.path.join(self.getSourceDir(), "intelpt-trace", "trace.json"))

self.expect("thread trace dump function-calls 2",
error=True, substrs=['error: no thread with index: "2"'])

self.expect("thread trace dump function-calls 1 -j",
substrs=['[{"tracedSegments":[{"firstInstructionId":"1","lastInstructionId":"22"}]}]'])

self.expect("thread trace dump function-calls 1 -J",
substrs=['''[
{
"tracedSegments": [
{
"firstInstructionId": "1",
"lastInstructionId": "22"
}
]
}
]'''])

# We test first some code without function call
self.expect("thread trace dump function-calls 1",
substrs=['''thread #1: tid = 3842849
[call tree #0]
a.out`main + 4 at main.cpp:2 to 4:0 [1, 22]'''])

def testFunctionCallsWithErrors(self):
self.expect("trace load -v " +
os.path.join(self.getSourceDir(), "intelpt-multi-core-trace", "trace.json"))

# We expect that tracing errors appear as a different tree
self.expect("thread trace dump function-calls 2",
substrs=['''thread #2: tid = 3497496
[call tree #0]
m.out`foo() + 65 at multi_thread.cpp:12:21 to 12:21 [4, 19524]
[call tree #1]
<tracing errors> [19532, 19532]'''])

self.expect("thread trace dump function-calls 2 -J",
substrs=['''[
{
"tracedSegments": [
{
"firstInstructionId": "4",
"lastInstructionId": "19524"
}
]
},
{
"tracedSegments": [
{
"firstInstructionId": "19532",
"lastInstructionId": "19532"
}
]
}
]'''])

self.expect("thread trace dump function-calls 3",
substrs=['''thread #3: tid = 3497497
[call tree #0]
m.out`bar() + 30 at multi_thread.cpp:19:3 to 20:6 [5, 61831]
[call tree #1]
<tracing errors> [61834, 61834]'''])

self.expect("thread trace dump function-calls 3 -J",
substrs=['''[
{
"tracedSegments": [
{
"firstInstructionId": "5",
"lastInstructionId": "61831"
}
]
},
{
"tracedSegments": [
{
"firstInstructionId": "61834",
"lastInstructionId": "61834"
}
]
}
]'''])

def testInlineFunctionCalls(self):
self.expect("file " + os.path.join(self.getSourceDir(), "inline-function", "a.out"))
self.expect("b main") # we'll trace from the beginning of main
self.expect("b 17")
self.expect("r")
self.expect("thread trace start")
self.expect("c")
self.expect("thread trace dump function-calls",
substrs=['''[call tree #0]
a.out`main + 8 at inline.cpp:15:7 to 16:14 [1, 5]
a.out`foo(int) at inline.cpp:8:16 to 9:15 [6, 13]
a.out`foo(int) + 22 [inlined] mult(int, int) at inline.cpp:2:7 to 5:10 [14, 21]
a.out`foo(int) + 49 at inline.cpp:9:15 to 12:1 [22, 26]
a.out`main + 25 at inline.cpp:16:14 to 16:14 [27, 27]'''])

self.expect("thread trace dump function-calls -J",
substrs=['''[
{
"tracedSegments": [
{
"firstInstructionId": "1",
"lastInstructionId": "5",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "6",
"lastInstructionId": "13",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "14",
"lastInstructionId": "21"
}
]
}
},
{
"firstInstructionId": "22",
"lastInstructionId": "26"
}
]
}
},
{
"firstInstructionId": "27",
"lastInstructionId": "27"
}
]
}
]'''])

def testIncompleteInlineFunctionCalls(self):
self.expect("file " + os.path.join(self.getSourceDir(), "inline-function", "a.out"))
self.expect("b 4") # we'll trace from the middle of the inline method
self.expect("b 17")
self.expect("r")
self.expect("thread trace start")
self.expect("c")
self.expect("thread trace dump function-calls",
substrs=['''[call tree #0]
a.out`main
a.out`foo(int)
a.out`foo(int) + 36 [inlined] mult(int, int) + 14 at inline.cpp:4:5 to 5:10 [1, 5]
a.out`foo(int) + 49 at inline.cpp:9:15 to 12:1 [6, 10]
a.out`main + 25 at inline.cpp:16:14 to 16:14 [11, 11]'''])

self.expect("thread trace dump function-calls -J",
substrs=['''[
{
"untracedPrefixSegment": {
"nestedCall": {
"untracedPrefixSegment": {
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "1",
"lastInstructionId": "5"
}
]
}
},
"tracedSegments": [
{
"firstInstructionId": "6",
"lastInstructionId": "10"
}
]
}
},
"tracedSegments": [
{
"firstInstructionId": "11",
"lastInstructionId": "11"
}
]
}
]'''])

def testMultifileFunctionCalls(self):
# This test is extremely important because it first calls the method foo() which requires going through the dynamic linking.
# You'll see the entry "a.out`symbol stub for: foo()" which will invoke the ld linker, which will in turn find the actual foo
# function and eventually invoke it. However, we don't have the image of the linker in the trace bundle, so we'll see errors
# because the decoder couldn't find the linker binary! After those failures, the linker will resume right where we return to
# main after foo() finished.
# Then, we call foo() again, but because it has already been loaded by the linker, we don't invoke the linker anymore! And
# we'll see a nice tree without errors in this second invocation. Something interesting happens here. We still have an
# invocation to the symbol stub for foo(), but it modifies the stack so that when we return from foo() we don't stop again
# at the symbol stub, but instead we return directly to main. This is an example of returning several levels up in the
# call stack.
# Not only that, we also have an inline method in between.
self.expect("trace load " + os.path.join(self.getSourceDir(), "intelpt-trace-multi-file", "multi-file-no-ld.json"))
self.expect("thread trace dump function-calls",
substrs=['''thread #1: tid = 815455
[call tree #0]
a.out`main + 15 at main.cpp:10 to 10:0 [1, 1]
a.out`symbol stub for: foo() to <+11> [3, 5]
a.out`a.out[0x0000000000400510] to a.out[0x0000000000400516] [6, 7]
[call tree #1]
<tracing errors> [8, 8]
[call tree #2]
a.out`main + 20 at main.cpp:10 to 12:0 [10, 14]
a.out`main + 34 [inlined] inline_function() at main.cpp:4 to 6:0 [16, 20]
a.out`main + 55 at main.cpp:14 to 16:0 [21, 25]
a.out`symbol stub for: foo() to <+0> [26, 26]
libfoo.so`foo() at foo.cpp:3 to 4:0 [27, 30]
libfoo.so`symbol stub for: bar() to <+0> [31, 31]
libbar.so`bar() at bar.cpp:1 to 4:0 [32, 40]
libfoo.so`foo() + 13 at foo.cpp:4 to 6:0 [41, 48]
a.out`main + 68 at main.cpp:16 to 16:0 [49, 51]'''])

self.expect("thread trace dump function-calls -J",
substrs=['''[
{
"tracedSegments": [
{
"firstInstructionId": "1",
"lastInstructionId": "1",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "3",
"lastInstructionId": "5",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "6",
"lastInstructionId": "7"
}
]
}
}
]
}
}
]
},
{
"tracedSegments": [
{
"firstInstructionId": "8",
"lastInstructionId": "8"
}
]
},
{
"tracedSegments": [
{
"firstInstructionId": "10",
"lastInstructionId": "14",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "16",
"lastInstructionId": "20"
}
]
}
},
{
"firstInstructionId": "21",
"lastInstructionId": "25",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "26",
"lastInstructionId": "26",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "27",
"lastInstructionId": "30",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "31",
"lastInstructionId": "31",
"nestedCall": {
"tracedSegments": [
{
"firstInstructionId": "32",
"lastInstructionId": "40"
}
]
}
}
]
}
},
{
"firstInstructionId": "41",
"lastInstructionId": "48"
}
]
}
}
]
}
},
{
"firstInstructionId": "49",
"lastInstructionId": "51"
}
]
}
]'''])
4 changes: 2 additions & 2 deletions lldb/test/API/commands/trace/TestTraceLoad.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def testLoadMultiCoreTrace(self):
"individualCounts": {
"software disabled tracing": 1,
"trace synchronization point": 1,
"HW clock tick": 8,
"CPU core changed": 1
"CPU core changed": 1,
"HW clock tick": 8
}
},
"continuousExecutions": 1,
Expand Down
Binary file not shown.
18 changes: 18 additions & 0 deletions lldb/test/API/commands/trace/inline-function/inline.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
__attribute__((always_inline)) inline int mult(int x, int y) {
int f = x * y;
f++;
f *= f;
return f;
}

int foo(int x) {
int z = mult(x, x - 1);
z++;
return z;
}

int main() {
int x = 12;
int z = foo(x);
return z + x;
}