memo_wise 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4c330faa7d0718ac3b4873dae8fb1933949481747f639fd6fe050724a2ce8dd
4
- data.tar.gz: 52777a008d6b5b41a37cec36f1b9fe0198b85df97fb11329d73bac71ad22b600
3
+ metadata.gz: 2fa12aeb35a6c4ca923eb23d70867b41600795143570f3d7c5b6444c76547980
4
+ data.tar.gz: bac4074e758731ac5d20925ab32ef3cd78d10a1c030228df76c1868e104e1a40
5
5
  SHA512:
6
- metadata.gz: 17a0e939940e73826fbca1876317bcd772e1cd07f16383a9d3e9c973c2545b4b8539c42110de98d8a8c8c4c4a4aa6a666b29cc40c6351001b2b9a7c6964918a3
7
- data.tar.gz: fd4a09fe2dab6dbae43fae753c15530982d1cc26c0ef7b47cdff40f0bff603d7c7c9d0c7d0509b7eb98c0e15b2bd2ca7c005900a00d97201e44a9fd49603d4b0
6
+ metadata.gz: 52e8b1543bca54371a2743fbc22b3cee32ba68a050ac414e373ca2027faba214956bca3aef61a05d25ca1d339b3e4ae01a417cdb5d1a4a407fba16b0a3cae56e
7
+ data.tar.gz: 1bc40ccaab12ed598cc86d62d51ca8081a386b208559ac49788ff87601c45809958c2e936692fbb2e78f3ff0f04b124636f795d941558239fc20edf04f7b979b
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  /.bundle/
2
2
  /benchmarks/.bundle/
3
+ /benchmarks/Gemfile.lock
3
4
  /.yardoc
4
5
  /_yardoc/
5
6
  /coverage/
data/.rubocop.yml CHANGED
@@ -1,2 +1,5 @@
1
1
  inherit_gem:
2
2
  panolint: rubocop.yml
3
+
4
+ Style/DocumentDynamicEvalDefinition:
5
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -5,8 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
9
- (nothing yet!)
8
+ ## Unreleased
9
+ ### Updated
10
+ - (Nothing, yet)
11
+ ### Fixed
12
+ - (Nothing, yet)
13
+ ### Breaking Changes
14
+ - None
15
+
16
+ ## [1.1.0] - 2021-07-29
17
+ ### Updated
18
+ - Improve performance across the board by:
19
+ - removing `Hash#fetch`
20
+ - using `Array#hash`
21
+ - avoiding multi-layer hash lookups for multi-argument methods
22
+ - optimizing for truthy results
23
+ - Add `dry-core` to benchmarks in README
24
+ ### Fixed
25
+ - Fix usage on module singleton classes
26
+ - Fix usage on module which would be extended by other classes
27
+ ### Breaking Changes
28
+ - None
10
29
 
11
30
  ## [1.0.0] - 2021-06-24
12
31
  ### Added
@@ -61,7 +80,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
61
80
  - Panolint
62
81
  - Dependabot setup
63
82
 
64
- [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...HEAD
83
+ [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...HEAD
84
+ [1.1.0]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...v1.1.0
65
85
  [1.0.0]: https://github.com/panorama-ed/memo_wise/compare/v0.4.0...v1.0.0
66
86
  [0.4.0]: https://github.com/panorama-ed/memo_wise/compare/v0.3.0...v0.4.0
67
87
  [0.3.0]: https://github.com/panorama-ed/memo_wise/compare/v0.2.0...v0.3.0
data/Gemfile.lock CHANGED
@@ -1,6 +1,6 @@
1
1
  GIT
2
2
  remote: https://github.com/panorama-ed/panolint.git
3
- revision: 6f38f73dafdc25370ecc367dc87d7e6340ae664d
3
+ revision: c709ebcc5fd9593db959df4a92c12cae1d1fb9af
4
4
  branch: main
5
5
  specs:
6
6
  panolint (0.1.3)
@@ -14,7 +14,7 @@ GIT
14
14
  PATH
15
15
  remote: .
16
16
  specs:
17
- memo_wise (1.0.0)
17
+ memo_wise (1.1.0)
18
18
 
19
19
  GEM
20
20
  remote: https://rubygems.org/
@@ -26,7 +26,7 @@ GEM
26
26
  tzinfo (~> 1.1)
27
27
  ansi (1.5.0)
28
28
  ast (2.4.2)
29
- brakeman (5.0.4)
29
+ brakeman (5.1.1)
30
30
  codecov (0.5.2)
31
31
  simplecov (>= 0.15, < 0.22)
32
32
  concurrent-ruby (1.1.9)
@@ -40,11 +40,11 @@ GEM
40
40
  concurrent-ruby (~> 1.0)
41
41
  minitest (5.14.4)
42
42
  parallel (1.20.1)
43
- parser (3.0.1.1)
43
+ parser (3.0.2.0)
44
44
  ast (~> 2.4.1)
45
45
  rack (2.2.3)
46
46
  rainbow (3.0.0)
47
- rake (13.0.3)
47
+ rake (13.0.6)
48
48
  redcarpet (3.5.1)
49
49
  regexp_parser (2.1.1)
50
50
  rexml (3.2.5)
data/README.md CHANGED
@@ -103,27 +103,43 @@ For more usage details, see our detailed [documentation](#documentation).
103
103
  ## Benchmarks
104
104
 
105
105
  Benchmarks measure memoized value retrieval time using
106
- [`benchmark-ips`](https://github.com/evanphx/benchmark-ips). All benchmarks are
107
- run on Ruby 3.0.1, except as indicated below for specific gems. Benchmarks are
106
+ [`benchmark-ips`](https://github.com/evanphx/benchmark-ips). Benchmarks are
108
107
  run in GitHub Actions and updated in every PR that changes code.
109
108
 
110
109
  **Values >1.00x represent how much _slower_ each gem’s memoized value retrieval
111
- is than the latest commit of `memo_wise`.**
112
-
113
- |Method arguments|`memery` (1.4.0)|`memoist`\* (0.16.2)|`memoized`\* (1.0.2)|`memoizer`\* (1.0.3)|
114
- |--|--|--|--|--|
115
- |`()` (none)|13.17x|2.64x|1.46x|3.10x|
116
- |`(a)`|9.76x|15.44x|11.98x|13.12x|
117
- |`(a, b)`|1.98x|2.25x|1.82x|1.98x|
118
- |`(a:)`|17.65x|23.64x|20.69x|21.61x|
119
- |`(a:, b:)`|4.16x|3.94x|3.53x|3.67x|
120
- |`(a, b:)`|3.96x|3.72x|3.27x|3.42x|
121
- |`(a, *args)`|1.93x|2.25x|1.93x|1.95x|
122
- |`(a:, **kwargs)`|3.06x|2.38x|2.10x|2.20x|
123
- |`(a, *args, b:, **kwargs)`|1.52x|1.79x|1.65x|1.65x|
124
-
125
- _\*Indicates a benchmark run on Ruby 2.7.3 because the gem raises errors in Ruby
126
- 3.0.1 due to its incorrect handling of keyword arguments._
110
+ is than the baseline.**
111
+
112
+ Benchmarks using Ruby 3.0.2:
113
+
114
+ |Method arguments|`memo_wise` (latest)|`dry-core` (0.7.1)|`memery` (1.4.0)|
115
+ |--|--|--|--|
116
+ |`()` (none)|baseline|0.98-1.44x|10.40-14.95x|
117
+ |`(a)`|baseline|2.10x|8.39x|
118
+ |`(a, b)`|baseline|baseline\*|4.45x|
119
+ |`(a:)`|baseline|1.89x|17.37x|
120
+ |`(a:, b:)`|baseline|baseline\*|9.66x|
121
+ |`(a, b:)`|baseline|baseline\*|8.78x|
122
+ |`(a, *args)`|baseline|1.88x|4.07x|
123
+ |`(a:, **kwargs)`|baseline|1.41x|4.98x|
124
+ |`(a, *args, b:, **kwargs)`|baseline|baseline\*|2.50x|
125
+
126
+ \*Indicates a run that was slower than the baseline but the difference was not
127
+ significant.
128
+
129
+ The following benchmarks are run on Ruby 2.7.4 because these gems raise errors
130
+ in Ruby 3.0.2 due to their incorrect handling of keyword arguments:
131
+
132
+ |Method arguments|`memo_wise` (latest)|`ddmemoize` (1.0.0)|`memoist` (0.16.2)|`memoized` (1.0.2)|`memoizer` (1.0.3)|
133
+ |--|--|--|--|--|--|
134
+ |`()` (none)|baseline|20.96-25.44x|2.49-3.31x|1.06-1.48x|2.96-3.83x|
135
+ |`(a)`|baseline|19.37x|13.28x|9.31x|10.78x|
136
+ |`(a, b)`|baseline|3.65x|2.64x|2.00x|2.28x|
137
+ |`(a:)`|baseline|25.88x|21.74x|17.68x|19.17x|
138
+ |`(a:, b:)`|baseline|5.72x|4.57x|3.90x|4.42x|
139
+ |`(a, b:)`|baseline|5.63x|5.06x|4.20x|4.18x|
140
+ |`(a, *args)`|baseline|5.13x|3.89x|3.25x|3.31x|
141
+ |`(a:, **kwargs)`|baseline|3.23x|2.55x|2.33x|2.47x|
142
+ |`(a, *args, b:, **kwargs)`|baseline|2.51x|2.00x|1.84x|1.88x|
127
143
 
128
144
  You can run benchmarks yourself with:
129
145
 
@@ -185,6 +201,13 @@ memoization between your tests with something like:
185
201
  after(:each) { helper.reset_memo_wise }
186
202
  ```
187
203
 
204
+ ## Further Reading
205
+
206
+ We've written more about MemoWise in a series of blog posts:
207
+
208
+ - [Introducing: MemoWise](https://medium.com/building-panorama-education/introducing-memowise-51a5f0523489)
209
+ - [Optimizing MemoWise Performance](https://ja.cob.land/optimizing-memowise-performance)
210
+
188
211
  ## Logo
189
212
 
190
213
  `MemoWise`'s logo was created by [Luci Cooke](https://www.lucicooke.com/). The
data/benchmarks/Gemfile CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- ruby ">= 2.7.2"
5
+ ruby ">= 2.7.4"
6
6
 
7
7
  gem "benchmark-ips", "2.9.1"
8
8
 
9
9
  if RUBY_VERSION > "3"
10
+ gem "dry-core", "0.7.1"
10
11
  gem "memery", "1.4.0"
11
12
  else
13
+ gem "ddmemoize", "1.0.0"
12
14
  gem "memoist", "0.16.2"
13
15
  gem "memoized", "1.0.2"
14
16
  gem "memoizer", "1.0.3"
@@ -6,13 +6,16 @@ require "memo_wise"
6
6
 
7
7
  # Some gems do not yet work in Ruby 3 so we only require them if they're loaded
8
8
  # in the Gemfile.
9
- %w[memery memoist memoized memoizer].
9
+ %w[memery memoist memoized memoizer ddmemoize dry-core].
10
10
  each { |gem| require gem if Gem.loaded_specs.key?(gem) }
11
11
 
12
12
  # The VERSION constant does not get loaded above for these gems.
13
13
  %w[memoized memoizer].
14
14
  each { |gem| require "#{gem}/version" if Gem.loaded_specs.key?(gem) }
15
15
 
16
+ # The Memoizable module from dry-core needs to be required manually
17
+ require "dry/core/memoizable" if Gem.loaded_specs.key?("dry-core")
18
+
16
19
  class BenchmarkSuiteWithoutGC
17
20
  def warming(*)
18
21
  run_gc
@@ -36,7 +39,7 @@ class BenchmarkSuiteWithoutGC
36
39
  end
37
40
  suite = BenchmarkSuiteWithoutGC.new
38
41
 
39
- BenchmarkGem = Struct.new(:klass, :inheritance_method, :memoization_method) do
42
+ BenchmarkGem = Struct.new(:klass, :activation_code, :memoization_method) do
40
43
  def benchmark_name
41
44
  "#{klass} (#{klass::VERSION})"
42
45
  end
@@ -46,70 +49,84 @@ end
46
49
  # using it to minimize the chance that our benchmarks are affected by ordering.
47
50
  # NOTE: Some gems do not yet work in Ruby 3 so we only test with them if they've
48
51
  # been `require`d.
52
+ # rubocop:disable Layout/LineLength
49
53
  BENCHMARK_GEMS = [
50
- BenchmarkGem.new(MemoWise, :prepend, :memo_wise),
51
- (BenchmarkGem.new(Memery, :include, :memoize) if defined?(Memery)),
52
- (BenchmarkGem.new(Memoist, :extend, :memoize) if defined?(Memoist)),
53
- (BenchmarkGem.new(Memoized, :include, :memoize) if defined?(Memoized)),
54
- (BenchmarkGem.new(Memoizer, :include, :memoize) if defined?(Memoizer))
54
+ BenchmarkGem.new(MemoWise, "prepend MemoWise", :memo_wise),
55
+ (BenchmarkGem.new(DDMemoize, "DDMemoize.activate(self)", :memoize) if defined?(DDMemoize)),
56
+ (BenchmarkGem.new(Dry::Core, "include Dry::Core::Memoizable", :memoize) if defined?(Dry::Core)),
57
+ (BenchmarkGem.new(Memery, "include Memery", :memoize) if defined?(Memery)),
58
+ (BenchmarkGem.new(Memoist, "extend Memoist", :memoize) if defined?(Memoist)),
59
+ (BenchmarkGem.new(Memoized, "include Memoized", :memoize) if defined?(Memoized)),
60
+ (BenchmarkGem.new(Memoizer, "include Memoizer", :memoize) if defined?(Memoizer))
55
61
  ].compact.shuffle
62
+ # rubocop:enable Layout/LineLength
56
63
 
57
64
  # Use metaprogramming to ensure that each class is created in exactly the
58
65
  # the same way.
59
66
  BENCHMARK_GEMS.each do |benchmark_gem|
60
67
  # rubocop:disable Security/Eval
61
- # rubocop:disable Style/DocumentDynamicEvalDefinition
62
68
  eval <<-CLASS, binding, __FILE__, __LINE__ + 1
69
+ # For these methods, we alternately return truthy and falsey values in
70
+ # order to benchmark memoization when the result of a method is falsey.
71
+ #
72
+ # We do this by checking if the first argument to a method is even.
63
73
  class #{benchmark_gem.klass}Example
64
- #{benchmark_gem.inheritance_method} #{benchmark_gem.klass}
74
+ #{benchmark_gem.activation_code}
65
75
 
66
76
  def no_args
67
77
  100
68
78
  end
69
79
  #{benchmark_gem.memoization_method} :no_args
70
80
 
81
+ # For the no_args case, we can't depend on arguments to alternate between
82
+ # returning truthy and falsey values, so instead make two separate
83
+ # no_args methods
84
+ def no_args_falsey
85
+ nil
86
+ end
87
+ #{benchmark_gem.memoization_method} :no_args_falsey
88
+
71
89
  def one_positional_arg(a)
72
- 100
90
+ 100 if a.even?
73
91
  end
74
92
  #{benchmark_gem.memoization_method} :one_positional_arg
75
93
 
76
94
  def positional_args(a, b)
77
- 100
95
+ 100 if a.even?
78
96
  end
79
97
  #{benchmark_gem.memoization_method} :positional_args
80
98
 
81
99
  def one_keyword_arg(a:)
82
- 100
100
+ 100 if a.even?
83
101
  end
84
102
  #{benchmark_gem.memoization_method} :one_keyword_arg
85
103
 
86
104
  def keyword_args(a:, b:)
87
- 100
105
+ 100 if a.even?
88
106
  end
89
107
  #{benchmark_gem.memoization_method} :keyword_args
90
108
 
91
109
  def positional_and_keyword_args(a, b:)
92
- 100
110
+ 100 if a.even?
93
111
  end
94
112
  #{benchmark_gem.memoization_method} :positional_and_keyword_args
95
113
 
96
114
  def positional_and_splat_args(a, *args)
97
- 100
115
+ 100 if a.even?
98
116
  end
99
117
  #{benchmark_gem.memoization_method} :positional_and_splat_args
100
118
 
101
119
  def keyword_and_double_splat_args(a:, **kwargs)
102
- 100
120
+ 100 if a.even?
103
121
  end
104
122
  #{benchmark_gem.memoization_method} :keyword_and_double_splat_args
105
123
 
106
124
  def positional_splat_keyword_and_double_splat_args(a, *args, b:, **kwargs)
107
- 100
125
+ 100 if a.even?
108
126
  end
109
127
  #{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
110
128
  end
111
129
  CLASS
112
- # rubocop:enable Style/DocumentDynamicEvalDefinition
113
130
  # rubocop:enable Security/Eval
114
131
  end
115
132
 
@@ -130,7 +147,26 @@ Benchmark.ips do |x|
130
147
  # retrieval time.
131
148
  instance.no_args
132
149
 
133
- x.report("#{benchmark_gem.benchmark_name}: ()") { instance.no_args }
150
+ x.report("#{benchmark_gem.benchmark_name}: () => truthy") do
151
+ instance.no_args
152
+ end
153
+ end
154
+
155
+ x.compare!
156
+ end
157
+
158
+ Benchmark.ips do |x|
159
+ x.config(suite: suite)
160
+ BENCHMARK_GEMS.each do |benchmark_gem|
161
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
162
+
163
+ # Run once to memoize the result value, so our benchmark only tests memoized
164
+ # retrieval time.
165
+ instance.no_args_falsey
166
+
167
+ x.report("#{benchmark_gem.benchmark_name}: () => falsey") do
168
+ instance.no_args_falsey
169
+ end
134
170
  end
135
171
 
136
172
  x.compare!
data/lib/memo_wise.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  require "memo_wise/internal_api"
4
6
  require "memo_wise/version"
5
7
 
@@ -120,6 +122,23 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
120
122
  MemoWise::InternalAPI.original_class_from_singleton(klass)
121
123
  )
122
124
  end
125
+
126
+ # Ensures a module extended by another class/module still works
127
+ # e.g. rails `ClassMethods` module
128
+ if klass.is_a?(Module) && !klass.is_a?(Class)
129
+ # Using `extended` without `included` & `prepended`
130
+ # As a call to `create_memo_wise_state!` is already included in
131
+ # `.allocate`/`#initialize`
132
+ #
133
+ # But a module/class extending another module with memo_wise
134
+ # would not call `.allocate`/`#initialize` before calling methods
135
+ #
136
+ # On method call `@_memo_wise` would still be `nil`
137
+ # causing error when fetching cache from `@_memo_wise`
138
+ def klass.extended(base)
139
+ MemoWise::InternalAPI.create_memo_wise_state!(base)
140
+ end
141
+ end
123
142
  when Hash
124
143
  unless method_name_or_hash.keys == [:self]
125
144
  raise ArgumentError,
@@ -154,14 +173,11 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
154
173
  # hash key is just the method name.
155
174
  if method.arity.zero?
156
175
  klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
157
- # def foo
158
- # @_memo_wise.fetch(:foo) do
159
- # @_memo_wise[:foo] = _memo_wise_original_foo
160
- # end
161
- # end
162
-
163
176
  def #{method_name}
164
- @_memo_wise.fetch(:#{method_name}) do
177
+ output = @_memo_wise[:#{method_name}]
178
+ if output || @_memo_wise.key?(:#{method_name})
179
+ output
180
+ else
165
181
  @_memo_wise[:#{method_name}] = #{original_memo_wised_name}
166
182
  end
167
183
  end
@@ -176,12 +192,14 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
176
192
  type == :req ? name : "#{name}: #{name}"
177
193
  end.join(", ")
178
194
  call_str = "(#{call_str})"
179
- fetch_key = method.parameters.map(&:last)
180
- fetch_key = if fetch_key.size > 1
181
- "[#{fetch_key.join(', ')}].freeze"
182
- else
183
- fetch_key.first.to_s
184
- end
195
+ fetch_key_params = method.parameters.map(&:last)
196
+ if fetch_key_params.size > 1
197
+ fetch_key_init =
198
+ "[:#{method_name}, #{fetch_key_params.join(', ')}].hash"
199
+ use_hashed_key = true
200
+ else
201
+ fetch_key = fetch_key_params.first.to_s
202
+ end
185
203
  else
186
204
  # If our method has arguments, we need to separate out our handling
187
205
  # of normal args vs. keyword args due to the changes in Ruby 3.
@@ -192,37 +210,46 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
192
210
 
193
211
  if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
194
212
  args_str = "(*args, **kwargs)"
195
- fetch_key = "[args, kwargs].freeze"
213
+ fetch_key_init = "[:#{method_name}, args, kwargs].hash"
214
+ use_hashed_key = true
196
215
  elsif has_arg
197
216
  args_str = "(*args)"
198
- fetch_key = "args"
217
+ fetch_key_init = "args.hash"
199
218
  else
200
219
  args_str = "(**kwargs)"
201
- fetch_key = "kwargs"
220
+ fetch_key_init = "kwargs.hash"
202
221
  end
203
222
  end
204
223
 
205
- # Note that we don't need to freeze args before using it as a hash key
206
- # because Ruby always copies argument arrays when splatted.
207
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
208
- # def foo(*args, **kwargs)
209
- # hash = @_memo_wise.fetch(:foo) do
210
- # @_memo_wise[:foo] = {}
211
- # end
212
- # hash.fetch([args, kwargs].freeze) do
213
- # hash[[args, kwargs].freeze] = _memo_wise_original_foo(*args, **kwargs)
214
- # end
215
- # end
216
-
217
- def #{method_name}#{args_str}
218
- hash = @_memo_wise.fetch(:#{method_name}) do
219
- @_memo_wise[:#{method_name}] = {}
224
+ if use_hashed_key
225
+ klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
226
+ def #{method_name}#{args_str}
227
+ key = #{fetch_key_init}
228
+ output = @_memo_wise[key]
229
+ if output || @_memo_wise.key?(key)
230
+ output
231
+ else
232
+ hashes = (@_memo_wise_hashes[:#{method_name}] ||= Set.new)
233
+ hashes << key
234
+ @_memo_wise[key] = #{original_memo_wised_name}#{call_str || args_str}
235
+ end
220
236
  end
221
- hash.fetch(#{fetch_key}) do
222
- hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
237
+ END_OF_METHOD
238
+ else
239
+ fetch_key ||= "key"
240
+ klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
241
+ def #{method_name}#{args_str}
242
+ hash = (@_memo_wise[:#{method_name}] ||= {})
243
+ #{"key = #{fetch_key_init}" if fetch_key_init}
244
+ output = hash[#{fetch_key}]
245
+ if output || hash.key?(#{fetch_key})
246
+ output
247
+ else
248
+ hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
249
+ end
223
250
  end
224
- end
225
- END_OF_METHOD
251
+ END_OF_METHOD
252
+ end
226
253
  end
227
254
 
228
255
  klass.send(visibility, method_name)
@@ -248,8 +275,8 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
248
275
  # (`...` or `*args, **kwargs`), making reflection on method parameters
249
276
  # useless without this.
250
277
  def target.instance_method(symbol)
251
- # TODO: Extract this method naming pattern
252
- original_memo_wised_name = :"_memo_wise_original_#{symbol}"
278
+ original_memo_wised_name =
279
+ MemoWise::InternalAPI.original_memo_wised_name(symbol)
253
280
 
254
281
  super.tap do |curr_method|
255
282
  # Start with calling the original `instance_method` on `symbol`,
@@ -439,10 +466,15 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
439
466
  if method(method_name).arity.zero?
440
467
  @_memo_wise[method_name] = yield
441
468
  else
442
- hash = @_memo_wise.fetch(method_name) do
443
- @_memo_wise[method_name] = {}
469
+ key = api.fetch_key(method_name, *args, **kwargs)
470
+ if api.use_hashed_key?(method_name)
471
+ hashes = @_memo_wise_hashes[method_name] ||= []
472
+ hashes << key
473
+ @_memo_wise[key] = yield
474
+ else
475
+ hash = @_memo_wise[method_name] ||= {}
476
+ hash[key] = yield
444
477
  end
445
- hash[api.fetch_key(method_name, *args, **kwargs)] = yield
446
478
  end
447
479
  end
448
480
 
@@ -511,7 +543,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
511
543
  #
512
544
  # ex.reset_memo_wise # reset "all methods" mode
513
545
  #
514
- def reset_memo_wise(method_name = nil, *args, **kwargs)
546
+ def reset_memo_wise(method_name = nil, *args, **kwargs) # rubocop:disable Metrics/PerceivedComplexity
515
547
  if method_name.nil?
516
548
  unless args.empty?
517
549
  raise ArgumentError, "Provided args when method_name = nil"
@@ -521,7 +553,9 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
521
553
  raise ArgumentError, "Provided kwargs when method_name = nil"
522
554
  end
523
555
 
524
- return @_memo_wise.clear
556
+ @_memo_wise.clear
557
+ @_memo_wise_hashes.clear
558
+ return
525
559
  end
526
560
 
527
561
  unless method_name.is_a?(Symbol)
@@ -537,9 +571,18 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
537
571
 
538
572
  if args.empty? && kwargs.empty?
539
573
  @_memo_wise.delete(method_name)
574
+ @_memo_wise_hashes[method_name]&.each do |hash|
575
+ @_memo_wise.delete(hash)
576
+ end
577
+ @_memo_wise_hashes.delete(method_name)
540
578
  else
541
- @_memo_wise[method_name]&.
542
- delete(api.fetch_key(method_name, *args, **kwargs))
579
+ key = api.fetch_key(method_name, *args, **kwargs)
580
+ if api.use_hashed_key?(method_name)
581
+ @_memo_wise_hashes[method_name]&.delete(key)
582
+ @_memo_wise.delete(key)
583
+ else
584
+ @_memo_wise[method_name]&.delete(key)
585
+ end
543
586
  end
544
587
  end
545
588
  end
@@ -10,10 +10,34 @@ module MemoWise
10
10
  #
11
11
  # @return [Object] the passed-in obj
12
12
  def self.create_memo_wise_state!(obj)
13
+ # `@_memo_wise` stores memoized results of method calls. For performance
14
+ # reasons, the structure differs for different types of methods. It looks
15
+ # like:
16
+ # {
17
+ # no_args_method_name: :memoized_result,
18
+ # single_arg_method_name: { arg1 => :memoized_result, ... },
19
+ # [:multi_arg_method_name, arg1, arg2].hash => :memoized_result
20
+ # }
13
21
  unless obj.instance_variables.include?(:@_memo_wise)
14
22
  obj.instance_variable_set(:@_memo_wise, {})
15
23
  end
16
24
 
25
+ # `@_memo_wise_hashes` stores the `Array#hash` values for each key in
26
+ # `@_memo_wise` that represents a multi-argument method call. We only use
27
+ # this data structure when resetting memoization for an entire method. It
28
+ # looks like:
29
+ # {
30
+ # multi_arg_method_name: Set[
31
+ # [:multi_arg_method_name, arg1, arg2].hash,
32
+ # [:multi_arg_method_name, arg1, arg3].hash,
33
+ # ...
34
+ # ],
35
+ # ...
36
+ # }
37
+ unless obj.instance_variables.include?(:@_memo_wise_hashes)
38
+ obj.instance_variable_set(:@_memo_wise_hashes, {})
39
+ end
40
+
17
41
  obj
18
42
  end
19
43
 
@@ -143,7 +167,9 @@ module MemoWise
143
167
  # * 1:1 relationship of singleton class to original class is documented
144
168
  # * Performance concern: searches all Class objects
145
169
  # But, only runs at load time
146
- ObjectSpace.each_object(Class).find { |cls| cls.singleton_class == klass }
170
+ ObjectSpace.each_object(Module).find do |cls|
171
+ cls.singleton_class == klass
172
+ end
147
173
  end
148
174
 
149
175
  # Convention we use for renaming the original method when we replace with
@@ -193,20 +219,41 @@ module MemoWise
193
219
  key = method.parameters.map.with_index do |(type, name), index|
194
220
  type == :req ? args[index] : kwargs[name]
195
221
  end
196
- key.size == 1 ? key.first : key
222
+ key.size == 1 ? key.first : [method_name, *key].hash
197
223
  else
198
224
  has_arg = MemoWise::InternalAPI.has_arg?(method)
199
225
 
200
226
  if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
201
- [args, kwargs].freeze
227
+ [method_name, args, kwargs].hash
202
228
  elsif has_arg
203
- args
229
+ args.hash
204
230
  else
205
- kwargs
231
+ kwargs.hash
206
232
  end
207
233
  end
208
234
  end
209
235
 
236
+ # Returns whether the given method should use an array's hash value as the
237
+ # cache lookup key. See the comments in `.create_memo_wise_state!` for an
238
+ # example.
239
+ #
240
+ # @param method_name [Symbol]
241
+ # Name of memoized method we're checking the implementation of
242
+ #
243
+ # @return [Boolean] true iff the method uses a hashed cache key; false
244
+ # otherwise
245
+ def use_hashed_key?(method_name)
246
+ method = target_class.instance_method(method_name)
247
+
248
+ if MemoWise::InternalAPI.has_arg?(method) &&
249
+ MemoWise::InternalAPI.has_kwarg?(method)
250
+ return true
251
+ end
252
+
253
+ MemoWise::InternalAPI.has_only_required_args?(method) &&
254
+ method.parameters.size > 1
255
+ end
256
+
210
257
  # Returns visibility of an instance method defined on class `target`.
211
258
  #
212
259
  # @param method_name [Symbol]
@@ -244,6 +291,8 @@ module MemoWise
244
291
  end
245
292
  end
246
293
 
294
+ private
295
+
247
296
  # @return [Class] where we look for method definitions
248
297
  def target_class
249
298
  if target.instance_of?(Class)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memo_wise
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Panorama Education
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2021-06-24 00:00:00.000000000 Z
14
+ date: 2021-07-30 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description:
17
17
  email:
@@ -39,9 +39,7 @@ files:
39
39
  - LICENSE.txt
40
40
  - README.md
41
41
  - Rakefile
42
- - benchmarks/.ruby-version
43
42
  - benchmarks/Gemfile
44
- - benchmarks/Gemfile.lock
45
43
  - benchmarks/benchmarks.rb
46
44
  - bin/console
47
45
  - bin/setup
@@ -1 +0,0 @@
1
- 3.0.0
@@ -1,29 +0,0 @@
1
- PATH
2
- remote: ..
3
- specs:
4
- memo_wise (0.4.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- benchmark-ips (2.9.1)
10
- memoist (0.16.2)
11
- memoized (1.0.2)
12
- memoizer (1.0.3)
13
-
14
- PLATFORMS
15
- x86_64-darwin-19
16
- x86_64-linux
17
-
18
- DEPENDENCIES
19
- benchmark-ips (= 2.9.1)
20
- memo_wise!
21
- memoist (= 0.16.2)
22
- memoized (= 1.0.2)
23
- memoizer (= 1.0.3)
24
-
25
- RUBY VERSION
26
- ruby 3.0.0p0
27
-
28
- BUNDLED WITH
29
- 2.2.3