Skip to content

perf(laravel): add a local cache to the metadata cache factories#8417

Open
nstoeckel wants to merge 1 commit into
api-platform:4.3from
nstoeckel:perf/laravel-metadata-local-cache
Open

perf(laravel): add a local cache to the metadata cache factories#8417
nstoeckel wants to merge 1 commit into
api-platform:4.3from
nstoeckel:perf/laravel-metadata-local-cache

Conversation

@nstoeckel

Copy link
Copy Markdown
Q A
Branch? 4.3
Bug fix? yes
New feature? no
Deprecations? no
Issues Closes #8413
License MIT

Problem

The Laravel metadata cache factories (CachePropertyMetadataFactory, CachePropertyNameCollectionMetadataFactory, CacheResourceCollectionMetadataFactory) go through the Laravel cache repository on every create() call: store resolution via the facade, key hashing (serialize() + xxh3), and a Repository::get() that dispatches a CacheHit event per lookup.

AbstractItemNormalizer resolves metadata once per property per item, so a large collection response makes tens of thousands of trips through that stack in a single request (48,640 property-metadata lookups for a 2,023-item collection in the profiling from #8413). The Symfony implementation guards against exactly this with CachedTrait's $localCache array. The Laravel port dropped that layer, so the per-request cost is items × properties instead of properties.

Fix

Mirror the Symfony implementation: check an in-memory array before consulting the persistent store.

return $this->localCache[$key] ??= Cache::store($this->cacheStore)->rememberForever($key, ...);

The factories are registered as singletons (ApiPlatformProvider / ApiPlatformDeferredProvider), so the array lives for the request, exactly like CachedTrait's $localCache on the Symfony side. The persistent store keeps its role of amortizing metadata building across requests.

The three classes go from final readonly to final with individually readonly constructor properties, since a mutable array property requires it. The classes are final, so nothing can have extended them.

??= instead of CachedTrait's array_key_exists(): the difference only matters when null is a legitimate cached value, and null is unrepresentable here. The decorated factories have non-nullable return types, and rememberForever() treats null from the store as a miss and falls through to the callback, so neither layer can ever produce it.

Measured on the reproduction from #8413 (2,023-item unpaginated collection, 7 scalar properties, plain JSON, v4.3.17): serialization drops from 1.18s to 0.61s.

Tests

Added Tests/Metadata/CacheMetadataFactoriesTest.php. The memoization proof avoids mocking the cache: perform a lookup, wipe the persistent store, look up again. The decorated factory must have been consulted exactly once, which is only possible if the second lookup was answered from memory. Also covers independent caching per key and the write to the persistent store.

The Laravel component suite passes unchanged.

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