rperf 0.7.0 → 0.8.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 +71 -47
- data/docs/help.md +154 -32
- data/docs/logo.svg +25 -0
- data/exe/rperf +121 -26
- data/ext/rperf/rperf.c +117 -89
- data/lib/rperf/rack.rb +25 -3
- data/lib/rperf/version.rb +1 -1
- data/lib/rperf/viewer.rb +798 -0
- data/lib/rperf.rb +166 -49
- metadata +6 -4
data/exe/rperf
CHANGED
|
@@ -31,7 +31,7 @@ def run_pprof_subcommand(name, banner, min_files:)
|
|
|
31
31
|
|
|
32
32
|
begin
|
|
33
33
|
parser.order!(ARGV)
|
|
34
|
-
rescue OptionParser::InvalidOption => e
|
|
34
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
35
35
|
$stderr.puts e.message
|
|
36
36
|
$stderr.puts parser
|
|
37
37
|
exit 1
|
|
@@ -44,7 +44,7 @@ def run_pprof_subcommand(name, banner, min_files:)
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
files = ARGV.shift(min_files > 1 ? [ARGV.size, min_files].min : 1)
|
|
47
|
-
files = ["rperf.
|
|
47
|
+
files = ["rperf.json.gz"] if files.empty? && min_files == 1
|
|
48
48
|
|
|
49
49
|
if min_files > 1 && files.size < min_files
|
|
50
50
|
$stderr.puts "Two profile files required."
|
|
@@ -59,12 +59,7 @@ def run_pprof_subcommand(name, banner, min_files:)
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
$stderr.puts "'go' command not found. Install Go to use 'rperf #{name}'."
|
|
64
|
-
$stderr.puts " https://go.dev/dl/"
|
|
65
|
-
exit 1
|
|
66
|
-
end
|
|
67
|
-
|
|
62
|
+
# Go check is deferred — only needed for pprof files, not marshal/json
|
|
68
63
|
yield mode, files
|
|
69
64
|
end
|
|
70
65
|
|
|
@@ -99,26 +94,119 @@ when "help"
|
|
|
99
94
|
when "report"
|
|
100
95
|
run_pprof_subcommand("report",
|
|
101
96
|
"Usage: rperf report [options] [file]\n" \
|
|
102
|
-
" Opens
|
|
103
|
-
" Default file: rperf.
|
|
97
|
+
" Opens profile in browser (default) or prints summary.\n" \
|
|
98
|
+
" Default file: rperf.json.gz\n" \
|
|
99
|
+
" For .json.gz: opens rperf viewer (no Go required).\n" \
|
|
100
|
+
" For .pb.gz: opens go tool pprof (requires Go).",
|
|
104
101
|
min_files: 1) do |mode, files|
|
|
105
102
|
report_file = files[0]
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
if report_file =~ /\.json(\.gz)?\z/
|
|
104
|
+
# Use rperf viewer
|
|
105
|
+
require_relative "../lib/rperf"
|
|
106
|
+
require_relative "../lib/rperf/viewer"
|
|
107
|
+
|
|
108
|
+
data = Rperf.load(report_file)
|
|
109
|
+
|
|
110
|
+
case mode
|
|
111
|
+
when :top
|
|
112
|
+
$stdout.puts Rperf::Text.encode(data, top_n: 20, header: false)
|
|
113
|
+
exit
|
|
114
|
+
when :text
|
|
115
|
+
$stdout.puts Rperf::Text.encode(data)
|
|
116
|
+
exit
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
port = find_available_port
|
|
120
|
+
|
|
121
|
+
app = Rperf::Viewer.new(
|
|
122
|
+
proc { |_| [404, { "content-type" => "text/plain" }, ["Not Found"]] },
|
|
123
|
+
path: ""
|
|
124
|
+
)
|
|
125
|
+
app.add_snapshot(data)
|
|
126
|
+
|
|
127
|
+
url = "http://localhost:#{port}/"
|
|
128
|
+
$stderr.puts "Serving rperf viewer at #{url}"
|
|
129
|
+
$stderr.puts "Press Ctrl-C to stop."
|
|
130
|
+
|
|
131
|
+
# Open browser after server is ready
|
|
132
|
+
Thread.new do
|
|
133
|
+
20.times do
|
|
134
|
+
TCPSocket.new("localhost", port).close
|
|
135
|
+
break
|
|
136
|
+
rescue Errno::ECONNREFUSED
|
|
137
|
+
sleep 0.1
|
|
138
|
+
end
|
|
139
|
+
case RUBY_PLATFORM
|
|
140
|
+
when /darwin/ then system("open", url)
|
|
141
|
+
when /linux/ then system("xdg-open", url, err: File::NULL) || system("sensible-browser", url, err: File::NULL)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Find an available Rack handler
|
|
146
|
+
begin
|
|
147
|
+
require "rackup/handler"
|
|
148
|
+
rescue LoadError
|
|
149
|
+
$stderr.puts "Error: 'rackup' gem is required for 'rperf report'. Install it with:"
|
|
150
|
+
$stderr.puts " gem install rackup"
|
|
151
|
+
exit 1
|
|
152
|
+
end
|
|
153
|
+
handler = Rackup::Handler.default
|
|
154
|
+
handler.run(app, Port: port, Host: "localhost", Silent: true)
|
|
155
|
+
exit
|
|
156
|
+
else
|
|
157
|
+
unless system("go", "version", out: File::NULL, err: File::NULL)
|
|
158
|
+
$stderr.puts "'go' command not found. Install Go to use pprof files, or use .json.gz format."
|
|
159
|
+
$stderr.puts " https://go.dev/dl/"
|
|
160
|
+
exit 1
|
|
161
|
+
end
|
|
162
|
+
case mode
|
|
163
|
+
when :top then exec("go", "tool", "pprof", "-top", report_file)
|
|
164
|
+
when :text then exec("go", "tool", "pprof", "-text", report_file)
|
|
165
|
+
else exec("go", "tool", "pprof", "-http=localhost:#{find_available_port}", report_file)
|
|
166
|
+
end
|
|
110
167
|
end
|
|
111
168
|
end
|
|
112
169
|
when "diff"
|
|
113
170
|
run_pprof_subcommand("diff",
|
|
114
|
-
"Usage: rperf diff [options] base
|
|
115
|
-
" Compare two
|
|
171
|
+
"Usage: rperf diff [options] base target\n" \
|
|
172
|
+
" Compare two profiles (shows target - base).\n" \
|
|
173
|
+
" Accepts .json.gz (auto-converted) or .pb.gz files.\n" \
|
|
174
|
+
" Requires Go (https://go.dev/dl/).",
|
|
116
175
|
min_files: 2) do |mode, files|
|
|
176
|
+
unless system("go", "version", out: File::NULL, err: File::NULL)
|
|
177
|
+
$stderr.puts "'go' command not found. Install Go to use 'rperf diff'."
|
|
178
|
+
$stderr.puts " https://go.dev/dl/"
|
|
179
|
+
exit 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
tmpfiles = []
|
|
183
|
+
files.each_with_index do |f, i|
|
|
184
|
+
if f =~ /\.json(\.gz)?\z/
|
|
185
|
+
require "tempfile"
|
|
186
|
+
require_relative "../lib/rperf"
|
|
187
|
+
data = Rperf.load(f)
|
|
188
|
+
tmp = Tempfile.new(["rperf_diff_#{i}", ".pb.gz"])
|
|
189
|
+
tmp.binmode
|
|
190
|
+
tmp.write(Rperf.__send__(:gzip, Rperf::PProf.encode(data)))
|
|
191
|
+
tmp.close
|
|
192
|
+
tmpfiles << tmp
|
|
193
|
+
files[i] = tmp.path
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
117
197
|
base_file, target_file = files
|
|
118
|
-
case mode
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
198
|
+
pprof_args = case mode
|
|
199
|
+
when :top then ["-top", "-diff_base=#{base_file}", target_file]
|
|
200
|
+
when :text then ["-text", "-diff_base=#{base_file}", target_file]
|
|
201
|
+
else ["-http=localhost:#{find_available_port}", "-diff_base=#{base_file}", target_file]
|
|
202
|
+
end
|
|
203
|
+
if tmpfiles.empty?
|
|
204
|
+
exec("go", "tool", "pprof", *pprof_args)
|
|
205
|
+
else
|
|
206
|
+
pid = spawn("go", "tool", "pprof", *pprof_args)
|
|
207
|
+
_, status = Process.wait2(pid)
|
|
208
|
+
tmpfiles.each(&:unlink)
|
|
209
|
+
exit(status.exitstatus || 1)
|
|
122
210
|
end
|
|
123
211
|
end
|
|
124
212
|
when "record", "stat", "exec"
|
|
@@ -129,7 +217,7 @@ else
|
|
|
129
217
|
exit 1
|
|
130
218
|
end
|
|
131
219
|
|
|
132
|
-
output = (subcommand == "record") ? "rperf.
|
|
220
|
+
output = (subcommand == "record") ? "rperf.json.gz" : nil
|
|
133
221
|
frequency = 1000
|
|
134
222
|
mode = (subcommand == "record") ? "cpu" : "wall"
|
|
135
223
|
format = nil
|
|
@@ -145,7 +233,7 @@ parser = OptionParser.new do |opts|
|
|
|
145
233
|
when "exec" then "Usage: rperf exec [options] command [args...]"
|
|
146
234
|
end
|
|
147
235
|
|
|
148
|
-
opts.on("-o", "--output PATH", "Output file#{subcommand == 'record' ? ' (default: rperf.
|
|
236
|
+
opts.on("-o", "--output PATH", "Output file#{subcommand == 'record' ? ' (default: rperf.json.gz)' : ' (default: none)'}") do |v|
|
|
149
237
|
output = v
|
|
150
238
|
end
|
|
151
239
|
|
|
@@ -159,8 +247,8 @@ parser = OptionParser.new do |opts|
|
|
|
159
247
|
end
|
|
160
248
|
|
|
161
249
|
if subcommand == "record"
|
|
162
|
-
opts.on("--format FORMAT", %w[pprof collapsed text],
|
|
163
|
-
"Output format: pprof, collapsed, or text (default: auto from extension)") do |v|
|
|
250
|
+
opts.on("--format FORMAT", %w[pprof collapsed text json],
|
|
251
|
+
"Output format: json, pprof, collapsed, or text (default: auto from extension)") do |v|
|
|
164
252
|
format = v
|
|
165
253
|
end
|
|
166
254
|
|
|
@@ -171,7 +259,14 @@ parser = OptionParser.new do |opts|
|
|
|
171
259
|
end
|
|
172
260
|
|
|
173
261
|
opts.on("--signal VALUE", "Timer signal (Linux only): signal number, or 'false' for nanosleep thread") do |v|
|
|
174
|
-
|
|
262
|
+
if v == "false"
|
|
263
|
+
signal = "false"
|
|
264
|
+
elsif v =~ /\A\d+\z/
|
|
265
|
+
signal = v
|
|
266
|
+
else
|
|
267
|
+
$stderr.puts "Error: --signal must be a signal number or 'false', got: #{v.inspect}"
|
|
268
|
+
exit 1
|
|
269
|
+
end
|
|
175
270
|
end
|
|
176
271
|
|
|
177
272
|
opts.on("--no-aggregate", "Disable sample aggregation (keep raw samples)") do
|
|
@@ -198,7 +293,7 @@ end
|
|
|
198
293
|
|
|
199
294
|
begin
|
|
200
295
|
parser.order!(ARGV)
|
|
201
|
-
rescue OptionParser::InvalidOption => e
|
|
296
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
202
297
|
$stderr.puts e.message
|
|
203
298
|
$stderr.puts parser
|
|
204
299
|
exit 1
|