Объединение дерева выборок именованных профилей в нисходящее суммирование
Автор вопроса: PhrogzОтветов: 6
У меня есть массив массивов имен, которые могут быть представлены в Ruby следующим образом:
samples = [
%w[a],
%w[a c],
%w[a],
%w[a],
%w[b],
%w[b],
%w[a],
%w[a e],
%w[a e],
%w[a c d],
%w[a c d],
%w[b],
%w[b c e],
%w[b c e],
%w[a c],
%w[a e],
%w[a e]
]
Это выходные данные профилировщика выборки, где каждый список имен представляет собой стек вызовов для конкретной выборки. Я хочу отобразить их как нисходящее дерево именованных значений, где значение в каждом узле представляет собой сумму обращений к этому конкретному пути вызова.
Для приведенного выше примера ввода дерево вывода должно быть:
root:0
a:4
e:4
c:2
d:2
b:3
c:0
e:2
(Мне не нужен вывод ASCII, как показано выше, а скорее древовидная структура, которая представляет это.)
Что такое простой и эффективный код, выдающий этот результат? У меня есть собственное решение, которое я опубликую в качестве ответа, но оно кажется мне далеким от идеального.
Изменить: я забыл указать тот факт, что дерево должно быть отсортировано по убыванию на каждом уровне. Я добавил примеры узлов и изменил вывод, чтобы отразить это.
Это Ruby 1.8.7, 1.9.2, 1.something_else? Спасибо robertodecurnex 31.05.2011
@NeX Простите, что не уточнил. 1.9.2 нормально. Phrogz 31.05.2011
Извините, я ухожу сейчас, так что у меня мало времени... Если этого недостаточно, напишите комментарий, и я постараюсь ответить позже.
Ответ 3 · рейтинг 1 · Phrogz
Вот гораздо лучший ответ, чем мой первый, вдохновленный @MichaelKohl:
require 'pnode' # see below
root = PNode.new("root",0)
samples.group_by{ |o| o }.each do |callstack,instances|
n = root; last_index = callstack.length-1
callstack.each_with_index do |name,i|
n = n[name]
n.time += instances.length if i==last_index
end
end
root.sort!
puts root
# pnode.rb
class PNode
attr_accessor :name, :time, :parent, :kids
def initialize(name,time=0,parent=nil)
@name, @time, @parent = name, time, parent
@kids = []; @by_name = {}
end
def []( name )
kids << (@by_name[name] = self.class.new(name,0,self)) unless @by_name[name]
@by_name[name]
end
def sort!
@kids.each(&:sort!).sort_by!{ |n| [-n.time,n.name] }
self
end
def to_s(lv=0)
[ "#{'..'*lv}#{name}:#{time}", *kids.map{|k| k.to_s(lv+1) } ].join("\n")
end
end
Ответ 4 · рейтинг 1 · robertodecurnex
Вот простой рекурсивный метод, который создаст дерево. Это может быть легко адаптировано на основе методов, которые вам понадобятся.
def to_tree(data)
tree = data.inject([0,{}]) do |cont, element|
if element.empty?
cont[0] += 1
else
node = element.shift
cont[1][node] ||= []
cont[1][node] << element
end
cont
end
tree[1].map do |k, v|
tree[1][k] = to_tree(v)
end
[tree[0],tree[1].sort{|a,b| b[1][0] <=> a[1][0]}]
end
def p_tree(node_tree, node_name='root', node_level=0)
p "#{' '*node_level}#{node_name}:#{node_tree[0]}"
node_tree[1].each do |node|
p_tree(node[1], node[0], node_level + 1)
end
end
samples = [
%w[a],
%w[a c],
%w[a],
%w[a],
%w[b],
%w[b],
%w[a],
%w[a e],
%w[a e],
%w[a c d],
%w[a c d],
%w[b],
%w[b c e],
%w[b c e],
%w[a c],
%w[a e],
%w[a e]
]
p_tree(to_tree(samples))
Метод, который в настоящее время печатает дерево, можно легко изменить, чтобы вместо этого создать объект дерева.
Ответ 5 · рейтинг 1 · Mike Dunlavey
Чтобы попытаться ответить на вопрос, нельзя ли просто построить дерево, вставляя каждый образец по одному? и сортировать их по ходу или в конце?
Если вы не возражаете против некоторых дополнительных предложений, проиллюстрированных этим примером а> :
Если вы беспокоитесь об эффективности из-за того, что у вас есть миллионы образцов стека, если вы просто случайным образом используете около 100 из них, вы получите достаточно информации для поиска узких мест. И вот почему.
Проблема с построением дерева, корнем которого является основная программа, заключается в том, что на самом деле может не быть "горячего пути", даже если есть добросовестное "горячее узкое место", как показано в этом примере. Решение состоит в том, чтобы позволить пользователю выбрать любую процедуру в качестве "корня", например c в вашем примере, которая активна 6/17 времени.
Когда вы получаете каждый образец стека, вы получаете не только имя подпрограммы, но и номер строки. Если пользователя интересует "горячая" процедура (которая появляется во многих образцах), он захочет знать, где внутри процедуры было потрачено время. Эта информация содержится в этих номерах строк. Поэтому я предлагаю использовать линии, а не подпрограммы, в качестве основной единицы построения дерева.
Предположим, что есть рекурсия, поэтому одна и та же процедура/строка может появляться в образце более одного раза. Что вы делаете? Ну, независимо от того, сколько раз подпрограмма/строка появляется в сэмпле, это все равно только один семпл. Стоимость строки - это просто доля выборок, которые ее содержат, с рекурсией или без нее, потому что предположим, что вы берете выборки каждые 10 мс. Если бы эту подпрограмму/строку можно было сделать так, чтобы она не требовала времени (скажем, путем ее удаления или исключения), все образцы, содержащие ее, исчезли бы из общего количества, рекурсии или нет. Итак, если я создаю представление бабочки, ориентированное на одну строку, мне нужно показать только один слой вызовов под ним и один выше. Поэтому я строю локальное нисходящее дерево, показывающее только те строки, которые появились ниже фокуса (в образцах), а также для восходящего дерева, независимо от рекурсии.
По моему опыту, я часто делаю это вручную, и автоматизация могла бы помочь, но когда я хочу исследовать конкретную горячую линию, я хочу иметь возможность видеть отдельные образцы стека, проходящие через эту линию. Причина в том, что я не забочусь об измерении. Я знаю, что это жарко. Меня волнует, почему он делал то, что делал, поэтому я знаю, смогу ли я обойтись без него. Вот что говорят мне образцы стека. Простого взгляда на горячую линию недостаточно.
Я знаю, что это больше, чем вы просили, но тот факт, что вы берете образцы стека и ищете способ их обобщить, означает, что вы действительно на правильном пути, IMO. Удачи.
P.S. Разве вы не хотите хранить инклюзивные счетчики на каждом узле, например:
root:17
a:12
e:4
c:4
d:2
b:5
c:2
e:2
потому что это скажет вам стоимость (в смысле потенциальной экономии) каждого узла. Если вас беспокоит "эксклюзивное время", обратите внимание на пункт 8 этого поста.
Ответ 6 · Phrogz
Вот мое далеко не идеальное решение. В свою защиту скажу, что он был создан с изменяющимся вводом, поэтому сначала он преобразуется в структуру данных, которая была изначально введена:
PNode = Struct.new(:name,:time,:parent) do
attr_writer :kids
def kids; @kids||=[]; end
def add( name, time=0 )
self.class.new( name, time, self ).tap{ |n| kids << n }
end
def to_s(lv=0)
[ "#{'..'*lv}#{name}:#{time}", *kids.map{|k| k.to_s(lv+1) } ].join("\n")
end
end
module Enumerable
def sum(init=0,method=:to_i)
inject(init){ |sum,o| sum+o.send(method) }
end
end
# Build a tree of calls
root = PNode.new('root',0)
samples.each do |callstack|
n = root; last_index = callstack.length-1
callstack.each_with_index do |name,i|
n = n.add( name, i==last_index ? 1 : 0 )
end
end
# Sum the tree values at each level
def top_down(nodes,lv=0,parent=nil)
nodes.group_by(&:name).sort_by{ |name,ns|
[ -ns.map(&:time).sum, name ]
}.map{ |name,same_name_calls|
self_time = same_name_calls.map(&:time).sum
PNode.new(name,self_time,parent).tap do |x|
x.kids = top_down( same_name_calls.map(&:kids).flatten, lv+1, x )
end
}
end
puts top_down(root.kids).map(&:to_s)