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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Rperf
2
- VERSION = "0.9.0"
2
+ VERSION = "0.10.0"
3
3
  end