From 592fb1ca9fe47231440565abf5395d830b40a8fa Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Thu, 13 Oct 2022 07:04:57 +1100 Subject: [PATCH] Memoize config hash on finalize (#151) Freezing the hash is beneficial at this point because it saves repeated expensive computation if that hash is to be used later in performance-sensitive situations, such as when serving as a cache key or similar. --- Gemfile | 1 + lib/dry/configurable/config.rb | 17 ++++++++++++++ .../dry/configurable/config_spec.rb | 22 +++++++++++++++++++ spec/spec_helper.rb | 3 +++ 4 files changed, 43 insertions(+) diff --git a/Gemfile b/Gemfile index 512a2ec..1de5aca 100644 --- a/Gemfile +++ b/Gemfile @@ -19,4 +19,5 @@ end group :tools do gem "hotch", platform: :mri gem "pry-byebug", platform: :mri + gem "rspec-benchmark" end diff --git a/lib/dry/configurable/config.rb b/lib/dry/configurable/config.rb index 4758569..7678e10 100644 --- a/lib/dry/configurable/config.rb +++ b/lib/dry/configurable/config.rb @@ -146,6 +146,16 @@ module Dry values.to_h { |key, value| [key, value.is_a?(self.class) ? value.to_h : value] } end + # @api private + alias_method :_dry_equalizer_hash, :hash + + # @api public + def hash + return @__hash__ if instance_variable_defined?(:@__hash__) + + _dry_equalizer_hash + end + # @api private def finalize!(freeze_values: false) values.each_value do |value| @@ -156,6 +166,13 @@ module Dry end end + # Memoize the hash for the object when finalizing (regardless of whether values themselves + # are to be frozen; the intention of finalization is that no further changes should be + # made). The benefit of freezing the hash at this point is that it saves repeated expensive + # computation (through Dry::Equalizer's hash implementation) if that hash is to be used + # later in performance-sensitive situations, such as when serving as a cache key or similar. + @__hash__ = _dry_equalizer_hash unless frozen? + freeze end diff --git a/spec/integration/dry/configurable/config_spec.rb b/spec/integration/dry/configurable/config_spec.rb index 4b38b13..93a726f 100644 --- a/spec/integration/dry/configurable/config_spec.rb +++ b/spec/integration/dry/configurable/config_spec.rb @@ -239,6 +239,28 @@ RSpec.describe Dry::Configurable::Config do end end + describe "#hash" do + it "returns the integer hash value for the convig based on its values" do + klass.setting :db + + expect(klass.config.hash).to be_an_instance_of(Integer) + expect { klass.config.db = "sqlite" }.to change { klass.config.hash } + end + + it "is memoized when the config is finalized", :performance do + klass.setting :a + klass.setting :b + klass.setting :c + klass.setting :d + klass.setting :e + + finalized_config = klass.config.dup.finalize! + + expect(finalized_config.hash).to eq klass.config.hash + expect { finalized_config.hash }.to perform_faster_than { klass.config.hash }.at_least(50).times + end + end + describe "#method_missing" do it "provides access to reader methods" do klass.setting :hello diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d4b4c5c..b368d96 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,7 @@ require_relative "support/coverage" require "pathname" +require "rspec-benchmark" SPEC_ROOT = Pathname(__FILE__).dirname @@ -22,6 +23,8 @@ RSpec.configure do |config| config.disable_monkey_patching! config.filter_run_when_matching :focus + config.include RSpec::Benchmark::Matchers + config.around do |example| module Test end