Tweaks and optimizations

This commit is contained in:
Valithor Obsidion 2024-12-25 10:09:30 -05:00
parent a05ff9e55c
commit 393022b342
4 changed files with 53 additions and 50 deletions

View File

@ -4,7 +4,7 @@ Hey there! This repo is an experiment to create a well-tuned and speedy URI rout
- It's fast
- It's simple
Prior to this proejct, I built the [SimpleRouter](https://github.com/splashsky/simplerouter) as a fork from a very cool project called the simplePHPRouter. That router was based on regex for parameterization, and the regex made it pretty painful to maintain if I ever left it alone for too long.
Prior to this project, I built the [SimpleRouter](https://github.com/splashsky/simplerouter) as a fork from a very cool project called the simplePHPRouter. That router was based on regex for parameterization, and the regex made it pretty painful to maintain if I ever left it alone for too long.
Since working on SimpleRouter, I've played with other languages (primarily Go) and found a few new ways of doing things.
@ -23,7 +23,7 @@ Take for example these routes:
A radix (and more specifically, a PATRICIA) trie takes the commonalities in these routes and makes them into nodes, or branches. `/` exists as the root node. `api/` is turned into a node, from which `v1/` and `/v2/no` branch. `hello` is taken as another branch with the `/` and `:param` child nodes. `/foo` is naturally it's only branch from the root.
By splitting these routes up into a trie based on their segments, you're able to iterate far more quickly through the tree to find what you're looking for. If a user then requests `/api/v1/hello/sky` the router can jump from the root, to `api/`, to `v/1`, to `hello/`, then to the final node much faster than if we had to chop up, say, an associative array and compare for every registered route.
By splitting these routes up into a trie based on their segments, you're able to iterate far more quickly through the tree to find what you're looking for. If a user then requests `/api/v1/hello/sky` the router can jump from the root, to `api/`, to `v1/`, to `hello/`, then to the final node much faster than if we had to chop up, say, an associative array and compare for every registered route.
The nodes can contain any arbitrary information, such as HTTP methods or handlers. From my experience, this method of lookup prefers specificity, and so it will always prefer the edges over the inner structures.

View File

@ -20,13 +20,8 @@ class SegmentRouter implements RouterInterface
return str_starts_with($segment, ':') ? ':x' : $segment;
}, explode('/', trim($route, '/')));
// Push each segment into the routes array as a node, except if this is the root node
$node = &$this->routes;
foreach ($segments as $segment) {
// skip an empty segment, which allows us to register handlers for the root node
if ($segment === '') continue;
$node = &$node[$segment]; // build the node tree as we go
}
// Recursively build the node tree
$this->addNode($this->routes, $segments, $method, $handler);
// Add the handler to the last node
$node[$method] = $handler;
@ -34,6 +29,32 @@ class SegmentRouter implements RouterInterface
return $this;
}
private function addNode(array &$node, array $segments, string $method, callable $handler): void
{
// Base case: if no more segments, add the handler
if (empty($segments)) {
$node[$method] = $handler;
return;
}
// Get the current segment
$segment = array_shift($segments);
// Skip empty segments
if ($segment === '') {
$this->addNode($node, $segments, $method, $handler);
return;
}
// Ensure the segment exists in the node
if (!isset($node[$segment])) {
$node[$segment] = [];
}
// Recur for the next segment
$this->addNode($node[$segment], $segments, $method, $handler);
}
/**
* Perform a lookup in the route tree for a given method and URI. Returns an array with a result code,
* a handler if found, and any dynamic parameters. Codes are 200 for success, 404 for not found, and

View File

@ -8,38 +8,28 @@ $params = ['id', 'slug', 'page', 'extra', 'foo', 'string', 'number', 'bar', 'baz
$endpoint = ['edit', 'create', 'delete', 'view', 'change', 'modify', 'generate', 'lift', 'lower', 'raise', 'drop', 'pick', 'choose', 'select', 'deselect', 'unselect', 'reselect', 'pick', 'unpick', 'repick', 'reselect', 'reunpick', 'rechoose', 'reselect'];
$midpoint = ['do', 'cause', 'effect', 'affect', 'impact', 'influence', 'change', 'modify', 'transform', 'alter', 'shift', 'adjust', 'adapt', 'convert', 'translate'];
// Generate routes
for ($i = 0; $i < 1000; $i++) {
$routes[] = makeRoute();
// write the routes array to a file
file_put_contents('routes/big.txt', implode("\n", $routes));
file_put_contents('routes/big.txt', implode("\n", array_map(fn() => makeRoute(), range(1, 1000))));
enum RoutePart: string {
case PARAMS = 'params';
case ENDPOINT = 'endpoint';
case MIDPOINT = 'midpoint';
public function matches(array $params, array $endpoint, array $midpoint): string {
return '/' . match ($this) {
self::PARAMS => ':' . $params[array_rand($params)],
self::ENDPOINT => $endpoint[array_rand($endpoint)],
self::MIDPOINT => $midpoint[array_rand($midpoint)],
};
}
}
function makeRoute(): string
{
global $methods, $apis, $params, $endpoint, $midpoint;
$method = $methods[array_rand($methods)];
$api = $apis[array_rand($apis)];
$route = "/$api";
$length = rand(1, 8);
$options = ['params', 'endpoint', 'midpoint'];
for ($i = 0; $i < $length; $i++) {
$option = $options[array_rand($options)];
switch ($option) {
case 'params':
$param = $params[array_rand($params)];
$route .= "/:$param";
break;
case 'endpoint':
$end = $endpoint[array_rand($endpoint)];
$route .= "/$end";
break;
case 'midpoint':
$mid = $midpoint[array_rand($midpoint)];
$route .= "/$mid";
break;
}
}
return $method . ' ' . $route;
return $methods[array_rand($methods)] .
' /' .
$apis[array_rand($apis)] .
implode('', array_map(fn($option) => $option->matches($params, $endpoint, $midpoint), array_rand(array_flip(RoutePart::cases()), rand(1, 8))));
}

View File

@ -68,10 +68,7 @@ $routes = [
}],
];
foreach ($routes as $route) {
[$method, $path, $handler] = $route;
$r->add($method, $path, $handler);
}
array_walk($routes, fn($route) => $r->add($route[0], $route[1], $route[2]));
for ($i = 0; $i < 10; $i++) {
[$method, $uri] = $routes[array_rand($routes)];
@ -118,17 +115,12 @@ function echoTitle(string $title) {
function readAndAddRoutes(string $file, &$r): array
{
$array = [];
$routes = file($file);
foreach ($routes as $route) {
return array_map(function($route) use ($r) {
[$method, $path] = explode(' ', $route);
$path = trim($path);
$array[] = [$method, $path];
$r->add($method, $path, function() {
return true;
});
}
return $array;
$r->add($method, $path, fn() => true);
return [$method, $path];
}, file($file));
}
function runIterations(int $iterations, $r, array $routes) {