Skip to content

YJIT is indirectly retaining ActiveRecord Object instances #552

Closed
@intrip

Description

@intrip

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:

Screenshot 2024-01-19 at 12 01 11

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions