rperf 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +15 -6
- data/docs/help.md +179 -10
- data/exe/rperf +247 -53
- data/ext/rperf/rperf.c +96 -43
- data/lib/rperf/meta.rb +343 -0
- data/lib/rperf/rack.rb +7 -2
- data/lib/rperf/table.rb +156 -0
- data/lib/rperf/version.rb +1 -1
- data/lib/rperf/viewer/viewer.html +1148 -0
- data/lib/rperf/viewer.rb +101 -653
- data/lib/rperf.rb +208 -69
- metadata +4 -1
data/lib/rperf/table.rb
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Flat table output for AI / machine consumption (`--format table` /
|
|
2
|
+
# `--format table-json`). Aggregation, diffing, and cutoff all happen here so
|
|
3
|
+
# consumers (LLMs, scripts) get a flat result table instead of a sample tree.
|
|
4
|
+
#
|
|
5
|
+
# TSV: header row, data rows, then a trailing "# summary" line of
|
|
6
|
+
# tab-separated key=value pairs.
|
|
7
|
+
# JSON: an array of row objects; the last element is { "summary": { ... } }.
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
|
|
11
|
+
module Rperf
|
|
12
|
+
module Table
|
|
13
|
+
ROWS_LIMIT = 50
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# --- single-profile report ---
|
|
18
|
+
|
|
19
|
+
# Rows: method, self_pct, total_pct, self_ms — self_pct descending,
|
|
20
|
+
# top 50 plus an "(other)" row aggregating the rest.
|
|
21
|
+
def report_rows(data, limit: ROWS_LIMIT)
|
|
22
|
+
flat, cum, total = flat_cum_by_name(data)
|
|
23
|
+
entries = flat.sort_by { |name, w| [-w, name] }
|
|
24
|
+
rows = entries.first(limit).map do |name, w|
|
|
25
|
+
{
|
|
26
|
+
method: name,
|
|
27
|
+
self_pct: pct(w, total),
|
|
28
|
+
total_pct: pct(cum[name], total),
|
|
29
|
+
self_ms: ms(w),
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
if entries.size > limit
|
|
33
|
+
# Aggregate the raw weights, not the already-rounded row values:
|
|
34
|
+
# per-row rounding errors are systematic and would make the (other)
|
|
35
|
+
# row inconsistent with itself (pct vs ms) and with the true total
|
|
36
|
+
rest_weight = entries.drop(limit).sum { |_, w| w }
|
|
37
|
+
rows << {
|
|
38
|
+
method: "(other)",
|
|
39
|
+
self_pct: pct(rest_weight, total),
|
|
40
|
+
total_pct: nil, # overlapping cumulative values cannot be summed
|
|
41
|
+
self_ms: ms(rest_weight),
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
rows
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def report_summary(data)
|
|
48
|
+
s = data[:summary] || Meta.build_summary(data)
|
|
49
|
+
out = {}
|
|
50
|
+
out[:total_ms] = s[:total_ms] if s[:total_ms]
|
|
51
|
+
out[:cpu_ms] = s[:cpu_ms] if s[:cpu_ms]
|
|
52
|
+
out[:allocated_objects] = s[:allocated_objects] if s[:allocated_objects]
|
|
53
|
+
out[:gc_count_minor] = s[:gc_count_minor] if s[:gc_count_minor]
|
|
54
|
+
out[:gc_count_major] = s[:gc_count_major] if s[:gc_count_major]
|
|
55
|
+
out
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def report_tsv(data)
|
|
59
|
+
rows = report_rows(data)
|
|
60
|
+
out = String.new
|
|
61
|
+
out << "method\tself_pct\ttotal_pct\tself_ms\n"
|
|
62
|
+
rows.each do |r|
|
|
63
|
+
out << [tsv_cell(r[:method]), r[:self_pct], r[:total_pct], r[:self_ms]].map { |v| v.nil? ? "" : v.to_s }.join("\t") << "\n"
|
|
64
|
+
end
|
|
65
|
+
out << summary_line(report_summary(data))
|
|
66
|
+
out
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def report_json(data)
|
|
70
|
+
JSON.generate(report_rows(data) + [{ summary: report_summary(data) }])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# --- diff (head - base) ---
|
|
74
|
+
|
|
75
|
+
# Rows: method, self_pct_base, self_pct_head, delta_pt — |delta_pt|
|
|
76
|
+
# descending, top 50. Per-method allocation data does not exist in
|
|
77
|
+
# rperf profiles, so allocation appears only in the summary.
|
|
78
|
+
def diff_rows(base, head, limit: ROWS_LIMIT)
|
|
79
|
+
base_flat, _, base_total = flat_cum_by_name(base)
|
|
80
|
+
head_flat, _, head_total = flat_cum_by_name(head)
|
|
81
|
+
names = (base_flat.keys | head_flat.keys)
|
|
82
|
+
rows = names.map do |name|
|
|
83
|
+
b = pct(base_flat[name] || 0, base_total)
|
|
84
|
+
h = pct(head_flat[name] || 0, head_total)
|
|
85
|
+
{
|
|
86
|
+
method: name,
|
|
87
|
+
self_pct_base: b,
|
|
88
|
+
self_pct_head: h,
|
|
89
|
+
delta_pt: (h - b).round(2),
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
rows.sort_by! { |r| [-r[:delta_pt].abs, r[:method]] }
|
|
93
|
+
rows.first(limit)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def diff_summary(base, head)
|
|
97
|
+
b = report_summary(base)
|
|
98
|
+
h = report_summary(head)
|
|
99
|
+
out = {}
|
|
100
|
+
%i[total_ms allocated_objects gc_count_minor gc_count_major].each do |key|
|
|
101
|
+
out[:"#{key}_base"] = b[key] if b[key]
|
|
102
|
+
out[:"#{key}_head"] = h[key] if h[key]
|
|
103
|
+
out[:"#{key}_delta"] = (h[key] - b[key]).round(1) if b[key] && h[key]
|
|
104
|
+
end
|
|
105
|
+
out
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def diff_tsv(base, head)
|
|
109
|
+
rows = diff_rows(base, head)
|
|
110
|
+
out = String.new
|
|
111
|
+
out << "method\tself_pct_base\tself_pct_head\tdelta_pt\n"
|
|
112
|
+
rows.each do |r|
|
|
113
|
+
out << [tsv_cell(r[:method]), r[:self_pct_base], r[:self_pct_head], r[:delta_pt]].join("\t") << "\n"
|
|
114
|
+
end
|
|
115
|
+
out << summary_line(diff_summary(base, head))
|
|
116
|
+
out
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def diff_json(base, head)
|
|
120
|
+
JSON.generate(diff_rows(base, head) + [{ summary: diff_summary(base, head) }])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# --- helpers ---
|
|
124
|
+
|
|
125
|
+
# Flat / cumulative weights merged by method name (compute_flat_cum keys
|
|
126
|
+
# are [label, path]; a method split across paths counts once).
|
|
127
|
+
def flat_cum_by_name(data)
|
|
128
|
+
result = Rperf.send(:compute_flat_cum, data[:aggregated_samples] || [])
|
|
129
|
+
flat = Hash.new(0)
|
|
130
|
+
result[:flat].each { |(label, _path), w| flat[label] += w }
|
|
131
|
+
cum = Hash.new(0)
|
|
132
|
+
result[:cum].each { |(label, _path), w| cum[label] += w }
|
|
133
|
+
[flat, cum, result[:total_weight]]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def pct(weight, total)
|
|
137
|
+
total > 0 ? (weight * 100.0 / total).round(2) : 0.0
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def ms(ns)
|
|
141
|
+
(ns / 1e6).round(1)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def summary_line(summary)
|
|
145
|
+
(["# summary"] + summary.map { |k, v| "#{k}=#{v}" }).join("\t") + "\n"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# TSV has no escaping convention — replace separator characters so a
|
|
149
|
+
# pathological method name (define_method allows any string) cannot
|
|
150
|
+
# shift columns or split a row.
|
|
151
|
+
def tsv_cell(value)
|
|
152
|
+
s = value.to_s
|
|
153
|
+
s.match?(/[\t\n\r]/) ? s.gsub(/[\t\n\r]/, " ") : s
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
data/lib/rperf/version.rb
CHANGED