Pre-compositing terrain with Mapnik
Optimise your Postgres setup, they said. It’ll make your rendering faster, they said.
So I did. Over months. I followed every guide I could find. I now have more indexes than the Yellow Pages printing contract. I have set work_mem to somewhere in the region of 27 petabytes.
And yet my rendering was still dog-slow.
Generating tiles while watching the query log, I noticed that tile generation would still be slow (as in, many seconds slow) even when no or few slow queries were running. Something else was clearly taking up the time.
Unfortunately, Mapnik doesn’t have a style profiler. So I wrote one, or at least, a rough approximation of a basic one. It’s pretty simple:
- Generate an image once to warm up the Postgres cache.
- Generate that image five more times, and time the result.
- Remove a layer from the stylesheet.
- Generate the image five times with the new stylesheet, and time the result.
Step 2 minus step 4 is the amount of time that layer takes to render. Repeat with any layers you think might be slow, and you can identify which are the slow ones.
The result, for me, was pretty obvious: the raster terrain. cycle.travel’s hillshading composites three layers together: a colour relief layer, a hillshade layer, and a slope highlight layer. These three separate VRTs are composited together in Mapnik using its comp-op features and a few raster-opacity tweaks.
It turned out that these three layers were responsible for fully half the tile generation time. That shouldn’t be too surprising: comp-ops are slow, good raster scaling is slow, retrieving large images from spinning rust is slow. By compositing the three layers together in advance, and serving just one VRT, I should be able to cut the time taken by 0.5 * 0.6666 = one third.
I spent a while trying to do this with ImageMagick, which in theory should have been simple. ImageMagick also has good compositing options and it should be less trouble than arguing with Mapnik bindings. Unfortunately, despite hours of experimentation, I couldn’t get anything that approached the quality of the pure Mapnik results. A combination of filters that worked ok in the Alps was too whited-out in the UK; one that showed good detail in the UK looked too blurred in the Alps. I have a page full of notes such as “reasonably good but the hill angles are weird”.
So, back to Mapnik. The trick here would be to remove all layers except the hillshading ones, then generate tiles with all three layers composited together. Create a VRT from this, and replace the three layers with this single one.
All sounds fairly simple. There are two gotchas, of which the first is entirely predictable: Mapnik bindings. Mapnik has tolerable bindings for Python and Node, but otherwise you’re on your own. I prefer working in Ruby, for which the official Mapnik bindings are a disaster zone. There’s a better set at ffi-mapnik, which in turn has been extended to become simple_mapnik. On this particular server I had ffi-mapnik already installed, so I used that, but otherwise I’d choose simple_mapnik.
The second gotcha is that Mapnik writes TIFFs happily, but not GeoTIFFs. All the georeferencing would be lost on creating the new images. There are a couple of approaches to fix this, but I eventually found the easiest was gdalcopyproj, which just copies tags from one GeoTIFF to another. That meant looping through every existing GeoTIFF, creating a new image with the same boundaries, and then copying the tags across.
And… it all works. Rendering is now noticeably faster.
This is all code to solve my particular problem rather than anything I wrote to be particularly reusable.
The profiler should be pretty easy to use elsewhere. Throw it the stylesheet name, a comma-separated list of layers to remove, and a bounding box, and it’ll do the rest. The compositor is more specific to my particular needs, but might serve as a basic example of using Mapnik from Ruby. And, as ever, you can play with the results over at cycle.travel. I took the opportunity to make the hillshade a little lighter, so until the older tiles expire there’ll be occasional mismatches, but overall it’s similar results – just faster.
Posted on Sunday 15 December 2019. Link.