parsing callstacks and emitting zones for each frame

This commit is contained in:
Marcos Slomp
2026-06-15 11:25:59 -07:00
parent ebc23ac4f0
commit ff70f87107

View File

@@ -3,6 +3,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
@@ -172,6 +173,21 @@ static int64_t ParseRetval( const char* line )
return neg ? -v : v;
}
// Extract the function name from a strace -k frame line, e.g.:
// "/lib/libc.so.6(__read_nocancel+0x7) [0xf1e07]" → "__read_nocancel"
static std::string ParseFrameName( const std::string& frame )
{
const char* p = frame.c_str();
const char* open = strchr( p, '(' );
if( !open ) return frame;
const char* name = open + 1;
const char* plus = strchr( name, '+' );
const char* close = strchr( name, ')' );
const char* end = ( plus && ( !close || plus < close ) ) ? plus : close;
if( !end ) return frame;
return std::string( name, end );
}
struct PendingEntry
{
uint64_t ts_begin;
@@ -179,6 +195,17 @@ struct PendingEntry
std::string args_text;
};
// A complete or resumed syscall zone held until its trailing callstack frames are collected.
struct PendingComplete
{
uint64_t tid;
uint64_t ts_begin;
uint64_t ts_end;
std::string name;
std::string args;
std::vector<std::string> frames; // innermost-first, as strace emits them
};
int main( int argc, char** argv )
{
if( argc != 3 ) Usage();
@@ -199,6 +226,40 @@ int main( int argc, char** argv )
std::unordered_map<uint64_t, std::string> threadNames;
std::unordered_map<uint64_t, PendingEntry> pending; // tid → in-flight syscall
// Holds a complete/resumed zone while we collect its trailing callstack frames.
std::optional<PendingComplete> pending_complete;
// Emit a pending zone as nested Tracy zones:
// outer callstack frames (one begin per frame, outermost first)
// syscall zone (begin + end)
// outer callstack frames (one end per frame, innermost first)
//
// frames[0] is the innermost frame (the libc/kernel syscall stub) and is
// skipped — it is already represented by the syscall zone itself.
auto flushComplete = [&]()
{
if( !pending_complete ) return;
const auto& pc = *pending_complete;
const size_t user_start = pc.frames.empty() ? 0 : 1; // skip innermost stub
// Open outer frames, outermost first (= reversed from strace order).
for( int i = (int)pc.frames.size() - 1; i >= (int)user_start; i-- )
timeline.push_back( { pc.tid, pc.ts_begin,
ParseFrameName( pc.frames[i] ), pc.frames[i],
false, "", 0 } );
// The syscall zone itself.
timeline.push_back( { pc.tid, pc.ts_begin, pc.name, pc.args, false, "", 0 } );
timeline.push_back( { pc.tid, pc.ts_end, "", "", true, "", 0 } );
// Close outer frames, innermost first.
for( size_t i = user_start; i < pc.frames.size(); i++ )
timeline.push_back( { pc.tid, pc.ts_end, "", "", true, "", 0 } );
pending_complete.reset();
};
char buf[65536];
// TID used when strace output has no [pid N] prefix (single-threaded traces or
// the very first calls of a process before any fork).
@@ -208,6 +269,24 @@ int main( int argc, char** argv )
{
const char* p = buf;
// Callstack frame from strace -k: " > /path/lib(func+0xoff) [0xaddr]"
// Must be checked before the isdigit guard.
if( p[0] == ' ' && p[1] == '>' )
{
if( pending_complete )
{
const char* frame = p + 2;
while( *frame == ' ' ) frame++;
std::string f( frame );
while( !f.empty() && strchr( "\r\n", f.back() ) ) f.pop_back();
if( !f.empty() ) pending_complete->frames.push_back( std::move( f ) );
}
continue;
}
// Any non-frame line closes the pending zone before we process it.
flushComplete();
// Only process lines whose first character is a digit.
// Silently skips strace warnings, "Process N attached/detached", etc.
if( !isdigit( (unsigned char)*p ) ) continue;
@@ -269,8 +348,9 @@ int main( int argc, char** argv )
if( it != pending.end() ) pending.erase( it );
// ts = timestamp of the return line; use as zone end.
timeline.push_back( { tid, ts_begin, syscall_name, std::move( args_text ), false, "", 0 } );
timeline.push_back( { tid, ts, "", "", true, "", 0 } );
pending_complete = PendingComplete{ tid, ts_begin, ts,
std::move( syscall_name ),
std::move( args_text ), {} };
}
// --- Process exit / kill ----------------------------------------
// "+++ exited with N +++" | "+++ killed by SIGNAL +++"
@@ -327,13 +407,15 @@ int main( int argc, char** argv )
}
}
timeline.push_back( { tid, ts, syscall_name, std::move( args ), false, "", 0 } );
timeline.push_back( { tid, ts + dur_ns, "", "", true, "", 0 } );
pending_complete = PendingComplete{ tid, ts, ts + dur_ns,
syscall_name,
std::move( args ), {} };
}
}
}
if( fin != stdin ) fclose( fin );
flushComplete(); // flush the last zone if the file ends with callstack frames
// Sort by timestamp. strace output is mostly ordered, but interleaved threads
// and resumed events can place end-of-zone before begin-of-zone after sorting.