Обновить

Комментарии 7

  foreach ($this->paramChildren as $child) {
        if (preg_match($child['regex'], $segment)) {

Во-первых, preg_match внутри foreaсh - это крайне медленная и затратная операция. Подобное подходит для того, чтобы сделать "в лоб", просто протестировать решение. Однако на скорости оно сказывается катастрофически, компилируя выражение в MARK-группы (имею ввиду подход с \G + (*MARK:route_name) для каждого роута) можно получить прирост более чем в 13000% (я не оговорился, именно тысяч). Это выкладки из Hoa issue.

Возможно именно по-этому на 10к итерациях мы получаем разницу ~5000% между вашим решением и symfony:

ascetic: 1.275s
nikic: 0.207s
symfony: 0.032s

P.S. Нет, не из-за этого. Это из-за "лишней ответственности", а именно из-за вызовов коллбека + миддлварей.

Если сделать полностью статичные роуты (без регулярок, то отличия):

ascetic: 1.634s
nikic: 0.001s
symfony: 0.005s

Если закомментить вызов миддлварей, хендлера и вообще весь код, оставив только "match", то:

ascetic: 0.011s
nikic: 0.002s
symfony: 0.006s

P.P.S. Я кстати ХЗ почему symfony быстрее fastroute на дефолтных кейсах. Отдебажил, вроде всё ок... Вот говнокод с бенчмарками. Допускаю, что у меня где-то косяк, но не нашёл.

Во-вторых, кажется, у вас архитектура неправильная (т.е. постановка задачи изначальная, как следствие и нарушение SRP + Open Closed). Задачи роутера - это найти нужный путь для самого роута, т.е.:

interface RouterInterface
{
    public function match(RequestInterface $request): ?MatchedRouteInterface;
    // ну или MatchedRouteInterface|never<Throwable>, как вам будет угодно
}

Мы отправляем реквест и получаем нужный роут для дальнейшей работы.

В вашем же случае Router

  1. Зачем-то зависит от Container, который потом размазывается по всем зависимостям внутри.

  2. Выполняет роль диспатчера (RequestHandlerInterface), хотя это задача не роутера, а как раз хендлера. Да, можно сделать адаптер RouterInterface <-> RequestHandlerInterface, но это более высокий уровень.

  3. Захардкожен на 1 единственный AttributeLoader, т.е. не только не поддерживает несколько лоадров, но ещё и отвечает не только за роутинг, но и за сборку роутов.

  4. Плюс ещё отвечает за кеширование

  5. Помимо этого коллекция роутов почему-то является не коллекцией роутов, а ещё и куском самого роутера: Что там делает public function match(string $method, string $uri): RouteMatchResult?

  6. и т.д... Если требуется более глубокий анализ и больше проблемных мест -- дайте знать. Я лишь поверхностно просмотрел код и оценил архитектуру.

Ну и плюс, как вы уже знаете, из-за вашего подхода в роутере есть баг, когда приоритет роутов меняется, вы указываете:

GET /user/{id}
GET /user/42

Но последний, из-за "статики" поднимается наверх и имеет приоритет выше, нежели первый указанный.

В общем, вы безусловно молодец и ваше решение имеет место быть. Все эти оптимизации -- тоже прикольные. Но на практике, кажется, всё довольно плохо.

P.S. OPCache использует mmap по дефолту, а не Shared Memory ;)

Наконец я дождался отличного комментария!
По поводу ответственности - гнался за автовайрингом... Наверное было бы правильно вынести в мидлваре зависимость от контейнера и вообще в отдельный проект.
С остальными замечаниями тоже согласен. В целом архитектурно тут всё плохо. Это я понимал изначально. С другой стороны хотелось собрать как гипотезу - собрал. Работает.
С бенчмарками у меня примерно такая-же ситуация. Симфони и фастроут впереди, хотя тесты сделаны с учётом жц приложения. Основная нагрузка на PSR, хотя немного оптимизировал. Далее падает на fallback. Регулярки в цикле... В общем всё так-же. Либо допиливать, либо выкидывать на помойку.
Тем не менее, среди всех PSR этот самый быстрый. Роутеры, где под капотом фастроут - от PSR замедляются в сотни раз. Не стоило изначально эти стандарты брать... Не зря в симфони от них отказались.

PS. В общем, я понял. Сами тесты некорректные. Получается PSR можно тестировать либо только с другими PSR роутерами, либо делать полноценные какие-то приложения. Мы сейчас берем сам матчер (Symfony, FastRoute), и гоняем его по циклу, с другой стороны PSR весь пыхтит как полноценное приложение - делает много работы, которой в принципе нет в FastRoute и Symfony.

Symfony в бенчмарке работает как голый URL-matcher: строка на вход → строка на выход. Waypoint — это полноценный PSR-15 RequestHandler, который прогоняет весь HTTP lifecycle с настоящими PSR-7 объектами. В реальном приложении Symfony тоже создаёт Request/Response через HttpKernel, но это не входит в бенчмарк роутинга.

Вот тут как раз можно сделать рефакторинг архитектуры, вычленить матчер и уже гонять только его. Тогда будет честно.

В общем, я понял. Сами тесты некорректные.

Ну да, я когда начал копаться - именно до этого и добрался:

Если закомментить вызов миддлварей, хендлера и вообще весь код, оставив только "match", то:

Там по говнокоду нельзя было матч вытащить. Пришлось рефачить. Бенчмарк выложил, жду от Вас замечаний, если они еще остались. В целом по результатам я понял, что не зря потратил время. Пациент скорее жив, чем мертв ))

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации