Class: AllocationStats

Inherits:
Object
  • Object
show all
Defined in:
lib/allocation_stats.rb,
lib/allocation_stats/allocation.rb,
lib/allocation_stats/trace_rspec.rb,
lib/allocation_stats/allocations_proxy.rb

Overview

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

Defined Under Namespace

Classes: Allocation, AllocationsProxy

Constant Summary collapse

RUBYLIBDIR =

a convenience constant

RbConfig::CONFIG["rubylibdir"]
GEMDIR =

a convenience constant

Gem.dir
TRACE_RSPEC_HOOK =
proc do |example|
   # TODO s/false/some config option/
   if true  # wrap loosely
     stats = AllocationStats.new(burn: 1).trace { example.run }
   else      # wrap tightly
     # Super hacky, but presumably more correct results?
     stats = AllocationStats.new(burn: 1)
     example_block = @example.instance_variable_get(:@example_block).clone

     @example.instance_variable_set(
       :@example_block,
       Proc.new do
         stats.trace { example_block.call }
       end
     )

     example.run
   end

   allocations = stats.allocations(alias_paths: true).
     not_from("rspec-core").not_from("rspec-expectations").not_from("rspec-mocks").
     group_by(:sourcefile, :sourceline, :class).
     sort_by_count

   AllocationStats.add_to_top_sites(allocations.all, @example.location)
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(burn: 0) ⇒ AllocationStats

Returns a new instance of AllocationStats.



41
42
43
44
45
46
# 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

Instance Attribute Details

#burnFixnum

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.

Returns:

  • (Fixnum)


28
29
30
# File 'lib/allocation_stats.rb', line 28

def burn
  @burn
end

#gc_profiler_reportObject

Returns the value of attribute gc_profiler_report.



30
31
32
# File 'lib/allocation_stats.rb', line 30

def gc_profiler_report
  @gc_profiler_report
end

#new_allocationsArray (readonly)

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.

Returns:

  • (Array)


39
40
41
# File 'lib/allocation_stats.rb', line 39

def new_allocations
  @new_allocations
end

Class Method Details

.add_to_top_sites(allocations, location, limit = 10) ⇒ Object

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

Parameters:

  • allocations (Hash)
  • location (String)

    the RSpec spec location that was being executed when the allocations occurred

  • limit (Fixnum) (defaults to: 10)

    size of the top sites Array



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
# 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

.top_sitesObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



52
53
54
# File 'lib/allocation_stats/trace_rspec.rb', line 52

def self.top_sites
  @top_sites
end

.top_sites=(value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



60
61
62
# File 'lib/allocation_stats/trace_rspec.rb', line 60

def self.top_sites=(value)
  @top_sites = value
end

.top_sites_textObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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.



105
106
107
108
109
110
111
112
113
114
115
116
117
# 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) ⇒ Object



48
49
50
51
# File 'lib/allocation_stats.rb', line 48

def self.trace(&block)
  allocation_stats = AllocationStats.new
  allocation_stats.trace(&block)
end

.trace_rspecObject



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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

Instance Method Details

#allocations(alias_paths: false) ⇒ Object

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



131
132
133
# File 'lib/allocation_stats.rb', line 131

def allocations(alias_paths: false)
  AllocationsProxy.new(@new_allocations, alias_paths: alias_paths)
end

#collect_new_allocationsObject



104
105
106
107
108
109
110
111
112
113
114
# 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

#inspectObject

Inspect @new_allocations, the canonical array of Allocation objects.



125
126
127
# File 'lib/allocation_stats.rb', line 125

def inspect
  @new_allocations.inspect
end

#startObject

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



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 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

#stopObject

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



117
118
119
120
121
122
# 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) ⇒ Object



53
54
55
56
57
58
59
# File 'lib/allocation_stats.rb', line 53

def trace(&block)
  if block_given?
    trace_block(&block)
  else
    start
  end
end

#trace_blockObject



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 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