summaryrefslogtreecommitdiff
path: root/bin/rspec-stackprof
blob: 4d72b44976b5ff6f6282fa8593730a5b1061c19c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative '../config/bundler_setup'
require 'stackprof'
require 'rspec'
require 'optparse'

defaults = {
  mode: 'wall',
  limit: 20,
  raw: false,
  mode_interval: 1000,
  mode_object_interval: 1,
  speedscope: false
}

options = {
  mode: ENV.fetch('MODE', defaults[:mode]) || defaults[:mode],
  interval: ENV['INTERVAL'],
  limit: ENV.fetch('LIMIT', nil) || defaults[:limit],
  raw: ENV.fetch('RAW', false) === 'true' || defaults[:raw],
  output: ENV.fetch('OUTPUT', nil),
  speedscope: ENV.fetch('SPEEDSCOPE', false) === 'true' || defaults[:speedscope]
}

OptionParser.new do |opt|
  opt.banner = <<DOCSTRING
Run rspec with stackprof enabled.

Usage:
  #{__FILE__} [--mode=<wall|cpu|object>] [--interval=<interval>] [--limit=<limit>] [--raw=false|true] [--speedscope=false|true] [--output=filename] filename [rspec options]

Examples:
  #{__FILE__} --mode=wall --interval=#{defaults[:mode_interval]} --limit=#{defaults[:limit]} --raw=true --output=tmp/example.dump spec/controllers/help_controller_spec.rb
  #{__FILE__} spec/policies/project_policy_spec.rb -e 'project visibility'
  #{__FILE__} --speedscope=true spec/controllers/help_controller_spec.rb
  MODE=cpu LIMIT=5 #{__FILE__} spec/controllers/help_controller_spec.rb
DOCSTRING
  opt.separator ''
  opt.separator 'Options:'

  opt.on('--mode wall|cpu|object', "sampling mode (default: #{defaults[:mode]}) [$MODE]")
  opt.on('--interval integer', Integer, "sample interval for mode selected. wall/cpu: microseconds with a default of #{defaults[:mode_interval]}, object: seconds with a default of #{defaults[:mode_object_interval]} [$INTERVAL]")
  opt.on('--limit integer', Integer, "limit output to N entries (default: #{defaults[:limit]}) [$LIMIT]")
  opt.on('--raw true|false', FalseClass, 'collect extra data required for flamegraph [$RAW]')
  opt.on('--output filename', String, 'output filename (default: tmp/rspec-filename.dump) [$OUTPUT]')
  opt.on('--speedscope true|false', FalseClass, "output in speedscope format (default: #{defaults[:speedscope]}) [$SPEEDSCOPE]")
end.order!(into: options)

case options[:mode]
when 'wall'
  options[:interval] ||= defaults[:mode_interval]
when 'cpu'
  options[:interval] ||= defaults[:mode_interval]
when 'object'
  options[:interval] ||= defaults[:mode_object_interval]
else
  raise ArgumentError, "Invalid mode: #{options[:mode]}"
end

filename = ARGV[0].split('/').last
extension = options[:speedscope] ? 'json' : 'dump'
options[:output] ||= "tmp/#{filename}.#{extension}"
options[:interval] = options[:interval].to_i

if options[:speedscope]
  profile = StackProf.run(mode: options[:mode].to_sym, interval: options[:interval], raw: true) do
    RSpec::Core::Runner.run(ARGV, $stderr, $stdout)
  end

  File.write(options[:output].to_s, Gitlab::Json.generate(profile))

  puts <<DOCSTRING
Wrote speedscope profile to: #{options[:output]}

If you have speedscope installed:
  Run: speedscope #{options[:output]}

On OSX:
  Run: cat #{options[:output]} | pbcopy
  Then paste it into https://www.speedscope.app/

Alternatively:
  Upload the file manually to https://www.speedscope.app/
DOCSTRING
else
  StackProf.run(mode: options[:mode].to_sym, out: options[:output], interval: options[:interval], raw: options[:raw]) do
    RSpec::Core::Runner.run(ARGV, $stderr, $stdout)
  end

  cmd = %W(bundle exec stackprof #{options[:output]} --text --limit #{options[:limit]})
  puts '> ' + cmd.join(' ')
  system(*cmd)
end