rsvg-bench - a benchmark for librsvg

- Tags: gnome, librsvg, performance, rust

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

Bar chart of timings

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!

Rsvg-bench is available in Gnome's gitlab instance.