diff --git a/monitor/CMakeLists.txt b/monitor/CMakeLists.txt new file mode 100644 index 00000000..65c0be83 --- /dev/null +++ b/monitor/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.16) + +set(TRACY_PUBLIC_DIR ${CMAKE_CURRENT_LIST_DIR}/../public) + +option(NO_ISA_EXTENSIONS "Disable ISA extensions (don't pass -march=native or -mcpu=native to the compiler)" OFF) + +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake) + +set(CMAKE_CXX_STANDARD 17) + +project( + tracy-monitor + LANGUAGES C CXX + VERSION ${TRACY_VERSION_STRING} +) + +find_package(Threads REQUIRED) + +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/config.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/vendor.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/GitRef.cmake) + +set(PROGRAM_FILES + src/monitor.cpp +) + +add_executable(${PROJECT_NAME} ${PROGRAM_FILES} ${TRACY_PUBLIC_DIR}/TracyClient.cpp) +target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC + $ + $) +target_compile_definitions(${PROJECT_NAME} PRIVATE + TRACY_ENABLE + TRACY_DELAYED_INIT + TRACY_MANUAL_LIFETIME + TRACY_NO_FRAME_IMAGE +) +target_link_libraries(${PROJECT_NAME} PRIVATE + Threads::Threads + ${CMAKE_DL_LIBS} +) + +add_git_ref(${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) \ No newline at end of file diff --git a/monitor/src/monitor.cpp b/monitor/src/monitor.cpp new file mode 100644 index 00000000..e441a2a3 --- /dev/null +++ b/monitor/src/monitor.cpp @@ -0,0 +1,324 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../public/tracy/Tracy.hpp" +#include "../public/common/TracyVersion.hpp" +#include "../public/client/TracyCallstack.hpp" +#include "GitRef.hpp" + +namespace tracy { + extern uint32_t ___tracy_magic_pid_override; + extern char ___tracy_magic_process_name[64]; +} + +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; +} + +static bool ReadProcessName( pid_t pid, char* buf, size_t bufSize ) +{ + char path[64]; + snprintf( path, sizeof( path ), "/proc/%d/comm", (int)pid ); + FILE* f = fopen( path, "r" ); + if( !f ) return false; + if( !fgets( buf, bufSize, f ) ) + { + fclose( f ); + return false; + } + fclose( f ); + // Remove trailing newline + size_t len = strlen( buf ); + while( len > 0 && ( buf[len-1] == '\n' || buf[len-1] == '\r' ) ) len--; + buf[len] = '\0'; + return len > 0; +} + +static bool CheckPerfPermissions() +{ + FILE* f = fopen( "/proc/sys/kernel/perf_event_paranoid", "r" ); + if( !f ) + { + fprintf( stderr, "Warning: Cannot read /proc/sys/kernel/perf_event_paranoid\n" ); + return true; // Assume OK + } + int paranoid = 2; + if( fscanf( f, "%d", ¶noid ) != 1 ) paranoid = 2; + fclose( f ); + + if( paranoid > 1 && geteuid() != 0 ) + { + fprintf( stderr, "Warning: perf_event_paranoid = %d. Profiling another process may require:\n", paranoid ); + fprintf( stderr, " - Running as root, or\n" ); + fprintf( stderr, " - Setting /proc/sys/kernel/perf_event_paranoid to -1 or 0, or\n" ); + fprintf( stderr, " - Granting CAP_PERFMON + CAP_SYS_PTRACE capabilities\n" ); + } + return true; +} + +static bool ProcessIsAlive( pid_t pid ) +{ + return kill( pid, 0 ) == 0; +} + +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( "Usage: %s [OPTIONS] program [arguments...]\n", progName ); + printf( " %s [OPTIONS] -p PID\n", progName ); + printf( "\n" ); + printf( "Options:\n" ); + printf( " -p PID Attach to existing process (PID)\n" ); + printf( " -h Show this help message\n" ); + printf( "\n" ); + printf( "Examples:\n" ); + printf( " %s ./my_program arg1 arg2\n", progName ); + printf( " %s -p 1234\n", progName ); + printf( "\n" ); + printf( "The monitor captures sampling profiling data from an external process\n" ); + printf( "and streams it to a Tracy server for visualization.\n" ); + printf( "\n" ); + printf( "In launch mode, the target program is started under ptrace control to\n" ); + printf( "ensure profiling begins before the first instruction executes.\n" ); + printf( "\n" ); + printf( "In attach mode (-p), the target must already be running.\n" ); +} + +static int RunAttached( pid_t pid ) +{ + if( !ProcessIsAlive( pid ) ) + { + fprintf( stderr, "Process %d does not exist or is not accessible.\n", (int)pid ); + return 1; + } + + s_targetPid = pid; + + char procName[64]; + if( ReadProcessName( pid, procName, sizeof( procName ) ) ) + { + memcpy( tracy::___tracy_magic_process_name, procName, sizeof( tracy::___tracy_magic_process_name ) ); + } + + printf( "Attaching to process %d", (int)pid ); + if( tracy::___tracy_magic_process_name[0] ) printf( " (%s)", tracy::___tracy_magic_process_name ); + printf( "...\n" ); + + tracy::InitExternalImageCache( pid ); + tracy::___tracy_magic_pid_override = (uint32_t)pid; + tracy::StartupProfiler(); + + printf( "Profiling started. Waiting for Tracy server connection...\n" ); + + // Wait for the target process to exit, or for a signal + while( !s_shouldQuit && ProcessIsAlive( pid ) ) + { + usleep( 100000 ); // 100ms poll + } + + if( s_shouldQuit ) + { + printf( "\nShutting down profiler...\n" ); + } + else + { + printf( "Target process %d exited.\n", (int)pid ); + } + + tracy::ShutdownProfiler(); + return 0; +} + +static int RunForked( int argc, char** argv ) +{ + 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 + 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; + if( waitpid( childPid, &status, 0 ) < 0 ) + { + fprintf( stderr, "waitpid failed: %s\n", strerror( errno ) ); + return 2; + } + + if( !WIFSTOPPED( status ) ) + { + fprintf( stderr, "Child process did not stop as expected (status=0x%x).\n", status ); + return 2; + } + + // The child has exec'd but is stopped. Its address space is now the target program. + // Read its process name and memory maps. + char procName[64]; + if( ReadProcessName( childPid, procName, sizeof( procName ) ) ) + { + memcpy( tracy::___tracy_magic_process_name, procName, sizeof( tracy::___tracy_magic_process_name ) ); + } + + printf( "Profiling '%s' (pid %d)...\n", + tracy::___tracy_magic_process_name[0] ? tracy::___tracy_magic_process_name : argv[0], + (int)childPid ); + + // Initialize the external image cache (target's /proc/pid/maps) + tracy::InitExternalImageCache( childPid ); + + // Set up the profiler to target the child + tracy::___tracy_magic_pid_override = (uint32_t)childPid; + tracy::StartupProfiler(); + + // Detach ptrace and let the child run + if( ptrace( PTRACE_DETACH, childPid, nullptr, nullptr ) < 0 ) + { + fprintf( stderr, "Warning: ptrace(DETACH) failed: %s\n", strerror( errno ) ); + // Not fatal -- the child might still run + } + + printf( "Profiling started. Waiting for Tracy server connection...\n" ); + + // 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 ) + { + if( WIFEXITED( wstatus ) ) + { + printf( "Target process exited with status %d.\n", WEXITSTATUS( wstatus ) ); + } + else if( WIFSIGNALED( wstatus ) ) + { + printf( "Target process killed by signal %d.\n", WTERMSIG( wstatus ) ); + } + break; + } + else if( ret < 0 && errno != EINTR ) + { + // Child already gone + break; + } + usleep( 100000 ); + } + + 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 ); + } + } + + tracy::ShutdownProfiler(); + return 0; +} + +int main( int argc, char** argv ) +{ + auto progName = argv[0]; + + 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 ); + + pid_t attachPid = 0; + bool wantAttach = false; + + static struct option longOptions[] = { + { "pid", required_argument, nullptr, 'p' }, + { "help", no_argument, nullptr, 'h' }, + { nullptr, 0, nullptr, 0 } + }; + + int c; + while( ( c = getopt_long( argc, argv, "+p:h", longOptions, nullptr ) ) != -1 ) + { + switch( c ) + { + case 'p': + attachPid = atoi( optarg ); + wantAttach = true; + break; + case 'h': + PrintUsage( argv[0] ); + return 0; + case '?': + fprintf( stderr, "Unknown option. Use -h for help.\n" ); + return 1; + } + } + + argv += optind; + argc -= optind; + + CheckPerfPermissions(); + + if( wantAttach ) + { + if( attachPid <= 0 ) + { + fprintf( stderr, "Invalid PID specified.\n" ); + return 1; + } + return RunAttached( attachPid ); + } + + if( argc < 1 ) + { + PrintUsage( progName ); // argv[0] was shifted, use original + return 1; + } + + return RunForked( argc, argv ); +}