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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +23 -3
- data/Gemfile.lock +5 -5
- data/README.md +41 -18
- data/benchmarks/Gemfile +3 -1
- data/benchmarks/benchmarks.rb +55 -19
- data/lib/memo_wise.rb +87 -44
- data/lib/memo_wise/internal_api.rb +54 -5
- data/lib/memo_wise/version.rb +1 -1
- metadata +2 -4
- data/benchmarks/.ruby-version +0 -1
- data/benchmarks/Gemfile.lock +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fa12aeb35a6c4ca923eb23d70867b41600795143570f3d7c5b6444c76547980
|
4
|
+
data.tar.gz: bac4074e758731ac5d20925ab32ef3cd78d10a1c030228df76c1868e104e1a40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52e8b1543bca54371a2743fbc22b3cee32ba68a050ac414e373ca2027faba214956bca3aef61a05d25ca1d339b3e4ae01a417cdb5d1a4a407fba16b0a3cae56e
|
7
|
+
data.tar.gz: 1bc40ccaab12ed598cc86d62d51ca8081a386b208559ac49788ff87601c45809958c2e936692fbb2e78f3ff0f04b124636f795d941558239fc20edf04f7b979b
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
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
|
-
##
|
9
|
-
|
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.
|
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:
|
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.
|
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.
|
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.
|
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.
|
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).
|
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
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|`()` (
|
116
|
-
|
117
|
-
|`(
|
118
|
-
|`(a
|
119
|
-
|`(a
|
120
|
-
|`(a
|
121
|
-
|`(a
|
122
|
-
|`(a
|
123
|
-
|`(a, *args
|
124
|
-
|
125
|
-
|
126
|
-
|
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.
|
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"
|
data/benchmarks/benchmarks.rb
CHANGED
@@ -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, :
|
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,
|
51
|
-
(BenchmarkGem.new(
|
52
|
-
(BenchmarkGem.new(
|
53
|
-
(BenchmarkGem.new(
|
54
|
-
(BenchmarkGem.new(
|
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.
|
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}: ()")
|
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
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
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
|
-
|
217
|
+
fetch_key_init = "args.hash"
|
199
218
|
else
|
200
219
|
args_str = "(**kwargs)"
|
201
|
-
|
220
|
+
fetch_key_init = "kwargs.hash"
|
202
221
|
end
|
203
222
|
end
|
204
223
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
222
|
-
|
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
|
-
|
225
|
-
|
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
|
-
|
252
|
-
|
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
|
-
|
443
|
-
|
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
|
-
|
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
|
-
|
542
|
-
|
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(
|
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].
|
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)
|
data/lib/memo_wise/version.rb
CHANGED
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.
|
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-
|
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
|
data/benchmarks/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
3.0.0
|
data/benchmarks/Gemfile.lock
DELETED
@@ -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
|