Skip to content

[BUGFIX] Keep API resources in memory per cache identifier - #187

Merged
klamparski merged 1 commit into
release/5.0.0from
186-bug-serializing-a-collection-reads-and-unserializes-the-api-resource-cache-entry-once-per-entity
Nov 5, 2026
Merged

klamparski merged 1 commit into
release/5.0.0from
186-bug-serializing-a-collection-reads-and-unserializes-the-api-resource-cache-entry-once-per-entity

Conversation

@klamparski

Copy link
Copy Markdown
Collaborator

Resolves: #186

ApiResourceRepository::getAll() read and unserialized the t3api cache entry on every call. Serialization calls it
for every entity (AbstractEntitySubscriber::addIri() → getByEntity()). The effect on one request:

  • the products collection of the functional test fixture read the entry 99 times;
  • a collection of 20,000 entities on TYPO3 14.3 took 46 s and up to 0.9 GB of memory.

The repository is a shared service. It now keeps the resources it has read, per cache identifier. The identifier
contains the API base path with the language prefix from the URL (/de/_api/...), so requests for another
language prefix in the same process still get their own resources. Requests that select the language with the
X-Locale header use the base path of the default language and share one identifier.

Measurements

DDEV test instances, PHP 8.4, GET /_api/news/tags?itemsPerPage=N, 20,000 additional tags, memory_limit 1 GB,
median of 3 runs, logging of the site test extension disabled:

N 12.4.45 before / after 13.4.33 before / after 14.3.5 before / after
1,000 1.22 s / 0.37 s 1.21 s / 0.38 s 1.67 s / 0.40 s
5,000 5.40 s / 1.32 s 6.35 s / 1.47 s 9.42 s / 1.63 s
20,000 27.8 s, 284 MB / 5.65 s, 128 MB 29.7 s, ≤ 455 MB / 5.75 s, 147 MB 46.3 s, ≤ 907 MB / 6.32 s, 144 MB

Tests

  • ApiResourceCacheDispatcherTest (functional): dispatches the products collection with related categories and
    tags through a read-counting t3api cache backend. It expects one read; before the change there were 99.
  • ApiResourceRepositoryTest (unit), getByEntityReadsCacheOnlyOnceForManyEntities: many lookups read the
    cache once.
  • ApiResourceRepositoryTest (unit), getAllReadsCacheOncePerApiBasePath: one read per API base path. A single
    memoized value that ignores the identifier fails this test.

Unit (365) and functional (217) suites pass. PHPStan and PHP-CS-Fixer report no errors.

Considerations

  • Lifetime. The copy lives as long as the process: one PHP-FPM request, one CLI command or one worker. Flushing
    the t3api cache in the same process does not drop it. API resources come from class annotations and
    configuration, which do not change while a process runs.
  • Shared ApiResource instances. Pagination::setParametersFromRequest() stores request parameters in the
    operation. AbstractCollectionResponse sets them before every read, so sharing is safe. Transient cache backends
    already share the instances today.
  • Not included:
    • the TYPO3 runtime cache: it has the same lifetime in practice and adds a dependency;
    • an entity class → ApiResource index in getByEntity(): it saves about 80 ms for 20,000 lookups over
      70 resources.

ApiResourceRepository::getAll() read and unserialized the t3api cache
entry on every call. Serialization calls it for every entity, so a
collection of N entities read the same entry N times and kept N copies
of the resource graph in memory.

The repository now keeps the resources it has read per cache
identifier, which contains the API base path with the language prefix.
@klamparski
klamparski merged commit 1db48e3 into release/5.0.0 Nov 5, 2026
26 checks passed
@klamparski
klamparski deleted the 186-bug-serializing-a-collection-reads-and-unserializes-the-api-resource-cache-entry-once-per-entity branch November 5, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant