Repository navigation
Conversation
Hamdy
force-pushed
the
hamdy/perf
branch
2 times, most recently
from
October 23, 2026 16:00
9b4272d to
1430904
Compare
Hamdy
marked this pull request as draft
October 23, 2026 16:04
Hamdy
force-pushed
the
hamdy/perf
branch
4 times, most recently
from
October 24, 2026 02:23
b3d9d60 to
d2d69a0
Compare
prepare_data fetched the view's fields and transformers inside object_to_hash, so once per object in a collection. It now fetches them once and passes them down. Extensions#pre_render returns early when no extensions are registered; reduce with a block on an empty array still allocates two objects. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: hamdy <hamdy.abdelbadeea@procore.com>
BlockExtractor called block.parameters for every block field of every object to tell Symbol#to_proc blocks apart from regular blocks. Proc#parameters allocates three arrays per call. Symbol#to_proc is a native lambda taking (obj, *args), so it is the only kind of block that is a lambda, has arity -2, and has no Ruby source location. lambda? and arity never allocate, and plain DSL blocks fail the first one; only a user lambda with arity -2 reaches source_location, which builds a [file, line] array. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: hamdy <hamdy.abdelbadeea@procore.com>
Hamdy
force-pushed
the
hamdy/perf
branch
3 times, most recently
from
October 24, 2026 04:01
c0fccab to
a054994
Compare
Hamdy
marked this pull request as ready for review
October 27, 2026 12:00
DateTimeFormatter asks every extracted value whether it implements strftime, including strings and numbers. Skip that unless the field or the global config sets a format, and keep reading the global format on each extract so a later change still applies. Signed-off-by: hamdy <hamdy.abdelbadeea@procore.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The render sequence diagram showed fields_for and transformers being fetched inside the per-object loop; prepare_data now fetches them once per render. The extension hook example in making-changes.md quotes Extensions#pre_render, which now returns early with no extensions. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: hamdy <hamdy.abdelbadeea@procore.com>
piotrekpasciak
left a comment
There was a problem hiding this comment.
Can we add an entry to the CHANGELOG.md file? It may be needed for the upcoming release.
This branch has not been deployed
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.
Checklist:
bundle exec rakelocally)Summary
Removes allocations and repeated work from the render path. Output is unchanged. Rendering is about 5-11% faster per object on plain fields and 10-15% faster on the CI IPS benchmarks. Block fields render about 38% faster and allocate 89% less, and associations with a default render about 10% faster and allocate 29% less.
Changes
One commit per change.
Fields and transformers are looked up once per render.
prepare_dataused to fetch them insideobject_to_hash, so once per object in a collection.Extensions#pre_renderalso returns early when there are no extensions, sincereducewith a block allocates even on an empty array.Symbol#to_procblocks are detected withoutProc#parameters.BlockExtractorcalledblock.parametersfor every block field of every object, andProc#parametersallocates three arrays each time.Symbol#to_procis a native lambda taking(obj, *args), so it is the only kind of block that is a lambda, has arity -2, and has no Ruby source location.lambda?andaritynever allocate, and plain DSL blocks stop at the first check; only a user lambda with arity -2 reachessource_location. Lambdas like->(obj, **opts)andMethod#to_procblocks still getlocal_options.Datetime formatting is skipped when no format is set.
AutoExtractorasked every extracted value, including strings and numbers, whether it responds tostrftime. It now calls the formatter only when the field or the global config sets a format. The global format is still read on every extract, so setting it after the first render still works, andconfig.datetime_format = falsestill raises as before. With no format the formatter returned the value unchanged anyway, so output is the same.Not included
Earlier revisions of this PR also reused the parent's
local_optionsfor a nested render with the same view, builtlocal_optionsonce per render, storedexclude_if_nilon theField, and memoized per-block and per-association results on the shared extractors. Those are dropped: the first two let a nested transformer's write tolocal_optionsleak into the parent's remaining fields, the third hid an option change made by a custom extractor, and the memos either mutated a VM-sharedSymbol#to_procobject or wrote to an unsynchronizedHashfrom renders for no measurable gain over the uncached check. The nestedlocal_optionsbehavior is covered inspec/integrations/base_spec.rb.Scope
CHANGELOG.mdhas the[REFACTOR]entry under## Unreleased.docs/architecture.md: render walkthrough, sequence diagram, and render performance rules match the code.docs/configuration.md(datetime_format),docs/extension-points.md(nestedlocal_optionsis a copy), anddocs/making-changes.md(exclude_if_nilread after extraction) are updated. Each doc'sgeneratedfield is bumped.Benchmarks
Measured against
mainat 29069ec (which includes #608) on the same machine, Ruby 3.3.9, alternatingmainand this branch over 5 rounds. The numbers are medians. The CI benchmarks are the ones underspec/benchmarks/plusthroughput_test.rbandallocations_test.rbfromhamdy/benchmarks(#614), run in two worktrees with the same benchmark files. This branch was faster in all 5 paired rounds on ActiveRecord IPS, on every throughput shape, and on the 1,000- and 10,000-object Basic Big-O rows, and in 4 of 5 on Basic IPS and the remaining Big-O rows. The fixtures use plainStringfields, so they show only the general-path gain; the targeted probe covers block fields and associations.Targeted probe (1,000
Structobjects,render_as_hash, allocations counted after one warm-up render; time per render is the best of five batches of 50, median over 5 rounds)&:symfieldsdefault:Iterations per second (higher is better)
Renders per second (higher is better)
Microseconds per object (lower is better)
Big-O render time, ms (lower is better)
Allocations per render (lower is better)
The Big-O rows at 100 objects are one ~0.2-0.3 ms sample per round and the two sides overlap (Basic: main 0.215-0.291 ms, this branch 0.181-0.259 ms; ActiveRecord: main 0.268-0.389 ms, this branch 0.251-0.287 ms), so read the 1,000 and 10,000 rows.
Tests
spec/units/block_extractor_spec.rb:Symbol#to_procblocks get only the object; plain blocks, lambdas (including->(obj, *rest)), andMethod#to_procblocks still receivelocal_options.spec/units/auto_extractor_spec.rb: with no format the formatter is skipped and field defaults still apply. Field and global formats still format dates and any object withstrftime, a bad value still raises,datetime_format = falsestill raises, and a global format set after an earlier extract still applies.spec/integrations/base_spec.rb: fields and transformers are fetched once for a collection, and a collection render allocates one object (the result hash) per rendered object. Render options are not mutated,pre_rendersees options without:view/:root/:meta, nested view and:optionsmerging still work, and a nested transformer's write tolocal_optionsdoes not reach the parent's later fields, whether or not the views match.spec/units/extensions_spec.rb: the empty-extensions case.