initial
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(NO_STATISTICS ON)
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
project(
|
||||
tracy-capture
|
||||
LANGUAGES C CXX
|
||||
VERSION ${TRACY_VERSION_STRING}
|
||||
)
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/config.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/vendor.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/server.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/GitRef.cmake)
|
||||
|
||||
set(PROGRAM_FILES
|
||||
src/capture.cpp
|
||||
src/CaptureOutput.cpp
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME} ${PROGRAM_FILES} ${COMMON_FILES} ${SERVER_FILES})
|
||||
add_git_ref(${PROJECT_NAME})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE TracyServer TracyGetOpt)
|
||||
set_property(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME})
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
add_executable(tracy-capture-daemon src/capturedaemon.cpp src/CaptureOutput.cpp ${COMMON_FILES} ${SERVER_FILES})
|
||||
add_git_ref(tracy-capture-daemon)
|
||||
target_link_libraries(tracy-capture-daemon PRIVATE TracyServer TracyGetOpt)
|
||||
|
||||
install(TARGETS tracy-capture-daemon DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
#ifdef _WIN32
|
||||
# include <io.h>
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <inttypes.h>
|
||||
#include <thread>
|
||||
|
||||
#include "CaptureOutput.hpp"
|
||||
#include "../../public/common/TracyProtocol.hpp"
|
||||
#include "../../public/common/TracyStackFrames.hpp"
|
||||
#include "../../server/TracyMemory.hpp"
|
||||
#include "../../server/TracyPrint.hpp"
|
||||
#include "../../server/TracyWorker.hpp"
|
||||
|
||||
static bool s_isTerminal = false;
|
||||
|
||||
void InitTerminalDetection()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
s_isTerminal = _isatty( fileno( stdout ) );
|
||||
#else
|
||||
s_isTerminal = isatty( fileno( stdout ) );
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IsTerminal()
|
||||
{
|
||||
return s_isTerminal;
|
||||
}
|
||||
|
||||
void AnsiPrintf( const char* ansiEscape, const char* format, ... )
|
||||
{
|
||||
if( IsTerminal() )
|
||||
{
|
||||
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
|
||||
{
|
||||
va_list args;
|
||||
va_start( args, format );
|
||||
vfprintf( stdout, format, args );
|
||||
va_end( args );
|
||||
}
|
||||
}
|
||||
|
||||
int WaitForConnection( tracy::Worker& worker )
|
||||
{
|
||||
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 )
|
||||
{
|
||||
printf( "\nThe client you are trying to connect to has disconnected during the initial\nconnection handshake. Please check your network configuration.\n" );
|
||||
return 3;
|
||||
}
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PrintWorkerFailure( tracy::Worker& worker )
|
||||
{
|
||||
const auto& failure = worker.GetFailureType();
|
||||
if( failure == tracy::Worker::Failure::None ) return;
|
||||
|
||||
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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrintCaptureProgress( tracy::Worker& worker, int64_t firstTime, int64_t memoryLimit )
|
||||
{
|
||||
if( !IsTerminal() ) return;
|
||||
|
||||
auto& lock = worker.GetMbpsDataLock();
|
||||
lock.lock();
|
||||
const auto mbps = worker.GetMbpsData().back();
|
||||
const auto compRatio = worker.GetCompRatio();
|
||||
const auto netTotal = worker.GetDataTransferred();
|
||||
const auto queueSize = worker.GetSendQueueSize();
|
||||
lock.unlock();
|
||||
|
||||
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 ) );
|
||||
printf( " | " );
|
||||
AnsiPrintf( ANSI_RED ANSI_BOLD, "%s query backlog", tracy::RealToString( queueSize ) );
|
||||
fflush( stdout );
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#ifndef __CAPTUREOUTPUT_HPP__
|
||||
#define __CAPTUREOUTPUT_HPP__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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"
|
||||
|
||||
namespace tracy { class Worker; }
|
||||
|
||||
void InitTerminalDetection();
|
||||
bool IsTerminal();
|
||||
|
||||
#ifdef __GNUC__
|
||||
[[gnu::format( __printf__, 2, 3 )]]
|
||||
#endif
|
||||
void AnsiPrintf( const char* ansiEscape, const char* format, ... );
|
||||
|
||||
int WaitForConnection( tracy::Worker& worker );
|
||||
|
||||
void PrintWorkerFailure( tracy::Worker& worker );
|
||||
|
||||
void PrintCaptureProgress( tracy::Worker& worker, int64_t firstTime, int64_t memoryLimit );
|
||||
|
||||
#endif
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
#ifdef _WIN32
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <inttypes.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "../../server/TracyFileWrite.hpp"
|
||||
#include "../../server/TracyPrint.hpp"
|
||||
#include "../../server/TracySysUtil.hpp"
|
||||
#include "../../server/TracyWorker.hpp"
|
||||
#include "../../public/common/TracyVersion.hpp"
|
||||
#include "GitRef.hpp"
|
||||
|
||||
#include "CaptureOutput.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
# include "../../getopt/getopt.h"
|
||||
#endif
|
||||
|
||||
|
||||
// This atomic is written by a signal handler (SigInt). Traditionally that would
|
||||
// have had to be `volatile sig_atomic_t`, and annoyingly, `bool` was
|
||||
// technically not allowed there, even though in practice it would work.
|
||||
// The good thing with C++11 atomics is that we can use atomic<bool> instead
|
||||
// here and be on the actually supported path.
|
||||
static std::atomic<bool> s_disconnect { false };
|
||||
|
||||
void SigInt( int )
|
||||
{
|
||||
s_disconnect.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
[[noreturn]] void Usage()
|
||||
{
|
||||
printf( "tracy-capture %i.%i.%i / %s\n\n", tracy::Version::Major, tracy::Version::Minor, tracy::Version::Patch, tracy::GitRef );
|
||||
printf( "Usage: capture -o output.tracy [-a address] [-p port] [-f] [-s seconds] [-m memlimit]\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
int main( int argc, char** argv )
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if( !AttachConsole( ATTACH_PARENT_PROCESS ) )
|
||||
{
|
||||
AllocConsole();
|
||||
SetConsoleMode( GetStdHandle( STD_OUTPUT_HANDLE ), 0x07 );
|
||||
}
|
||||
#endif
|
||||
|
||||
InitTerminalDetection();
|
||||
|
||||
bool overwrite = false;
|
||||
const char* address = "127.0.0.1";
|
||||
const char* output = nullptr;
|
||||
int port = 8086;
|
||||
int seconds = -1;
|
||||
int64_t memoryLimit = -1;
|
||||
|
||||
int c;
|
||||
while( ( c = getopt( argc, argv, "a:o:p:fs:m:" ) ) != -1 )
|
||||
{
|
||||
switch( c )
|
||||
{
|
||||
case 'a':
|
||||
address = optarg;
|
||||
break;
|
||||
case 'o':
|
||||
output = optarg;
|
||||
break;
|
||||
case 'p':
|
||||
port = atoi( optarg );
|
||||
break;
|
||||
case 'f':
|
||||
overwrite = true;
|
||||
break;
|
||||
case 's':
|
||||
seconds = atoi(optarg);
|
||||
break;
|
||||
case 'm':
|
||||
memoryLimit = std::clamp( atoll( optarg ), 1ll, 999ll ) * tracy::GetPhysicalMemorySize() / 100;
|
||||
break;
|
||||
default:
|
||||
Usage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( !address || !output ) Usage();
|
||||
|
||||
struct stat st;
|
||||
if( stat( output, &st ) == 0 && !overwrite )
|
||||
{
|
||||
printf( "Output file %s already exists! Use -f to force overwrite.\n", output );
|
||||
return 4;
|
||||
}
|
||||
|
||||
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...", address, port );
|
||||
fflush( stdout );
|
||||
tracy::Worker worker( address, port, memoryLimit );
|
||||
int result = WaitForConnection( worker );
|
||||
if( result != 0 ) return result;
|
||||
printf( "\nTimer resolution: %s\n", tracy::TimeToString( worker.GetResolution() ) );
|
||||
|
||||
#ifdef _WIN32
|
||||
signal( SIGINT, SigInt );
|
||||
#else
|
||||
struct sigaction sigint, oldsigint;
|
||||
memset( &sigint, 0, sizeof( sigint ) );
|
||||
sigint.sa_handler = SigInt;
|
||||
sigaction( SIGINT, &sigint, &oldsigint );
|
||||
#endif
|
||||
|
||||
const auto firstTime = worker.GetFirstTime();
|
||||
|
||||
const auto t0 = std::chrono::high_resolution_clock::now();
|
||||
while( worker.IsConnected() )
|
||||
{
|
||||
if( s_disconnect.load( std::memory_order_relaxed ) )
|
||||
{
|
||||
worker.Disconnect();
|
||||
s_disconnect.store(false, std::memory_order_relaxed );
|
||||
break;
|
||||
}
|
||||
|
||||
PrintCaptureProgress( worker, firstTime, memoryLimit );
|
||||
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
|
||||
if( seconds != -1 )
|
||||
{
|
||||
const auto dur = std::chrono::high_resolution_clock::now() - t0;
|
||||
if( std::chrono::duration_cast<std::chrono::seconds>(dur).count() >= seconds )
|
||||
{
|
||||
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() - firstTime ), 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;
|
||||
}
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
#ifdef _WIN32
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <signal.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "../../getopt/getopt.h"
|
||||
#include "../../public/common/TracySocket.hpp"
|
||||
#include "../../public/common/TracyVersion.hpp"
|
||||
#include "../../server/TracyBroadcast.hpp"
|
||||
#include "../../server/TracyFileWrite.hpp"
|
||||
#include "../../server/TracyMemory.hpp"
|
||||
#include "../../server/TracyPrint.hpp"
|
||||
#include "../../server/TracySysUtil.hpp"
|
||||
#include "../../server/TracyWorker.hpp"
|
||||
#include "GitRef.hpp"
|
||||
|
||||
#include "CaptureOutput.hpp"
|
||||
|
||||
static std::atomic<bool> g_shutdown{false};
|
||||
static std::mutex g_clientsMutex;
|
||||
static uint16_t g_listenPort = 8086;
|
||||
static std::string g_filterName;
|
||||
static int g_filterPort = 0;
|
||||
static int64_t g_memoryLimit = -1;
|
||||
|
||||
void SigInt( int )
|
||||
{
|
||||
g_shutdown.store( true, std::memory_order_relaxed );
|
||||
}
|
||||
|
||||
struct ClientStats
|
||||
{
|
||||
std::atomic<float> mbps{0};
|
||||
std::atomic<int64_t> txBytes{0};
|
||||
std::atomic<int64_t> memUsage{0};
|
||||
std::atomic<int64_t> firstTime{-1};
|
||||
};
|
||||
|
||||
struct ClientSession
|
||||
{
|
||||
std::string id;
|
||||
std::string programName;
|
||||
std::string address;
|
||||
uint16_t port;
|
||||
std::string outputFile;
|
||||
std::thread thread;
|
||||
std::atomic<bool> active{true};
|
||||
std::atomic<bool> finished{false};
|
||||
ClientStats stats;
|
||||
std::atomic<uint64_t> fileSize{0};
|
||||
};
|
||||
|
||||
static std::map<std::string, ClientSession*> g_clients;
|
||||
static std::unordered_set<std::string> g_outputFiles;
|
||||
|
||||
[[noreturn]] void Usage()
|
||||
{
|
||||
printf( "tracy-capture-daemon %i.%i.%i / %s\n\n", tracy::Version::Major, tracy::Version::Minor, tracy::Version::Patch, tracy::GitRef );
|
||||
printf( "Usage: tracy-capture-daemon -o <output_dir> [options]\n\n" );
|
||||
printf( "Options:\n" );
|
||||
printf( " -o, --output <dir> Output directory (required)\n" );
|
||||
printf( " -p, --port <port> UDP listen port (default: 8086)\n" );
|
||||
printf( " -m, --memory <limit> Memory limit per client as %% of system RAM\n" );
|
||||
printf( " --filter-name <pattern> Only capture clients matching program name\n" );
|
||||
printf( " --filter-port <port> Only capture clients with specific data port\n" );
|
||||
printf( " -h, --help Show this help\n" );
|
||||
printf( " -V, --version Show version information\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
std::string SanitizeName( const std::string& name )
|
||||
{
|
||||
std::string result;
|
||||
for( char c : name )
|
||||
{
|
||||
if( ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || ( c >= '0' && c <= '9' ) || c == '_' || c == '-' )
|
||||
{
|
||||
result += c;
|
||||
}
|
||||
else if( c == ' ' || c == '\t' )
|
||||
{
|
||||
result += '_';
|
||||
}
|
||||
}
|
||||
if( result.empty() ) result = "unknown";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string GenerateOutputFilename( const std::string& outputDir, const std::string& programName, const std::string& address, uint16_t port )
|
||||
{
|
||||
std::string base = SanitizeName( programName ) + "_" + address + "_" + std::to_string( port );
|
||||
std::string candidate = base + ".tracy";
|
||||
std::string path = outputDir + "/" + candidate;
|
||||
|
||||
int idx = 0;
|
||||
while( g_outputFiles.count( path ) || std::filesystem::exists( path ) )
|
||||
{
|
||||
idx++;
|
||||
candidate = base + "_" + std::to_string( idx ) + ".tracy";
|
||||
path = outputDir + "/" + candidate;
|
||||
}
|
||||
|
||||
g_outputFiles.insert( path );
|
||||
return path;
|
||||
}
|
||||
|
||||
bool MatchesFilters( const tracy::BroadcastMessage& msg )
|
||||
{
|
||||
if( !g_filterName.empty() )
|
||||
{
|
||||
if( strstr( msg.programName, g_filterName.c_str() ) == nullptr )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if( g_filterPort > 0 && msg.listenPort != g_filterPort )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CaptureThread( ClientSession* session, const std::string& address, uint16_t port, int64_t memoryLimit, const std::string& outputFile )
|
||||
{
|
||||
printf( "Connecting to %s:%u...\n", address.c_str(), port );
|
||||
fflush( stdout );
|
||||
|
||||
tracy::Worker worker( address.c_str(), port, memoryLimit );
|
||||
|
||||
int result = WaitForConnection( worker );
|
||||
if( result != 0 )
|
||||
{
|
||||
session->active = false;
|
||||
session->finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
printf( "Connected to %s (%s:%u)\n", session->programName.c_str(), address.c_str(), port );
|
||||
|
||||
int64_t firstTime = worker.GetFirstTime();
|
||||
session->stats.firstTime = firstTime;
|
||||
|
||||
while( session->active && worker.IsConnected() )
|
||||
{
|
||||
auto& lock = worker.GetMbpsDataLock();
|
||||
lock.lock();
|
||||
float mbps = worker.GetMbpsData().back();
|
||||
int64_t txTotal = worker.GetDataTransferred();
|
||||
lock.unlock();
|
||||
|
||||
session->stats.mbps = mbps;
|
||||
session->stats.txBytes = txTotal;
|
||||
session->stats.memUsage = tracy::memUsage.load( std::memory_order_relaxed );
|
||||
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
|
||||
}
|
||||
|
||||
printf( "\nSaving %s...", outputFile.c_str() );
|
||||
fflush( stdout );
|
||||
|
||||
auto file = std::unique_ptr<tracy::FileWrite>( tracy::FileWrite::Open( outputFile.c_str(), tracy::FileCompression::Zstd, 3, 4 ) );
|
||||
if( file )
|
||||
{
|
||||
worker.Write( *file, false );
|
||||
file->Finish();
|
||||
auto stats = file->GetCompressionStatistics();
|
||||
session->fileSize = stats.second;
|
||||
AnsiPrintf( ANSI_GREEN ANSI_BOLD, " done!\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiPrintf( ANSI_RED ANSI_BOLD, " failed!\n" );
|
||||
}
|
||||
|
||||
session->finished = true;
|
||||
session->active = false;
|
||||
}
|
||||
|
||||
void RefreshDisplay( const std::string& listenAddr )
|
||||
{
|
||||
if( !IsTerminal() ) return;
|
||||
|
||||
printf( "\033[H\033[J" );
|
||||
|
||||
size_t clientCount = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
clientCount = g_clients.size();
|
||||
}
|
||||
|
||||
printf( "[%zu client%s] Listening on %s:%u... Press Ctrl+C to stop\n\n", clientCount, clientCount == 1 ? "" : "s", listenAddr.c_str(), g_listenPort );
|
||||
|
||||
int idx = 1;
|
||||
float totalMbps = 0;
|
||||
int64_t totalTx = 0;
|
||||
int64_t totalMem = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
for( auto& [id, session] : g_clients )
|
||||
{
|
||||
printf( " [%d] %s @ %s:%u ", idx, session->programName.c_str(), session->address.c_str(), session->port );
|
||||
|
||||
if( session->finished )
|
||||
{
|
||||
printf( "finished (" );
|
||||
printf( "%s", tracy::MemSizeToString( session->fileSize.load() ) );
|
||||
printf( ")" );
|
||||
}
|
||||
else if( session->active )
|
||||
{
|
||||
float mbps = session->stats.mbps.load();
|
||||
int64_t tx = session->stats.txBytes.load();
|
||||
int64_t mem = session->stats.memUsage.load();
|
||||
int64_t firstTime = session->stats.firstTime.load();
|
||||
|
||||
printf( "%.1f Mbps | %s | %s", mbps, tracy::MemSizeToString( tx ), tracy::MemSizeToString( mem ) );
|
||||
|
||||
totalMbps += mbps;
|
||||
totalTx += tx;
|
||||
totalMem += mem;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf( "connecting..." );
|
||||
}
|
||||
printf( "\n" );
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
printf( "\nTotal: %.1f Mbps | %s | Mem: %s", totalMbps, tracy::MemSizeToString( totalTx ), tracy::MemSizeToString( totalMem ) );
|
||||
fflush( stdout );
|
||||
}
|
||||
|
||||
void PrintSummary()
|
||||
{
|
||||
printf( "\n\n=== Capture Summary ===\n" );
|
||||
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
int idx = 1;
|
||||
int64_t totalSize = 0;
|
||||
|
||||
for( auto& [id, session] : g_clients )
|
||||
{
|
||||
int64_t size = session->fileSize.load();
|
||||
totalSize += size;
|
||||
printf( " [%d] %s @ %s:%u -> %s (%s)\n", idx++, session->programName.c_str(), session->address.c_str(), session->port, session->outputFile.c_str(), tracy::MemSizeToString( size ) );
|
||||
}
|
||||
|
||||
printf( "\nTotal: %zu files, %s\n", g_clients.size(), tracy::MemSizeToString( totalSize ) );
|
||||
}
|
||||
|
||||
int main( int argc, char** argv )
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if( !AttachConsole( ATTACH_PARENT_PROCESS ) )
|
||||
{
|
||||
AllocConsole();
|
||||
SetConsoleMode( GetStdHandle( STD_OUTPUT_HANDLE ), 0x07 );
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string outputDir;
|
||||
|
||||
static struct option longOptions[] = {
|
||||
{ "output", required_argument, nullptr, 'o' },
|
||||
{ "port", required_argument, nullptr, 'p' },
|
||||
{ "memory", required_argument, nullptr, 'm' },
|
||||
{ "filter-name", required_argument, nullptr, 1 },
|
||||
{ "filter-port", required_argument, nullptr, 2 },
|
||||
{ "help", no_argument, nullptr, 'h' },
|
||||
{ "version", no_argument, nullptr, 'V' },
|
||||
{ nullptr, 0, nullptr, 0 }
|
||||
};
|
||||
|
||||
int c;
|
||||
while( ( c = getopt_long( argc, argv, "o:p:m:hV", longOptions, nullptr ) ) != -1 )
|
||||
{
|
||||
switch( c )
|
||||
{
|
||||
case 'o':
|
||||
outputDir = optarg;
|
||||
break;
|
||||
case 'p':
|
||||
g_listenPort = atoi( optarg );
|
||||
break;
|
||||
case 'm':
|
||||
g_memoryLimit = std::clamp( atoll( optarg ), 1ll, 999ll ) * tracy::GetPhysicalMemorySize() / 100;
|
||||
break;
|
||||
case 1:
|
||||
g_filterName = optarg;
|
||||
break;
|
||||
case 2:
|
||||
g_filterPort = atoi( optarg );
|
||||
break;
|
||||
case 'h':
|
||||
Usage();
|
||||
break;
|
||||
case 'V':
|
||||
printf( "tracy-capture-daemon %i.%i.%i / %s\n", tracy::Version::Major, tracy::Version::Minor, tracy::Version::Patch, tracy::GitRef );
|
||||
exit( 0 );
|
||||
default:
|
||||
Usage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( outputDir.empty() )
|
||||
{
|
||||
fprintf( stderr, "Error: Output directory is required (-o)\n\n" );
|
||||
Usage();
|
||||
}
|
||||
|
||||
std::filesystem::create_directories( outputDir );
|
||||
|
||||
InitTerminalDetection();
|
||||
|
||||
#ifdef _WIN32
|
||||
signal( SIGINT, SigInt );
|
||||
#else
|
||||
struct sigaction sigint, oldsigint;
|
||||
memset( &sigint, 0, sizeof( sigint ) );
|
||||
sigint.sa_handler = SigInt;
|
||||
sigaction( SIGINT, &sigint, &oldsigint );
|
||||
#endif
|
||||
|
||||
tracy::UdpListen udpSocket;
|
||||
if( !udpSocket.Listen( g_listenPort ) )
|
||||
{
|
||||
fprintf( stderr, "Error: Failed to listen on port %u\n", g_listenPort );
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf( "Listening on 0.0.0.0:%u... Press Ctrl+C to stop\n", g_listenPort );
|
||||
printf( "Output directory: %s\n", outputDir.c_str() );
|
||||
|
||||
const std::string listenAddr = "0.0.0.0";
|
||||
auto lastDisplay = std::chrono::steady_clock::now();
|
||||
|
||||
while( !g_shutdown )
|
||||
{
|
||||
tracy::IpAddress clientAddr;
|
||||
size_t len;
|
||||
const char* msg = udpSocket.Read( len, clientAddr, 100 );
|
||||
|
||||
if( msg )
|
||||
{
|
||||
auto parsed = tracy::ParseBroadcastMessage( msg, len );
|
||||
if( parsed )
|
||||
{
|
||||
std::string clientId = std::to_string( parsed->pid ) + "_" + clientAddr.GetText() + "_" + std::to_string( parsed->listenPort );
|
||||
|
||||
bool isNew = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
isNew = g_clients.find( clientId ) == g_clients.end();
|
||||
}
|
||||
|
||||
if( isNew && MatchesFilters( *parsed ) )
|
||||
{
|
||||
std::string addressStr = clientAddr.GetText();
|
||||
std::string outputFile = GenerateOutputFilename( outputDir, parsed->programName, addressStr, parsed->listenPort );
|
||||
|
||||
auto session = new ClientSession();
|
||||
session->id = clientId;
|
||||
session->programName = parsed->programName;
|
||||
session->address = addressStr;
|
||||
session->port = parsed->listenPort;
|
||||
session->outputFile = outputFile;
|
||||
session->active = true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
g_clients[clientId] = session;
|
||||
}
|
||||
|
||||
session->thread = std::thread( CaptureThread, session, addressStr, parsed->listenPort, g_memoryLimit, outputFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if( std::chrono::duration_cast<std::chrono::milliseconds>( now - lastDisplay ).count() >= 100 )
|
||||
{
|
||||
RefreshDisplay( listenAddr );
|
||||
lastDisplay = now;
|
||||
}
|
||||
}
|
||||
|
||||
printf( "\n\nShutting down... waiting for %zu client(s) to finish\n", g_clients.size() );
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
for( auto& [id, session] : g_clients )
|
||||
{
|
||||
session->active = false;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
for( auto& [id, session] : g_clients )
|
||||
{
|
||||
if( session->thread.joinable() )
|
||||
{
|
||||
session->thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrintSummary();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( g_clientsMutex );
|
||||
for( auto& [id, session] : g_clients )
|
||||
{
|
||||
delete session;
|
||||
}
|
||||
g_clients.clear();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user