memo_wise 1.2.0 → 1.3.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: 3a2fbbb2a403502d6a492068a9a0c8c103f717ea5b32e5029ba1a0aebab61db6
4
- data.tar.gz: c2c8118caa1621670b6cd666f92147eb5c3f069c7b910062ed0f635568764dd7
3
+ metadata.gz: 4ff51bfee4fcc634d50588005e79c4a74702fd0d8deb8ab23dd917134cedf42f
4
+ data.tar.gz: 1cb14df9bcef27a3442b48cf1aee370835bbf9070b1f44bb3d8ec8266158bdab
5
5
  SHA512:
6
- metadata.gz: 699415e3445e60bf6037fdb6887a064a4d082159be15c8c52a0711be5b3b233e3a7caa974f6d31df769d9bd0c3825d7a9f11b17b1e3b98ec9202ee8224801204
7
- data.tar.gz: f6f377da39902d30310e7304e0caa7195e372c5fe0666e05ea30d2c2f936f3cc7d00c3588e453d5ab4ba7393a4f747f5c328acd4f4024cfa126ecb5f683fd24c
6
+ metadata.gz: 36a3f525833af24f076568ed9bdb43a06e90441788f2f8e8d26c0303ec7e0697a56c8401a3e24c06300cb3f2df199768ae00d8cf95cc90808fb4d9940ab9a931
7
+ data.tar.gz: f60569c63d693272856137c179b826a598c63a5762fedd76faf630b26fd3301d6ca37756f99cb7940501d5bcfd4c375094176a617610c35fc8788b67580159ce
data/CHANGELOG.md CHANGED
@@ -5,9 +5,17 @@ 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
8
+ ## [Unreleased]
9
+
9
10
  - Nothing yet!
10
11
 
12
+ ## [1.3.0] - 2021-11-22
13
+
14
+ - Fix thread-safety issue in concurrent calls to zero-arg method in unmemoized
15
+ state which resulted in a `nil` value being accidentally returned in one thread
16
+ - Fix bugs related to child classes inheriting from parent classes that use
17
+ `MemoWise`
18
+
11
19
  ## [1.2.0] - 2021-11-10
12
20
 
13
21
  ### Updated
@@ -93,7 +101,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
93
101
  - Panolint
94
102
  - Dependabot setup
95
103
 
96
- [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...HEAD
104
+ [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.3.0...HEAD
105
+ [1.3.0]: https://github.com/panorama-ed/memo_wise/compare/v1.2.0...v1.3.0
106
+ [1.2.0]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...v1.2.0
97
107
  [1.1.0]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...v1.1.0
98
108
  [1.0.0]: https://github.com/panorama-ed/memo_wise/compare/v0.4.0...v1.0.0
99
109
  [0.4.0]: https://github.com/panorama-ed/memo_wise/compare/v0.3.0...v0.4.0
data/Gemfile.lock CHANGED
@@ -14,7 +14,7 @@ GIT
14
14
  PATH
15
15
  remote: .
16
16
  specs:
17
- memo_wise (1.2.0)
17
+ memo_wise (1.3.0)
18
18
 
19
19
  GEM
20
20
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -20,6 +20,7 @@
20
20
  * Support for [resetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#reset_memo_wise-instance_method) and [presetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#preset_memo_wise-instance_method) memoized values
21
21
  * Support for memoization on frozen objects
22
22
  * Support for memoization of class and module methods
23
+ * Support for inheritance of memoized class and instance methods
23
24
  * Full [documentation](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise) and [test coverage](https://codecov.io/gh/panorama-ed/memo_wise)!
24
25
 
25
26
  ## Installation
@@ -56,9 +57,9 @@ class Example
56
57
  x
57
58
  end
58
59
  memo_wise :slow_value
59
-
60
+
60
61
  private
61
-
62
+
62
63
  # maintains privacy of the memoized method
63
64
  def private_slow_method(x)
64
65
  sleep x
@@ -117,15 +118,15 @@ Results using Ruby 3.0.2:
117
118
 
118
119
  |Method arguments|`Dry::Core`\* (0.7.1)|`Memery` (1.4.0)|
119
120
  |--|--|--|
120
- |`()` (none)|1.51x|19.82x|
121
- |`(a)`|2.30x|11.38x|
122
- |`(a, b)`|0.45x|2.10x|
123
- |`(a:)`|2.20x|22.83x|
124
- |`(a:, b:)`|0.49x|4.53x|
125
- |`(a, b:)`|0.46x|4.35x|
126
- |`(a, *args)`|0.89x|2.03x|
127
- |`(a:, **kwargs)`|0.82x|3.18x|
128
- |`(a, *args, b:, **kwargs)`|0.60x|1.62x|
121
+ |`()` (none)|1.42x|17.84x|
122
+ |`(a)`|2.48x|11.48x|
123
+ |`(a, b)`|0.46x|2.05x|
124
+ |`(a:)`|2.18x|20.50x|
125
+ |`(a:, b:)`|0.48x|4.22x|
126
+ |`(a, b:)`|0.48x|4.08x|
127
+ |`(a, *args)`|0.90x|1.97x|
128
+ |`(a:, **kwargs)`|0.80x|3.02x|
129
+ |`(a, *args, b:, **kwargs)`|0.61x|1.54x|
129
130
 
130
131
  \* `Dry::Core`
131
132
  [may cause incorrect behavior caused by hash collisions](https://github.com/dry-rb/dry-core/issues/63).
@@ -134,15 +135,15 @@ Results using Ruby 2.7.4 (because these gems raise errors in Ruby 3.x):
134
135
 
135
136
  |Method arguments|`DDMemoize` (1.0.0)|`Memoist` (0.16.2)|`Memoized` (1.0.2)|`Memoizer` (1.0.3)|
136
137
  |--|--|--|--|--|
137
- |`()` (none)|35.29x|3.46x|1.67x|4.27x|
138
- |`(a)`|25.04x|16.96x|12.83x|14.68x|
139
- |`(a, b)`|3.20x|2.28x|1.84x|2.04x|
140
- |`(a:)`|34.17x|27.77x|24.07x|25.39x|
141
- |`(a:, b:)`|5.22x|4.29x|3.74x|4.00x|
142
- |`(a, b:)`|4.81x|3.99x|3.49x|3.66x|
143
- |`(a, *args)`|3.21x|2.30x|1.97x|2.00x|
144
- |`(a:, **kwargs)`|2.84x|2.39x|2.12x|2.19x|
145
- |`(a, *args, b:, **kwargs)`|2.10x|1.80x|1.67x|1.66x|
138
+ |`()` (none)|33.90x|3.44x|1.56x|4.03x|
139
+ |`(a)`|24.56x|17.02x|12.94x|14.91x|
140
+ |`(a, b)`|3.14x|2.35x|1.84x|2.03x|
141
+ |`(a:)`|34.42x|28.14x|23.83x|25.26x|
142
+ |`(a:, b:)`|5.13x|4.28x|3.77x|3.97x|
143
+ |`(a, b:)`|4.83x|4.08x|3.50x|3.66x|
144
+ |`(a, *args)`|3.03x|2.25x|1.95x|1.95x|
145
+ |`(a:, **kwargs)`|2.90x|2.44x|2.14x|2.24x|
146
+ |`(a, *args, b:, **kwargs)`|2.05x|1.77x|1.65x|1.64x|
146
147
 
147
148
  You can run benchmarks yourself with:
148
149
 
@@ -206,7 +207,13 @@ after(:each) { helper.reset_memo_wise }
206
207
 
207
208
  ## Further Reading
208
209
 
209
- We've written more about MemoWise in a series of blog posts:
210
+ We presented at RubyConf 2021:
211
+
212
+ - Achieving Fast Method Metaprogramming: Lessons from `MemoWise`
213
+ ([slides](https://docs.google.com/presentation/d/1XgERQ0YHplwJKM3wNQwZn584d_9szYZp2WsDEXoY_7Y/edit?usp=sharing) /
214
+ [benchmarks](https://gist.github.com/JacobEvelyn/17b7b000e50151c30eaea928f1fcdc11))
215
+
216
+ And we've written more about `MemoWise` in a series of blog posts:
210
217
 
211
218
  - [Introducing: MemoWise](https://medium.com/building-panorama-education/introducing-memowise-51a5f0523489)
212
219
  - [Optimizing MemoWise Performance](https://ja.cob.land/optimizing-memowise-performance)
@@ -241,6 +248,7 @@ Then carry out these steps:
241
248
 
242
249
  1. Update `CHANGELOG.md`:
243
250
  - Add an entry for the upcoming version _x.y.z_
251
+ - Add a link for this version's comparison to the bottom of `CHANGELOG.md`
244
252
  - Move content from _Unreleased_ to the upcoming version _x.y.z_
245
253
  - Commit with title `Update CHANGELOG.md for x.y.z`
246
254
 
@@ -63,8 +63,7 @@ BENCHMARK_GEMS = [
63
63
  # Use metaprogramming to ensure that each class is created in exactly the
64
64
  # the same way.
65
65
  BENCHMARK_GEMS.each do |benchmark_gem|
66
- # rubocop:disable Security/Eval
67
- eval <<-CLASS, binding, __FILE__, __LINE__ + 1
66
+ eval <<~HEREDOC, binding, __FILE__, __LINE__ + 1 # rubocop:disable Security/Eval
68
67
  # For these methods, we alternately return truthy and falsey values in
69
68
  # order to benchmark memoization when the result of a method is falsey.
70
69
  #
@@ -125,8 +124,7 @@ BENCHMARK_GEMS.each do |benchmark_gem|
125
124
  end
126
125
  #{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
127
126
  end
128
- CLASS
129
- # rubocop:enable Security/Eval
127
+ HEREDOC
130
128
  end
131
129
 
132
130
  # We pre-create argument lists for our memoized methods with arguments, so that
@@ -161,15 +161,72 @@ module MemoWise
161
161
  def self.original_class_from_singleton(klass)
162
162
  raise ArgumentError, "Must be a singleton class: #{klass.inspect}" unless klass.singleton_class?
163
163
 
164
+ # Since we call this method a lot, we memoize the results. This can have a
165
+ # huge impact; for example, in our test suite this drops our test times
166
+ # from over five minutes to just a few seconds.
167
+ @original_class_from_singleton ||= {}
168
+
164
169
  # Search ObjectSpace
165
170
  # * 1:1 relationship of singleton class to original class is documented
166
171
  # * Performance concern: searches all Class objects
167
- # But, only runs at load time
168
- ObjectSpace.each_object(Module).find do |cls|
172
+ # But, only runs at load time and results are memoized
173
+ @original_class_from_singleton[klass] ||= ObjectSpace.each_object(Module).find do |cls|
169
174
  cls.singleton_class == klass
170
175
  end
171
176
  end
172
177
 
178
+ # Increment the class's method index counter, and return an index to use for
179
+ # the given method name.
180
+ #
181
+ # @param klass [Class]
182
+ # Original class on which a method is being memoized
183
+ #
184
+ # @param method_name [Symbol]
185
+ # The name of the method being memoized
186
+ #
187
+ # @return [Integer]
188
+ # The index within `@_memo_wise` to store the method's memoized results
189
+ def self.next_index!(klass, method_name)
190
+ # `@_memo_wise_indices` stores the `@_memo_wise` indices of different
191
+ # method names. We only use this data structure when resetting or
192
+ # presetting memoization. It looks like:
193
+ # {
194
+ # single_arg_method_name: 0,
195
+ # other_single_arg_method_name: 1
196
+ # }
197
+ memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
198
+ memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
199
+
200
+ # When a parent and child class both use `class << self` to define
201
+ # memoized class methods, the child class' singleton is not considered a
202
+ # descendent of the parent class' singleton. Because we store the index
203
+ # counter as a class variable that can be shared up the inheritance chain,
204
+ # we want to detect this case and store it on the original class instead
205
+ # of the singleton to make the counter shared correctly.
206
+ counter_class = klass.singleton_class? ? original_class_from_singleton(klass) : klass
207
+
208
+ # We use a class variable for tracking the index to make this work with
209
+ # inheritance structures. When a parent and child class both use
210
+ # MemoWise, we want the child class's index to not "reset" back to 0 and
211
+ # overwrite the behavior of a memoized parent method. Using a class
212
+ # variable will share the index data between parent and child classes.
213
+ #
214
+ # However, we don't use a class variable for `@_memo_wise_indices`
215
+ # because we want to allow instance and class methods with the same name
216
+ # to both be memoized, and using a class variable would share that index
217
+ # data between them.
218
+ index = if counter_class.class_variable_defined?(:@@_memo_wise_index_counter)
219
+ counter_class.class_variable_get(:@@_memo_wise_index_counter)
220
+ else
221
+ 0
222
+ end
223
+
224
+ memo_wise_indices[method_name] = index
225
+ counter_class.class_variable_set(:@@_memo_wise_index_counter, index + 1) # rubocop:disable Style/ClassVars
226
+
227
+ index
228
+ end
229
+
173
230
  # Convention we use for renaming the original method when we replace with
174
231
  # the memoized version in {MemoWise.memo_wise}.
175
232
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/memo_wise.rb CHANGED
@@ -56,7 +56,7 @@ module MemoWise
56
56
  # :nocov:
57
57
  all_args = RUBY_VERSION < "2.7" ? "*" : "..."
58
58
  # :nocov:
59
- class_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
59
+ class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
60
60
  # On Ruby 2.7 or greater:
61
61
  #
62
62
  # def initialize(...)
@@ -75,7 +75,7 @@ module MemoWise
75
75
  MemoWise::InternalAPI.create_memo_wise_state!(self)
76
76
  super
77
77
  end
78
- END_OF_METHOD
78
+ HEREDOC
79
79
 
80
80
  # @private
81
81
  #
@@ -156,6 +156,17 @@ module MemoWise
156
156
  klass = klass.singleton_class
157
157
  end
158
158
 
159
+ if klass.singleton_class?
160
+ # This ensures that a memoized method defined on a parent class can
161
+ # still be used in a child class.
162
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
163
+ def inherited(subclass)
164
+ super
165
+ MemoWise::InternalAPI.create_memo_wise_state!(subclass)
166
+ end
167
+ HEREDOC
168
+ end
169
+
159
170
  raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
160
171
 
161
172
  api = MemoWise::InternalAPI.new(klass)
@@ -167,39 +178,27 @@ module MemoWise
167
178
  klass.send(:private, original_memo_wised_name)
168
179
 
169
180
  method_arguments = MemoWise::InternalAPI.method_arguments(method)
170
- # `@_memo_wise_indices` stores the `@_memo_wise` indices of different
171
- # method names. We only use this data structure when resetting or
172
- # presetting memoization. It looks like:
173
- # {
174
- # single_arg_method_name: 0,
175
- # other_single_arg_method_name: 1
176
- # }
177
- memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
178
- memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
179
- index = klass.instance_variable_get(:@_memo_wise_index_counter) || 0
180
-
181
- memo_wise_indices[method_name] = index
182
- klass.instance_variable_set(:@_memo_wise_index_counter, index + 1)
181
+ index = MemoWise::InternalAPI.next_index!(klass, method_name)
183
182
 
184
183
  case method_arguments
185
184
  when MemoWise::InternalAPI::NONE
186
185
  # Zero-arg methods can use simpler/more performant logic because the
187
186
  # hash key is just the method name.
188
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
187
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
189
188
  def #{method_name}
190
- _memo_wise_output = @_memo_wise[#{index}]
191
- if _memo_wise_output || @_memo_wise_sentinels[#{index}]
192
- _memo_wise_output
189
+ if @_memo_wise_sentinels[#{index}]
190
+ @_memo_wise[#{index}]
193
191
  else
192
+ ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
194
193
  @_memo_wise_sentinels[#{index}] = true
195
- @_memo_wise[#{index}] = #{original_memo_wised_name}
194
+ ret
196
195
  end
197
196
  end
198
- END_OF_METHOD
197
+ HEREDOC
199
198
  when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
200
199
  key = method.parameters.first.last
201
200
 
202
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
201
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
203
202
  def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
204
203
  _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
205
204
  _memo_wise_output = _memo_wise_hash[#{key}]
@@ -209,7 +208,7 @@ module MemoWise
209
208
  _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
210
209
  end
211
210
  end
212
- END_OF_METHOD
211
+ HEREDOC
213
212
  # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
214
213
  # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
215
214
  else
@@ -225,7 +224,7 @@ module MemoWise
225
224
  # consistent performance. In general, this should still be faster for
226
225
  # truthy results because `Hash#[]` generally performs hash lookups
227
226
  # faster than `Hash#fetch`.
228
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
227
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
229
228
  def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
230
229
  _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
231
230
  _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
@@ -236,7 +235,7 @@ module MemoWise
236
235
  _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
237
236
  end
238
237
  end
239
- END_OF_METHOD
238
+ HEREDOC
240
239
  end
241
240
 
242
241
  klass.send(visibility, method_name)
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.2.0
4
+ version: 1.3.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-11-10 00:00:00.000000000 Z
14
+ date: 2021-11-23 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description:
17
17
  email: