Skip to content

Read trace events through each event's own format file - #57

Merged
tamnd merged 2 commits into
mainfrom
event-formats
Sep 5, 2026
Merged

tamnd merged 2 commits into
mainfrom
event-formats

Conversation

@tamnd

@tamnd tamnd commented Sep 5, 2026

Copy link
Copy Markdown
Owner

This finishes the kxray.trace row in #2. #56 did the flat function tracer, and this does the last third: trace events, read through each event's own format file rather than through a layout written down in Python.

Why the format file and not a table of offsets

Because the offsets are not a property of the event. They are a property of the machine that compiled the kernel. Here is sched_switch on the 32 bit box this project pins:

	field:long prev_state;	offset:32;	size:4;	signed:1;
	field:char next_comm[16];	offset:36;	size:16;	signed:0;

On the machine you are reading this on, long is eight bytes, so prev_state is eight bytes, next_comm starts at 40, and every field after that has moved too. A parser holding those numbers as constants is right on one of the two machines and reads the wrong bytes on the other, and it reports nothing wrong while doing it. The kernel publishes the file precisely so nobody has to guess, so this reads it.

Four formats, one capture, all real Tier 0

The formats are in a new corpora/events/tier0/, each with its own metadata. They were picked because each has a field shape the others do not.

sched_switch for prev_state, above. sched_wakeup for comm, a char[16] copied into the record when the event fires. sched_process_exec for filename, a __data_loc char[], which is four bytes in the record holding an offset and a length with the string itself further along, and which is why the size column says 4 for a field that prints as /bin/true. sys_enter for args, unsigned long args[6], the only array here that is not a string: one field of 24 bytes rather than six fields of four.

sys_enter is in no capture. It is kept anyway, because a corpus of formats holding only the shapes one capture happened to use is a corpus that tests one code path.

corpora/traces/tier0/events-exec.txt is the shell forking, execing /bin/true, and doing it again for sleep. Thirteen events. The banner says tracer: nop and that is correct rather than a mistake: events are not a tracer, they are switched on one at a time under events/ and they record whether or not a tracer is running.

The line worth the whole PR

time      cpu  task            context    event         prev_comm    prev_pid  prev_state  next_comm    next_pid
--------  ---  --------------  ---------  ------------  -----------  --------  ----------  -----------  --------
3.049350  0    sleep-38        nopreempt  sched_switch  sh           38        R+          rcu_preempt  13

The header says sleep-38. The payload says prev_comm=sh prev_pid=38. Same pid, two names, and both are correct.

The payload holds a copy of the comm made when the event fired, which was before pid 38 had exec'd. The header is not stored per line at all: ftrace keeps a map from pid to name and fills that column in when the buffer is printed, and by then the map says sleep. So the payload is the truth about the moment and the header is the truth about the pid afterwards.

That is the argument for reading a payload through its format instead of trusting the header, and it is not a hypothetical, it is line nine of the file.

What reading through the format gives you

Three things a plain key=value split cannot.

Values arrive as numbers where the format declares numbers. pid=38 is the integer and comm=sh is the string, and nothing downstream guesses by looking at the characters. target_cpu prints as 000 because of a %03d in the print fmt, and reads back as 0.

Keys the format does not declare are named, and so are fields the format declares that the line did not print. Neither is a fault. A print fmt is free to leave a field out. This is how you find out it did, rather than by wondering later where a field went.

And a field the format calls a number that arrived as text is recorded as symbolic rather than treated as a failure. prev_state is declared long and reaches you as S, R+, I or X, because print fmt ran it through __print_flags before the text existed. The record and the line are both correct and are not the same thing. On the committed capture, prev_state is the only symbolic field and every one of the thirteen events binds cleanly to its format with nothing unknown and nothing missing.

What this deliberately does not do

It does not read the ring buffer in binary. trace_pipe_raw is where that lives and it is what the offsets and sizes in a format are actually for, and it is a separate piece of work.

It does not evaluate print fmt. That line is a C expression with nested macro expansions in it, and for kmem:kmalloc it is several thousand characters of GFP flag resolution. Evaluating it properly means being a C compiler. It is kept as text, because a person reading it learns something.

Housekeeping

TraceLog now holds what all three parsers share: the source, the tracer name, the line accounting, and the buffer counts off the banner that say whether the trace has holes in it. The per tracer classes add only the list of things on their lines. That refactor is why FunctionLog moved.

The new class is TraceEvent rather than Event, because Event in kxray/models.py already means a non frame line inside a function_graph tape. TraceEvent is also what the kernel calls these.

tests/test_function_graph.py now selects its artefacts by the tracer key in each capture's metadata instead of by globbing every .txt. Three tracers write into those directories now and only one of them is that parser's.

The kernel publishes the layout of every event at
events/<group>/<event>/format so that nothing has to hard code one. It
is not a nicety: sched_switch declares `long prev_state`, which is four
bytes on the 32 bit box this project pins and eight almost everywhere
else, and every field after it moves.

Four real format files, one real capture, a reader for each, and the
three things reading a line through its format gives you that a plain
key=value split cannot.
@tamnd
tamnd merged commit a5501ff into main Sep 5, 2026
3 checks passed
@tamnd
tamnd deleted the event-formats branch September 5, 2026 17:00
@tamnd tamnd mentioned this pull request Sep 5, 2026
20 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant