Nokogiri: Memory leak when using Nokogiri::XML::Builder with XML namespaces

Created on 21 Jun 2018  ·  5Comments  ·  Source: sparklemotion/nokogiri

What problems are you experiencing?
Memory usage (VSZ and RSS) grows uncontrollably when using Nokogiri::XML::Builder with XML namespaces.

What's the output from nokogiri -v?

# Nokogiri (1.8.3)
    ---
    warnings: []
    nokogiri: 1.8.3
    ruby:
      version: 2.5.1
      platform: x86_64-linux
      description: ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
      engine: ruby
    libxml:
      binding: extension
      source: packaged
      libxml2_path: "/home/p/.gem/ruby/2.5.1/gems/nokogiri-1.8.3/ports/x86_64-pc-linux-gnu/libxml2/2.9.8"
      libxslt_path: "/home/p/.gem/ruby/2.5.1/gems/nokogiri-1.8.3/ports/x86_64-pc-linux-gnu/libxslt/1.1.32"
      libxml2_patches:
      - 0001-Revert-Do-not-URI-escape-in-server-side-includes.patch
      libxslt_patches: []
      compiled: 2.9.8
      loaded: 2.9.8

Can you provide a self-contained script that reproduces what you're seeing?

loop do
  Nokogiri::XML::Builder.new do |xml|
    xml.send 'env:Envelope', env: 'http://schemas.xmlsoap.org/soap/envelope/'
  end
end

In fact, even the following snippet is enough to observe the memory leak, although arguably incomplete without the namespace declaration:

loop do
  Nokogiri::XML::Builder.new do |xml|
    xml.send 'env:Envelope'
  end
end

The memory leak seems to be provoked solely by specifying a namespace prefix in the element name (here env:).

Here's a script doing essentially the same but with more informational output:

require 'nokogiri'
require 'awesome_print'

puts "Nokogiri version: #{Nokogiri::VERSION}"

def count_instances
  objs = Hash.new(0)
  ObjectSpace.each_object { |o| objs[o.class] += 1 }
  puts "Top 5 instances:"
  ap Hash[objs.sort_by{ |_, v| -v }.take(5)]
end

def obj_stat
  h = ObjectSpace.count_objects
  # puts "Total object count:\n#{h.ai}"
  puts "Total object count:\t#{h[:TOTAL] - h[:FREE]}"
  puts "heap_live_slots:\t#{GC.stat[:heap_live_slots]}"
end

def utilization
  puts "\nVSZ/RSS:\t\t#{`ps -o vsz,rss -p #{Process.pid} -h`}"
  obj_stat
  count_instances
end


100.times do
  5_000.times do
    Nokogiri::XML::Builder.new do |xml|
      xml.send 'env:Envelope'
    end
  end
  GC.start
  utilization
end

The output also confirms that it's not Ruby objects being leaked, as the number of Ruby objects does not change. Only VSZ and RSS grow.

According to Valgrind, most memory definitely lost is allocated in a function called xmlStrndup() in nokogiri.so:

==10000== 1,999,888 bytes in 499,972 blocks are definitely lost in loss record 11,339 of 11,340
==10000==    at 0x4C2E0D6: malloc (vg_replace_malloc.c:299)
==10000==    by 0x4F0B953: objspace_xmalloc0 (gc.c:7921)
==10000==    by 0x8953695: xmlStrndup (in /home/p/.gem/ruby/2.5.1/gems/nokogiri-1.8.3/lib/nokogiri/nokogiri.so)
==10000==    by 0x88D49F6: xmlSplitQName2 (in /home/p/.gem/ruby/2.5.1/gems/nokogiri-1.8.3/lib/nokogiri/nokogiri.so)
==10000==    by 0x88891DD: relink_namespace (xml_node.c:41)
==10000==    by 0x8889A37: reparent_node_with (xml_node.c:320)
==10000==    by 0x8889A37: add_child (xml_node.c:1142)
==10000==    by 0x504F345: vm_call_cfunc_with_frame (vm_insnhelper.c:1918)
==10000==    by 0x504F345: vm_call_cfunc (vm_insnhelper.c:1934)
==10000==    by 0x505AF88: vm_exec_core (insns.def:915)
==10000==    by 0x505EA26: vm_exec (vm.c:1778)
==10000==    by 0x5063616: vm_call0_body.constprop.141 (vm_eval.c:127)
==10000==    by 0x5063F73: vm_call0 (vm_eval.c:58)
==10000==    by 0x5063F73: rb_call0 (vm_eval.c:296)
==10000==    by 0x506470D: rb_call (vm_eval.c:589)
==10000==    by 0x506470D: rb_funcallv (vm_eval.c:815)
==10000== 
==10000== 4,499,847 bytes in 499,983 blocks are definitely lost in loss record 11,340 of 11,340
==10000==    at 0x4C2E0D6: malloc (vg_replace_malloc.c:299)
==10000==    by 0x4F0B953: objspace_xmalloc0 (gc.c:7921)
==10000==    by 0x8953695: xmlStrndup (in /home/p/.gem/ruby/2.5.1/gems/nokogiri-1.8.3/lib/nokogiri/nokogiri.so)
==10000==    by 0x8953735: xmlStrdup (in /home/p/.gem/ruby/2.5.1/gems/nokogiri-1.8.3/lib/nokogiri/nokogiri.so)
==10000==    by 0x88D4A37: xmlSplitQName2 (in /home/p/.gem/ruby/2.5.1/gems/nokogiri-1.8.3/lib/nokogiri/nokogiri.so)
==10000==    by 0x88891DD: relink_namespace (xml_node.c:41)
==10000==    by 0x8889A37: reparent_node_with (xml_node.c:320)
==10000==    by 0x8889A37: add_child (xml_node.c:1142)
==10000==    by 0x504F345: vm_call_cfunc_with_frame (vm_insnhelper.c:1918)
==10000==    by 0x504F345: vm_call_cfunc (vm_insnhelper.c:1934)
==10000==    by 0x505AF88: vm_exec_core (insns.def:915)
==10000==    by 0x505EA26: vm_exec (vm.c:1778)
==10000==    by 0x5063616: vm_call0_body.constprop.141 (vm_eval.c:127)
==10000==    by 0x5063F73: vm_call0 (vm_eval.c:58)
==10000==    by 0x5063F73: rb_call0 (vm_eval.c:296)

My wild guess is that the strings returned by xmlStrndup() should have been freed by relink_namespace() in xml_node.c, but I'm no expert in C.

topimemory

Most helpful comment

OK, I believe I've fixed this. Will be in 1.8.4 presuming the builds go green.

Thanks so much for the bug report, and the thorough and complete analysis that you did! Also, thanks for using the bug-report template.

♥ ♥ ♥

All 5 comments

Thanks for reporting. I'll investigate this weekend.

I've reproduced this memory leak. Looking now!

OK, I believe I've fixed this. Will be in 1.8.4 presuming the builds go green.

Thanks so much for the bug report, and the thorough and complete analysis that you did! Also, thanks for using the bug-report template.

♥ ♥ ♥

Thanks for the fix! 👍

We also found that the leak can be avoided in versions < 1.8.4 when setting the namespace like this
xml[:env].send 'Envelope', 'xmlns:env': 'http://schemas.xmlsoap.org/soap/envelope/' instead of xml.send 'env:Envelope'.

Was this page helpful?
0 / 5 - 0 ratings