Skip to content

Instantly share code, notes, and snippets.

@palkan
Last active January 28, 2021 21:38
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save palkan/ee031c4f857e33ca8c7ff28f5c2df117 to your computer and use it in GitHub Desktop.
Save palkan/ee031c4f857e33ca8c7ff28f5c2df117 to your computer and use it in GitHub Desktop.
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']
# if you want to render flamegraphs
gem "stackprof", require: false # required by flamegraph
gem "flamegraph", require: false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment