Module: Rperf::Table

Defined in:
lib/rperf/table.rb

Constant Summary collapse

ROWS_LIMIT =
50

Class Method Summary collapse

Class Method Details

.diff_json(base, head) ⇒ Object



123
124
125
126
# File 'lib/rperf/table.rb', line 123

def diff_json(base, head)
  require "json"
  JSON.generate(diff_rows(base, head) + [{ summary: diff_summary(base, head) }])
end

.diff_rows(base, head, limit: ROWS_LIMIT) ⇒ Object

Rows: method, self_pct_base, self_pct_head, delta_pt — |delta_pt| descending, top 50. Per-method allocation data does not exist in rperf profiles, so allocation appears only in the summary.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rperf/table.rb', line 82

def diff_rows(base, head, limit: ROWS_LIMIT)
  base_flat, _, base_total = flat_cum_by_name(base)
  head_flat, _, head_total = flat_cum_by_name(head)
  names = (base_flat.keys | head_flat.keys)
  rows = names.map do |name|
    b = pct(base_flat[name] || 0, base_total)
    h = pct(head_flat[name] || 0, head_total)
    {
      method: name,
      self_pct_base: b,
      self_pct_head: h,
      delta_pt: (h - b).round(2),
    }
  end
  rows.sort_by! { |r| [-r[:delta_pt].abs, r[:method]] }
  rows.first(limit)
end

.diff_summary(base, head) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rperf/table.rb', line 100

def diff_summary(base, head)
  b = report_summary(base)
  h = report_summary(head)
  out = {}
  %i[total_ms allocated_objects gc_count_minor gc_count_major].each do |key|
    out[:"#{key}_base"] = b[key] if b[key]
    out[:"#{key}_head"] = h[key] if h[key]
    out[:"#{key}_delta"] = (h[key] - b[key]).round(1) if b[key] && h[key]
  end
  out
end

.diff_tsv(base, head) ⇒ Object



112
113
114
115
116
117
118
119
120
121
# File 'lib/rperf/table.rb', line 112

def diff_tsv(base, head)
  rows = diff_rows(base, head)
  out = String.new
  out << "method\tself_pct_base\tself_pct_head\tdelta_pt\n"
  rows.each do |r|
    out << [tsv_cell(r[:method]), r[:self_pct_base], r[:self_pct_head], r[:delta_pt]].join("\t") << "\n"
  end
  out << summary_line(diff_summary(base, head))
  out
end

.flat_cum_by_name(data) ⇒ Object

Flat / cumulative weights merged by method name (compute_flat_cum keys are [label, path]; a method split across paths counts once).



132
133
134
135
136
137
138
139
# File 'lib/rperf/table.rb', line 132

def flat_cum_by_name(data)
  result = Rperf.send(:compute_flat_cum, data[:aggregated_samples] || [])
  flat = Hash.new(0)
  result[:flat].each { |(label, _path), w| flat[label] += w }
  cum = Hash.new(0)
  result[:cum].each { |(label, _path), w| cum[label] += w }
  [flat, cum, result[:total_weight]]
end

.ms(ns) ⇒ Object



145
146
147
# File 'lib/rperf/table.rb', line 145

def ms(ns)
  (ns / 1e6).round(1)
end

.pct(weight, total) ⇒ Object



141
142
143
# File 'lib/rperf/table.rb', line 141

def pct(weight, total)
  total > 0 ? (weight * 100.0 / total).round(2) : 0.0
end

.report_json(data) ⇒ Object



72
73
74
75
# File 'lib/rperf/table.rb', line 72

def report_json(data)
  require "json"
  JSON.generate(report_rows(data) + [{ summary: report_summary(data) }])
end

.report_rows(data, limit: ROWS_LIMIT) ⇒ Object

Rows: method, self_pct, total_pct, self_ms — self_pct descending, top 50 plus an "(other)" row aggregating the rest.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/rperf/table.rb', line 24

def report_rows(data, limit: ROWS_LIMIT)
  flat, cum, total = flat_cum_by_name(data)
  entries = flat.sort_by { |name, w| [-w, name] }
  rows = entries.first(limit).map do |name, w|
    {
      method: name,
      self_pct: pct(w, total),
      total_pct: pct(cum[name], total),
      self_ms: ms(w),
    }
  end
  if entries.size > limit
    # Aggregate the raw weights, not the already-rounded row values:
    # per-row rounding errors are systematic and would make the (other)
    # row inconsistent with itself (pct vs ms) and with the true total
    rest_weight = entries.drop(limit).sum { |_, w| w }
    rows << {
      method: "(other)",
      self_pct: pct(rest_weight, total),
      total_pct: nil,  # overlapping cumulative values cannot be summed
      self_ms: ms(rest_weight),
    }
  end
  rows
end

.report_summary(data) ⇒ Object



50
51
52
53
54
55
56
57
58
59
# File 'lib/rperf/table.rb', line 50

def report_summary(data)
  s = data[:summary] || Meta.build_summary(data)
  out = {}
  out[:total_ms] = s[:total_ms] if s[:total_ms]
  out[:cpu_ms] = s[:cpu_ms] if s[:cpu_ms]
  out[:allocated_objects] = s[:allocated_objects] if s[:allocated_objects]
  out[:gc_count_minor] = s[:gc_count_minor] if s[:gc_count_minor]
  out[:gc_count_major] = s[:gc_count_major] if s[:gc_count_major]
  out
end

.report_tsv(data) ⇒ Object



61
62
63
64
65
66
67
68
69
70
# File 'lib/rperf/table.rb', line 61

def report_tsv(data)
  rows = report_rows(data)
  out = String.new
  out << "method\tself_pct\ttotal_pct\tself_ms\n"
  rows.each do |r|
    out << [tsv_cell(r[:method]), r[:self_pct], r[:total_pct], r[:self_ms]].map { |v| v.nil? ? "" : v.to_s }.join("\t") << "\n"
  end
  out << summary_line(report_summary(data))
  out
end

.summary_line(summary) ⇒ Object



149
150
151
# File 'lib/rperf/table.rb', line 149

def summary_line(summary)
  (["# summary"] + summary.map { |k, v| "#{k}=#{v}" }).join("\t") + "\n"
end

.tsv_cell(value) ⇒ Object

TSV has no escaping convention — replace separator characters so a pathological method name (define_method allows any string) cannot shift columns or split a row.



156
157
158
159
# File 'lib/rperf/table.rb', line 156

def tsv_cell(value)
  s = value.to_s
  s.match?(/[\t\n\r]/) ? s.gsub(/[\t\n\r]/, " ") : s
end