Share content-type tables between middleware instances - #2885
Open
ericproulx wants to merge 1 commit into
Open
Conversation
Grape builds one content-type-aware middleware per API instance, and each one memoized its own derived tables. A memory profile of an app mounting 13 APIs held 42 of them (20 Formatter, 20 Error, 2 Versioner) covering four distinct content-type registrations: 42 lookup tables and 41 mime-type tables for four distinct values, 15.1 kB of the 849.7 kB retained. Both tables are derived from nothing but the registry, so they now live in value-keyed caches on Grape::ContentTypes — Hash keys compare by value, so every API registering the same content types shares one table. On the same app that is 42 lookup tables down to 4 and 41 mime-type tables down to 3, 17.4 kB down to 4.9 kB. What is left is the registry Hash itself, which InheritableSetting builds fresh per API instance. The cached tables are frozen now that they are shared, and each cache keeps a frozen copy of the registry as its key: the caller's Hash stays reachable through `middleware.options[:content_types]`, and mutating a live key would corrupt a cache that is now process-wide. Formatter is the only middleware that reads `mime_types` — Error resolves `content_type` through the lookup, and the versioners read the registry directly — so it warms that table itself instead of PrecomputedContentTypes building one for all 42 instances. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
perf/share-content-type-tables
branch
from
September 2, 2026 09:26
3fb667c to
bb9d8a9
Compare
Danger ReportNo issues found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Grape builds one content-type-aware middleware per API instance, and each one memoized its own derived tables. Both tables are derived from nothing but the content-type registry, so they now live in value-keyed caches on
Grape::ContentTypes— Hash keys compare by value, so every API registering the same content types shares one table.Grape::ContentTypes::LookupCache/MimeTypesCachefollow thePartsCache/PatternCache/CoercerCacheidiom already in the codebase.lookup_formoved here fromGrape::Middleware::PrecomputedContentTypes.Grape::Middleware::Formatterwarmsmime_typesitself. It is the only reader —Errorresolvescontent_typethrough the lookup, and the versioners read the registry directly — soPrecomputedContentTypesno longer builds that table for every instance that includes it.== DEFAULTSfast path inmime_types_forstays, soGrape::ContentTypes::MIME_TYPESis still returned by identity for a default registry.Memory
A
memory_profilerrun over an app mounting 13 APIs held 42 content-type-aware middleware (20Formatter, 20Error, 2Versioner) covering four distinct content-type registrations:@content_types_lookup@mime_types@content_types17.4 kB → 4.9 kB, of which the duplicated portion goes from 15.1 kB to 2.7 kB. It scales with the number of mounted APIs — this app held roughly 750 B of duplicate tables per API instance. What is left is the registry Hash itself, which
InheritableSettingbuilds fresh per API instance; deduplicating that belongs inInheritableSetting, not here.Nothing moves per request: the tables are still warmed at middleware construction and read from an ivar, so
dup.call!(env)inherits them exactly as before.Backward compatibility
Grape::ContentTypes.mime_types_fornow returns a frozen Hash shared between callers with equal content types, where it previously returned a fresh mutable one — except for a default registry, where it already returned the frozenMIME_TYPESconstant. Each cache also stores a frozen copy of the registry as its key rather than the caller's Hash: the caller's registry stays reachable throughmiddleware.options[:content_types], and mutating a live key would corrupt a cache that is now process-wide. A spec pins that a post-lookup mutation of the registry leaves the cached table alone.Test plan
grape-swaggerparser error).🤖 Generated with Claude Code