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.
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.data"] if files.empty? && min_files == 1
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
- unless system("go", "version", out: File::NULL, err: File::NULL)
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 pprof profile in browser (default) or prints summary.\n" \
103
- " Default file: rperf.data",
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
- case mode
107
- when :top then exec("go", "tool", "pprof", "-top", report_file)
108
- when :text then exec("go", "tool", "pprof", "-text", report_file)
109
- else exec("go", "tool", "pprof", "-http=localhost:#{find_available_port}", report_file)
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.pb.gz target.pb.gz\n" \
115
- " Compare two pprof profiles (shows target - base).",
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
- when :top then exec("go", "tool", "pprof", "-top", "-diff_base=#{base_file}", target_file)
120
- when :text then exec("go", "tool", "pprof", "-text", "-diff_base=#{base_file}", target_file)
121
- else exec("go", "tool", "pprof", "-http=localhost:#{find_available_port}", "-diff_base=#{base_file}", target_file)
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.data" : nil
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.data)' : ' (default: none)'}") do |v|
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
- signal = (v == "false") ? "false" : v
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