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,123 @@
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 Set
11
+ # This {Weak::Set} strategy targets JRuby >= 9.4.6.0 and TruffleRuby >= 22.
12
+ # Older versions require additional indirections implemented in
13
+ # {Weak::Set::StrongSecondaryKeys}:
14
+ #
15
+ # - https://github.com/jruby/jruby/issues/7862
16
+ # - https://github.com/oracle/truffleruby/issues/2267
17
+ #
18
+ # The `ObjectSpace::WeakMap` on JRuby and TruffleRuby has strong keys and
19
+ # weak values. Thus, only the value object can be garbage collected to
20
+ # remove the entry while the key defines a strong object reference which
21
+ # prevents the key object from being garbage collected.
22
+ #
23
+ # As a workaround, we use the element's object_id as a key. Being an
24
+ # `Integer`, the object_id is generally is not garbage collected anyway but
25
+ # allows to uniquely identity the object.
26
+ #
27
+ # The `ObjectSpace::WeakMap` class does not allow to explicitly delete
28
+ # entries. We emulate this by setting the garbage-collectible value of a
29
+ # deleted entry to a simple new object. This value will be garbage collected
30
+ # on the next GC run which will then remove the entry. When accessing
31
+ # elements, we delete and filter out these recently deleted entries.
32
+ module StrongKeys
33
+ class DeletedEntry; end
34
+ private_constant :DeletedEntry
35
+
36
+ # Checks if this strategy is usable for the current Ruby version.
37
+ #
38
+ # @return [Bool] truethy for Ruby, TruffleRuby and modern JRuby, falsey
39
+ # otherwise
40
+ def self.usable?
41
+ case RUBY_ENGINE
42
+ when "ruby", "truffleruby"
43
+ true
44
+ when "jruby"
45
+ Gem::Version.new(RUBY_ENGINE_VERSION) >= Gem::Version.new("9.4.6.0")
46
+ end
47
+ end
48
+
49
+ # @!macro weak_set_method_add
50
+ def add(obj)
51
+ @map[obj.__id__] = obj
52
+ self
53
+ end
54
+
55
+ # @!macro weak_set_method_clear
56
+ def clear
57
+ @map = ObjectSpace::WeakMap.new
58
+ self
59
+ end
60
+
61
+ # @!macro weak_set_method_delete_question
62
+ def delete?(obj)
63
+ key = obj.__id__
64
+ return unless @map.key?(key) && @map[key].equal?(obj)
65
+
66
+ # If there is a valid value in the `ObjectSpace::WeakMap` (with a strong
67
+ # object_id key), we replace the value of the strong key with a
68
+ # temporary DeletedEntry object. As we do not keep any strong reference
69
+ # to this object, this will cause the key/value entry to vanish from the
70
+ # `Objectpace::WeakMap when the DeletedEntry object is eventually
71
+ # garbage collected.
72
+ @map[key] = DeletedEntry.new
73
+ self
74
+ end
75
+
76
+ # @!macro weak_set_method_each
77
+ def each
78
+ return enum_for(__method__) { size } unless block_given?
79
+
80
+ @map.values.each do |obj|
81
+ yield(obj) unless DeletedEntry === obj
82
+ end
83
+ self
84
+ end
85
+
86
+ # @!macro weak_set_method_include_question
87
+ def include?(obj)
88
+ key = obj.__id__
89
+ !!(@map.key?(key) && @map[key].equal?(obj))
90
+ end
91
+
92
+ # @!macro weak_set_method_prune
93
+ def prune
94
+ self
95
+ end
96
+
97
+ # @!macro weak_set_method_replace
98
+ def replace(enum)
99
+ map = ObjectSpace::WeakMap.new
100
+ do_with_enum(enum) do |obj|
101
+ map[obj.__id__] = obj
102
+ end
103
+ @map = map
104
+
105
+ self
106
+ end
107
+
108
+ # @!macro weak_set_method_size
109
+ def size
110
+ # Compared to using `ObjectSpace::WeakMap#each_value` like we do in
111
+ # `Weak::Set::WeakKeys`, this version is
112
+ # * ~12% faster on JRuby >= 9.4.6.0
113
+ # * sam-ish on TruffleRuby 24 with a slight advantage to this version
114
+ @map.values.delete_if { |obj| DeletedEntry === obj }.size
115
+ end
116
+
117
+ # @!macro weak_set_method_to_a
118
+ def to_a
119
+ @map.values.delete_if { |obj| DeletedEntry === obj }
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,154 @@
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 Set
11
+ # This {Weak::Set} strategy targets JRuby < 9.4.6.0.
12
+ #
13
+ # These JRuby versions have a similar `ObjectSpace::WeakMap` as newer
14
+ # JRubies with strong keys and weak values. Thus, only the value object can
15
+ # be garbage collected to remove the entry while the key defines a strong
16
+ # object reference which prevents the key object from being garbage
17
+ # collected.
18
+ #
19
+ # Additionally, `Integer` values (including object_ids) can have multiple
20
+ # different object representations in JRuby, making them not strictly equal.
21
+ # Thus, we can not use the object_id as a key in an `ObjectSpace::WeakMap`
22
+ # as we do in {Weak::Set::StrongKeys} for newer JRuby versions.
23
+ #
24
+ # As a workaround we use a more indirect implementation with a secondary
25
+ # lookup table for the keys which is inspired by
26
+ # [Google::Protobuf::Internal::LegacyObjectCache](https://github.com/protocolbuffers/protobuf/blob/afe2de261861717026c3b57ec83678590d5de838/ruby/lib/google/protobuf/internal/object_cache.rb#L42-L96)
27
+ #
28
+ # This secondary key map is a regular Hash which stores a mapping from an
29
+ # element's object_id to a separate Object which in turn is used as the key
30
+ # in the `ObjectSpace::WeakMap`.
31
+ #
32
+ # Being a regular Hash, the keys and values of the secondary key map are not
33
+ # automatically garbage collected as elements in the `ObjectSpace::WeakMap`
34
+ # are removed. However, its entries are rather cheap with Integer keys and
35
+ # "empty" objects as values. We perform manual garbage collection of this
36
+ # secondary key map during {StrongSecondaryKeys#include?} if required.
37
+ #
38
+ # As this strategy is the most conservative with the fewest requirements to
39
+ # the `ObjectSpace::WeakMap`, we use it as a default or fallback if there is
40
+ # no better strategy.
41
+ module StrongSecondaryKeys
42
+ class DeletedEntry; end
43
+ private_constant :DeletedEntry
44
+
45
+ # Checks if this strategy is usable for the current Ruby version.
46
+ #
47
+ # @return [Bool] always `true` to indicate that this stragegy should be
48
+ # usable with any Ruby implementation which provides an
49
+ # `ObjectSpace::WeakMap`.
50
+ def self.usable?
51
+ true
52
+ end
53
+
54
+ # @!macro weak_set_method_add
55
+ def add(obj)
56
+ key = @key_map[obj.__id__] ||= Object.new.freeze
57
+ @map[key] = obj
58
+ self
59
+ end
60
+
61
+ # @!macro weak_set_method_clear
62
+ def clear
63
+ @map = ObjectSpace::WeakMap.new
64
+ @key_map = {}
65
+ self
66
+ end
67
+
68
+ # @!macro weak_set_method_delete_question
69
+ def delete?(obj)
70
+ # When deleting, we still retain the key to avoid having to re-create it
71
+ # when `obj` is re-added to the {Weak::Set} again before the next GC.
72
+ #
73
+ # If `obj` is not added again, the key is eventually removed with our next
74
+ # GC of the `@key_map`.
75
+ key = @key_map[obj.__id__]
76
+ if key && @map.key?(key) && @map[key].equal?(obj)
77
+ # If there is a valid value in the `ObjectSpace::WeakMap` (with a
78
+ # strong object_id key), we replace the value of the strong key with a
79
+ # DeletedEntry marker object. This will cause the key/value entry to
80
+ # vanish from the `ObjectSpace::WeakMap` when the DeletedEntry object
81
+ # is eventually garbage collected.
82
+ @map[key] = DeletedEntry.new
83
+ self
84
+ end
85
+ end
86
+
87
+ # @!macro weak_set_method_each
88
+ def each
89
+ return enum_for(__method__) { size } unless block_given?
90
+
91
+ @map.values.each do |obj|
92
+ yield(obj) unless DeletedEntry === obj
93
+ end
94
+ self
95
+ end
96
+
97
+ # @!macro weak_set_method_include_question
98
+ def include?(obj)
99
+ key = @key_map[obj.__id__]
100
+ value = !!(key && @map.key?(key) && @map[key].equal?(obj))
101
+
102
+ auto_prune
103
+ value
104
+ end
105
+
106
+ # @!macro weak_set_method_prune
107
+ def prune
108
+ @key_map.each do |id, key|
109
+ @key_map.delete(id) unless @map.key?(key)
110
+ end
111
+ self
112
+ end
113
+
114
+ # @!macro weak_set_method_replace
115
+ def replace(enum)
116
+ map = ObjectSpace::WeakMap.new
117
+ key_map = {}
118
+ do_with_enum(enum) do |obj|
119
+ key = key_map[obj.__id__] ||= Object.new.freeze
120
+ map[key] = obj
121
+ end
122
+ @map = map
123
+ @key_map = key_map
124
+
125
+ self
126
+ end
127
+
128
+ # @!macro weak_set_method_size
129
+ def size
130
+ # Compared to using `ObjectSpace::WeakMap#each_value` like we do in
131
+ # {WeakKeys}, this version is ~12% faster on JRuby < 9.4.6.0
132
+ @map.values.delete_if { |obj| DeletedEntry === obj }.size
133
+ end
134
+
135
+ # @!macro weak_set_method_to_a
136
+ def to_a
137
+ @map.values.delete_if { |obj| DeletedEntry === obj }
138
+ end
139
+
140
+ private
141
+
142
+ # Prune unneeded entries from the `@key_map` Hash if we could remove at
143
+ # least 2000 entries or 20% of the table size (whichever is greater).
144
+ # Since the cost of the GC pass is O(N), we want to make sure that we
145
+ # condition this on overall table size, to avoid O(N^2) CPU costs.
146
+ def auto_prune
147
+ key_map_size = @key_map.size
148
+ cutoff = [2000, (key_map_size * 0.2).ceil].max
149
+
150
+ prune if key_map_size - @map.size > cutoff
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,107 @@
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 Set
11
+ # This {Weak::Set} strategy targets Ruby < 3.3.0.
12
+ #
13
+ # Its `ObjectSpace::WeakMap` uses weak keys and weak values so that either
14
+ # the key or the value can be independently garbage collected. If either of
15
+ # them vanishes, the entry is removed.
16
+ #
17
+ # The `ObjectSpace::WeakMap` does not allow to explicitly delete entries. We
18
+ # emulate this by setting the garbage-collectible value of a deleted entry
19
+ # to a simple new object. This value will be garbage collected on the next
20
+ # GC run which will then remove the entry. When accessing elements, we
21
+ # delete and filter out these recently deleted entries.
22
+ module WeakKeys
23
+ class DeletedEntry; end
24
+ private_constant :DeletedEntry
25
+
26
+ # Checks if this strategy is usable for the current Ruby version.
27
+ #
28
+ # @return [Bool] truethy for Ruby (aka. MRI, aka. YARV), falsey otherwise
29
+ def self.usable?
30
+ RUBY_ENGINE == "ruby"
31
+ end
32
+
33
+ # @!macro weak_set_method_add
34
+ def add(obj)
35
+ @map[obj] = obj
36
+ self
37
+ end
38
+
39
+ # @!macro weak_set_method_clear
40
+ def clear
41
+ @map = ObjectSpace::WeakMap.new
42
+ self
43
+ end
44
+
45
+ # @!macro weak_set_method_delete_question
46
+ def delete?(obj)
47
+ return unless include?(obj)
48
+
49
+ # If there is a valid entry in the `ObjectSpace::WeakMap`, we replace
50
+ # the value for the `obj` with a temporary DeletedEntry object. As we do
51
+ # not keep any strong reference to this object, this will cause the
52
+ # key/value entry to vanish from the `ObjectSpace::WeakMap` when the
53
+ # `DeletedEntry` object is eventually garbage collected.
54
+ #
55
+ # This ensures that we don't retain unnecessary entries in the map which
56
+ # we would have to skip over.
57
+ @map[obj] = DeletedEntry.new
58
+ self
59
+ end
60
+
61
+ # @!macro weak_set_method_each
62
+ def each
63
+ return enum_for(__method__) { size } unless block_given?
64
+
65
+ @map.values.each do |obj|
66
+ yield(obj) unless DeletedEntry === obj
67
+ end
68
+ self
69
+ end
70
+
71
+ # @!macro weak_set_method_include_question
72
+ def include?(obj)
73
+ !!(@map.key?(obj) && @map[obj].equal?(obj))
74
+ end
75
+
76
+ # @!macro weak_set_method_prune
77
+ def prune
78
+ self
79
+ end
80
+
81
+ # @!macro weak_set_method_replace
82
+ def replace(enum)
83
+ map = ObjectSpace::WeakMap.new
84
+ do_with_enum(enum) do |obj|
85
+ map[obj] = obj
86
+ end
87
+ @map = map
88
+
89
+ self
90
+ end
91
+
92
+ # @!macro weak_set_method_size
93
+ def size
94
+ count = 0
95
+ @map.each_value do |obj|
96
+ count = count.succ unless DeletedEntry === obj
97
+ end
98
+ count
99
+ end
100
+
101
+ # @!macro weak_set_method_to_a
102
+ def to_a
103
+ @map.values.delete_if { |obj| DeletedEntry === obj }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,94 @@
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 Set
11
+ # This {Weak::Set} strategy targets Ruby >= 3.3.0.
12
+ # Older Ruby versions require additional indirections implemented in
13
+ # {Weak::Set::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_set_method_add
35
+ def add(obj)
36
+ @map[obj] = true
37
+ self
38
+ end
39
+
40
+ # @!macro weak_set_method_clear
41
+ def clear
42
+ @map = ObjectSpace::WeakMap.new
43
+ self
44
+ end
45
+
46
+ # @!macro weak_set_method_delete_question
47
+ def delete?(obj)
48
+ # `ObjectSpace::WeakMap#delete` returns the value if it was removed. As
49
+ # we set it to true, `ObjectSpace::WeakMap#delete` returns either true
50
+ # or nil here.
51
+ self if @map.delete(obj)
52
+ end
53
+
54
+ # @!macro weak_set_method_each
55
+ def each(&block)
56
+ return enum_for(__method__) { size } unless block_given?
57
+
58
+ @map.keys.each(&block)
59
+ self
60
+ end
61
+
62
+ # @!macro weak_set_method_include_question
63
+ def include?(obj)
64
+ @map.key?(obj)
65
+ end
66
+
67
+ # @!macro weak_set_method_prune
68
+ def prune
69
+ self
70
+ end
71
+
72
+ # @!macro weak_set_method_replace
73
+ def replace(enum)
74
+ map = ObjectSpace::WeakMap.new
75
+ do_with_enum(enum) do |obj|
76
+ map[obj] = true
77
+ end
78
+ @map = map
79
+
80
+ self
81
+ end
82
+
83
+ # @!macro weak_set_method_size
84
+ def size
85
+ @map.size
86
+ end
87
+
88
+ # @!macro weak_set_method_to_a
89
+ def to_a
90
+ @map.keys
91
+ end
92
+ end
93
+ end
94
+ end