stackprof 0.2.10 → 0.2.25

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.
@@ -5,9 +5,32 @@
5
5
  body {
6
6
  margin: 0;
7
7
  padding: 0;
8
- font-family: Monaco, "Liberation Mono", Courier, monospace;
8
+ font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
9
9
  font-size: 10pt;
10
10
  }
11
+ .overview-container {
12
+ position: relative;
13
+ }
14
+ .overview {
15
+ cursor: col-resize;
16
+ }
17
+ .overview-viewport-overlay {
18
+ position: absolute;
19
+ top: 0;
20
+ left: 0;
21
+ width: 1;
22
+ height: 1;
23
+ background-color: rgba(0, 0, 0, 0.25);
24
+ transform-origin: top left;
25
+ cursor: -moz-grab;
26
+ cursor: -webkit-grab;
27
+ cursor: grab;
28
+ }
29
+ .moving {
30
+ cursor: -moz-grabbing;
31
+ cursor: -webkit-grabbing;
32
+ cursor: grabbing;
33
+ }
11
34
  .info {
12
35
  display: block;
13
36
  height: 40px;
@@ -36,32 +59,15 @@
36
59
  max-width: 70%;
37
60
  word-wrap: break-word;
38
61
  }
39
- .legend:hover + .flamegraph .flames:not(.highlighted) {
40
- opacity: 0.25;
41
- }
42
- .legend:hover ~ .zoom .flames:not(.highlighted) {
43
- opacity: 0.25;
44
- }
45
- .brush .extent {
46
- stroke: #999;
47
- fill-opacity: .125;
48
- shape-rendering: crispEdges;
49
- }
50
- .label {
51
- white-space: nowrap;
52
- display: inline-flex;
53
- align-items: center;
54
- vertical-align: middle;
55
- padding-left: 1px;
56
- }
57
62
  </style>
58
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
59
- <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.8/d3.min.js"></script>
60
63
  <script src="flamegraph.js"></script>
61
64
  </head>
62
65
  <body>
63
66
  <div class="legend"></div>
64
- <div class="flamegraph"></div>
67
+ <div class="overview-container">
68
+ <canvas class="overview"></canvas>
69
+ <div class="overview-viewport-overlay"></div>
70
+ </div>
65
71
  <div class="info">
66
72
  <div style="float: right; text-align: right">
67
73
  <div class="samples"></div>
@@ -70,7 +76,7 @@
70
76
  <div class="frame"></div>
71
77
  <div class="file"></div>
72
78
  </div>
73
- <div class="zoom"></div>
79
+ <canvas class="flamegraph"></canvas>
74
80
  <script type="text/javascript">
75
81
  var queryDict = {}
76
82
  location.search.substr(1).split("&").forEach(function(item) {queryDict[item.split("=")[0]] = decodeURIComponent(item.split("=")[1])})
@@ -11,13 +11,20 @@ module StackProf
11
11
  Middleware.interval = options[:interval] || 1000
12
12
  Middleware.raw = options[:raw] || false
13
13
  Middleware.enabled = options[:enabled]
14
- Middleware.path = options[:path] || 'tmp'
14
+ options[:path] = 'tmp/' if options[:path].to_s.empty?
15
+ Middleware.path = options[:path]
16
+ Middleware.metadata = options[:metadata] || {}
15
17
  at_exit{ Middleware.save } if options[:save_at_exit]
16
18
  end
17
19
 
18
20
  def call(env)
19
21
  enabled = Middleware.enabled?(env)
20
- StackProf.start(mode: Middleware.mode, interval: Middleware.interval, raw: Middleware.raw) if enabled
22
+ StackProf.start(
23
+ mode: Middleware.mode,
24
+ interval: Middleware.interval,
25
+ raw: Middleware.raw,
26
+ metadata: Middleware.metadata,
27
+ ) if enabled
21
28
  @app.call(env)
22
29
  ensure
23
30
  if enabled
@@ -30,7 +37,7 @@ module StackProf
30
37
  end
31
38
 
32
39
  class << self
33
- attr_accessor :enabled, :mode, :interval, :raw, :path
40
+ attr_accessor :enabled, :mode, :interval, :raw, :path, :metadata
34
41
 
35
42
  def enabled?(env)
36
43
  if enabled.respond_to?(:call)
@@ -40,11 +47,20 @@ module StackProf
40
47
  end
41
48
  end
42
49
 
43
- def save(filename = nil)
50
+ def save
44
51
  if results = StackProf.results
45
- FileUtils.mkdir_p(Middleware.path)
46
- filename ||= "stackprof-#{results[:mode]}-#{Process.pid}-#{Time.now.to_i}.dump"
47
- File.open(File.join(Middleware.path, filename), 'wb') do |f|
52
+ path = Middleware.path
53
+ is_directory = path != path.chomp('/')
54
+
55
+ if is_directory
56
+ filename = "stackprof-#{results[:mode]}-#{Process.pid}-#{Time.now.to_i}.dump"
57
+ else
58
+ filename = File.basename(path)
59
+ path = File.dirname(path)
60
+ end
61
+
62
+ FileUtils.mkdir_p(path)
63
+ File.open(File.join(path, filename), 'wb') do |f|
48
64
  f.write Marshal.dump(results)
49
65
  end
50
66
  filename
@@ -1,8 +1,44 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pp'
2
- require 'digest/md5'
4
+ require 'digest/sha2'
5
+ require 'json'
3
6
 
4
7
  module StackProf
5
8
  class Report
9
+ MARSHAL_SIGNATURE = "\x04\x08"
10
+
11
+ class << self
12
+ def from_file(file)
13
+ if (content = IO.binread(file)).start_with?(MARSHAL_SIGNATURE)
14
+ new(Marshal.load(content))
15
+ else
16
+ from_json(JSON.parse(content))
17
+ end
18
+ end
19
+
20
+ def from_json(json)
21
+ new(parse_json(json))
22
+ end
23
+
24
+ def parse_json(json)
25
+ json.keys.each do |key|
26
+ value = json.delete(key)
27
+ from_json(value) if value.is_a?(Hash)
28
+
29
+ new_key = case key
30
+ when /\A[0-9]*\z/
31
+ key.to_i
32
+ else
33
+ key.to_sym
34
+ end
35
+
36
+ json[new_key] = value
37
+ end
38
+ json
39
+ end
40
+ end
41
+
6
42
  def initialize(data)
7
43
  @data = data
8
44
  end
@@ -16,7 +52,7 @@ module StackProf
16
52
  def normalized_frames
17
53
  id2hash = {}
18
54
  @data[:frames].each do |frame, info|
19
- id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}")
55
+ id2hash[frame.to_s] = info[:hash] = Digest::SHA256.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}")
20
56
  end
21
57
  @data[:frames].inject(Hash.new) do |hash, (frame, info)|
22
58
  info = hash[id2hash[frame.to_s]] = info.dup
@@ -38,7 +74,7 @@ module StackProf
38
74
  end
39
75
 
40
76
  def max_samples
41
- @data[:max_samples] ||= frames.max_by{ |addr, frame| frame[:samples] }.last[:samples]
77
+ @data[:max_samples] ||= @data[:frames].values.max_by{ |frame| frame[:samples] }[:samples]
42
78
  end
43
79
 
44
80
  def files
@@ -55,8 +91,8 @@ module StackProf
55
91
 
56
92
  def add_lines(a, b)
57
93
  return b if a.nil?
58
- return a+b if a.is_a? Fixnum
59
- return [ a[0], a[1]+b ] if b.is_a? Fixnum
94
+ return a+b if a.is_a? Integer
95
+ return [ a[0], a[1]+b ] if b.is_a? Integer
60
96
  [ a[0]+b[0], a[1]+b[1] ]
61
97
  end
62
98
 
@@ -68,6 +104,11 @@ module StackProf
68
104
  f.puts Marshal.dump(@data.reject{|k,v| k == :files })
69
105
  end
70
106
 
107
+ def print_json(f=STDOUT)
108
+ require "json"
109
+ f.puts JSON.generate(@data, max_nesting: false)
110
+ end
111
+
71
112
  def print_stackcollapse
72
113
  raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
73
114
 
@@ -80,18 +121,20 @@ module StackProf
80
121
  end
81
122
  end
82
123
 
83
- def print_flamegraph(f=STDOUT, skip_common=true)
124
+ def print_timeline_flamegraph(f=STDOUT, skip_common=true)
125
+ print_flamegraph(f, skip_common, false)
126
+ end
127
+
128
+ def print_alphabetical_flamegraph(f=STDOUT, skip_common=true)
129
+ print_flamegraph(f, skip_common, true)
130
+ end
131
+
132
+ def print_flamegraph(f, skip_common, alphabetical=false)
84
133
  raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
85
134
 
86
- stacks = []
87
- max_x = 0
88
- max_y = 0
89
- while len = raw.shift
90
- max_y = len if len > max_y
91
- stack = raw.slice!(0, len+1)
92
- stacks << stack
93
- max_x += stack.last
94
- end
135
+ stacks, max_x, max_y = flamegraph_stacks(raw)
136
+
137
+ stacks.sort! if alphabetical
95
138
 
96
139
  f.puts 'flamegraph(['
97
140
  max_y.times do |y|
@@ -141,13 +184,233 @@ module StackProf
141
184
  f.puts '])'
142
185
  end
143
186
 
187
+ def flamegraph_stacks(raw)
188
+ stacks = []
189
+ max_x = 0
190
+ max_y = 0
191
+ idx = 0
192
+
193
+ while len = raw[idx]
194
+ idx += 1
195
+ max_y = len if len > max_y
196
+ stack = raw.slice(idx, len+1)
197
+ idx += len+1
198
+ stacks << stack
199
+ max_x += stack.last
200
+ end
201
+
202
+ return stacks, max_x, max_y
203
+ end
204
+
144
205
  def flamegraph_row(f, x, y, weight, addr)
145
- frame = frames[addr]
206
+ frame = @data[:frames][addr]
146
207
  f.print ',' if @rows_started
147
208
  @rows_started = true
148
209
  f.puts %{{"x":#{x},"y":#{y},"width":#{weight},"frame_id":#{addr},"frame":#{frame[:name].dump},"file":#{frame[:file].dump}}}
149
210
  end
150
211
 
212
+ def convert_to_d3_flame_graph_format(name, stacks, depth)
213
+ weight = 0
214
+ children = []
215
+ stacks.chunk do |stack|
216
+ if depth == stack.length - 1
217
+ :leaf
218
+ else
219
+ stack[depth]
220
+ end
221
+ end.each do |val, child_stacks|
222
+ if val == :leaf
223
+ child_stacks.each do |stack|
224
+ weight += stack.last
225
+ end
226
+ else
227
+ frame = @data[:frames][val]
228
+ child_name = "#{ frame[:name] } : #{ frame[:file] } : #{ frame[:line] }"
229
+ child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
230
+ weight += child_data["value"]
231
+ children << child_data
232
+ end
233
+ end
234
+
235
+ {
236
+ "name" => name,
237
+ "value" => weight,
238
+ "children" => children,
239
+ }
240
+ end
241
+
242
+ def print_d3_flamegraph(f=STDOUT, skip_common=true)
243
+ raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
244
+
245
+ stacks, * = flamegraph_stacks(raw)
246
+
247
+ # d3-flame-grpah supports only alphabetical flamegraph
248
+ stacks.sort!
249
+
250
+ require "json"
251
+ json = JSON.generate(convert_to_d3_flame_graph_format("<root>", stacks, 0), max_nesting: false)
252
+
253
+ # This html code is almost copied from d3-flame-graph sample code.
254
+ # (Apache License 2.0)
255
+ # https://github.com/spiermar/d3-flame-graph/blob/gh-pages/index.html
256
+
257
+ f.print <<-END
258
+ <!DOCTYPE html>
259
+ <html lang="en">
260
+ <head>
261
+ <meta charset="utf-8">
262
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
263
+ <meta name="viewport" content="width=device-width, initial-scale=1">
264
+
265
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
266
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.css">
267
+
268
+ <style>
269
+
270
+ /* Space out content a bit */
271
+ body {
272
+ padding-top: 20px;
273
+ padding-bottom: 20px;
274
+ }
275
+
276
+ /* Custom page header */
277
+ .header {
278
+ padding-bottom: 20px;
279
+ padding-right: 15px;
280
+ padding-left: 15px;
281
+ border-bottom: 1px solid #e5e5e5;
282
+ }
283
+
284
+ /* Make the masthead heading the same height as the navigation */
285
+ .header h3 {
286
+ margin-top: 0;
287
+ margin-bottom: 0;
288
+ line-height: 40px;
289
+ }
290
+
291
+ /* Customize container */
292
+ .container {
293
+ max-width: 990px;
294
+ }
295
+
296
+ address {
297
+ text-align: right;
298
+ }
299
+ </style>
300
+
301
+ <title>stackprof (mode: #{ data[:mode] })</title>
302
+
303
+ <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
304
+ <!--[if lt IE 9]>
305
+ <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
306
+ <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
307
+ <![endif]-->
308
+ </head>
309
+ <body>
310
+ <div class="container">
311
+ <div class="header clearfix">
312
+ <nav>
313
+ <div class="pull-right">
314
+ <form class="form-inline" id="form">
315
+ <a class="btn" href="javascript: resetZoom();">Reset zoom</a>
316
+ <a class="btn" href="javascript: clear();">Clear</a>
317
+ <div class="form-group">
318
+ <input type="text" class="form-control" id="term">
319
+ </div>
320
+ <a class="btn btn-primary" href="javascript: search();">Search</a>
321
+ </form>
322
+ </div>
323
+ </nav>
324
+ <h3 class="text-muted">stackprof (mode: #{ data[:mode] })</h3>
325
+ </div>
326
+ <div id="chart">
327
+ </div>
328
+ <address>
329
+ powered by <a href="https://github.com/spiermar/d3-flame-graph">d3-flame-graph</a>
330
+ </address>
331
+ <hr>
332
+ <div id="details">
333
+ </div>
334
+ </div>
335
+
336
+ <!-- D3.js -->
337
+ <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
338
+
339
+ <!-- d3-tip -->
340
+ <script type="text/javascript" src=https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js></script>
341
+
342
+ <!-- d3-flamegraph -->
343
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.min.js"></script>
344
+
345
+ <script type="text/javascript">
346
+ var flameGraph = d3.flamegraph()
347
+ .width(960)
348
+ .cellHeight(18)
349
+ .transitionDuration(750)
350
+ .minFrameSize(5)
351
+ .transitionEase(d3.easeCubic)
352
+ .sort(true)
353
+ //Example to sort in reverse order
354
+ //.sort(function(a,b){ return d3.descending(a.name, b.name);})
355
+ .title("")
356
+ .onClick(onClick)
357
+ .differential(false)
358
+ .selfValue(false);
359
+
360
+
361
+ // Example on how to use custom tooltips using d3-tip.
362
+ // var tip = d3.tip()
363
+ // .direction("s")
364
+ // .offset([8, 0])
365
+ // .attr('class', 'd3-flame-graph-tip')
366
+ // .html(function(d) { return "name: " + d.data.name + ", value: " + d.data.value; });
367
+
368
+ // flameGraph.tooltip(tip);
369
+
370
+ var details = document.getElementById("details");
371
+ flameGraph.setDetailsElement(details);
372
+
373
+ // Example on how to use custom labels
374
+ // var label = function(d) {
375
+ // return "name: " + d.name + ", value: " + d.value;
376
+ // }
377
+ // flameGraph.label(label);
378
+
379
+ // Example of how to set fixed chart height
380
+ // flameGraph.height(540);
381
+
382
+ d3.select("#chart")
383
+ .datum(#{ json })
384
+ .call(flameGraph);
385
+
386
+ document.getElementById("form").addEventListener("submit", function(event){
387
+ event.preventDefault();
388
+ search();
389
+ });
390
+
391
+ function search() {
392
+ var term = document.getElementById("term").value;
393
+ flameGraph.search(term);
394
+ }
395
+
396
+ function clear() {
397
+ document.getElementById('term').value = '';
398
+ flameGraph.clear();
399
+ }
400
+
401
+ function resetZoom() {
402
+ flameGraph.resetZoom();
403
+ }
404
+
405
+ function onClick(d) {
406
+ console.info("Clicked on " + d.data.name);
407
+ }
408
+ </script>
409
+ </body>
410
+ </html>
411
+ END
412
+ end
413
+
151
414
  def print_graphviz(options = {}, f = STDOUT)
152
415
  if filter = options[:filter]
153
416
  mark_stack = []
@@ -185,7 +448,7 @@ module StackProf
185
448
  call, total = info.values_at(:samples, :total_samples)
186
449
  break if total < node_minimum || (limit && index >= limit)
187
450
 
188
- sample = ''
451
+ sample = ''.dup
189
452
  sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total
190
453
  sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples)
191
454
  fontsize = (1.0 * call / max_samples) * 28 + 10
@@ -293,6 +556,47 @@ module StackProf
293
556
  end
294
557
  end
295
558
 
559
+ # Walk up and down the stack from a given starting point (name). Loops
560
+ # until `:exit` is selected
561
+ def walk_method(name)
562
+ method_choice = /#{Regexp.escape name}/
563
+ invalid_choice = false
564
+
565
+ # Continue walking up and down the stack until the users selects "exit"
566
+ while method_choice != :exit
567
+ print_method method_choice unless invalid_choice
568
+ STDOUT.puts "\n\n"
569
+
570
+ # Determine callers and callees for the current frame
571
+ new_frames = frames.select {|_, info| info[:name] =~ method_choice }
572
+ new_choices = new_frames.map {|frame, info| [
573
+ callers_for(frame).sort_by(&:last).reverse.map(&:first),
574
+ (info[:edges] || []).map{ |k, w| [data[:frames][k][:name], w] }.sort_by{ |k,v| -v }.map(&:first)
575
+ ]}.flatten + [:exit]
576
+
577
+ # Print callers and callees for selection
578
+ STDOUT.puts "Select next method:"
579
+ new_choices.each_with_index do |method, index|
580
+ STDOUT.printf "%2d) %s\n", index + 1, method.to_s
581
+ end
582
+
583
+ # Pick selection
584
+ STDOUT.printf "> "
585
+ selection = STDIN.gets.chomp.to_i - 1
586
+ STDOUT.puts "\n\n\n"
587
+
588
+ # Determine if it was a valid choice
589
+ # (if not, don't re-run .print_method)
590
+ if new_choice = new_choices[selection]
591
+ invalid_choice = false
592
+ method_choice = new_choice == :exit ? :exit : %r/^#{Regexp.escape new_choice}$/
593
+ else
594
+ invalid_choice = true
595
+ STDOUT.puts "Invalid choice. Please select again..."
596
+ end
597
+ end
598
+ end
599
+
296
600
  def print_files(sort_by_total=false, limit=nil, f = STDOUT)
297
601
  list = files.map{ |file, vals| [file, vals.values.inject([0,0]){ |sum, n| add_lines(sum, n) }] }
298
602
  list = list.sort_by{ |file, samples| -samples[1] }
@@ -388,7 +692,8 @@ module StackProf
388
692
  end
389
693
  end
390
694
  end
695
+ rescue SystemCallError
696
+ f.puts " SOURCE UNAVAILABLE"
391
697
  end
392
-
393
698
  end
394
699
  end
@@ -0,0 +1,37 @@
1
+ module StackProf
2
+ # Define the same methods as stackprof.c
3
+ class << self
4
+ def running?
5
+ false
6
+ end
7
+
8
+ def run(*args)
9
+ unimplemented
10
+ end
11
+
12
+ def start(*args)
13
+ unimplemented
14
+ end
15
+
16
+ def stop
17
+ unimplemented
18
+ end
19
+
20
+ def results(*args)
21
+ unimplemented
22
+ end
23
+
24
+ def sample
25
+ unimplemented
26
+ end
27
+
28
+ def use_postponed_job!
29
+ # noop
30
+ end
31
+
32
+ private def unimplemented
33
+ raise "Use --cpusampler=flamegraph or --cpusampler instead of StackProf on TruffleRuby.\n" \
34
+ "See https://www.graalvm.org/tools/profiling/ and `ruby --help:cpusampler` for more details."
35
+ end
36
+ end
37
+ end
data/lib/stackprof.rb CHANGED
@@ -1,4 +1,21 @@
1
- require "stackprof/stackprof"
1
+ if RUBY_ENGINE == 'truffleruby'
2
+ require "stackprof/truffleruby"
3
+ else
4
+ require "stackprof/stackprof"
5
+ end
6
+
7
+ if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
8
+ StackProf.use_postponed_job!
9
+ elsif RUBY_VERSION == "3.2.0"
10
+ # 3.2.0 crash is the signal is received at the wrong time.
11
+ # Fixed in https://github.com/ruby/ruby/pull/7116
12
+ # The fix is backported in 3.2.1: https://bugs.ruby-lang.org/issues/19336
13
+ StackProf.use_postponed_job!
14
+ end
15
+
16
+ module StackProf
17
+ VERSION = '0.2.25'
18
+ end
2
19
 
3
20
  StackProf.autoload :Report, "stackprof/report.rb"
4
21
  StackProf.autoload :Middleware, "stackprof/middleware.rb"
data/sample.rb CHANGED
@@ -24,9 +24,9 @@ class A
24
24
  end
25
25
  end
26
26
 
27
- #profile = StackProf.run(:object, 1) do
28
- #profile = StackProf.run(:wall, 1000) do
29
- profile = StackProf.run(:cpu, 1000) do
27
+ #profile = StackProf.run(mode: :object, interval: 1) do
28
+ #profile = StackProf.run(mode: :wall, interval: 1000) do
29
+ profile = StackProf.run(mode: :cpu, interval: 1000) do
30
30
  1_000_000.times do
31
31
  A.new
32
32
  end
data/stackprof.gemspec CHANGED
@@ -1,11 +1,18 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'stackprof'
3
- s.version = '0.2.10'
3
+ s.version = '0.2.25'
4
4
  s.homepage = 'http://github.com/tmm1/stackprof'
5
5
 
6
6
  s.authors = 'Aman Gupta'
7
7
  s.email = 'aman@tmm1.net'
8
8
 
9
+ s.metadata = {
10
+ 'bug_tracker_uri' => 'https://github.com/tmm1/stackprof/issues',
11
+ 'changelog_uri' => "https://github.com/tmm1/stackprof/blob/v#{s.version}/CHANGELOG.md",
12
+ 'documentation_uri' => "https://www.rubydoc.info/gems/stackprof/#{s.version}",
13
+ 'source_code_uri' => "https://github.com/tmm1/stackprof/tree/v#{s.version}"
14
+ }
15
+
9
16
  s.files = `git ls-files`.split("\n")
10
17
  s.extensions = 'ext/stackprof/extconf.rb'
11
18
 
@@ -14,9 +21,11 @@ Gem::Specification.new do |s|
14
21
  s.executables << 'stackprof-flamegraph.pl'
15
22
  s.executables << 'stackprof-gprof2dot.py'
16
23
 
17
- s.summary = 'sampling callstack-profiler for ruby 2.1+'
24
+ s.summary = 'sampling callstack-profiler for ruby 2.2+'
18
25
  s.description = 'stackprof is a fast sampling profiler for ruby code, with cpu, wallclock and object allocation samplers.'
19
26
 
27
+ s.required_ruby_version = '>= 2.2'
28
+
20
29
  s.license = 'MIT'
21
30
 
22
31
  s.add_development_dependency 'rake-compiler', '~> 0.9'
@@ -0,0 +1 @@
1
+ {: modeI"cpu:ET
@@ -0,0 +1 @@
1
+ { "mode": "cpu" }