DEV Community

Cover image for Teeny, a route system for PHP

Teeny, a route system for PHP

Guilherme Nascimento on February 03, 2021

Teeny is a micro-route system that is really micro, supports PHP 5.3 to PHP 8, is extremely simple and ready to use. For create your project use: ...
Collapse
 
hbgl profile image
hbgl

I am a little disappointed that you used the naive implementation of sequentially matching regular expressions for routes with parameters.

Collapse
 
brcontainer profile image
Guilherme Nascimento

I am very grateful for your feedback. Any suggestions are welcome, if you have an idea to improve this I am willing to implement it.

Collapse
 
hbgl profile image
hbgl • Edited

Sure. Not too long ago, I was looking into route matching solutions for PHP and found nikic/FastRoute. The basic idea behind FastRoute is that you do not match a path against each pattern one by one, but instead you combine all patterns and match in one shot. By bundling the patterns, you provide more information to the regex engine, which, in theory, can lead to better performance.

Then the question becomes, how do combine the patterns? In FastRoute, a path is matched against chunks of patterns with 10 to 30 patterns per chunk. It is a good solution, but it is ultimately a workaround for the limitations of the underlying PCRE engine. This blog post explains the implementation in greater detail.

So next, I was looking for a regex engine with first class support for testing against multiple patterns. I found Hyperscan which is a "high-performance multiple regex matching library" developed by Intel. It is a C++ library with a C API, so one could write a PHP extension that exposes PHP bindings for Hyperscan.

You could of course implement your own solution with a radix tree or another algorithm instead of using a regex engine.

Thread Thread
 
haruanm profile image
Haruan Justino

I understand the suggestion, seems to be interesting, at the same time, how much performance it would improve?
I feel that this solution would make the debugging complex and wouldn't meaningfully improve the performance.

Thread Thread
 
brcontainer profile image
Guilherme Nascimento • Edited

Thanks for comment! Apparently you are correct, at least in the initial tests I did, using ApacheBench. Perhaps the complexity of taking advantage of a configuration was more costly than a simple sequential implementation. Tested with Apache2.4 + PHP7.4 + 16GB RAM + Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz + M.2 (SSD):

FastRoute:

  • ab -n 1000 -c 10 http://localhost/fastroute/articles/12345/foobar results: Requests per second: 1473.39
  • Memory usage (peak after register shutdown): 0.487Mb
<?php
require 'vendor/autoload.php';

$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
    $r->addRoute('GET', '/fastroute/users', 'hello_world');
    $r->addRoute('GET', '/fastroute/user/{id:\d+}', 'hello_world');
    $r->addRoute('GET', '/fastroute/articles', 'hello_world');
    $r->addRoute('GET', '/fastroute/articles/{id:\d+}/{title}', 'hello_world');
});
Enter fullscreen mode Exit fullscreen mode

Teeny:

  • ab -n 1000 -c 10 http://localhost/teeny/articles/12345/foobar results: Requests per second: 2506.86
  • Memory usage (peak after register shutdown): 0.388Mb
<?php
require_once 'vendor/teeny.php';
require_once 'vendor/autoload.php';

$app = new \Inphinit\Teeny;

$app->action('GET', '/users', 'hello_world');
$app->action('GET', '/user/{id:num}', 'hello_world');
$app->action('GET', '/articles', 'hello_world');
$app->action('GET', '/articles/<id:num>/<title>', 'hello_world');
Enter fullscreen mode Exit fullscreen mode

More requests per second is better.

Thread Thread
 
hbgl profile image
hbgl

The performance impact depends heavily on your application. If you only have a couple of routes, then it probably doesn't matter which router you use. I did some testing with around 300 routes. The difference between a sequentially scanning router like Symfony Routing and an batch scanning router like FastRoute is noticeable.

Here is the setup that I used for benchmarking: github.com/hbgl/php-routing-bench

Thread Thread
 
brcontainer profile image
Guilherme Nascimento • Edited

I did a few tests, but from what I noticed (so far), while the average is 20 regex (routes) grouped, it really is more efficient, however when it passes that or results inverts, the grouped regex is usually twice the time compared to separate regexs. So in terms of cost benefit for now it is better to keep it as it is, because grouping will only improve the performance of things that are already technically fast (where there are few routes) and where there are many routes it will get worse (this is due to the tests I managed to do) . It doesn't mean that I may not be able to reach a more efficient result in the future, I will work for that. Anyway, one thing I noticed that can be improved is to separate the routes "without regex" from those "with regex". Grateful for the links and suggestions.

Update: dev.to/brcontainer/improving-the-r...

Thread Thread
 
pierstoval profile image
Alex Rock

As of this PR, Symfony is way faster than FastRoute when using a compiled version of the UrlMatcher. As a reminder, compiling the UrlMatcher is the default and recommended way of using the Symfony Routing component, because it gives a true performance boost in both loading routes (because matcher is compiled and you don't have to recreate the entire route collection) and matching them (because the compiled UrlMatcher is optimized for runtime).

I suggest to make another benchmark with Symfony Routing instead of FastRoute, and use a compiled version of the UrlMatcher 🙂

Thread Thread
 
brcontainer profile image
Guilherme Nascimento

Thanks for commenting. I will test. Two days ago I made a change to the route system that greatly increased performance github.com/inphinit/teeny/blob/mas..., using $slice = array_slice($this->paramRoutes, $indexRoutes, $limit); to get 20 "regex routes" and testing them at the same time (using a single preg_match), all combined with the (?J) to allow groups with the same name and then separate the "callbacks" from those routes and set group names to identify the routes to lessen the work on the "PHP side"

  • Before (version 0.2.6): 1566.68 requests per sec
  • Before (version 0.2.6): Time per request 6.383ms
  • After (version 0.2.7 and 0.2.8): 3995.64 requests per sec
  • After (version 0.2.7 and 0.2.8): Time per request 2.503ms

I still promise that I will test the "Symfony Routing component". Thanks!

Thread Thread
 
hbgl profile image
hbgl

Thanks for pointing it out, Alex. Symfony's compiler is pretty interesting. It compiles all dynamic routes into a regex that resembles a trie.

gist.github.com/hbgl/cfa637dcd9aa3...

To be fair, FastRoute also has another dispatcher implementation that uses an almost identical algorithm.

Collapse
 
sroehrl profile image
neoan

Really nice! Or should I say cute, given the size?