Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ flamegraph(open: false) {
}
```

### Explicit start and stop

You can also start and stop the flamegraph explicitly:

```ruby
# config/boot.rb
require "singed"
Singed.output_directory ||= Dir.pwd + "/tmp/speedscope"
Singed.start
# Let some code to run here...
# and then stop the flamegraph with e.g. rails runner 'Singed.stop'
Singed.stop
```

### RSpec

If you are using RSpec, you can use the `flamegraph` metadata to capture it for you.
Expand Down
24 changes: 24 additions & 0 deletions lib/singed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
module Singed
extend self

attr_reader :current_flamegraph

# Where should flamegraphs be saved?
def output_directory=(directory)
@output_directory = Pathname.new(directory)
Expand Down Expand Up @@ -46,6 +48,28 @@ def filter_line(line)
line
end

def start(label = nil, ignore_gc: false, interval: 1000)
return unless enabled?
return if profiling?

@current_flamegraph = Flamegraph.new(label: label, ignore_gc: ignore_gc, interval: interval)
@current_flamegraph.start
end

def stop
return nil unless profiling?

flamegraph = @current_flamegraph
@current_flamegraph = nil
flamegraph.stop
flamegraph.save
flamegraph
end

def profiling?
@current_flamegraph&.started? || false
end

autoload :Flamegraph, "singed/flamegraph"
autoload :Report, "singed/report"
autoload :RackMiddleware, "singed/rack_middleware"
Expand Down
31 changes: 24 additions & 7 deletions lib/singed/flamegraph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,31 @@ def initialize(label: nil, ignore_gc: false, interval: 1000, filename: nil)
end

def record
return yield unless Singed.enabled?
return yield if filename.exist? # file existing means its been captured already
start
yield
ensure
stop
end

result = nil
@profile = StackProf.run(mode: :wall, raw: true, ignore_gc: @ignore_gc, interval: @interval) do
result = yield
end
result
def start
return false unless Singed.enabled?
return false if filename.exist? # file existing means its been captured already
return false if started?

StackProf.start(mode: :wall, raw: true, ignore_gc: @ignore_gc, interval: @interval)
@started = true
end

def stop
return nil unless started?

@started = false
StackProf.stop
@profile = StackProf.results
end

def started?
!!@started
end

def save
Expand Down
85 changes: 85 additions & 0 deletions spec/singed_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

require "tempfile"
require "pathname"

RSpec.describe Singed do
around do |example|
original_output_directory = Singed.output_directory
original_enabled = Singed.enabled?
begin
example.run
ensure
Singed.output_directory = original_output_directory if original_output_directory
Singed.enabled = original_enabled
Singed.instance_variable_set(:@current_flamegraph, nil)
end
end

describe ".start" do
before do
Singed.enabled = true
Singed.output_directory = Dir.mktmpdir("singed-spec")
end

it "creates a current flamegraph and starts profiling" do
Singed.start

expect(Singed.current_flamegraph).to be_a(Singed::Flamegraph)
expect(Singed.profiling?).to be true
expect(Singed.current_flamegraph.started?).to be true
end

it "does nothing when already profiling" do
Singed.start
first = Singed.current_flamegraph
Singed.start

expect(Singed.current_flamegraph).to be first
end

it "does nothing when disabled" do
Singed.enabled = false
Singed.start

expect(Singed.current_flamegraph).to be_nil
expect(Singed.profiling?).to be false
end
end

describe ".stop" do
before do
Singed.enabled = true
Singed.output_directory = Dir.mktmpdir("singed-spec")
end

it "returns nil when not profiling" do
expect(Singed.stop).to be_nil
end

it "stops profiling, saves the result file, and returns the flamegraph with profile data" do
Singed.start
# Run some code to generate profile samples
100.times { 2**10 }
flamegraph = Singed.stop

expect(flamegraph).to be_a(Singed::Flamegraph)
expect(Singed.profiling?).to be false
expect(Singed.current_flamegraph).to be_nil

# Profile data is returned (StackProf results hash)
expect(flamegraph.profile).to be_a(Hash)
expect(flamegraph.profile).to include(:mode, :version, :interval)
expect(flamegraph.profile[:mode]).to eq(:wall)
expect(flamegraph.profile[:samples]).to be >= 0
end

it "creates the result file on disk" do
Singed.start
100.times { 2**10 }
flamegraph = Singed.stop

expect(Pathname(flamegraph.filename)).to exist
end
end
end