11/08/2018, 21:17

Tham khảo về memory-profiling và WeakRef trong Ruby

Gần đây tôi có đọc bài viết về analyize memory leak trên ruby của Sam Saffron. Bài viết có đề cập đến một số kỹ thuật debug khá thú vị như Sử dụng ObjectSpace để snapshot process hiện tại ObjectSpace.each_object do |o| begin object_ids << o.object_id rescue # skip ...

Gần đây tôi có đọc bài viết về analyize memory leak trên ruby của Sam Saffron. Bài viết có đề cập đến một số kỹ thuật debug khá thú vị như

  • Sử dụng ObjectSpace để snapshot process hiện tại
ObjectSpace.each_object do |o|
 begin
    object_ids << o.object_id
  rescue
    # skip
  end
end

IO.binwrite(filename, Marshal::dump(object_ids))
  • Sử dụng heapdump để dump ra cả quá trình allocate object và destroy object dựa trên các lần GC (mỗi lần GC được gọi là một generation)
require 'objspace'
ObjectSpace.trace_object_allocations_start

io=File.open("/tmp/my_dump", "w")
ObjectSpace.dump_all(output: io); 
io.close

Sử dụng dump_all sẽ giúp chúng ta có thêm một số thông tin rất hữu ích như: sau mỗi lần GC sẽ có những object nào, object đó phát sinh từ dòng code nào.

Thử chạy dump_all trên môi trường cá nhân, tôi thu được một số thông tin rất hữu ích, ví dụ như:

{"address":"0x7f8db8aabe08", "type":"STRING", "class":"0x7f8db0829660", "embedded":true, "bytesize":1, "value":"
", "encoding":"UTF-8", "flags":{"wb_protected":true, "old":true, "marked":true}}

Từ thông tin trên tôi sẽ thấy có một object String, được lưu ở địa chỉ 0x7f8db8aabe08, class nằm ở địa chỉ 0x7f8db0829660, có size là 1 byte, giá trị là newline, encoding là UTF-8, flag gc là wb_protected, đã được mark để xử lý ở lần GC tiếp theo.

Những bạn nào muốn tham khảo kĩ hơn về các technique đó có thể tham khảo ở bài viết trên. Hơi lan man một chút nhưng tôi sẽ trở về chủ đề chính của bài viết là về WeakRef

Ở bài viết trên của Sam, thì có đề cập đến việc gem therubyracer bị leak weak reference của một số lượng lớn các object:

At the top of our list we can see our JavaScript engine therubyracer is leaking lots of objects, in particular we can see the weak references it uses to maintain Ruby to JavaScript mappings are being kept around for way too long.

WeakRef là một khái niệm khá thú vị, và có ứng dụng trong một số bài toán khá hay. Tài liệu về WeakRef có thể được tìm thấy tại đây.

Weak Reference class that allows a referenced object to be garbage-collected.

Từ định nghĩa trên bạn có thể hình dung Weak Reference dùng trong trường hợp khi bạn có những object mà chỉ tồn tại trong một khoảng thời gian ngắn, cho những mục đích tức thời. Những object này sẽ được GC giải quyết ngay khi có thể.

Hãy thử xem qua ví dụ trên tài liệu của ruby-doc:

foo = Object.new            # create a new object instance
p foo.to_s                  # original's class
foo = WeakRef.new(foo)      # reassign foo with WeakRef instance
p foo.to_s                  # should be same class
GC.start                    # start the garbage collector
p foo.to_s                  # should raise exception (recycled)

Từ ví dụ trên chúng ta có thể thấy 2 điểm:

  • Có thể gán được một object đã tồn tại thành WeakReference
  • WeakRefrence object sẽ được collect ngay khi GC được chạy

Vậy chúng ta nên dùng WeakRef khi nào? Cũng giống như java, WeakRef được sử dụng nhiều cho mục đích caching, khi mà một số quan hệ mapping, hay object mà chúng ta muốn lưu chúng tạm thời nhằm mục đích tăng tốc các xử lý lặp đi lặp lại nhiều lần.

Ví dụ trong ruby-doc cũng thể hiện khá rõ ý tưởng này với implementation của weak-hash:

require 'weakref'

class WeakHash < Hash
  def []= key, obj
    super WeakRef.new(key), WeakRef.new(obj)
  end
end

WeakHash sẽ lưu giữ các cặp key và object, và việc sử dụng WeakRef ở cả key và obj sẽ giúp cho hash object có thể được dọn dẹp bất cứ khi nào hệ thống thiếu bộ nhớ.

Đào sâu một chút, chúng ta có thể thấy sử dụng WeakHash để lưu cache về view, hay cache về translation trong rails có thể đem lại hiệu quả tốt hơn về bộ nhớ, so với việc sử dụng cache mà phải invalidate trực tiếp như hiện tại.

Trở lại bài viết của Sam, tại sao việc sử dụng WeakRef lại gây ra leak, và trong trường hợp thế nào thì sẽ gây leak?
Bài viết của Sam đề cập đến đoạn code gây leak như ở dưới đây, đoạn code nằm trong source code của therubyracer

class WeakValueMap
   def initialize
      @values = {}
   end

   def [](key)
      if ref = @values[key]
        ref.object
      end
   end

   def []=(key, value)
     @values[key] = V8::Weak::Ref.new(value)
   end
end

Các bạn có thể thấy đoạn code khả nghi

     @values[key] = V8::Weak::Ref.new(value)

đoạn code này sẽ tạo ra một WeakRef mỗi lần có một cặp key-value được gán vào map. Vấn đề là ở chỗ

ngay cả khi key đã tồn tại rồi thì vẫn có một WeakRef mới được tạo ra

Vậy vấn đề ở đây là gì? WeakRef mới được tạo ra nhưng reference đến nó là key, thì lại không được xoá đi, khiến cho WeakRef đó tồn tại mãi mãi với chương trình, và đây chính là nguyên nhân gây ra leak. Rất thú vị phải không :).

Để giải quyết bài toán này, Sam đã làm như sau:

class WeakValueMap
  ...

  def []=(key, value)
    ref = V8::Weak::Ref.new(value)
    ObjectSpace.define_finalizer(value, self.class.ensure_cleanup(@values, key, ref))

    @values[key] = ref
  end

  def self.ensure_cleanup(values,key,ref)
    proc {
      values.delete(key) if values[key] == ref
    }
  end
end

Sam giải quyết bài toán bằng cách

Mỗi khi key của hash được xoá đi, thì sẽ thực hiện clean up value tương ứng với key đó một cách thủ công

Cách giải quyết trên cho chúng ta học được một kĩ thuật mới, đó là sử dụng hàm define_finalizer của ObjectSpace. Hàm này các bạn có thể hiểu nó giống như de-constructor trong C++, sẽ được gọi khi object được clean-up hoặc bị xoá đi, hoặc chương trình kết thúc.

Như vậy chúng ta đã nắm thêm được rất nhiều kĩ thuật mới để profiling memory tren ruby cũng như về khái niệm, và cách sử dụng WeakReference trên ruby.

Tham khảo

  • http://tmm1.net/ruby21-oobgc/
  • http://blog.skylight.io/hunting-for-leaks-in-ruby/
0