Files
tracy/monitor/src/monitor.cpp
2026-09-06 01:07:47 +02:00

1257 lines
45 KiB
C++

// tracy-monitor: profile a process that was not built with the Tracy client.
// Launch mode: tracy-monitor [OPTIONS] program [args...]; attach mode:
// tracy-monitor [OPTIONS] -p PID | -n NAME. Runs the standard Tracy client in
// "external mode". With TRACY_ON_DEMAND, samples are captured only while a
// Tracy server is connected. See --help for usage and permission requirements.
//
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <memory>
#include <sys/ioctl.h>
#include <linux/perf_event.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../public/tracy/Tracy.hpp"
#include "../public/common/TracyVersion.hpp"
#include "../public/client/TracyCallstack.hpp"
#include "GitRef.hpp"
#include "../../server/TracyFileWrite.hpp"
#include "../../server/TracyPrint.hpp"
#include "../../server/TracyWorker.hpp"
#include "../../util/CaptureConsole.hpp"
#include "../../util/CaptureFileBackup.hpp"
// The monitor is only meaningful with the client's full sampling +
// symbolication stack. These configurations would either fail to build
// (TRACY_NO_CALLSTACK: the external-target API lives in the callstack
// code) or silently produce an empty capture (IsSystemTracingFailed()
// cannot see them), so reject them here rather than in the client.
#if defined TRACY_NO_CALLSTACK
# error "tracy-monitor requires callstack support: TRACY_NO_CALLSTACK is not supported"
#endif
#if defined TRACY_NO_SYSTEM_TRACING
# error "tracy-monitor requires system tracing: TRACY_NO_SYSTEM_TRACING is not supported"
#endif
#if defined TRACY_NO_SAMPLING
# error "tracy-monitor requires callstack sampling: TRACY_NO_SAMPLING is not supported"
#endif
#if defined TRACY_SAMPLING_PROFILER_MANUAL_START
# error "tracy-monitor starts sampling itself via StartupProfiler(): TRACY_SAMPLING_PROFILER_MANUAL_START is not supported"
#endif
static volatile sig_atomic_t s_shouldQuit = 0;
static pid_t s_targetPid = 0;
static bool s_isForked = false;
static void SignalHandler( int /*sig*/ )
{
s_shouldQuit = 1;
if( s_isForked && s_targetPid != 0 )
{
// We launched the target under ptrace, so forward the signal to wake
// a blocking waitpid and let the child exit. kill() is async-signal-safe.
kill( s_targetPid, SIGINT );
}
}
// kill( pid, 0 ) succeeds even for zombies, so liveness also checks the
// /proc state: a zombie is dead - nothing left to sample, and in attach
// mode we are not the parent, so we can neither reap it nor learn its exit.
static bool ProcessIsAlive( pid_t pid )
{
if( kill( pid, 0 ) != 0 ) return false;
char path[32];
snprintf( path, sizeof( path ), "/proc/%d/stat", (int)pid );
FILE* f = fopen( path, "r" );
if( !f ) return false;
char buf[512];
const size_t n = fread( buf, 1, sizeof( buf ) - 1, f );
fclose( f );
buf[n] = 0;
// the state char follows the last ')' (comm may contain spaces/parens)
const char* state = strrchr( buf, ')' );
return state && state[1] == ' ' && state[2] != 'Z' && state[2] != '\0';
}
// --- capability probes --------------------------------------------------
static const char* GetTraceFsPath()
{
static char path[ 4096 ];
static char debugPath[ 4096 ];
bool haveDebug = false;
FILE* f = fopen( "/proc/mounts", "r" );
if( !f ) return nullptr;
char line[ 4096 ];
while( fgets( line, sizeof( line ), f ) )
{
char dir[ 4096 ];
char type[ 64 ];
if( sscanf( line, "%*s %4095s %63s", dir, type ) != 2 ) continue;
if( strcmp( type, "tracefs" ) == 0 )
{
snprintf( path, sizeof( path ), "%s", dir );
fclose( f );
return path;
}
if( !haveDebug && strcmp( type, "debugfs" ) == 0 )
{
snprintf( debugPath, sizeof( debugPath ), "%s/tracing", dir );
haveDebug = true;
}
}
fclose( f );
if( haveDebug )
{
snprintf( path, sizeof( path ), "%s", debugPath );
return path;
}
return nullptr;
}
enum class TracepointStatus { Open, NoTraceFs, IdNotReadable, OpenDenied };
// open a system-wide per-CPU tracepoint (the client's sched/vblank shape);
// report the failure distinctly - tracefs readability (root) and the perf open (paranoid/CAP_PERFMON) are independent gates.
static TracepointStatus CheckSystemWideTracepoint( const char* eventPath )
{
const char* traceFs = GetTraceFsPath();
if( !traceFs ) return TracepointStatus::NoTraceFs;
char path[512];
snprintf( path, sizeof( path ), "%s%s/id", traceFs, eventPath );
FILE* f = fopen( path, "r" );
if( !f ) return TracepointStatus::IdNotReadable;
int id = -1;
if( fscanf( f, "%d", &id ) != 1 ) id = -1;
fclose( f );
if( id < 0 ) return TracepointStatus::IdNotReadable;
perf_event_attr pe = {};
pe.type = PERF_TYPE_TRACEPOINT;
pe.size = sizeof( pe );
pe.config = (uint64_t)id;
pe.sample_period = 1;
pe.sample_type = PERF_SAMPLE_TIME | PERF_SAMPLE_RAW;
pe.disabled = 1;
pe.inherit = 1;
const long fd = syscall( __NR_perf_event_open, &pe, -1, 0, -1, 0 );
if( fd < 0 ) return TracepointStatus::OpenDenied;
close( (int)fd );
return TracepointStatus::Open;
}
// Power plots read RAPL energy counters (the client scans the same tree).
static bool CanReadRapl()
{
const char* base = "/sys/devices/virtual/powercap/intel-rapl";
DIR* dir = opendir( base );
if( !dir ) return false;
struct dirent* ent;
bool ok = false;
while( ( ent = readdir( dir ) ) )
{
if( ent->d_type != DT_DIR || strncmp( ent->d_name, "intel-rapl:", 11 ) != 0 ) continue;
char path[512];
snprintf( path, sizeof( path ), "%s/%s/energy_uj", base, ent->d_name );
FILE* f = fopen( path, "r" );
if( f )
{
fclose( f );
ok = true;
break;
}
}
closedir( dir );
return ok;
}
// --- sampling preflight ---------------------------------------------------
// verify the per-CPU pid-filtered event mechanism the client relies on:
// perf_event_open(attr, target, cpu) with a direct mmap of the event's ring
// (the normal client's own topology); exclude_kernel=1 so it works at any
// paranoid level.
static bool PreflightOutputMechanism( pid_t pid )
{
perf_event_attr pe = {};
pe.type = PERF_TYPE_SOFTWARE;
pe.size = sizeof( pe );
pe.config = PERF_COUNT_SW_CPU_CLOCK;
pe.sample_type = PERF_SAMPLE_TID | PERF_SAMPLE_TIME;
pe.sample_freq = 100;
pe.freq = 1;
pe.disabled = 1;
pe.inherit = 1;
pe.exclude_kernel = 1;
pe.exclude_callchain_kernel = 1;
const long fd = syscall( __NR_perf_event_open, &pe, pid, 0, -1, 0 );
if( fd < 0 )
{
fprintf( stderr, "Warning: preflight per-CPU event open failed: %s\n", strerror( errno ) );
return false;
}
const size_t mapSize = 64 * 1024 + 4096;
void* map = mmap( nullptr, mapSize, PROT_READ | PROT_WRITE, MAP_SHARED, (int)fd, 0 );
bool ok = map != MAP_FAILED;
if( !ok ) fprintf( stderr, "Warning: preflight ring mmap refused: %s (kernel ABI mismatch?)\n", strerror( errno ) );
if( map != MAP_FAILED ) munmap( map, mapSize );
close( (int)fd );
return ok;
}
// probe whether the client's per-thread hardware PMU counters can open at all
// (the client drops them silently on failure, losing the IPC/cache/branch plots); informational only.
static bool ProbeHwCounters( pid_t pid, int& errOut )
{
perf_event_attr pe = {};
pe.type = PERF_TYPE_HARDWARE;
pe.size = sizeof( pe );
pe.config = PERF_COUNT_HW_CPU_CYCLES;
pe.disabled = 1;
pe.precise_ip = 3;
long fd = syscall( __NR_perf_event_open, &pe, pid, -1, -1, 0 );
while( fd < 0 && pe.precise_ip > 0 )
{
pe.precise_ip--;
fd = syscall( __NR_perf_event_open, &pe, pid, -1, -1, 0 );
}
if( fd < 0 && ( errno == EACCES || errno == EPERM ) )
{
pe.exclude_kernel = 1;
fd = syscall( __NR_perf_event_open, &pe, pid, -1, -1, 0 );
}
if( fd >= 0 )
{
close( (int)fd );
return true;
}
errOut = errno;
return false;
}
// mirror the client's exact external perf_event_open (SysTraceStart/OpenSampleEvent):
// a per-CPU event with a pid filter on the target (direct ring mmap), plus an
// informational hardware PMU probe. Failing fast here yields an accurate
// capability report instead of starting the profiler with no samples.
static bool PreflightSamplingEvent( pid_t pid, bool& kernelFrames, bool& hwStats, int& hwErrno )
{
perf_event_attr pe = {};
pe.type = PERF_TYPE_SOFTWARE;
pe.size = sizeof( pe );
pe.config = PERF_COUNT_SW_CPU_CLOCK;
pe.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_CALLCHAIN;
pe.sample_freq = 1000;
pe.freq = 1;
pe.disabled = 1;
pe.inherit = 1;
long dataFd = syscall( __NR_perf_event_open, &pe, pid, 0, -1, 0 );
if( dataFd < 0 )
{
const int err = errno;
if( err == ESRCH )
{
fprintf( stderr, "Target process %d no longer exists.\n", (int)pid );
return false;
}
if( err == EACCES || err == EPERM )
{
// no kernel access: retry user-space-only, in both the event and the callchain
pe.exclude_kernel = 1;
pe.exclude_callchain_kernel = 1;
dataFd = syscall( __NR_perf_event_open, &pe, pid, 0, -1, 0 );
if( dataFd >= 0 )
{
close( (int)dataFd );
kernelFrames = false;
}
}
else
{
// any other error (EINVAL, ENOSYS under seccomp, exhaustion): the client's same
// event shape would fail identically and produce an empty capture, so fail here.
fprintf( stderr, "Cannot open perf events for pid %d: %s\n", (int)pid, strerror( err ) );
return false;
}
}
else
{
close( (int)dataFd );
kernelFrames = true;
}
if( dataFd < 0 )
{
fprintf( stderr, "Cannot open perf events for pid %d: %s\n", (int)pid, strerror( errno ) );
fprintf( stderr, "Profiling another process requires one of:\n" );
fprintf( stderr, " - running as root (or CAP_PERFMON + CAP_SYS_PTRACE), or\n" );
fprintf( stderr, " - /proc/sys/kernel/perf_event_paranoid <= 1 for kernel frames,\n" );
fprintf( stderr, " <= -1 for system-wide events (context switches / per-thread\n" );
fprintf( stderr, " CPU / wait stacks), or\n" );
fprintf( stderr, " - in attach mode: the target must belong to the same user\n" );
fprintf( stderr, " (uid and gid) and be dumpable, or the monitor must hold\n" );
fprintf( stderr, " CAP_SYS_PTRACE (yama ptrace_scope does not apply: sampling\n" );
fprintf( stderr, " uses ptrace READ access, and the monitor never attaches).\n" );
return false;
}
if( !PreflightOutputMechanism( pid ) )
{
fprintf( stderr, "Per-CPU sample rings are unavailable on this kernel; external sampling will not work.\n" );
return false;
}
hwStats = ProbeHwCounters( pid, hwErrno );
return true;
}
// --- reporting --------------------------------------------------------------
static const char* FormatHz( int hz )
{
static char buf[32];
if( hz >= 1000 && hz % 1000 == 0 )
{
snprintf( buf, sizeof( buf ), "%d kHz", hz / 1000 );
}
else
{
snprintf( buf, sizeof( buf ), "%d Hz", hz );
}
return buf;
}
static int EffectiveSamplingHz()
{
const char* env = getenv( "TRACY_SAMPLING_HZ" );
int hz = 10000; // client default on Linux
if( env )
{
const int parsed = atoi( env );
if( parsed > 0 ) hz = ( parsed > 1000000 ) ? 1000000 : parsed;
}
// mirror the client's clamp: the kernel caps the rate at perf_event_max_sample_rate
FILE* f = fopen( "/proc/sys/kernel/perf_event_max_sample_rate", "r" );
if( f )
{
int sysMax = 0;
if( fscanf( f, "%d", &sysMax ) == 1 && sysMax > 0 && sysMax < hz ) hz = sysMax;
fclose( f );
}
return hz;
}
static int EffectivePort()
{
const char* env = getenv( "TRACY_PORT" );
if( env )
{
const int port = atoi( env );
if( port > 0 && port <= 65535 ) return port;
}
return 8086;
}
static const char* TracepointReason( TracepointStatus status )
{
switch( status )
{
case TracepointStatus::NoTraceFs: return "tracefs is not readable — running as root is required";
case TracepointStatus::IdNotReadable: return "the event id is not readable — running as root is required";
case TracepointStatus::OpenDenied: return "system-wide events are denied — needs perf_event_paranoid <= -1 or CAP_PERFMON";
default: return nullptr;
}
}
static void PrintStartupReport( bool kernelFrames, bool hwStats, int hwErrno, const char* output )
{
const TracepointStatus ctxSwitches = CheckSystemWideTracepoint( "/events/sched/sched_switch" );
const TracepointStatus vsync = CheckSystemWideTracepoint( "/events/drm/drm_vblank_event" );
const bool power = CanReadRapl();
printf( "tracy-monitor %i.%i.%i / %s\n", tracy::Version::Major, tracy::Version::Minor, tracy::Version::Patch, tracy::GitRef );
printf( "Profiling '%s' (pid %d) on port %d\n", tracy::GetExternalTargetName(), (int)tracy::GetExternalTargetPid(), EffectivePort() );
printf( " sampling: %s, %s callchain — leaf-only stacks for -fomit-frame-pointer targets\n", FormatHz( EffectiveSamplingHz() ), kernelFrames ? "kernel" : "user-space" );
printf( " kernel frames: %s\n", kernelFrames ? "yes" : "no (kernel sampling denied — perf_event_paranoid > 1 without CAP_PERFMON)" );
if( hwStats )
{
printf( " hardware sampling statistics: yes\n" );
}
else
{
printf( " hardware sampling statistics: no (PMU counters unavailable: %s)\n", strerror( hwErrno ) );
}
if( ctxSwitches == TracepointStatus::Open )
{
printf( " ctx switches / per-thread CPU: yes\n" );
}
else
{
printf( " ctx switches / per-thread CPU: no (%s)\n", TracepointReason( ctxSwitches ) );
}
printf( " power (RAPL): %s\n", power ? "yes" : "no (needs read access to /sys/devices/virtual/powercap/intel-rapl)" );
if( vsync == TracepointStatus::Open )
{
printf( " vsync: yes\n" );
}
else
{
printf( " vsync: no (%s)\n", TracepointReason( vsync ) );
}
if( output )
{
printf( " output: %s\n", output );
}
#ifdef TRACY_ON_DEMAND
printf( " recording: on-demand: samples are captured only while a Tracy server is connected; pre-connection samples are discarded\n" );
#endif
printf( "\n" );
if( !output )
{
printf( "Open the Tracy profiler and connect to this host:port (or it will auto-discover).\n" );
}
fflush( stdout );
}
// --- trace saving ---------------------------------------------------------------
// No-op until the worker's first rate sample arrives and when stdout is not a terminal.
static void PrintProgress( tracy::Worker* worker )
{
if( !worker || !worker->HasData() ) return;
PrintCaptureProgress( *worker, worker->GetFirstTime(), -1 );
}
// True when the status line actually occupies the current line, so that a
// newline is needed to terminate it. PrintCaptureProgress skips rendering
// until the first rate sample arrives, and printing a bare "\n" then would
// add a stray blank line.
static bool ProgressLineActive( tracy::Worker* worker )
{
if( !worker || !IsTerminal() ) return false;
auto& lock = worker->GetMbpsDataLock();
lock.lock();
const bool active = !worker->GetMbpsData().empty();
lock.unlock();
return active;
}
static bool WaitForWorker( tracy::Worker& worker, const char** reasonOut )
{
*reasonOut = nullptr;
for( int i = 0; i < 100; i++ )
{
if( worker.HasData() ) return true;
if( s_shouldQuit )
{
*reasonOut = "interrupted before the capture started";
return false;
}
const auto hs = worker.GetHandshakeStatus();
if( hs != tracy::HandshakePending )
{
// the first connection was made and rejected or dropped; the worker
// never reconnects, so this outcome is final
switch( (tracy::HandshakeStatus)hs )
{
case tracy::HandshakeProtocolMismatch:
*reasonOut = "the client rejected the protocol version (the client and the worker come from the same build; this should not happen)";
break;
case tracy::HandshakeNotAvailable:
*reasonOut = "another server (e.g. the Tracy profiler GUI) is already connected to the client, and the client serves only one server at a time";
break;
default:
*reasonOut = "the client dropped the connection during the handshake";
break;
}
return false;
}
PrintProgress( &worker );
usleep( 100000 );
}
*reasonOut = "the worker did not connect to the client within 10 seconds";
return false;
}
// All of the client's data has reached the worker by the time this runs.
static int SaveTrace( tracy::Worker& worker, const char* output )
{
// IsConnected() flips only after the last data was processed, so this
// also bounds the worker's own teardown.
while( worker.IsConnected() )
{
usleep( 100000 );
}
const auto failure = worker.GetFailureType();
if( failure != tracy::Worker::Failure::None )
{
fprintf( stderr, "Capture failure: %s\n", tracy::Worker::GetFailureString( failure ) );
const auto& fd = worker.GetFailureData();
if( !fd.message.empty() )
{
fprintf( stderr, "Context: %s\n", fd.message.c_str() );
}
}
if( !worker.HasData() )
{
fprintf( stderr, "No trace saved: the worker received no data from the client.\n" );
tracy::RestoreOutputBackup();
return 1;
}
printf( "Time span: %s, callstack samples: %s\n",
tracy::TimeToString( worker.GetLastTime() - worker.GetFirstTime() ),
tracy::RealToString( worker.GetCallstackSampleCount() ) );
printf( "Saving trace to %s...", output );
fflush( stdout );
auto f = std::unique_ptr<tracy::FileWrite>( tracy::FileWrite::Open( output, tracy::FileCompression::Zstd, 3, 4 ) );
if( !f )
{
fprintf( stderr, "\nCannot open output file %s for writing!\n", output );
tracy::RestoreOutputBackup();
return 5;
}
worker.Write( *f, false );
f->Finish();
const auto stats = f->GetCompressionStatistics();
printf( " done\nTrace size %s (%.2f%% ratio)\n", tracy::MemSizeToString( stats.second ), 100.f * stats.second / stats.first );
tracy::DiscardOutputBackup();
return 0;
}
// --- attach mode ---------------------------------------------------------------
static int FindPidsByComm( const char* name, pid_t* outPids, int maxPids )
{
int count = 0;
DIR* proc = opendir( "/proc" );
if( !proc ) return 0;
struct dirent* ent;
while( ( ent = readdir( proc ) ) )
{
if( ent->d_name[0] < '0' || ent->d_name[0] > '9' ) continue;
char path[64];
snprintf( path, sizeof( path ), "/proc/%s/comm", ent->d_name );
FILE* f = fopen( path, "r" );
if( !f ) continue;
char comm[32] = {};
if( fgets( comm, sizeof( comm ), f ) )
{
size_t len = strlen( comm );
while( len > 0 && ( comm[len-1] == '\n' || comm[len-1] == '\r' ) ) comm[--len] = '\0';
if( strcmp( comm, name ) == 0 && count < maxPids )
{
// gate on /proc/<pid>/exe: zombies keep a comm but have no executable to map
char exe[256] = {};
snprintf( path, sizeof( path ), "/proc/%s/exe", ent->d_name );
if( readlink( path, exe, sizeof( exe ) - 1 ) >= 0 )
{
outPids[count++] = (pid_t)atoi( ent->d_name );
}
}
}
fclose( f );
}
closedir( proc );
return count;
}
static bool VerifyClientListening()
{
for( int i = 0; i < 40; i++ )
{
if( tracy::IsDataPortListening() ) return true;
usleep( 125000 );
}
return false;
}
static int RunAttached( pid_t pid, const char* output )
{
if( kill( pid, 0 ) != 0 )
{
fprintf( stderr, "Process %d does not exist or is not accessible.\n", (int)pid );
return 1;
}
if( !ProcessIsAlive( pid ) )
{
fprintf( stderr, "Process %d is a zombie (it has already exited); nothing to attach to.\n", (int)pid );
return 1;
}
s_targetPid = pid;
printf( "Attaching to process %d", (int)pid );
fflush( stdout );
if( !tracy::InitExternalTarget( pid ) ) return 1;
printf( " (%s)...\n", tracy::GetExternalTargetName() );
fflush( stdout );
bool kernelFrames = false;
bool hwStats = false;
int hwErrno = 0;
if( !PreflightSamplingEvent( pid, kernelFrames, hwStats, hwErrno ) ) return 1;
if( !GetTraceFsPath() )
{
fprintf( stderr, "No tracefs mount found: the client's system tracing cannot start without it, so no samples would be captured.\n" );
return 1;
}
tracy::StartupProfiler();
if( tracy::IsSystemTracingFailed() )
{
fprintf( stderr, "The client failed to start sampling although the preflight passed; the capture would contain no samples (the target may have exited between checks, or the kernel rejected the event setup).\n" );
tracy::ShutdownProfiler();
return 1;
}
if( !VerifyClientListening() )
{
if( const char* port = getenv( "TRACY_PORT" ) )
{
fprintf( stderr, "The client could not listen on port %s (the port is most likely in use, or the bind was denied). Retry, or choose another port with --port.\n", port );
}
else
{
fprintf( stderr, "The client could not listen on any of the scan ports 8086-8105 (they are most likely all in use, or the bind was denied). Retry, or choose another port with --port.\n" );
}
tracy::ShutdownProfiler();
return 1;
}
PrintStartupReport( kernelFrames, hwStats, hwErrno, output );
// The worker is created only after the client started: the worker's threads
// name themselves via the client's SetThreadName(), which requires the client
// to be up (the monitor runs it with TRACY_MANUAL_LIFETIME).
std::unique_ptr<tracy::Worker> worker;
if( output )
{
worker = std::make_unique<tracy::Worker>( "127.0.0.1", (uint16_t)EffectivePort(), -1 );
const char* reason = nullptr;
if( !WaitForWorker( *worker, &reason ) )
{
fprintf( stderr, "Cannot capture to %s: %s.\n", output, reason );
tracy::RestoreOutputBackup();
tracy::ShutdownProfiler();
return 1;
}
}
// never signal the target: attach mode must leave it running when we quit
while( !s_shouldQuit && ProcessIsAlive( pid ) )
{
PrintProgress( worker.get() );
usleep( 100000 ); // 100ms poll
}
if( ProgressLineActive( worker.get() ) ) printf( "\n" ); // terminate the status line
if( s_shouldQuit )
{
printf( "\nShutting down profiler...\n" );
}
else
{
printf( "Target exited.\n" );
}
tracy::ShutdownProfiler();
if( worker ) return SaveTrace( *worker, output );
return 0;
}
// --- launch mode ---------------------------------------------------------------
static int RunForked( int argc, char** argv, const char* output )
{
pid_t childPid = fork();
if( childPid < 0 )
{
fprintf( stderr, "Unable to fork: %s\n", strerror( errno ) );
return 2;
}
if( childPid == 0 )
{
// Child process: request ptrace stop at exec, then exec the target.
// Don't leak the monitor's own TRACY_* env into a target that links its own client.
unsetenv( "TRACY_SAMPLING_HZ" );
unsetenv( "TRACY_PORT" );
if( ptrace( PTRACE_TRACEME, 0, nullptr, nullptr ) < 0 )
{
fprintf( stderr, "ptrace(TRACEME) failed: %s\n", strerror( errno ) );
_exit( 2 );
}
execvp( argv[0], argv );
fprintf( stderr, "Unable to exec '%s': %s\n", argv[0], strerror( errno ) );
_exit( 2 );
}
// Parent: wait for the child to stop at the exec boundary (SIGTRAP)
s_targetPid = childPid;
s_isForked = true;
int status;
for(;;)
{
if( waitpid( childPid, &status, 0 ) >= 0 ) break;
if( errno == EINTR ) continue;
fprintf( stderr, "waitpid failed: %s\n", strerror( errno ) );
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
return 2;
}
while( WIFSTOPPED( status ) && WSTOPSIG( status ) != SIGTRAP )
{
// A caught signal (the handlers are inherited by the child) arriving between
// PTRACE_TRACEME and exec produces a signal-delivery stop before the exec stop.
// Re-inject it and keep waiting for the exec stop: running the setup on the
// pre-exec child would capture the monitor's own image and mis-symbolicate.
// Job-control signals must never be re-injected: re-delivering SIGSTOP/SIGTSTP
// re-stops the child, so the exec stop would never be reached (or, for a
// stop carrying the 0x80 job-control bit, PTRACE_CONT rejects the signal
// number outright). Resume those without a signal instead: the stop is
// delivered as a group stop at most once more, and the next resume lifts it.
const int rawStopSig = WSTOPSIG( status );
const int stopSig = rawStopSig & 0x7f;
const int resumeSig = ( rawStopSig & 0x80 || stopSig == SIGSTOP || stopSig == SIGTSTP ) ? 0 : stopSig;
if( ptrace( PTRACE_CONT, childPid, 0, (void*)(unsigned long)resumeSig ) != 0 )
{
fprintf( stderr, "ptrace failed: %s\n", strerror( errno ) );
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
return 2;
}
for(;;)
{
if( waitpid( childPid, &status, 0 ) >= 0 ) break;
if( errno == EINTR ) continue;
fprintf( stderr, "waitpid failed: %s\n", strerror( errno ) );
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
return 2;
}
}
if( !WIFSTOPPED( status ) )
{
// Child exited or was killed before reaching the post-exec SIGTRAP.
if( s_shouldQuit )
{
fprintf( stderr, "\nInterrupted before target started.\n" );
}
else if( WIFEXITED( status ) )
{
fprintf( stderr, "Target exited before profiling began (status %d) -- exec failed?\n", WEXITSTATUS( status ) );
}
else if( WIFSIGNALED( status ) )
{
fprintf( stderr, "Target killed by signal %d before profiling began.\n", WTERMSIG( status ) );
}
else
{
fprintf( stderr, "Child process did not stop as expected (status=0x%x).\n", status );
}
return 2;
}
// The child is stopped post-exec: read its name/exe/maps and check sampling
// permissions while we hold it stopped.
if( !tracy::InitExternalTarget( childPid ) )
{
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
return 1;
}
printf( "Profiling '%s' (pid %d)...\n", tracy::GetExternalTargetName(), (int)childPid );
fflush( stdout );
bool kernelFrames = false;
bool hwStats = false;
int hwErrno = 0;
if( !PreflightSamplingEvent( childPid, kernelFrames, hwStats, hwErrno ) )
{
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
return 1;
}
if( !GetTraceFsPath() )
{
fprintf( stderr, "No tracefs mount found: the client's system tracing cannot start without it, so no samples would be captured.\n" );
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
return 1;
}
tracy::StartupProfiler();
if( tracy::IsSystemTracingFailed() )
{
fprintf( stderr, "The client failed to start sampling although the preflight passed; the capture would contain no samples (the target may have exited between checks, or the kernel rejected the event setup).\n" );
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
tracy::ShutdownProfiler();
return 1;
}
// Detach ptrace and let the child run. If detach fails the child stays
// stopped forever, so this has to be fatal.
if( ptrace( PTRACE_DETACH, childPid, nullptr, nullptr ) < 0 )
{
fprintf( stderr, "ptrace(DETACH) failed: %s -- killing child.\n", strerror( errno ) );
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
tracy::ShutdownProfiler();
return 2;
}
if( !VerifyClientListening() )
{
if( const char* port = getenv( "TRACY_PORT" ) )
{
fprintf( stderr, "The client could not listen on port %s (the port is most likely in use, or the bind was denied). Retry, or choose another port with --port.\n", port );
}
else
{
fprintf( stderr, "The client could not listen on any of the scan ports 8086-8105 (they are most likely all in use, or the bind was denied). Retry, or choose another port with --port.\n" );
}
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
tracy::ShutdownProfiler();
return 1;
}
PrintStartupReport( kernelFrames, hwStats, hwErrno, output );
std::unique_ptr<tracy::Worker> worker;
if( output )
{
worker = std::make_unique<tracy::Worker>( "127.0.0.1", (uint16_t)EffectivePort(), -1 );
const char* reason = nullptr;
if( !WaitForWorker( *worker, &reason ) )
{
fprintf( stderr, "Cannot capture to %s: %s.\n", output, reason );
tracy::RestoreOutputBackup();
kill( childPid, SIGKILL );
waitpid( childPid, nullptr, 0 );
tracy::ShutdownProfiler();
return 1;
}
}
// Wait for child to exit, or for a signal
for(;;)
{
if( s_shouldQuit ) break;
int wstatus;
pid_t ret = waitpid( childPid, &wstatus, WNOHANG );
if( ret > 0 )
{
// the monitor's exit code reflects the monitor, not the profiled application
if( WIFEXITED( wstatus ) )
{
printf( "Target exited with status %d.\n", WEXITSTATUS( wstatus ) );
}
else if( WIFSIGNALED( wstatus ) )
{
printf( "Target exited (signal %d).\n", WTERMSIG( wstatus ) );
}
break;
}
else if( ret < 0 && errno != EINTR )
{
// Child already gone
break;
}
PrintProgress( worker.get() );
usleep( 100000 );
}
if( ProgressLineActive( worker.get() ) ) printf( "\n" ); // terminate the status line
if( s_shouldQuit && ProcessIsAlive( childPid ) )
{
printf( "\nForwarding signal to child and shutting down...\n" );
kill( childPid, SIGINT );
// Give it a moment to exit
usleep( 500000 );
if( ProcessIsAlive( childPid ) )
{
kill( childPid, SIGKILL );
}
waitpid( childPid, nullptr, 0 );
}
else if( s_shouldQuit )
{
// Child is a zombie (it died since the last poll): reap it.
waitpid( childPid, nullptr, WNOHANG );
}
tracy::ShutdownProfiler();
if( worker ) return SaveTrace( *worker, output );
return 0;
}
// --- CLI --------------------------------------------------------------------
static void PrintUsage( const char* progName )
{
printf( "tracy-monitor %i.%i.%i / %s\n\n", tracy::Version::Major, tracy::Version::Minor, tracy::Version::Patch, tracy::GitRef );
printf( "Profiles a process that was not built with the Tracy client.\n" );
printf( "\n" );
printf( "Usage: %s [OPTIONS] program [arguments...]\n", progName );
printf( " %s [OPTIONS] -p PID\n", progName );
printf( " %s [OPTIONS] -n NAME\n", progName );
printf( "\n" );
printf( "Options:\n" );
printf( " -p, --pid PID Attach to an existing process (PID)\n" );
printf( " -n, --name NAME Attach to an existing process by name; the name is the\n" );
printf( " /proc/<pid>/comm value, truncated to 15 characters. If\n" );
printf( " several processes match, their PIDs are listed; use -p.\n" );
printf( " --hz N Sampling frequency in Hz (default 10000, range 1..1000000)\n" );
printf( " --port N Listen port for Tracy servers (default 8086)\n" );
printf( " -o, --output FILE Save the trace to FILE (.tracy) when the capture\n" );
printf( " ends. While capturing, the monitor acts as the Tracy\n" );
printf( " server itself, so the profiler GUI cannot connect\n" );
printf( " -f, --force Replace the output file if it exist\n" );
printf( " -h, --help Show this help message\n" );
printf( "\n" );
printf( "Exit codes: 0 on success; 1 when the target cannot be profiled\n" );
printf( "(bad option, no matching process, permission or kernel refusal) or\n" );
printf( "the trace cannot be saved; 2 when the monitor itself fails to start the\n" );
printf( "target (fork/exec/ptrace); 4 when the output file already exists\n" );
printf( "(use -f to overwrite); 5 when the output file cannot be opened or used.\n" );
printf( "\n" );
printf( "Permission requirements (perf_event_paranoid / capabilities):\n" );
printf( " sampling + symbolication ptrace READ access to the target:\n" );
printf( " same user (uid and gid) and dumpable,\n" );
printf( " or CAP_SYS_PTRACE (yama ptrace_scope\n" );
printf( " does not apply; launch mode's\n" );
printf( " PTRACE_TRACEME needs CAP_SYS_PTRACE at\n" );
printf( " scope 2 and is blocked at scope 3)\n" );
printf( " kernel frames in callchains paranoid <= 1 or CAP_PERFMON\n" );
printf( " IPC / cache / branch statistics hardware PMU counters must be\n" );
printf( " available (often restricted in VMs\n" );
printf( " and containers)\n" );
printf( " context switches / wait stacks tracefs readable (root on most\n" );
printf( " systems) AND paranoid <= -1 or\n" );
printf( " CAP_PERFMON for the event open\n" );
printf( " power (RAPL) plot read access to\n" );
printf( " /sys/devices/virtual/powercap/intel-rapl\n" );
printf( "\n" );
printf( "Examples:\n" );
printf( " %s ./my_program arg1 arg2\n", progName );
printf( " %s -o trace.tracy -n my_program\n", progName );
printf( "\n" );
printf( "In launch mode the target is started under ptrace control so profiling\n" );
printf( "begins before its first instruction; quitting the monitor terminates the\n" );
printf( "target. In attach mode (-p/-n) the target must already be running and is\n" );
printf( "left running when the monitor quits.\n" );
}
// Reserve the first free port in the client's 8086..8105 scan range and hand the
// probe socket to the client (SetReservedListenSocket) so the reservation is atomic.
// The probe must mirror the client's bind exactly (ListenSocket::Listen): IPv6
// dual-stack first, IPv4 only if socket() itself fails, SO_REUSEADDR,
// TRACY_ONLY_LOCALHOST narrows to loopback, fd CLOEXEC (no leak into the target).
static bool ProbeClientBind( int port, bool ipv4Only, bool onlyLocalhost, int& fdOut )
{
fdOut = -1;
int s = -1;
int family = AF_INET6;
if( !ipv4Only ) s = socket( AF_INET6, SOCK_STREAM, 0 );
if( s < 0 )
{
family = AF_INET;
s = socket( AF_INET, SOCK_STREAM, 0 );
}
if( s < 0 ) return false;
fcntl( s, F_SETFD, FD_CLOEXEC );
int val = 1;
setsockopt( s, SOL_SOCKET, SO_REUSEADDR, &val, sizeof( val ) );
bool ok = false;
if( family == AF_INET6 )
{
sockaddr_in6 addr = {};
addr.sin6_family = AF_INET6;
if( onlyLocalhost ) addr.sin6_addr = in6addr_loopback;
addr.sin6_port = htons( (uint16_t)port );
ok = bind( s, (sockaddr*)&addr, sizeof( addr ) ) == 0 && listen( s, 4 ) == 0;
}
else
{
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = onlyLocalhost ? htonl( INADDR_LOOPBACK ) : INADDR_ANY;
addr.sin_port = htons( (uint16_t)port );
ok = bind( s, (sockaddr*)&addr, sizeof( addr ) ) == 0 && listen( s, 4 ) == 0;
}
if( ok )
{
fdOut = s;
}
else
{
close( s );
}
return ok;
}
static void ReservePortIfUnpinned()
{
if( getenv( "TRACY_PORT" ) ) return;
const char* onlyIPv4 = getenv( "TRACY_ONLY_IPV4" );
const char* onlyLocalhost = getenv( "TRACY_ONLY_LOCALHOST" );
const bool ipv4Only = onlyIPv4 && onlyIPv4[0] == '1';
const bool localhost = onlyLocalhost && onlyLocalhost[0] == '1';
for( int i=0; i<20; i++ )
{
const int port = 8086 + i;
int fd = -1;
if( ProbeClientBind( port, ipv4Only, localhost, fd ) )
{
char buf[8];
snprintf( buf, sizeof( buf ), "%d", port );
setenv( "TRACY_PORT", buf, 1 );
tracy::SetReservedListenSocket( fd );
return;
}
}
}
int main( int argc, char** argv )
{
auto progName = argv[0];
InitTerminalDetection();
if( argc < 2 )
{
PrintUsage( progName );
return 1;
}
// Install signal handlers for graceful shutdown
struct sigaction sa = {};
sa.sa_handler = SignalHandler;
sigemptyset( &sa.sa_mask );
sa.sa_flags = 0;
sigaction( SIGINT, &sa, nullptr );
sigaction( SIGTERM, &sa, nullptr );
sigaction( SIGHUP, &sa, nullptr );
sigaction( SIGQUIT, &sa, nullptr );
// TRACY_NO_SYS_TRACE / TRACY_NO_SAMPLING would silently disable the monitor's
// primary data sources (IsSystemTracingFailed() stays false); refuse them up front.
const char* noSysTrace = getenv( "TRACY_NO_SYS_TRACE" );
const char* noSampling = getenv( "TRACY_NO_SAMPLING" );
if( ( noSysTrace && noSysTrace[0] == '1' ) || ( noSampling && noSampling[0] == '1' ) )
{
if( noSysTrace && noSysTrace[0] == '1' )
{
fprintf( stderr, "tracy-monitor: TRACY_NO_SYS_TRACE=1 is set; the monitor requires system tracing. Unset the variable and retry.\n" );
}
if( noSampling && noSampling[0] == '1' )
{
fprintf( stderr, "tracy-monitor: TRACY_NO_SAMPLING=1 is set; the monitor requires callstack sampling. Unset the variable and retry.\n" );
}
return 1;
}
// external sampling opens one event per CPU for each existing thread
// (attach) or per CPU (launch) per event type; raise the soft fd limit
// so multithreaded targets fit (failing opens degrade gracefully).
{
struct rlimit rl = {};
if( getrlimit( RLIMIT_NOFILE, &rl ) == 0 && rl.rlim_cur < 65536 )
{
const rlim_t want = rl.rlim_max > 65536 ? 65536 : rl.rlim_max;
if( want > rl.rlim_cur )
{
rl.rlim_cur = want;
if( setrlimit( RLIMIT_NOFILE, &rl ) != 0 )
{
fprintf( stderr, "tracy-monitor: warning: could not raise RLIMIT_NOFILE (%s); very multithreaded targets may be only partially sampled.\n", strerror( errno ) );
}
}
}
}
pid_t attachPid = 0;
char attachName[128] = {};
bool wantAttach = false;
const char* output = nullptr;
bool overwrite = false;
enum { OptHz = 256, OptPort };
static struct option longOptions[] =
{
{ "pid", required_argument, nullptr, 'p' },
{ "name", required_argument, nullptr, 'n' },
{ "hz", required_argument, nullptr, OptHz },
{ "port", required_argument, nullptr, OptPort },
{ "output", required_argument, nullptr, 'o' },
{ "force", no_argument, nullptr, 'f' },
{ "help", no_argument, nullptr, 'h' },
{ nullptr, 0, nullptr, 0 }
};
int c;
while( ( c = getopt_long( argc, argv, "+p:n:o:fh", longOptions, nullptr ) ) != -1 )
{
switch( c )
{
case 'p':
attachPid = atoi( optarg );
wantAttach = true;
break;
case 'n':
if( strlen( optarg ) >= sizeof( attachName ) )
{
fprintf( stderr, "Process name too long (max %zu characters).\n", sizeof( attachName ) - 1 );
return 1;
}
snprintf( attachName, sizeof( attachName ), "%s", optarg );
wantAttach = true;
break;
case OptHz:
{
const int hz = atoi( optarg );
if( hz < 1 || hz > 1000000 )
{
fprintf( stderr, "Invalid sample rate %s (range 1..1000000 Hz).\n", optarg );
return 1;
}
char buf[16];
snprintf( buf, sizeof( buf ), "%d", hz );
setenv( "TRACY_SAMPLING_HZ", buf, 1 );
break;
}
case OptPort:
{
const int port = atoi( optarg );
if( port < 1 || port > 65535 )
{
fprintf( stderr, "Invalid port %s (range 1..65535).\n", optarg );
return 1;
}
char buf[8];
snprintf( buf, sizeof( buf ), "%d", port );
setenv( "TRACY_PORT", buf, 1 );
break;
}
case 'o':
output = optarg;
break;
case 'f':
overwrite = true;
break;
case 'h':
PrintUsage( argv[0] );
return 0;
case '?':
fprintf( stderr, "Unknown option. Use -h for help.\n" );
return 1;
}
}
// validate a pinned TRACY_PORT like --port: the client pins to any nonzero value
// (single listen, no fallback), so a bad value would report a port never used.
{
const char* portEnv = getenv( "TRACY_PORT" );
if( portEnv )
{
char* end = nullptr;
const long port = strtol( portEnv, &end, 10 );
if( end == portEnv || *end != '\0' || port < 1 || port > 65535 )
{
fprintf( stderr, "Invalid TRACY_PORT '%s' (expected a number in 1..65535); unset it or use --port.\n", portEnv );
return 1;
}
}
}
ReservePortIfUnpinned();
if( output )
{
const char* prepError = nullptr;
const auto prep = tracy::PrepareOutputFile( output, overwrite, &prepError );
if( prep == tracy::OutputPrep::Exists )
{
fprintf( stderr, "Output file %s already exists! Use -f to force overwrite.\n", output );
return 4;
}
if( prep == tracy::OutputPrep::Unusable )
{
fprintf( stderr, "Cannot use output file: %s!\n", prepError );
return 5;
}
}
if( wantAttach )
{
if( attachName[0] )
{
pid_t pids[32] = {};
const int numPids = FindPidsByComm( attachName, pids, 32 );
if( numPids == 0 )
{
fprintf( stderr, "No process named '%s' (names are /proc/<pid>/comm values, truncated to 15 characters).\n", attachName );
return 1;
}
if( numPids > 1 )
{
fprintf( stderr, "Several processes named '%s':", attachName );
for( int i=0; i<numPids; i++ ) fprintf( stderr, " %d", (int)pids[i] );
fprintf( stderr, "\nUse -p PID to disambiguate.\n" );
return 1;
}
return RunAttached( pids[0], output );
}
if( attachPid <= 0 )
{
fprintf( stderr, "Invalid PID specified.\n" );
return 1;
}
return RunAttached( attachPid, output );
}
argv += optind;
argc -= optind;
if( argc < 1 )
{
PrintUsage( progName ); // argv[0] was shifted, use original
return 1;
}
return RunForked( argc, argv, output );
}