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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/LICENSE.txt +21 -0
- data/README.md +232 -0
- data/lib/weak/map/abstract_strong_keys.rb +87 -0
- data/lib/weak/map/deletable.rb +65 -0
- data/lib/weak/map/strong_keys.rb +186 -0
- data/lib/weak/map/strong_secondary_keys.rb +229 -0
- data/lib/weak/map/weak_keys.rb +134 -0
- data/lib/weak/map/weak_keys_with_delete.rb +126 -0
- data/lib/weak/map.rb +714 -0
- data/lib/weak/set/strong_keys.rb +123 -0
- data/lib/weak/set/strong_secondary_keys.rb +154 -0
- data/lib/weak/set/weak_keys.rb +107 -0
- data/lib/weak/set/weak_keys_with_delete.rb +94 -0
- data/lib/weak/set.rb +749 -0
- data/lib/weak/version.rb +14 -0
- data/lib/weak.rb +45 -0
- metadata +65 -0
@@ -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
|