weak 0.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.
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Holger Just
4
+ #
5
+ # This software may be modified and distributed under the terms
6
+ # of the MIT license. See the LICENSE.txt file for details.
7
+
8
+ require "set"
9
+
10
+ require "weak/map/abstract_strong_keys"
11
+
12
+ ##
13
+ module Weak
14
+ class Map
15
+ # This {Weak::Map} strategy targets JRuby >= 9.4.6.0 and TruffleRuby >= 22.
16
+ # Older versions require additional indirections implemented in
17
+ # {StrongSecondaryKeys}:
18
+ #
19
+ # - https://github.com/jruby/jruby/issues/7862
20
+ # - https://github.com/oracle/truffleruby/issues/2267
21
+ #
22
+ # The `ObjectSpace::WeakMap` on JRuby and TruffleRuby has strong keys and
23
+ # weak values. Thus, only the value object in an `ObjectSpace::WeakMap` can
24
+ # be garbage collected to remove the entry while the key defines a strong
25
+ # object reference which prevents the key object from being garbage
26
+ # collected.
27
+ #
28
+ # As a workaround, we use the element's object_id as a key. Being an
29
+ # `Integer`, the object_id is generally is not garbage collected anyway but
30
+ # allows to uniquely identity the object.
31
+ #
32
+ # As we need to store both a key and value object for each key-value pair in
33
+ # our `Weak::Map`, we use two separate `ObjectSpace::WeakMap` objects for
34
+ # storing those. This allows keys and values to be independently garbage
35
+ # collected. When accessing a logical key in the {Weak::Map}, we need to
36
+ # manually check if we have a valid entry for both the stored key and the
37
+ # associated value.
38
+ #
39
+ # The `ObjectSpace::WeakMap` does not allow to explicitly delete entries. We
40
+ # emulate this by setting the garbage-collectible value of a deleted entry
41
+ # to a simple new object. This value will be garbage collected on the next
42
+ # GC run which will then remove the entry. When accessing elements, we
43
+ # delete and filter out these recently deleted entries.
44
+ module StrongKeys
45
+ include AbstractStrongKeys
46
+
47
+ # Checks if this strategy is usable for the current Ruby version.
48
+ #
49
+ # @return [Bool] truethy for Ruby, TruffleRuby and modern JRuby, falsey
50
+ # otherwise
51
+ def self.usable?
52
+ case RUBY_ENGINE
53
+ when "ruby", "truffleruby"
54
+ true
55
+ when "jruby"
56
+ Gem::Version.new(RUBY_ENGINE_VERSION) >= Gem::Version.new("9.4.6.0")
57
+ end
58
+ end
59
+
60
+ # @!macro weak_map_accessor_read
61
+ def [](key)
62
+ _get(key.__id__) { _default(key) }
63
+ end
64
+
65
+ # @!macro weak_map_accessor_write
66
+ def []=(key, value)
67
+ id = key.__id__
68
+
69
+ @keys[id] = key.nil? ? NIL : key
70
+ @values[id] = value.nil? ? NIL : value
71
+ value
72
+ end
73
+
74
+ # @!macro weak_map_method_clear
75
+ def clear
76
+ @keys = ObjectSpace::WeakMap.new
77
+ @values = ObjectSpace::WeakMap.new
78
+ self
79
+ end
80
+
81
+ # @!macro weak_map_method_delete
82
+ def delete(key)
83
+ _delete(key.__id__) { yield(key) if block_given? }
84
+ end
85
+
86
+ # @!macro weak_map_method_each_key
87
+ def each_key
88
+ return enum_for(__method__) { size } unless block_given?
89
+
90
+ @keys.values.each do |raw_key|
91
+ next if DeletedEntry === raw_key
92
+
93
+ key = value!(raw_key)
94
+ id = key.__id__
95
+ if missing?(@values[id])
96
+ @keys[id] = DeletedEntry.new
97
+ else
98
+ yield key
99
+ end
100
+ end
101
+
102
+ self
103
+ end
104
+
105
+ # @!macro weak_map_method_each_pair
106
+ def each_pair
107
+ return enum_for(__method__) { size } unless block_given?
108
+
109
+ @keys.values.each do |raw_key|
110
+ next if DeletedEntry === raw_key
111
+
112
+ key = value!(raw_key)
113
+ id = key.__id__
114
+
115
+ raw_value = @values[id]
116
+ if missing?(raw_value)
117
+ @keys[id] = DeletedEntry.new
118
+ else
119
+ yield [key, value!(raw_value)]
120
+ end
121
+ end
122
+
123
+ self
124
+ end
125
+
126
+ # @!macro weak_map_method_each_value
127
+ def each_value
128
+ return enum_for(__method__) { size } unless block_given?
129
+
130
+ @keys.values.each do |raw_key|
131
+ next if DeletedEntry === raw_key
132
+
133
+ key = value!(raw_key)
134
+ id = key.__id__
135
+
136
+ raw_value = @values[id]
137
+ if missing?(raw_value)
138
+ @keys[id] = DeletedEntry.new
139
+ else
140
+ yield value!(raw_value)
141
+ end
142
+ end
143
+
144
+ self
145
+ end
146
+
147
+ # @!macro weak_map_method_fetch
148
+ def fetch(key, default = UNDEFINED, &block)
149
+ _get(key.__id__) { _fetch_default(key, default, &block) }
150
+ end
151
+
152
+ # @!macro weak_map_method_include_question
153
+ def include?(key)
154
+ _get(key.__id__) { return false }
155
+ true
156
+ end
157
+
158
+ # @!macro weak_map_method_prune
159
+ def prune
160
+ value_keys = ::Set.new(@values.keys)
161
+
162
+ @keys.keys.each do |id|
163
+ next if value_keys.delete?(id)
164
+ @keys[id] = DeletedEntry.new
165
+ end
166
+
167
+ value_keys.each do |id|
168
+ @values[id] = DeletedEntry.new
169
+ end
170
+
171
+ self
172
+ end
173
+
174
+ private
175
+
176
+ def auto_prune
177
+ s1 = @keys.size
178
+ s2 = @values.size
179
+ s1, s2 = s2, s1 if s1 < s2
180
+
181
+ cutoff = [2000, (s1 * 0.2).ceil].max
182
+ prune unless s1 - s2 > cutoff
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Holger Just
4
+ #
5
+ # This software may be modified and distributed under the terms
6
+ # of the MIT license. See the LICENSE.txt file for details.
7
+
8
+ require "set"
9
+
10
+ require "weak/map/abstract_strong_keys"
11
+
12
+ ##
13
+ module Weak
14
+ class Map
15
+ # This {Weak::Map} strategy targets JRuby < 9.4.6.0.
16
+ #
17
+ # These JRuby versions have a similar `ObjectSpace::WeakMap` as newer
18
+ # JRubies with strong keys and weak values. Thus, only the value object can
19
+ # be garbage collected to remove the entry while the key defines a strong
20
+ # object reference which prevents the key object from being garbage
21
+ # collected.
22
+ #
23
+ # As we need to store both a key and value object for each key-value pair in
24
+ # our `Weak::Map`, we use two separate `ObjectSpace::WeakMap` objects for
25
+ # storing those. This allows keys and values to be independently garbage
26
+ # collected. When accessing a logical key in the {Weak::Map}, we need to
27
+ # manually check if we have a valid entry for both the stored key and the
28
+ # associated value.
29
+ #
30
+ # Additionally, `Integer` values (including object_ids) can have multiple
31
+ # different object representations in JRuby, making them not strictly equal.
32
+ # Thus, we can not use the object_id as a key in an `ObjectSpace::WeakMap`
33
+ # as we do in {Weak::Map::StrongKeys} for newer JRuby versions.
34
+ #
35
+ # As a workaround we use a more indirect implementation with a secondary
36
+ # lookup table for the `ObjectSpace::WeakMap` keys which is inspired by
37
+ # [Google::Protobuf::Internal::LegacyObjectCache](https://github.com/protocolbuffers/protobuf/blob/afe2de261861717026c3b57ec83678590d5de838/ruby/lib/google/protobuf/internal/object_cache.rb#L42-L96)
38
+ #
39
+ # This secondary key map is a regular Hash which stores a mapping from the
40
+ # key's object_id to a separate Object which in turn is used as a key
41
+ # in the `ObjectSpace::WeakMap` for the stored keys and values.
42
+ #
43
+ # Being a regular Hash, the keys and values of the secondary key map are not
44
+ # automatically garbage collected as elements in the `ObjectSpace::WeakMap`
45
+ # are removed. However, its entries are rather cheap with Integer keys and
46
+ # "empty" objects as values.
47
+ #
48
+ # As this strategy is the most conservative with the fewest requirements to
49
+ # the `ObjectSpace::WeakMap`, we use it as a default or fallback if there is
50
+ # no better strategy.
51
+ module StrongSecondaryKeys
52
+ include AbstractStrongKeys
53
+
54
+ # Checks if this strategy is usable for the current Ruby version.
55
+ #
56
+ # @return [Bool] always `true` to indicate that this stragegy should be
57
+ # usable with any Ruby implementation which provides an
58
+ # `ObjectSpace::WeakMap`.
59
+ def self.usable?
60
+ true
61
+ end
62
+
63
+ # @!macro weak_map_accessor_read
64
+ def [](key)
65
+ id = @key_map[key.__id__]
66
+ unless id
67
+ auto_prune
68
+ return _default(key)
69
+ end
70
+
71
+ _get(id) { _default(key) }
72
+ end
73
+
74
+ # @!macro weak_map_accessor_write
75
+ def []=(key, value)
76
+ id = @key_map[key.__id__] ||= Object.new.freeze
77
+
78
+ @keys[id] = key.nil? ? NIL : key
79
+ @values[id] = value.nil? ? NIL : value
80
+ value
81
+ end
82
+
83
+ # @!macro weak_map_method_clear
84
+ def clear
85
+ @keys = ObjectSpace::WeakMap.new
86
+ @values = ObjectSpace::WeakMap.new
87
+ @key_map = {}
88
+ self
89
+ end
90
+
91
+ # @!macro weak_map_method_delete
92
+ def delete(key)
93
+ id = @key_map[key.__id__]
94
+ return block_given? ? yield(key) : nil unless id
95
+
96
+ _delete(id) { yield(key) if block_given? }
97
+ end
98
+
99
+ # @!macro weak_map_method_each_key
100
+ def each_key
101
+ return enum_for(__method__) { size } unless block_given?
102
+
103
+ @keys.values.each do |raw_key|
104
+ next if DeletedEntry === raw_key
105
+
106
+ key = value!(raw_key)
107
+ next unless (id = @key_map[key.__id__])
108
+ if missing?(@values[id])
109
+ @keys[id] = DeletedEntry.new
110
+ else
111
+ yield key
112
+ end
113
+ end
114
+
115
+ self
116
+ end
117
+
118
+ # @!macro weak_map_method_each_pair
119
+ def each_pair
120
+ return enum_for(__method__) { size } unless block_given?
121
+
122
+ @keys.values.each do |raw_key|
123
+ next if DeletedEntry === raw_key
124
+
125
+ key = value!(raw_key)
126
+ next unless (id = @key_map[key.__id__])
127
+
128
+ raw_value = @values[id]
129
+ if missing?(raw_value)
130
+ @keys[id] = DeletedEntry.new
131
+ else
132
+ yield [key, value!(raw_value)]
133
+ end
134
+ end
135
+
136
+ self
137
+ end
138
+
139
+ # @!macro weak_map_method_each_value
140
+ def each_value
141
+ return enum_for(__method__) { size } unless block_given?
142
+
143
+ @keys.values.each do |raw_key|
144
+ next if DeletedEntry === raw_key
145
+
146
+ key = value!(raw_key)
147
+ next unless (id = @key_map[key.__id__])
148
+
149
+ raw_value = @values[id]
150
+ if missing?(raw_value)
151
+ @keys[id] = DeletedEntry.new
152
+ else
153
+ yield value!(raw_value)
154
+ end
155
+ end
156
+
157
+ self
158
+ end
159
+
160
+ # @!macro weak_map_method_fetch
161
+ def fetch(key, default = UNDEFINED, &block)
162
+ id = @key_map[key.__id__]
163
+ unless id
164
+ auto_prune
165
+ return _fetch_default(key, default, &block)
166
+ end
167
+
168
+ _get(id) { _fetch_default(key, default, &block) }
169
+ end
170
+
171
+ # @!macro weak_map_method_include_question
172
+ def include?(key)
173
+ id = @key_map[key.__id__]
174
+ unless id
175
+ auto_prune
176
+ return false
177
+ end
178
+
179
+ _get(id) { return false }
180
+ true
181
+ end
182
+
183
+ # @!macro weak_map_method_prune
184
+ def prune
185
+ orphaned_value_keys = ::Set.new(@values.keys)
186
+ remaining_keys = ::Set.new
187
+
188
+ @keys.keys.each do |id|
189
+ if orphaned_value_keys.delete?(id)
190
+ # Here, we have found a valid value belonging to the key. As both
191
+ # key and value are valid, we keep the @key_map entry.
192
+ remaining_keys << id
193
+ else
194
+ # Here, the value was missing (i.e. garbage collected). We mark the
195
+ # still present key as deleted
196
+ @keys[id] = DeletedEntry.new
197
+ end
198
+ end
199
+
200
+ # Mark all (remaining) values as deleted for which we have not found a
201
+ # matching key above
202
+ orphaned_value_keys.each do |id|
203
+ @values[id] = DeletedEntry.new
204
+ end
205
+
206
+ # Finally, remove all @key_map entries for which we have not seen a
207
+ # valid key and value above
208
+ @key_map.keep_if { |_, id| remaining_keys.include?(id) }
209
+
210
+ self
211
+ end
212
+
213
+ private
214
+
215
+ # prune unneeded entries from the `@key_map` Hash as well as
216
+ # garbage-collected entries from `@keys` and `@values` if we could remove
217
+ # at least 2000 entries or 20% of the table size (whichever is greater).
218
+ # Since the cost of the GC pass is O(N), we want to make sure that we
219
+ # condition this on overall table size, to avoid O(N^2) CPU costs.
220
+ def auto_prune
221
+ key_map_size = @key_map.size
222
+ cutoff = [2000, (key_map_size * 0.2).ceil].max
223
+ key_value_size = [@keys.size, @values.size].max
224
+
225
+ prune if key_map_size - key_value_size > cutoff
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Holger Just
4
+ #
5
+ # This software may be modified and distributed under the terms
6
+ # of the MIT license. See the LICENSE.txt file for details.
7
+
8
+ require "weak/map/deletable"
9
+
10
+ ##
11
+ module Weak
12
+ class Map
13
+ # This {Weak::Map} strategy targets Ruby < 3.3.0.
14
+ #
15
+ # Its `ObjectSpace::WeakMap` uses weak keys and weak values so that either
16
+ # the key or the value can be independently garbage collected. If either of
17
+ # them vanishes, the entry is removed.
18
+ #
19
+ # The `ObjectSpace::WeakMap` does not allow to explicitly delete entries.
20
+ # We emulate this by setting the garbage-collectible value of a deleted
21
+ # entry to a simple new object. This value will be garbage collected on the
22
+ # next GC run which will then remove the entry. When accessing elements, we
23
+ # delete and filter out these recently deleted entries.
24
+ module WeakKeys
25
+ include Deletable
26
+
27
+ # Checks if this strategy is usable for the current Ruby version.
28
+ #
29
+ # @return [Bool] truethy for Ruby (aka. MRI, aka. YARV), falsey otherwise
30
+ def self.usable?
31
+ RUBY_ENGINE == "ruby"
32
+ end
33
+
34
+ # @!macro weak_map_accessor_read
35
+ def [](key)
36
+ raw_value = @map[key]
37
+ missing?(raw_value) ? _default(key) : value!(raw_value)
38
+ end
39
+
40
+ # @!macro weak_map_accessor_write
41
+ def []=(key, value)
42
+ @map[key] = value.nil? ? NIL : value
43
+ value
44
+ end
45
+
46
+ # @!macro weak_map_method_clear
47
+ def clear
48
+ @map = ObjectSpace::WeakMap.new
49
+ self
50
+ end
51
+
52
+ # @!macro weak_map_method_delete
53
+ def delete(key)
54
+ raw_value = @map[key]
55
+ if have?(raw_value)
56
+ @map[key] = DeletedEntry.new
57
+ value!(raw_value)
58
+ elsif block_given?
59
+ yield(key)
60
+ end
61
+ end
62
+
63
+ # @!macro weak_map_method_each_key
64
+ def each_key
65
+ return enum_for(__method__) { size } unless block_given?
66
+
67
+ @map.keys.each do |key|
68
+ yield key unless missing?(@map[key])
69
+ end
70
+ self
71
+ end
72
+
73
+ # @!macro weak_map_method_each_pair
74
+ def each_pair
75
+ return enum_for(__method__) { size } unless block_given?
76
+
77
+ @map.keys.each do |key|
78
+ raw_value = @map[key]
79
+ yield [key, value!(raw_value)] unless missing?(raw_value)
80
+ end
81
+ self
82
+ end
83
+
84
+ # @!macro weak_map_method_each_value
85
+ def each_value
86
+ return enum_for(__method__) { size } unless block_given?
87
+
88
+ @map.values.each do |raw_value|
89
+ yield value!(raw_value) unless missing?(raw_value)
90
+ end
91
+ self
92
+ end
93
+
94
+ # @!macro weak_map_method_fetch
95
+ def fetch(key, default = UNDEFINED, &block)
96
+ raw_value = @map[key]
97
+ if have?(raw_value)
98
+ value!(raw_value)
99
+ else
100
+ _fetch_default(key, default, &block)
101
+ end
102
+ end
103
+
104
+ # @!macro weak_map_method_include_question
105
+ def include?(key)
106
+ have?(@map[key])
107
+ end
108
+
109
+ # @!macro weak_map_method_keys
110
+ def keys
111
+ @map.keys.delete_if { |key| missing?(@map[key]) }
112
+ end
113
+
114
+ # @!macro weak_map_method_prune
115
+ def prune
116
+ self
117
+ end
118
+
119
+ # @!macro weak_map_method_size
120
+ def size
121
+ each_key.count
122
+ end
123
+
124
+ # @!macro weak_map_method_values
125
+ def values
126
+ values = []
127
+ @map.values.each do |raw_value|
128
+ values << value!(raw_value) unless missing?(raw_value)
129
+ end
130
+ values
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Holger Just
4
+ #
5
+ # This software may be modified and distributed under the terms
6
+ # of the MIT license. See the LICENSE.txt file for details.
7
+
8
+ ##
9
+ module Weak
10
+ class Map
11
+ # This {Weak::Map} strategy targets Ruby >= 3.3.0.
12
+ # Older Ruby versions require additional indirections implemented in
13
+ # {Weak::Map::WeakKeys}:
14
+ #
15
+ # - https://bugs.ruby-lang.org/issues/19561
16
+ #
17
+ # Ruby's `ObjectSpace::WeakMap` uses weak keys and weak values so that
18
+ # either the key or the value can be independently garbage collected. If
19
+ # either of them vanishes, the entry is removed.
20
+ #
21
+ # The `ObjectSpace::WeakMap` also allows to delete entries. This allows us
22
+ # to directly use the `ObjectSpace::WeakMap` as a storage the same way a
23
+ # `Set` uses a `Hash` object object as storage.
24
+ module WeakKeysWithDelete
25
+ # Checks if this strategy is usable for the current Ruby version.
26
+ #
27
+ # @return [Bool] truethy for Ruby (aka. MRI, aka. YARV) >= 3.3.0,
28
+ # falsey otherwise
29
+ def self.usable?
30
+ RUBY_ENGINE == "ruby" &&
31
+ ObjectSpace::WeakMap.instance_methods.include?(:delete)
32
+ end
33
+
34
+ # @!macro weak_map_accessor_read
35
+ def [](key)
36
+ value = @map[key]
37
+ value = _default(key) if value.nil? && !@map.key?(key)
38
+ value
39
+ end
40
+
41
+ # @!macro weak_map_accessor_write
42
+ def []=(key, value)
43
+ @map[key] = value
44
+ value
45
+ end
46
+
47
+ # @!macro weak_map_method_clear
48
+ def clear
49
+ @map = ObjectSpace::WeakMap.new
50
+ self
51
+ end
52
+
53
+ # @!macro weak_map_method_delete
54
+ def delete(key, &block)
55
+ @map.delete(key, &block)
56
+ end
57
+
58
+ # @!macro weak_map_method_each_key
59
+ def each_key
60
+ return enum_for(__method__) { size } unless block_given?
61
+
62
+ @map.keys.each do |key|
63
+ yield(key)
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ # @!macro weak_map_method_each_pair
70
+ def each_pair(&block)
71
+ return enum_for(__method__) { size } unless block_given?
72
+
73
+ array = []
74
+ @map.each do |key, value|
75
+ array << key << value
76
+ end
77
+ array.each_slice(2, &block)
78
+
79
+ self
80
+ end
81
+
82
+ # @!macro weak_map_method_each_value
83
+ def each_value
84
+ return enum_for(__method__) { size } unless block_given?
85
+
86
+ @map.values.each do |value|
87
+ yield(value)
88
+ end
89
+
90
+ self
91
+ end
92
+
93
+ # @!macro weak_map_method_fetch
94
+ def fetch(key, default = UNDEFINED, &block)
95
+ value = @map[key]
96
+ value = _fetch_default(key, default, &block) if value.nil? && !@map.key?(key)
97
+ value
98
+ end
99
+
100
+ # @!macro weak_map_method_include_question
101
+ def include?(key)
102
+ @map.key?(key)
103
+ end
104
+
105
+ # @!macro weak_map_method_keys
106
+ def keys
107
+ @map.keys
108
+ end
109
+
110
+ # @!macro weak_map_method_prune
111
+ def prune
112
+ self
113
+ end
114
+
115
+ # @!macro weak_map_method_size
116
+ def size
117
+ @map.size
118
+ end
119
+
120
+ # @!macro weak_map_method_values
121
+ def values
122
+ @map.values
123
+ end
124
+ end
125
+ end
126
+ end