Skip to content

Memoize nested attribute classes - #552

Open
marekls wants to merge 5 commits into
okuramasafumi:mainfrom
marekls:nested-attribute-class-once
Open

Memoize nested attribute classes#552
marekls wants to merge 5 commits into
okuramasafumi:mainfrom
marekls:nested-attribute-class-once

Conversation

@marekls

@marekls marekls commented Jul 28, 2026

Copy link
Copy Markdown

Problem

NestedAttribute#value built a fresh subclass, reset its attributes, applied the key transformation and evaluated the block for every object it serialized. None of that depends on the object, so a nested attribute got more expensive the more objects a resource serialized.

Solution

The class is built once and memoized on the attribute. The key_transformation setter, which transform_keys! calls after class definition, drops the memo so the next serialization picks up the new transformation.

Behavioural change

A nested attribute body is evaluated once instead of once per serialized object, so side effects or runtime conditionals inside it no longer run for every object.

Benchmark

benchmark/nested_attributes.rb (added in this PR) serializes 100 posts and compares the same JSON produced with the nested hash built by hand in a plain attribute vs produced by nested_attribute, for a plain attribute and for an association with 20 comments per post. The script asserts both sides serialize the same string. Both runs used it on Ruby 4.0.5 (arm64, YJIT). Manual is the baseline, is unaffected by this PR, and each run carries its own manual figure — a nested figure is only meaningful next to the manual one from the same run.

Speed (higher is better):

before: manual before: nested after: manual after: nested
plain attribute 21 835 i/s 1071 i/s (20.4x slower) 22 316 i/s 13 214 i/s (1.69x slower)
association 883 i/s 416 i/s (2.13x slower) 1178 i/s 1031 i/s (1.14x slower)

Allocated memory per serialization (lower is better):

before: manual before: nested after: manual after: nested
plain attribute 45 368 B 369 368 B (8.14x more) 45 368 B 81 368 B (1.79x more)
association 647 361 B 1 061 761 B (1.64x more) 647 361 B 703 361 B (1.09x more)

End to end: a nested attribute carrying a plain attribute goes from 1071 i/s to 13 214 i/s (12.3x) and from 369 kB to 81 kB per serialization, one carrying an association from 416 i/s to 1031 i/s (2.5x) and from 1.06 MB to 703 kB.

A nested attribute still costs 1.69x the hand-built hash: a nested resource is instantiated per object and its hash merged, so this removes the cost of building the class, not the cost of the instance.

Tests

New tests cover the nested attribute body being evaluated once however many objects are serialized, and transform_keys! after a first serialization still taking effect.

Summary by CodeRabbit

  • Performance
    • Improved nested attribute serialization by caching the generated nested resource so nested attribute bodies are evaluated once and reused across serialized objects.
  • Bug Fixes
    • Nested attribute output now updates correctly when key transformation settings change.
  • Tests
    • Added coverage to ensure nested attribute blocks evaluate once and re-evaluate when key transformations change.
  • Documentation
    • Updated the changelog to reflect the nested attribute serialization improvement.
  • Chores
    • Refreshed nested-attribute benchmark coverage and updated related type signatures.

marekls added 3 commits July 28, 2026 12:08
`NestedAttribute#value` built a fresh subclass, reset its attributes,
applied the key transformation and evaluated the block on every object
it serialized, so a nested attribute got more expensive the more objects
a resource serialized.

None of that work depends on the object, so the class is built once and
memoized on the attribute. The `key_transformation` setter, which
`transform_keys!` calls after class definition, drops the memo so the
next serialization picks up the new transformation.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Nested attribute resource classes are lazily memoized and invalidated when key transformations change. Tests cover evaluation behavior, while benchmarks compare manual and nested serialization scenarios.

Changes

Nested attribute memoization

Layer / File(s) Summary
Resource-class caching and typed API
lib/alba/nested_attribute.rb, sig/alba/nested_attribute.rbs
Nested attributes lazily build and reuse resource classes, reset the cache after key transformation changes, and declare the updated signatures.
Serialization behavior coverage
test/usecases/nested_attribute_test.rb
Tests verify one block evaluation across multiple objects and re-evaluation after key transformation changes.
Benchmark scenarios and release notes
benchmark/nested_attributes.rb, CHANGELOG.md
Benchmarks compare manual and nested attribute and association serialization, and the changelog documents the updated behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Poem

I cached the class beneath the tree,
So nested leaves bloom once for thee.
Transform the keys; I’ll build anew,
With tests and benchmarks hopping through.
“Less repeated work!” the bunny sings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: memoizing nested attribute classes instead of rebuilding them repeatedly.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmark/nested_attributes.rb`:
- Around line 3-4: Update the benchmark resource definitions so each flat case
and its nested counterpart serialize the same output shape, including the
details wrapper and placement of body/comments. Keep the comparison pairs
aligned throughout the benchmark rather than measuring root-level versus nested
representations; update the description only if equivalent shapes cannot be
preserved.

In `@lib/alba/nested_attribute.rb`:
- Around line 21-23: Synchronize key_transformation= and resource_class with the
same lock so updating the transformation and invalidating the cached resource
class occur atomically. Protect lazy resource_class construction as well,
ensuring concurrent first access evaluates the transformation block only once
and never caches a class built from an outdated transformation. Add a concurrent
regression test covering transformation updates during lazy construction.

In `@sig/alba/nested_attribute.rbs`:
- Around line 22-24: Update the RBS declarations for
NestedAttribute#resource_class and `#build_resource_class` by adding private
visibility before both methods, matching their visibility in the Ruby
implementation and preventing typed callers from invoking them publicly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a55cd4f-be24-45e4-826c-5793e7057451

📥 Commits

Reviewing files that changed from the base of the PR and between ac1db73 and 155ca2c.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • benchmark/nested_attributes.rb
  • lib/alba/nested_attribute.rb
  • sig/alba/nested_attribute.rbs
  • test/usecases/nested_attribute_test.rb

Comment thread benchmark/nested_attributes.rb Outdated
Comment thread lib/alba/nested_attribute.rb
Comment thread sig/alba/nested_attribute.rbs Outdated
marekls added 2 commits July 28, 2026 12:31
The benchmark compared a nested attribute against a resource that emitted
the same values at the root, so the two sides produced different JSON and
part of what it measured was the wrapper hash rather than nesting itself.
The baseline now builds the nested hash by hand in a plain attribute, and
the script asserts both sides serialize the same string.
@marekls marekls changed the title Nested attribute class once Memoize nested attribute classes Jul 28, 2026
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