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.
@@ -0,0 +1,13 @@
1
+ require "rperf"
2
+
3
+ module Rperf::ActiveJobMiddleware
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ around_perform do |job, block|
8
+ Rperf.profile(job: job.class.name) do
9
+ block.call
10
+ end
11
+ end
12
+ end
13
+ end
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
@@ -0,0 +1,9 @@
1
+ require "rperf"
2
+
3
+ class Rperf::SidekiqMiddleware
4
+ def call(_worker, job, _queue)
5
+ Rperf.profile(job: job["class"]) do
6
+ yield
7
+ end
8
+ end
9
+ end
data/lib/rperf/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rperf
2
- VERSION = "0.5.0"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/rperf.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "rperf/version"
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
- _c_start(frequency, c_mode, aggregate, c_signal)
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
- samples_raw.each do |frames, weight, thread_seq|
505
- key = [frames.map { |path, label| [intern.(path), intern.(label)] }, thread_seq || 0]
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 label
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.5.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: