Tracing Wayland Messages
jay trace prints the Wayland messages exchanged between the compositor and
one or more clients. It is like strace -p, but for Wayland connections. Use
it to see what an application is asking the compositor to do.
Unlike WAYLAND_DEBUG=1, tracing is attached from the outside: the application
does not have to be restarted, does not need any special environment, and does
not have to cooperate.
Warning
A trace contains everything the client sends and receives, including key events, window titles, and clipboard activity. Treat trace output as sensitive.
Selecting Clients
Trace a single client by its ID, or click the window you are interested in:
~$ jay trace id 42
~$ jay trace select-window
The ID is the one jay clients reports. Both of these commands exit on their
own once the traced client disconnects.
Trace every client instead:
~$ jay trace all
Or select clients by their properties, using a match expression:
~$ jay trace match -e 'comm = "chromium"'
~$ jay trace match -e 'sandbox-app-id = "com.spotify.Client"'
jay trace all and jay trace match run until interrupted, and they also pick
up clients that connect later. You can therefore start the trace before
launching the application you want to look at:
~$ jay trace match -e 'comm = "alacritty"' &
~$ alacritty
Note
A match with no criteria matches every client, so
jay trace match -e ''is the same asjay trace all.
Reading the Output
Each message is printed on one line, in a format similar to WAYLAND_DEBUG=1:
[12:20:30.035290] {7} -> wl_display#1.get_registry(registry: wl_registry#2)
[12:20:30.035290] {7} wl_registry#2.global(name: 35, interface: "wl_shm", version: 2)
A line consists of:
- The UTC time at which the compositor processed the message. The compositor handles many messages in one go, so consecutive lines often carry the same timestamp.
- The client ID in braces. This is the same ID that
jay clientsshows. ->for a message sent by the client, two blank spaces for a message sent by the compositor.- The interface, object ID, and name of the message.
- The arguments, each prefixed with its parameter name.
Colors are used when the output goes to a terminal.
Arguments are formatted from Jay’s own message definitions rather than the
standard XML protocol definitions, so some of them read differently than under
WAYLAND_DEBUG=1:
- The contents of arrays are printed instead of being elided.
- An array of
u16oru32values prints those values rather than the underlying bytes. - A
u64argument that the XML definitions split intohiandlohalves prints as a single parameter. - File descriptors print as
fd. The descriptor itself is not part of the trace.
Object IDs
Wayland clients reuse object IDs, so under WAYLAND_DEBUG=1 the same number
can refer to a number of unrelated objects over the life of a connection. Jay
assigns every object a unique ID instead:
[12:41:30.070469] {22} -> wl_display#1.sync(callback: wl_callback#6)
[12:41:30.070469] {22} wl_callback#6.done(callback_data: 0)
[12:41:30.070504] {22} -> wl_display#1.sync(callback: wl_callback#7)
[12:41:30.070504] {22} wl_callback#7.done(callback_data: 0)
The second wl_callback gets ID 7 even though the client reused ID 6. This
way wl_surface#50 always refers to the same object throughout the trace,
which makes the output much easier to follow.
--raw-ids- Print the IDs that actually appear on the wire, as
WAYLAND_DEBUG=1does.
Dropped Messages
The compositor writes the trace into a fixed-size buffer that jay trace
reads from. If jay trace does not keep up, the compositor drops messages
rather than slowing the client down. A message whose arguments are too large
for the buffer is dropped as well.
Dropped messages are counted and reported on the next message that gets through:
Missed 22 messages from client 21
The count is not lost silently, but the dropped messages themselves are gone.
If you see these lines, write the trace to a file with -o instead of to a
terminal or a slow command, and narrow the trace to the clients you are
actually interested in.
Tip
This also makes it safe to stop the output of a terminal with
ctrl-swhile you look at what has already been printed, and to resume it withctrl-qafterwards. Neither the traced client nor the compositor is slowed down; the messages produced in the meantime are simply dropped.
Writing to Files
-o <PREFIX>,--output <PREFIX>- Write to
<PREFIX>.<client id>.<suffix>instead of standard output, one file per client. The suffix istxt, orjsonlwith--json. -c,--combine- Write every client to a single
<PREFIX>.<suffix>file. Requires-o.
~$ jay trace -o logs all # logs.1234.txt, logs.1235.txt, ...
~$ jay trace -o logs -c all # logs.txt
Output written with -o is never colored, whether it goes to a file or to a
command.
Piping to a Command
If the argument to -o begins with a pipe, |, the rest of it is not a file
prefix but a shell command. Jay runs it through $SHELL -c and writes the
trace to its standard input:
~$ jay --json trace -o '| jq -r .msg' select-window
The command inherits Jay’s standard output and standard error, so in the example above the message names appear on your terminal.
Without -c, one instance of the command is started per traced client, and
JAY_CLIENT_ID is set in its environment to the ID of that client. That is
what makes per-client output possible from a single invocation:
~$ jay --json trace -o '| jq -r .inf > $JAY_CLIENT_ID' all
With -c, a single instance receives the traces of every client and
JAY_CLIENT_ID is not set:
~$ jay --json trace -o '| grep -F wl_pointer' -c all
Tip
Use single quotes around the argument.
$JAY_CLIENT_IDis expanded by the shell that Jay starts, not by the shell you type the command into.
Unless -c is used, tracing of a client stops when the command reading its
output exits.
JSONL Format
With the global --json flag the trace is emitted as JSONL – one JSON object
per line. Every object carries a t field naming the record type and a cl
field with the client ID. Unlike the query commands, trace records always
contain every field, so --all-json-fields has no effect.
{"t":"n", ...}- A client was attached to the trace. Carries an
infoobject describing it. {"t":"m", ...}- A Wayland message.
{"t":"x", ...}- Messages were dropped. Carries the number of dropped
messages in
n. {"t":"d", ...}- The client disconnected and was detached from the trace.
An attach record looks like this:
{
"t": "n",
"cl": 21,
"info": {
"id": 21,
"sandboxed": false,
"sandbox_engine": null,
"sandbox_app_id": null,
"sandbox_instance_id": null,
"uid": 1000,
"pid": 4711,
"is_xwayland": false,
"comm": "firefox",
"exe": "/usr/lib/firefox/firefox",
"tag": null,
"connect_time_us": 1786451740500000,
"now_us": 1786451740511460
}
}
Its id is the client ID and the rest of the fields are the ones jay clients
reports, plus two timestamps. sandbox_engine, sandbox_app_id,
sandbox_instance_id, and tag are null when unset. connect_time_us is
when the client connected and now_us is when the trace was attached, both in
microseconds since the Unix epoch.
A message record looks like this:
{
"t": "m",
"cl": 21,
"us": 1786451740511460,
"inf": "wl_display",
"id": 1,
"msg": "get_registry",
"args": {
"registry": 2
}
}
us- The time the compositor processed the message, in microseconds since the Unix epoch.
inf,id- The interface and object ID. The ID is unique unless
--raw-idswas passed. msg- The name of the message.
args- The arguments, keyed by parameter name. Object IDs and numeric arguments are
numbers, fixed-point arguments are numbers with a fractional part, booleans
are
trueorfalse, strings are strings, and arrays are arrays of numbers. A nullable string that is unset, and any file descriptor, isnull.
A dropped-message record carries the count in n:
{
"t": "x",
"cl": 21,
"n": 22
}
A detach record carries nothing beyond the client ID:
{
"t": "d",
"cl": 21
}
Note
The direction of a message is not part of the JSONL output – there is no equivalent of the
->arrow in the text format.
Because every record is a standalone JSON object, the stream composes with the usual tools. To watch a single interface live:
~$ jay --json trace select-window | jq -r 'select(.inf == "wl_pointer") | .msg'
Or to record a session and count messages by interface afterwards:
~$ jay --json trace -o traces -c all
~$ jq -r 'select(.t == "m") | .inf' traces.jsonl | sort | uniq -c | sort -rn