Read tracepoint field offsets from tracefs format files.

The client decoded sched_switch, sched_waking and drm_vblank_event at
fixed positions behind an 8-byte common header. The kernel can extend the
header, which shifts the field positions.

The format files are generated from the live trace_entry (offsetof/ALIGN),
making them the authoritative layout.
This commit is contained in:
Bartosz Taudul
2026-09-08 21:32:27 +02:00
parent 2ba979056c
commit 25ba41c44e

View File

@@ -672,6 +672,161 @@ static char* GetTraceFsPath()
return ret;
}
struct TracepointField
{
uint32_t offset;
uint32_t size;
};
static struct
{
TracepointField prevPid, prevPrio, prevState, nextPid, nextPrio;
uint32_t minRecordSize;
} s_switchLayout;
static struct
{
TracepointField pid;
uint32_t minRecordSize;
} s_wakingLayout;
static struct
{
TracepointField crtc;
uint32_t minRecordSize;
} s_vsyncLayout;
// Reads the whole format file of a tracepoint event. Caller owns the buffer.
static char* ReadTracepointFormat( const char* base, const char* eventPath )
{
const auto blen = strlen( base );
const auto plen = strlen( eventPath );
const char* suffix = "/format";
const auto slen = strlen( suffix );
auto path = (char*)tracy_malloc( blen + plen + slen + 1 );
memcpy( path, base, blen );
memcpy( path + blen, eventPath, plen );
memcpy( path + blen + plen, suffix, slen + 1 );
const int fd = open( path, O_RDONLY );
tracy_free( path );
if( fd < 0 ) return nullptr;
size_t cap = 4096;
size_t len = 0;
auto buf = (char*)tracy_malloc( cap );
for(;;)
{
const auto cnt = read( fd, buf + len, cap - 1 - len );
if( cnt <= 0 ) break;
len += (size_t)cnt;
if( len + 1 == cap )
{
cap *= 2;
buf = (char*)tracy_realloc( buf, cap );
}
}
close( fd );
buf[len] = '\0';
return buf;
}
static const char* FindInLine( const char* s, const char* end, const char* needle )
{
const auto nlen = strlen( needle );
while( s + nlen <= end )
{
if( memcmp( s, needle, nlen ) == 0 ) return s;
s++;
}
return nullptr;
}
static bool ParseTracepointField( const char* format, const char* name, TracepointField& field )
{
const auto nlen = strlen( name );
auto line = format;
while( *line )
{
const auto eol = strchr( line, '\n' );
const auto end = eol ? eol : line + strlen( line );
auto s = line;
while( s < end && ( *s == ' ' || *s == '\t' ) ) s++;
if( strncmp( s, "field:", 6 ) == 0 )
{
s += 6;
const auto semi = (const char*)memchr( s, ';', end - s );
if( semi )
{
auto tok = semi;
while( tok > s && tok[-1] != ' ' && tok[-1] != '\t' ) tok--;
auto tlen = (size_t)( semi - tok );
const auto bracket = (const char*)memchr( tok, '[', tlen );
if( bracket ) tlen = (size_t)( bracket - tok );
if( tlen == nlen && memcmp( tok, name, tlen ) == 0 )
{
const auto offsetTag = FindInLine( semi + 1, end, "offset:" );
const auto sizeTag = FindInLine( semi + 1, end, "size:" );
if( !offsetTag || !sizeTag ) return false;
const int off = atoi( offsetTag + 7 );
const int sz = atoi( sizeTag + 5 );
if( off < 0 || sz <= 0 ) return false;
field.offset = (uint32_t)off;
field.size = (uint32_t)sz;
return true;
}
}
}
line = eol ? eol + 1 : end;
}
return false;
}
static uint32_t MaxLayoutEnd( uint32_t cur, const TracepointField& field )
{
const auto e = field.offset + field.size;
return e > cur ? e : cur;
}
static bool ParseSchedSwitchFormat( const char* format )
{
auto& f = s_switchLayout;
if( !ParseTracepointField( format, "prev_pid", f.prevPid ) || f.prevPid.size != 4 ) return false;
if( !ParseTracepointField( format, "prev_prio", f.prevPrio ) || f.prevPrio.size != 4 ) return false;
if( !ParseTracepointField( format, "prev_state", f.prevState ) || ( f.prevState.size != 4 && f.prevState.size != 8 ) ) return false;
if( !ParseTracepointField( format, "next_pid", f.nextPid ) || f.nextPid.size != 4 ) return false;
if( !ParseTracepointField( format, "next_prio", f.nextPrio ) || f.nextPrio.size != 4 ) return false;
uint32_t min = 0;
min = MaxLayoutEnd( min, f.prevPid );
min = MaxLayoutEnd( min, f.prevPrio );
min = MaxLayoutEnd( min, f.prevState );
min = MaxLayoutEnd( min, f.nextPid );
min = MaxLayoutEnd( min, f.nextPrio );
f.minRecordSize = min;
return true;
}
static bool ParseSchedWakingFormat( const char* format )
{
auto& f = s_wakingLayout;
if( !ParseTracepointField( format, "pid", f.pid ) || f.pid.size != 4 ) return false;
f.minRecordSize = MaxLayoutEnd( 0, f.pid );
return true;
}
static bool ParseDrmVblankFormat( const char* format )
{
auto& f = s_vsyncLayout;
if( !ParseTracepointField( format, "crtc", f.crtc ) || f.crtc.size != 4 ) return false;
f.minRecordSize = MaxLayoutEnd( 0, f.crtc );
return true;
}
// Categories of the running kernel's perf_event_open() ABI, defined by the
// perf_event_attr fields Tracy uses. use_clockid/clockid exist since Linux
// 4.1 (commit 34f439278c), sample_max_stack since Linux 4.8 (commit
@@ -727,6 +882,38 @@ bool SysTraceStart( int64_t& samplingPeriod )
const auto vsyncIdStr = ReadFile( traceFsPath, "/events/drm/drm_vblank_event/id" );
if( vsyncIdStr ) vsyncId = atoi( vsyncIdStr );
bool switchLayout = false, wakingLayout = false, vsyncLayout = false;
if( switchId != -1 )
{
const auto switchFormat = ReadTracepointFormat( traceFsPath, "/events/sched/sched_switch" );
if( switchFormat )
{
switchLayout = ParseSchedSwitchFormat( switchFormat );
tracy_free( switchFormat );
}
if( !switchLayout ) TracyDebug( "Failed to parse sched_switch format, context switch capture disabled." );
}
if( wakingId != -1 )
{
const auto wakingFormat = ReadTracepointFormat( traceFsPath, "/events/sched/sched_waking" );
if( wakingFormat )
{
wakingLayout = ParseSchedWakingFormat( wakingFormat );
tracy_free( wakingFormat );
}
if( !wakingLayout ) TracyDebug( "Failed to parse sched_waking format, waking capture disabled." );
}
if( vsyncId != -1 )
{
const auto vsyncFormat = ReadTracepointFormat( traceFsPath, "/events/drm/drm_vblank_event" );
if( vsyncFormat )
{
vsyncLayout = ParseDrmVblankFormat( vsyncFormat );
tracy_free( vsyncFormat );
}
if( !vsyncLayout ) TracyDebug( "Failed to parse drm_vblank_event format, vsync capture disabled." );
}
tracy_free( traceFsPath );
TracyDebug( "sched_switch id: %i", switchId );
@@ -981,7 +1168,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
s_ctxBufferIdx = s_numBuffers;
// vsync
if( !noVsync && vsyncId != -1 )
if( !noVsync && vsyncId != -1 && vsyncLayout )
{
pe = {};
pe.type = PERF_TYPE_TRACEPOINT;
@@ -1013,7 +1200,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
}
// context switches
if( !noCtxSwitch && switchId != -1 )
if( !noCtxSwitch && switchId != -1 && switchLayout )
{
s_ctxSwitchCallchain = !noWaitStacks;
@@ -1053,7 +1240,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
}
}
if( wakingId != -1 )
if( wakingId != -1 && wakingLayout )
{
pe = {};
pe.type = PERF_TYPE_TRACEPOINT;
@@ -1398,15 +1585,7 @@ void SysTraceWorker( void* ptr )
// u64 ip[cnt] // PERF_SAMPLE_CALLCHAIN, if enabled
// u32 size
// u8 data[size]
// Data (not ABI stable, but has not changed since it was added, in 2009):
// u8 hdr[8]
// u8 prev_comm[16]
// u32 prev_pid
// u32 prev_prio
// lng prev_state
// u8 next_comm[16]
// u32 next_pid
// u32 next_prio
// Field offsets within the record come from the format file, parsed in SysTraceStart.
offset += sizeof( perf_event_header ) + sizeof( uint64_t );
@@ -1419,72 +1598,69 @@ void SysTraceWorker( void* ptr )
traceOffset = offset;
offset += sizeof( uint64_t ) * cnt;
}
offset += sizeof( uint32_t ) + 8 + 16;
offset += sizeof( uint32_t );
struct
{
uint32_t prev_pid, prev_prio;
long prev_state;
char next_comm[16];
uint32_t next_pid, next_prio;
} buf;
TRACY_ASSERT( offset + s_switchLayout.minRecordSize <= rbPos + hdr.size );
ring.Read( &buf, offset, sizeof( buf ) );
const auto& f = s_switchLayout;
uint32_t prev_pid, prev_prio, next_pid, next_prio;
uint64_t prev_state = 0;
ring.Read( &prev_pid, offset + f.prevPid.offset, sizeof( prev_pid ) );
ring.Read( &prev_prio, offset + f.prevPrio.offset, sizeof( prev_prio ) );
ring.Read( &prev_state, offset + f.prevState.offset, f.prevState.size );
ring.Read( &next_pid, offset + f.nextPid.offset, sizeof( next_pid ) );
ring.Read( &next_prio, offset + f.nextPrio.offset, sizeof( next_prio ) );
uint8_t oldThreadWaitReason = 100;
uint8_t oldThreadState;
if( buf.prev_state & 0x0001 ) oldThreadState = 104;
else if( buf.prev_state & 0x0002 ) oldThreadState = 101;
else if( buf.prev_state & 0x0004 ) oldThreadState = 105;
else if( buf.prev_state & 0x0008 ) oldThreadState = 106;
else if( buf.prev_state & 0x0010 ) oldThreadState = 108;
else if( buf.prev_state & 0x0020 ) oldThreadState = 109;
else if( buf.prev_state & 0x0040 ) oldThreadState = 110;
else if( buf.prev_state & 0x0080 ) oldThreadState = 102;
if( prev_state & 0x0001 ) oldThreadState = 104;
else if( prev_state & 0x0002 ) oldThreadState = 101;
else if( prev_state & 0x0004 ) oldThreadState = 105;
else if( prev_state & 0x0008 ) oldThreadState = 106;
else if( prev_state & 0x0010 ) oldThreadState = 108;
else if( prev_state & 0x0020 ) oldThreadState = 109;
else if( prev_state & 0x0040 ) oldThreadState = 110;
else if( prev_state & 0x0080 ) oldThreadState = 102;
else oldThreadState = 103;
TracyLfqPrepare( QueueType::ContextSwitch );
MemWrite( &item->contextSwitch.time, t0 );
MemWrite( &item->contextSwitch.oldThread, buf.prev_pid );
MemWrite( &item->contextSwitch.newThread, buf.next_pid );
MemWrite( &item->contextSwitch.oldThread, prev_pid );
MemWrite( &item->contextSwitch.newThread, next_pid );
MemWrite( &item->contextSwitch.cpu, uint8_t( ring.GetCpu() ) );
MemWrite( &item->contextSwitch.oldThreadWaitReason, oldThreadWaitReason );
MemWrite( &item->contextSwitch.oldThreadState, oldThreadState );
MemWrite( &item->contextSwitch.previousCState, uint8_t( 0 ) );
MemWrite( &item->contextSwitch.newThreadPriority, int8_t( buf.next_prio ) );
MemWrite( &item->contextSwitch.oldThreadPriority, int8_t( buf.prev_prio ) );
MemWrite( &item->contextSwitch.newThreadPriority, int8_t( next_prio ) );
MemWrite( &item->contextSwitch.oldThreadPriority, int8_t( prev_prio ) );
TracyLfqCommit;
if( cnt > 0 && buf.prev_pid != 0 && CurrentProcOwnsThread( buf.prev_pid ) )
if( cnt > 0 && prev_pid != 0 && CurrentProcOwnsThread( prev_pid ) )
{
auto trace = GetCallstackBlock( cnt, ring, traceOffset );
TracyLfqPrepare( QueueType::CallstackSampleContextSwitch );
MemWrite( &item->callstackSampleFat.time, t0 );
MemWrite( &item->callstackSampleFat.thread, buf.prev_pid );
MemWrite( &item->callstackSampleFat.thread, prev_pid );
MemWrite( &item->callstackSampleFat.ptr, (uint64_t)trace );
TracyLfqCommit;
}
}
else if( rid == EventWaking)
else if( rid == EventWaking )
{
// See /sys/kernel/debug/tracing/events/sched/sched_waking/format
// Layout:
// u64 time // PERF_SAMPLE_TIME
// u32 size
// u8 data[size]
// Data:
// u8 hdr[8]
// u8 comm[16]
// u32 pid
// i32 prio
// i32 target_cpu
const uint32_t dataOffset = sizeof( perf_event_header ) + sizeof( uint64_t ) + sizeof( uint32_t );
offset += dataOffset + 8 + 16;
// Field offsets within the record come from the format file, parsed in SysTraceStart.
const uint64_t dataOffset = offset + sizeof( perf_event_header ) + sizeof( uint64_t ) + sizeof( uint32_t );
TRACY_ASSERT( dataOffset + s_wakingLayout.minRecordSize <= rbPos + hdr.size );
uint32_t pid;
ring.Read( &pid, offset, sizeof( uint32_t ) );
ring.Read( &pid, dataOffset + s_wakingLayout.pid.offset, sizeof( uint32_t ) );
TracyLfqPrepare( QueueType::ThreadWakeup );
MemWrite( &item->threadWakeup.time, t0 );
MemWrite( &item->threadWakeup.thread, pid );
@@ -1503,17 +1679,13 @@ void SysTraceWorker( void* ptr )
// u64 time
// u32 size
// u8 data[size]
// Data (not ABI stable):
// u8 hdr[8]
// i32 crtc
// u32 seq
// i64 ktime
// u8 high precision
// Field offsets within the record come from the format file, parsed in SysTraceStart.
offset += sizeof( perf_event_header ) + sizeof( uint64_t ) + sizeof( uint32_t ) + 8;
const uint64_t dataOffset = offset + sizeof( perf_event_header ) + sizeof( uint64_t ) + sizeof( uint32_t );
TRACY_ASSERT( dataOffset + s_vsyncLayout.minRecordSize <= rbPos + hdr.size );
int32_t crtc;
ring.Read( &crtc, offset, sizeof( int32_t ) );
ring.Read( &crtc, dataOffset + s_vsyncLayout.crtc.offset, sizeof( int32_t ) );
// Note: The timestamp value t0 might be off by a number of microseconds from the
// true hardware vblank event. The ktime value should be used instead, but it is