Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/memory/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ def lookup_class_name(klass)
# @parameter obj [String] The string object to cache.
# @returns [String] A cached copy of the string (truncated to 64 characters).
def lookup_string(obj)
# This string is shortened to 64 characters which is what the string report shows. The string report can still list unique strings longer than 64 characters separately because the object_id of the shortened string will be different.
@string_cache[obj] ||= String.new << obj[0, 64]
# This string is shortened to 64 characters which is what the string report shows. The string report (by value) can still list unique strings longer than 64 characters separately because the `object_id` of the shortened string will be different.
@string_cache[obj] ||= obj[0, 64]
rescue RuntimeError => error
# It is possible for the String to be temporarily locked from another Fiber which raises an error when we try to use it as a hash key. i.e: `Socket#read` locks a buffer string while reading data into it. In this case we `#dup`` the string to get an unlocked copy.
if error.message == "can't modify string; temporarily locked"
@string_cache[obj.dup] ||= String.new << obj[0, 64]
@string_cache[obj.dup] ||= obj[0, 64]
else
raise
end
Expand Down
42 changes: 42 additions & 0 deletions test/memory/sampler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,48 @@ class MyThing
memory.report # buffer string is locked while reading ObjectSpace#each_object
end

it "does not retain string references" do
# Ruby 3.2 has different shared string behaviour:
skip_unless_minimum_ruby_version("3.3")

# Strings longer than 23 characters share a reference to a "shared" frozen string which should also be GC'd
sampler.run do
5.times do |i|
short_text = "SHORT TEXT ##{i}"
short_text.dup

long_text = "LONG TEXT ##{i} 12345678901234567890123456789012345678901234567890"
long_text.dup

very_long_text = "VERY LONG TEXT ##{i} 12345678901234567890123456789012345678901234567890 12345678901234567890123456789012345678901234567890 12345678901234567890123456789012345678901234567890 12345678901234567890123456789012345678901234567890 12345678901234567890123456789012345678901234567890"
very_long_text.dup

# Prevent the last frozen string from being the return value of the block:
nil
end
end

# 30 strings should be allocated (5 iterations * 6 strings per iteration):
# - short_text (interpolated result)
# - short_text.dup
# - long_text (interpolated result)
# - long_text.dup
# - very_long_text (interpolated result)
# - very_long_text.dup
expect(sampler.allocated.size).to be == 30

# Get unique string values:
string_allocations = sampler.allocated.select{|a| a.class_name == "String"}
unique_strings = string_allocations.group_by(&:value).size

# 15 unique strings (5 iterations * 3 unique strings per iteration):
expect(unique_strings).to be == 15

# No strings should be retained (all were eligible for GC):
retained_strings = string_allocations.select(&:retained)
expect(retained_strings.size).to be == 0
end

with "#as_json" do
it "returns allocation count" do
x = nil
Expand Down
Loading