If you have read anything I have written about routing, you will know that I am somewhat frustrated by Sanic’s router for some time now. I have been itching to replace it. The problem is how? There are a few solutions out there.
@vltr went through a great exercise a couple years ago to benchmark various solutions: https://github.com/vltr/very-stupid-simple-routers-bench
TLDR:
- Falcon is fast
- xrtr is fast
- Kua is not so impressive
- Routes … No offense to the contributors, but I don’t understand by what basis they call it “Speedy”
Review
First, let’s go through some of the mechanisms (ignoring Kua and Routes).
Sanic
Sanic’s router works by regex
. Basically it groups routes into a few buckets (example, do we need to pull params out of the path?), chooses the right bucket of routes, then loops through them stopping on a regex
match.
This provides for a somewhat varied performance. Regular expressions have had a history of performance issues in Python. It has, to be fair, improved in recent Python releases. That aside, routes that are defined early will be faster to match than those defined later. More routes leads to performance decrease.
Perhaps one of my biggest complaints is that it needs to be called repeatedly in Sanic, and leads to a low level of flexibility.
One thing that it does to well, is make use of lru_cache
so that when it sees the same URL again, it does not need to reevaluate.
Falcon
The strategy employed by Falcon is interesting. Basically at run time it analyzes all the routes that have been added and generates a dynamic function (somewhat AST-like) that looks something like this:
def find(path, return_values, patterns, converters, params):
path_len = len(path)
if path_len > 0:
if path[0] == 'foo':
if path_len > 1:
fragment = path[1]
field_value = converters[0].convert(fragment)
if field_value is not None:
params['id'] = field_value
if path_len == 2:
return return_values[0]
return None
return None
return None
return None
Basically it is just a bunch of if
statements. It is very efficient and very fast. It does slow down a bit when you need to start adding type conversions, and when doing multiple calls. It also does not appear to be capable of handling domain or header based routing.
xrtr
This is a package that has been a work in progress by @vltr. The idea was to build a router using Cython. It is fast and flexible (it can handle routing of more than just URLs). It outperforms every other implementation, and can be a solid choice where appropriate.
hyprtr
This is a project that I started working on a few years ago to leverage hyperscan
(the ridiculously fast C based regex engine). It is fully compatible with the styling and type conversion of the existing Sanic router, which none of the other solutions are. It works out of the box as a drop in replacement for Sanic.
It also is as fast as xrtr
. But, because it uses hyperscan
, it is not Windows compatible. It will remain for now as an unreleased repo on my machine.
Solution
In my recent post on RFC #1630, I am working on the signal implementation. One thing that I am working on requires signals to be routed to handlers. The existing Sanic router cannot handle this. So, I came back to the idea of perhaps building a new router that could do multiple types of routing.
With the urging of @vltr, I went back to the approach by Falcon and used it as inspiration. The idea I came up with:
- Handles static routes (no params in URL) faster than any other solution out there
- Is pure python
- Can route based upon headers, domains, or ANYTHING else you want
- Does not slow down for type conversion
- Does not slow down based upon size and number of endpoints’
- Leverages
lru_cache
- Fully compatible with Sanic as a drop in replacement with fairly minimal integration
Here is a benchmark I did.
New AST Router executed (new_router) for 50000 times in 0.016282 seconds
New AST Router best time was 0.000000 seconds
hyprtr Router executed (hyprtr_router) for 50000 times in 0.032355 seconds
hyprtr Router best time was 0.000001 seconds
xrtr Router executed (xrtr_router) for 50000 times in 0.038492 seconds
xrtr Router best time was 0.000001 seconds
Sanic Router executed (sanic_router) for 50000 times in 0.056148 seconds
Sanic Router best time was 0.000001 seconds
Falcon Router executed (falcon_router) for 50000 times in 0.148959 seconds
Falcon Router best time was 0.000003 seconds
What is telling about this test is that:
- There are 100 routes per router.
- They are testing against a dynamic route (need to grab params from route, therefore slower)
- Type conversion, so that the params are cast to a specific type
- Multiple calls to the router per test
I am thinking that I will create a repo on GitHub/huge-success
to play around with it. But, I am getting excited that we may have a direction for replacing the existing Sanic router, and a tool for building out the signals API.
Name suggestions?