Skip to content

Share content-type tables between middleware instances - #2885

Open
ericproulx wants to merge 1 commit into
masterfrom
perf/share-content-type-tables
Open

Share content-type tables between middleware instances#2885
ericproulx wants to merge 1 commit into
masterfrom
perf/share-content-type-tables

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

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 / MimeTypesCache follow the PartsCache / PatternCache / CoercerCache idiom already in the codebase. lookup_for moved here from Grape::Middleware::PrecomputedContentTypes.
  • Grape::Middleware::Formatter warms mime_types itself. It is the only reader — Error resolves content_type through the lookup, and the versioners read the registry directly — so PrecomputedContentTypes no longer builds that table for every instance that includes it.
  • The == DEFAULTS fast path in mime_types_for stays, so Grape::ContentTypes::MIME_TYPES is still returned by identity for a default registry.

Memory

A memory_profiler run over an app mounting 13 APIs held 42 content-type-aware middleware (20 Formatter, 20 Error, 2 Versioner) covering four distinct content-type registrations:

live objects before → after held before → after
@content_types_lookup 42 → 4 7,488 B → 1,024 B
@mime_types 41 → 3 6,560 B → 480 B
@content_types 21 → 21 3,360 B (unchanged)

17.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 InheritableSetting builds fresh per API instance; deduplicating that belongs in InheritableSetting, 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_for now 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 frozen MIME_TYPES constant. 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 through middleware.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

  • Full RSpec suite passes locally (2657 examples, 0 failures).
  • RuboCop clean on the changed files.
  • Ran a real mounted app's suite against the patched gem; failures are byte-identical to the same run without this change (live-server integration specs and a grape-swagger parser error).
  • CI green.

🤖 Generated with Claude Code

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
ericproulx force-pushed the perf/share-content-type-tables branch from 3fb667c to bb9d8a9 Compare September 2, 2026 09:26
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

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