1
0
mirror of https://github.com/wolfpld/tracy.git synced 2025-03-20 07:40:02 +08:00

Merge 4f769258b4752f236eaacc750b1900640f7b5019 into 873c6ecac8bc324509bbd8ddbf62096b3b8153de

This commit is contained in:
Grégoire Roussel 2025-03-10 16:46:53 +01:00 committed by GitHub
commit 9665db7e90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1468 additions and 260 deletions

View File

@ -5,6 +5,7 @@
"${workspaceFolder}/capture",
"${workspaceFolder}/csvexport",
"${workspaceFolder}/import",
"${workspaceFolder}/multiprocess",
"${workspaceFolder}/update",
"${workspaceFolder}/test",
"${workspaceFolder}",

View File

@ -17,12 +17,17 @@ 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})
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})

View File

@ -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 )
const auto handshake = static_cast<tracy::HandshakeStatus>(worker.GetHandshakeStatus());
int status = checkHandshake(handshake);
if( status != 0 )
{
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 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; 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" );
}
}
}
}
}
}
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<std::chrono::nanoseconds>( t1 - t0 ).count() ) );
fflush( stdout );
auto f = std::unique_ptr<tracy::FileWrite>( tracy::FileWrite::Open( output, tracy::FileCompression::Zstd, 3, 4 ) );

224
capture/src/lib.cpp Normal file
View File

@ -0,0 +1,224 @@
// common library file for capture utilities
#ifdef _WIN32
# include <io.h>
# include <windows.h>
#else
# include <unistd.h>
#endif
#include <cstdarg>
#include <inttypes.h>
#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;
}
}

View File

@ -0,0 +1,706 @@
#include <algorithm>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <memory>
#include <optional>
#include <signal.h>
#include <sstream>
#include <thread>
#include <utility>
#include <vector>
#ifdef _WIN32
# include <windows.h>
#else
# include <unistd.h>
#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<bool> 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<tracy::Worker> worker = nullptr;
bool disconnectSent = false;
std::chrono::time_point<std::chrono::high_resolution_clock> detectedAt;
std::chrono::time_point<std::chrono::high_resolution_clock> 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<RunningClient> 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 <output/prefix> [--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 <str> 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 <ipv4 str> [SINGLE] Target IP address\n" );
printf( " -p/--port <int> [SINGLE] Target port\n" );
printf( " -s/--stop-after <float> [SINGLE] Stop profiling after this duration (in seconds) \n" );
printf( " -m/--memlimit <float> [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 <prefix>.<client_pid>.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 "<prefix>."
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<int>( 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 "<prefix>.tracy" to "<prefix>"
args.outputFileName = args.outputFileName.substr( 0, args.outputFileName.size() - SUFFIX_LEN );
}
// add a final "." to restrict filename matches to the form <prefix>.<pid>.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<std::chrono::duration<float>>( 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<tracy::HandshakeStatus>( 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<std::chrono::nanoseconds>( t1 - t0 ).count() ) );
fflush( stdout );
auto f = std::unique_ptr<tracy::FileWrite>( 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<RunningClient> 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<tracy::Worker>( 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<tracy::HandshakeStatus>( 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<std::chrono::duration<double>>( 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<std::chrono::nanoseconds>( client.finishedAt - client.detectedAt )
.count() ) );
std::filesystem::path filepath = args.outputDirectory / client.saveName( args.outputFileName );
auto f = std::unique_ptr<tracy::FileWrite>(
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 );
}
}

View File

@ -17,6 +17,7 @@ set(TRACY_SERVER_SOURCES
TracyMemory.cpp
TracyMmap.cpp
TracyPrint.cpp
TracyProtocolServer.cpp
TracySysUtil.cpp
TracyTaskDispatch.cpp
TracyTextureCompression.cpp

View File

@ -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})

337
multiprocess/src/merger.cpp Normal file
View File

@ -0,0 +1,337 @@
#include "TracyFileRead.hpp"
#include "TracyFileWrite.hpp"
#include "TracyWorker.hpp"
#include "getopt.h"
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <iostream>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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<tracy::Worker::ImportEventTimeline> timeline;
std::vector<tracy::Worker::ImportEventMessages> messages;
std::vector<tracy::Worker::ImportEventPlots> plots;
std::unordered_map<uint64_t, std::string> threadNames;
std::string name;
std::string process;
static ExportedWorker merge( std::vector<ExportedWorker const*> 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<uint64_t, uint64_t> 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<ExportedWorker> fromTracyFile( std::string const& filepath, bool exportPlots )
{
std::unique_ptr<tracy::FileRead> 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<uint64_t> 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<int64_t, double> 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 <filepath> 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<std::string> 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<ExportedWorker> 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<ExportedWorker const*> 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>( 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;
}

View File

@ -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"
@ -87,7 +88,7 @@ enum class ViewShutdown { False, True, Join };
static tracy::unordered_flat_map<uint64_t, ClientData> clients;
static std::unique_ptr<tracy::View> 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;
@ -458,76 +459,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() )
{
@ -544,16 +484,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() )

View File

@ -36,7 +36,7 @@ namespace tracy
double s_time = 0;
View::View( void(*cbMainThread)(const std::function<void()>&, 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 )

View File

@ -1679,12 +1679,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 ) );

View File

@ -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 = 72 };

View File

@ -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;
}

View File

@ -112,6 +112,8 @@ private:
class IpAddress
{
public:
static constexpr size_t TEXT_SIZE = 17;
IpAddress();
~IpAddress();
@ -127,7 +129,7 @@ public:
private:
uint32_t m_number;
char m_text[17];
char m_text[TEXT_SIZE];
};
class UdpListen

View File

@ -0,0 +1,107 @@
#include "TracyProtocolServer.hpp"
#include <cassert>
#include <cstring>
#include <iostream>
namespace tracy
{
std::optional<tracy::BroadcastMessage> 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 );
}
}

View File

@ -0,0 +1,17 @@
// server-side functions supporting the protocol
#ifndef __TRACYPROTOCOLSERVER_HPP__
#define __TRACYPROTOCOLSERVER_HPP__
#include "TracyProtocol.hpp"
#include "TracySocket.hpp"
#include <optional>
namespace tracy
{
// create the latest version of broadcast message, migrating older versions if possible
std::optional<tracy::BroadcastMessage> ParseBroadcastMessage( const char* msg, size_t msgLen );
// internal unique ID for a client
uint64_t ClientUniqueID( tracy::IpAddress const& addr, uint16_t port );
}
#endif

View File

@ -313,7 +313,7 @@ Worker::Worker( const char* name, const char* program, const std::vector<ImportE
, m_buffer( nullptr )
, m_onDemand( false )
, m_inconsistentSamples( false )
, m_memoryLimit( -1 )
, m_memoryLimit( NO_WORKER_MEMORY_LIMIT )
, m_traceVersion( CurrentVersion )
{
m_data.sourceLocationExpand.push_back( 0 );
@ -556,7 +556,7 @@ Worker::Worker( FileRead& f, EventType::Type eventMask, bool bgTasks, bool allow
, m_stream( nullptr )
, m_buffer( nullptr )
, m_inconsistentSamples( false )
, m_memoryLimit( -1 )
, m_memoryLimit( NO_WORKER_MEMORY_LIMIT )
, m_allowStringModification( allowStringModification )
{
auto loadStart = std::chrono::high_resolution_clock::now();

View File

@ -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 ) {}