Compare commits

...

1 Commits

Author SHA1 Message Date
Marcos Slomp
0c541a2e1b experimental stracy tool 2026-06-15 11:03:08 -07:00
2 changed files with 404 additions and 0 deletions

26
stracy/CMakeLists.txt Normal file
View File

@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.16)
set(NO_STATISTICS ON)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake)
set(CMAKE_CXX_STANDARD 20)
project(
tracy-import-strace
LANGUAGES C CXX
VERSION ${TRACY_VERSION_STRING}
)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/config.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/vendor.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/server.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/GitRef.cmake)
add_executable(tracy-import-strace
src/stracy.cpp
)
add_git_ref(tracy-import-strace)
target_link_libraries(tracy-import-strace PRIVATE TracyServer)
set_property(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} PROPERTY VS_STARTUP_PROJECT tracy-import-strace)

378
stracy/src/stracy.cpp Normal file
View File

@@ -0,0 +1,378 @@
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
#include "../../server/TracyFileWrite.hpp"
#include "../../server/TracyWorker.hpp"
#include "../../public/common/TracyVersion.hpp"
#include "GitRef.hpp"
static void Usage()
{
printf( "tracy-import-strace %d.%d.%d / %s\n\n",
tracy::Version::Major, tracy::Version::Minor, tracy::Version::Patch, tracy::GitRef );
printf( "Usage: tracy-import-strace input.strace output.tracy\n" );
printf( " tracy-import-strace - output.tracy (read from stdin)\n\n" );
printf( "Recommended strace invocation:\n" );
printf( " strace -ttt -T -f -o trace.log <program> [args...]\n" );
printf( " strace -ttt -T -f -p <pid> -o trace.log\n\n" );
printf( "Mapped events:\n" );
printf( " syscall entry/return -> timeline zones\n" );
printf( " signals -> messages\n" );
printf( " process exit/kill -> messages\n" );
exit( 1 );
}
// Parse "SSSSSSSSSS.UUUUUU" → nanoseconds without floating-point precision loss.
// Advances *p past the consumed characters. Returns UINT64_MAX on parse error.
static uint64_t ParseTimestamp( const char*& p )
{
if( !isdigit( (unsigned char)*p ) ) return UINT64_MAX;
uint64_t sec = 0;
while( isdigit( (unsigned char)*p ) )
sec = sec * 10 + ( *p++ - '0' );
if( *p != '.' ) return UINT64_MAX;
p++;
uint64_t usec = 0;
int digits = 0;
while( isdigit( (unsigned char)*p ) && digits < 6 )
{
usec = usec * 10 + ( *p++ - '0' );
digits++;
}
for( ; digits < 6; digits++ ) usec *= 10; // pad to microseconds if fewer digits
while( isdigit( (unsigned char)*p ) ) p++; // discard extra precision
return sec * 1000000000ULL + usec * 1000ULL;
}
// Consume "[pid N]" prefix and return N, or return default_tid if not present.
// Advances *p past the consumed characters (including trailing space).
static uint64_t ConsumePidPrefix( const char*& p, uint64_t default_tid )
{
if( p[0] != '[' || strncmp( p, "[pid", 4 ) != 0 ) return default_tid;
const char* q = p + 4;
while( *q == ' ' ) q++;
if( !isdigit( (unsigned char)*q ) ) return default_tid;
uint64_t tid = 0;
while( isdigit( (unsigned char)*q ) )
tid = tid * 10 + ( *q++ - '0' );
while( *q == ' ' ) q++;
if( *q != ']' ) return default_tid;
p = q + 1;
return tid;
}
// Parse "<N.NNNNNN>" trailing duration → nanoseconds. Returns 0 if not present.
static uint64_t ParseTrailingDuration( const char* line )
{
const char* p = line + strlen( line );
while( p > line && strchr( " \r\n", p[-1] ) ) p--;
if( p == line || p[-1] != '>' ) return 0;
p--;
const char* end = p;
while( p > line && p[-1] != '<' ) p--;
if( p == line || p[-1] != '<' ) return 0;
char tmp[32];
size_t len = end - p;
if( len == 0 || len >= sizeof( tmp ) ) return 0;
memcpy( tmp, p, len );
tmp[len] = '\0';
double dur_sec = 0.0;
if( sscanf( tmp, "%lf", &dur_sec ) != 1 ) return 0;
return (uint64_t)( dur_sec * 1e9 );
}
// Return the syscall name — the identifier before the first '('. Empty on failure.
static std::string ParseSyscallName( const char* p )
{
const char* start = p;
while( *p && *p != '(' && *p != ' ' && *p != '\n' ) p++;
if( p == start || *p != '(' ) return {};
return std::string( start, p );
}
// Extract syscall arguments for a COMPLETE call line.
// Finds the last " = " (the retval separator), then the ')' before it.
// This correctly handles " = " appearing inside string arguments.
static std::string ExtractArgsComplete( const char* line_start )
{
const char* open = strchr( line_start, '(' );
if( !open ) return {};
const char* args = open + 1;
// Walk the whole line to find the last " = "
const char* last_eq = nullptr;
for( const char* p = args; *p; p++ )
if( p[0] == ' ' && p[1] == '=' && p[2] == ' ' )
last_eq = p;
if( !last_eq ) return {};
// Find the closing ')' that immediately precedes " = "
const char* close = last_eq;
while( close > args && *close != ')' ) close--;
if( close <= args ) return {};
return std::string( args, close );
}
// Extract syscall arguments for an UNFINISHED call line, up to the delimiter.
static std::string ExtractArgsUnfinished( const char* line_start, const char* delim )
{
const char* open = strchr( line_start, '(' );
if( !open ) return {};
const char* args = open + 1;
const char* end = strstr( args, delim );
if( !end || end <= args ) return {};
while( end > args && end[-1] == ' ' ) end--; // trim trailing space
return std::string( args, end );
}
// Extract a quoted string from strace arg output, e.g. the "name" in
// prctl(PR_SET_NAME, "name") = 0. Returns empty string if not found.
static std::string ExtractQuotedString( const char* p )
{
const char* open = strchr( p, '"' );
if( !open ) return {};
open++;
const char* close = strchr( open, '"' );
if( !close ) return {};
return std::string( open, close );
}
// Parse the integer return value from a complete strace line " = N <dur>".
// Returns -1 on failure.
static int64_t ParseRetval( const char* line )
{
const char* eq = strstr( line, " = " );
if( !eq ) return -1;
const char* p = eq + 3;
bool neg = ( *p == '-' );
if( neg ) p++;
if( !isdigit( (unsigned char)*p ) ) return -1;
int64_t v = 0;
while( isdigit( (unsigned char)*p ) ) v = v * 10 + (*p++ - '0');
return neg ? -v : v;
}
struct PendingEntry
{
uint64_t ts_begin;
std::string syscall_name;
std::string args_text;
};
int main( int argc, char** argv )
{
if( argc != 3 ) Usage();
const char* input_path = argv[1];
const char* output_path = argv[2];
FILE* fin = ( strcmp( input_path, "-" ) == 0 ) ? stdin : fopen( input_path, "r" );
if( !fin )
{
fprintf( stderr, "Cannot open input: %s\n", input_path );
return 1;
}
std::vector<tracy::Worker::ImportEventTimeline> timeline;
std::vector<tracy::Worker::ImportEventMessages> messages;
std::vector<tracy::Worker::ImportEventPlots> plots; // required by Worker API; unused
std::unordered_map<uint64_t, std::string> threadNames;
std::unordered_map<uint64_t, PendingEntry> pending; // tid → in-flight syscall
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).
constexpr uint64_t kDefaultTid = 0;
while( fgets( buf, sizeof( buf ), fin ) )
{
const char* p = buf;
// Only process lines whose first character is a digit.
// Silently skips strace warnings, "Process N attached/detached", etc.
if( !isdigit( (unsigned char)*p ) ) continue;
// Detect which line format strace used:
// Format A (-o to file/pipe): "PID TIMESTAMP syscall..."
// Format B (to stderr/tty): "TIMESTAMP [pid N] syscall..."
// Distinguish by whether the first numeric token contains a '.' (timestamp)
// or is followed by a space without a '.' (bare PID).
uint64_t tid = kDefaultTid;
{
const char* q = p;
while( isdigit( (unsigned char)*q ) ) q++;
if( *q == ' ' )
{
// Format A: leading PID token
while( isdigit( (unsigned char)*p ) ) tid = tid * 10 + (*p++ - '0');
while( *p == ' ' ) p++;
}
// else: Format B — timestamp comes first, [pid N] (if any) follows it
}
uint64_t ts = ParseTimestamp( p );
if( ts == UINT64_MAX ) continue;
while( *p == ' ' ) p++;
// Format B may carry "[pid N]" after the timestamp; Format A already has tid.
if( tid == kDefaultTid )
tid = ConsumePidPrefix( p, kDefaultTid );
while( *p == ' ' ) p++;
// Register the thread name the first time we see this TID.
if( threadNames.find( tid ) == threadNames.end() )
{
char name[32];
if( tid == kDefaultTid )
snprintf( name, sizeof( name ), "main" );
else
snprintf( name, sizeof( name ), "%" PRIu64, tid );
threadNames[tid] = name;
}
// --- Resumed syscall -------------------------------------------
// "<... SYSCALL resumed>) = RETVAL <DUR>"
if( strncmp( p, "<...", 4 ) == 0 )
{
const char* name_start = p + 4;
while( *name_start == ' ' ) name_start++;
const char* resumed = strstr( name_start, " resumed" );
if( !resumed ) continue;
std::string syscall_name( name_start, resumed );
auto it = pending.find( tid );
uint64_t ts_begin = ( it != pending.end() ) ? it->second.ts_begin : ts;
std::string args_text = ( it != pending.end() ) ? it->second.args_text : "";
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 } );
}
// --- Process exit / kill ----------------------------------------
// "+++ exited with N +++" | "+++ killed by SIGNAL +++"
else if( strncmp( p, "+++", 3 ) == 0 )
{
std::string raw( p );
while( !raw.empty() && strchr( "\r\n", raw.back() ) ) raw.pop_back();
messages.push_back( { tid, ts, std::to_string( tid ) + ": " + raw } );
}
// --- Signal ------------------------------------------------------
// "--- SIGNAME {...} ---"
else if( strncmp( p, "---", 3 ) == 0 )
{
std::string raw( p );
while( !raw.empty() && strchr( "\r\n", raw.back() ) ) raw.pop_back();
messages.push_back( { tid, ts, std::to_string( tid ) + ": " + raw } );
}
// --- Regular syscall (complete or unfinished) --------------------
else
{
std::string syscall_name = ParseSyscallName( p );
if( syscall_name.empty() ) continue;
if( strstr( p, "<unfinished ...>" ) )
{
// Syscall blocked — park in the pending table.
std::string args = ExtractArgsUnfinished( p, " <unfinished" );
pending[tid] = PendingEntry{ ts, std::move( syscall_name ), std::move( args ) };
}
else
{
// Complete call with return value (and optional duration from -T).
uint64_t dur_ns = ParseTrailingDuration( buf );
std::string args = ExtractArgsComplete( p );
// prctl(PR_SET_NAME, "name") — update the thread's display name.
if( syscall_name == "prctl" && strstr( p, "PR_SET_NAME" ) )
{
std::string name = ExtractQuotedString( strstr( p, "PR_SET_NAME" ) );
if( !name.empty() )
threadNames[tid] = std::move( name );
}
// clone/clone3 with CLONE_THREAD — pre-name the child thread so it
// has a label before it calls prctl itself.
else if( ( syscall_name == "clone" || syscall_name == "clone3" ) &&
strstr( p, "CLONE_THREAD" ) )
{
int64_t child_tid = ParseRetval( buf );
if( child_tid > 0 && threadNames.find( (uint64_t)child_tid ) == threadNames.end() )
{
char name[32];
snprintf( name, sizeof( name ), "%" PRId64, child_tid );
threadNames[(uint64_t)child_tid] = name;
}
}
timeline.push_back( { tid, ts, syscall_name, std::move( args ), false, "", 0 } );
timeline.push_back( { tid, ts + dur_ns, "", "", true, "", 0 } );
}
}
}
if( fin != stdin ) fclose( fin );
// 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.
std::stable_sort( timeline.begin(), timeline.end(),
[]( const auto& a, const auto& b ) { return a.timestamp < b.timestamp; } );
std::stable_sort( messages.begin(), messages.end(),
[]( const auto& a, const auto& b ) { return a.timestamp < b.timestamp; } );
// Shift all timestamps so the trace starts at t = 0.
uint64_t mts = UINT64_MAX;
for( const auto& e : timeline ) mts = std::min( mts, e.timestamp );
for( const auto& e : messages ) mts = std::min( mts, e.timestamp );
if( mts == UINT64_MAX ) mts = 0;
for( auto& e : timeline ) e.timestamp -= mts;
for( auto& e : messages ) e.timestamp -= mts;
const size_t zone_count = timeline.size() / 2;
fprintf( stderr, "Parsed %zu zones, %zu messages, %zu threads\n",
zone_count, messages.size(), threadNames.size() );
// Tracy Worker expects basenames, not full paths.
auto basename = []( const char* path ) -> const char* {
const char* s = path;
for( const char* q = path; *q; q++ )
if( *q == '/' || *q == '\\' ) s = q + 1;
return s;
};
tracy::Worker worker( basename( output_path ), basename( input_path ),
timeline, messages, plots, threadNames );
auto w = std::unique_ptr<tracy::FileWrite>( tracy::FileWrite::Open( output_path, tracy::FileCompression::Fast ) );
if( !w )
{
fprintf( stderr, "Cannot open output file: %s\n", output_path );
return 1;
}
worker.Write( *w, false );
return 0;
}