Merge pull request #1387 from Lectem/wip/offline-res-for-any-toolchain

Offline resolution for any toolchain
This commit is contained in:
Bartosz Taudul
2026-06-06 14:48:54 +02:00
committed by GitHub
6 changed files with 202 additions and 29 deletions

View File

@@ -2041,6 +2041,20 @@ filesystem setup as the one used to run the tracy instrumented application).
You can do path substitution with the \texttt{-p} option to perform any number of path
substitions in order to use symbols located elsewhere.
By default symbol resolution is performed with the platform's native facility: the DbgHelp
library on Windows, and the \texttt{addr2line} tool found in \texttt{PATH} elsewhere. You can
override this with the \texttt{-a} option, passing the path to a custom
\texttt{addr2line}-compatible tool (for instance an \texttt{addr2line} from a cross-compilation
toolchain, or \texttt{llvm-addr2line}). The \texttt{-a} option works on all platforms, including
Windows, and takes precedence over the platform default.
Extra arguments can be passed verbatim to the resolution tool with the \texttt{-A} option. Tracy
records callstack frame offsets relative to the image base, but \texttt{addr2line}-compatible
tools expect a full virtual address for images that have a non-zero preferred image base (such as
PE on Windows or Mach-O on Apple). For these, pass \texttt{-A "--relative-address"} so that
\texttt{llvm-addr2line} or \texttt{llvm-symbolizer} adds the image base back. ELF images need no
such adjustment.
\begin{bclogo}[
noborder=true,
couleur=black!5,

View File

@@ -11,6 +11,22 @@
#include "OfflineSymbolResolver.h"
bool ResolveSymbols( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
#ifdef _WIN32
// On Windows the default (no custom tool given) is the DbgHelp backend.
if( addr2lineToolPath.empty() )
{
return ResolveSymbolsDbgHelp( imagePath, inputEntryList, resolvedEntries );
}
#endif
// Everywhere else, and whenever a custom tool is given, use the addr2line-compatible backend.
// An empty path lets that backend fall back to the 'addr2line' found in PATH.
return ResolveSymbolsAddr2Line( addr2lineToolPath, addr2lineArgs, imagePath, inputEntryList, resolvedEntries );
}
bool ApplyPathSubstitutions( std::string& path, const PathSubstitutionList& pathSubstitutionlist )
{
for( const auto& substitution : pathSubstitutionlist )
@@ -31,7 +47,35 @@ tracy::StringIdx AddSymbolString( tracy::Worker& worker, const std::string& str
return tracy::StringIdx( location.idx );
}
bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstitutionlist, bool verbose )
void ResetSymbols( tracy::Worker& worker )
{
std::cout << "Resetting callstack frame symbols to the unresolved state..." << std::endl;
const tracy::StringIdx unresolvedName = AddSymbolString( worker, "[unresolved]" );
const tracy::StringIdx unknownFile = AddSymbolString( worker, "[unknown]" );
uint64_t frameCount = 0;
auto& callstackFrameMap = worker.GetCallstackFrameMap();
for( auto it = callstackFrameMap.begin(); it != callstackFrameMap.end(); ++it )
{
if( !it->second ) continue;
tracy::CallstackFrameData& frameData = *it->second;
for( uint8_t f = 0; f < frameData.size; f++ )
{
tracy::CallstackFrame& frame = frameData.data[f];
frame.name = unresolvedName;
frame.file = unknownFile;
frame.line = 0;
++frameCount;
}
}
std::cout << "Reset " << frameCount << " callstack frames." << std::endl;
}
bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstitutionlist,
const std::string& addr2lineToolPath, const std::string& addr2lineArgs, bool verbose )
{
uint64_t callstackFrameCount = worker.GetCallstackFrameCount();
std::string relativeSoNameMatch = "[unresolved]";
@@ -91,7 +135,7 @@ bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& p
}
SymbolEntryList resolvedEntries;
ResolveSymbols( imagePath, entries, resolvedEntries );
ResolveSymbols( addr2lineToolPath, addr2lineArgs, imagePath, entries, resolvedEntries );
if( resolvedEntries.size() != entries.size() )
{
@@ -131,7 +175,8 @@ bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& p
return true;
}
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings, bool verbose )
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings,
const std::string& addr2lineToolPath, const std::string& addr2lineArgs, bool verbose )
{
std::cout << "Resolving and patching symbols..." << std::endl;
@@ -160,7 +205,7 @@ void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSu
}
}
if ( !PatchSymbolsWithRegex(worker, pathSubstitutionList, verbose) )
if ( !PatchSymbolsWithRegex(worker, pathSubstitutionList, addr2lineToolPath, addr2lineArgs, verbose) )
{
std::cerr << "Failed to patch symbols" << std::endl;
}

View File

@@ -29,12 +29,41 @@ struct SymbolEntry
using SymbolEntryList = std::vector<SymbolEntry>;
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
// Dispatches to the appropriate backend depending on the platform and whether a custom
// addr2line-compatible tool was specified. When addr2lineToolPath is non-empty, the tool at
// that path is invoked (on any platform); otherwise the platform default is used (DbgHelp on
// Windows, the 'addr2line' found in PATH elsewhere). addr2lineArgs are extra arguments passed
// verbatim to the addr2line-compatible tool (e.g. "--relative-address").
bool ResolveSymbols( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries );
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings, bool verbose = false );
// Backend invoking an addr2line-compatible tool. Available on all platforms. An empty
// addr2lineToolPath falls back to the 'addr2line' found in PATH. addr2lineArgs are inserted
// verbatim into the tool's command line.
bool ResolveSymbolsAddr2Line( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries );
#ifdef _WIN32
// Backend using the Windows DbgHelp library.
bool ResolveSymbolsDbgHelp( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries );
#endif
// Resets all callstack frame symbols back to the unresolved state ("[unresolved]" / "[unknown]"),
// so a subsequent PatchSymbols pass re-resolves every frame. This is useful to chain several
// resolution passes with different path substitutions. Only meaningful for traces captured with
// TRACY_SYMBOL_OFFLINE_RESOLVE, where each frame's symAddr holds the image-relative offset.
void ResetSymbols( tracy::Worker& worker );
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings,
const std::string& addr2lineToolPath = std::string(),
const std::string& addr2lineArgs = std::string(), bool verbose = false );
using PathSubstitutionList = std::vector<std::pair<std::regex, std::string> >;
bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstituionlist, bool verbose = false );
bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstituionlist,
const std::string& addr2lineToolPath = std::string(),
const std::string& addr2lineArgs = std::string(), bool verbose = false );
#endif // __SYMBOLRESOLVER_HPP__

View File

@@ -1,5 +1,3 @@
#ifndef _WIN32
#include "OfflineSymbolResolver.h"
#include <fstream>
@@ -10,6 +8,11 @@
#include <memory>
#include <stdio.h>
#ifdef _WIN32
# define popen _popen
# define pclose _pclose
#endif
std::string ExecShellCommand( const char* cmd )
{
std::array<char, 128> buffer;
@@ -29,23 +32,66 @@ std::string ExecShellCommand( const char* cmd )
class SymbolResolver
{
public:
SymbolResolver()
SymbolResolver( const std::string& addr2lineToolPath, const std::string& addr2lineArgs )
{
// Extra arguments are inserted verbatim into the tool invocation. Tracy records frame
// offsets as RVAs; for images with a non-zero preferred image base (PE, Mach-O) the user
// can pass "--relative-address" here so llvm-addr2line / llvm-symbolizer add the base back.
if( !addr2lineArgs.empty() )
{
m_addr2LineArgs = " " + addr2lineArgs;
}
if( !addr2lineToolPath.empty() )
{
// If the value looks like a path (not a bare command name resolved via PATH), verify
// it exists so a wrong path fails with an actionable error instead of a cryptic shell one.
const bool looksLikePath = addr2lineToolPath.find( '/' ) != std::string::npos ||
addr2lineToolPath.find( '\\' ) != std::string::npos;
if( looksLikePath && !std::ifstream( addr2lineToolPath ).good() )
{
std::cerr << "Specified symbol resolution tool not found: '" << addr2lineToolPath
<< "' (check the path passed to the '-a' option)" << std::endl;
return;
}
// A user-provided path may contain spaces or other shell-special characters.
escapeShellParam( addr2lineToolPath, m_addr2LinePath );
std::cout << "Using user-specified symbol resolution tool: '" << addr2lineToolPath.c_str() << "'" << std::endl;
return;
}
#ifdef _WIN32
std::cerr << "No symbol resolution tool specified (use the '-a' option to provide one)" << std::endl;
#else
std::stringstream result( ExecShellCommand("which addr2line") );
std::getline(result, m_addr2LinePath);
if( !m_addr2LinePath.length() )
{
std::cerr << "'addr2line' was not found in the system, please installed it" << std::endl;
std::cerr << "'addr2line' was not found in the system, please install it" << std::endl;
}
else
{
std::cout << "Using 'addr2line' found at: '" << m_addr2LinePath.c_str() << "'" << std::endl;
}
#endif
}
static void escapeShellParam(std::string const& s, std::string& out)
{
#ifdef _WIN32
// cmd.exe / the CRT command parser do not understand POSIX backslash escapes, and
// backslashes are path separators on Windows. Wrap the parameter in double quotes
// (which handles spaces) and drop any embedded quotes, which cannot appear in a path.
out.reserve( s.size() + 2 );
out.push_back( '"' );
for( char c : s )
{
if( c != '"' ) out.push_back( c );
}
out.push_back( '"' );
#else
out.reserve( s.size() + 2 );
out.push_back( '"' );
for( unsigned char c : s )
@@ -73,34 +119,51 @@ public:
}
}
out.push_back( '"' );
#endif
}
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
if( !m_addr2LinePath.length() ) return false;
std:: string escapedPath;
escapeShellParam( imagePath, escapedPath );
// Command-line length limits: cmd.exe (used by _popen on Windows) allows ~8191 characters;
// a single POSIX 'sh -c' argument is capped by MAX_ARG_STRLEN (128 KiB on Linux).
// 8000 stays under all of these, so a single conservative budget works on every platform.
const size_t maxCmdLength = 8000;
size_t entryIdx = 0;
while( entryIdx < inputEntryList.size() )
{
const size_t startIdx = entryIdx;
const size_t batchEndIdx = std::min( inputEntryList.size(), startIdx + (size_t)1024 );
printf( "Resolving symbols [%zu-%zu]\n", startIdx, batchEndIdx );
// generate a single addr2line cmd line for all addresses in one invocation
// generate a single addr2line cmd line for as many addresses as fit the length budget
std::stringstream ss;
ss << m_addr2LinePath << " -C -f -e " << escapedPath << " -a ";
for( ; entryIdx < batchEndIdx; entryIdx++ )
ss << m_addr2LinePath << " -C -f" << m_addr2LineArgs << " -e " << escapedPath << " -a ";
while( entryIdx < inputEntryList.size() )
{
const FrameEntry& entry = inputEntryList[entryIdx];
ss << " 0x" << std::hex << entry.symbolOffset;
entryIdx++;
// always include at least one address, then stop once near the length limit
if( static_cast<size_t>( ss.tellp() ) >= maxCmdLength ) break;
}
const size_t batchEndIdx = entryIdx;
std::string resultStr = ExecShellCommand( ss.str().c_str() );
printf( "Resolving symbols [%zu-%zu]\n", startIdx, batchEndIdx );
std::string cmd = ss.str();
#ifdef _WIN32
// _popen runs the command through 'cmd.exe /c', which strips the outermost pair of
// quotes. Wrap the whole command so the quoting around the (possibly spaced) tool
// and image paths survives.
cmd = "\"" + cmd + "\"";
#endif
std::string resultStr = ExecShellCommand( cmd.c_str() );
std::stringstream result( resultStr );
//printf("executing: '%s' got '%s'\n", ss.str().c_str(), result.str().c_str());
@@ -147,13 +210,13 @@ public:
private:
std::string m_addr2LinePath;
std::string m_addr2LineArgs;
};
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
bool ResolveSymbolsAddr2Line( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
static SymbolResolver symbolResolver;
static SymbolResolver symbolResolver( addr2lineToolPath, addr2lineArgs );
return symbolResolver.ResolveSymbols( imagePath, inputEntryList, resolvedEntries );
}
#endif // #ifndef _WIN32

View File

@@ -122,8 +122,8 @@ private:
char SymbolResolver::s_symbolResolutionBuffer[symbolResolutionBufferSize];
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
bool ResolveSymbolsDbgHelp( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
static SymbolResolver resolver;
return resolver.ResolveSymbolsForModule( imagePath, inputEntryList, resolvedEntries );

View File

@@ -38,7 +38,12 @@ void Usage()
printf( " c: context switches, s: sampling data, C: symbol code, S: source cache\n" );
printf( " -c: scan for source files missing in cache and add if found\n" );
printf( " -r: resolve symbols and patch callstack frames\n");
printf( " -R: reset all callstack frame symbols to unresolved (e.g. to re-run resolution)\n");
printf( " -p: substitute symbol resolution path with an alternative: \"REGEX_MATCH;REPLACEMENT\"\n");
printf( " -a: path to a custom addr2line-compatible tool to use for symbol resolution\n");
printf( " -A: extra arguments passed verbatim to the symbol resolution tool,\n");
printf( " e.g. \"--relative-address\" for llvm-addr2line on PE/Mach-O images\n");
printf( " -v: verbose output while resolving symbols\n");
printf( " -j: number of threads to use for compression (-1 to use all cores)\n" );
exit( 1 );
@@ -61,10 +66,14 @@ int main( int argc, char** argv )
bool buildDict = false;
bool cacheSource = false;
bool resolveSymbols = false;
bool resetSymbols = false;
std::vector<std::string> pathSubstitutions;
std::string addr2lineToolPath;
std::string addr2lineArgs;
bool verboseSymbols = false;
int c;
while( ( c = getopt( argc, argv, "4hez:ds:crp:j:" ) ) != -1 )
while( ( c = getopt( argc, argv, "4hez:ds:crRp:a:A:vj:" ) ) != -1 )
{
switch( c )
{
@@ -137,9 +146,21 @@ int main( int argc, char** argv )
case 'r':
resolveSymbols = true;
break;
case 'R':
resetSymbols = true;
break;
case 'p':
pathSubstitutions.push_back(optarg);
break;
case 'a':
addr2lineToolPath = optarg;
break;
case 'A':
addr2lineArgs = optarg;
break;
case 'v':
verboseSymbols = true;
break;
case 'j':
streams = atoi( optarg );
break;
@@ -171,7 +192,7 @@ int main( int argc, char** argv )
{
const auto t0 = std::chrono::high_resolution_clock::now();
const bool allowBgThreads = false;
const bool allowStringModification = resolveSymbols;
const bool allowStringModification = resolveSymbols || resetSymbols;
tracy::Worker worker( *f, (tracy::EventType::Type)events, allowBgThreads, allowStringModification );
#ifndef TRACY_NO_STATISTICS
@@ -181,7 +202,8 @@ int main( int argc, char** argv )
const auto t1 = std::chrono::high_resolution_clock::now();
if( cacheSource ) worker.CacheSourceFiles();
if( resolveSymbols ) PatchSymbols( worker, pathSubstitutions );
if( resetSymbols ) ResetSymbols( worker );
if( resolveSymbols ) PatchSymbols( worker, pathSubstitutions, addr2lineToolPath, addr2lineArgs, verboseSymbols );
auto w = std::unique_ptr<tracy::FileWrite>( tracy::FileWrite::Open( output, clev, zstdLevel, streams ) );
if( !w )