memo_wise 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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