class AllocationStats

Container for an aggregation of object allocation data. Pass a block to {#trace AllocationStats.new.trace}. Then use the AllocationStats object's public interface to dig into the data and discover useful information.

Copyright 2014 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0, found in the LICENSE file.

Copyright 2014 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0, found in the LICENSE file.

Constants

GEMDIR

a convenience constant

RUBYLIBDIR

a convenience constant

TRACE_RSPEC_HOOK

Attributes

burn[RW]

@!attribute [rw] burn @return [Fixnum] burn count for block tracing. Defaults to 0. When called with a block, trace will yield the block @burn-times before actually tracing the object allocations. This offers the benefit of pre-memoizing objects, and loading any required Ruby files before tracing.

gc_profiler_report[RW]
new_allocations[R]

@!attribute [r] new_allocations @return [Array] allocation data for all new objects that were allocated during the {#initialize} block. It is better to use {#allocations}, which returns an {AllocationsProxy}, which has a much more convenient, domain-specific API for filtering, sorting, and grouping {Allocation} objects, than this plain Array object.

Public Class Methods

add_to_top_sites(allocations, location, limit = 10) click to toggle source

Add a Hash of allocation groups (derived from an `AllocationStats.allocations…group_by(…)`) to the top allocation sites (file/line/class groups).

@param [Hash] allocations @param [String] location the RSpec spec location that was being executed

when the allocations occurred

@param [Fixnum] limit size of the top sites Array

# File lib/allocation_stats/trace_rspec.rb, line 72
def self.add_to_top_sites(allocations, location, limit = 10)
  if allocations.size > limit
    allocations = allocations.to_a[0...limit].to_h  # top 10 or so
  end

  # TODO: not a great algorithm so far... can instead:
  # * oly insert when an allocation won't be immediately dropped
  # * insert into correct position and pop rather than sort and slice
  allocations.each do |k,v|
    next if k[0] =~ /spec_helper\.rb$/

    if site = @top_sites.detect { |s| s[:key] == k }
      if lower_idx = site[:counts].index { |loc, count| count < v.size }
        site[:counts].insert(lower_idx, [location, v.size])
      else
        site[:counts] << [location, v.size]
      end
      site[:counts].pop if site[:counts].size > 3
    else
      @top_sites << { key: k, counts: [[location, v.size]] }
    end
  end

  @top_sites = @top_sites.sort_by! { |site|
    -site[:counts].map(&:last).max
  }[0...limit]
end
new(burn: 0) click to toggle source
# File lib/allocation_stats.rb, line 41
def initialize(burn: 0)
  @burn = burn
  # Copying ridiculous workaround from:
  # https://github.com/ruby/ruby/commit/7170baa878ac0223f26fcf8c8bf25492415e6eaa
  Class.name
end
top_sites() click to toggle source

Read the sorted list of the top “sites”, that is, top file/line/class groups, encountered while tracing RSpec.

@api private

# File lib/allocation_stats/trace_rspec.rb, line 52
def self.top_sites
  @top_sites
end
top_sites=(value) click to toggle source

Write to the sorted list of the top “sites”, that is, top file/line/class groups, encountered while tracing RSpec.

@api private

# File lib/allocation_stats/trace_rspec.rb, line 60
def self.top_sites=(value)
  @top_sites = value
end
top_sites_text() click to toggle source

Textual String representing the sorted list of the top allocation sites. For each site, this String includes the number of allocations, the class, the sourcefile, the sourceline, and the location of the RSpec spec.

@api private

# File lib/allocation_stats/trace_rspec.rb, line 105
def self.top_sites_text
  return "" if @top_sites.empty?

  result = "Top #{@top_sites.size} allocation sites:\n"
  @top_sites.each do |site|
    result << "  %s allocations at %s:%d\n" % [site[:key][2], site[:key][0], site[:key][1]]
    site[:counts].each do |location, count|
      result << "    %3d allocations during %s\n" % [count, location]
    end
  end

  result
end
trace(&block) click to toggle source
# File lib/allocation_stats.rb, line 48
def self.trace(&block)
  allocation_stats = AllocationStats.new
  allocation_stats.trace(&block)
end
trace_rspec() click to toggle source
# File lib/allocation_stats/trace_rspec.rb, line 5
def self.trace_rspec
  @top_sites = []

  if (!const_defined?(:RSpec))
    raise StandardError, "Cannot trace RSpec until RSpec is loaded"
  end

  ::RSpec.configure do |config|
    config.around(&TRACE_RSPEC_HOOK)
  end

  at_exit do
    puts AllocationStats.top_sites_text
  end
end

Public Instance Methods

allocations(alias_paths: false) click to toggle source

Proxy for the @new_allocations array that allows for individual filtering, sorting, and grouping of the Allocation objects.

# File lib/allocation_stats.rb, line 131
def allocations(alias_paths: false)
  AllocationsProxy.new(@new_allocations, alias_paths: alias_paths)
end
collect_new_allocations() click to toggle source
# File lib/allocation_stats.rb, line 104
def collect_new_allocations
  @new_allocations = []
  ObjectSpace.each_object.to_a.each do |object|
    next if ObjectSpace.allocation_sourcefile(object).nil?
    next if ObjectSpace.allocation_sourcefile(object) == __FILE__
    next if @existing_object_ids[object.__id__ / 1000] &&
            @existing_object_ids[object.__id__ / 1000].include?(object.__id__)

    @new_allocations << Allocation.new(object)
  end
end
inspect() click to toggle source

Inspect @new_allocations, the canonical array of {Allocation} objects.

# File lib/allocation_stats.rb, line 125
def inspect
  @new_allocations.inspect
end
start() click to toggle source

Begin tracing object allocations. Tracing must be stopped with AllocationStats#stop. Garbage collection is disabled while tracing is enabled.

# File lib/allocation_stats.rb, line 88
def start
  GC.start
  GC.disable

  @existing_object_ids = {}

  ObjectSpace.each_object.to_a.each do |object|
    @existing_object_ids[object.__id__ / 1000] ||= []
    @existing_object_ids[object.__id__ / 1000] << object.__id__
  end

  ObjectSpace.trace_object_allocations_start

  return self
end
stop() click to toggle source

Stop tracing object allocations that was started with AllocationStats#start.

# File lib/allocation_stats.rb, line 117
def stop
  collect_new_allocations
  ObjectSpace.trace_object_allocations_stop
  ObjectSpace.trace_object_allocations_clear
  profile_and_start_gc
end
trace(&block) click to toggle source
# File lib/allocation_stats.rb, line 53
def trace(&block)
  if block_given?
    trace_block(&block)
  else
    start
  end
end
trace_block() { || ... } click to toggle source
# File lib/allocation_stats.rb, line 61
def trace_block
  @burn.times { yield }

  GC.start
  GC.disable

  @existing_object_ids = {}

  ObjectSpace.each_object.to_a.each do |object|
    @existing_object_ids[object.__id__ / 1000] ||= []
    @existing_object_ids[object.__id__ / 1000] << object.__id__
  end

  ObjectSpace.trace_object_allocations {
    yield
  }

  collect_new_allocations
  ObjectSpace.trace_object_allocations_clear
  profile_and_start_gc

  return self
end

Private Instance Methods

profile_and_start_gc() click to toggle source
# File lib/allocation_stats.rb, line 135
def profile_and_start_gc
  GC::Profiler.enable
  GC.enable
  GC.start
  @gc_profiler_report = GC::Profiler.result
  GC::Profiler.disable
end