Skip to content

Commit 09fa531

Browse files
authored
Methods for explicit start and stop flamegraph recording (#52)
* Methods for explicit start and stop * add some tests * Code review fixes
1 parent b163d73 commit 09fa531

File tree

4 files changed

+143
-8
lines changed

4 files changed

+143
-8
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ flamegraph(open: false) {
4747
}
4848
```
4949

50+
### Explicit start and stop
51+
52+
You can also start and stop the flamegraph explicitly:
53+
54+
```ruby
55+
# config/boot.rb
56+
require "singed"
57+
Singed.output_directory ||= Dir.pwd + "/tmp/speedscope"
58+
Singed.start
59+
# Let some code to run here...
60+
# and then stop the flamegraph with e.g. rails runner 'Singed.stop'
61+
flamegraph = Singed.stop
62+
# The flamegraph is saved to the output directory
63+
# Open it with your browser:
64+
flamegraph.open
65+
```
66+
67+
Note that `Singed.start` can't be run multiple times in parallel, instantiate multiple `Singed::Flamegraph` objects instead and call `start` on them.
68+
5069
### RSpec
5170

5271
If you are using RSpec, you can use the `flamegraph` metadata to capture it for you.

lib/singed.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module Singed
99

1010
# Where should flamegraphs be saved?
1111
def output_directory=(directory)
12-
@output_directory = Pathname.new(directory)
12+
@output_directory = directory && Pathname.new(directory)
1313
end
1414

1515
def self.output_directory
@@ -46,6 +46,28 @@ def filter_line(line)
4646
line
4747
end
4848

49+
def start(label = nil, ignore_gc: false, interval: 1000)
50+
return unless enabled?
51+
return if profiling?
52+
53+
@current_flamegraph = Flamegraph.new(label: label, ignore_gc: ignore_gc, interval: interval)
54+
@current_flamegraph.tap(&:start)
55+
end
56+
57+
def stop
58+
return nil unless profiling?
59+
60+
flamegraph = @current_flamegraph
61+
@current_flamegraph = nil
62+
flamegraph.stop
63+
flamegraph.save
64+
flamegraph
65+
end
66+
67+
def profiling?
68+
@current_flamegraph&.started? || false
69+
end
70+
4971
autoload :Flamegraph, "singed/flamegraph"
5072
autoload :Report, "singed/report"
5173
autoload :RackMiddleware, "singed/rack_middleware"

lib/singed/flamegraph.rb

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,31 @@ def initialize(label: nil, ignore_gc: false, interval: 1000, filename: nil)
2323
end
2424

2525
def record
26-
return yield unless Singed.enabled?
27-
return yield if filename.exist? # file existing means its been captured already
26+
start
27+
yield
28+
ensure
29+
stop
30+
end
2831

29-
result = nil
30-
@profile = StackProf.run(mode: :wall, raw: true, ignore_gc: @ignore_gc, interval: @interval) do
31-
result = yield
32-
end
33-
result
32+
def start
33+
return false unless Singed.enabled?
34+
return false if filename.exist? # file existing means its been captured already
35+
return false if started?
36+
37+
StackProf.start(mode: :wall, raw: true, ignore_gc: @ignore_gc, interval: @interval)
38+
@started = true
39+
end
40+
41+
def stop
42+
return nil unless started?
43+
44+
@started = false
45+
StackProf.stop
46+
@profile = StackProf.results
47+
end
48+
49+
def started?
50+
!!@started
3451
end
3552

3653
def save

spec/singed_spec.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require "tempfile"
4+
require "pathname"
5+
6+
RSpec.describe Singed do
7+
around do |example|
8+
original_output_directory = Singed.output_directory
9+
Singed.output_directory = Dir.mktmpdir("singed-spec")
10+
original_enabled = Singed.enabled?
11+
begin
12+
example.run
13+
ensure
14+
Singed.output_directory = original_output_directory
15+
Singed.enabled = original_enabled
16+
Singed.instance_variable_set(:@current_flamegraph, nil)
17+
end
18+
end
19+
20+
describe ".start" do
21+
before { Singed.enabled = true }
22+
23+
it "creates a current flamegraph and starts profiling" do
24+
current_flamegraph = Singed.start
25+
26+
expect(current_flamegraph).to be_a(Singed::Flamegraph)
27+
expect(Singed.profiling?).to be true
28+
expect(current_flamegraph.started?).to be true
29+
end
30+
31+
it "does nothing when already profiling" do
32+
Singed.start
33+
expect(Singed.start).to be_nil
34+
end
35+
36+
it "does nothing when disabled" do
37+
Singed.enabled = false
38+
expect(Singed.start).to be_nil
39+
expect(Singed.profiling?).to be_falsey
40+
end
41+
end
42+
43+
describe ".stop" do
44+
before do
45+
Singed.enabled = true
46+
Singed.output_directory = Dir.mktmpdir("singed-spec")
47+
end
48+
49+
it "returns nil when not profiling" do
50+
expect(Singed.stop).to be_nil
51+
end
52+
53+
it "stops profiling, saves the result file, and returns the flamegraph with profile data" do
54+
Singed.start
55+
# Run some code to generate profile samples
56+
100.times { 2**10 }
57+
flamegraph = Singed.stop
58+
59+
expect(flamegraph).to be_a(Singed::Flamegraph)
60+
expect(Singed.profiling?).to be false
61+
62+
# Profile data is returned (StackProf results hash)
63+
expect(flamegraph.profile).to be_a(Hash)
64+
expect(flamegraph.profile).to include(:mode, :version, :interval)
65+
expect(flamegraph.profile[:mode]).to eq(:wall)
66+
expect(flamegraph.profile[:samples]).to be >= 0
67+
end
68+
69+
it "creates the result file on disk" do
70+
Singed.start
71+
100.times { 2**10 }
72+
flamegraph = Singed.stop
73+
74+
expect(Pathname(flamegraph.filename)).to exist
75+
end
76+
end
77+
end

0 commit comments

Comments
 (0)