Librsvg 2.42.0 came out with a rather major performance regression
compared to 2.40.20: SVGs with many transform
attributes would slow it down. It was fixed in 2.42.1. We changed
from using a parser that would recompile regexes each time it was
called, to one that does simple string-based matching and
parsing.
When I rewrote librsvg's parser for the transform
attribute from C
to Rust, I was just learning about writing parsers in Rust.
I chose lalrpop, an excellent, Yacc-like parser generator for Rust.
It generates big, fast parsers, like what you would need for a
compiler — but it compiles the tokenizer's regexes each time you call
the parser. This is not a problem for a compiler, where you basically
call the parser only once, but in librsvg, we may call it thousands of
times for an SVG file with thousands of objects with transform
attributes.
So, for 2.42.1 I rewrote that parser using rust-cssparser. This is what Servo uses to parse CSS data; it's a simple tokenizer with an API that knows about CSS's particular constructs. This is exactly the kind of data that librsvg cares about. Today all of librsvg's internal parsers work using rust-cssparser, or they are so simple that they can be done with Rust's normal functions to split strings and such.
Getting good timings
Librsvg ships with rsvg-convert
, a command-line utility that can
render an SVG file and write the output to a PNG. While it would be
possible to get timings for SVG rendering by timing how long
rsvg-convert
takes to run, it's a bit clunky for that. The process
startup adds noise to the timings, and it only handles one file at a
time.
So, I've written rsvg-bench, a small utility to get timings out of librsvg. I wanted a tool that:
-
Is able to process many SVG images with a single command. For example, this lets us answer a question like, "how long does version N of librsvg take to render a directory full of SVG icons?" — which is important for the performance of an application chooser.
-
Is able to repeatedly process SVG files, for example, "render this SVG 1000 times in a row". This is useful to get accurate timings, as a single render may only take a few microseconds and may be hard to measure. It also helps with running profilers, as they will be able to get more useful samples if the SVG rendering process runs repeatedly for a long time.
-
Exercises librsvg's major code paths for parsing and rendering separately. For example, librsvg uses different parts of the XML parser depending on whether it is being pushed data, vs. being asked to pull data from a stream. Also, we may only want to benchmark the parser but not the renderer; or we may want to parse SVGs only once but render them many times after that.
-
Is aware of librsvg's peculiarities, such as the extra pass to convert a Cairo image surface to a GdkPixbuf when one uses the convenience function
rsvg_handle_get_pixbuf()
.
Currently rsvg-bench supports all of that.
An initial benchmark
I ran this
/usr/bin/time rsvg-bench -p 1 -r 1 /usr/share/icons
to cause every SVG icon in /usr/share/icons
to be parsed once, and
rendered once (i.e. just render every file sequentially). I did this
for librsvg 2.40.20 (C only), and 2.42.{0, 1, 2} (C and Rust). There
are 5522 SVG files in there. The timings look like this:
version | time (sec) |
---|---|
2.40.20 | 95.54 |
2.42.0 | 209.50 |
2.42.1 | 97.18 |
2.42.2 | 95.89 |
So, 2.42.0 was over twice as slow as the C-only version, due to the parsing problems. But now, 2.42.2 is practically just as fast as the C only version. What made this possible?
- 2.40.20 - the old C-only version
- 2.42.0 - C + Rust, with a lalrpop parser for the
transform
attribute - 2.42.1 - Servo's cssparser for the
transform
attribute - 2.42.2 - removed most C-to-Rust string copies during parsing
I have started taking profiles of rsvg-bench runs with sysprof, and there are some improvements worth making. Expect news soon!