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 +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
|