Description
We noticed a slow and steady memory leak in our web app, to investigate it I enabled ObjectSpace.trace_object_allocations_start
and extracted a few heap dumps, then started analysing them with sheap.
I found a common pattern: A yjit_root
node holding references to ActiveRecord methods holding references to instances of ActiveRecord models then holding references to their instance attributes. These objects attributes sums up and creates a slow and steady memory leak:
[<ROOT vm (3194 refs)>,
<DATA 0x7fc02024b070 yjit_root (11268 refs)>,
<IMEMO 0x7fbffcfa5370 ment (4 refs)>,
<IMEMO 0x7fc01d729090 iseq (491 refs)>,
<OBJECT 0x7fc01d43c9e0 (0x7fc01d43c800) (6 refs)>,
<OBJECT 0x7fc01d466740 ActiveRecord::Associations::HasManyThroughAssociation (7 refs)>,
<OBJECT 0x7fbfd60d1108 Entry (6 refs)>,
<HASH 0x7fc0203c6198 (11 refs)>,
<OBJECT 0x7fc01c866418 ActiveRecord::Associations::BelongsToPolymorphicAssociation (5 refs)>,
<OBJECT 0x7fbfd5f74f58 Message (3 refs)>,
<HASH 0x7fc01c6feda0 (1 refs)>,
<OBJECT 0x7fbfe5875af8 ActiveRecord::Associations::HasOneAssociation (4 refs)>,
<OBJECT 0x7fbfd5f622b8 ActionText::EncryptedRichText (3 refs)>,
<OBJECT 0x7fbfe5871a98 ActiveModel::LazyAttributeSet (6 refs)>,
<HASH 0x7fc01c6f9300 (5 refs)>,
<OBJECT 0x7fbfd5f61228 ActionText::Content (6 refs)>,
<OBJECT 0x7fbfd5f3da08 ActionText::Fragment (1 refs)>,
<OBJECT 0x7fbfe58c2948 Okra::HTML::Node (1 refs)>,
<ARRAY 0x7fbfd5f611d8 (3 refs)>,
<OBJECT 0x7fbfe58c2998 Okra::HTML::Node (3 refs)>,
<ARRAY 0x7fbfe5871458 (5 refs)>,
<OBJECT 0x7fbfe58c2c68 Okra::HTML::Node (3 refs)>,
<ARRAY 0x7fc01f2a0008 (55 refs)>,
<OBJECT 0x7fbfe58ab8b0 Okra::HTML::Node (3 refs)>,
<ARRAY 0x7fbfe58ab9a0 (8 refs)>,
<ARRAY 0x7fbfd5f5c390 (2 refs)>,
<STRING 0x7fbfd5f5c3b8 "630">]
This is the node data until you reach the ActiveRecord model instance:
irb#1():068> $diff.after.at("0x7fbffcfa5370").data
=>
{"address"=>"0x7fbffcfa5370",
"type"=>"IMEMO",
"shape_id"=>0,
"slot_size"=>40,
"imemo_type"=>"ment",
"references"=>["0x7fc01d578660", "0x7fc01d5d6c38", "0x7fc01d729090", "0x7fc01d854050"],
"file"=>"/usr/local/bundle/ruby/3.3.0/bundler/gems/rails-139c5678aa5d/activerecord/lib/active_record/relation/query_methods.rb",
"line"=>1595,
"method"=>"build_arel",
"generation"=>18,
"memsize"=>48,
"flags"=>{"wb_protected"=>true, "old"=>true, "uncollectible"=>true, "marked"=>true}}
irb#1():069> $diff.after.at("0x7fbffcfa5370").data.except("references")
irb#1():073> $diff.after.at("0x7fc01d729090").data.except("references")
=>
{"address"=>"0x7fc01d729090",
"type"=>"IMEMO",
"shape_id"=>0,
"slot_size"=>40,
"imemo_type"=>"iseq",
"file"=>"/usr/local/bundle/ruby/3.3.0/bundler/gems/rails-139c5678aa5d/activerecord/lib/active_record/relation/query_methods.rb",
"generation"=>4,
"memsize"=>1736,
"flags"=>{"wb_protected"=>true, "old"=>true, "uncollectible"=>true, "marked"=>true}}=>
{"address"=>"0x7fc01d43c9e0",
"type"=>"OBJECT",
"shape_id"=>5394,
"slot_size"=>160,
"class"=>"0x7fc01d43c800",
"embedded"=>true,
"ivars"=>18,
"references"=>["0x7fc000f4c000", "0x7fc007f63168", "0x7fc01d43c760", "0x7fbfec306138", "0x7fc01d466740", "0x7fbfd5fd30f8"],
"file"=>"<internal:kernel>",
"line"=>48,
"method"=>"clone",
"generation"=>31,
"memsize"=>160,
"flags"=>{"wb_protected"=>true, "old"=>true, "uncollectible"=>true, "marked"=>true}}
irb#1():072> $diff.after.at("0x7fc01d466740").data
=>
{"address"=>"0x7fc01d466740",
"type"=>"OBJECT",
"shape_id"=>5439,
"slot_size"=>160,
"class"=>"0x7fbfed5b4ed8",
"embedded"=>true,
"ivars"=>12,
"references"=>["0x7fc01c55e108", "0x7fbfd60d1108", "0x7fbfd5fdad30", "0x7fbfd5fdad08", "0x7fc01d463b80", "0x7fc01d466560", "0x7fc01d466380"],
"file"=>"/usr/local/bundle/ruby/3.3.0/bundler/gems/rails-139c5678aa5d/activerecord/lib/active_record/associations.rb",
"line"=>320,
"method"=>"new",
"generation"=>31,
"memsize"=>160,
"flags"=>{"wb_protected"=>true, "old"=>true, "uncollectible"=>true, "marked"=>true}}
irb#1():073>
irb#1():075> $diff.after.at("0x7fbfd60d1108").data
=>
{"address"=>"0x7fbfd60d1108",
"type"=>"OBJECT",
"shape_id"=>6155,
"slot_size"=>40,
"class"=>"0x7fbfed48ed88",
"ivars"=>26,
"references"=>["0x7fbfdee4b420", "0x7fc01e29a820", "0x7fc0203c6198", "0x7fbfecef2820", "0x7fbfeceef0a8", "0x7fbfecee2038"],
"file"=>"/usr/local/bundle/ruby/3.3.0/bundler/gems/rails-139c5678aa5d/activerecord/lib/active_record/persistence.rb",
"line"=>642,
"method"=>"allocate",
"generation"=>31,
"memsize"=>296,
"flags"=>{"wb_protected"=>true, "old"=>true, "uncollectible"=>true, "marked"=>true}}
The fact that ActiveRecord instances hold references to their data looks good to me, to me looks like the issue is that yjit_root
keeps indirectly referencing them, which prevents the objects from being GCed hence the leak.
I've tried disabling YJIT on 1 host (hey-default-app-109) and the leak was gone, you can see it from this overnight graph of memory usage in the hosts:

In these hosts we are running YJIT with the default options so just RUBYOPT="--yjit"
Can you help me figure out what's going on here? This issue applies only to a subset of web hosts, which receives specific traffic that creates the "Entry" > "Message" > "Okra" objects.
Happy to tell you any additional info that could help, the next week I'll be OFF so if I won't answer quickly that's the reason 😅. Thanks!