blob: 22fdf8545aaf726f7845eedd974bee93f03c8311 [file] [view] [edit]
# webkit-sysprof
A command-line tool for processing [Sysprof](https://gitlab.gnome.org/GNOME/sysprof)
`.syscap` capture files recorded from WebKit (GTK/WPE ports). It extracts marks
(timeline events) and counters (time-series metrics) from a capture and lets you dump,
summarize, analyze (rendering/frame-timing statistics), or plot delta-time histograms for
them.
## Installation
```
pip install -e .
```
This installs the `webkit-sysprof` command.
## Commands
### `dump`
Dump marks, counters, or collapsed stack traces from a capture as CSV or JSON.
```
webkit-sysprof dump [--marks|--counters|--collapsed-stacktraces] [-f csv|json] CAPTURE_FILE
```
`--marks` is the default when no flag is given. `-f`/`--format`: `csv` (default) or
`json`. `json` prints the same rows as `csv`, just as a JSON array of objects. `group`
names the kind of process a mark came from, e.g. `WebKit (Web)`, and `pid` the process
itself, which two of one kind share a group name for.
```
$ webkit-sysprof dump capture.syscap
group;pid;name;message;time;duration;end_time
...
$ webkit-sysprof dump --counters capture.syscap
category;name;description;time;offset;value
...
$ webkit-sysprof dump -f json capture.syscap
[{"group": "...", "pid": ..., "name": "...", "message": "...", "time": ..., "duration": ..., "end_time": ...}, ...]
```
#### `--collapsed-stacktraces`
Dump the capture's stack samples in the folded-stack format that
[FlameGraph](https://github.com/brendangregg/FlameGraph)'s `flamegraph.pl` reads
directly, and that matches what its own `stackcollapse-perf.pl` produces from `perf
script` output: one line per distinct stack, semicolon-separated from the process down
to the innermost frame, followed by a space and how many samples had exactly that
stack. `-f json` gives the same rows as a JSON array of `{"stack": ..., "count": ...}`
objects instead.
```
$ webkit-sysprof dump --collapsed-stacktraces capture.syscap
WPEWebProcess;my_thread_main;WebCore::TextureMapperLayer::paint;memcpy+0x30 3
```
The output in the above format may be very useful for AI agents to digest the profiling
data gathered by sysprof.
### `summary`
Print a summary of a capture: document metadata, mark counts by name, and counter counts
by category/name.
```
webkit-sysprof summary CAPTURE_FILE
```
```
$ webkit-sysprof summary capture.syscap
File: capture.syscap
Title: capture.syscap
Subtitle: Recording at 14:00:04 04/27/26
Timespan: 0.0000 - 4.4673 [s]
Marks: 669
AcquireTexture: 80
...
Counters: 53
CPU Frequency / CPU 0: 23 values
...
```
### `analyze`
Compute rendering statistics (vblank intervals, theoretical FPS, frame compositions per
vblank), a frame cycle breakdown, and duration/count statistics for a fixed set of
marks (styling, layout, rendering, compositing and tiles).
```
webkit-sysprof analyze CAPTURE_FILE [-f text|json] [-t TIMESPAN] [-e]
```
- `-f`/`--format`: `text` (default) or `json`.
- `-t`/`--timespan`: restrict the analysis to a window in milliseconds relative to the
capture start, e.g. `0-5000`. Either side may be omitted (`500-` or `500` mean "from
500ms to the end", `-500` means "from the start to 500ms"). Defaults to the whole
capture (`-`), and is clamped to it.
- `-e`/`--explain`: interleave the text report with paragraphs explaining how to read
it, in particular what theoretical FPS and the frame cycle phases do and do not
measure.
```
$ webkit-sysprof analyze capture.syscap
Timespan: 0.0000 - 4.4673 [s]
Duration: 4.4673 [s]
Rendering:
- vblanks: 35
...
$ webkit-sysprof analyze -f json -t 0-2000 capture.syscap
{"document": {...}, "statistics": {...}, "rendering": {...}, "frame_cycle": {...}}
```
Restricting the timespan matters more than it looks: page load dominates the tail of
most metrics, so a whole-capture run largely measures startup. Skipping the first
seconds is usually what you want when comparing configurations.
#### Theoretical FPS
`frames rendered` counts `DidRenderFrame` marks of every process (one per composition,
emitted once it is painted and before the frame is handed over) and
`theoretical FPS` divides that by the analyzed duration. `analyze --explain` prints
what that does and does not say, and how it relates to the display refresh rate.
Note that `vblanks per LayerTreeHostRenderingUpdate` (and its `more than 1 per update`
count) only covers the `LayerTreeHostRenderingUpdate` mark. A rendering update that fits
inside one vblank interval can still miss frames, because compositing happens after it.
Use the frame cycle for the complete per-frame budget.
#### Frame cycle
A frame cycle is one turn of the engine's rendering loop, split into the phases the
frame's time went into. `analyze --explain` prints what a cycle is, which mark
delimits each phase and how to read every number of the section.
#### Reading the mark statistics
Three marks measure something other than what their name suggests:
- `StyleRecalc` is an umbrella mark: `RenderTreeBuild`, the `CompositingUpdate` that
follows a style change and, where style resolution interleaves layout, the layout
marks all run inside it, so its duration is not style resolution alone.
- `CompositingUpdate` is emitted wherever the compositing layers are updated, after a
style change, after layout and on scrolling, so its samples mix runs of very
different lengths and its percentiles are not centered on a typical value.
- `PaintTile` runs on the painting threads, so summing it over a frame gives aggregate
work rather than elapsed time. Its `#pixels` statistic is the dirty area taken from
the mark's `dirty region <x>x<y>+<width>+<height>` message.
Percentiles (P25, P75 and P99) are interpolated between the samples, so they lie
between the reported minimum and maximum, and are left out below two samples.
`analyze --explain` prints the long form of all of this in the report itself, next to
the table it applies to.
### `cycle-analysis`
Draw every frame cycle as a bar, one cell per slice of time, colored by whatever the
engine is doing at that moment. Where `analyze` reports the median cycle, this shows the
individual ones, which is how a cycle stalled by layout is told apart from one stalled
by a long timer or a slow `requestAnimationFrame` callback.
```
webkit-sysprof cycle-analysis CAPTURE_FILE [-t TIMESPAN] [-n MAX_CYCLES]
[-r RESOLUTION] [--order first|slowest]
[--min-duration MS] [--min-length MS]
[--max-cells CELLS] [--no-tile-lane]
[--color auto|always|never]
```
- `-n`/`--max-cycles`: how many cycles to draw (default 40).
- `-r`/`--resolution`: milliseconds per cell (default 0.2).
- `--order`: `first` draws the cycles in capture order, `slowest` the worst ones first,
which is usually what you want when hunting dropped frames.
- `--min-duration`: skip cycles shorter than this, another way to isolate bad frames.
- `--min-length`: shortest bar to draw, in milliseconds (default 16). Bars are padded to
at least this length so the refresh marker stays visible, and the padding is drawn
blank because it is not part of the cycle.
- `--max-cells`: a cycle longer than this many cells is truncated and marked with `»`
(default 400). Only the cycle counts: a short bar whose padding did not fit is drawn
whole and is not marked.
- `--no-tile-lane`: drop the `tiles` lane.
- `--color`: `auto` colorizes only when writing to a terminal. Without color each cell
becomes a letter instead of a colored block, and the legend maps both.
```
$ webkit-sysprof cycle-analysis capture.syscap -t 30000-30200 -n 2 -r 0.5
0 2 4 6 8 10 12 14 16 18 2
┼─┴─┼─┴─┼─┴─┼─┴─┼─┴─┼─┴─┼─┴─┼─┴─┼─┴─┼─┴─┼ [ms]
#1 20.40 ms main ▕uuRRRRRRSSSSBBLL........UUUffPPPP│PPPPwww
tiles ▕.................2344432.........│.......
#2 20.40 ms main ▕uuRRRRRRSSSSBBLL........UUUffPPPP│PPPPwww
tiles ▕.................2344432.........│.......
```
Reading the first bar: the rendering update opens (`u`), then `requestAnimationFrame`
callbacks (`R`), style resolution (`S`), render tree build (`B`) and layout (`L`),
an idle stretch during which the lower lane shows two to four tiles painting in
parallel, then the tile upload (`U`), the flush of compositing state (`f`), the paint
(`P`) and the wait for the composition to complete (`w`). This cycle crosses the refresh
marker, so it cost a frame.
The `main` lane holds the marks of the process that rendered the cycle, which for all
but the timer marks is its main thread. `EventLoopRun`, `WebCoreTimerExecution` and
`WebCoreThreadTimers` are emitted by any thread running a run loop, and a capture names
the process a mark came from but not the thread, so those three cannot be told apart
from main-thread work. The innermost mark covering a cell wins, so nested marks are
drawn over their parents and the bar reads as the real call structure rather than as
flat top-level phases.
The `tiles` lane is **not** the main thread and does not show an activity. It counts how
many tiles are being rasterized at that instant on the painting threads, so `3` means
three `PaintTile` marks overlap there. Painting is asynchronous, which is why this lane
comes alive where the `main` lane goes idle: the main thread has handed the tiles off
and is waiting for them before it can composite. It answers what the main lane cannot,
namely whether an idle stretch is the engine waiting on parallel work or doing nothing.
A vertical line crosses all bars at each multiple of the refresh interval. A cycle that
reaches past the first line took longer than one display refresh, so it cost a frame. The
line replaces the cell it is drawn in, so where a refresh interval is only a few cells
wide, the header says so and no line is drawn rather than most of the lane being markers.
A cycle only ever runs between two rendering updates of one process, so where more than
one rendered, each bar names the process it belongs to and is drawn from that process's
marks alone.
The legend lists the activities present, the average time each took per drawn cycle, and
the marks it is drawn from. The average covers the whole of every cycle drawn, including
whatever part of a truncated one did not fit in its bar. Several activities aggregate more than one mark, so that
list is what says exactly what a color covers. It also makes clear that `idle` is the
absence of any mark rather than a mark of its own.
### `delta-histogram`
Plot a histogram of the time between consecutive occurrences of a given mark (e.g. to
visualize `DisplayLinkUpdate` jitter).
```
webkit-sysprof delta-histogram CAPTURE_FILE MARK_TYPE [-t TIMESPAN]
```
`-t`/`--timespan` behaves exactly as in `analyze`.
```
$ webkit-sysprof delta-histogram capture.syscap DisplayLinkUpdate
$ webkit-sysprof delta-histogram capture.syscap DisplayLinkUpdate -t 0-2000
```
Opens a matplotlib window with a linear-scale and a log-scale histogram of the deltas
(in milliseconds), annotated with mean/median/min/max lines. If the mark occurs fewer
than twice in the (optionally trimmed) capture, it prints `No data available for mark:
<MARK_TYPE>` instead.
## Development
Run tests:
```
Tools/Scripts/test-webkitpy webkitsysprof
```