diff --git a/.vscode/settings.json b/.vscode/settings.json index 52d602c307..9b19c6bc9e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "${workspaceFolder}/capture", "${workspaceFolder}/csvexport", "${workspaceFolder}/import", + "${workspaceFolder}/multiprocess", "${workspaceFolder}/update", "${workspaceFolder}/test", "${workspaceFolder}", diff --git a/capture/CMakeLists.txt b/capture/CMakeLists.txt index 2417c7817b..d3f83d26d5 100644 --- a/capture/CMakeLists.txt +++ b/capture/CMakeLists.txt @@ -18,10 +18,15 @@ include(${CMAKE_CURRENT_LIST_DIR}/../cmake/config.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/vendor.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/server.cmake) -set(PROGRAM_FILES + +add_executable(${PROJECT_NAME} src/capture.cpp ) - -add_executable(${PROJECT_NAME} ${PROGRAM_FILES} ${COMMON_FILES} ${SERVER_FILES}) target_link_libraries(${PROJECT_NAME} PRIVATE TracyServer TracyGetOpt) + +add_executable(tracy-multicapture + src/multicapture.cpp +) +target_link_libraries(tracy-multicapture TracyServer TracyGetOpt) + set_property(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME}) diff --git a/capture/src/capture.cpp b/capture/src/capture.cpp index ff833f739a..925f7ef9d7 100644 --- a/capture/src/capture.cpp +++ b/capture/src/capture.cpp @@ -28,6 +28,8 @@ # include "../../getopt/getopt.h" #endif +#include "lib.cpp" // temporary hack to carefully share code between capture and multicapture + // This atomic is written by a signal handler (SigInt). Traditionally that would // have had to be `volatile sig_atomic_t`, and annoyingly, `bool` was @@ -44,52 +46,6 @@ void SigInt( int ) s_disconnect.store(true, std::memory_order_relaxed); } -static bool s_isStdoutATerminal = false; - -void InitIsStdoutATerminal() { -#ifdef _WIN32 - s_isStdoutATerminal = _isatty( fileno( stdout ) ); -#else - s_isStdoutATerminal = isatty( fileno( stdout ) ); -#endif -} - -bool IsStdoutATerminal() { return s_isStdoutATerminal; } - -#define ANSI_RESET "\033[0m" -#define ANSI_BOLD "\033[1m" -#define ANSI_BLACK "\033[30m" -#define ANSI_RED "\033[31m" -#define ANSI_GREEN "\033[32m" -#define ANSI_YELLOW "\033[33m" -#define ANSI_BLUE "\033[34m" -#define ANSI_MAGENTA "\033[35m" -#define ANSI_CYAN "\033[36m" -#define ANSI_ERASE_LINE "\033[2K" - -// Like printf, but if stdout is a terminal, prepends the output with -// the given `ansiEscape` and appends ANSI_RESET. -void AnsiPrintf( const char* ansiEscape, const char* format, ... ) { - if( IsStdoutATerminal() ) - { - // Prepend ansiEscape and append ANSI_RESET. - char buf[256]; - va_list args; - va_start( args, format ); - vsnprintf( buf, sizeof buf, format, args ); - va_end( args ); - printf( "%s%s" ANSI_RESET, ansiEscape, buf ); - } - else - { - // Just a normal printf. - va_list args; - va_start( args, format ); - vfprintf( stdout, format, args ); - va_end( args ); - } -} - [[noreturn]] void Usage() { printf( "Usage: capture -o output.tracy [-a address] [-p port] [-f] [-s seconds] [-m memlimit]\n" ); @@ -113,7 +69,7 @@ int main( int argc, char** argv ) const char* output = nullptr; int port = 8086; int seconds = -1; - int64_t memoryLimit = -1; + int64_t memoryLimit = tracy::NO_WORKER_MEMORY_LIMIT; int c; while( ( c = getopt( argc, argv, "a:o:p:fs:m:" ) ) != -1 ) @@ -167,22 +123,13 @@ int main( int argc, char** argv ) tracy::Worker worker( address, port, memoryLimit ); while( !worker.HasData() ) { - const auto handshake = worker.GetHandshakeStatus(); - if( handshake == tracy::HandshakeProtocolMismatch ) - { - printf( "\nThe client you are trying to connect to uses incompatible protocol version.\nMake sure you are using the same Tracy version on both client and server.\n" ); - return 1; - } - if( handshake == tracy::HandshakeNotAvailable ) - { - printf( "\nThe client you are trying to connect to is no longer able to sent profiling data,\nbecause another server was already connected to it.\nYou can do the following:\n\n 1. Restart the client application.\n 2. Rebuild the client application with on-demand mode enabled.\n" ); - return 2; - } - if( handshake == tracy::HandshakeDropped ) + const auto handshake = static_cast(worker.GetHandshakeStatus()); + int status = checkHandshake(handshake); + if( status != 0 ) { - printf( "\nThe client you are trying to connect to has disconnected during the initial\nconnection handshake. Please check your network configuration.\n" ); - return 3; + return status; } + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); } printf( "\nQueue delay: %s\nTimer resolution: %s\n", tracy::TimeToString( worker.GetDelay() ), tracy::TimeToString( worker.GetResolution() ) ); @@ -196,9 +143,6 @@ int main( int argc, char** argv ) sigaction( SIGINT, &sigint, &oldsigint ); #endif - const auto firstTime = worker.GetFirstTime(); - auto& lock = worker.GetMbpsDataLock(); - const auto t0 = std::chrono::high_resolution_clock::now(); while( worker.IsConnected() ) { @@ -210,45 +154,14 @@ int main( int argc, char** argv ) worker.Disconnect(); // Relaxed order is sufficient because only this thread ever reads // this value. - s_disconnect.store(false, std::memory_order_relaxed ); + s_disconnect.store( false, std::memory_order_relaxed ); break; } - - lock.lock(); - const auto mbps = worker.GetMbpsData().back(); - const auto compRatio = worker.GetCompRatio(); - const auto netTotal = worker.GetDataTransferred(); - lock.unlock(); - // Output progress info only if destination is a TTY to avoid bloating // log files (so this is not just about usage of ANSI color codes). if( IsStdoutATerminal() ) { - const char* unit = "Mbps"; - float unitsPerMbps = 1.f; - if( mbps < 0.1f ) - { - unit = "Kbps"; - unitsPerMbps = 1000.f; - } - AnsiPrintf( ANSI_ERASE_LINE ANSI_CYAN ANSI_BOLD, "\r%7.2f %s", mbps * unitsPerMbps, unit ); - printf( " /"); - AnsiPrintf( ANSI_CYAN ANSI_BOLD, "%5.1f%%", compRatio * 100.f ); - printf( " ="); - AnsiPrintf( ANSI_YELLOW ANSI_BOLD, "%7.2f Mbps", mbps / compRatio ); - printf( " | "); - AnsiPrintf( ANSI_YELLOW, "Tx: "); - AnsiPrintf( ANSI_GREEN, "%s", tracy::MemSizeToString( netTotal ) ); - printf( " | "); - AnsiPrintf( ANSI_RED ANSI_BOLD, "%s", tracy::MemSizeToString( tracy::memUsage.load( std::memory_order_relaxed ) ) ); - if( memoryLimit > 0 ) - { - printf( " / " ); - AnsiPrintf( ANSI_BLUE ANSI_BOLD, "%s", tracy::MemSizeToString( memoryLimit ) ); - } - printf( " | "); - AnsiPrintf( ANSI_RED, "%s", tracy::TimeToString( worker.GetLastTime() - firstTime ) ); - fflush( stdout ); + printWorkerUpdate( worker, memoryLimit, true, true ); } std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); @@ -259,91 +172,16 @@ int main( int argc, char** argv ) { // Relaxed order is sufficient because only this thread ever reads // this value. - s_disconnect.store(true, std::memory_order_relaxed ); + s_disconnect.store( true, std::memory_order_relaxed ); } } } const auto t1 = std::chrono::high_resolution_clock::now(); - const auto& failure = worker.GetFailureType(); - if( failure != tracy::Worker::Failure::None ) - { - AnsiPrintf( ANSI_RED ANSI_BOLD, "\nInstrumentation failure: %s", tracy::Worker::GetFailureString( failure ) ); - auto& fd = worker.GetFailureData(); - if( !fd.message.empty() ) - { - printf( "\nContext: %s", fd.message.c_str() ); - } - if( fd.callstack != 0 ) - { - AnsiPrintf( ANSI_BOLD, "\nFailure callstack:\n" ); - auto& cs = worker.GetCallstack( fd.callstack ); - int fidx = 0; - for( auto& entry : cs ) - { - auto frameData = worker.GetCallstackFrame( entry ); - if( !frameData ) - { - printf( "%3i. %p\n", fidx++, (void*)worker.GetCanonicalPointer( entry ) ); - } - else - { - const auto fsz = frameData->size; - for( uint8_t f=0; fdata[f]; - auto txt = worker.GetString( frame.name ); - - if( fidx == 0 && f != fsz-1 ) - { - auto test = tracy::s_tracyStackFrames; - bool match = false; - do - { - if( strcmp( txt, *test ) == 0 ) - { - match = true; - break; - } - } - while( *++test ); - if( match ) continue; - } - - if( f == fsz-1 ) - { - printf( "%3i. ", fidx++ ); - } - else - { - AnsiPrintf( ANSI_BLACK ANSI_BOLD, "inl. " ); - } - AnsiPrintf( ANSI_CYAN, "%s ", txt ); - txt = worker.GetString( frame.file ); - if( frame.line == 0 ) - { - AnsiPrintf( ANSI_YELLOW, "(%s)", txt ); - } - else - { - AnsiPrintf( ANSI_YELLOW, "(%s:%" PRIu32 ")", txt, frame.line ); - } - if( frameData->imageName.Active() ) - { - AnsiPrintf( ANSI_MAGENTA, " %s\n", worker.GetString( frameData->imageName ) ); - } - else - { - printf( "\n" ); - } - } - } - } - } - } + printWorkerFailure( worker, "" ); printf( "\nFrames: %" PRIu64 "\nTime span: %s\nZones: %s\nElapsed time: %s\nSaving trace...", - worker.GetFrameCount( *worker.GetFramesBase() ), tracy::TimeToString( worker.GetLastTime() - firstTime ), tracy::RealToString( worker.GetZoneCount() ), + worker.GetFrameCount( *worker.GetFramesBase() ), tracy::TimeToString( worker.GetLastTime() - worker.GetFirstTime() ), tracy::RealToString( worker.GetZoneCount() ), tracy::TimeToString( std::chrono::duration_cast( t1 - t0 ).count() ) ); fflush( stdout ); auto f = std::unique_ptr( tracy::FileWrite::Open( output, tracy::FileCompression::Zstd, 3, 4 ) ); diff --git a/capture/src/lib.cpp b/capture/src/lib.cpp new file mode 100644 index 0000000000..af31d37e7a --- /dev/null +++ b/capture/src/lib.cpp @@ -0,0 +1,224 @@ +// common library file for capture utilities +#ifdef _WIN32 +# include +# include +#else +# include +#endif +#include +#include + +#include "TracyPrint.hpp" +#include "TracyStackFrames.hpp" +#include "TracyWorker.hpp" + +static bool s_isStdoutATerminal = false; + +void InitIsStdoutATerminal() +{ +#ifdef _WIN32 + s_isStdoutATerminal = _isatty( fileno( stdout ) ); +#else + s_isStdoutATerminal = isatty( fileno( stdout ) ); +#endif +} + +bool IsStdoutATerminal() { return s_isStdoutATerminal; } + +#define ANSI_RESET "\033[0m" +#define ANSI_BOLD "\033[1m" +#define ANSI_BLACK "\033[30m" +#define ANSI_RED "\033[31m" +#define ANSI_GREEN "\033[32m" +#define ANSI_YELLOW "\033[33m" +#define ANSI_BLUE "\033[34m" +#define ANSI_MAGENTA "\033[35m" +#define ANSI_CYAN "\033[36m" +#define ANSI_ERASE_LINE "\033[2K" +#define ANSI_UP_ONE_LINE "\033[1;A" + +// Like printf, but if stdout is a terminal, prepends the output with +// the given `ansiEscape` and appends ANSI_RESET. +#ifdef __GNUC__ +[[gnu::format( __printf__, 2, 3 )]] +#endif +void AnsiPrintf( const char* ansiEscape, const char* format, ... ) +{ + if( IsStdoutATerminal() ) + { + // Prepend ansiEscape and append ANSI_RESET. + char buf[256]; + va_list args; + va_start( args, format ); + vsnprintf( buf, sizeof buf, format, args ); + va_end( args ); + printf( "%s%s" ANSI_RESET, ansiEscape, buf ); + } + else + { + // Just a normal printf. + va_list args; + va_start( args, format ); + vfprintf( stdout, format, args ); + va_end( args ); + } +} + +// Check handshake status +// If failure, printf helpful message and return non-zero +int checkHandshake( tracy::HandshakeStatus handshake ) +{ + if( handshake == tracy::HandshakeProtocolMismatch ) + { + printf( + "\nThe client you are trying to connect to uses incompatible protocol version.\nMake sure you are using the same Tracy version on both client and server.\n" ); + return 1; + } + if( handshake == tracy::HandshakeNotAvailable ) + { + printf( + "\nThe client you are trying to connect to is no longer able to sent profiling data,\nbecause another server was already connected to it.\nYou can do the following:\n\n 1. Restart the client application.\n 2. Rebuild the client application with on-demand mode enabled.\n" ); + return 2; + } + if( handshake == tracy::HandshakeDropped ) + { + printf( + "\nThe client you are trying to connect to has disconnected during the initial\nconnection handshake. Please check your network configuration.\n" ); + return 3; + } + return 0; +} + +void printCurrentMemoryUsage( int64_t memoryLimit ) +{ + AnsiPrintf( ANSI_RED ANSI_BOLD, "%s", tracy::MemSizeToString( tracy::memUsage.load( std::memory_order_relaxed ) ) ); + if( memoryLimit > 0 ) + { + printf( " / " ); + AnsiPrintf( ANSI_BLUE ANSI_BOLD, "%s", tracy::MemSizeToString( memoryLimit ) ); + } +} + +void printWorkerUpdate( tracy::Worker& worker, int64_t memoryLimit, bool erase, bool memoryUsage ) +{ + auto& lock = worker.GetMbpsDataLock(); + lock.lock(); + const auto mbps = worker.GetMbpsData().back(); + const auto compRatio = worker.GetCompRatio(); + const auto netTotal = worker.GetDataTransferred(); + lock.unlock(); + + const char* unit = "Mbps"; + float unitsPerMbps = 1.f; + if( mbps < 0.1f ) + { + unit = "Kbps"; + unitsPerMbps = 1000.f; + } + if( erase ) + { + AnsiPrintf( ANSI_ERASE_LINE, "\r" ); + } + AnsiPrintf( ANSI_CYAN ANSI_BOLD, "%7.2f %s", mbps * unitsPerMbps, unit ); + printf( " /" ); + AnsiPrintf( ANSI_CYAN ANSI_BOLD, "%5.1f%%", compRatio * 100.f ); + printf( " =" ); + AnsiPrintf( ANSI_YELLOW ANSI_BOLD, "%7.2f Mbps", mbps / compRatio ); + printf( " | " ); + AnsiPrintf( ANSI_YELLOW, "Tx: " ); + AnsiPrintf( ANSI_GREEN, "%s", tracy::MemSizeToString( netTotal ) ); + if( memoryUsage ) + { + printf( " | " ); + printCurrentMemoryUsage( memoryLimit ); + } + + printf( " | " ); + AnsiPrintf( ANSI_RED, "%s", tracy::TimeToString( worker.GetLastTime() - worker.GetFirstTime() ) ); + fflush( stdout ); +} + +bool printWorkerFailure( tracy::Worker& worker, char const* prefix ) +{ + auto const& failure = worker.GetFailureType(); + if( failure == tracy::Worker::Failure::None ) + { + return false; + } + else + { + AnsiPrintf( ANSI_RED ANSI_BOLD, "\n%s Instrumentation failure: %s", prefix, + tracy::Worker::GetFailureString( failure ) ); + auto& fd = worker.GetFailureData(); + if( !fd.message.empty() ) + { + printf( "\nContext: %s", fd.message.c_str() ); + } + if( fd.callstack != 0 ) + { + AnsiPrintf( ANSI_BOLD, "\nFailure callstack:\n" ); + auto& cs = worker.GetCallstack( fd.callstack ); + int fidx = 0; + for( auto& entry : cs ) + { + auto frameData = worker.GetCallstackFrame( entry ); + if( !frameData ) + { + printf( "%3i. %p\n", fidx++, (void*)worker.GetCanonicalPointer( entry ) ); + } + else + { + const auto fsz = frameData->size; + for( uint8_t f = 0; f < fsz; f++ ) + { + const auto& frame = frameData->data[f]; + auto txt = worker.GetString( frame.name ); + + if( fidx == 0 && f != fsz - 1 ) + { + auto test = tracy::s_tracyStackFrames; + bool match = false; + do + { + if( strcmp( txt, *test ) == 0 ) + { + match = true; + break; + } + } while( *++test ); + if( match ) continue; + } + + if( f == fsz - 1 ) + { + printf( "%3i. ", fidx++ ); + } + else + { + AnsiPrintf( ANSI_BLACK ANSI_BOLD, "inl. " ); + } + AnsiPrintf( ANSI_CYAN, "%s ", txt ); + txt = worker.GetString( frame.file ); + if( frame.line == 0 ) + { + AnsiPrintf( ANSI_YELLOW, "(%s)", txt ); + } + else + { + AnsiPrintf( ANSI_YELLOW, "(%s:%" PRIu32 ")", txt, frame.line ); + } + if( frameData->imageName.Active() ) + { + AnsiPrintf( ANSI_MAGENTA, " %s\n", worker.GetString( frameData->imageName ) ); + } + else + { + printf( "\n" ); + } + } + } + } + } + return true; + } +} diff --git a/capture/src/multicapture.cpp b/capture/src/multicapture.cpp new file mode 100644 index 0000000000..91aea6ff28 --- /dev/null +++ b/capture/src/multicapture.cpp @@ -0,0 +1,706 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +# include +#else +# include +#endif + +#include "../../getopt/getopt.h" // windows and apple clang don't provide getopt_long + +#include "TracyFileWrite.hpp" +#include "TracyPrint.hpp" +#include "TracyProtocol.hpp" +#include "TracyProtocolServer.hpp" +#include "TracySocket.hpp" +#include "TracySysUtil.hpp" +#include "TracyWorker.hpp" +#include "tracy_lz4.hpp" + +#include "lib.cpp" + +using namespace std::chrono_literals; + +constexpr const char* TRACE_FILE_SUFFIX = ".tracy"; + +static std::atomic s_disconnect{ false }; + +void SignalHandler_SigInt( int ) +{ + // We don't need stronger ordering since this signal handler doesn't do + // anything else that would need to be ordered relatively to this. + s_disconnect.store( true, std::memory_order_relaxed ); +} + +void SetupDisconnectSignalHandler() +{ +#ifdef _WIN32 + signal( SIGINT, SignalHandler_SigInt ); +#else + struct sigaction sigint, oldsigint; + memset( &sigint, 0, sizeof( sigint ) ); + sigint.sa_handler = SignalHandler_SigInt; + sigaction( SIGINT, &sigint, &oldsigint ); +#endif +} + +enum class ClientStatus +{ + DISCOVERED, + RUNNING, + FINISHED, + DISABLED +}; + +struct RunningClient +{ + uint64_t id; + ClientStatus status; + tracy::BroadcastMessage msg; + char addr[tracy::IpAddress::TEXT_SIZE]; + std::unique_ptr worker = nullptr; + bool disconnectSent = false; + std::chrono::time_point detectedAt; + std::chrono::time_point finishedAt; + + RunningClient() = default; + + RunningClient( RunningClient&& in ) + : id{ in.id } + , status{ in.status } + , msg{ in.msg } + , worker{ std::move( in.worker ) } + , disconnectSent{ in.disconnectSent } + , detectedAt{ in.detectedAt } + , finishedAt{ in.finishedAt } + { + memcpy( addr, in.addr, tracy::IpAddress::TEXT_SIZE ); + } + + explicit RunningClient( tracy::BroadcastMessage const& inmsg, tracy::IpAddress const& inaddr ) + : msg{ inmsg } // copied from profiler/main.cpp TODO: merge put in stdlib + , status{ ClientStatus::DISCOVERED } + , id{ tracy::ClientUniqueID( inaddr, inmsg.listenPort ) } + , detectedAt( std::chrono::high_resolution_clock::now() ) + { + memcpy( addr, inaddr.GetText(), tracy::IpAddress::TEXT_SIZE ); + } + + std::string saveName( std::string const& prefix ) const + { + std::ostringstream oss; + oss << prefix << msg.pid << "-" << msg.programName << ".tracy"; + return oss.str(); + } + + std::string runtimeName() const + { + std::ostringstream oss; + oss << "[" << msg.pid << "] " << msg.programName; + return oss.str(); + } +}; + +std::optional receive_client_broadcast( tracy::UdpListen* socket ) +{ + tracy::IpAddress addr; + size_t msgLen; + auto msg = socket->Read( msgLen, addr, 0 ); + if( !msg ) return std::nullopt; + auto parsed = tracy::ParseBroadcastMessage( msg, msgLen ); + if( parsed.has_value() ) + { + auto msg = parsed.value(); + return RunningClient( msg, addr ); + } + return std::nullopt; +} + +std::ostream& operator<<( std::ostream& os, RunningClient const& client ) +{ + os << client.msg.programName << "@" << client.msg.pid; + return os; +} + +constexpr const std::chrono::nanoseconds MULTI_CAPTURE_LOOP_INTERVAL = 100ms; +constexpr const std::chrono::nanoseconds PRINT_UPDATE_INTERVAL = 200ms; + +[[noreturn]] void Usage() +{ + printf( "Usage: capture -o [--multi] [-a address] [-p port] [-s seconds] [-m percent]\n" ); + + printf( "Options (a SINGLE tag indicates the option is only accessible in single capture mode):\n" ); + printf( " -o/--output Output file path (in MULTI mode, it is interpreted as a prefix)\n" ); + printf( " -f/--force Overwrite existing files\n" ); + printf( " -M/--multi Enable multi-capture mode\n" ); + printf( " -v/--verbose Verbose output\n" ); + + printf( " -a/--address [SINGLE] Target IP address\n" ); + printf( " -p/--port [SINGLE] Target port\n" ); + printf( " -s/--stop-after [SINGLE] Stop profiling after this duration (in seconds) \n" ); + printf( " -m/--memlimit [SINGLE] Set a memory limit (in %% of the total RAM)\n" ); + + printf( "\nIn single-capture mode, 'capture' directly connects to the TCP data stream at address:port .\n" ); + printf( "In multi-capture mode, profiled targets are detected from UDP broadcast packets;\n" ); + printf( "capture stops once all detected targets disconnect.\n" ); + printf( "Output files are of the form ..tracy\n" ); + + exit( 1 ); +} + +struct CaptureArgs +{ + std::filesystem::path outputPath; + + std::filesystem::path outputDirectory; // filled from 'output' + std::string outputFileName; // filled from 'output'. In multi mode, contains "." + + bool multi = false; + std::string address = "127.0.0.1"; + uint16_t port = tracy::DEFAULT_CLIENT_DATA_TCP_PORT; + std::chrono::milliseconds stopAfter = -1ms; + bool verbose = false; + bool overwrite = false; + int64_t memoryLimit = tracy::NO_WORKER_MEMORY_LIMIT; + + // option parsing + static CaptureArgs parse( int argc, char* argv[] ) + { + CaptureArgs args; + bool setIncompatibleOptionWithMultiCapture = false; + int c; + const struct option long_options[] = { { "memlimit", required_argument, 0, 'm' }, + { "multi", no_argument, 0, 'M' }, + { "stop-after", required_argument, 0, 's' }, + { "address", required_argument, 0, 'a' }, + { "port", required_argument, 0, 'p' }, + { "verbose", no_argument, 0, 'v' }, + { "output", required_argument, 0, 'o' }, + { "force", no_argument, 0, 'f' }, + { 0, 0, 0, 0 } }; + while( ( c = getopt_long( argc, argv, "o:m:a:p:s:Mfv", long_options, nullptr ) ) != -1 ) + { + switch( c ) + { + case 'o': + args.outputPath = optarg; + break; + case 'M': + args.multi = true; + break; + case 'v': + args.verbose = true; + break; + case 'f': + args.overwrite = true; + break; + case 'm': + args.memoryLimit = atol( optarg ); + args.memoryLimit = std::clamp( atoll( optarg ), 1ll, 999ll ) * tracy::GetPhysicalMemorySize() / 100; + setIncompatibleOptionWithMultiCapture = true; + break; + case 'a': + args.address = optarg; + setIncompatibleOptionWithMultiCapture = true; + break; + case 'p': + args.port = atoi( optarg ); + setIncompatibleOptionWithMultiCapture = true; + break; + case 's': + { + float stopAfterInSeconds = strtof( optarg, nullptr ); + args.stopAfter = static_cast( stopAfterInSeconds * 1000 ) * 1ms; + setIncompatibleOptionWithMultiCapture = true; + break; + } + default: + Usage(); + break; + } + } + + if( args.outputPath.empty() ) + { + Usage(); + } + + if( args.multi and setIncompatibleOptionWithMultiCapture ) + { + std::cout << "ERROR: both --multi mode, and another option incompatible with it were requested\n\n"; + Usage(); + } + + // process 'outputPath' argument + // - extract directory / file prefix (for multi mode) + // - check file existence, fail if --force not given + args.outputFileName = args.outputPath.filename().generic_string(); + + args.outputDirectory = args.outputPath.parent_path(); + if( args.outputDirectory.empty() ) + { + args.outputDirectory = "."; + } + if( not std::filesystem::is_directory( args.outputDirectory ) ) + { + std::cout << "ERROR: target directory " << args.outputDirectory << " does not exist." << std::endl; + exit( 1 ); + } + + if( args.multi ) + { + const size_t SUFFIX_LEN = strlen( TRACE_FILE_SUFFIX ); + if( args.outputFileName.ends_with( TRACE_FILE_SUFFIX ) ) + { + // convert ".tracy" to "" + args.outputFileName = args.outputFileName.substr( 0, args.outputFileName.size() - SUFFIX_LEN ); + } + // add a final "." to restrict filename matches to the form ..tracy + args.outputFileName += "."; + + // we need to check all files starting with the prefix + std::cout << "search for existing files matching '" << args.outputDirectory.string() << "/" + << args.outputFileName << "*" << TRACE_FILE_SUFFIX << "'" << std::endl; + + auto foundExisting = false; + for( auto const& entry : std::filesystem::directory_iterator( args.outputDirectory ) ) + { + auto filename = entry.path().filename().string(); + if( filename.starts_with( args.outputFileName ) and filename.ends_with( TRACE_FILE_SUFFIX ) ) + { + if( args.overwrite ) + { + if( args.verbose ) + { + std::cout << "Deleting " << entry.path() << std::endl; + } + std::filesystem::remove( entry ); + } + else + { + std::cout << "Conflict: file already exist: " << entry.path() << std::endl; + foundExisting = true; + } + } + } + if( foundExisting and not args.overwrite ) + { + printf( "Files matching the target prefix already exists! Use -f to clear them all before starting " + "capture.\n" ); + exit( 1 ); + } + } + else + { + if( std::filesystem::exists( args.outputDirectory / args.outputFileName ) ) + { + if( args.overwrite ) + { + std::filesystem::remove( args.outputPath ); + } + else + { + std::cout << "ERROR: Output file " << args.outputPath + << " already exists! Use -f to force overwrite." << std::endl; + exit( 4 ); + } + } + } + + return args; + } + + // debug - don't merge + void print() + { + std::cout << "memlimit " << memoryLimit << "\n"; + std::cout << "output directory " << outputDirectory << "\n"; + std::cout << "output filename " << outputFileName << "\n"; + std::cout << "verbose " << verbose << "\n"; + std::cout << "address " << address << "\n"; + std::cout << "port " << port << "\n"; + std::cout << "stopAfter (seconds)" + << std::chrono::duration_cast>( stopAfter ).count() << "\n"; + std::cout << "multi " << multi << "\n"; + } +}; + +int runCaptureSingle( CaptureArgs const& args ) +{ + std::string const outputStr = ( args.outputDirectory / args.outputFileName ).generic_string(); + char const* output = outputStr.c_str(); + FILE* test = fopen( output, "wb" ); + if( !test ) + { + printf( "Cannot open output file %s for writing!\n", output ); + return 5; + } + fclose( test ); + unlink( output ); + + printf( "Connecting to %s:%i...", args.address.c_str(), args.port ); + fflush( stdout ); + tracy::Worker worker( args.address.c_str(), args.port, args.memoryLimit ); + while( !worker.HasData() ) + { + const auto handshake = static_cast( worker.GetHandshakeStatus() ); + int status = checkHandshake( handshake ); + if( status != 0 ) + { + return status; + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); + } + printf( "\nQueue delay: %s\nTimer resolution: %s\n", tracy::TimeToString( worker.GetDelay() ), + tracy::TimeToString( worker.GetResolution() ) ); + + SetupDisconnectSignalHandler(); + + const auto t0 = std::chrono::high_resolution_clock::now(); + while( worker.IsConnected() ) + { + // Relaxed order is sufficient here because `s_disconnect` is only ever + // set by this thread or by the SigInt handler, and that handler does + // nothing else than storing `s_disconnect`. + if( s_disconnect.load( std::memory_order_relaxed ) ) + { + worker.Disconnect(); + // Relaxed order is sufficient because only this thread ever reads + // this value. + s_disconnect.store( false, std::memory_order_relaxed ); + break; + } + // Output progress info only if destination is a TTY to avoid bloating + // log files (so this is not just about usage of ANSI color codes). + if( IsStdoutATerminal() ) + { + printWorkerUpdate( worker, args.memoryLimit, true, true ); + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); + if( args.stopAfter > 0s ) + { + const auto dur = std::chrono::high_resolution_clock::now() - t0; + if( dur >= args.stopAfter ) + { + // Relaxed order is sufficient because only this thread ever reads + // this value. + s_disconnect.store( true, std::memory_order_relaxed ); + } + } + } + const auto t1 = std::chrono::high_resolution_clock::now(); + + printWorkerFailure( worker, "" ); + + printf( "\nFrames: %" PRIu64 "\nTime span: %s\nZones: %s\nElapsed time: %s\nSaving trace...", + worker.GetFrameCount( *worker.GetFramesBase() ), + tracy::TimeToString( worker.GetLastTime() - worker.GetFirstTime() ), + tracy::RealToString( worker.GetZoneCount() ), + tracy::TimeToString( std::chrono::duration_cast( t1 - t0 ).count() ) ); + fflush( stdout ); + auto f = std::unique_ptr( tracy::FileWrite::Open( output, tracy::FileCompression::Zstd, 3, 4 ) ); + if( f ) + { + worker.Write( *f, false ); + AnsiPrintf( ANSI_GREEN ANSI_BOLD, " done!\n" ); + f->Finish(); + const auto stats = f->GetCompressionStatistics(); + printf( "Trace size %s (%.2f%% ratio)\n", tracy::MemSizeToString( stats.second ), + 100.f * stats.second / stats.first ); + } + else + { + AnsiPrintf( ANSI_RED ANSI_BOLD, " failed!\n" ); + } + + return 0; +} + +int runCaptureMulti( CaptureArgs const& args ) +{ + using hr_clock = std::chrono::high_resolution_clock; + auto const startCaptureTimestamp = hr_clock::now(); + + // configure signal handling early + // to ensure the capture loop halts at some point, once we got the order to stop: + // - we stop discovering new clients + // - we send disconnect signal to existing ones + SetupDisconnectSignalHandler(); + + tracy::UdpListen broadListen = tracy::UdpListen(); + if( !broadListen.Listen( tracy::DEFAULT_BROADCAST_UDP_PORT ) ) + { + std::cout << "Failed to listen to UDP broadcast on port" << tracy::DEFAULT_BROADCAST_UDP_PORT << std::endl; + return 1; + } + if( args.verbose ) + { + std::cout << "Listening for client UDP broadcast messages on port " << tracy::DEFAULT_BROADCAST_UDP_PORT + << std::endl; + } + + std::vector knownClients; + bool stillRunningClients = false; + bool waitingForFirstClient = true; // set to false if we find a client, or if SIGINT is caught + auto last_status_print = hr_clock::now(); + // tracks how many lines to erase before print multi-line status update + // if any other message is printed, it must be reset back to 0 to avoid "eating" a line + int liveUpdateLines = 0; + + while( stillRunningClients or waitingForFirstClient ) + { + // discover new clients + if( not s_disconnect.load( std::memory_order::relaxed ) ) + { + while( true ) + { + auto bcastClientOpt = receive_client_broadcast( &broadListen ); + if( not bcastClientOpt.has_value() ) + { + break; + } + RunningClient& bcastClient = bcastClientOpt.value(); + + // check if client already exists + bool matchesExistingClient = false; + for( auto const& alreadyKnownClient : knownClients ) + { + if( bcastClient.id == alreadyKnownClient.id ) + { + matchesExistingClient = true; + break; + } + } + if( not matchesExistingClient ) + { + if( bcastClient.msg.protocolVersion != tracy::ProtocolVersion ) + { + AnsiPrintf( ANSI_RED, "Rejecting client %s; bad protocol version\n", + bcastClient.runtimeName().c_str() ); + liveUpdateLines = 0; + bcastClient.status = ClientStatus::DISABLED; + knownClients.emplace_back( std::move( bcastClient ) ); + continue; + } + if( args.verbose ) + { + std::cout << "Detected client:" << "\n" + << "\tName: " << bcastClient.msg.programName << "\n" + << "\tID: " << bcastClient.id << "\n" + << "\tAddress: " << bcastClient.addr << "\n" + << "\tPort: " << bcastClient.msg.listenPort << std::endl; + liveUpdateLines = 0; + } + bcastClient.worker = std::make_unique( bcastClient.addr, bcastClient.msg.listenPort, + tracy::NO_WORKER_MEMORY_LIMIT ); + knownClients.emplace_back( std::move( bcastClient ) ); + } + } + } + + // review pending clients - once handsake succeeded we move them to the running client list + for( auto& candidate : knownClients ) + { + if( candidate.status != ClientStatus::DISCOVERED ) + { + continue; + } + + // if data got through, promote it to a running client + if( candidate.worker->HasData() ) + { + if( args.verbose ) + { + printf( "Connected to client '%s' (from %s:%d, PID: %lu)\n", candidate.msg.programName, + candidate.addr, candidate.msg.listenPort, candidate.msg.pid ); + liveUpdateLines = 0; + } + candidate.status = ClientStatus::RUNNING; + + waitingForFirstClient = false; + continue; + } + + // check if handshake failed - if so, remove the client + const auto handshake = static_cast( candidate.worker->GetHandshakeStatus() ); + if( checkHandshake( handshake ) != 0 ) + { + printf( "-> client '%s' (from %s:%d, PID: %lu) was ignored because of failed handshake status\n", + candidate.msg.programName, candidate.addr, candidate.msg.listenPort, candidate.msg.pid ); + liveUpdateLines = 0; + candidate.status = ClientStatus::DISABLED; + candidate.worker = nullptr; + } + } + + // review running clients + // If we notice a disconnect, print the failure message if relevant + stillRunningClients = false; + for( auto& client : knownClients ) + { + if( client.status == ClientStatus::RUNNING ) + { + if( client.worker->IsConnected() ) + { + stillRunningClients = true; + } + else + { + if( printWorkerFailure( *client.worker, client.runtimeName().c_str() ) ) + { + printf( "\n" ); + liveUpdateLines = 0; + } + client.status = ClientStatus::FINISHED; + client.finishedAt = hr_clock::now(); + } + } + } + + // if disconnecting, send disconnect to active workers + if( s_disconnect.load( std::memory_order::relaxed ) ) + { + waitingForFirstClient = false; + for( auto& client : knownClients ) + { + if( client.status == ClientStatus::RUNNING and not client.disconnectSent ) + { + std::cout << "disconnecting " << client << std::endl; + client.worker->Disconnect(); + liveUpdateLines = 0; + client.disconnectSent = true; + } + } + } + + // Print status update + // Done only if we have a terminal output to avoid bloating log files + // we print at regular time intervals, and one last time when there are no clients left + auto now = hr_clock::now(); + if( IsStdoutATerminal() and ( now - last_status_print > PRINT_UPDATE_INTERVAL or not stillRunningClients ) ) + { + for( ; liveUpdateLines > 0; liveUpdateLines-- ) + { + AnsiPrintf( ANSI_UP_ONE_LINE ANSI_ERASE_LINE, "" ); + } + double elapsedSeconds = + std::chrono::duration_cast>( now - startCaptureTimestamp ).count(); + AnsiPrintf( ANSI_YELLOW, "t=%.1lfs", elapsedSeconds ); + AnsiPrintf( ANSI_RED, " | " ); + printCurrentMemoryUsage( tracy::NO_WORKER_MEMORY_LIMIT ); + printf( "\n" ); + liveUpdateLines++; + for( auto const& client : knownClients ) + { + char const* statusColor; + switch( client.status ) + { + case ClientStatus::RUNNING: + statusColor = ANSI_GREEN; + break; + case ClientStatus::FINISHED: + statusColor = ANSI_BLUE; + break; + default: + continue; + } + AnsiPrintf( statusColor, "%s", client.runtimeName().c_str() ); + printWorkerUpdate( *client.worker, tracy::NO_WORKER_MEMORY_LIMIT, false, false ); + printf( "\n" ); + liveUpdateLines++; + } + last_status_print = now; + } + + std::this_thread::sleep_for( MULTI_CAPTURE_LOOP_INTERVAL ); + } + + // end of main loop + // checking if we have something to save, and if so do it + auto endCaptureTimestamp = std::chrono::high_resolution_clock::now(); + bool gotAtLeastOneClient = + std::any_of( knownClients.begin(), knownClients.end(), []( auto const& client ) + { return client.status == ClientStatus::RUNNING or client.status == ClientStatus::FINISHED; } ); + if( not gotAtLeastOneClient ) + { + std::cout << "Did not capture data, exiting" << std::endl; + return 0; + } + + std::cout << "Writing output..." << std::endl; + for( auto& client : knownClients ) + { + if( client.status != ClientStatus::FINISHED ) + { + continue; + } + AnsiPrintf( ANSI_BLUE, "- %s\n", client.runtimeName().c_str() ); + printf( " Frames: %" PRIu64 "\n Time span: %s\n Zones: %s\n Elapsed time: %s\n Saving trace...", + client.worker->GetFrameCount( *client.worker->GetFramesBase() ), + tracy::TimeToString( client.worker->GetLastTime() - client.worker->GetFirstTime() ), + tracy::RealToString( client.worker->GetZoneCount() ), + tracy::TimeToString( + std::chrono::duration_cast( client.finishedAt - client.detectedAt ) + .count() ) ); + std::filesystem::path filepath = args.outputDirectory / client.saveName( args.outputFileName ); + auto f = std::unique_ptr( + tracy::FileWrite::Open( filepath.generic_string().c_str(), tracy::FileCompression::Zstd, 3, 4 ) ); + if( f ) + { + client.worker->Write( *f, false ); + AnsiPrintf( ANSI_GREEN, "done: %s\n", filepath.string().c_str() ); + f->Finish(); + const auto stats = f->GetCompressionStatistics(); + printf( " Trace size %s (%.2f%% ratio)\n", tracy::MemSizeToString( stats.second ), + 100.f * stats.second / stats.first ); + } + else + { + AnsiPrintf( ANSI_RED, "failed!\n" ); + } + } + + return 0; +} + +int main( int argc, char* argv[] ) +{ +#ifdef _WIN32 + if( !AttachConsole( ATTACH_PARENT_PROCESS ) ) + { + AllocConsole(); + SetConsoleMode( GetStdHandle( STD_OUTPUT_HANDLE ), 0x07 ); + } +#endif + + InitIsStdoutATerminal(); + + CaptureArgs args = CaptureArgs::parse( argc, argv ); + args.print(); // remove before merge + + if( args.multi ) + { + return runCaptureMulti( args ); + } + else + { + return runCaptureSingle( args ); + } +} diff --git a/cmake/server.cmake b/cmake/server.cmake index c12a340842..34d100652a 100644 --- a/cmake/server.cmake +++ b/cmake/server.cmake @@ -17,6 +17,7 @@ set(TRACY_SERVER_SOURCES TracyMemory.cpp TracyMmap.cpp TracyPrint.cpp + TracyProtocolServer.cpp TracySysUtil.cpp TracyTaskDispatch.cpp TracyTextureCompression.cpp diff --git a/multiprocess/CMakeLists.txt b/multiprocess/CMakeLists.txt new file mode 100644 index 0000000000..fe0fa91b9c --- /dev/null +++ b/multiprocess/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.16) + +option(NO_ISA_EXTENSIONS "Disable ISA extensions (don't pass -march=native or -mcpu=native to the compiler)" OFF) +option(NO_STATISTICS OFF) # we need those to get processed source zone locations + +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake) + +set(CMAKE_CXX_STANDARD 20) + +project( + tracy-multiprocess + LANGUAGES C CXX ASM + 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) + + +add_executable(tracy-merge + src/merger.cpp) +target_link_libraries(tracy-merge PRIVATE TracyServer TracyGetOpt) + +set_property(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME}) diff --git a/multiprocess/src/merger.cpp b/multiprocess/src/merger.cpp new file mode 100644 index 0000000000..d00146b226 --- /dev/null +++ b/multiprocess/src/merger.cpp @@ -0,0 +1,337 @@ +#include "TracyFileRead.hpp" +#include "TracyFileWrite.hpp" +#include "TracyWorker.hpp" +#include "getopt.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +bool orderImportTimelineEvents( tracy::Worker::ImportEventTimeline const& a, + tracy::Worker::ImportEventTimeline const& b ) +{ + return a.timestamp < b.timestamp; +} + +bool orderImportMessageEvents( tracy::Worker::ImportEventMessages const& a, + tracy::Worker::ImportEventMessages const& b ) +{ + return a.timestamp < b.timestamp; +} + +struct ExportedWorker +{ + std::vector timeline; + std::vector messages; + std::vector plots; + + std::unordered_map threadNames; + std::string name; + std::string process; + + static ExportedWorker merge( std::vector inputList ) + { + uint64_t nextFreeThreadId = 1; + ExportedWorker out; + + // for some data, arbitrarily take the infos from the first trace + auto const& firstExport = *inputList[0]; + out.name = firstExport.name; + out.process = firstExport.process; + + // quick pass to allocate output vectors + size_t numTimelineEvents = 0, numMessages = 0, numPlots = 0; + for( auto const& inputWorker : inputList ) + { + numTimelineEvents += inputWorker->timeline.size(); + numMessages += inputWorker->messages.size(); + numPlots += inputWorker->plots.size(); + } + out.timeline.reserve( numTimelineEvents ); + out.messages.reserve( numMessages ); + out.plots.reserve( numPlots ); + + size_t eventsSortedSoFar = 0; + size_t messagesSortedSoFar = 0; + + // keep track of registered threads to avoid overlaps + std::unordered_map localThreadToMultiprocess; + + for( auto exportPtr : inputList ) + { + ExportedWorker const& exported = *exportPtr; + + // rebuild thread mapping + // we try to keep original thread IDs intact if possible, falling back to made-up IDs + // in case of conflict + for( auto const& threadId : exported.threadNames ) + { + uint64_t multiprocessId = threadId.first; + if( localThreadToMultiprocess.contains( multiprocessId ) ) + { + // oh-oh, conflict - let's take a random ID instead; + multiprocessId = nextFreeThreadId++; + } + localThreadToMultiprocess[threadId.first] = multiprocessId; + out.threadNames[multiprocessId] = threadId.second; + } + + // translate all events with the right thread IDs + for( auto&& event : exported.timeline ) + { + tracy::Worker::ImportEventTimeline& inserted = out.timeline.emplace_back( event ); + inserted.tid = localThreadToMultiprocess[inserted.tid]; + } + for( auto&& message : exported.messages ) + { + tracy::Worker::ImportEventMessages& inserted = out.messages.emplace_back( message ); + inserted.tid = localThreadToMultiprocess[inserted.tid]; + } + for( auto&& plots : exported.plots ) + { + out.plots.emplace_back( plots ); + } + + // sort timeline and messages events + std::inplace_merge( out.timeline.begin(), out.timeline.begin() + eventsSortedSoFar, out.timeline.end(), + orderImportTimelineEvents ); + eventsSortedSoFar += exported.timeline.size(); + std::inplace_merge( out.messages.begin(), out.messages.begin() + messagesSortedSoFar, out.messages.end(), + orderImportMessageEvents ); + messagesSortedSoFar += exported.messages.size(); + } + return out; + } + + static std::optional fromTracyFile( std::string const& filepath, bool exportPlots ) + { + std::unique_ptr sourceFile{ tracy::FileRead::Open( ( filepath.c_str() ) ) }; + if( !sourceFile ) + { + std::cerr << "Could not find file" << std::endl; + return std::nullopt; + } + std::cout << "reading " << filepath << std::endl; + + tracy::Worker worker{ *sourceFile, tracy::EventType::All, + true, // otherwise source zones are empty + false }; + while( !worker.AreSourceLocationZonesReady() ) + { + std::cout << "Waiting for source locations" << std::endl; + std::this_thread::sleep_for( 1s ); + } + + ExportedWorker exportedData; + exportedData.name = worker.GetCaptureName(); + exportedData.process = worker.GetCaptureProgram(); + std::cout << exportedData.name << " (" << exportedData.process << ")" << std::endl; + + std::unordered_set seenThreadIds; + + auto& sourceLocationZones = worker.GetSourceLocationZones(); + std::cout << "- " << sourceLocationZones.size() << " events" << std::endl; + for( auto&& zone_it : sourceLocationZones ) + { + const tracy::SourceLocation& sourceLoc = worker.GetSourceLocation( zone_it.first ); + std::string zoneFilePath = worker.GetString( sourceLoc.file ); + int zoneLine = sourceLoc.line; + std::string zoneName = worker.GetZoneName( sourceLoc ); + + auto const& zones = zone_it.second; + for( auto&& zoneData : zones.zones ) + { + const auto zone_event = zoneData.Zone(); + const uint64_t threadFullId = worker.DecompressThread( zoneData.Thread() ); + const auto start = zone_event->Start(); + const auto end = zone_event->End(); + seenThreadIds.emplace( threadFullId ); + + auto& startEvent = exportedData.timeline.emplace_back(); + startEvent.locFile = zoneFilePath; + startEvent.locLine = zoneLine; + startEvent.name = zoneName; + startEvent.tid = threadFullId; + startEvent.isEnd = false; + startEvent.timestamp = zone_event->Start(); + + auto& endEvent = exportedData.timeline.emplace_back(); + endEvent.locFile = zoneFilePath; + endEvent.locLine = zoneLine; + endEvent.name = zoneName; + endEvent.tid = threadFullId; + endEvent.isEnd = true; + endEvent.timestamp = zone_event->End(); + } + } + // need to sort because we split 'begin' and 'end' events + std::sort( exportedData.timeline.begin(), exportedData.timeline.end(), orderImportTimelineEvents ); + + auto const& messages = worker.GetMessages(); + std::cout << "- " << messages.size() << " messages" << std::endl; + for( auto const& messages_it : worker.GetMessages() ) + { + tracy::MessageData const& messageData = *messages_it; + tracy::Worker::ImportEventMessages importMessage; + uint64_t const threadId = worker.DecompressThread( messageData.thread ); + importMessage.tid = threadId; + importMessage.message = worker.GetString( messageData.ref ); + importMessage.timestamp = messageData.time; + + exportedData.messages.push_back( importMessage ); + seenThreadIds.emplace( threadId ); + } + // to be sure, but should not do a lot + std::sort( exportedData.messages.begin(), exportedData.messages.end(), orderImportMessageEvents ); + + if( exportPlots ) + { + auto const& plots = worker.GetPlots(); + std::cout << "- " << plots.size() << " plots" << std::endl; + for( auto const& plots_it : worker.GetPlots() ) + { + tracy::Worker::ImportEventPlots importPlot; + importPlot.name = worker.GetString( plots_it->name ); + importPlot.format = plots_it->format; + + importPlot.data.resize( plots_it->data.size() ); + for( auto const& elt : plots_it->data ) + { + std::pair dataPoint{ elt.time.Val(), elt.val }; + importPlot.data.push_back( dataPoint ); + } + exportedData.plots.push_back( importPlot ); + } + } + + for( auto&& tid : seenThreadIds ) + { + std::string name = worker.GetThreadName( tid ); + exportedData.threadNames[tid] = exportedData.process + "/" + name; + } + + return exportedData; + } +}; + +[[noreturn]] void Usage() +{ + printf( "Usage: merge [-fp] -o output.tracy input1.tracy [input2.tracy]...\n\n" ); + printf( "Options\n" ); + printf( " --output/-o Output file path\n" ); + printf( " --force/-f Overwrite output file if it exists\n" ); + printf( " --export-plots/-p (experimental) Also exports plots\n" ); + exit( 1 ); +} + +struct Args +{ + std::vector inputPaths; + std::string outputPath; + bool exportPlots = false; + + static Args parse( int argc, char* argv[] ) + { + Args args; + // option parsing + bool overwrite = false; + int c; + const struct option long_options[] = { + { "output", required_argument, 0, 'o' }, + { "force", no_argument, 0, 'f' }, + { "export-plots", no_argument, 0, 'p' }, + { 0, 0, 0, 0 }, + }; + while( ( c = getopt_long( argc, argv, "o:fp", long_options, nullptr ) ) != -1 ) + { + switch( c ) + { + case 'o': + args.outputPath = optarg; + break; + case 'f': + overwrite = true; + break; + case 'p': + args.exportPlots = true; + break; + default: + Usage(); + break; + } + } + for( int argIndex = optind; argIndex < argc; argIndex++ ) + { + args.inputPaths.push_back( argv[argIndex] ); + } + if( args.inputPaths.size() == 0 or args.outputPath.empty() ) + { + Usage(); + } + if( std::filesystem::exists( args.outputPath ) and not overwrite ) + { + printf( "Output file %s already exists! Use -f to force overwrite.\n", args.outputPath.c_str() ); + exit( 4 ); + } + for( auto const& input : args.inputPaths ) + { + if( not std::filesystem::exists( input ) ) + { + printf( "Input file %s does not exist!\n", input.c_str() ); + exit( 4 ); + } + } + return args; + } +}; + +int main( int argc, char* argv[] ) +{ + auto args = Args::parse( argc, argv ); + + std::vector exports; + for( auto path : args.inputPaths ) + { + auto importedOpt = ExportedWorker::fromTracyFile( path, args.exportPlots ); + if( not importedOpt.has_value() ) + { + std::cerr << "Error importing " << path << std::endl; + return 1; + } + exports.push_back( ( importedOpt.value() ) ); + } + + std::vector exportRefs; + std::transform( exports.cbegin(), exports.cend(), std::back_inserter( exportRefs ), + []( ExportedWorker const& ex ) { return &ex; } ); + + auto mergedImport = ExportedWorker::merge( exportRefs ); + + { + std::cout << "Writing " << args.outputPath << std::endl; + auto outputFileWrite = std::unique_ptr( tracy::FileWrite::Open( args.outputPath.c_str() ) ); + if( !outputFileWrite ) + { + fprintf( stderr, "Cannot open output file!\n" ); + exit( 1 ); + } + tracy::Worker outputWorker( mergedImport.name.c_str(), mergedImport.process.c_str(), mergedImport.timeline, + mergedImport.messages, mergedImport.plots, mergedImport.threadNames ); + outputWorker.Write( *outputFileWrite, false ); + outputFileWrite->Finish(); + } + + std::cout << "done" << std::endl; + + return 0; +} diff --git a/profiler/src/main.cpp b/profiler/src/main.cpp index 8292a692a1..a7745a10cc 100644 --- a/profiler/src/main.cpp +++ b/profiler/src/main.cpp @@ -46,6 +46,7 @@ #include "../../server/tracy_robin_hood.h" #include "../../server/TracyFileHeader.hpp" #include "../../server/TracyFileRead.hpp" +#include "../../server/TracyProtocolServer.hpp" #include "../../server/TracyPrint.hpp" #include "../../server/TracySysUtil.hpp" #include "../../server/TracyWorker.hpp" @@ -85,7 +86,7 @@ enum class ViewShutdown { False, True, Join }; static tracy::unordered_flat_map clients; static std::unique_ptr view; static tracy::BadVersionState badVer; -static uint16_t port = 8086; +static uint16_t port = tracy::DEFAULT_BROADCAST_UDP_PORT; static const char* connectTo = nullptr; static char title[128]; static std::thread loadThread, updateThread, updateNotesThread; @@ -452,76 +453,15 @@ static void UpdateBroadcastClients() { auto msg = broadcastListen->Read( len, addr, 0 ); if( !msg ) break; - if( len > sizeof( tracy::BroadcastMessage ) ) continue; - uint16_t broadcastVersion; - memcpy( &broadcastVersion, msg, sizeof( uint16_t ) ); - if( broadcastVersion <= tracy::BroadcastVersion ) + auto parsedMessageOpt = tracy::ParseBroadcastMessage(msg, len); + if (parsedMessageOpt.has_value()) { - uint32_t protoVer; - char procname[tracy::WelcomeMessageProgramNameSize]; - int32_t activeTime; - uint16_t listenPort; - uint64_t pid; - - switch( broadcastVersion ) - { - case 3: - { - tracy::BroadcastMessage bm; - memcpy( &bm, msg, len ); - protoVer = bm.protocolVersion; - strcpy( procname, bm.programName ); - activeTime = bm.activeTime; - listenPort = bm.listenPort; - pid = bm.pid; - break; - } - case 2: - { - if( len > sizeof( tracy::BroadcastMessage_v2 ) ) continue; - tracy::BroadcastMessage_v2 bm; - memcpy( &bm, msg, len ); - protoVer = bm.protocolVersion; - strcpy( procname, bm.programName ); - activeTime = bm.activeTime; - listenPort = bm.listenPort; - pid = 0; - break; - } - case 1: - { - if( len > sizeof( tracy::BroadcastMessage_v1 ) ) continue; - tracy::BroadcastMessage_v1 bm; - memcpy( &bm, msg, len ); - protoVer = bm.protocolVersion; - strcpy( procname, bm.programName ); - activeTime = bm.activeTime; - listenPort = bm.listenPort; - pid = 0; - break; - } - case 0: - { - if( len > sizeof( tracy::BroadcastMessage_v0 ) ) continue; - tracy::BroadcastMessage_v0 bm; - memcpy( &bm, msg, len ); - protoVer = bm.protocolVersion; - strcpy( procname, bm.programName ); - activeTime = bm.activeTime; - listenPort = 8086; - pid = 0; - break; - } - default: - assert( false ); - break; - } - + auto parsedMessage = parsedMessageOpt.value(); auto address = addr.GetText(); + const auto clientId = tracy::ClientUniqueID(addr, parsedMessage.listenPort); const auto ipNumerical = addr.GetNumber(); - const auto clientId = uint64_t( ipNumerical ) | ( uint64_t( listenPort ) << 32 ); auto it = clients.find( clientId ); - if( activeTime >= 0 ) + if( parsedMessage.activeTime >= 0 ) { if( it == clients.end() ) { @@ -538,16 +478,16 @@ static void UpdateBroadcastClients() } ); } resolvLock.unlock(); - clients.emplace( clientId, ClientData { time, protoVer, activeTime, listenPort, pid, procname, std::move( ip ) } ); + clients.emplace( clientId, ClientData { time, parsedMessage.protocolVersion, parsedMessage.activeTime, parsedMessage.listenPort, parsedMessage.pid, parsedMessage.programName, std::move( ip ) } ); } else { it->second.time = time; - it->second.activeTime = activeTime; - it->second.port = listenPort; - it->second.pid = pid; - it->second.protocolVersion = protoVer; - if( strcmp( it->second.procName.c_str(), procname ) != 0 ) it->second.procName = procname; + it->second.activeTime = parsedMessage.activeTime; + it->second.port = parsedMessage.listenPort; + it->second.pid = parsedMessage.pid; + it->second.protocolVersion = parsedMessage.protocolVersion; + if( strcmp( it->second.procName.c_str(), parsedMessage.programName ) != 0 ) it->second.procName = parsedMessage.programName; } } else if( it != clients.end() ) diff --git a/profiler/src/profiler/TracyView.cpp b/profiler/src/profiler/TracyView.cpp index bdeb7d720c..f07b715f80 100644 --- a/profiler/src/profiler/TracyView.cpp +++ b/profiler/src/profiler/TracyView.cpp @@ -36,7 +36,7 @@ namespace tracy double s_time = 0; View::View( void(*cbMainThread)(const std::function&, bool), const char* addr, uint16_t port, ImFont* fixedWidth, ImFont* smallFont, ImFont* bigFont, SetTitleCallback stcb, SetScaleCallback sscb, AttentionCallback acb, const Config& config, AchievementsMgr* amgr ) - : m_worker( addr, port, config.memoryLimit == 0 ? -1 : ( config.memoryLimitPercent * tracy::GetPhysicalMemorySize() / 100 ) ) + : m_worker( addr, port, config.memoryLimit == 0 ? NO_WORKER_MEMORY_LIMIT : ( config.memoryLimitPercent * tracy::GetPhysicalMemorySize() / 100 ) ) , m_staticView( false ) , m_viewMode( ViewMode::LastFrames ) , m_viewModeHeuristicTry( true ) diff --git a/public/client/TracyProfiler.cpp b/public/client/TracyProfiler.cpp index 80fe1561b6..2ad0a6b5fb 100644 --- a/public/client/TracyProfiler.cpp +++ b/public/client/TracyProfiler.cpp @@ -1629,12 +1629,12 @@ void Profiler::Worker() auto dataPort = m_userPort != 0 ? m_userPort : TRACY_DATA_PORT; #else const bool dataPortSearch = m_userPort == 0; - auto dataPort = m_userPort != 0 ? m_userPort : 8086; + auto dataPort = m_userPort != 0 ? m_userPort : DEFAULT_CLIENT_DATA_TCP_PORT; #endif #ifdef TRACY_BROADCAST_PORT const auto broadcastPort = TRACY_BROADCAST_PORT; #else - const auto broadcastPort = 8086; + const auto broadcastPort = DEFAULT_BROADCAST_UDP_PORT; #endif while( m_timeBegin.load( std::memory_order_relaxed ) == 0 ) std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); diff --git a/public/common/TracyProtocol.hpp b/public/common/TracyProtocol.hpp index ad90a7ad7b..e92ed06681 100644 --- a/public/common/TracyProtocol.hpp +++ b/public/common/TracyProtocol.hpp @@ -7,6 +7,9 @@ namespace tracy { +constexpr uint16_t DEFAULT_BROADCAST_UDP_PORT = 8086; +constexpr uint16_t DEFAULT_CLIENT_DATA_TCP_PORT = 8086; + constexpr unsigned Lz4CompressBound( unsigned isize ) { return isize + ( isize / 255 ) + 16; } enum : uint32_t { ProtocolVersion = 68 }; diff --git a/public/common/TracySocket.cpp b/public/common/TracySocket.cpp index bdba361965..64567296a3 100644 --- a/public/common/TracySocket.cpp +++ b/public/common/TracySocket.cpp @@ -651,7 +651,7 @@ void IpAddress::Set( const struct sockaddr& addr ) #else auto ai = (const struct sockaddr_in*)&addr; #endif - inet_ntop( AF_INET, &ai->sin_addr, m_text, 17 ); + inet_ntop( AF_INET, &ai->sin_addr, m_text, TEXT_SIZE ); m_number = ai->sin_addr.s_addr; } diff --git a/public/common/TracySocket.hpp b/public/common/TracySocket.hpp index f7713aac66..d3cbcb405b 100644 --- a/public/common/TracySocket.hpp +++ b/public/common/TracySocket.hpp @@ -112,6 +112,8 @@ class UdpBroadcast class IpAddress { public: + static constexpr size_t TEXT_SIZE = 17; + IpAddress(); ~IpAddress(); @@ -127,7 +129,7 @@ class IpAddress private: uint32_t m_number; - char m_text[17]; + char m_text[TEXT_SIZE]; }; class UdpListen diff --git a/server/TracyProtocolServer.cpp b/server/TracyProtocolServer.cpp new file mode 100644 index 0000000000..5b220be7b2 --- /dev/null +++ b/server/TracyProtocolServer.cpp @@ -0,0 +1,107 @@ +#include "TracyProtocolServer.hpp" +#include +#include +#include + +namespace tracy +{ +std::optional ParseBroadcastMessage( const char* msg, size_t msgLen ) +{ + if( msgLen < sizeof( uint16_t ) ) + { + std::cout << "Received too short broadcast message" << std::endl; + return std::nullopt; + } + uint16_t broadcastVersion; + memcpy( &broadcastVersion, msg, sizeof( uint16_t ) ); + if( broadcastVersion > tracy::BroadcastVersion ) + { + std::cout << "Received broadcast message with unsupported version: " << broadcastVersion << std::endl; + return std::nullopt; + } + switch( broadcastVersion ) + { + case 3: + { + if( msgLen > sizeof( tracy::BroadcastMessage ) ) + { + std::cout << "Received unexpected size broadcast v3 message" << std::endl; + return std::nullopt; + } + tracy::BroadcastMessage bm; + memcpy( &bm, msg, msgLen ); + return bm; + break; + } + case 2: + { + if( msgLen > sizeof( tracy::BroadcastMessage_v2 ) ) + { + std::cout << "Received unexpected size broadcast v2 message" << std::endl; + return std::nullopt; + } + tracy::BroadcastMessage_v2 bm; + memcpy( &bm, msg, msgLen ); + + tracy::BroadcastMessage out; + out.broadcastVersion = broadcastVersion; + out.protocolVersion = bm.protocolVersion; + out.activeTime = bm.activeTime; + out.listenPort = bm.listenPort; + strcpy( out.programName, bm.programName ); + out.pid = 0; + return out; + break; + } + case 1: + { + if( msgLen > sizeof( tracy::BroadcastMessage_v1 ) ) + { + std::cout << "Received unexpected size broadcast v1 message" << std::endl; + return std::nullopt; + } + tracy::BroadcastMessage_v1 bm; + memcpy( &bm, msg, msgLen ); + + tracy::BroadcastMessage out; + out.broadcastVersion = broadcastVersion; + out.protocolVersion = bm.protocolVersion; + out.activeTime = bm.activeTime; + out.listenPort = bm.listenPort; + strcpy( out.programName, bm.programName ); + out.pid = 0; + return out; + break; + } + case 0: + { + if( msgLen > sizeof( tracy::BroadcastMessage_v0 ) ) + { + std::cout << "Received unexpected size broadcast v0 message" << std::endl; + return std::nullopt; + } + tracy::BroadcastMessage_v0 bm; + memcpy( &bm, msg, msgLen ); + + tracy::BroadcastMessage out; + out.broadcastVersion = broadcastVersion; + out.protocolVersion = bm.protocolVersion; + out.activeTime = bm.activeTime; + out.listenPort = tracy::DEFAULT_CLIENT_DATA_TCP_PORT; + strcpy( out.programName, bm.programName ); + out.pid = 0; + return out; + break; + } + default: + assert( false ); + break; + } + return std::nullopt; +} + +uint64_t ClientUniqueID( tracy::IpAddress const& addr, uint16_t port ) +{ + return uint64_t( addr.GetNumber() ) | ( uint64_t( port ) << 32 ); +} +} diff --git a/server/TracyProtocolServer.hpp b/server/TracyProtocolServer.hpp new file mode 100644 index 0000000000..c2203c3702 --- /dev/null +++ b/server/TracyProtocolServer.hpp @@ -0,0 +1,17 @@ +// server-side functions supporting the protocol +#ifndef __TRACYPROTOCOLSERVER_HPP__ +#define __TRACYPROTOCOLSERVER_HPP__ + +#include "TracyProtocol.hpp" +#include "TracySocket.hpp" +#include + +namespace tracy +{ +// create the latest version of broadcast message, migrating older versions if possible +std::optional ParseBroadcastMessage( const char* msg, size_t msgLen ); +// internal unique ID for a client +uint64_t ClientUniqueID( tracy::IpAddress const& addr, uint16_t port ); +} + +#endif diff --git a/server/TracyWorker.cpp b/server/TracyWorker.cpp index 5c2972914c..c1281963af 100644 --- a/server/TracyWorker.cpp +++ b/server/TracyWorker.cpp @@ -318,7 +318,7 @@ Worker::Worker( const char* name, const char* program, const std::vectorcalibratedGpuTime = gpuTime; ctx->calibratedCpuTime = TscTime( ev.cpuTime ); } - + void Worker::ProcessGpuTimeSync( const QueueGpuTimeSync& ev ) { auto ctx = m_gpuCtxMap[ev.context]; diff --git a/server/TracyWorker.hpp b/server/TracyWorker.hpp index 0a4ca8292c..53e4d85a3b 100644 --- a/server/TracyWorker.hpp +++ b/server/TracyWorker.hpp @@ -52,6 +52,8 @@ namespace EventType }; } +constexpr int64_t NO_WORKER_MEMORY_LIMIT = -1; // anything <0 would do + struct UnsupportedVersion : public std::exception { UnsupportedVersion( int version ) : version( version ) {}