memo_wise 1.2.0 → 1.3.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: 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: