Rendering Performance Improvements#566
Merged
lessthanjacob merged 12 commits intomainfrom Apr 9, 2026
Merged
Conversation
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
…s in Rendering Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
a5ef505 to
4f5168c
Compare
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
7d52d4f to
bbb86fa
Compare
rafafloresta
approved these changes
Feb 3, 2026
jhollinger
previously approved these changes
Feb 6, 2026
…ance-improvements Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
jhollinger
previously approved these changes
Mar 9, 2026
Replace three cache ivars (@Finalized, @cached_fields_for, @cached_transformers) with a single @cache reference that is either nil or a deeply frozen Hash. This provides the same lazy compilation behavior with two improvements: - Thread safety on JRuby/TruffleRuby without relying on boolean visibility guarantees (atomic reference assignment + frozen payload) - Zero lock overhead on the hot path after first compilation (nil check before mutex instead of always acquiring) Also freeze local_options_with_view in Rendering#prepare_data to prevent accidental mutation of the shared options hash across iterations. Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
jhollinger
approved these changes
Apr 9, 2026
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.
Background
#562 has surfaced a clear regression in recent versions of the library:
This has been compounding for awhile, and there are two core contributing factors:
viewis built each time thatviewis used, and we've been adding more processing in this path over time.renderprocess, we're allocating a lot extraHashinstances as a side-effect of managing and passing aroundlocal_options.While there are certainly a lot of opportunities for rework here, this PR aims to improve performance while avoiding too significant of a refactor (with the understanding that we'd like to put more emphasis on moving to a V2 version of the library in the near future).
Changelog
ViewCollectionto avoid recalculating the structure of each view at render time.@cachehash. Subsequent reads are a nil check on a single reference (no lock, no computation).inherit(subclassed blueprints) and when a new view is accessed for the first time (dynamic view creation).viewintolocal_optionsonce per render call, instead of every time a field is extracted. Freeze the merged hash to prevent accidental mutation across iterations.options_without_defaultHash inAssociationExtractorif:defaultor:default_ifkeys are present.Configuration#default_extractorand reuse it for eachField.AssociationExtractorper Blueprint and reuse it for eachAssociation.Metrics/MethodLengthto 20.ViewCollectionCachingThere were a few options considered for caching fields/transformers in
ViewCollection:TracePointto build the full cache when aBlueprintclass definition ends.Option 1 struck the best balance between consistency/safety and code simplicity, at the expense of negligible overhead when the first
renderoccurs.Option 2 incurs less overhead, but requires more complexity for consistent cache handling and thread-safety.
Option 3 puts the cache building in a spot where we wouldn't add any overhead to
rendercalls, but is more complex and harder to reason about.Thread Safety
The cache uses a frozen snapshot pattern: on first access, all view structures are compiled into a single, deeply frozen Hash (
{ fields: { ... }.freeze, transformers: { ... }.freeze }.freeze) and assigned to@cachein a single reference write. AMutexserializes the compilation itself to prevent duplicate work, but once@cacheis populated, reads bypass the mutex entirely (justreturn if @cache).This approach is safe across all Ruby engines (MRI, JRuby, TruffleRuby). The earlier implementation used a
@finalizedboolean flag with a double-checked lock, which is formally unsafe on JRuby: an unlocked read of a boolean ivar has no memory visibility guarantee under the JVM memory model. The frozen snapshot avoids this because object reference assignment is atomic, and the snapshot itself is immutable once published.Introducing a library like
concurrent-rubycould also work here, but given the simplicity of the use case, the current approach felt sufficient.Benchmarks
Using the same benchmark in the linked issue:
Comparisons
Speed (Iterations/Second)
Memory Usage