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/lib/rperf.rb
CHANGED
|
@@ -19,15 +19,19 @@ module Rperf
|
|
|
19
19
|
@stat_start_mono = nil
|
|
20
20
|
|
|
21
21
|
# Starts profiling.
|
|
22
|
-
# format: :pprof, :collapsed, or :text. nil = auto-detect from output extension
|
|
22
|
+
# format: :json, :pprof, :collapsed, or :text. nil = auto-detect from output extension
|
|
23
|
+
# .json.gz → json (rperf native, default)
|
|
23
24
|
# .collapsed → collapsed stacks (FlameGraph / speedscope compatible)
|
|
24
25
|
# .txt → text report (human/AI readable flat + cumulative table)
|
|
25
|
-
#
|
|
26
|
+
# .pb.gz → pprof protobuf (gzip compressed)
|
|
26
27
|
def self.start(frequency: 1000, mode: :cpu, output: nil, verbose: false, format: nil, stat: false, signal: nil, aggregate: true, defer: false)
|
|
27
28
|
raise ArgumentError, "frequency must be a positive integer (got #{frequency.inspect})" unless frequency.is_a?(Integer) && frequency > 0
|
|
28
29
|
raise ArgumentError, "frequency must be <= 10000 (10KHz), got #{frequency}" if frequency > 10_000
|
|
29
30
|
raise ArgumentError, "mode must be :cpu or :wall, got #{mode.inspect}" unless %i[cpu wall].include?(mode)
|
|
30
31
|
c_mode = mode == :cpu ? 0 : 1
|
|
32
|
+
unless signal.nil? || signal == false || signal.is_a?(Integer)
|
|
33
|
+
raise ArgumentError, "signal must be nil, false, or an Integer, got #{signal.inspect}"
|
|
34
|
+
end
|
|
31
35
|
c_signal = signal.nil? ? -1 : (signal ? signal.to_i : 0)
|
|
32
36
|
if c_signal > 0
|
|
33
37
|
raise ArgumentError, "signal mode is only supported on Linux" unless RUBY_PLATFORM =~ /linux/
|
|
@@ -41,7 +45,10 @@ module Rperf
|
|
|
41
45
|
@output = output
|
|
42
46
|
@format = format
|
|
43
47
|
@stat = stat
|
|
44
|
-
|
|
48
|
+
if @stat
|
|
49
|
+
@stat_start_mono = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
50
|
+
@stat_start_times = Process.times
|
|
51
|
+
end
|
|
45
52
|
@label_set_table = nil
|
|
46
53
|
@label_set_index = nil
|
|
47
54
|
_c_start(frequency, c_mode, aggregate, c_signal, defer)
|
|
@@ -50,11 +57,21 @@ module Rperf
|
|
|
50
57
|
begin
|
|
51
58
|
yield
|
|
52
59
|
ensure
|
|
53
|
-
|
|
60
|
+
result = stop
|
|
54
61
|
end
|
|
62
|
+
result
|
|
55
63
|
end
|
|
56
64
|
end
|
|
57
65
|
|
|
66
|
+
# VM state integer → label value mapping.
|
|
67
|
+
# These values appear in the "Ruby" label key.
|
|
68
|
+
VM_STATE_LABELS = {
|
|
69
|
+
1 => ["%GVL", "blocked"],
|
|
70
|
+
2 => ["%GVL", "wait"],
|
|
71
|
+
3 => ["%GC", "mark"],
|
|
72
|
+
4 => ["%GC", "sweep"],
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
58
75
|
def self.stop
|
|
59
76
|
data = _c_stop
|
|
60
77
|
return unless data
|
|
@@ -63,17 +80,19 @@ module Rperf
|
|
|
63
80
|
# :aggregated_samples. Build aggregated view so encoders always work.
|
|
64
81
|
if data[:raw_samples] && !data[:aggregated_samples]
|
|
65
82
|
merged = {}
|
|
66
|
-
data[:raw_samples].each do |frames, weight, thread_seq, label_set_id|
|
|
67
|
-
key = [frames, thread_seq || 0, label_set_id || 0]
|
|
83
|
+
data[:raw_samples].each do |frames, weight, thread_seq, label_set_id, vm_state|
|
|
84
|
+
key = [frames, thread_seq || 0, label_set_id || 0, vm_state || 0]
|
|
68
85
|
if merged.key?(key)
|
|
69
86
|
merged[key] += weight
|
|
70
87
|
else
|
|
71
88
|
merged[key] = weight
|
|
72
89
|
end
|
|
73
90
|
end
|
|
74
|
-
data[:aggregated_samples] = merged.map { |(frames, ts, lsi), w| [frames, w, ts, lsi] }
|
|
91
|
+
data[:aggregated_samples] = merged.map { |(frames, ts, lsi, vs), w| [frames, w, ts, lsi, vs] }
|
|
75
92
|
end
|
|
76
93
|
|
|
94
|
+
merge_vm_state_labels!(data)
|
|
95
|
+
|
|
77
96
|
print_stats(data) if @verbose
|
|
78
97
|
print_stat(data) if @stat
|
|
79
98
|
|
|
@@ -95,7 +114,10 @@ module Rperf
|
|
|
95
114
|
# This allows interval-based profiling where each snapshot covers only
|
|
96
115
|
# the period since the last clear.
|
|
97
116
|
def self.snapshot(clear: false)
|
|
98
|
-
_c_snapshot(clear)
|
|
117
|
+
data = _c_snapshot(clear)
|
|
118
|
+
return unless data
|
|
119
|
+
merge_vm_state_labels!(data)
|
|
120
|
+
data
|
|
99
121
|
end
|
|
100
122
|
|
|
101
123
|
# Label set management for per-context profiling.
|
|
@@ -130,6 +152,9 @@ module Rperf
|
|
|
130
152
|
#
|
|
131
153
|
# Values of nil remove that key. Existing labels are merged.
|
|
132
154
|
def self.label(**kw, &block)
|
|
155
|
+
return yield if block && !_c_running?
|
|
156
|
+
return unless _c_running?
|
|
157
|
+
|
|
133
158
|
_init_label_sets unless @label_set_table
|
|
134
159
|
|
|
135
160
|
cur_id = _c_get_label
|
|
@@ -189,11 +214,53 @@ module Rperf
|
|
|
189
214
|
end
|
|
190
215
|
|
|
191
216
|
|
|
217
|
+
# Merge vm_state from C samples into label_sets as a "Ruby" label key.
|
|
218
|
+
# Mutates data in place: updates label_set_id on each sample, strips vm_state,
|
|
219
|
+
# and extends label_sets with new entries as needed.
|
|
220
|
+
def self.merge_vm_state_labels!(data)
|
|
221
|
+
samples_key = data[:aggregated_samples] ? :aggregated_samples : :raw_samples
|
|
222
|
+
samples = data[samples_key]
|
|
223
|
+
return unless samples
|
|
224
|
+
|
|
225
|
+
orig_label_sets = data[:label_sets]
|
|
226
|
+
label_sets = (orig_label_sets || [{}]).dup
|
|
227
|
+
mapping = {} # [original_label_set_id, vm_state] => new_label_set_id
|
|
228
|
+
modified = false
|
|
229
|
+
|
|
230
|
+
samples.each do |sample|
|
|
231
|
+
vm_state = sample[4] || 0
|
|
232
|
+
next if vm_state == 0
|
|
233
|
+
next unless VM_STATE_LABELS.key?(vm_state)
|
|
234
|
+
|
|
235
|
+
label_set_id = sample[3] || 0
|
|
236
|
+
cache_key = [label_set_id, vm_state]
|
|
237
|
+
new_id = mapping[cache_key]
|
|
238
|
+
unless new_id
|
|
239
|
+
base = label_sets[label_set_id] || {}
|
|
240
|
+
key, value = VM_STATE_LABELS[vm_state]
|
|
241
|
+
new_ls = base.merge(key => value).freeze
|
|
242
|
+
new_id = label_sets.size
|
|
243
|
+
label_sets << new_ls
|
|
244
|
+
mapping[cache_key] = new_id
|
|
245
|
+
end
|
|
246
|
+
sample[3] = new_id
|
|
247
|
+
modified = true
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Strip vm_state (5th element) from all samples
|
|
251
|
+
samples.each { |s| s.pop if s.size > 4 }
|
|
252
|
+
|
|
253
|
+
# Only set label_sets if they were already present or we added vm_state labels
|
|
254
|
+
data[:label_sets] = label_sets if orig_label_sets || modified
|
|
255
|
+
end
|
|
256
|
+
private_class_method :merge_vm_state_labels!
|
|
257
|
+
|
|
192
258
|
# Saves profiling data to a file.
|
|
193
|
-
# format: :pprof, :collapsed, or :text. nil = auto-detect from path extension
|
|
259
|
+
# format: :json, :pprof, :collapsed, or :text. nil = auto-detect from path extension
|
|
260
|
+
# .json.gz → json (rperf native, default)
|
|
194
261
|
# .collapsed → collapsed stacks (FlameGraph / speedscope compatible)
|
|
195
262
|
# .txt → text report (human/AI readable flat + cumulative table)
|
|
196
|
-
#
|
|
263
|
+
# .pb.gz → pprof protobuf (gzip compressed)
|
|
197
264
|
def self.save(path, data, format: nil)
|
|
198
265
|
write_data(path, data, format)
|
|
199
266
|
end
|
|
@@ -205,17 +272,38 @@ module Rperf
|
|
|
205
272
|
File.write(path, Collapsed.encode(data))
|
|
206
273
|
when :text
|
|
207
274
|
File.write(path, Text.encode(data))
|
|
275
|
+
when :json
|
|
276
|
+
require "json"
|
|
277
|
+
File.binwrite(path, gzip(JSON.generate(data.merge(rperf_version: VERSION))))
|
|
208
278
|
else
|
|
209
279
|
File.binwrite(path, gzip(PProf.encode(data)))
|
|
210
280
|
end
|
|
211
281
|
end
|
|
212
282
|
private_class_method :write_data
|
|
213
283
|
|
|
284
|
+
# Load a profile saved by rperf record (.json.gz).
|
|
285
|
+
# Returns the data hash (same format as Rperf.stop / Rperf.snapshot).
|
|
286
|
+
# Warns to stderr if the file was saved by a different rperf version.
|
|
287
|
+
def self.load(path)
|
|
288
|
+
compressed = File.binread(path)
|
|
289
|
+
raw = Zlib::GzipReader.new(StringIO.new(compressed)).read
|
|
290
|
+
require "json"
|
|
291
|
+
data = JSON.parse(raw, symbolize_names: true)
|
|
292
|
+
saved_version = data.delete(:rperf_version)
|
|
293
|
+
if saved_version && saved_version != VERSION
|
|
294
|
+
$stderr.puts "rperf: warning: file was saved by rperf #{saved_version} (current: #{VERSION})"
|
|
295
|
+
elsif saved_version.nil?
|
|
296
|
+
$stderr.puts "rperf: warning: file has no version info (may be from an older rperf)"
|
|
297
|
+
end
|
|
298
|
+
data
|
|
299
|
+
end
|
|
300
|
+
|
|
214
301
|
def self.detect_format(path, format)
|
|
215
302
|
return format.to_sym if format
|
|
216
303
|
case path.to_s
|
|
217
|
-
when /\.collapsed\z/
|
|
218
|
-
when /\.txt\z/
|
|
304
|
+
when /\.collapsed\z/ then :collapsed
|
|
305
|
+
when /\.txt\z/ then :text
|
|
306
|
+
when /\.json(\.gz)?\z/ then :json
|
|
219
307
|
else :pprof
|
|
220
308
|
end
|
|
221
309
|
end
|
|
@@ -233,16 +321,15 @@ module Rperf
|
|
|
233
321
|
def self.print_stats(data)
|
|
234
322
|
count = data[:sampling_count] || 0
|
|
235
323
|
total_ns = data[:sampling_time_ns] || 0
|
|
236
|
-
sample_count = data[:sampling_count] || 0
|
|
237
324
|
mode = data[:mode] || :cpu
|
|
238
325
|
frequency = data[:frequency] || 0
|
|
239
326
|
|
|
240
327
|
total_ms = total_ns / 1_000_000.0
|
|
241
328
|
avg_us = count > 0 ? total_ns / count / 1000.0 : 0.0
|
|
242
329
|
|
|
243
|
-
$stderr.puts "[
|
|
244
|
-
$stderr.puts "[
|
|
245
|
-
$stderr.puts "[
|
|
330
|
+
$stderr.puts "[Rperf] mode=#{mode} frequency=#{frequency}Hz"
|
|
331
|
+
$stderr.puts "[Rperf] sampling: #{count} calls, #{format("%.2f", total_ms)}ms total, #{format("%.1f", avg_us)}us/call avg"
|
|
332
|
+
$stderr.puts "[Rperf] samples recorded: #{count}"
|
|
246
333
|
|
|
247
334
|
print_top(data)
|
|
248
335
|
end
|
|
@@ -291,13 +378,13 @@ module Rperf
|
|
|
291
378
|
|
|
292
379
|
def self.print_top_table(kind, table, total_weight)
|
|
293
380
|
top = table.sort_by { |_, w| -w }.first(TOP_N)
|
|
294
|
-
$stderr.puts "[
|
|
381
|
+
$stderr.puts "[Rperf] top #{top.size} by #{kind}:"
|
|
295
382
|
top.each do |key, weight|
|
|
296
383
|
label, path = key
|
|
297
384
|
ms = weight / 1_000_000.0
|
|
298
385
|
pct = total_weight > 0 ? weight * 100.0 / total_weight : 0.0
|
|
299
386
|
loc = path.empty? ? "" : " (#{path})"
|
|
300
|
-
$stderr.puts format("[
|
|
387
|
+
$stderr.puts format("[Rperf] %8.1fms %5.1f%% %s%s", ms, pct, label, loc)
|
|
301
388
|
end
|
|
302
389
|
end
|
|
303
390
|
|
|
@@ -314,8 +401,9 @@ module Rperf
|
|
|
314
401
|
samples_raw = data[:aggregated_samples] || []
|
|
315
402
|
real_ns = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @stat_start_mono) * 1_000_000_000).to_i
|
|
316
403
|
times = Process.times
|
|
317
|
-
|
|
318
|
-
|
|
404
|
+
start_times = @stat_start_times || Struct.new(:utime, :stime).new(0.0, 0.0)
|
|
405
|
+
user_ns = ((times.utime - start_times.utime) * 1_000_000_000).to_i
|
|
406
|
+
sys_ns = ((times.stime - start_times.stime) * 1_000_000_000).to_i
|
|
319
407
|
|
|
320
408
|
command = ENV["RPERF_STAT_COMMAND"] || "(unknown)"
|
|
321
409
|
|
|
@@ -327,7 +415,7 @@ module Rperf
|
|
|
327
415
|
$stderr.puts format(" %14s ms real", format_ms(real_ns))
|
|
328
416
|
|
|
329
417
|
if samples_raw.size > 0
|
|
330
|
-
breakdown, total_weight = compute_stat_breakdown(samples_raw)
|
|
418
|
+
breakdown, total_weight = compute_stat_breakdown(samples_raw, data[:label_sets])
|
|
331
419
|
print_stat_breakdown(breakdown, total_weight)
|
|
332
420
|
print_stat_runtime_info(data)
|
|
333
421
|
print_stat_system_info
|
|
@@ -338,20 +426,25 @@ module Rperf
|
|
|
338
426
|
$stderr.puts
|
|
339
427
|
end
|
|
340
428
|
|
|
341
|
-
def self.compute_stat_breakdown(samples_raw)
|
|
429
|
+
def self.compute_stat_breakdown(samples_raw, label_sets)
|
|
342
430
|
breakdown = Hash.new(0)
|
|
343
431
|
total_weight = 0
|
|
344
432
|
|
|
345
|
-
samples_raw.each do |frames, weight|
|
|
433
|
+
samples_raw.each do |frames, weight, _thread_seq, label_set_id|
|
|
346
434
|
total_weight += weight
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
435
|
+
category = :cpu_execution
|
|
436
|
+
if label_sets && label_set_id && label_set_id > 0
|
|
437
|
+
ls = label_sets[label_set_id]
|
|
438
|
+
if ls
|
|
439
|
+
gvl = ls["%GVL"]
|
|
440
|
+
gc = ls["%GC"]
|
|
441
|
+
if gvl == "blocked" then category = :gvl_blocked
|
|
442
|
+
elsif gvl == "wait" then category = :gvl_wait
|
|
443
|
+
elsif gc == "mark" then category = :gc_marking
|
|
444
|
+
elsif gc == "sweep" then category = :gc_sweeping
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
355
448
|
breakdown[category] += weight
|
|
356
449
|
end
|
|
357
450
|
|
|
@@ -363,11 +456,11 @@ module Rperf
|
|
|
363
456
|
$stderr.puts
|
|
364
457
|
|
|
365
458
|
[
|
|
366
|
-
[:cpu_execution, "CPU execution"],
|
|
367
|
-
[:gvl_blocked, "[
|
|
368
|
-
[:gvl_wait, "[
|
|
369
|
-
[:gc_marking, "[
|
|
370
|
-
[:gc_sweeping, "[
|
|
459
|
+
[:cpu_execution, "[Rperf] CPU execution"],
|
|
460
|
+
[:gvl_blocked, "[Rperf] GVL blocked (I/O, sleep)"],
|
|
461
|
+
[:gvl_wait, "[Rperf] GVL wait (contention)"],
|
|
462
|
+
[:gc_marking, "[Rperf] GC marking"],
|
|
463
|
+
[:gc_sweeping, "[Rperf] GC sweeping"],
|
|
371
464
|
].each do |key, label|
|
|
372
465
|
w = breakdown[key]
|
|
373
466
|
next if w == 0
|
|
@@ -378,20 +471,20 @@ module Rperf
|
|
|
378
471
|
private_class_method :print_stat_breakdown
|
|
379
472
|
|
|
380
473
|
def self.print_stat_runtime_info(data)
|
|
381
|
-
thread_count = data[:detected_thread_count] || 0
|
|
382
|
-
$stderr.puts STAT_LINE.call(format_integer(thread_count), " ", "[Ruby] detected threads") if thread_count > 0
|
|
383
474
|
gc = GC.stat
|
|
384
475
|
$stderr.puts STAT_LINE.call(format_ms(gc[:time] * 1_000_000), "ms",
|
|
385
|
-
"[Ruby] GC time (%s count: %s minor, %s major)" % [
|
|
476
|
+
"[Ruby ] GC time (%s count: %s minor, %s major)" % [
|
|
386
477
|
format_integer(gc[:count]),
|
|
387
478
|
format_integer(gc[:minor_gc_count]),
|
|
388
479
|
format_integer(gc[:major_gc_count])])
|
|
389
|
-
$stderr.puts STAT_LINE.call(format_integer(gc[:total_allocated_objects]), " ", "[Ruby] allocated objects")
|
|
390
|
-
$stderr.puts STAT_LINE.call(format_integer(gc[:total_freed_objects]), " ", "[Ruby] freed objects")
|
|
480
|
+
$stderr.puts STAT_LINE.call(format_integer(gc[:total_allocated_objects]), " ", "[Ruby ] allocated objects")
|
|
481
|
+
$stderr.puts STAT_LINE.call(format_integer(gc[:total_freed_objects]), " ", "[Ruby ] freed objects")
|
|
482
|
+
thread_count = data[:detected_thread_count] || 0
|
|
483
|
+
$stderr.puts STAT_LINE.call(format_integer(thread_count), " ", "[Ruby ] detected threads") if thread_count > 0
|
|
391
484
|
if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
|
|
392
485
|
yjit = RubyVM::YJIT.runtime_stats
|
|
393
486
|
if yjit[:ratio_in_yjit]
|
|
394
|
-
$stderr.puts STAT_LINE.call(format("%.1f%%", yjit[:ratio_in_yjit] * 100), " ", "[Ruby] YJIT code execution ratio")
|
|
487
|
+
$stderr.puts STAT_LINE.call(format("%.1f%%", yjit[:ratio_in_yjit] * 100), " ", "[Ruby ] YJIT code execution ratio")
|
|
395
488
|
end
|
|
396
489
|
end
|
|
397
490
|
end
|
|
@@ -401,12 +494,20 @@ module Rperf
|
|
|
401
494
|
sys_stats = get_system_stats
|
|
402
495
|
maxrss_kb = sys_stats[:maxrss_kb]
|
|
403
496
|
if maxrss_kb
|
|
404
|
-
$stderr.puts STAT_LINE.call(format_integer((maxrss_kb / 1024.0).round), "MB", "[OS] peak memory (maxrss)")
|
|
497
|
+
$stderr.puts STAT_LINE.call(format_integer((maxrss_kb / 1024.0).round), "MB", "[OS ] peak memory (maxrss)")
|
|
498
|
+
end
|
|
499
|
+
if sys_stats[:page_faults_minor]
|
|
500
|
+
minor = sys_stats[:page_faults_minor]
|
|
501
|
+
major = sys_stats[:page_faults_major]
|
|
502
|
+
$stderr.puts STAT_LINE.call(
|
|
503
|
+
format_integer(minor + major), " ",
|
|
504
|
+
"[OS ] page faults (%s minor, %s major)" % [
|
|
505
|
+
format_integer(minor), format_integer(major)])
|
|
405
506
|
end
|
|
406
507
|
if sys_stats[:ctx_voluntary]
|
|
407
508
|
$stderr.puts STAT_LINE.call(
|
|
408
509
|
format_integer(sys_stats[:ctx_voluntary] + sys_stats[:ctx_involuntary]), " ",
|
|
409
|
-
"[OS] context switches (%s voluntary, %s involuntary)" % [
|
|
510
|
+
"[OS ] context switches (%s voluntary, %s involuntary)" % [
|
|
410
511
|
format_integer(sys_stats[:ctx_voluntary]),
|
|
411
512
|
format_integer(sys_stats[:ctx_involuntary])])
|
|
412
513
|
end
|
|
@@ -415,7 +516,7 @@ module Rperf
|
|
|
415
516
|
w = sys_stats[:io_write_bytes]
|
|
416
517
|
$stderr.puts STAT_LINE.call(
|
|
417
518
|
format_integer(((r + w) / 1024.0 / 1024.0).round), "MB",
|
|
418
|
-
"[OS] disk I/O (%s MB read, %s MB write)" % [
|
|
519
|
+
"[OS ] disk I/O (%s MB read, %s MB write)" % [
|
|
419
520
|
format_integer((r / 1024.0 / 1024.0).round),
|
|
420
521
|
format_integer((w / 1024.0 / 1024.0).round)])
|
|
421
522
|
end
|
|
@@ -436,6 +537,10 @@ module Rperf
|
|
|
436
537
|
samples = data[:sampling_count] || samples_raw.size
|
|
437
538
|
$stderr.puts format(" %d samples / %d triggers, %.1f%% profiler overhead",
|
|
438
539
|
samples, triggers, overhead_pct)
|
|
540
|
+
dropped = data[:dropped_samples] || 0
|
|
541
|
+
if dropped > 0
|
|
542
|
+
$stderr.puts format(" WARNING: %d samples dropped due to memory allocation failure", dropped)
|
|
543
|
+
end
|
|
439
544
|
end
|
|
440
545
|
private_class_method :print_stat_footer
|
|
441
546
|
|
|
@@ -477,6 +582,12 @@ module Rperf
|
|
|
477
582
|
stats[:maxrss_kb] = rss if rss && rss > 0
|
|
478
583
|
end
|
|
479
584
|
|
|
585
|
+
if File.readable?("/proc/self/stat")
|
|
586
|
+
fields = File.read("/proc/self/stat").split
|
|
587
|
+
stats[:page_faults_minor] = fields[9].to_i
|
|
588
|
+
stats[:page_faults_major] = fields[11].to_i
|
|
589
|
+
end
|
|
590
|
+
|
|
480
591
|
if File.readable?("/proc/self/io")
|
|
481
592
|
# Linux: parse /proc/self/io
|
|
482
593
|
File.read("/proc/self/io").each_line do |line|
|
|
@@ -500,16 +611,22 @@ module Rperf
|
|
|
500
611
|
raise ArgumentError, "RPERF_MODE must be 'cpu' or 'wall', got: #{_rperf_mode_str.inspect}"
|
|
501
612
|
end
|
|
502
613
|
_rperf_mode = _rperf_mode_str == "wall" ? :wall : :cpu
|
|
503
|
-
_rperf_format =
|
|
614
|
+
_rperf_format = if ENV["RPERF_FORMAT"]
|
|
615
|
+
unless %w[pprof collapsed text json].include?(ENV["RPERF_FORMAT"])
|
|
616
|
+
raise ArgumentError, "RPERF_FORMAT must be one of pprof, collapsed, text, json, got: #{ENV["RPERF_FORMAT"].inspect}"
|
|
617
|
+
end
|
|
618
|
+
ENV["RPERF_FORMAT"].to_sym
|
|
619
|
+
end
|
|
504
620
|
_rperf_stat = ENV["RPERF_STAT"] == "1"
|
|
505
621
|
_rperf_signal = case ENV["RPERF_SIGNAL"]
|
|
506
622
|
when nil then nil
|
|
507
623
|
when "false" then false
|
|
508
|
-
|
|
624
|
+
when /\A\d+\z/ then ENV["RPERF_SIGNAL"].to_i
|
|
625
|
+
else raise ArgumentError, "RPERF_SIGNAL must be a signal number or 'false', got: #{ENV["RPERF_SIGNAL"].inspect}"
|
|
509
626
|
end
|
|
510
627
|
_rperf_aggregate = ENV["RPERF_AGGREGATE"] != "0"
|
|
511
628
|
_rperf_start_opts = { frequency: (ENV["RPERF_FREQUENCY"] || 1000).to_i, mode: _rperf_mode,
|
|
512
|
-
output: _rperf_stat ? ENV["RPERF_OUTPUT"] : (ENV["RPERF_OUTPUT"] || "rperf.
|
|
629
|
+
output: _rperf_stat ? ENV["RPERF_OUTPUT"] : (ENV["RPERF_OUTPUT"] || "rperf.json.gz"),
|
|
513
630
|
verbose: ENV["RPERF_VERBOSE"] == "1",
|
|
514
631
|
format: _rperf_format,
|
|
515
632
|
stat: _rperf_stat,
|
|
@@ -692,7 +809,7 @@ module Rperf
|
|
|
692
809
|
intern.("frequency: #{frequency}Hz"),
|
|
693
810
|
intern.("ruby: #{RUBY_DESCRIPTION}"),
|
|
694
811
|
]
|
|
695
|
-
doc_url_idx = intern.("https://ko1.github.io/rperf/help.html")
|
|
812
|
+
doc_url_idx = intern.("https://ko1.github.io/rperf/docs/help.html")
|
|
696
813
|
|
|
697
814
|
# field 6: string_table (repeated string)
|
|
698
815
|
string_table.each do |s|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rperf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Koichi Sasada
|
|
@@ -38,8 +38,8 @@ dependencies:
|
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '3.6'
|
|
40
40
|
description: A safepoint-based sampling performance profiler that uses thread CPU
|
|
41
|
-
time deltas as weights to correct safepoint bias. Outputs pprof, collapsed
|
|
42
|
-
or text report.
|
|
41
|
+
time deltas as weights to correct safepoint bias. Outputs JSON, pprof, collapsed
|
|
42
|
+
stacks, or text report.
|
|
43
43
|
executables:
|
|
44
44
|
- rperf
|
|
45
45
|
extensions:
|
|
@@ -48,6 +48,7 @@ extra_rdoc_files: []
|
|
|
48
48
|
files:
|
|
49
49
|
- README.md
|
|
50
50
|
- docs/help.md
|
|
51
|
+
- docs/logo.svg
|
|
51
52
|
- exe/rperf
|
|
52
53
|
- ext/rperf/extconf.rb
|
|
53
54
|
- ext/rperf/rperf.c
|
|
@@ -56,6 +57,7 @@ files:
|
|
|
56
57
|
- lib/rperf/rack.rb
|
|
57
58
|
- lib/rperf/sidekiq.rb
|
|
58
59
|
- lib/rperf/version.rb
|
|
60
|
+
- lib/rperf/viewer.rb
|
|
59
61
|
homepage: https://github.com/ko1/rperf
|
|
60
62
|
licenses:
|
|
61
63
|
- MIT
|
|
@@ -74,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
76
|
- !ruby/object:Gem::Version
|
|
75
77
|
version: '0'
|
|
76
78
|
requirements: []
|
|
77
|
-
rubygems_version: 4.0.
|
|
79
|
+
rubygems_version: 4.0.6
|
|
78
80
|
specification_version: 4
|
|
79
81
|
summary: Safepoint-based sampling performance profiler for Ruby
|
|
80
82
|
test_files: []
|