1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
|
# frozen_string_literal: true
require 'objspace'
module MemoryProfiler
# Reporter is the top level API used for generating memory reports.
#
# @example Measure object allocation in a block
# report = Reporter.report(top: 50) do
# 5.times { "foo" }
# end
class Reporter
class << self
attr_accessor :current_reporter
end
attr_reader :top, :trace, :generation, :report_results
def initialize(opts = {})
@top = opts[:top] || 50
@trace = opts[:trace] && Array(opts[:trace])
@ignore_files = opts[:ignore_files] && Regexp.new(opts[:ignore_files])
@allow_files = opts[:allow_files] && /#{Array(opts[:allow_files]).join('|')}/
end
# Helper for generating new reporter and running against block.
# @param [Hash] opts the options to create a report with
# @option opts :top max number of entries to output
# @option opts :trace a class or an array of classes you explicitly want to trace
# @option opts :ignore_files a regular expression used to exclude certain files from tracing
# @option opts :allow_files a string or array of strings to selectively include in tracing
# @return [MemoryProfiler::Results]
def self.report(opts = {}, &block)
self.new(opts).run(&block)
end
def start
3.times { GC.start }
GC.start
GC.disable
@generation = GC.count
ObjectSpace.trace_object_allocations_start
end
def stop
ObjectSpace.trace_object_allocations_stop
allocated = object_list(generation)
retained = StatHash.new.compare_by_identity
GC.enable
# for whatever reason doing GC in a block is more effective at
# freeing objects.
# full_mark: true, immediate_mark: true, immediate_sweep: true are already default
3.times { GC.start }
# another start outside of the block to release the block
GC.start
# Caution: Do not allocate any new Objects between the call to GC.start and the completion of the retained
# lookups. It is likely that a new Object would reuse an object_id from a GC'd object.
ObjectSpace.each_object do |obj|
next unless ObjectSpace.allocation_generation(obj) == generation
found = allocated[obj.__id__]
retained[obj.__id__] = found if found
end
ObjectSpace.trace_object_allocations_clear
@report_results = Results.new
@report_results.register_results(allocated, retained, top)
end
# Collects object allocation and memory of ruby code inside of passed block.
def run(&block)
start
begin
yield
rescue Exception
ObjectSpace.trace_object_allocations_stop
GC.enable
raise
else
stop
end
end
private
# Iterates through objects in memory of a given generation.
# Stores results along with meta data of objects collected.
def object_list(generation)
helper = Helpers.new
result = StatHash.new.compare_by_identity
ObjectSpace.each_object do |obj|
next unless ObjectSpace.allocation_generation(obj) == generation
file = ObjectSpace.allocation_sourcefile(obj) || "(no name)"
next if @ignore_files && @ignore_files =~ file
next if @allow_files && !(@allow_files =~ file)
klass = helper.object_class(obj)
next if @trace && !trace.include?(klass)
begin
line = ObjectSpace.allocation_sourceline(obj)
location = helper.lookup_location(file, line)
class_name = helper.lookup_class_name(klass)
gem = helper.guess_gem(file)
# we do memsize first to avoid freezing as a side effect and shifting
# storage to the new frozen string, this happens on @hash[s] in lookup_string
memsize = ObjectSpace.memsize_of(obj)
string = klass == String ? helper.lookup_string(obj) : nil
# compensate for API bug
memsize = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] if memsize > 100_000_000_000
result[obj.__id__] = MemoryProfiler::Stat.new(class_name, gem, file, location, memsize, string)
rescue
# give up if any any error occurs inspecting the object
end
end
result
end
end
end
|