Updating the Tile Cannon

Imgur

Ah, my trusty tile cannon. It has been blasting tiles at screens for Mecklenburg since 2012 with such reliability and blazing performance that I sometimes write its uptime stats on a rolled newspaper and use it to beat my Esri-using colleagues when AGS goes down.

Naturally, I had to rewrite it.

The Problem

There wasn’t one. But there were some things that could be better.

  • I thought when referencing your MBTiles file name in the route, it would be nice if you didn’t have to put “.mbtiles” in the URL, and that I’d staple it on at the server, you’re welcome. I can’t tell you how many times people face planted on this. Lesson: obviousness trumps everything.
  • You had to stick the file extension on the end of the request. I used that to set response headers and whatnot. Face plants galore. A lot of new MBTiles users don’t know what is coming out of the thing, and they shouldn’t have to. I should have inferred that from the MBTiles output and set headers accordingly.
  • I wanted to remove the @mapbox/mbtiles dependency. It often points at old SQLite3 versions, which would require compilation on newer Node versions, and…face plants. Plus performing a SQLite query isn’t something I need to abstract.
  • I didn’t have a route to either list or describe the MBTiles files siting on the server, which is a problem if the user isn’t the one posting files.
  • More faster. Because why not.

The Solution

The new bits are up on Github. Although the tile route has changed slightly - you can now list the database with the .mbtiles extension and you no longer add the extension for the file output - the old way works too. This is to mitigate the aforementioned face plants without breaking current apps.

So now you can do this:

1
http://localhost:3000/tiles/12/1128/1620.png

or this:

1
http://localhost:3000/tiles.mbtiles/12/1128/1620

There are also additional routes to list the .mbtiles files available (ex: http://localhost:3000/list) and to show the metadata for a tile set (ex: http://localhost:3000/tiles.mbtiles/meta).

All in 80 lines of code, a few less than the last iteration. Working with a project where the code base is small enough to jam the entire thing in your head at one time is a rare treat.

About the Faster Thing

I was able to ramp up the performance a good bit over the last iteration. I did a lot of spaghetti throwing here, but it mostly came down to two things, in order of importance: going straight at SQLite, and switching from Express to Fastify.

Going straight at SQLite rather than through node-mbtiles sped things up a measurable amount, probably by removing some overhead (node-mbtiles does a lot of stuff I’m not doing). But the biggest and most significant change was using SQLite3’s caching.

node-sqlite3 has a built-in database object cache to avoid opening the same database multiple times. To use the caching feature, simply use new sqlite3.cached.Database() instead of new sqlite3.Database().

Yes please.

Fastify is ~2x as fast as Express if you believe in benchmarks, but I doubt Express was much of a bottleneck in this scenario. So…maybe I picked up a little here? I like Fastify anyway, and it does have fast in the name.

Speaking of benchmarks…

TL;DR

I’m calling it ~40% faster.

On my Ryzen 1600 (first gen, shut up), the old server handed 10,000 requests, 50 concurrently, in 3.413 seconds, with a mean response time of 17.063ms and a max response time of 163ms. As you can see from the chart, there’s a fair bit of variability, with a ~150ms difference from mean to high.

The new server handled that load in 2.044 seconds, with a mean response time of 10.222ms and a max response time of 61ms. It’s also a flatter response curve, with a 51ms difference from mean to high.

Old Cannon

Imgur

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Document Length:        56003 bytes
Concurrency Level: 50
Time taken for tests: 3.413 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 563790000 bytes
HTML transferred: 560030000 bytes
Requests per second: 2930.39 [#/sec] (mean)
Time per request: 17.063 [ms] (mean)
Time per request: 0.341 [ms] (mean, across all concurrent requests)
Transfer rate: 161340.20 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 6 17 12.5 14 163
Waiting: 6 14 10.9 12 162
Total: 6 17 12.5 14 163

Percentage of the requests served within a certain time (ms)
50% 14
66% 16
75% 17
80% 18
90% 21
95% 29
98% 52
99% 65
100% 163 (longest request)

New Cannon

Imgur

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Document Length:        56003 bytes
Concurrency Level: 50
Time taken for tests: 2.044 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 562350000 bytes
HTML transferred: 560030000 bytes
Requests per second: 4891.25 [#/sec] (mean)
Time per request: 10.222 [ms] (mean)
Time per request: 0.204 [ms] (mean, across all concurrent requests)
Transfer rate: 268612.63 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.2 0 2
Processing: 4 10 4.7 9 60
Waiting: 4 7 4.0 6 57
Total: 4 10 4.8 9 61

Percentage of the requests served within a certain time (ms)
50% 9
66% 10
75% 11
80% 12
90% 15
95% 16
98% 23
99% 24
100% 61 (longest request)

Have at it

The new bits are up on Github. File an issue if something terrible happens.

I’d be remiss if I didn’t credit Chris Helms for the original mbtiles-server project, from whence my fork sprang, and without which I’d have been lost in a sea of swearing. Please spray any praise for my little tile cannon in a wide enough arc that some of it ends up on his shirt.