FactoryProf: profiler for your FactoryGirl
class FactoryProf | |
module FloatDuration | |
refine Float do | |
def duration | |
t = self | |
format("%02d:%02d.%03d", t / 60, t % 60, t.modulo(1) * 1000) | |
end | |
end | |
end | |
using FloatDuration | |
module RunnerExt | |
def run(strategy = @strategy) | |
return super unless strategy == :create | |
FactoryProf.tracker.track(@name) do | |
super | |
end | |
end | |
end | |
class << self | |
attr_reader :tracker | |
def init | |
FactoryGirl::FactoryRunner.prepend RunnerExt | |
@flamegraph = ENV['FPROF'] == 'flamegraph' | |
@tracker = flamegraph? ? FlamegraphTracker.new : Tracker.new | |
end | |
def flamegraph? | |
@flamegraph == true | |
end | |
end | |
class Tracker | |
def initialize | |
@depth = 0 | |
end | |
def track(factory) | |
@depth += 1 | |
res = nil | |
begin | |
res = if @depth == 1 | |
ActiveSupport::Notifications.instrument('factory.create', name: factory) { yield } | |
else | |
yield | |
end | |
ensure | |
@depth -= 1 | |
end | |
res | |
end | |
end | |
module FlamegraphRendererExt | |
def graph_data | |
table = [] | |
prev = [] | |
@stacks.each_with_index do |stack, _pos| | |
next unless stack | |
col = [] | |
stack.each_with_index do |(frame, _time), i| | |
if !prev[i].nil? | |
last_col = prev[i] | |
if last_col[0] == frame | |
last_col[1] += 1 | |
col << nil | |
next | |
end | |
end | |
prev[i] = [frame, 1] | |
col << prev[i] | |
end | |
prev = prev[0..col.length - 1].to_a | |
table << col | |
end | |
data = [] | |
table.each_with_index do |col, col_num| | |
col.each_with_index do |row, row_num| | |
next unless row && row.length == 2 | |
data << { | |
x: col_num + 1, | |
y: row_num + 1, | |
width: row[1], | |
frame: "`#{row[0]}" | |
} | |
end | |
end | |
data | |
end | |
end | |
class FlamegraphTracker | |
class Stack < Array | |
attr_reader :fingerprint | |
def initialize | |
super | |
@fingerprint = '' | |
end | |
def <<(sample) | |
@fingerprint += ":#{sample.first}" | |
super | |
end | |
end | |
def initialize | |
require "flamegraph" | |
Flamegraph::Renderer.prepend(FlamegraphRendererExt) | |
@stacks = [] | |
@depth = 0 | |
@total_time = 0.0 | |
@current_stack = Stack.new | |
at_exit { render_flamegraph } | |
end | |
def track(factory) | |
@depth += 1 | |
start = Time.now | |
sample = [factory] | |
@current_stack << sample | |
res = nil | |
begin | |
res = yield | |
ensure | |
sample << (Time.now - start) | |
@depth -= 1 | |
flush_sample if @depth.zero? | |
end | |
res | |
end | |
def flush_sample | |
@total_time += @current_stack.first.last | |
@stacks << @current_stack | |
@current_stack = Stack.new | |
end | |
def render_flamegraph | |
sorted_stacks = @stacks.sort_by(&:fingerprint) | |
renderer = Flamegraph::Renderer.new(sorted_stacks) | |
rendered = renderer.graph_html(false) | |
filename = "tmp/factory-flame-#{Time.now.to_i}.html" | |
File.open(Rails.root.join(filename), "w") do |f| | |
f.write(rendered) | |
end | |
puts "\nFlamegraph written to #{filename}" | |
puts "\nTotal time: #{@total_time.duration}" | |
end | |
end | |
end | |
FactoryProf.init if ENV['FPROF'] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment