RoutePreloader loads a lot of routes

Created on 2 February 2025, 3 days ago

Problem/Motivation

I was profiling some stuff and noticed that route preloading takes up a considerable amount of memory, about 1.5MB in my project.

As a logged in user with navigation module enabled, I also saw 9 calls to preloading routes.

2 of those are in early bootstrap from πŸ“Œ RouteNormalizerRequestSubscriber causes two extra route cache lookups Active .

Then there is a huge list from preloading, 168 routes in my case. Lots of things from layout builder (23 in total) , entity browser, many entity routes such as edit, delete, revision view/revert/delete, autocomplete callbacks, sitemap and more.

Then several more calls from navigation module because it loads each section separately from the menu tree. I believe that this would go away once we improve caching there, as we can properly cache the full navigation section, so not focusing on that here.

Steps to reproduce

Proposed resolution

I kind of see two options. One is to lower the amount of routes we preload, add some more checks, maybe check the is_admin flag instead.

But I'm wondering if there's a different solution. It's only a vague idea at this point, but what if we instead of a full preload, only flag those routes to use a ChainedFast bin (bootstrap?) when requested, but then only load them when actually used?

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

πŸ› Bug report
Status

Active

Version

11.1 πŸ”₯

Component

routing system

Created by

πŸ‡¨πŸ‡­Switzerland berdir Switzerland

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @berdir
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    When disabling RoutePreloader and all render cache bins, I get 77 calls in total to preloadRoutes() for an anonymous user on my frontpage only 6 of them actually fetch additional routes (including the two from redirect), many of them already deliberately preload their specific routes. and mostly happening within cacheable bits. LocalTask does all the node local tasks at once, a few menus, some weird bits like .

    Worth noting: Our site building approach almost exclusively uses nodes with paragraphs, we basically have no other linked routes such as views or something.

    In total, less than 100kb memory reported through that method by Blackfire.

    With render caches (but page and dynamic page cache disabled), I'm getting exactly 2 as anon user, those from redirect. As an admin, on the user edit form with navigation module I get 800 calls to preloadRoutes(), 13 cache lookups. On the frontpage, it's 11 with render caches on, 17 with them off. 500-600kb memory usage for admin.

    (I'm optimistic that a lot of these will actually go away if we can fully cache the navigation)

    Is there a third option? Can we just remove the RoutePreloader, do we really need this?

  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland
  • Pipeline finished with Canceled
    3 days ago
    Total: 92s
    #412509
  • Pipeline finished with Failed
    3 days ago
    Total: 522s
    #412510
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Somehow missed πŸ› RoutePreloader loads a lot of routes Active . Will keep this separate for now to do some more testing as the approach is quite different.

  • Pipeline finished with Failed
    3 days ago
    Total: 503s
    #412528
  • Pipeline finished with Failed
    3 days ago
    Total: 129s
    #412758
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Doing some tests with a different implementation.

    Basically, I disabled the route preloader for now completely. On a production system that's been running for a bit, I see around 110 variations of route preloader caches with the current approach. I assume that without preloading a ton of routes on every request, there will be a lot more of those variations. Right now, the cache is always for a bunch of routes, keyed by a hash of those.

    I'm changing that to using getMultiple() so we don't get many overlapping variations of different sets of routes. I'm not sure what the performance difference between a multi-cache load and a single one is. It's multiple rows, multiple cache items to expand and check. On its own, possibly slower, but I plan to combine it with the second idea:

    As mentioned, I'm playing with the idea of using a ChainedFast backend for either some (either hardcoded on not /admin or by still using the Preloader, but only to feed the list of "frontend" routes) or all. For the mix, it would split the routes, and load them from the fast or regular bin. At this point, getMultiple() a bunch of routes should be a lot faster than the regular query.

    Right now we cache the serialized strings because we know we don't need all of them. Without the big preload, that's no longer true and we will use those routes quite reliable. So we could consider to stop doing that and benefit from improvements such as πŸ“Œ Make igbinary the default serializer if available, it saves 50% time on unserialize and memory footprint Active by caching the route objects and letting the cache backend/serializer optimize it. would also

    For size comparison as to how big the router table is, on our install profile:

    select count(*), sum(length(route)) from router;
    +----------+--------------------+
    | count(*) | sum(length(route)) |
    +----------+--------------------+
    |      876 |            1244156 |
    +----------+--------------------+
    
    select count(*), sum(length(route)) from router where path not like '/admin%';
    +----------+--------------------+
    | count(*) | sum(length(route)) |
    +----------+--------------------+
    |      175 |             231266 |
    +----------+--------------------+
    
    MariaDB [db]> select count(*), sum(length(data)) from config;
    +----------+-------------------+
    | count(*) | sum(length(data)) |
    +----------+-------------------+
    |     1078 |           2124343 |
    +----------+-------------------+
    
    MariaDB [db]> select count(*), sum(length(data)) from cache_discovery;
    +----------+-------------------+
    | count(*) | sum(length(data)) |
    +----------+-------------------+
    |       40 |           1454662 |
    +----------+-------------------+
    

    Full router table is around 1.2MB, similar to discovery cache and around half the size of the config table. Non-admin routes is only 200kb.

    Sure, router could get a lot bigger, but every view that would be added there will also add a much bigger chunk to config.

    Unsure how fancy we want to get with this, happy for some opinions.

  • First commit to issue fork.
  • Merge request !11105Draft: No caching β†’ (Open) created by catch
  • Pipeline finished with Failed
    1 day ago
    Total: 135s
    #414651
  • πŸ‡¬πŸ‡§United Kingdom catch

    I tried to see how many routes we really have to load on most pages.

    Made some small changes to @Berdir's MR to remove all caching, which is pushed to a draft MR.

    Took these steps:

    install standard
    uninstall toolbar
    install navigation

    Left myself logged in as admin.

    On a dynamic_page_cache hit, I get only get route lookups for the front page view route. This is for the is_front logic in preprocess + drupal settings. We could potentially cache that lookup independently in the bootstrap cache because it is done on every page. Would love to get rid of is_front altogether too but that's not easy unfortunately.

    After truncating the dynamic page cache, I got , , the same views route, and bigpipe.no_js.

    I think we could potentially handle the special and similar routes to circumvent the database/cache altogether too, not sure how yet but we never need to match against those routes, only load them.

    Overall I think we could aim for zero route lookups when dynamic page cache is warm, one or two when it's cold. And then use @berdir's approach (without the serialization hack) to cache the individual routes.

  • Pipeline finished with Failed
    about 1 hour ago
    #415762
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    I think I tested with disabled render caches, my concerns around the caching atm are how the impact is cold caches, agreed that warm caches should have very few lookups.

    A variant of the caching idea above is that we could also only use the fast cache and just don't cache others. It should only happen in the backend and for admins, and it's essentially a 1:1 of a query lookup and cache lookup.

    I pushed a proof of concept for that, still using the route preloader and a new method, didn't add to interface yet, route preloader is now a misleading name.

    RouteProvider already implements events too. I wonder if we should just inline that cacheable routes stuff and make it an implementation detail, unsure.

Production build 0.71.5 2024