Skip to content

Add compiled views performance optimization - #613

Open
piotrekpasciak wants to merge 1 commit into
procore-oss:mainfrom
piotrekpasciak:perf/simple-compiled-views
Open

piotrekpasciak wants to merge 1 commit into
procore-oss:mainfrom
piotrekpasciak:perf/simple-compiled-views

Conversation

@piotrekpasciak

@piotrekpasciak piotrekpasciak commented Oct 22, 2026 •

Copy link
Copy Markdown

Checklist:

  • I have updated the necessary documentation
  • I have signed off all my commits as required by DCO
  • My build is green

Changes:

Standard rendering Compiled view
Rendering#object_to_hash (rendering.rb:160) The generated call(object, options), which builds the Hash directly.
Field#extract, Field#skip? (field.rb:16-24) Inlined into the generated method. if_callable / unless_callable are looked up once at compile time (ViewCompiler.field_conditions), and the procs are called directly.
AutoExtractor#extract, which picks an extractor per call Decided once at compile time in ViewCompiler.read.
PublicSendExtractor object.name, or object.public_send(REFS[n]) for unusual method names.
BlockExtractor REFS[n].call(object, **options) or REFS[n].call(object), using the same Symbol#to_proc check, done once.
HashExtractor Never used by the compiled path: Hash objects always fall back to object_to_hash.
DateTimeFormatter#format, EmptyTypes default logic Skipped. Fields with datetime_format, default or default_if aren't compiled, and the global versions turn off compiling for the whole render.
AssociationExtractor#extract (static blueprint:, no options:) ViewCompiler.render_association. It skips default handling, merge_association_options and the Proc-blueprint check.
Child Blueprint's hashify → prepare_data render_association calls the child's compiled view directly. The view is checked at compile time (blueprint.view?(view)). This is why the README warns that overridden hashify / prepare_data aren't called for associations.

Following message chain for each object, then each field makes current approach slow:

Rendering#object_to_hash                        
  └─ Field#skip?  → if_callable / unless_callable (field-level or global)
  └─ Field#extract
       └─ AutoExtractor#extract
            ├─ pick extractor: BlockExtractor | HashExtractor | PublicSendExtractor
            ├─ DateTimeFormatter#format
            └─ EmptyTypes#use_default_value? / default_value
       └─ (associations) AssociationExtractor#extract
            ├─ strip default/default_if, extract value, default handling
            ├─ merge_association_options, association_blueprint (Proc check)
            └─ child Blueprint.hashify → UnknownView check → pre_render → prepare_data → object_to_hash …
  └─ exclude_if_nil check, result_hash[name] = value
  └─ transformers

Benchmarks from repository:

Benchmarked with benchmark-ips on Ruby 3.3.10 (arm64) against three configs: main, this branch with compile_views off, and this branch with compile_views = true. Each config ran in its own process. Each config ran twice, interleaved, and the table shows the best run. Error was at most ±1%. All configs produce identical output for every scenario (checked by comparing an MD5 of the marshaled result).

Scenario main (i/s) branch, off branch, on off vs main on vs main
simple ×100 (3 fields) 5,456 5,506 113,114 1.01x 20.7x
medium ×100 (8 fields: block / if / name / exclude_if_nil) 1,795 1,804 22,348 1.00x 12.5x
nested ×100 (4 fields + 5-item association) 631 629 10,733 1.00x 17.0x
fallback ×100 (datetime_format / default fields) 3,040 3,054 6,993 1.00x 2.3x
single object (medium) 154,470 154,106 849,444 1.00x 5.5x
render to JSON, nested ×100 609 610 6,805 1.00x 11.2x

Takeaways

  • With compile_views off (the default), performance is the same as main, so nothing changes for anyone who doesn't opt in.
  • With it on, rendering is 2.3x–21x faster. Views made only of simple fields and associations gain the most.
  • Fields that use standard rendering (datetime_format, default, custom extractors) gain the least.

Caveats

  • The test objects are plain Ruby Structs. ActiveRecord attribute reads cost more, so the gains in real apps will probably be smaller.
  • Part of the nested-association gain comes from skipping the child Blueprint's hashify/prepare_data and from not copying the options Hash when the view is the same (see the README's "Compiled Views" section).

Benchmarks from a real back-end application:

Compile true without extensions:
#<Benchmark::Tms:0x000000017ca588e0
 @cstime=0.0,
 @cutime=0.0,
 @label="",
 @real=4.673728000023402,
 @stime=0.029916000000000054,
 @total=3.9254900000000035,
 @utime=3.8955740000000034>

Compile true with extensions:
#<Benchmark::Tms:0x000000017790db70
 @cstime=0.0,
 @cutime=0.0,
 @label="",
 @real=5.055712000001222,
 @stime=0.04298000000000002,
 @total=4.356721999999991,
 @utime=4.313741999999991>
[10] DEVELOPMENT(#<Rest::V1:

Compile false with extensions:
#<Benchmark::Tms:0x000000017a11d340
 @cstime=0.0,
 @cutime=0.0,
 @label="",
 @real=5.642139999894425,
 @stime=0.032307000000001196,
 @total=4.940421999999996,
 @utime=4.908114999999995>

Code used for benchmarking inside of the endpoint:

Benchmark.measure do
  100.times do
    Rest::Shared::Observations::ItemsBlueprint.render(
      paginated_observations,
      view: serializer_view,
      company: current_company,
      custom_status_enabled: observations_custom_status_enabled?,
      company_translation_service: company_translation_service,
      custom_field_objects: custom_field_objects,
      custom_field_grouped_by: custom_field_grouped_by,
      rolodex: lightweight_rolodex(assignee_login_information_ids(paginated_observations)),
      project_activations: project_activations_for_project,
      **permissions_serializer_options,
      **asset_ids_serializer_options(paginated_observations)
    )
  end
end

@piotrekpasciak
piotrekpasciak requested review from a team and ritikesh as code owners October 22, 2026 14:16
@piotrekpasciak
piotrekpasciak force-pushed the perf/simple-compiled-views branch from 90ad482 to e50dc5d Compare October 22, 2026 15:14
@piotrekpasciak piotrekpasciak changed the title [MM-9640] Add compiled views performance optimization Add compiled views performance optimization Oct 23, 2026
@piotrekpasciak
piotrekpasciak force-pushed the perf/simple-compiled-views branch from e50dc5d to b1eb4ac Compare October 23, 2026 08:40
Signed-off-by: Piotr Pasciak <piotrek.pasciak@gmail.com>
@piotrekpasciak
piotrekpasciak force-pushed the perf/simple-compiled-views branch from b1eb4ac to 4db8933 Compare October 23, 2026 08:54

This branch has not been deployed

No deployments
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