Update readme, exclude tests from package

This commit is contained in:
Sky Johnson 2024-09-13 12:35:42 -05:00
parent a094bd0658
commit 5b30dea8fd
3 changed files with 43 additions and 69 deletions

View File

@ -33,74 +33,39 @@ One flaw(-ish) of the SimpleRouter implementation (and many other implementation
In order to circumvent this, we can rely on our node structure; if a node begins with our delimiter `:` then we can take the related segment from the URI and use that as a parameter, regardless of the value. This means we have extremely low overhead in the logic required to pull parameters from URIs.
To prevent creating many unnecessary children nodes and handlers, all parameters are converted to a `:x` node internally. Any value will satisfy a parameter node. These parameters are then passed into the hanlder function call.
## Performance
Of course, what good is a router that's slow? We need to be able to lookup routes and get the handler as quickly as possible. Now, you may note there are multiple routers here; these are implementations in their experimental phase to find the most memory and time efficient lookup operations possible.
For our benchmarks, which you can find in their respective files in [tests](tests/), we create a single instance of a router, load routes from the `.txt` files, write their respective arrays to `.txt` files in [storage](tests/storage/), then perform three iterations each; 10k, 100k, 1m requests. In these iterations, we pick a random URI from the full list, and have the router perform the lookup on that randomly selected URI. The test fails only if a `404` or `405` is returned.
Below are the results from our most rigorous tests; performing 1 million lookups on 1000 randomized routes with various lengths and parameters.
### SimpleRouter
This is an old project of mine and the first router I ever tried to write. Foundationally it relies on tokenizing an incoming URI and matching it to regex, then looking through the internal routes array.
There are three tests performed, each running in iterations of 10000, 100000, 1000000;
- blog: 4 routes
- github: 203 routes
- big: 1000 routes
And one final test to ensure parameter handling is correct. You can run these tests by simply running `php test.php` in the `/tests` directory.
These are my results for the big 1,000,000 test:
```php
Running 1000000 iterations
(100000 lookups) M: 1846.2 kb - T: 32.6156370640 s
(200000 lookups) M: 1846.2 kb - T: 63.9784071445 s
(300000 lookups) M: 1846.2 kb - T: 96.9934570789 s
(400000 lookups) M: 1846.2 kb - T: 130.2443051338 s
(500000 lookups) M: 1846.2 kb - T: 161.8348190784 s
(600000 lookups) M: 1846.3 kb - T: 197.4232161045 s
(700000 lookups) M: 1846.1 kb - T: 231.8421580791 s
(800000 lookups) M: 1846 kb - T: 262.8337080479 s
(900000 lookups) M: 1846.2 kb - T: 296.1434569359 s
Time: 330.9394941330 s
Avg/lookup: 0.0003309396 s
// CPU: 11th gen Intel Core i7-1185G7 @ 3.0GHz
// RAM: 32GB 3200MHz DDR4
🚀 Running 1000000 iterations...
✔️ Done!
Peak memory: 2726.5 kb
Time: 1.5776410103 s
Avg/lookup: 0.0000008231 s
Shortest lookup: 0.0000009537 s
Longest lookup: 0.0018200874 s
```
Interestingly, it has the lowest memory cost of the current iterations, but the absolute highest total time and time per request. The time issue is likely due to hugely unoptimized tokenization.
### TrieRouter
This is my first iteration of a PATRICIA trie router in PHP. I don't think it's currently perfect, as we could probably work on storing nodes as bytes rather than strings, but it's a good proof of concept for a tree based mechanism.
## Usage
Using the router is simple! The implementations here must adhere to the Router interface;
```php
Running 1000000 iterations
(100000 lookups) M: 4718.3 kb - T: 0.0581219196 s
(200000 lookups) M: 4718.3 kb - T: 0.1310830116 s
(300000 lookups) M: 4718.3 kb - T: 0.1909840107 s
(400000 lookups) M: 4718.3 kb - T: 0.2500770092 s
(500000 lookups) M: 4718.3 kb - T: 0.3067679405 s
(600000 lookups) M: 4718.3 kb - T: 0.3660039902 s
(700000 lookups) M: 4718.3 kb - T: 0.4237358570 s
(800000 lookups) M: 4718.3 kb - T: 0.4837160110 s
(900000 lookups) M: 4718.3 kb - T: 0.5422408581 s
Time: 0.6060788631 s
Avg/lookup: 0.0000006061 s
$router->add('GET', '/api/v1/:test', function($test) {
echo $test;
});
$router->lookup($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']); // ['code' => 200, 'handler' => callable, 'params' => []]
```
You can immediately see a ***huge*** time difference from SimpleRouter. Responses are in microseconds rather than milliseconds, but we're using 3x+ as much memory. From experimentation (and you can see this in the [visualization](tests/storage/trie/big.txt)) that the trie method creates a gigantic number of child elements to store the handler for every endpoint.
### SegmentRouter
This second iteration is the first to achieve the best of both worlds; lower memory usage and lower time per request! In order to achieve this, we simply split routes into segments and store each segment as a node. This means that there are no extraneous child elements and navigating to an endpoint requires less effort. The [visualization](tests/storage/segment/big.txt) also shows how much simpler the tree is compared to TrieRouter.
```php
Running 1000000 iterations
(100000 lookups) M: 2891.8 kb - T: 0.0500328541 s
(200000 lookups) M: 2891.8 kb - T: 0.0995390415 s
(300000 lookups) M: 2891.8 kb - T: 0.1491589546 s
(400000 lookups) M: 2891.8 kb - T: 0.1987509727 s
(500000 lookups) M: 2891.8 kb - T: 0.2471258640 s
(600000 lookups) M: 2891.8 kb - T: 0.2962870598 s
(700000 lookups) M: 2891.8 kb - T: 0.3496289253 s
(800000 lookups) M: 2891.8 kb - T: 0.3990900517 s
(900000 lookups) M: 2891.8 kb - T: 0.4483740330 s
Time: 0.4971950054 s
Avg/lookup: 0.0000004973 s
```
Truly our most impressive show yet. By simplifying the structure of our tree and only storing what we need, we can achieve pretty incredible results in only 3 MB of RAM.
This API isn't expected to change, as all the router is expected to do is to look up our requested URI and return the handler and params for it.

View File

@ -16,5 +16,10 @@
}
],
"minimum-stability": "stable",
"require": {}
"require": {},
"archive": {
"exclude": [
"/tests"
]
}
}

View File

@ -2,8 +2,12 @@
require_once 'color.php';
require_once __DIR__ . '/../src/Router.php';
require_once __DIR__ . '/../src/SegmentRouter.php';
use Sharkk\Router\Router;
const ROUTES = __DIR__ . '/routes/';
const TREES = __DIR__ . '/trees/';
use Sharkk\Router\SegmentRouter;
// if there's a flag, reset the opcache
if (in_array('-f', $argv)) {
@ -11,11 +15,11 @@ if (in_array('-f', $argv)) {
echoTitle("opcache reset");
}
$r = new Router();
$r = new SegmentRouter();
// Blog lookups
$blog = readAndAddRoutes('routes/blog.txt', $r);
writeRoutesToFile($r->routes, 'trees/blog.txt');
$blog = readAndAddRoutes(ROUTES . 'blog.txt', $r);
writeRoutesToFile($r->routes, TREES . 'blog.txt');
echoTitle("Starting blog lookups");
runIterations(10000, $r, $blog);
runIterations(100000, $r, $blog);
@ -24,8 +28,8 @@ unset($blog);
// Github lookups
$r->clear();
$github = readAndAddRoutes('routes/github.txt', $r);
writeRoutesToFile($r->routes, 'trees/github.txt');
$github = readAndAddRoutes(ROUTES . 'github.txt', $r);
writeRoutesToFile($r->routes, TREES . 'github.txt');
echoTitle("Starting github lookups");
runIterations(10000, $r, $github);
runIterations(100000, $r, $github);
@ -34,8 +38,8 @@ unset($github);
// Big lookups
$r->clear();
$big = readAndAddRoutes('routes/big.txt', $r);
writeRoutesToFile($r->routes, 'trees/big.txt');
$big = readAndAddRoutes(ROUTES . 'big.txt', $r);
writeRoutesToFile($r->routes, TREES . 'big.txt');
echoTitle("Starting big lookups");
runIterations(10000, $r, $big);
runIterations(100000, $r, $big);
@ -93,7 +97,7 @@ function echoTitle(string $title) {
echo "\n";
}
function readAndAddRoutes(string $file, Router &$r): array
function readAndAddRoutes(string $file, &$r): array
{
$array = [];
$routes = file($file);