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
7 changes: 5 additions & 2 deletions ext/stackprof/stackprof.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ static struct {

size_t overall_signals;
size_t overall_samples;
size_t newobj_signals;
size_t during_gc;
size_t unrecorded_gc_samples;
st_table *frames;
Expand Down Expand Up @@ -96,6 +97,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
_stackprof.frames = st_init_numtable();
_stackprof.overall_signals = 0;
_stackprof.overall_samples = 0;
_stackprof.newobj_signals = 0;
_stackprof.during_gc = 0;
}

Expand Down Expand Up @@ -572,9 +574,10 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
static void
stackprof_newobj_handler(VALUE tpval, void *data)
{
_stackprof.overall_signals++;
if (RTEST(_stackprof.interval) && _stackprof.overall_signals % NUM2LONG(_stackprof.interval))
_stackprof.newobj_signals++;
if (RTEST(_stackprof.interval) && _stackprof.newobj_signals % NUM2LONG(_stackprof.interval))
return;
_stackprof.overall_signals++;
stackprof_job_handler(0);
}

Expand Down
4 changes: 2 additions & 2 deletions lib/stackprof/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ def initialize(app, options = {})
end

def call(env)
enabled = Middleware.enabled?(env)
StackProf.start(mode: Middleware.mode, interval: Middleware.interval, raw: Middleware.raw) if enabled
enabled, mode = Middleware.enabled?(env)
StackProf.start(mode: mode || Middleware.mode, interval: Middleware.interval, raw: Middleware.raw) if enabled
@app.call(env)
ensure
if enabled
Expand Down
33 changes: 28 additions & 5 deletions lib/stackprof/report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,34 @@ def frames(sort_by_total=false)
@data[:frames].sort_by{ |iseq, stats| -stats[sort_by_total ? :total_samples : :samples] }.inject({}){|h, (k, v)| h[k] = v; h}
end

# normalized frames is used when we want to combine multiple
# output files, (via +() below). In order to simplify combination
# of files that are from "different" source revisions, the frames
# are normalised by converting them to an MD5 using the "name, file, line"
# once frames are converted, every edge that references the original
# needs to adjusted also.
def normalized_frames
id2hash = {}
@data[:frames].each do |frame, info|
id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}")
# id2 hash contains the original frames converted into a MD5
# based on the name, file, line of the original frame
# flamegraph likes the frames to be numbers, so md5.to_i(16)
id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}").to_i(16)
end
@data[:frames].inject(Hash.new) do |hash, (frame, info)|
# Convert all existing raw frames to use the new mapping via id2hash
# the array of raw frames contains a sequence of frame slices. Each
# frame slice is preceded by a count of the number of subsequent frames
# in the slice. Since the counts need not be normalized and we can
# identify frames by looking for a mapping in id2hash, add the
# normalized frame value when it is available, and just copy the
# existing value otherwise
raw_frames = @data[:raw].map {|v| id2hash[v.to_s] || v } if @data[:raw]
# return both the normalized frames and the normalized raw frames
return @data[:frames].inject(Hash.new) do |hash, (frame, info)|
info = hash[id2hash[frame.to_s]] = info.dup
info[:edges] = info[:edges].inject(Hash.new){ |edges, (edge, weight)| edges[id2hash[edge.to_s]] = weight; edges } if info[:edges]
hash
end
end, raw_frames || []
end

def version
Expand Down Expand Up @@ -362,7 +380,9 @@ def +(other)
raise ArgumentError, "cannot combine #{modeline} with #{other.modeline}" unless modeline == other.modeline
raise ArgumentError, "cannot combine v#{version} with v#{other.version}" unless version == other.version

f1, f2 = normalized_frames, other.normalized_frames
# collect the normalized and the normalized raw frames
f1, raw_frames1 = normalized_frames
f2, raw_frames2 = other.normalized_frames
frames = (f1.keys + f2.keys).uniq.inject(Hash.new) do |hash, id|
if f1[id].nil?
hash[id] = f2[id]
Expand Down Expand Up @@ -390,14 +410,17 @@ def +(other)
end

d1, d2 = data, other.data
# ensure that the new data contains the normalized frames
# and the normalized raw frames also
data = {
version: version,
mode: d1[:mode],
interval: d1[:interval],
samples: d1[:samples] + d2[:samples],
gc_samples: d1[:gc_samples] + d2[:gc_samples],
missed_samples: d1[:missed_samples] + d2[:missed_samples],
frames: frames
frames: frames,
raw: raw_frames1 + raw_frames2
}

self.class.new(data)
Expand Down
15 changes: 15 additions & 0 deletions test/test_middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,19 @@ def test_raw
StackProf::Middleware.new(Object.new, raw: true)
assert StackProf::Middleware.raw
end

def test_enabled_should_override_mode_if_a_proc
proc_called = false
middleware = StackProf::Middleware.new(proc {|env| proc_called = true}, enabled: Proc.new{ [true, 'foo'] })
env = Hash.new { true }
enabled, mode = StackProf::Middleware.enabled?(env)
assert enabled
assert_equal 'foo', mode

StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval, raw: false})
StackProf.expects(:stop)

middleware.call(env)
assert proc_called
end
end
8 changes: 8 additions & 0 deletions test/test_stackprof.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ def test_object_allocation_interval
assert_equal 10, profile[:samples]
end

def test_object_allocation_missed_samples
profile = StackProf.run(mode: :object, interval: 100) do
1000.times { Object.new }
end
assert_equal 10, profile[:samples]
assert_equal 0, profile[:missed_samples]
end

def test_cputime
profile = StackProf.run(mode: :cpu, interval: 500) do
math
Expand Down
Loading