Skip to content

Memoize version lookups in VersionGem::Api - #36

Open
joe-sharp wants to merge 2 commits into
ruby-oauth:mainfrom
joe-sharp:memoize-api-gaps
Open

joe-sharp wants to merge 2 commits into
ruby-oauth:mainfrom
joe-sharp:memoize-api-gaps

Conversation

@joe-sharp

@joe-sharp joe-sharp commented Nov 2, 2026 •

Copy link
Copy Markdown

Two memoization gaps in VersionGem::Api, fixed as separate commits.

  1. _to_a used = instead of ||= (⚡️ Memoize split version string in _to_a). The VERSION string was split again on every call to major, minor, patch, and pre.
  2. pre never cached nil (⚡️ Cache nil pre-release in pre). ||= treats nil as unset, so a version with no pre-release re-ran _to_a on every pre call. The fix checks defined?(@pre) instead.

Each commit adds a spec that fails without its fix, a Keep a Changelog entry under Unreleased → Fixed, and a DCO sign-off.

Summary

Cold lookups are about 30% faster, and warm pre on a version with no pre-release is about 9× faster. The split count drops from 4–6 per cold lookup to 1.

Benchmark

Ruby 4.0.9, 20,000 iterations per run, median of 7 runs. splits/op counts String#split calls per operation. Three interleaved runs of main and three of this branch, rerun on the same machine:

Scenario splits/op main → branch µs/op main (3 runs) µs/op branch (3 runs)
Cold lookup, 1.2.3 (major, minor, patch, pre, to_h, to_a) 6 → 1 15.47, 14.34, 14.34 10.35, 10.15, 10.34
Cold lookup, 1.2.3.pre-78 4 → 1 15.07, 13.55, 13.85 10.32, 10.40, 10.73
Cold lookup, Epoch 3012.34.56 6 → 1 18.27, 17.69, 18.30 13.75, 13.41, 13.42
Warm pre, 1.2.3 (no pre-release) 1 → 0 0.80, 0.81, 0.88 0.085, 0.084, 0.083
Warm pre, 1.2.3.pre-78 0 → 0 0.082, 0.083, 0.081 0.083, 0.083, 0.083
Warm major 0 → 0 0.084, 0.081, 0.081 0.080, 0.080, 0.081
Warm to_a (Epoch) 0 → 0 0.084, 0.084, 0.083 0.081, 0.083, 0.081

Takeaways

  • Cold lookups are about 30% faster (≈14.7 µs → ≈10.3 µs) and split the version string once instead of 4–6 times.
  • Warm pre on a version with no pre-release is about 9× faster (≈0.83 µs → ≈0.08 µs). This is the fix 2 win.
  • Warm lookups for cached values are unchanged, as expected.
  • Caveat: these were run on a laptop with other load, so absolute numbers are noisy. The split counts are exact.

Reproducing

The benchmark script is not in the repo. It loads lib/ directly, prepends a counter onto String#split, and times ITER calls of each scenario. Run it with:

ruby -I lib bench_api.rb <label>

Run it against a checkout of main (git archive main lib) and against this branch to compare.

Testing

  • bundle exec rspec: 32 examples, 0 failures.
  • bundle exec rubocop lib/version_gem/api.rb spec/version_gem/api_spec.rb: no offenses.
  • bundle exec kettle-test was not run, because mise is not installed in the environment used for this change. The full RSpec suite was run directly instead.

🤖 Generated with Claude Code

joe-sharp and others added 2 commits November 2, 2026 01:18
_to_a assigned with = instead of ||=, so the VERSION string was split
again on every call to major, minor, patch, and pre. Memoize it like
the other cached helpers.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: joe-sharp <joesharp13@gmail.com>
||= does not memoize nil, so pre re-ran _to_a on every call for
versions without a pre-release suffix. Check defined?(@pre) instead.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: joe-sharp <joesharp13@gmail.com>

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