Problem/Motivation
ResourceObjectNormalizer::getNormalization() calls ResourceObjectNormalizationCacher::get() once per resource object.
A collection of N resources with M included resources can require N + M individual normalization-cache lookups, plus reads for redirects. With the default database cache backend, each read is a database query.
VariationCache::getMultiple() can be used to pull items in bulk with one backend read per redirect level. JSON:API does not currently use it.
Steps to reproduce
- Create 50 published articles.
- Bypass response caching while keeping the
jsonapi_normalizationscache bin enabled. - Request
/jsonapi/node/article?page[limit]=50. Allow the request to complete, including the termination phase that caches the normalizations. - Repeat the request while logging reads from the
jsonapi_normalizationsbin. Observe that resource normalizations are read individually, and there may be other reads needed to follow cache redirects.
Proposed resolution
Add ResourceObjectNormalizationCacher::prefetch(). It reads the normalizations of a list of resource objects with VariationCache::getMultiple() and provides a one-time hit/miss set for the current request. get() uses and removes a prefetched item if one exists; otherwise, it reads the cache as before.
Prefetched items are tied to the request that read them.
DataNormalizer::normalize() calls prefetch() before normalizing the items. This makes one batch for the primary data and one for the included data.
User interface changes
None.
API changes
None. (We add a method but this is @internal.)
Data model changes
None.
AI disclosure: I used AI to help prepare the IS and MR (Astra and Opus 5.5).
Issue fork drupal-3626255
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
deciphered@ptmkenny I had an agent test this one against my site, since the other two would not touch its code paths (no fields disabled through JSON:API Extras, and its includes are
entity_reference_revisions, which the relationship normalizer already loads per item before the include resolver runs).Site: Drupal 11.4.6, Paragraphs, 39 nodes with 9 to 23 paragraphs each. Requests: individual nodes with
include=field_content,field_content.field_media,..., and a 50-item collection with the same includes.Backport blocker. As-is the MR fails on 11.x: every JSON:API request throws
ServiceCircularReferenceExceptionforjsonapi.serializer.DataNormalizergains a constructor argument with an#[Autowire]attribute, butjsonapi.services.ymlonly hasautowire: truein_defaultsonmain(from #3578374), so on 11.x the argument is never supplied. Adding this toserializer.normalizer.data.jsonapifixes it:An 11.x backport needs that line or the autowire default. Everything below is the MR plus that line.
Query counts, from
Database::startLog()on a fresh PHP process per request, page and dynamic page caches bypassed, normalizations already cached (before / after):Responses were byte-identical. Counts repeat exactly between runs.
Timings, median of 40 HTTP requests, base and patched interleaved, opcache warm:
jsonapi_menu_itemswith 38 links 32.7 ms to 27.8 ms (-15%).That is about 0.07 ms per removed read on loopback, so a database on another host gains more. The JSON:API kernel suite passes on 11.4.6 with the patch.
One review nit:
prefetch()copies the two@todolines about POST/PATCH and #3381898 fromget()andset(), so the same note now appears three times in the class. A shared guard, or one note pointing at the others, would keep a future fix to one place.Happy to run it against the site's production environment (MariaDB on a separate pod) once the 11.x form is settled.
Comment #4
ptmkenny commented@Deciphered Thanks for the feedback! I have fixed the MR to work with Drupal 11.