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/exe/rperf
CHANGED
|
@@ -10,12 +10,102 @@ def find_available_port
|
|
|
10
10
|
port
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# Whether a GUI browser can be opened. Without a display, xdg-open /
|
|
14
|
+
# sensible-browser fall back to terminal browsers (w3m etc.), which take
|
|
15
|
+
# over the TTY and cannot run the viewer's JavaScript anyway.
|
|
16
|
+
def can_open_browser?
|
|
17
|
+
case RUBY_PLATFORM
|
|
18
|
+
when /darwin/ then true
|
|
19
|
+
when /linux/ then !!(ENV["DISPLAY"] || ENV["WAYLAND_DISPLAY"])
|
|
20
|
+
else false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Serve a Rperf::Viewer app and open the browser (only with a GUI and a
|
|
25
|
+
# local bind). Does not return (runs the Rack handler until interrupted).
|
|
26
|
+
def serve_viewer(app, port: nil, host: nil)
|
|
27
|
+
# Find an available Rack handler first, before claiming to serve
|
|
28
|
+
begin
|
|
29
|
+
require "rackup/handler"
|
|
30
|
+
rescue LoadError
|
|
31
|
+
$stderr.puts "Error: 'rackup' gem is required for 'rperf report'. Install it with:"
|
|
32
|
+
$stderr.puts " gem install rackup"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if port
|
|
37
|
+
# Fail fast on a taken port — otherwise the readiness probe below would
|
|
38
|
+
# "succeed" against the foreign server and open the browser on it, and
|
|
39
|
+
# the handler would die with a raw EADDRINUSE backtrace
|
|
40
|
+
begin
|
|
41
|
+
TCPServer.open(host || "localhost", port) {}
|
|
42
|
+
rescue Errno::EADDRINUSE
|
|
43
|
+
$stderr.puts "Error: port #{port} is already in use (choose another --port)"
|
|
44
|
+
exit 1
|
|
45
|
+
rescue SystemCallError
|
|
46
|
+
# unbindable host etc. — let the handler report it
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
port ||= find_available_port
|
|
50
|
+
host ||= "localhost"
|
|
51
|
+
local = %w[localhost 127.0.0.1 ::1].include?(host)
|
|
52
|
+
# 0.0.0.0 binds all interfaces; show a URL that works from outside
|
|
53
|
+
display_host = (host == "0.0.0.0") ? Socket.gethostname : host
|
|
54
|
+
url = "http://#{display_host}:#{port}/"
|
|
55
|
+
$stderr.puts "Serving rperf viewer at #{url} (bound to #{host})"
|
|
56
|
+
unless local
|
|
57
|
+
$stderr.puts "WARNING: the viewer has no authentication; anyone who can reach #{host}:#{port} can read the profile."
|
|
58
|
+
end
|
|
59
|
+
auto_open = local && can_open_browser?
|
|
60
|
+
$stderr.puts "Open the URL in a browser (not auto-opening)." unless auto_open
|
|
61
|
+
$stderr.puts "Press Ctrl-C to stop."
|
|
62
|
+
|
|
63
|
+
# Open browser after server is ready
|
|
64
|
+
if auto_open
|
|
65
|
+
Thread.new do
|
|
66
|
+
20.times do
|
|
67
|
+
TCPSocket.new(host, port).close
|
|
68
|
+
break
|
|
69
|
+
rescue Errno::ECONNREFUSED
|
|
70
|
+
sleep 0.1
|
|
71
|
+
end
|
|
72
|
+
case RUBY_PLATFORM
|
|
73
|
+
when /darwin/ then system("open", url)
|
|
74
|
+
when /linux/ then system("xdg-open", url, out: File::NULL, err: File::NULL) || system("sensible-browser", url, out: File::NULL, err: File::NULL)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
handler = Rackup::Handler.default
|
|
80
|
+
begin
|
|
81
|
+
handler.run(app, Port: port, Host: host, Silent: true)
|
|
82
|
+
rescue Errno::EADDRINUSE
|
|
83
|
+
$stderr.puts "Error: port #{port} is already in use (choose another --port)"
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def run_pprof_subcommand(banner, min_files:)
|
|
14
89
|
mode = :http
|
|
90
|
+
port = nil
|
|
91
|
+
host = nil
|
|
15
92
|
|
|
16
93
|
parser = OptionParser.new do |opts|
|
|
17
94
|
opts.banner = banner
|
|
18
95
|
|
|
96
|
+
opts.on("--port PORT", Integer, "Port for the web UI (default: auto). Useful for SSH port forwarding") do |v|
|
|
97
|
+
unless v.between?(1, 65535)
|
|
98
|
+
$stderr.puts "Error: --port must be 1..65535, got #{v}"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
port = v
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
opts.on("--host HOST", "Bind address for the web UI (default: localhost). " \
|
|
105
|
+
"Use 0.0.0.0 to allow external access — the viewer has NO authentication") do |v|
|
|
106
|
+
host = v
|
|
107
|
+
end
|
|
108
|
+
|
|
19
109
|
opts.on("--top", "Print top functions by #{min_files > 1 ? 'diff' : 'flat time'}") do
|
|
20
110
|
mode = :top
|
|
21
111
|
end
|
|
@@ -24,6 +114,12 @@ def run_pprof_subcommand(name, banner, min_files:)
|
|
|
24
114
|
mode = :text
|
|
25
115
|
end
|
|
26
116
|
|
|
117
|
+
opts.on("--format FORMAT", %w[table table-json],
|
|
118
|
+
"Flat #{min_files > 1 ? 'diff ' : ''}table for AI/machine consumption: " \
|
|
119
|
+
"'table' (TSV) or 'table-json' (JSON)") do |v|
|
|
120
|
+
mode = (v == "table") ? :table : :table_json
|
|
121
|
+
end
|
|
122
|
+
|
|
27
123
|
if min_files == 1
|
|
28
124
|
opts.on("--html", "Output static HTML viewer to stdout") do
|
|
29
125
|
mode = :html
|
|
@@ -38,27 +134,29 @@ def run_pprof_subcommand(name, banner, min_files:)
|
|
|
38
134
|
|
|
39
135
|
begin
|
|
40
136
|
parser.order!(ARGV)
|
|
41
|
-
rescue OptionParser::
|
|
137
|
+
rescue OptionParser::ParseError => e
|
|
42
138
|
$stderr.puts e.message
|
|
43
139
|
$stderr.puts parser
|
|
44
140
|
exit 1
|
|
45
141
|
end
|
|
46
142
|
|
|
47
|
-
|
|
48
|
-
msg = min_files > 1 ? "Two profile files required." : "No profile file specified."
|
|
49
|
-
$stderr.puts msg if min_files > 1
|
|
50
|
-
# For report, use default file
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
files = ARGV.shift(min_files > 1 ? [ARGV.size, min_files].min : 1)
|
|
143
|
+
files = ARGV.shift(min_files)
|
|
54
144
|
files = ["rperf.json.gz"] if files.empty? && min_files == 1
|
|
55
145
|
|
|
56
|
-
if
|
|
146
|
+
if files.size < min_files
|
|
57
147
|
$stderr.puts "Two profile files required."
|
|
58
148
|
$stderr.puts parser
|
|
59
149
|
exit 1
|
|
60
150
|
end
|
|
61
151
|
|
|
152
|
+
# order! stops at the first non-option — anything left over (e.g. options
|
|
153
|
+
# placed after the file argument) would be silently ignored
|
|
154
|
+
unless ARGV.empty?
|
|
155
|
+
$stderr.puts "Unexpected extra arguments: #{ARGV.join(' ')} (options must come before the file arguments)"
|
|
156
|
+
$stderr.puts parser
|
|
157
|
+
exit 1
|
|
158
|
+
end
|
|
159
|
+
|
|
62
160
|
files.each do |f|
|
|
63
161
|
unless File.exist?(f)
|
|
64
162
|
$stderr.puts "File not found: #{f}"
|
|
@@ -67,11 +165,11 @@ def run_pprof_subcommand(name, banner, min_files:)
|
|
|
67
165
|
end
|
|
68
166
|
|
|
69
167
|
# Go check is deferred — only needed for pprof files, not marshal/json
|
|
70
|
-
yield mode, files
|
|
168
|
+
yield mode, files, port, host
|
|
71
169
|
end
|
|
72
170
|
|
|
73
171
|
def self.help_text
|
|
74
|
-
|
|
172
|
+
File.read(File.expand_path("../docs/help.md", __dir__))
|
|
75
173
|
end
|
|
76
174
|
|
|
77
175
|
USAGE = "Usage: rperf record [options] command [args...]\n" \
|
|
@@ -101,15 +199,36 @@ when "help"
|
|
|
101
199
|
puts help_text
|
|
102
200
|
exit
|
|
103
201
|
when "report"
|
|
104
|
-
run_pprof_subcommand(
|
|
202
|
+
run_pprof_subcommand(
|
|
105
203
|
"Usage: rperf report [options] [file]\n" \
|
|
106
204
|
" Opens profile in browser (default) or prints summary.\n" \
|
|
107
205
|
" Default file: rperf.json.gz\n" \
|
|
108
206
|
" For .json.gz: opens rperf viewer (no Go required).\n" \
|
|
109
207
|
" For .pb.gz: opens go tool pprof (requires Go).",
|
|
110
|
-
min_files: 1) do |mode, files|
|
|
208
|
+
min_files: 1) do |mode, files, port, host|
|
|
111
209
|
report_file = files[0]
|
|
112
|
-
if report_file
|
|
210
|
+
if File.directory?(report_file)
|
|
211
|
+
# Time-travel mode: list all profiles in the directory (meta/summary
|
|
212
|
+
# only — bodies are lazy-loaded on selection)
|
|
213
|
+
unless mode == :http
|
|
214
|
+
$stderr.puts "Directory input opens the time-travel viewer; --top/--text/--html/--format are not supported"
|
|
215
|
+
exit 1
|
|
216
|
+
end
|
|
217
|
+
require_relative "../lib/rperf"
|
|
218
|
+
require_relative "../lib/rperf/viewer"
|
|
219
|
+
app = Rperf::Viewer.new(
|
|
220
|
+
proc { |_| [404, { "content-type" => "text/plain" }, ["Not Found"]] },
|
|
221
|
+
path: ""
|
|
222
|
+
)
|
|
223
|
+
count = app.add_snapshot_dir(report_file)
|
|
224
|
+
if count == 0
|
|
225
|
+
$stderr.puts "No .json(.gz) profiles found in #{report_file}"
|
|
226
|
+
exit 1
|
|
227
|
+
end
|
|
228
|
+
$stderr.puts "Loaded #{count} snapshot#{count == 1 ? '' : 's'} from #{report_file}"
|
|
229
|
+
serve_viewer(app, port: port, host: host)
|
|
230
|
+
exit
|
|
231
|
+
elsif report_file =~ /\.json(\.gz)?\z/
|
|
113
232
|
# Use rperf viewer
|
|
114
233
|
require_relative "../lib/rperf"
|
|
115
234
|
require_relative "../lib/rperf/viewer"
|
|
@@ -123,49 +242,33 @@ when "report"
|
|
|
123
242
|
when :text
|
|
124
243
|
$stdout.puts Rperf::Text.encode(data)
|
|
125
244
|
exit
|
|
245
|
+
when :table
|
|
246
|
+
$stdout.print Rperf::Table.report_tsv(data)
|
|
247
|
+
exit
|
|
248
|
+
when :table_json
|
|
249
|
+
$stdout.puts Rperf::Table.report_json(data)
|
|
250
|
+
exit
|
|
126
251
|
when :html
|
|
127
252
|
$stdout.puts Rperf::Viewer.render_static_html(data)
|
|
128
253
|
exit
|
|
129
254
|
end
|
|
130
255
|
|
|
131
|
-
port = find_available_port
|
|
132
|
-
|
|
133
256
|
app = Rperf::Viewer.new(
|
|
134
257
|
proc { |_| [404, { "content-type" => "text/plain" }, ["Not Found"]] },
|
|
135
258
|
path: ""
|
|
136
259
|
)
|
|
137
260
|
app.add_snapshot(data)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Thread.new do
|
|
145
|
-
20.times do
|
|
146
|
-
TCPSocket.new("localhost", port).close
|
|
147
|
-
break
|
|
148
|
-
rescue Errno::ECONNREFUSED
|
|
149
|
-
sleep 0.1
|
|
150
|
-
end
|
|
151
|
-
case RUBY_PLATFORM
|
|
152
|
-
when /darwin/ then system("open", url)
|
|
153
|
-
when /linux/ then system("xdg-open", url, err: File::NULL) || system("sensible-browser", url, err: File::NULL)
|
|
154
|
-
end
|
|
261
|
+
serve_viewer(app, port: port, host: host)
|
|
262
|
+
exit
|
|
263
|
+
else
|
|
264
|
+
if mode == :table || mode == :table_json
|
|
265
|
+
$stderr.puts "--format table requires a .json.gz / .json profile (got: #{report_file})"
|
|
266
|
+
exit 1
|
|
155
267
|
end
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
begin
|
|
159
|
-
require "rackup/handler"
|
|
160
|
-
rescue LoadError
|
|
161
|
-
$stderr.puts "Error: 'rackup' gem is required for 'rperf report'. Install it with:"
|
|
162
|
-
$stderr.puts " gem install rackup"
|
|
268
|
+
if mode == :html
|
|
269
|
+
$stderr.puts "--html requires a .json.gz / .json profile (got: #{report_file})"
|
|
163
270
|
exit 1
|
|
164
271
|
end
|
|
165
|
-
handler = Rackup::Handler.default
|
|
166
|
-
handler.run(app, Port: port, Host: "localhost", Silent: true)
|
|
167
|
-
exit
|
|
168
|
-
else
|
|
169
272
|
unless system("go", "version", out: File::NULL, err: File::NULL)
|
|
170
273
|
$stderr.puts "'go' command not found. Install Go to use pprof files, or use .json.gz format."
|
|
171
274
|
$stderr.puts " https://go.dev/dl/"
|
|
@@ -174,17 +277,38 @@ when "report"
|
|
|
174
277
|
case mode
|
|
175
278
|
when :top then exec("go", "tool", "pprof", "-top", report_file)
|
|
176
279
|
when :text then exec("go", "tool", "pprof", "-text", report_file)
|
|
177
|
-
else
|
|
280
|
+
else
|
|
281
|
+
# pprof has its own browser launcher — suppress it without a GUI
|
|
282
|
+
http_args = ["-http=#{host || %q(localhost)}:#{port || find_available_port}"]
|
|
283
|
+
http_args << "-no_browser" unless can_open_browser?
|
|
284
|
+
exec("go", "tool", "pprof", *http_args, report_file)
|
|
178
285
|
end
|
|
179
286
|
end
|
|
180
287
|
end
|
|
181
288
|
when "diff"
|
|
182
|
-
run_pprof_subcommand(
|
|
289
|
+
run_pprof_subcommand(
|
|
183
290
|
"Usage: rperf diff [options] base target\n" \
|
|
184
291
|
" Compare two profiles (shows target - base).\n" \
|
|
185
292
|
" Accepts .json.gz (auto-converted) or .pb.gz files.\n" \
|
|
186
|
-
" Requires Go (https://go.dev/dl/).",
|
|
187
|
-
min_files: 2) do |mode, files|
|
|
293
|
+
" Requires Go (https://go.dev/dl/), except --format table/table-json.",
|
|
294
|
+
min_files: 2) do |mode, files, port, host|
|
|
295
|
+
# Table diff is computed in Ruby — no Go required
|
|
296
|
+
if mode == :table || mode == :table_json
|
|
297
|
+
unless files.all? { |f| f =~ /\.json(\.gz)?\z/ }
|
|
298
|
+
$stderr.puts "--format table requires .json.gz / .json profiles"
|
|
299
|
+
exit 1
|
|
300
|
+
end
|
|
301
|
+
require_relative "../lib/rperf"
|
|
302
|
+
base = Rperf.load(files[0])
|
|
303
|
+
head = Rperf.load(files[1])
|
|
304
|
+
if mode == :table
|
|
305
|
+
$stdout.print Rperf::Table.diff_tsv(base, head)
|
|
306
|
+
else
|
|
307
|
+
$stdout.puts Rperf::Table.diff_json(base, head)
|
|
308
|
+
end
|
|
309
|
+
exit
|
|
310
|
+
end
|
|
311
|
+
|
|
188
312
|
unless system("go", "version", out: File::NULL, err: File::NULL)
|
|
189
313
|
$stderr.puts "'go' command not found. Install Go to use 'rperf diff'."
|
|
190
314
|
$stderr.puts " https://go.dev/dl/"
|
|
@@ -210,7 +334,10 @@ when "diff"
|
|
|
210
334
|
pprof_args = case mode
|
|
211
335
|
when :top then ["-top", "-diff_base=#{base_file}", target_file]
|
|
212
336
|
when :text then ["-text", "-diff_base=#{base_file}", target_file]
|
|
213
|
-
else
|
|
337
|
+
else
|
|
338
|
+
args = ["-http=#{host || %q(localhost)}:#{port || find_available_port}"]
|
|
339
|
+
args << "-no_browser" unless can_open_browser?
|
|
340
|
+
args + ["-diff_base=#{base_file}", target_file]
|
|
214
341
|
end
|
|
215
342
|
if tmpfiles.empty?
|
|
216
343
|
exec("go", "tool", "pprof", *pprof_args)
|
|
@@ -230,6 +357,7 @@ else
|
|
|
230
357
|
end
|
|
231
358
|
|
|
232
359
|
output = (subcommand == "record") ? "rperf.json.gz" : nil
|
|
360
|
+
output_given = false
|
|
233
361
|
frequency = 1000
|
|
234
362
|
mode = (subcommand == "record") ? "cpu" : "wall"
|
|
235
363
|
format = nil
|
|
@@ -238,6 +366,8 @@ verbose = false
|
|
|
238
366
|
aggregate = true
|
|
239
367
|
stat_report = (subcommand == "exec")
|
|
240
368
|
inherit = true
|
|
369
|
+
meta_labels = {}
|
|
370
|
+
snapshot_dir = nil
|
|
241
371
|
|
|
242
372
|
parser = OptionParser.new do |opts|
|
|
243
373
|
opts.banner = case subcommand
|
|
@@ -248,6 +378,7 @@ parser = OptionParser.new do |opts|
|
|
|
248
378
|
|
|
249
379
|
opts.on("-o", "--output PATH", "Output file#{subcommand == 'record' ? ' (default: rperf.json.gz)' : ' (default: none)'}") do |v|
|
|
250
380
|
output = v
|
|
381
|
+
output_given = true
|
|
251
382
|
end
|
|
252
383
|
|
|
253
384
|
opts.on("-f", "--frequency HZ", Integer, "Sampling frequency in Hz (default: 1000)") do |v|
|
|
@@ -268,7 +399,21 @@ parser = OptionParser.new do |opts|
|
|
|
268
399
|
opts.on("-p", "--print", "Print text profile to stdout (same as --format=text --output=/dev/stdout)") do
|
|
269
400
|
format = "text"
|
|
270
401
|
output = "/dev/stdout"
|
|
402
|
+
output_given = true
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
opts.on("--snapshot-dir DIR", "Save output as rperf-<sha7>-<timestamp>.json.gz in DIR") do |v|
|
|
406
|
+
snapshot_dir = v
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
opts.on("--label KEY=VALUE", "Add a label to profile metadata (repeatable)") do |v|
|
|
411
|
+
key, value = v.split("=", 2)
|
|
412
|
+
if key.nil? || key.empty? || value.nil?
|
|
413
|
+
$stderr.puts "Error: --label must be KEY=VALUE, got: #{v.inspect}"
|
|
414
|
+
exit 1
|
|
271
415
|
end
|
|
416
|
+
meta_labels[key] = value
|
|
272
417
|
end
|
|
273
418
|
|
|
274
419
|
opts.on("--signal VALUE", "Timer signal (Linux only): signal number, or 'false' for nanosleep thread") do |v|
|
|
@@ -310,7 +455,7 @@ end
|
|
|
310
455
|
|
|
311
456
|
begin
|
|
312
457
|
parser.order!(ARGV)
|
|
313
|
-
rescue OptionParser::
|
|
458
|
+
rescue OptionParser::ParseError => e
|
|
314
459
|
$stderr.puts e.message
|
|
315
460
|
$stderr.puts parser
|
|
316
461
|
exit 1
|
|
@@ -345,6 +490,47 @@ if signal && signal != "false"
|
|
|
345
490
|
end
|
|
346
491
|
end
|
|
347
492
|
|
|
493
|
+
# Load the library BEFORE exporting RPERF_ENABLED — requiring it afterwards
|
|
494
|
+
# would trigger the ENV-based auto-start in the CLI process itself.
|
|
495
|
+
require_relative "../lib/rperf"
|
|
496
|
+
require "json"
|
|
497
|
+
|
|
498
|
+
# Collect git metadata here, not in the profiled process: the app may chdir,
|
|
499
|
+
# which would point git at the wrong repository. The profiled process reads
|
|
500
|
+
# RPERF_META_GIT instead of running git ("null" = checked, not a repo).
|
|
501
|
+
git_info = Rperf::Meta.collect_git
|
|
502
|
+
ENV["RPERF_META_GIT"] = git_info ? JSON.generate(git_info) : "null"
|
|
503
|
+
ENV["RPERF_META_LABELS"] = JSON.generate(meta_labels) unless meta_labels.empty?
|
|
504
|
+
|
|
505
|
+
if snapshot_dir
|
|
506
|
+
if output_given
|
|
507
|
+
$stderr.puts "Error: --snapshot-dir and -o/--output (or -p) are mutually exclusive"
|
|
508
|
+
exit 1
|
|
509
|
+
end
|
|
510
|
+
if format && format != "json"
|
|
511
|
+
$stderr.puts "Error: --snapshot-dir requires JSON format (got --format=#{format})"
|
|
512
|
+
exit 1
|
|
513
|
+
end
|
|
514
|
+
FileUtils.mkdir_p(snapshot_dir)
|
|
515
|
+
output = File.join(snapshot_dir, Rperf::Meta.snapshot_filename(git_info))
|
|
516
|
+
# Timestamp resolution is one second and the in-git name has no pid —
|
|
517
|
+
# don't overwrite an earlier run from the same second on the same commit
|
|
518
|
+
output = output.sub(/\.json\.gz\z/, "-#{Process.pid}.json.gz") if File.exist?(output)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
if output
|
|
522
|
+
# Resolve now: the profiled app may chdir, which would silently relocate a
|
|
523
|
+
# relative output path. Validate writability up front, too — discovering an
|
|
524
|
+
# unwritable path in at_exit wastes the entire profiling run.
|
|
525
|
+
output = File.expand_path(output)
|
|
526
|
+
out_dir = File.dirname(output)
|
|
527
|
+
writable = File.exist?(output) ? File.writable?(output) : (File.directory?(out_dir) && File.writable?(out_dir))
|
|
528
|
+
unless writable
|
|
529
|
+
$stderr.puts "Error: output path is not writable: #{output}"
|
|
530
|
+
exit 1
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
348
534
|
# Add lib dir to RUBYLIB so -rrperf can find the correct version.
|
|
349
535
|
# RUBYLIB handles spaces in paths safely (PATH_SEPARATOR delimited).
|
|
350
536
|
# RUBYOPT -r<path> does not support spaces, so we use RUBYLIB + -rrperf.
|
|
@@ -367,8 +553,8 @@ if subcommand == "stat" || subcommand == "exec"
|
|
|
367
553
|
end
|
|
368
554
|
|
|
369
555
|
# Multi-process (fork) support: create a session directory for aggregation
|
|
556
|
+
session_dir = nil
|
|
370
557
|
if inherit
|
|
371
|
-
require_relative "../lib/rperf"
|
|
372
558
|
session_dir = Rperf.send(:_create_session_dir, clean_stale: true)
|
|
373
559
|
if session_dir
|
|
374
560
|
ENV["RPERF_ROOT_PROCESS"] = Process.pid.to_s
|
|
@@ -376,4 +562,12 @@ if inherit
|
|
|
376
562
|
end
|
|
377
563
|
end
|
|
378
564
|
|
|
379
|
-
|
|
565
|
+
begin
|
|
566
|
+
exec(*ARGV)
|
|
567
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
568
|
+
$stderr.puts "rperf: failed to execute #{ARGV.first.inspect}: #{e.message}"
|
|
569
|
+
# exec never ran — remove the eagerly created session dir instead of
|
|
570
|
+
# leaving it for the stale-PID sweep
|
|
571
|
+
FileUtils.rm_rf(session_dir) if session_dir
|
|
572
|
+
exit(e.is_a?(Errno::ENOENT) ? 127 : 126)
|
|
573
|
+
end
|