Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vernier #29

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ PATH
singed (0.2.2)
colorize
stackprof (>= 0.2.13)
vernier

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -67,6 +68,7 @@ GEM
lint_roller (~> 1.1)
rubocop-performance (~> 1.20.2)
unicode-display_width (2.5.0)
vernier (1.0.1)

PLATFORMS
ruby
Expand Down
38 changes: 38 additions & 0 deletions lib/singed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "json"
require "stackprof"
require "colorize"
require "vernier"

module Singed
extend self
Expand Down Expand Up @@ -34,6 +35,10 @@ def backtrace_cleaner
@backtrace_cleaner
end

def vernier_hooks
@vernier_hooks ||= []
end

def silence_line?(line)
return backtrace_cleaner.silence_line?(line) if backtrace_cleaner

Expand All @@ -46,6 +51,39 @@ def filter_line(line)
line
end

def profiler_class_for(profiler)
case profiler
when :stackprof, nil then Singed::Flamegraph::Stackprof
when :vernier then Singed::Flamegraph::Vernier
else
raise ArgumentError, "Unknown profiler: #{profiler}"
end
end

def profile(label = "flamegraph", profiler: nil, open: true, announce_io: $stdout, **profiler_options, &)
profiler_class = profiler_class_for(profiler)

fg = profiler_class.new(
label: label,
announce_io: announce_io,
**profiler_options
)

result = fg.record(&)
fg.save
fg.open if open

result
end

def stackprof(label = "stackprof", open: true, announce_io: $stdout, **stackprof_options, &)
profile(label, profiler: :stackprof, open: open, announce_io: announce_io, **stackprof_options, &)
end

def vernier(label = "vernier", open: true, announce_io: $stdout, **vernier_options, &)
profile(label, profiler: :vernier, open: open, announce_io: announce_io, **vernier_options, &)
end

autoload :Flamegraph, "singed/flamegraph"
autoload :Report, "singed/report"
autoload :RackMiddleware, "singed/rack_middleware"
Expand Down
132 changes: 93 additions & 39 deletions lib/singed/flamegraph.rb
Original file line number Diff line number Diff line change
@@ -1,55 +1,37 @@
module Singed
class Flamegraph
attr_accessor :profile, :filename
attr_accessor :profile, :filename, :announce_io

def initialize(label: nil, ignore_gc: false, interval: 1000, filename: nil)
# it's been created elsewhere, ie rbspy
if filename
if ignore_gc
raise ArgumentError, "ignore_gc not supported when given an existing file"
end

if label
raise ArgumentError, "label not supported when given an existing file"
end

@filename = filename
else
@ignore_gc = ignore_gc
@interval = interval
@time = Time.now # rubocop:disable Rails/TimeZone
@filename = self.class.generate_filename(label: label, time: @time)
end
def initialize(label: nil, announce_io: $stdout)
@time = Time.now
@announce_io = announce_io
@filename ||= self.class.generate_filename(label: label, time: @time)
end

def record
return yield unless Singed.enabled?
return yield if filename.exist? # file existing means its been captured already
def record(&block)
raise NotImplementedError
end

result = nil
@profile = StackProf.run(mode: :wall, raw: true, ignore_gc: @ignore_gc, interval: @interval) do
result = yield
end
result
def record?
Singed.enabled?
end

def save
if filename.exist?
raise ArgumentError, "File #{filename} already exists"
end

report = Singed::Report.new(@profile)
report.filter!
filename.dirname.mkpath
filename.open("w") { |f| report.print_json(f) }
raise NotImplementedError
end

def open
system open_command
def open_command
raise NotImplementedError
end

def open_command
@open_command ||= "npx speedscope #{@filename}"
def open(open: true)
if open
# use npx, so we don't have to add it as a dependency
announce_io.puts "🔥📈 #{"Captured flamegraph, opening with".colorize(:bold).colorize(:red)}: #{open_command}"
system open_command
else
announce_io.puts "🔥📈 #{"Captured flamegraph to file".colorize(:bold).colorize(:red)}: #{filename}"
end
end

def self.generate_filename(label: nil, time: Time.now) # rubocop:disable Rails/TimeZone
Expand All @@ -62,5 +44,77 @@
file = file.relative_path_from(pwd) if file.absolute? && file.to_s.start_with?(pwd.to_s)
file
end

def self.validate_options(klass, method_name, options)
method = klass.instance_method(:method_name)
options.each do |key, value|
if method.parameters.none? { |type, name| type == :key && name == key }
raise ArgumentError, "Unknown option #{key} for #{klass}.#{method_name}"
end
end
end

class Stackprof < Flamegraph
DEFAULT_OPTIONS = {
mode: :wall,
raw: true
}.freeze

def initialize(label: nil, announce_io: $stdout, **stackprof_options)
super(label: label)
@stackprof_options = stackprof_options
end

def record(&block)
result = nil
stackprof_options = DEFAULT_OPTIONS.merge(@stackprof_options)
@profile = ::StackProf.run(**stackprof_options) do
result = yield
end
result
end

def save
if filename.exist?
raise ArgumentError, "File #{filename} already exists"
end

report = Singed::Report.new(@profile)
report.filter!
filename.dirname.mkpath
filename.open("w") { |f| report.print_json(f) }
end

def open_command
# use npx, so we don't have to add it as a dependency
@open_command ||= "npx speedscope #{@filename}"
end


Check failure on line 93 in lib/singed/flamegraph.rb

View workflow job for this annotation

GitHub Actions / StandardRB

lib/singed/flamegraph.rb#L92-L93

Extra empty line detected at class body end.
end

Check failure on line 94 in lib/singed/flamegraph.rb

View workflow job for this annotation

GitHub Actions / StandardRB

lib/singed/flamegraph.rb#L93-L94

Extra blank line detected.

Check failure on line 94 in lib/singed/flamegraph.rb

View workflow job for this annotation

GitHub Actions / StandardRB

lib/singed/flamegraph.rb#L93-L94

Extra empty line detected at class body end.

class Vernier < Flamegraph
def initialize(label: nil, announce_io: $stdout, **vernier_options)
super(label: label, announce_io: announce_io)

@vernier_options = {hooks: Singed.vernier_hooks}.merge(vernier_options)
end

def record
vernier_options = {out: filename.to_s}.merge(@vernier_options)
validate_options(::Vernier, :run, vernier_options)
::Vernier.run(**vernier_options) do
yield
end
end

def open_command
@open_command ||= "profile-viewer #{@filename}"
end

def save
# no-op, since it already writes out
end
end
end
end
16 changes: 2 additions & 14 deletions lib/singed/kernel_ext.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
module Kernel
def flamegraph(label = nil, open: true, ignore_gc: false, interval: 1000, io: $stdout, &)
fg = Singed::Flamegraph.new(label: label, ignore_gc: ignore_gc, interval: interval)
result = fg.record(&)
fg.save

if open
# use npx, so we don't have to add it as a dependency
io.puts "🔥📈 #{"Captured flamegraph, opening with".colorize(:bold).colorize(:red)}: #{fg.open_command}"
fg.open
else
io.puts "🔥📈 #{"Captured flamegraph to file".colorize(:bold).colorize(:red)}: #{fg.filename}"
end

result
def flamegraph(label = nil, profiler: nil, open: true, io: $stdout, **profiler_options, &)
Singed.profile(label, profiler: profiler, open: open, announce_io: io, **profiler_options, &)
end
end
2 changes: 2 additions & 0 deletions lib/singed/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class Railtie < Rails::Railtie
ActiveSupport.on_load(:action_controller) do
ActionController::Base.include(Singed::ControllerExt)
end

Singed.vernier_hooks << :rails
end

def self.init!
Expand Down
1 change: 1 addition & 0 deletions singed.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Gem::Specification.new do |spec|

spec.add_dependency "colorize"
spec.add_dependency "stackprof", ">= 0.2.13"
spec.add_dependency "vernier"

spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec"
Expand Down
Loading