Compare commits

..

19 Commits

Author SHA1 Message Date
Bartosz Taudul
8b86ada40e Fix checks. 2026-06-17 01:31:56 +02:00
Bartosz Taudul
616f33ff65 Consistent wait.time checks. 2026-06-17 01:31:01 +02:00
Bartosz Taudul
023cb20ba9 Only display wait reason or state if present. 2026-06-17 00:33:00 +02:00
Bartosz Taudul
073cd266ac Use proper thread id for wait stacks.
ContextSwitchData::Thread() is something related to fibers. Pass and use
known thread id from external context instead.
2026-06-17 00:21:51 +02:00
Bartosz Taudul
2e252d3988 Fix indentation level. 2026-06-17 00:14:03 +02:00
Bartosz Taudul
77978b68ca Explain wait stacks in callstack skill. 2026-06-16 23:53:41 +02:00
Bartosz Taudul
d9939362f5 Add wait stack data to the LLM attachment. 2026-06-16 23:53:40 +02:00
Bartosz Taudul
bba05bd3ad Update manual. 2026-06-16 23:33:16 +02:00
Bartosz Taudul
83a4a13cbd Display wait stack label and tooltip in callstack window. 2026-06-16 23:28:24 +02:00
Bartosz Taudul
9e9aabe9a1 Push CallstackViewWait to View::DrawCallstackTable(). 2026-06-16 23:18:40 +02:00
Bartosz Taudul
382a887ce9 Provide default values for View::DrawCallstackTable() parameters. 2026-06-16 23:17:47 +02:00
Bartosz Taudul
fbd1c55151 Extend View::ViewCallstack() to support optional wait stack data. 2026-06-16 23:10:43 +02:00
Bartosz Taudul
398bab8041 Store wait stack data in CallstackView. 2026-06-16 23:05:59 +02:00
Bartosz Taudul
69245e751b Cosmetics. 2026-06-16 23:05:58 +02:00
Bartosz Taudul
d571f2bd59 Fix integer trace parameter input field size. 2026-06-16 19:17:18 +02:00
Bartosz Taudul
781938317d Bump pugixml to 1.16. 2026-06-16 18:10:38 +02:00
Bartosz Taudul
f9365abe4f Do not use samplers in renderer.
Tracy requires some textures to have repeat wrapping mode set. The ImGui
implementation of samplers doesn't make it easy to achieve. Disalbe use
of samplers and rely on texture flags, as done originally.
2026-06-16 16:02:08 +02:00
Bartosz Taudul
3f91f35d59 Merge pull request #1403 from wolfpld/slomp/gl-feature-check
Add routine to check for GL features/extensions at run-time
2026-06-16 14:50:30 +02:00
Marcos Slomp
1de94aa856 add routine to check for GL features/extensions at run-time 2026-06-15 21:19:12 -07:00
16 changed files with 164 additions and 524 deletions

View File

@@ -0,0 +1,13 @@
diff --git a/backends/imgui_impl_opengl3.cpp b/backends/imgui_impl_opengl3.cpp
index a9e32b7ac..2cdbc4812 100644
--- a/backends/imgui_impl_opengl3.cpp
+++ b/backends/imgui_impl_opengl3.cpp
@@ -1069,7 +1069,7 @@ bool ImGui_ImplOpenGL3_Init(const char* glsl_version)
bd->HasPolygonMode = (!bd->GlProfileIsES2 && !bd->GlProfileIsES3);
#endif
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER
- bd->HasBindSampler = (bd->GlVersion >= 330 || bd->GlProfileIsES3);
+ //bd->HasBindSampler = (bd->GlVersion >= 330 || bd->GlProfileIsES3);
#endif
bd->HasClipOrigin = (bd->GlVersion >= 450);
#ifdef IMGUI_IMPL_OPENGL_HAS_EXTENSIONS

View File

@@ -142,6 +142,7 @@ CPMAddPackage(
PATCHES
"${CMAKE_CURRENT_LIST_DIR}/imgui-emscripten.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-loader.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-no-samplers.patch"
)
set(IMGUI_SOURCES
@@ -271,7 +272,7 @@ if(NOT EMSCRIPTEN)
CPMAddPackage(
NAME pugixml
GITHUB_REPOSITORY zeux/pugixml
GIT_TAG v1.15
GIT_TAG v1.16
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyPugixml INTERFACE)

View File

@@ -11,7 +11,7 @@ The user manual
**Bartosz Taudul** [\<wolf@nereid.pl\>](mailto:wolf@nereid.pl)
2026-06-15 <https://github.com/wolfpld/tracy>
2026-06-16 <https://github.com/wolfpld/tracy>
# Quick overview {#quick-overview .unnumbered}
@@ -4301,6 +4301,8 @@ A single stack frame may have multiple function call places associated with it.
If the call stack shows a crash (see section [2.5](#crashhandling)), a red * Crash* label will be displayed. Clicking it will center the timeline on the crash. Note that the crash stack may contain OS or Tracy frames where the crash was intercepted and processed.
If the call stack shows a wait stack (see section [3.17.5.1](#waitstacks)), a blue * Wait stack* label will be displayed. Hovering the  mouse pointer over it will display a tooltip displaying how much time was spent waiting in the stack, what was the wait reason and status.
Stack frame location may be displayed in the following number of ways, depending on the *Frame at* option selection:
- *Source code* -- displays source file and line number associated with the frame.
@@ -4330,6 +4332,8 @@ Clicking on the * Summary* button will use Tracy Assist to generate a brief s
 - Caret Right icon
 - Skull icon
 - Hourglass Half icon
 - Arrow Pointer icon
 - Shield Halved icon
 - Scissors icon
 - Door Open icon

View File

@@ -4684,6 +4684,8 @@ A single stack frame may have multiple function call places associated with it.
If the call stack shows a crash (see section~\ref{crashhandling}), a red \emph{\faSkull{}~Crash} label will be displayed. Clicking it will center the timeline on the crash. Note that the crash stack may contain OS or Tracy frames where the crash was intercepted and processed.
If the call stack shows a wait stack (see section~\ref{waitstacks}), a blue \emph{\faHourglassHalf{}~Wait stack} label will be displayed. Hovering the \faArrowPointer{}~mouse pointer over it will display a tooltip displaying how much time was spent waiting in the stack, what was the wait reason and status.
Stack frame location may be displayed in the following number of ways, depending on the \emph{Frame~at} option selection:
\begin{itemize}

View File

@@ -103,6 +103,12 @@ Tracy Profiler can intercept crashes and report them to the user for analysis. T
Each frame in a call stack has an associated instruction pointer, `ip` the return address where the execution will return from the function a frame above. This address is somewhere in the symbol code. The start of the symbol is provided as the `baseAddr` value. This base address identifies the symbol and can be used in various symbol-related tool calls as the symbol address.
# Wait stacks
Some call stacks represent time spent waiting for something to happen. For example, the program may want to read something from the disk. In such cases, program execution will be paused, and the CPU will start running kernel code responsible for filesystem access, I/O routines, or just idling while waiting for a response from the hardware.
A wait stack is identified by the presence of the `wait_time` field, which shows how much time was spent waiting for execution to return to the program. Further information about the wait stack can be inferred from the optional fields `wait_reason` (with an explanation in `wait_reason_hint`) and `wait_state` (with an explanation in `wait_state_hint`).
# Inspecting call stacks
1. Focus on user's code. Ignore standard library boilerplate.

View File

@@ -1531,11 +1531,18 @@ void View::AddLlmQuery( const char* query )
#endif
}
void View::ViewCallstack( uint32_t callstack, uint32_t thread )
void View::ViewCallstack( uint32_t callstack, uint32_t thread, int64_t waitTime, const char* waitReason, const char* waitReasonCode, const char* waitState, const char* waitStateCode )
{
m_callstackView = {
.id = callstack,
.thread = thread
.thread = thread,
.wait = {
.time = waitTime,
.reason = waitReason,
.reasonCode = waitReasonCode,
.state = waitState,
.stateCode = waitStateCode
}
};
}

View File

@@ -189,7 +189,7 @@ public:
void AddLlmAttachment( const nlohmann::json& json );
void AddLlmQuery( const char* query );
void ViewCallstack( uint32_t callstack, uint32_t thread );
void ViewCallstack( uint32_t callstack, uint32_t thread, int64_t waitTime = 0, const char* waitReason = nullptr, const char* waitReasonCode = nullptr, const char* waitState = nullptr, const char* waitStateCode = nullptr );
nlohmann::json GetCallstackJson( const CallstackFrameId* data, size_t size ) const;
@@ -261,10 +261,20 @@ private:
uint32_t count;
};
struct CallstackViewWait
{
int64_t time;
const char* reason;
const char* reasonCode;
const char* state;
const char* stateCode;
};
struct CallstackView
{
uint32_t id;
uint64_t thread;
CallstackViewWait wait;
};
void InitTextEditor();
@@ -283,7 +293,7 @@ private:
void DrawSampleList( const TimelineContext& ctx, const std::vector<SamplesDraw>& drawList, const Vector<SampleData>& vec, int offset, uint64_t tid );
void DrawZoneList( const TimelineContext& ctx, const std::vector<TimelineDraw>& drawList, int offset, uint64_t tid, int maxDepth, double margin );
void DrawThreadCropper( const int depth, const uint64_t tid, const float xPos, const float yPos, const float ostep, const float cropperWidth, const bool hasCtxSwitches );
void DrawContextSwitchList( const TimelineContext& ctx, const std::vector<ContextSwitchDraw>& drawList, const Vector<ContextSwitchData>& ctxSwitch, int offset, int endOffset, bool isFiber );
void DrawContextSwitchList( const TimelineContext& ctx, const std::vector<ContextSwitchDraw>& drawList, const Vector<ContextSwitchData>& ctxSwitch, int offset, int endOffset, bool isFiber, uint64_t tid );
int DispatchGpuZoneLevel( const Vector<short_ptr<GpuEvent>>& vec, bool hover, double pxns, int64_t nspx, const ImVec2& wpos, int offset, int depth, uint64_t thread, float yMin, float yMax, int64_t begin, int drift );
template<typename Adapter, typename V>
int DrawGpuZoneLevel( const V& vec, bool hover, double pxns, int64_t nspx, const ImVec2& wpos, int offset, int depth, uint64_t thread, float yMin, float yMax, int64_t begin, int drift );
@@ -304,8 +314,8 @@ private:
void DrawAllocList();
void DrawCompare();
void DrawCallstackWindow();
void DrawCallstackTable( uint32_t callstack, uint64_t thread, bool globalEntriesButton, bool showThread );
void DrawCallstackTable( const CallstackFrameId* data, size_t size, uint64_t thread, bool globalEntriesButton, bool showThread, bool hasCrashed = false, int64_t callstack = -1 );
void DrawCallstackTable( uint32_t callstack, uint64_t thread = 0, const CallstackViewWait& wait = {}, bool globalEntriesButton = false, bool showThread = false );
void DrawCallstackTable( const CallstackFrameId* data, size_t size, uint64_t thread = 0, const CallstackViewWait& wait = {}, bool globalEntriesButton = false, bool showThread = false, bool hasCrashed = false, int64_t callstack = -1 );
void DrawMemoryAllocWindow();
void DrawInfo();
void DrawTextEditor();

View File

@@ -24,22 +24,22 @@ void View::DrawCallstackWindow()
ImGui::Begin( "Call stack", &show, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse );
if( !ImGui::GetCurrentWindowRead()->SkipItems )
{
DrawCallstackTable( m_callstackView.id, m_callstackView.thread, true, true );
DrawCallstackTable( m_callstackView.id, m_callstackView.thread, m_callstackView.wait, true, true );
}
ImGui::End();
if( !show ) m_callstackView = {};
}
void View::DrawCallstackTable( uint32_t callstack, uint64_t thread, bool globalEntriesButton, bool showThread )
void View::DrawCallstackTable( uint32_t callstack, uint64_t thread, const CallstackViewWait& wait, bool globalEntriesButton, bool showThread )
{
auto& crash = m_worker.GetCrashEvent();
const bool hasCrashed = crash.thread != 0 && crash.callstack == callstack;
auto& cs = m_worker.GetCallstack( callstack );
DrawCallstackTable( cs.data(), cs.size(), thread, globalEntriesButton, showThread, hasCrashed, callstack );
DrawCallstackTable( cs.data(), cs.size(), thread, wait, globalEntriesButton, showThread, hasCrashed, callstack );
}
void View::DrawCallstackTable( const CallstackFrameId* data, size_t size, uint64_t thread, bool globalEntriesButton, bool showThread, bool hasCrashed, int64_t callstack )
void View::DrawCallstackTable( const CallstackFrameId* data, size_t size, uint64_t thread, const CallstackViewWait& wait, bool globalEntriesButton, bool showThread, bool hasCrashed, int64_t callstack )
{
if( ClipboardButton() )
{
@@ -113,7 +113,7 @@ void View::DrawCallstackTable( const CallstackFrameId* data, size_t size, uint64
}
if( s_config.llm )
{
auto Attach = [this, data, size, hasCrashed, thread, callstack]() {
auto Attach = [this, data, size, wait, hasCrashed, thread, callstack]() {
auto json = GetCallstackJson( data, size );
if( hasCrashed )
{
@@ -131,6 +131,14 @@ void View::DrawCallstackTable( const CallstackFrameId* data, size_t size, uint64
json["thread_id"] = thread;
}
if( callstack >= 0 ) json["id"] = callstack;
if( wait.time != 0 )
{
json["wait_time"] = TimeToString( wait.time );
if( wait.reasonCode ) json["wait_reason"] = wait.reasonCode;
if( wait.reason ) json["wait_reason_hint"] = wait.reason;
if( wait.stateCode ) json["wait_state"] = wait.stateCode;
if( wait.state ) json["wait_state_hint"] = wait.state;
}
AddLlmAttachment( json );
};
@@ -201,6 +209,32 @@ void View::DrawCallstackTable( const CallstackFrameId* data, size_t size, uint64
}
}
if( wait.time != 0 )
{
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
TextColoredUnformatted( ImVec4( 0.6f, 0.6f, 1.f, 1.f ), ICON_FA_HOURGLASS_HALF " Wait stack" );
if( ImGui::IsItemHovered() )
{
ImGui::BeginTooltip();
TextFocused( "Time:", TimeToString( wait.time ) );
if( wait.reasonCode )
{
TextFocused( "Reason:", wait.reasonCode );
ImGui::SameLine();
TextDisabledUnformatted( wait.reason );
}
if( wait.stateCode )
{
TextFocused( "State:", wait.stateCode );
ImGui::SameLine();
TextDisabledUnformatted( wait.state );
}
ImGui::EndTooltip();
}
}
if( globalEntriesButton && m_worker.AreCallstackSamplesReady() )
{
auto frame = m_worker.GetCallstackFrame( *data );

View File

@@ -214,6 +214,7 @@ bool View::DrawConnection()
else
{
auto val = int( p.val );
ImGui::SetNextItemWidth( 100 * GetScale() );
if( ImGui::InputInt( "", &val, 1, 100, ImGuiInputTextFlags_EnterReturnsTrue ) )
{
m_worker.SetParameter( idx, int32_t( val ) );

View File

@@ -166,7 +166,7 @@ const char* View::DecodeContextSwitchState( uint8_t state )
}
}
void View::DrawContextSwitchList( const TimelineContext& ctx, const std::vector<ContextSwitchDraw>& drawList, const Vector<ContextSwitchData>& ctxSwitch, int offset, int endOffset, bool isFiber )
void View::DrawContextSwitchList( const TimelineContext& ctx, const std::vector<ContextSwitchDraw>& drawList, const Vector<ContextSwitchData>& ctxSwitch, int offset, int endOffset, bool isFiber, uint64_t tid )
{
constexpr float MinCtxSize = 4;
@@ -210,6 +210,12 @@ void View::DrawContextSwitchList( const TimelineContext& ctx, const std::vector<
if( hover )
{
int64_t waitTime = 0;
const char* waitReason = nullptr;
const char* waitReasonCode = nullptr;
const char* waitState = nullptr;
const char* waitStateCode = nullptr;
bool tooltip = false;
if( ImGui::IsMouseHoveringRect( wpos + ImVec2( px0, offset ), wpos + ImVec2( pxw, offset + ty ) ) )
{
@@ -221,8 +227,9 @@ void View::DrawContextSwitchList( const TimelineContext& ctx, const std::vector<
}
else
{
waitTime = ev.WakeupVal() - prev.End();
TextFocused( "Thread is", migration ? "migrating CPUs" : "waiting" );
TextFocused( "Waiting time:", TimeToString( ev.WakeupVal() - prev.End() ) );
TextFocused( "Waiting time:", TimeToString( waitTime ) );
if( migration )
{
TextFocused( "CPU:", RealToString( prev.Cpu() ) );
@@ -235,18 +242,22 @@ void View::DrawContextSwitchList( const TimelineContext& ctx, const std::vector<
}
if( prev.Reason() != 100 )
{
TextFocused( "Wait reason:", DecodeContextSwitchReasonCode( prev.Reason() ) );
waitReason = DecodeContextSwitchReason( prev.Reason() );
waitReasonCode = DecodeContextSwitchReasonCode( prev.Reason() );
TextFocused( "Wait reason:", waitReasonCode );
ImGui::SameLine();
ImGui::PushFont( g_fonts.normal, FontSmall );
ImGui::AlignTextToFramePadding();
TextDisabledUnformatted( DecodeContextSwitchReason( prev.Reason() ) );
TextDisabledUnformatted( waitReason );
ImGui::PopFont();
}
TextFocused( "Wait state:", DecodeContextSwitchStateCode( prev.State() ) );
waitState = DecodeContextSwitchState( prev.State() );
waitStateCode = DecodeContextSwitchStateCode( prev.State() );
TextFocused( "Wait state:", waitStateCode );
ImGui::SameLine();
ImGui::PushFont( g_fonts.normal, FontSmall );
ImGui::AlignTextToFramePadding();
TextDisabledUnformatted( DecodeContextSwitchState( prev.State() ) );
TextDisabledUnformatted( waitState );
ImGui::PopFont();
}
tooltip = true;
@@ -275,16 +286,23 @@ void View::DrawContextSwitchList( const TimelineContext& ctx, const std::vector<
const auto waitStack = v.data;
if( waitStack )
{
ImGui::Separator();
TextDisabledUnformatted( ICON_FA_HOURGLASS_HALF " Wait stack:" );
CallstackTooltipContents( waitStack );
if( ImGui::IsMouseClicked( 0 ) )
{
m_callstackView = {
.id = waitStack,
.thread = m_worker.DecompressThread( ev.Thread() )
};
}
ImGui::Separator();
TextDisabledUnformatted( ICON_FA_HOURGLASS_HALF " Wait stack:" );
CallstackTooltipContents( waitStack );
if( ImGui::IsMouseClicked( 0 ) )
{
m_callstackView = {
.id = waitStack,
.thread = tid,
.wait = {
.time = waitTime,
.reason = waitReason,
.reasonCode = waitReasonCode,
.state = waitState,
.stateCode = waitStateCode
}
};
}
}
ImGui::EndTooltip();
}
@@ -616,7 +634,7 @@ void View::DrawWaitStacks()
PrintStringPercent( buf, 100. * data[m_waitStack]->second / totalCount );
TextDisabledUnformatted( buf );
ImGui::Separator();
DrawCallstackTable( data[m_waitStack]->first, 0, false, false );
DrawCallstackTable( data[m_waitStack]->first );
break;
}
case 1:

View File

@@ -292,7 +292,7 @@ bool View::DrawCpuData( const TimelineContext& ctx, const std::vector<CpuUsageDr
TextFocused( "Thread:", m_worker.GetThreadName( thread ) );
ImGui::SameLine();
ImGui::TextDisabled( "(%s)", RealToString( thread ) );
m_drawThreadMigrations = thread;
m_cpuDataThread = thread;
}
@@ -337,7 +337,7 @@ bool View::DrawCpuData( const TimelineContext& ctx, const std::vector<CpuUsageDr
if( it != v.begin() )
{
auto& prev = *( it - 1 );
ImGui::Separator();
TextFocused( "Wait reason:", DecodeContextSwitchReasonCode( prev.Reason() ) );
@@ -349,7 +349,7 @@ bool View::DrawCpuData( const TimelineContext& ctx, const std::vector<CpuUsageDr
TextFocused( "Wait state:", DecodeContextSwitchStateCode( prev.State() ) );
TextFocused( "Waiting time:", TimeToString( it->WakeupVal() - prev.End() ) );
}
// Do we have information about the readying thread?
if( it->Start() - it->WakeupVal() )
{
@@ -369,7 +369,7 @@ bool View::DrawCpuData( const TimelineContext& ctx, const std::vector<CpuUsageDr
bool wakeupThreadLocal, wakeupThreadUntracked;
const char* wakeUpThreadProgram;
auto wakeuplabel = GetThreadContextData( wakeupThread, wakeupThreadLocal, wakeupThreadUntracked, wakeUpThreadProgram );
uint32_t wakeupThreadColor = getDisplayThreadColor( wakeupThread, wakeupThreadLocal, wakeupThreadUntracked );
TextColoredUnformatted( HighlightColor<75>( wakeupThreadColor ), wakeuplabel );
ImGui::SameLine();

View File

@@ -1077,7 +1077,7 @@ void View::DrawZoneInfoWindow()
{
if( ImGui::TreeNode( "Call stack" ) )
{
DrawCallstackTable( m_worker.GetZoneExtra( ev ).callstack.Val(), tid, false, false );
DrawCallstackTable( m_worker.GetZoneExtra( ev ).callstack.Val(), tid );
ImGui::TreePop();
}
}
@@ -1091,7 +1091,7 @@ void View::DrawZoneInfoWindow()
TextDisabledUnformatted( ICON_FA_WAND_SPARKLES );
if( expand )
{
DrawCallstackTable( cs.data(), cs.size(), tid, false, false );
DrawCallstackTable( cs.data(), cs.size(), tid );
ImGui::TreePop();
}
}
@@ -1568,7 +1568,7 @@ void View::DrawGpuInfoWindow()
{
if( ImGui::TreeNode( "Call stack" ) )
{
DrawCallstackTable( ev.callstack.Val(), tid, false, false );
DrawCallstackTable( ev.callstack.Val(), tid );
ImGui::TreePop();
}
}

View File

@@ -84,7 +84,7 @@ void View::DrawThread( const TimelineContext& ctx, const ThreadData& thread, con
{
auto ctxSwitch = m_worker.GetContextSwitchData( thread.id );
assert( ctxSwitch );
DrawContextSwitchList( ctx, ctxDraw, ctxSwitch->v, ctxOffset, offset, thread.isFiber );
DrawContextSwitchList( ctx, ctxDraw, ctxSwitch->v, ctxOffset, offset, thread.isFiber, thread.id );
}
if( hasSamples && !samplesDraw.empty() )
{

View File

@@ -107,6 +107,12 @@ public:
assert( m_context != 255 );
if( !CheckFeature( "GL_ARB_timer_query" ) )
{
Profiler::LogString( MessageSourceType::Tracy, MessageSeverity::Warning, Color::Tomato, 0,
"OpenGL context does not support GL_ARB_timer_query." );
}
GLint bits;
glGetQueryiv( GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &bits );
if( bits == 0 )
@@ -209,6 +215,30 @@ public:
}
private:
// Returns whether the driver advertises a single extension (full GL_-prefixed token).
static bool CheckFeature( const char* feature )
{
GLint major = 0;
glGetIntegerv( GL_MAJOR_VERSION, &major );
if( glGetError() != GL_NO_ERROR ) major = 0; // pre-3.0: enum not supported
if( major >= 3 )
{
GLint numExt = 0;
glGetIntegerv( GL_NUM_EXTENSIONS, &numExt );
for( GLint i = 0; i < numExt; i++ )
{
auto ext = (const char*)glGetStringi( GL_EXTENSIONS, i );
if( ext && strcmp( ext, feature ) == 0 ) return true;
}
return false;
}
// pre GL3 fallback:
auto exts = (const char*)glGetString( GL_EXTENSIONS );
return exts && strstr( exts, feature ) != nullptr;
}
#ifdef TRACY_OPENGL_AUTO_CALIBRATION
// Monotonic host ns for the inter-calibration interval (cpuDelta), kept
// separate from Profiler::GetTime() as in the D3D12/Vulkan backends.

View File

@@ -1,26 +0,0 @@
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-import-strace
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)
add_executable(tracy-import-strace
src/stracy.cpp
)
add_git_ref(tracy-import-strace)
target_link_libraries(tracy-import-strace PRIVATE TracyServer)
set_property(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} PROPERTY VS_STARTUP_PROJECT tracy-import-strace)

View File

@@ -1,460 +0,0 @@
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "../../server/TracyFileWrite.hpp"
#include "../../server/TracyWorker.hpp"
#include "../../public/common/TracyVersion.hpp"
#include "GitRef.hpp"
static void Usage()
{
printf( "tracy-import-strace %d.%d.%d / %s\n\n",
tracy::Version::Major, tracy::Version::Minor, tracy::Version::Patch, tracy::GitRef );
printf( "Usage: tracy-import-strace input.strace output.tracy\n" );
printf( " tracy-import-strace - output.tracy (read from stdin)\n\n" );
printf( "Recommended strace invocation:\n" );
printf( " strace -ttt -T -f -o trace.log <program> [args...]\n" );
printf( " strace -ttt -T -f -p <pid> -o trace.log\n\n" );
printf( "Mapped events:\n" );
printf( " syscall entry/return -> timeline zones\n" );
printf( " signals -> messages\n" );
printf( " process exit/kill -> messages\n" );
exit( 1 );
}
// Parse "SSSSSSSSSS.UUUUUU" → nanoseconds without floating-point precision loss.
// Advances *p past the consumed characters. Returns UINT64_MAX on parse error.
static uint64_t ParseTimestamp( const char*& p )
{
if( !isdigit( (unsigned char)*p ) ) return UINT64_MAX;
uint64_t sec = 0;
while( isdigit( (unsigned char)*p ) )
sec = sec * 10 + ( *p++ - '0' );
if( *p != '.' ) return UINT64_MAX;
p++;
uint64_t usec = 0;
int digits = 0;
while( isdigit( (unsigned char)*p ) && digits < 6 )
{
usec = usec * 10 + ( *p++ - '0' );
digits++;
}
for( ; digits < 6; digits++ ) usec *= 10; // pad to microseconds if fewer digits
while( isdigit( (unsigned char)*p ) ) p++; // discard extra precision
return sec * 1000000000ULL + usec * 1000ULL;
}
// Consume "[pid N]" prefix and return N, or return default_tid if not present.
// Advances *p past the consumed characters (including trailing space).
static uint64_t ConsumePidPrefix( const char*& p, uint64_t default_tid )
{
if( p[0] != '[' || strncmp( p, "[pid", 4 ) != 0 ) return default_tid;
const char* q = p + 4;
while( *q == ' ' ) q++;
if( !isdigit( (unsigned char)*q ) ) return default_tid;
uint64_t tid = 0;
while( isdigit( (unsigned char)*q ) )
tid = tid * 10 + ( *q++ - '0' );
while( *q == ' ' ) q++;
if( *q != ']' ) return default_tid;
p = q + 1;
return tid;
}
// Parse "<N.NNNNNN>" trailing duration → nanoseconds. Returns 0 if not present.
static uint64_t ParseTrailingDuration( const char* line )
{
const char* p = line + strlen( line );
while( p > line && strchr( " \r\n", p[-1] ) ) p--;
if( p == line || p[-1] != '>' ) return 0;
p--;
const char* end = p;
while( p > line && p[-1] != '<' ) p--;
if( p == line || p[-1] != '<' ) return 0;
char tmp[32];
size_t len = end - p;
if( len == 0 || len >= sizeof( tmp ) ) return 0;
memcpy( tmp, p, len );
tmp[len] = '\0';
double dur_sec = 0.0;
if( sscanf( tmp, "%lf", &dur_sec ) != 1 ) return 0;
return (uint64_t)( dur_sec * 1e9 );
}
// Return the syscall name — the identifier before the first '('. Empty on failure.
static std::string ParseSyscallName( const char* p )
{
const char* start = p;
while( *p && *p != '(' && *p != ' ' && *p != '\n' ) p++;
if( p == start || *p != '(' ) return {};
return std::string( start, p );
}
// Extract syscall arguments for a COMPLETE call line.
// Finds the last " = " (the retval separator), then the ')' before it.
// This correctly handles " = " appearing inside string arguments.
static std::string ExtractArgsComplete( const char* line_start )
{
const char* open = strchr( line_start, '(' );
if( !open ) return {};
const char* args = open + 1;
// Walk the whole line to find the last " = "
const char* last_eq = nullptr;
for( const char* p = args; *p; p++ )
if( p[0] == ' ' && p[1] == '=' && p[2] == ' ' )
last_eq = p;
if( !last_eq ) return {};
// Find the closing ')' that immediately precedes " = "
const char* close = last_eq;
while( close > args && *close != ')' ) close--;
if( close <= args ) return {};
return std::string( args, close );
}
// Extract syscall arguments for an UNFINISHED call line, up to the delimiter.
static std::string ExtractArgsUnfinished( const char* line_start, const char* delim )
{
const char* open = strchr( line_start, '(' );
if( !open ) return {};
const char* args = open + 1;
const char* end = strstr( args, delim );
if( !end || end <= args ) return {};
while( end > args && end[-1] == ' ' ) end--; // trim trailing space
return std::string( args, end );
}
// Extract a quoted string from strace arg output, e.g. the "name" in
// prctl(PR_SET_NAME, "name") = 0. Returns empty string if not found.
static std::string ExtractQuotedString( const char* p )
{
const char* open = strchr( p, '"' );
if( !open ) return {};
open++;
const char* close = strchr( open, '"' );
if( !close ) return {};
return std::string( open, close );
}
// Parse the integer return value from a complete strace line " = N <dur>".
// Returns -1 on failure.
static int64_t ParseRetval( const char* line )
{
const char* eq = strstr( line, " = " );
if( !eq ) return -1;
const char* p = eq + 3;
bool neg = ( *p == '-' );
if( neg ) p++;
if( !isdigit( (unsigned char)*p ) ) return -1;
int64_t v = 0;
while( isdigit( (unsigned char)*p ) ) v = v * 10 + (*p++ - '0');
return neg ? -v : v;
}
// Extract the function name from a strace -k frame line, e.g.:
// "/lib/libc.so.6(__read_nocancel+0x7) [0xf1e07]" → "__read_nocancel"
static std::string ParseFrameName( const std::string& frame )
{
const char* p = frame.c_str();
const char* open = strchr( p, '(' );
if( !open ) return frame;
const char* name = open + 1;
const char* plus = strchr( name, '+' );
const char* close = strchr( name, ')' );
const char* end = ( plus && ( !close || plus < close ) ) ? plus : close;
if( !end ) return frame;
return std::string( name, end );
}
struct PendingEntry
{
uint64_t ts_begin;
std::string syscall_name;
std::string args_text;
};
// A complete or resumed syscall zone held until its trailing callstack frames are collected.
struct PendingComplete
{
uint64_t tid;
uint64_t ts_begin;
uint64_t ts_end;
std::string name;
std::string args;
std::vector<std::string> frames; // innermost-first, as strace emits them
};
int main( int argc, char** argv )
{
if( argc != 3 ) Usage();
const char* input_path = argv[1];
const char* output_path = argv[2];
FILE* fin = ( strcmp( input_path, "-" ) == 0 ) ? stdin : fopen( input_path, "r" );
if( !fin )
{
fprintf( stderr, "Cannot open input: %s\n", input_path );
return 1;
}
std::vector<tracy::Worker::ImportEventTimeline> timeline;
std::vector<tracy::Worker::ImportEventMessages> messages;
std::vector<tracy::Worker::ImportEventPlots> plots; // required by Worker API; unused
std::unordered_map<uint64_t, std::string> threadNames;
std::unordered_map<uint64_t, PendingEntry> pending; // tid → in-flight syscall
// Holds a complete/resumed zone while we collect its trailing callstack frames.
std::optional<PendingComplete> pending_complete;
// Emit a pending zone as nested Tracy zones:
// outer callstack frames (one begin per frame, outermost first)
// syscall zone (begin + end)
// outer callstack frames (one end per frame, innermost first)
//
// frames[0] is the innermost frame (the libc/kernel syscall stub) and is
// skipped — it is already represented by the syscall zone itself.
auto flushComplete = [&]()
{
if( !pending_complete ) return;
const auto& pc = *pending_complete;
const size_t user_start = pc.frames.empty() ? 0 : 1; // skip innermost stub
// Open outer frames, outermost first (= reversed from strace order).
for( int i = (int)pc.frames.size() - 1; i >= (int)user_start; i-- )
timeline.push_back( { pc.tid, pc.ts_begin,
ParseFrameName( pc.frames[i] ), pc.frames[i],
false, "", 0 } );
// The syscall zone itself.
timeline.push_back( { pc.tid, pc.ts_begin, pc.name, pc.args, false, "", 0 } );
timeline.push_back( { pc.tid, pc.ts_end, "", "", true, "", 0 } );
// Close outer frames, innermost first.
for( size_t i = user_start; i < pc.frames.size(); i++ )
timeline.push_back( { pc.tid, pc.ts_end, "", "", true, "", 0 } );
pending_complete.reset();
};
char buf[65536];
// TID used when strace output has no [pid N] prefix (single-threaded traces or
// the very first calls of a process before any fork).
constexpr uint64_t kDefaultTid = 0;
while( fgets( buf, sizeof( buf ), fin ) )
{
const char* p = buf;
// Callstack frame from strace -k: " > /path/lib(func+0xoff) [0xaddr]"
// Must be checked before the isdigit guard.
if( p[0] == ' ' && p[1] == '>' )
{
if( pending_complete )
{
const char* frame = p + 2;
while( *frame == ' ' ) frame++;
std::string f( frame );
while( !f.empty() && strchr( "\r\n", f.back() ) ) f.pop_back();
if( !f.empty() ) pending_complete->frames.push_back( std::move( f ) );
}
continue;
}
// Any non-frame line closes the pending zone before we process it.
flushComplete();
// Only process lines whose first character is a digit.
// Silently skips strace warnings, "Process N attached/detached", etc.
if( !isdigit( (unsigned char)*p ) ) continue;
// Detect which line format strace used:
// Format A (-o to file/pipe): "PID TIMESTAMP syscall..."
// Format B (to stderr/tty): "TIMESTAMP [pid N] syscall..."
// Distinguish by whether the first numeric token contains a '.' (timestamp)
// or is followed by a space without a '.' (bare PID).
uint64_t tid = kDefaultTid;
{
const char* q = p;
while( isdigit( (unsigned char)*q ) ) q++;
if( *q == ' ' )
{
// Format A: leading PID token
while( isdigit( (unsigned char)*p ) ) tid = tid * 10 + (*p++ - '0');
while( *p == ' ' ) p++;
}
// else: Format B — timestamp comes first, [pid N] (if any) follows it
}
uint64_t ts = ParseTimestamp( p );
if( ts == UINT64_MAX ) continue;
while( *p == ' ' ) p++;
// Format B may carry "[pid N]" after the timestamp; Format A already has tid.
if( tid == kDefaultTid )
tid = ConsumePidPrefix( p, kDefaultTid );
while( *p == ' ' ) p++;
// Register the thread name the first time we see this TID.
if( threadNames.find( tid ) == threadNames.end() )
{
char name[32];
if( tid == kDefaultTid )
snprintf( name, sizeof( name ), "main" );
else
snprintf( name, sizeof( name ), "%" PRIu64, tid );
threadNames[tid] = name;
}
// --- Resumed syscall -------------------------------------------
// "<... SYSCALL resumed>) = RETVAL <DUR>"
if( strncmp( p, "<...", 4 ) == 0 )
{
const char* name_start = p + 4;
while( *name_start == ' ' ) name_start++;
const char* resumed = strstr( name_start, " resumed" );
if( !resumed ) continue;
std::string syscall_name( name_start, resumed );
auto it = pending.find( tid );
uint64_t ts_begin = ( it != pending.end() ) ? it->second.ts_begin : ts;
std::string args_text = ( it != pending.end() ) ? it->second.args_text : "";
if( it != pending.end() ) pending.erase( it );
// ts = timestamp of the return line; use as zone end.
pending_complete = PendingComplete{ tid, ts_begin, ts,
std::move( syscall_name ),
std::move( args_text ), {} };
}
// --- Process exit / kill ----------------------------------------
// "+++ exited with N +++" | "+++ killed by SIGNAL +++"
else if( strncmp( p, "+++", 3 ) == 0 )
{
std::string raw( p );
while( !raw.empty() && strchr( "\r\n", raw.back() ) ) raw.pop_back();
messages.push_back( { tid, ts, std::to_string( tid ) + ": " + raw } );
}
// --- Signal ------------------------------------------------------
// "--- SIGNAME {...} ---"
else if( strncmp( p, "---", 3 ) == 0 )
{
std::string raw( p );
while( !raw.empty() && strchr( "\r\n", raw.back() ) ) raw.pop_back();
messages.push_back( { tid, ts, std::to_string( tid ) + ": " + raw } );
}
// --- Regular syscall (complete or unfinished) --------------------
else
{
std::string syscall_name = ParseSyscallName( p );
if( syscall_name.empty() ) continue;
if( strstr( p, "<unfinished ...>" ) )
{
// Syscall blocked — park in the pending table.
std::string args = ExtractArgsUnfinished( p, " <unfinished" );
pending[tid] = PendingEntry{ ts, std::move( syscall_name ), std::move( args ) };
}
else
{
// Complete call with return value (and optional duration from -T).
uint64_t dur_ns = ParseTrailingDuration( buf );
std::string args = ExtractArgsComplete( p );
// prctl(PR_SET_NAME, "name") — update the thread's display name.
if( syscall_name == "prctl" && strstr( p, "PR_SET_NAME" ) )
{
std::string name = ExtractQuotedString( strstr( p, "PR_SET_NAME" ) );
if( !name.empty() )
threadNames[tid] = std::move( name );
}
// clone/clone3 with CLONE_THREAD — pre-name the child thread so it
// has a label before it calls prctl itself.
else if( ( syscall_name == "clone" || syscall_name == "clone3" ) &&
strstr( p, "CLONE_THREAD" ) )
{
int64_t child_tid = ParseRetval( buf );
if( child_tid > 0 && threadNames.find( (uint64_t)child_tid ) == threadNames.end() )
{
char name[32];
snprintf( name, sizeof( name ), "%" PRId64, child_tid );
threadNames[(uint64_t)child_tid] = name;
}
}
pending_complete = PendingComplete{ tid, ts, ts + dur_ns,
syscall_name,
std::move( args ), {} };
}
}
}
if( fin != stdin ) fclose( fin );
flushComplete(); // flush the last zone if the file ends with callstack frames
// Sort by timestamp. strace output is mostly ordered, but interleaved threads
// and resumed events can place end-of-zone before begin-of-zone after sorting.
std::stable_sort( timeline.begin(), timeline.end(),
[]( const auto& a, const auto& b ) { return a.timestamp < b.timestamp; } );
std::stable_sort( messages.begin(), messages.end(),
[]( const auto& a, const auto& b ) { return a.timestamp < b.timestamp; } );
// Shift all timestamps so the trace starts at t = 0.
uint64_t mts = UINT64_MAX;
for( const auto& e : timeline ) mts = std::min( mts, e.timestamp );
for( const auto& e : messages ) mts = std::min( mts, e.timestamp );
if( mts == UINT64_MAX ) mts = 0;
for( auto& e : timeline ) e.timestamp -= mts;
for( auto& e : messages ) e.timestamp -= mts;
const size_t zone_count = timeline.size() / 2;
fprintf( stderr, "Parsed %zu zones, %zu messages, %zu threads\n",
zone_count, messages.size(), threadNames.size() );
// Tracy Worker expects basenames, not full paths.
auto basename = []( const char* path ) -> const char* {
const char* s = path;
for( const char* q = path; *q; q++ )
if( *q == '/' || *q == '\\' ) s = q + 1;
return s;
};
tracy::Worker worker( basename( output_path ), basename( input_path ),
timeline, messages, plots, threadNames );
auto w = std::unique_ptr<tracy::FileWrite>( tracy::FileWrite::Open( output_path, tracy::FileCompression::Fast ) );
if( !w )
{
fprintf( stderr, "Cannot open output file: %s\n", output_path );
return 1;
}
worker.Write( *w, false );
return 0;
}