Problem/Motivation

ContextualController::render() renders the requested IDs one after another. Each ID's links run route access checks that upcast entity parameters, so the entities load one by one, although EntityStorageBase::loadMultiple() can batch loads across Fibers since #1237636: Lazy load multiple entities at a time using fibers.

Those access checks are most of the request. Across 12 contextual/render requests on Standard and Umami pages, as editor and admin, they took 58% of request time with a warm entity cache and 72% with a cold one.

The gain is limited to users with "access contextual links". contextual.js also caches rendered links per ID in sessionStorage, so it mostly applies to the first view of a page in a session.

Proposed resolution

Render each ID in its own Fiber and resume them in a loop, as Renderer::replacePlaceholders() does. Use renderInIsolation(): renderRoot() throws a LogicException when a second call starts while the first is suspended. Duplicate IDs still render once per occurrence.

A prototype, swapped in for the controller in a functional test with access_manager decorated to count checks and queries, returned identical JSON for the same 672 access checks:

  • Warm entity cache: 419 ms to 283 ms, 1,103 to 582 queries.
  • Cold entity cache: 510 ms to 467 ms, 1,473 to 949 queries.

Times are single requests with query counting on; the query counts are the reliable part.

Most of the remaining cost is Edit links on moderated content. Content Moderation sets load_latest_revision on entity form routes, and revision loads don't suspend, so they stay unbatched.

Remaining tasks

  • Decide whether Fiber-aware revision loading belongs here or in a follow-up.

Comments

graber created an issue.