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
- 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”
First, let’s go through some of the mechanisms (ignoring Kua and Routes).
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
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.
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 == 'foo': if path_len > 1: fragment = path field_value = converters.convert(fragment) if field_value is not None: params['id'] = field_value if path_len == 2: return return_values 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.
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.
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.
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’
- 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.