Объединение дерева выборок именованных профилей в нисходящее суммирование

У меня есть массив массивов имен, которые могут быть представлены в 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, как показано выше, а скорее древовидная структура, которая представляет это.)

Что такое простой и эффективный код, выдающий этот результат?
У меня есть собственное решение, которое я опубликую в качестве ответа, но оно кажется мне далеким от идеального.

Изменить: я забыл указать тот факт, что дерево должно быть отсортировано по убыванию на каждом уровне. Я добавил примеры узлов и изменил вывод, чтобы отразить это.


person Phrogz    schedule 31.05.2011    source источник
comment
Это Ruby 1.8.7, 1.9.2, 1.something_else? Спасибо   -  person robertodecurnex    schedule 31.05.2011
comment
@NeX Простите, что не уточнил. 1.9.2 нормально.   -  person Phrogz    schedule 31.05.2011


Ответы (6)


arrow_upward
3
arrow_downward

[edit] Рекурсивный класс Tree с функциональным программированием (для простоты я использую ostruct):

require 'ostruct'

class Tree < OpenStruct
  def self.new_from_array(plain)
    Tree.new(:node => "root", :count => 0, :children => children_from_array(plain))
  end

  def self.children_from_array(plain)
    plain.group_by(&:first).map do |node, group|
      terminal, leaves = group.map { |xs| xs.drop(1) }.partition(&:empty?)
      Tree.new(:node => node, :count => terminal.size, :children => children_from_array(leaves))
    end.sort_by(&:count).reverse
  end

  def inspect(indent=0)
    node_info = " "*indent + "#{self.node}: #{self.count}"
    ([node_info] + self.children.map { |tree| tree.inspect(indent+2) }).join("\n")
  end
end

Пример:

>> Tree.new_from_array(samples)
=>
root: 0
  a: 4
    e: 4
    c: 2
      d: 2
  b: 3
    c: 0
      e: 2

Вы можете настроить inspect в соответствии с вашими потребностями в визуализации.

person tokland    schedule 31.05.2011

arrow_upward
2
arrow_downward

Это поможет вам начать?

>> samples.each_with_object(Hash.new(0)) { |arr, h| h[arr] += 1 }
#=> {["a"]=>4, ["a", "c"]=>2, ["b"]=>3, ["a", "c", "d"]=>2, ["b", "c", "e"]=>2}

Когда у вас есть счетчики для каждого массива, вы можете начать группировать их, используя group_by или что-то подобное.

Обратите внимание, что если вы используете 1.8, вам придется использовать inject вместо each_with_object:

>> samples.inject(Hash.new(0)) { |h, arr| h[arr] += 1 ; h}
#=> {["a"]=>4, ["a", "c"]=>2, ["b"]=>3, ["a", "c", "d"]=>2, ["b", "c", "e"]=>2}

Извините, я ухожу сейчас, так что у меня мало времени... Если этого недостаточно, напишите комментарий, и я постараюсь ответить позже.

person Michael Kohl    schedule 31.05.2011
comment
Я думаю, что группировки недостаточно. Для того чтобы сделать дерево нужно что-то посложнее. - person robertodecurnex; 31.05.2011
comment
Очень полезно, спасибо. Интересно, что samples.group_by{|o|o}.map{|x,y|[x,y.length]} примерно на 25% быстрее, чем Hash(0).tap{|h| samples.each{|c| h[c]+=1 } (где tap/each немного быстрее, чем each_with_object). - person Phrogz; 31.05.2011
comment
@NeX: я никогда не говорил, что это полное решение, это было просто для начала работы. - person Michael Kohl; 31.05.2011

arrow_upward
1
arrow_downward

Вот гораздо лучший ответ, чем мой первый, вдохновленный @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
person Phrogz    schedule 31.05.2011

arrow_upward
1
arrow_downward

Вот простой рекурсивный метод, который создаст дерево. Это может быть легко адаптировано на основе методов, которые вам понадобятся.

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))

Метод, который в настоящее время печатает дерево, можно легко изменить, чтобы вместо этого создать объект дерева.

person robertodecurnex    schedule 31.05.2011

arrow_upward
1
arrow_downward

Чтобы попытаться ответить на вопрос, нельзя ли просто построить дерево, вставляя каждый образец по одному? и сортировать их по ходу или в конце?

Если вы не возражаете против некоторых дополнительных предложений, проиллюстрированных этим примером :

Я знаю, что это больше, чем вы просили, но тот факт, что вы берете образцы стека и ищете способ их обобщить, означает, что вы действительно на правильном пути, IMO. Удачи.


P.S. Разве вы не хотите хранить инклюзивные счетчики на каждом узле, например:

root:17
  a:12
    e:4
    c:4
      d:2
  b:5
    c:2
      e:2

потому что это скажет вам стоимость (в смысле потенциальной экономии) каждого узла. Если вас беспокоит «эксклюзивное время», обратите внимание на пункт 8 этого поста.

person Mike Dunlavey    schedule 31.05.2011

arrow_upward
0
arrow_downward

Вот мое далеко не идеальное решение. В свою защиту скажу, что он был создан с изменяющимся вводом, поэтому сначала он преобразуется в структуру данных, которая была изначально введена:

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)
person Phrogz    schedule 31.05.2011