rperf 0.5.0 → 0.7.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 +69 -28
- data/docs/help.md +153 -6
- data/exe/rperf +1 -1
- data/ext/rperf/rperf.c +406 -121
- data/lib/rperf/active_job.rb +13 -0
- data/lib/rperf/rack.rb +15 -0
- data/lib/rperf/sidekiq.rb +9 -0
- data/lib/rperf/version.rb +1 -1
- data/lib/rperf.rb +141 -12
- metadata +4 -1
data/lib/rperf/rack.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "rperf"
|
|
2
|
+
|
|
3
|
+
class Rperf::RackMiddleware
|
|
4
|
+
def initialize(app, label_key: :endpoint)
|
|
5
|
+
@app = app
|
|
6
|
+
@label_key = label_key
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
endpoint = "#{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
|
|
11
|
+
Rperf.profile(@label_key => endpoint) do
|
|
12
|
+
@app.call(env)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/rperf/version.rb
CHANGED
data/lib/rperf.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
require_relative "rperf/version"
|
|
2
2
|
require "zlib"
|
|
3
3
|
require "stringio"
|
|
4
4
|
|
|
@@ -23,7 +23,7 @@ module Rperf
|
|
|
23
23
|
# .collapsed → collapsed stacks (FlameGraph / speedscope compatible)
|
|
24
24
|
# .txt → text report (human/AI readable flat + cumulative table)
|
|
25
25
|
# otherwise (.pb.gz etc) → pprof protobuf (gzip compressed)
|
|
26
|
-
def self.start(frequency: 1000, mode: :cpu, output: nil, verbose: false, format: nil, stat: false, signal: nil, aggregate: true)
|
|
26
|
+
def self.start(frequency: 1000, mode: :cpu, output: nil, verbose: false, format: nil, stat: false, signal: nil, aggregate: true, defer: false)
|
|
27
27
|
raise ArgumentError, "frequency must be a positive integer (got #{frequency.inspect})" unless frequency.is_a?(Integer) && frequency > 0
|
|
28
28
|
raise ArgumentError, "frequency must be <= 10000 (10KHz), got #{frequency}" if frequency > 10_000
|
|
29
29
|
raise ArgumentError, "mode must be :cpu or :wall, got #{mode.inspect}" unless %i[cpu wall].include?(mode)
|
|
@@ -42,7 +42,9 @@ module Rperf
|
|
|
42
42
|
@format = format
|
|
43
43
|
@stat = stat
|
|
44
44
|
@stat_start_mono = Process.clock_gettime(Process::CLOCK_MONOTONIC) if @stat
|
|
45
|
-
|
|
45
|
+
@label_set_table = nil
|
|
46
|
+
@label_set_index = nil
|
|
47
|
+
_c_start(frequency, c_mode, aggregate, c_signal, defer)
|
|
46
48
|
|
|
47
49
|
if block_given?
|
|
48
50
|
begin
|
|
@@ -61,15 +63,15 @@ module Rperf
|
|
|
61
63
|
# :aggregated_samples. Build aggregated view so encoders always work.
|
|
62
64
|
if data[:raw_samples] && !data[:aggregated_samples]
|
|
63
65
|
merged = {}
|
|
64
|
-
data[:raw_samples].each do |frames, weight, thread_seq|
|
|
65
|
-
key = [frames, thread_seq || 0]
|
|
66
|
+
data[:raw_samples].each do |frames, weight, thread_seq, label_set_id|
|
|
67
|
+
key = [frames, thread_seq || 0, label_set_id || 0]
|
|
66
68
|
if merged.key?(key)
|
|
67
69
|
merged[key] += weight
|
|
68
70
|
else
|
|
69
71
|
merged[key] = weight
|
|
70
72
|
end
|
|
71
73
|
end
|
|
72
|
-
data[:aggregated_samples] = merged.map { |(frames, ts), w| [frames, w, ts] }
|
|
74
|
+
data[:aggregated_samples] = merged.map { |(frames, ts, lsi), w| [frames, w, ts, lsi] }
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
print_stats(data) if @verbose
|
|
@@ -84,6 +86,109 @@ module Rperf
|
|
|
84
86
|
data
|
|
85
87
|
end
|
|
86
88
|
|
|
89
|
+
# Returns a snapshot of the current profiling data without stopping.
|
|
90
|
+
# Only works in aggregate mode (the default). Returns nil if not profiling.
|
|
91
|
+
# The returned data has the same format as stop's return value and can be
|
|
92
|
+
# passed to save(), PProf.encode(), Collapsed.encode(), or Text.encode().
|
|
93
|
+
#
|
|
94
|
+
# +clear:+ if true, resets aggregated data after taking the snapshot.
|
|
95
|
+
# This allows interval-based profiling where each snapshot covers only
|
|
96
|
+
# the period since the last clear.
|
|
97
|
+
def self.snapshot(clear: false)
|
|
98
|
+
_c_snapshot(clear)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Label set management for per-context profiling.
|
|
102
|
+
# Label sets are stored as an Array of Hashes, indexed by label_set_id.
|
|
103
|
+
# Index 0 is reserved (no labels).
|
|
104
|
+
|
|
105
|
+
@label_set_table = nil # Array of frozen Hash
|
|
106
|
+
@label_set_index = nil # Hash → id (for dedup)
|
|
107
|
+
|
|
108
|
+
def self._init_label_sets
|
|
109
|
+
@label_set_table = [{}] # id 0 = no labels
|
|
110
|
+
@label_set_index = { {} => 0 }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self._intern_label_set(hash)
|
|
114
|
+
frozen = hash.frozen? ? hash : hash.freeze
|
|
115
|
+
@label_set_index[frozen] ||= begin
|
|
116
|
+
id = @label_set_table.size
|
|
117
|
+
@label_set_table << frozen
|
|
118
|
+
_c_set_label_sets(@label_set_table)
|
|
119
|
+
id
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Sets labels on the current thread for profiling annotation.
|
|
124
|
+
# With a block: restores previous labels when the block exits.
|
|
125
|
+
# Without a block: sets labels persistently on the current thread.
|
|
126
|
+
# Labels are key-value pairs written into pprof sample labels.
|
|
127
|
+
#
|
|
128
|
+
# Rperf.label(request: "abc") { handle_request }
|
|
129
|
+
# Rperf.label(request: "abc") # persistent set
|
|
130
|
+
#
|
|
131
|
+
# Values of nil remove that key. Existing labels are merged.
|
|
132
|
+
def self.label(**kw, &block)
|
|
133
|
+
_init_label_sets unless @label_set_table
|
|
134
|
+
|
|
135
|
+
cur_id = _c_get_label
|
|
136
|
+
cur_labels = @label_set_table[cur_id] || {}
|
|
137
|
+
|
|
138
|
+
new_labels = cur_labels.merge(kw).reject { |_, v| v.nil? }
|
|
139
|
+
new_id = _intern_label_set(new_labels)
|
|
140
|
+
_c_set_label(new_id)
|
|
141
|
+
|
|
142
|
+
if block
|
|
143
|
+
begin
|
|
144
|
+
yield
|
|
145
|
+
ensure
|
|
146
|
+
_c_set_label(cur_id)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Profiles the given block: activates timer sampling for the duration
|
|
152
|
+
# and optionally applies labels. Use with start(defer: true) to profile
|
|
153
|
+
# only specific sections of code.
|
|
154
|
+
#
|
|
155
|
+
# Rperf.start(defer: true, mode: :wall)
|
|
156
|
+
# Rperf.profile(endpoint: "/users") { handle_request }
|
|
157
|
+
# data = Rperf.stop
|
|
158
|
+
#
|
|
159
|
+
# Nesting is supported: timer stays active until the outermost profile exits.
|
|
160
|
+
# Requires a block. Raises if profiling is not started.
|
|
161
|
+
def self.profile(**kw, &block)
|
|
162
|
+
raise ArgumentError, "Rperf.profile requires a block" unless block
|
|
163
|
+
raise RuntimeError, "Rperf is not started" unless _c_running?
|
|
164
|
+
|
|
165
|
+
_init_label_sets unless @label_set_table
|
|
166
|
+
|
|
167
|
+
cur_id = _c_get_label
|
|
168
|
+
cur_labels = @label_set_table[cur_id] || {}
|
|
169
|
+
new_labels = cur_labels.merge(kw).reject { |_, v| v.nil? }
|
|
170
|
+
new_id = _intern_label_set(new_labels)
|
|
171
|
+
_c_set_label(new_id)
|
|
172
|
+
|
|
173
|
+
_c_profile_inc
|
|
174
|
+
|
|
175
|
+
begin
|
|
176
|
+
yield
|
|
177
|
+
ensure
|
|
178
|
+
_c_profile_dec
|
|
179
|
+
_c_set_label(cur_id)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns the current thread's labels as a Hash.
|
|
184
|
+
# Returns an empty Hash if no labels are set or profiling is not running.
|
|
185
|
+
def self.labels
|
|
186
|
+
return {} unless @label_set_table
|
|
187
|
+
cur_id = _c_get_label
|
|
188
|
+
@label_set_table[cur_id] || {}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
|
|
87
192
|
# Saves profiling data to a file.
|
|
88
193
|
# format: :pprof, :collapsed, or :text. nil = auto-detect from path extension
|
|
89
194
|
# .collapsed → collapsed stacks (FlameGraph / speedscope compatible)
|
|
@@ -498,17 +603,30 @@ module Rperf
|
|
|
498
603
|
end
|
|
499
604
|
}
|
|
500
605
|
|
|
501
|
-
# Convert string frames to index frames and merge identical stacks per thread
|
|
606
|
+
# Convert string frames to index frames and merge identical stacks per thread/label
|
|
502
607
|
merged = Hash.new(0)
|
|
503
608
|
thread_seq_key = intern.("thread_seq")
|
|
504
|
-
|
|
505
|
-
|
|
609
|
+
label_sets = data[:label_sets] # Array of Hash (may be nil)
|
|
610
|
+
samples_raw.each do |frames, weight, thread_seq, label_set_id|
|
|
611
|
+
key = [frames.map { |path, label| [intern.(path), intern.(label)] }, thread_seq || 0, label_set_id || 0]
|
|
506
612
|
merged[key] += weight
|
|
507
613
|
end
|
|
508
614
|
merged = merged.to_a
|
|
509
615
|
|
|
616
|
+
# Intern label set keys/values for pprof labels
|
|
617
|
+
label_key_indices = {} # String key → string_table index
|
|
618
|
+
if label_sets
|
|
619
|
+
label_sets.each do |ls|
|
|
620
|
+
ls.each do |k, v|
|
|
621
|
+
sk = k.to_s
|
|
622
|
+
label_key_indices[sk] ||= intern.(sk)
|
|
623
|
+
intern.(v.to_s) # ensure value is interned
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
510
628
|
# Build location/function tables
|
|
511
|
-
locations, functions = build_tables(merged.map { |(frames, _), w| [frames, w] })
|
|
629
|
+
locations, functions = build_tables(merged.map { |(frames, _, _), w| [frames, w] })
|
|
512
630
|
|
|
513
631
|
# Intern type label and unit
|
|
514
632
|
type_label = mode == :wall ? "wall" : "cpu"
|
|
@@ -521,8 +639,8 @@ module Rperf
|
|
|
521
639
|
# field 1: sample_type (repeated ValueType)
|
|
522
640
|
buf << encode_message(1, encode_value_type(type_idx, ns_idx))
|
|
523
641
|
|
|
524
|
-
# field 2: sample (repeated Sample) with thread_seq
|
|
525
|
-
merged.each do |(frames, thread_seq), weight|
|
|
642
|
+
# field 2: sample (repeated Sample) with thread_seq + user labels
|
|
643
|
+
merged.each do |(frames, thread_seq, label_set_id), weight|
|
|
526
644
|
sample_buf = "".b
|
|
527
645
|
loc_ids = frames.map { |f| locations[f] }
|
|
528
646
|
sample_buf << encode_packed_uint64(1, loc_ids)
|
|
@@ -533,6 +651,17 @@ module Rperf
|
|
|
533
651
|
label_buf << encode_int64(3, thread_seq) # num
|
|
534
652
|
sample_buf << encode_message(3, label_buf)
|
|
535
653
|
end
|
|
654
|
+
if label_sets && label_set_id && label_set_id > 0
|
|
655
|
+
ls = label_sets[label_set_id]
|
|
656
|
+
if ls
|
|
657
|
+
ls.each do |k, v|
|
|
658
|
+
label_buf = "".b
|
|
659
|
+
label_buf << encode_int64(1, label_key_indices[k.to_s]) # key
|
|
660
|
+
label_buf << encode_int64(2, string_index[v.to_s]) # str
|
|
661
|
+
sample_buf << encode_message(3, label_buf)
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
end
|
|
536
665
|
buf << encode_message(2, sample_buf)
|
|
537
666
|
end
|
|
538
667
|
|
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.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Koichi Sasada
|
|
@@ -52,6 +52,9 @@ files:
|
|
|
52
52
|
- ext/rperf/extconf.rb
|
|
53
53
|
- ext/rperf/rperf.c
|
|
54
54
|
- lib/rperf.rb
|
|
55
|
+
- lib/rperf/active_job.rb
|
|
56
|
+
- lib/rperf/rack.rb
|
|
57
|
+
- lib/rperf/sidekiq.rb
|
|
55
58
|
- lib/rperf/version.rb
|
|
56
59
|
homepage: https://github.com/ko1/rperf
|
|
57
60
|
licenses:
|