Skip to content

Build Product DSL ivar names once instead of per access - #439

Open
tas50 wants to merge 1 commit into
chef:mainfrom
tas50:perf/product-dsl
Open

Build Product DSL ivar names once instead of per access#439
tas50 wants to merge 1 commit into
chef:mainfrom
tas50:perf/product-dsl

Conversation

@tas50

@tas50 tas50 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Each of the 10 generated Product DSL property methods interpolated a String and interned a Symbol on every read and every write:

instance_variable_get("@#{prop}".to_sym)

prop is fixed at the moment the method is defined, so the Symbol can be built once in the enclosing block and captured by the closure.

Measurements

Ruby 4.0.6:

before after speedup allocations
lookup + omnibus_project 893 ns 499 ns 1.79x 5.04 → 3.04
lookup + install_path (default) 771 ns 555 ns 1.39x 6.02 → 4.02
products_available_on_downloads_site 17193 ns 11846 ns 1.45x
all 10 props × 32 products 266 µs 220 µs 1.21x 1401 → 983

Proc-valued properties are unchanged, as expected — they are dominated by the Mixlib::Versioning.parse call in version_for, not by the ivar lookup.

One thing I tried and rejected

I also tried replacing version.to_sym == :latest in ProductMatrix#lookup with a direct comparison, on the theory that interning a Symbol per lookup was wasteful.

It measured worse — allocations went 3.04 → 4.02 and the call got slower. This file has no frozen_string_literal comment, so the "latest" literal allocates a String on every call, while to_sym on an already-interned String is nearly free. That change is deliberately not included here.

Verification

  • 576 unit examples and the functional spec pass; chefstyle clean.
  • Explicit assertions across String properties, Proc properties resolving on both sides of a version boundary, computed defaults, :not_available, a missing key, and the PACKAGE_ROUTER_ENDPOINT environment override.
  • All 176 golden SHA256 digests match main.

Each of the 10 generated DSL property methods interpolated a String and
interned a Symbol on every read and every write:

  instance_variable_get("@#{prop}".to_sym)

`prop` is fixed when the method is defined, so the Symbol can be built once in
the enclosing block and captured by the closure.

Measured on Ruby 4.0.6:

  lookup + omnibus_project    893 ns -> 499 ns   1.79x   5.04 -> 3.04 allocations
  lookup + install_path       771 ns -> 555 ns   1.39x   6.02 -> 4.02 allocations
  products_available_...    17193 ns -> 11846 ns 1.45x
  all 10 props x 32 products  266 us -> 220 us   1.21x   1401 -> 983 allocations

Proc-valued properties are unchanged, as expected: they are dominated by the
Mixlib::Versioning.parse call in `version_for`, not by the ivar lookup.

I also tried replacing `version.to_sym == :latest` in ProductMatrix#lookup
with a direct comparison, on the theory that interning a Symbol per lookup was
wasteful. It measured worse -- allocations went 3.04 -> 4.02 and the call got
slower -- because this file has no frozen_string_literal comment, so the
"latest" literal allocates a String on every call while `to_sym` on an already
interned String is nearly free. That change is deliberately not included.

Output is unchanged: 576 unit examples pass and all 176 golden SHA256 digests
match main.

Signed-off-by: Tim Smith <tsmith84@proton.me>
@tas50
tas50 requested review from a team and jaymzh as code owners August 28, 2026 03:58
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