sperf 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 125090cb17cacbc9157402fcb9db54117011ccde812da775ea00b6c1f6bee535
4
- data.tar.gz: 1430d997988a6538059a0c63be28d4f05f43681d1f2efbd84020490308a5c279
3
+ metadata.gz: 70bf807f20a737cfb74344fb33a3f0e984d6bdd369ea3ff25f528467df676bb8
4
+ data.tar.gz: 5fb3a228a2d0a600dcbe648688c2e068c2634c575900aa058e256b55ae14f176
5
5
  SHA512:
6
- metadata.gz: 17a52cfc856ae47f254bf0df3450ff76cd471703c97e029907abbf639aa9dd8d6971ef8b18acf6a9784b87b295981533776c147b0fc31993e33dcf528cf15a3d
7
- data.tar.gz: 6bbad7152c998859f4ec1f07d42eeca8824918ad635b0454fbe13635d9b913ad309b09f8e7c760f5cdd4cc1d32d1316ba50a5fb7c1863e7718cb1df01a2761ad
6
+ metadata.gz: cd24d9e9c959baf5623a2601f3f46dce83d340d6a1b721dce2da5ddc3066c1d8e1f8cfd3af2aaa72cd85bb230be703dd1823021976e7fbbb1f9f36fd50aac0f8
7
+ data.tar.gz: df95dbacd3a6eef0794a54503856607becd5028fb0e917d9d2e4b957f68d993ea05c590f57f4e1fa21d51737dea052ddbebb116616babbc5c650e643c0eeba56
data/exe/sperf CHANGED
@@ -1,476 +1,2 @@
1
1
  #!/usr/bin/env ruby
2
- require "optparse"
3
- require "socket"
4
-
5
- def find_available_port
6
- server = TCPServer.new("localhost", 0)
7
- port = server.addr[1]
8
- server.close
9
- port
10
- end
11
-
12
- HELP_TEXT = <<'HELP'
13
- sperf - safepoint-based sampling performance profiler for Ruby
14
-
15
- OVERVIEW
16
-
17
- sperf profiles Ruby programs by sampling at safepoints and using actual
18
- time deltas (nanoseconds) as weights to correct safepoint bias.
19
- POSIX systems (Linux, macOS). Requires Ruby >= 3.4.0.
20
-
21
- CLI USAGE
22
-
23
- sperf record [options] command [args...]
24
- sperf stat [options] command [args...]
25
- sperf report [options] [file]
26
- sperf help
27
-
28
- record: Profile and save to file.
29
- -o, --output PATH Output file (default: sperf.data)
30
- -f, --frequency HZ Sampling frequency in Hz (default: 1000)
31
- -m, --mode MODE cpu or wall (default: cpu)
32
- --format FORMAT pprof, collapsed, or text (default: auto from extension)
33
- --signal VALUE Timer signal (Linux only): signal number, or 'false'
34
- for nanosleep thread (default: auto)
35
- -v, --verbose Print sampling statistics to stderr
36
-
37
- stat: Run command and print performance summary to stderr.
38
- Always uses wall mode. No file output by default.
39
- -o, --output PATH Also save profile to file (default: none)
40
- -f, --frequency HZ Sampling frequency in Hz (default: 1000)
41
- --signal VALUE Timer signal (Linux only): signal number, or 'false'
42
- for nanosleep thread (default: auto)
43
- -v, --verbose Print additional sampling statistics
44
-
45
- Shows: user/sys/real time, time breakdown (CPU execution, GVL blocked,
46
- GVL wait, GC marking, GC sweeping), and top 5 hot functions.
47
-
48
- report: Open pprof profile with go tool pprof. Requires Go.
49
- --top Print top functions by flat time
50
- --text Print text report
51
- Default (no flag): opens interactive web UI in browser.
52
- Default file: sperf.data
53
-
54
- diff: Compare two pprof profiles (target - base). Requires Go.
55
- --top Print top functions by diff
56
- --text Print text diff report
57
- Default (no flag): opens diff in browser.
58
-
59
- Examples:
60
- sperf record ruby app.rb
61
- sperf record -o profile.pb.gz ruby app.rb
62
- sperf record -m wall -f 500 -o profile.pb.gz ruby server.rb
63
- sperf record -o profile.collapsed ruby app.rb
64
- sperf record -o profile.txt ruby app.rb
65
- sperf stat ruby app.rb
66
- sperf stat -o profile.pb.gz ruby app.rb
67
- sperf report
68
- sperf report --top profile.pb.gz
69
- sperf diff before.pb.gz after.pb.gz
70
- sperf diff --top before.pb.gz after.pb.gz
71
-
72
- RUBY API
73
-
74
- require "sperf"
75
-
76
- # Block form (recommended) — profiles the block and writes to file
77
- Sperf.start(output: "profile.pb.gz", frequency: 500, mode: :cpu) do
78
- # code to profile
79
- end
80
-
81
- # Manual start/stop — returns data hash for programmatic use
82
- Sperf.start(frequency: 1000, mode: :wall)
83
- # ... code to profile ...
84
- data = Sperf.stop
85
-
86
- # Save data to file later
87
- Sperf.save("profile.pb.gz", data)
88
- Sperf.save("profile.collapsed", data)
89
- Sperf.save("profile.txt", data)
90
-
91
- Sperf.start parameters:
92
- frequency: Sampling frequency in Hz (Integer, default: 1000)
93
- mode: :cpu or :wall (Symbol, default: :cpu)
94
- output: File path to write on stop (String or nil)
95
- verbose: Print statistics to stderr (true/false, default: false)
96
- format: :pprof, :collapsed, :text, or nil for auto-detect (Symbol or nil)
97
-
98
- Sperf.stop return value:
99
- nil if profiler was not running; otherwise a Hash:
100
- { mode: :cpu, # or :wall
101
- frequency: 500,
102
- sampling_count: 1234,
103
- sampling_time_ns: 56789,
104
- samples: [ # Array of [frames, weight]
105
- [frames, weight], # frames: [[path, label], ...] deepest-first
106
- ... # weight: Integer (nanoseconds)
107
- ] }
108
-
109
- Sperf.save(path, data, format: nil)
110
- Writes data to path. format: :pprof, :collapsed, or :text.
111
- nil auto-detects from extension.
112
-
113
- PROFILING MODES
114
-
115
- cpu Measures per-thread CPU time via Linux thread clock.
116
- Use for: finding functions that consume CPU cycles.
117
- Ignores time spent sleeping, in I/O, or waiting for GVL.
118
-
119
- wall Measures wall-clock time (CLOCK_MONOTONIC).
120
- Use for: finding where wall time goes, including I/O, sleep, GVL
121
- contention, and off-CPU waits.
122
- Includes synthetic frames (see below).
123
-
124
- OUTPUT FORMATS
125
-
126
- pprof (default)
127
- Gzip-compressed protobuf. Standard pprof format.
128
- Extension convention: .pb.gz
129
- View with: go tool pprof, pprof-rs, or speedscope (via import).
130
-
131
- collapsed
132
- Plain text. One line per unique stack: "frame1;frame2;...;leaf weight\n"
133
- Frames are semicolon-separated, bottom-to-top. Weight in nanoseconds.
134
- Extension convention: .collapsed
135
- Compatible with: FlameGraph (flamegraph.pl), speedscope.
136
-
137
- text
138
- Human/AI-readable report. Shows total time, then flat and cumulative
139
- top-N tables sorted by weight descending. No parsing needed.
140
- Extension convention: .txt
141
- Example output:
142
- Total: 1523.4ms (cpu)
143
- Samples: 4820, Frequency: 500Hz
144
-
145
- Flat:
146
- 820.3ms 53.8% Array#each (app/models/user.rb)
147
- 312.1ms 20.5% JSON.parse (lib/json/parser.rb)
148
- ...
149
-
150
- Cumulative:
151
- 1401.2ms 92.0% UsersController#index (app/controllers/users_controller.rb)
152
- ...
153
-
154
- Format is auto-detected from the output file extension:
155
- .collapsed → collapsed
156
- .txt → text
157
- anything else → pprof
158
- The --format flag (CLI) or format: parameter (API) overrides auto-detect.
159
-
160
- SYNTHETIC FRAMES
161
-
162
- In wall mode, sperf adds synthetic frames that represent non-CPU time:
163
-
164
- [GVL blocked] Time the thread spent off-GVL (I/O, sleep, C extension
165
- releasing GVL). Attributed to the stack at SUSPENDED.
166
- [GVL wait] Time the thread spent waiting to reacquire the GVL after
167
- becoming ready. Indicates GVL contention. Same stack.
168
-
169
- In both modes, GC time is tracked:
170
-
171
- [GC marking] Time spent in GC marking phase (wall time).
172
- [GC sweeping] Time spent in GC sweeping phase (wall time).
173
-
174
- These always appear as the leaf (deepest) frame in a sample.
175
-
176
- INTERPRETING RESULTS
177
-
178
- Weight unit is always nanoseconds regardless of mode.
179
-
180
- Flat time: weight attributed directly to a function (it was the leaf).
181
- Cumulative time: weight for all samples where the function appears
182
- anywhere in the stack.
183
-
184
- High flat time → the function itself is expensive.
185
- High cum but low flat → the function calls expensive children.
186
-
187
- To convert: 1_000_000 ns = 1 ms, 1_000_000_000 ns = 1 s.
188
-
189
- DIAGNOSING COMMON PERFORMANCE PROBLEMS
190
-
191
- Problem: high CPU usage
192
- Mode: cpu
193
- Look for: functions with high flat cpu time.
194
- Action: optimize the hot function or call it less.
195
-
196
- Problem: slow request / high latency
197
- Mode: wall
198
- Look for: functions with high cum wall time.
199
- If [GVL blocked] is dominant → I/O or sleep is the bottleneck.
200
- If [GVL wait] is dominant → GVL contention; reduce GVL-holding work
201
- or move work to Ractors / child processes.
202
-
203
- Problem: GC pauses
204
- Mode: cpu or wall
205
- Look for: [GC marking] and [GC sweeping] samples.
206
- High [GC marking] → too many live objects; reduce allocations.
207
- High [GC sweeping] → too many short-lived objects; reuse or pool.
208
-
209
- Problem: multithreaded app slower than expected
210
- Mode: wall
211
- Look for: [GVL wait] time across threads.
212
- High [GVL wait] means threads are serialized on the GVL.
213
-
214
- READING COLLAPSED STACKS PROGRAMMATICALLY
215
-
216
- Each line: "bottom_frame;...;top_frame weight_ns"
217
- Parse with:
218
- File.readlines("profile.collapsed").each do |line|
219
- stack, weight = line.rpartition(" ").then { |s, _, w| [s, w.to_i] }
220
- frames = stack.split(";")
221
- # frames[0] is bottom (main), frames[-1] is leaf (hot)
222
- end
223
-
224
- READING PPROF PROGRAMMATICALLY
225
-
226
- Decompress + parse protobuf:
227
- require "zlib"; require "stringio"
228
- raw = Zlib::GzipReader.new(StringIO.new(File.binread("profile.pb.gz"))).read
229
- # raw is a protobuf binary; use google-protobuf gem or pprof tooling.
230
-
231
- Or convert to text with pprof CLI:
232
- go tool pprof -text profile.pb.gz
233
- go tool pprof -top profile.pb.gz
234
- go tool pprof -flame profile.pb.gz
235
-
236
- ENVIRONMENT VARIABLES
237
-
238
- Used internally by the CLI to pass options to the auto-started profiler:
239
- SPERF_ENABLED=1 Enable auto-start on require
240
- SPERF_OUTPUT=path Output file path
241
- SPERF_FREQUENCY=hz Sampling frequency
242
- SPERF_MODE=cpu|wall Profiling mode
243
- SPERF_FORMAT=fmt pprof, collapsed, or text
244
- SPERF_VERBOSE=1 Print statistics
245
- SPERF_SIGNAL=N|false Timer signal number or 'false' for nanosleep (Linux only)
246
-
247
- TIPS
248
-
249
- - Default frequency (1000 Hz) works well for most cases; overhead is < 0.2%.
250
- - For long-running production profiling, lower frequency (100-500) reduces overhead further.
251
- - Profile representative workloads, not micro-benchmarks.
252
- - Compare cpu and wall profiles to distinguish CPU-bound from I/O-bound.
253
- - The verbose flag (-v) shows sampling overhead and top functions on stderr.
254
- HELP
255
-
256
- USAGE = "Usage: sperf record [options] command [args...]\n" \
257
- " sperf stat [options] command [args...]\n" \
258
- " sperf report [options] [file]\n" \
259
- " sperf diff [options] base.pb.gz target.pb.gz\n" \
260
- " sperf help\n"
261
-
262
- # Handle top-level flags before subcommand parsing
263
- case ARGV.first
264
- when "-v", "--version"
265
- require "sperf"
266
- puts "sperf #{Sperf::VERSION}"
267
- exit
268
- when "-h", "--help"
269
- puts USAGE
270
- puts
271
- puts "Run 'sperf help' for full documentation"
272
- exit
273
- end
274
-
275
- subcommand = ARGV.shift
276
-
277
- case subcommand
278
- when "help"
279
- puts HELP_TEXT
280
- exit
281
- when "report"
282
- # sperf report: wrapper around go tool pprof
283
- report_mode = :http # default: open in browser
284
- report_file = nil
285
-
286
- report_parser = OptionParser.new do |opts|
287
- opts.banner = "Usage: sperf report [options] [file]\n" \
288
- " Opens pprof profile in browser (default) or prints summary.\n" \
289
- " Default file: sperf.data"
290
-
291
- opts.on("--top", "Print top functions by flat time") do
292
- report_mode = :top
293
- end
294
-
295
- opts.on("--text", "Print text report") do
296
- report_mode = :text
297
- end
298
-
299
- opts.on("-h", "--help", "Show this help") do
300
- puts opts
301
- exit
302
- end
303
- end
304
-
305
- begin
306
- report_parser.order!(ARGV)
307
- rescue OptionParser::InvalidOption => e
308
- $stderr.puts e.message
309
- $stderr.puts report_parser
310
- exit 1
311
- end
312
-
313
- report_file = ARGV.shift || "sperf.data"
314
-
315
- unless File.exist?(report_file)
316
- $stderr.puts "File not found: #{report_file}"
317
- exit 1
318
- end
319
-
320
- unless system("go", "version", out: File::NULL, err: File::NULL)
321
- $stderr.puts "'go' command not found. Install Go to use 'sperf report'."
322
- $stderr.puts " https://go.dev/dl/"
323
- exit 1
324
- end
325
-
326
- case report_mode
327
- when :top
328
- exec("go", "tool", "pprof", "-top", report_file)
329
- when :text
330
- exec("go", "tool", "pprof", "-text", report_file)
331
- else
332
- exec("go", "tool", "pprof", "-http=localhost:#{find_available_port}", report_file)
333
- end
334
- when "diff"
335
- # sperf diff: compare two pprof profiles via go tool pprof -diff_base
336
- diff_mode = :http
337
- diff_parser = OptionParser.new do |opts|
338
- opts.banner = "Usage: sperf diff [options] base.pb.gz target.pb.gz\n" \
339
- " Compare two pprof profiles (shows target - base)."
340
-
341
- opts.on("--top", "Print top functions by diff") do
342
- diff_mode = :top
343
- end
344
-
345
- opts.on("--text", "Print text diff report") do
346
- diff_mode = :text
347
- end
348
-
349
- opts.on("-h", "--help", "Show this help") do
350
- puts opts
351
- exit
352
- end
353
- end
354
-
355
- begin
356
- diff_parser.order!(ARGV)
357
- rescue OptionParser::InvalidOption => e
358
- $stderr.puts e.message
359
- $stderr.puts diff_parser
360
- exit 1
361
- end
362
-
363
- if ARGV.size < 2
364
- $stderr.puts "Two profile files required."
365
- $stderr.puts diff_parser
366
- exit 1
367
- end
368
-
369
- base_file, target_file = ARGV.shift(2)
370
-
371
- [base_file, target_file].each do |f|
372
- unless File.exist?(f)
373
- $stderr.puts "File not found: #{f}"
374
- exit 1
375
- end
376
- end
377
-
378
- unless system("go", "version", out: File::NULL, err: File::NULL)
379
- $stderr.puts "'go' command not found. Install Go to use 'sperf diff'."
380
- $stderr.puts " https://go.dev/dl/"
381
- exit 1
382
- end
383
-
384
- case diff_mode
385
- when :top
386
- exec("go", "tool", "pprof", "-top", "-diff_base=#{base_file}", target_file)
387
- when :text
388
- exec("go", "tool", "pprof", "-text", "-diff_base=#{base_file}", target_file)
389
- else
390
- exec("go", "tool", "pprof", "-http=localhost:#{find_available_port}", "-diff_base=#{base_file}", target_file)
391
- end
392
- when "record", "stat"
393
- # continue below
394
- else
395
- $stderr.puts "Unknown subcommand: #{subcommand.inspect}" if subcommand
396
- $stderr.puts USAGE
397
- exit 1
398
- end
399
-
400
- output = (subcommand == "stat") ? nil : "sperf.data"
401
- frequency = 1000
402
- mode = (subcommand == "stat") ? "wall" : "cpu"
403
- format = nil
404
- signal = nil
405
- verbose = false
406
-
407
- parser = OptionParser.new do |opts|
408
- opts.banner = USAGE
409
-
410
- opts.on("-o", "--output PATH", "Output file#{subcommand == 'stat' ? ' (default: none)' : ' (default: sperf.data)'}") do |v|
411
- output = v
412
- end
413
-
414
- opts.on("-f", "--frequency HZ", Integer, "Sampling frequency in Hz (default: 1000)") do |v|
415
- frequency = v
416
- end
417
-
418
- if subcommand == "record"
419
- opts.on("-m", "--mode MODE", %w[cpu wall], "Profiling mode: cpu or wall (default: cpu)") do |v|
420
- mode = v
421
- end
422
-
423
- opts.on("--format FORMAT", %w[pprof collapsed text],
424
- "Output format: pprof, collapsed, or text (default: auto from extension)") do |v|
425
- format = v
426
- end
427
- end
428
-
429
- opts.on("--signal VALUE", "Timer signal (Linux only): signal number, or 'false' for nanosleep thread") do |v|
430
- signal = (v == "false") ? "false" : v
431
- end
432
-
433
- opts.on("-v", "--verbose", "Print sampling statistics to stderr") do
434
- verbose = true
435
- end
436
-
437
- opts.on("-h", "--help", "Show this help") do
438
- puts opts
439
- puts
440
- puts "Run 'sperf help' for full documentation (modes, formats, diagnostics guide, etc.)"
441
- exit
442
- end
443
- end
444
-
445
- begin
446
- parser.order!(ARGV)
447
- rescue OptionParser::InvalidOption => e
448
- $stderr.puts e.message
449
- $stderr.puts parser
450
- exit 1
451
- end
452
-
453
- if ARGV.empty?
454
- $stderr.puts "No command specified."
455
- $stderr.puts parser
456
- exit 1
457
- end
458
-
459
- # Add lib dir to RUBYLIB so -rsperf can find the extension
460
- lib_dir = File.expand_path("../lib", __dir__)
461
- ENV["RUBYLIB"] = [lib_dir, ENV["RUBYLIB"]].compact.join(File::PATH_SEPARATOR)
462
- ENV["RUBYOPT"] = "-rsperf #{ENV['RUBYOPT']}".strip
463
- ENV["SPERF_ENABLED"] = "1"
464
- ENV["SPERF_OUTPUT"] = output if output
465
- ENV["SPERF_FREQUENCY"] = frequency.to_s
466
- ENV["SPERF_MODE"] = mode
467
- ENV["SPERF_FORMAT"] = format if format
468
- ENV["SPERF_VERBOSE"] = "1" if verbose
469
- ENV["SPERF_SIGNAL"] = signal if signal
470
-
471
- if subcommand == "stat"
472
- ENV["SPERF_STAT"] = "1"
473
- ENV["SPERF_STAT_COMMAND"] = ARGV.join(" ")
474
- end
475
-
476
- exec(*ARGV)
2
+ require_relative '../lib/sperf'
data/lib/sperf/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sperf
2
- VERSION = "0.2.1"
3
- end
2
+ VERSION = "0.3.0"
3
+ end