Debugging
One of the simplest ways to debug Servo is to print interesting variables with the println!
, eprintln!
, or dbg!
macros.
In general, these should only be used temporarily; you’ll need to remove them or convert them to proper debug logging before your pull request will be merged.
Debug logging with log
and RUST_LOG
Servo uses the log
crate for long-term debug logging and error messages:
#![allow(unused)] fn main() { log::error!("hello"); log::warn!("hello"); log::info!("hello"); log::debug!("hello"); log::trace!("hello"); }
Unlike macros like println!
, log
adds a timestamp and tells you where the message came from:
[2024-05-01T09:07:42Z ERROR servoshell::app] hello
[2024-05-01T09:07:42Z WARN servoshell::app] hello
[2024-05-01T09:07:42Z INFO servoshell::app] hello
[2024-05-01T09:07:42Z DEBUG servoshell::app] hello
[2024-05-01T09:07:42Z TRACE servoshell::app] hello
You can use RUST_LOG
to filter the output of log
by level (off
, error
, warn
, info
, debug
, trace
) and/or by where the message came from, also known as the “target”.
Usually the target is a Rust module path like servoshell::app
, but there are some special targets too (see § Event tracing).
To set RUST_LOG
, prepend it to your command or use export
:
$ RUST_LOG=warn ./mach run -d test.html # Uses the prepended RUST_LOG.
$ export RUST_LOG=warn
$ ./mach run -d test.html # Uses the exported RUST_LOG.
See the env_logger
docs for more details, but here are some examples:
- to enable all messages up to and including
debug
level, but nottrace
:RUST_LOG=debug
- to enable all messages from
servo::*
,servoshell::*
, or any target starting withservo
:RUST_LOG=servo=trace
(or justRUST_LOG=servo
) - to enable all messages from any target starting with
style
, but onlyerror
andwarn
messages fromstyle::rule_tree
:RUST_LOG=style,style::rule_tree=warn
Note that even when a log message is filtered out, it can still impact runtime performance, albeit only slightly.
Some builds of Servo, including official nightly releases, remove DEBUG and TRACE messages at compile time, so enabling them with RUST_LOG
will have no effect.
Event tracing
In the constellation, the compositor, and servoshell, we log messages sent to and received from other components, using targets of the form component>other@Event
or component<other@Event
.
This means you can select which event types to log at runtime with RUST_LOG
!
For example, in the constellation (more details):
- to trace only events from script:
RUST_LOG='constellation<=off,constellation<script@'
- to trace all events except for ReadyToPresent events:
RUST_LOG='constellation<,constellation<compositor@ReadyToPresent=off'
- to trace only script InitiateNavigateRequest events:
RUST_LOG='constellation<=off,constellation<script@InitiateNavigateRequest'
In the compositor (more details):
- to trace only MoveResizeWebView events:
RUST_LOG='compositor<constellation@MoveResizeWebView'
- to trace all events except for Forwarded events:
RUST_LOG=compositor<,compositor<constellation@Forwarded=off
In servoshell (more details):
- to trace only events from servo:
RUST_LOG='servoshell<=off,servoshell>=off,servoshell<servo@'
- to trace all events except for AxisMotion events:
RUST_LOG='servoshell<,servoshell>,servoshell<winit@WindowEvent(AxisMotion)=off'
- to trace only winit window moved events:
RUST_LOG='servoshell<=off,servoshell>=off,servoshell<winit@WindowEvent(Moved)'
Event tracing can generate an unwieldy amount of output. In general, we recommend the following config to keep things usable:
constellation<,constellation>,constellation<compositor@ForwardEvent(MouseMoveEvent)=off,constellation<compositor@LogEntry=off,constellation<compositor@ReadyToPresent=off,constellation<script@LogEntry=off
compositor<,compositor>
servoshell<,servoshell>,servoshell<winit@DeviceEvent=off,servoshell<winit@MainEventsCleared=off,servoshell<winit@NewEvents(WaitCancelled)=off,servoshell<winit@RedrawEventsCleared=off,servoshell<winit@RedrawRequested=off,servoshell<winit@UserEvent(WakerEvent)=off,servoshell<winit@WindowEvent(CursorMoved)=off,servoshell<winit@WindowEvent(AxisMotion)=off,servoshell<servo@EventDelivered=off,servoshell<servo@ReadyToPresent=off,servoshell>servo@Idle=off,servoshell>servo@MouseWindowMoveEventClass=off
Other debug logging
mach run
does this automatically, but to print a backtrace when Servo panics:
$ RUST_BACKTRACE=1 target/debug/servo test.html
Use -Z
(-- --debug
) to enable debug options.
For example, to print the stacking context tree after each layout, or get help about these options:
$ ./mach run -Z dump-stacking-context-tree test.html
$ ./mach run -Z help # Lists available debug options.
$ ./mach run -- --debug help # Same as above: lists available debug options.
$ ./mach run --debug # Not the same! This just chooses target/debug.
On macOS, you can also add some Cocoa-specific debug options, after an extra --
:
$ ./mach run -- test.html -- -NSShowAllViews YES
Running servoshell with a debugger
To run servoshell with a debugger, use --debugger-cmd
.
Note that if you choose gdb
or lldb
, we automatically use rust-gdb
and rust-lldb
.
$ ./mach run --debugger-cmd=gdb test.html # Same as `--debugger-cmd=rust-gdb`.
$ ./mach run --debugger-cmd=lldb test.html # Same as `--debugger-cmd=rust-lldb`.
To pass extra options to the debugger, you’ll need to run the debugger yourself:
$ ./mach run --debugger-cmd=gdb -ex=r test.html # Passes `-ex=r` to servoshell.
$ rust-gdb -ex=r --args target/debug/servo test.html # Passes `-ex=r` to gdb.
$ ./mach run --debugger-cmd=lldb -o r test.html # Passes `-o r` to servoshell.
$ rust-lldb -o r -- target/debug/servo test.html # Passes `-o r` to lldb.
$ ./mach run --debugger-cmd=rr -M test.html # Passes `-M` to servoshell.
$ rr record -M target/debug/servo test.html # Passes `-M` to rr.
Many debuggers need extra options to separate servoshell’s arguments from their own options, and --debugger-cmd
will pass those options automatically for a few debuggers, including gdb
and lldb
.
For other debuggers, --debugger-cmd
will only work if the debugger needs no extra options:
$ ./mach run --debugger-cmd=rr test.html # Good, because it’s...
# servoshell arguments ^^^^^^^^^
$ rr target/debug/servo test.html # equivalent to this.
# servoshell arguments ^^^^^^^^^
$ ./mach run --debugger-cmd=renderdoccmd capture test.html # Bad, because it’s...
# renderdoccmd arguments? ^^^^^^^
# servoshell arguments ^^^^^^^^^
$ renderdoccmd target/debug/servo capture test.html # equivalent to this.
# => target/debug/servo is not a valid command.
$ renderdoccmd capture target/debug/servo test.html # Good.
# ^^^^^^^ renderdoccmd arguments
# servoshell arguments ^^^^^^^^^
Debugging with gdb or lldb
To search for a function by name or regex:
(lldb) image lookup -r -n <name>
(gdb) info functions <name>
To list the running threads:
(lldb) thread list
(lldb) info threads
Other commands for gdb or lldb include:
(gdb) b a_servo_function # Add a breakpoint.
(gdb) run # Run until breakpoint is reached.
(gdb) bt # Print backtrace.
(gdb) frame n # Choose the stack frame by its number in `bt`.
(gdb) next # Run one line of code, stepping over function calls.
(gdb) step # Run one line of code, stepping into function calls.
(gdb) print varname # Print a variable in the current scope.
See this gdb tutorial or this lldb tutorial more details.
To inspect variables in lldb, you can also type gui
, then use the arrow keys to expand variables:
(lldb) gui
┌──<Variables>───────────────────────────────────────────────────────────────────────────┐
│ ◆─(&mut gfx::paint_task::PaintTask<Box<CompositorProxy>>) self = 0x000070000163a5b0 │
│ ├─◆─(msg::constellation_msg::PipelineId) id │
│ ├─◆─(url::Url) _url │
│ │ ├─◆─(collections::string::String) scheme │
│ │ │ └─◆─(collections::vec::Vec<u8>) vec │
│ │ ├─◆─(url::SchemeData) scheme_data │
│ │ ├─◆─(core::option::Option<collections::string::String>) query │
│ │ └─◆─(core::option::Option<collections::string::String>) fragment │
│ ├─◆─(std::sync::mpsc::Receiver<gfx::paint_task::LayoutToPaintMsg>) layout_to_paint_port│
│ ├─◆─(std::sync::mpsc::Receiver<gfx::paint_task::ChromeToPaintMsg>) chrome_to_paint_port│
└────────────────────────────────────────────────────────────────────────────────────────┘
If lldb crashes on certain lines involving the profile()
function, it’s not just you.
Comment out the profiling code, and only keep the inner function, and that should do it.
Reversible debugging with rr (Linux only)
rr is like gdb, but lets you rewind. Start by running servoshell via rr:
$ ./mach run --debugger=rr test.html # Either this...
$ rr target/debug/servo test.html # ...or this.
Then replay the trace, using gdb commands or rr commands:
$ rr replay
(rr) continue
(rr) reverse-cont
To run one or more tests repeatedly until the result is unexpected:
$ ./mach test-wpt --chaos path/to/test [path/to/test ...]
Traces recorded by rr can take up a lot of space.
To delete them, go to ~/.local/share/rr
.
OpenGL debugging with RenderDoc (Linux or Windows only)
RenderDoc lets you debug Servo’s OpenGL activity. Start by running servoshell via renderdoccmd:
$ renderdoccmd capture -d . target/debug/servo test.html
While servoshell is running, run qrenderdoc
, then choose File > Attach to Running Instance.
Once attached, you can press F12 or Print Screen to capture a frame.