From eccde8ae7c459cec80736edef173ef5bccdc511c Mon Sep 17 00:00:00 2001 From: Andrew Metcalf Date: Wed, 8 May 2019 17:02:55 -0700 Subject: [PATCH 1/3] Add a test for Report#+ --- test/test_report.rb | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/test_report.rb b/test/test_report.rb index 6ca1536f..fd959044 100644 --- a/test/test_report.rb +++ b/test/test_report.rb @@ -2,7 +2,7 @@ require 'stackprof' require 'minitest/autorun' -class ReportDumpTest < MiniTest::Test +class ReportTest < MiniTest::Test require 'stringio' def test_dump_to_stdout @@ -26,8 +26,37 @@ def test_dump_to_file assert_dump data, f.string end + def test_add + p1 = StackProf.run(mode: :cpu, raw: true) do + foo + end + p2 = StackProf.run(mode: :cpu, raw: true) do + foo + bar + end + + combined = StackProf::Report.new(p1) + StackProf::Report.new(p2) + + assert_equal 2, find_method_frame(combined, :foo)[:total_samples] + assert_equal 1, find_method_frame(combined, :bar)[:total_samples] + end + private + def find_method_frame(profile, name) + profile.frames.values.find do |frame| + frame[:name] == "ReportTest##{name}" + end + end + + def foo + StackProf.sample + end + + def bar + StackProf.sample + end + def assert_dump(expected, marshal_data) assert_equal expected, Marshal.load(marshal_data) end From 68fc7137de62d53d94d096a0314f770e2daaafcd Mon Sep 17 00:00:00 2001 From: Andrew Metcalf Date: Thu, 9 May 2019 06:33:15 -0700 Subject: [PATCH 2/3] Implement Report#merge for efficiently merging many profiles --- lib/stackprof/report.rb | 85 ++++++++++++++++++++++++----------------- test/test_report.rb | 79 +++++++++++++++++++++++++++++++++----- 2 files changed, 120 insertions(+), 44 deletions(-) diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index 523646b3..bd5684b5 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -358,49 +358,66 @@ def print_file(filter, f = STDOUT) end def +(other) - raise ArgumentError, "cannot combine #{other.class}" unless self.class == other.class - 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 - frames = (f1.keys + f2.keys).uniq.inject(Hash.new) do |hash, id| - if f1[id].nil? - hash[id] = f2[id] - elsif f2[id] - hash[id] = f1[id] - hash[id][:total_samples] += f2[id][:total_samples] - hash[id][:samples] += f2[id][:samples] - if f2[id][:edges] - edges = hash[id][:edges] ||= {} - f2[id][:edges].each do |edge, weight| - edges[edge] ||= 0 - edges[edge] += weight + merge(other) + end + + def merge(*others) + other_class = others.find {|o| o.class != self.class} + if other_class + raise ArgumentError, "cannot combine #{self.class} with #{other_class}" + end + + other_modeline = others.find {|o| o.modeline != modeline} + if other_modeline + raise ArgumentError, "cannot combine #{modeline} with #{other_modeline}" + end + + other_version = others.find {|o| o.version != version} + if other_version + raise ArgumentError, "cannot combine v#{version} with v#{other_version}" + end + + all_frames = [normalized_frames] + others.map(&:normalized_frames) + merged = {} + + all_frames.flat_map(&:keys).uniq.each do |id| + all_frames.each do |frame| + next unless frame[id] + if !merged[id] + merged[id] = frame[id] + else + merged[id][:total_samples] += frame[id][:total_samples] + merged[id][:samples] += frame[id][:samples] + if frame[id][:edges] + edges = merged[id][:edges] ||= {} + frame[id][:edges].each do |edge, weight| + edges[edge] ||= 0 + edges[edge] += weight + end end - end - if f2[id][:lines] - lines = hash[id][:lines] ||= {} - f2[id][:lines].each do |line, weight| - lines[line] = add_lines(lines[line], weight) + if frame[id][:lines] + lines = merged[id][:lines] ||= {} + frame[id][:lines].each do |line, weight| + lines[line] = add_lines(lines[line], weight) + end end end - else - hash[id] = f1[id] end - hash end - d1, d2 = data, other.data - data = { + all_data = [data] + others.map(&:data) + + new_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 + mode: data[:mode], + interval: data[:interval], + samples: all_data.map {|d| d[:samples]}.inject(:+), + gc_samples: all_data.map {|d| d[:gc_samples]}.inject(:+), + missed_samples: all_data.map {|d| d[:missed_samples]}.inject(:+), + frames: merged } - self.class.new(data) + self.class.new(new_data) end private diff --git a/test/test_report.rb b/test/test_report.rb index fd959044..c1b536cc 100644 --- a/test/test_report.rb +++ b/test/test_report.rb @@ -26,19 +26,68 @@ def test_dump_to_file assert_dump data, f.string end - def test_add - p1 = StackProf.run(mode: :cpu, raw: true) do - foo - end - p2 = StackProf.run(mode: :cpu, raw: true) do - foo - bar + def test_merge + data = [ + StackProf.run(mode: :cpu, raw: true) do + foo + end, + StackProf.run(mode: :cpu, raw: true) do + foo + bar + end, + StackProf.run(mode: :cpu, raw: true) do + foo + bar + baz + end + ] + expectations = { + foo: { + total_samples: 3, + samples: 3, + total_lines: 1, + }, + bar: { + total_samples: 4, + samples: 2, + total_lines: 2, + has_boz_edge: true + }, + baz: { + total_samples: 2, + samples: 1, + total_lines: 2, + has_boz_edge: true, + }, + boz: { + total_samples: 3, + samples: 3, + total_lines: 1, + }, + } + + reports = data.map {|d| StackProf::Report.new(d)} + combined = reports[0].merge(*reports[1..-1]) + + frames = expectations.keys.inject(Hash.new) do |hash, key| + hash[key] = find_method_frame(combined, key) + hash end - combined = StackProf::Report.new(p1) + StackProf::Report.new(p2) + expectations.each do |key, expect| + frame = frames[key] + assert_equal expect[:total_samples], frame[:total_samples], key + assert_equal expect[:samples], frame[:samples], key + + assert_equal expect[:total_lines], frame[:lines].length, key + assert_includes frame[:lines], frame[:line] + 1, key + assert_equal [expect[:samples], expect[:samples]], frame[:lines][frame[:line] + 1], key + + if expect[:has_boz_edge] + assert_equal ({frames[:boz][:hash] => expect[:samples]}), frame[:edges] + end + end - assert_equal 2, find_method_frame(combined, :foo)[:total_samples] - assert_equal 1, find_method_frame(combined, :bar)[:total_samples] end private @@ -55,6 +104,16 @@ def foo def bar StackProf.sample + boz + end + + def baz + StackProf.sample + boz + end + + def boz + StackProf.sample end def assert_dump(expected, marshal_data) From d3b8537a6fea0890603eb0f91c72e5b9ef58773f Mon Sep 17 00:00:00 2001 From: Andrew Metcalf Date: Tue, 14 May 2019 09:51:56 -0700 Subject: [PATCH 3/3] Simplify merge logic --- lib/stackprof/report.rb | 44 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index bd5684b5..18729256 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -377,35 +377,39 @@ def merge(*others) raise ArgumentError, "cannot combine v#{version} with v#{other_version}" end - all_frames = [normalized_frames] + others.map(&:normalized_frames) + all = [self] + others merged = {} - all_frames.flat_map(&:keys).uniq.each do |id| - all_frames.each do |frame| - next unless frame[id] + all.each do |report| + report.normalized_frames.each do |id, frame| if !merged[id] - merged[id] = frame[id] - else - merged[id][:total_samples] += frame[id][:total_samples] - merged[id][:samples] += frame[id][:samples] - if frame[id][:edges] - edges = merged[id][:edges] ||= {} - frame[id][:edges].each do |edge, weight| - edges[edge] ||= 0 - edges[edge] += weight - end + merged[id] = frame + next + end + + merged_frame = merged[id] + + merged_frame[:total_samples] += frame[:total_samples] + merged_frame[:samples] += frame[:samples] + + if frame[:edges] + merged_edges = (merged_frame[:edges] ||= {}) + frame[:edges].each do |edge, weight| + merged_edges[edge] ||= 0 + merged_edges[edge] += weight end - if frame[id][:lines] - lines = merged[id][:lines] ||= {} - frame[id][:lines].each do |line, weight| - lines[line] = add_lines(lines[line], weight) - end + end + + if frame[:lines] + merged_lines = (merged_frame[:lines] ||= {}) + frame[:lines].each do |line, weight| + merged_lines[line] = add_lines(merged_lines[line], weight) end end end end - all_data = [data] + others.map(&:data) + all_data = all.map(&:data) new_data = { version: version,