stackflame 0.0.1
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 +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +28 -0
- data/Rakefile +2 -0
- data/example/ar_profile.rb +23 -0
- data/lib/stackflame/version.rb +3 -0
- data/lib/stackflame.rb +43 -0
- data/stackflame.gemspec +28 -0
- data/vendor/flamegraph.js +357 -0
- data/vendor/viewer.html +85 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1b3079b183df5c34aa2346423f760c4f46447cd7
|
4
|
+
data.tar.gz: fd0ff8bde7e03b081eb2436c7465928a5b39e084
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cc074f970f4423ce0d9cbc1f59832836e70b4c51b4740c8abb7c99d010f4eb0b52bd192fb772bcd33780a89ab530ec8cd58c915b5ae96a2c8e1c6b65ee2d95f7
|
7
|
+
data.tar.gz: b85c35082cb6ef953ca518d2033199ddb12979e82b63d81d5e0ebdbbbf51abe3e9050d14dac08f635c68cee2b5d4233ebaf8a847c7e434d93647c7177a4eb05f
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Takashi Kokubun
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Stackflame
|
2
|
+
|
3
|
+
Stackflame provides a simple API to deal with Flamegraph of [stackprof](https://github.com/tmm1/stackprof).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
$ gem install stackflame
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require "stackflame"
|
15
|
+
|
16
|
+
Stackflame.profile do
|
17
|
+
# some code to profile
|
18
|
+
100.times { User.create }
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
If you use OSX, flamegraph will be opened with your default browser.
|
23
|
+
|
24
|
+

|
25
|
+
|
26
|
+
## License
|
27
|
+
|
28
|
+
MIT License
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "yaml"
|
3
|
+
require "stackflame"
|
4
|
+
|
5
|
+
Stackflame.profile do
|
6
|
+
ActiveRecord::Base.configurations["profile"] = {
|
7
|
+
adapter: "sqlite3",
|
8
|
+
database: "test.sqlite3",
|
9
|
+
pool: 5,
|
10
|
+
timeout: 5000,
|
11
|
+
}
|
12
|
+
ActiveRecord::Base.establish_connection :profile
|
13
|
+
|
14
|
+
ActiveRecord::Schema.define do
|
15
|
+
create_table :users, force: true do |t|
|
16
|
+
t.column :created_at, :datetime
|
17
|
+
t.column :updated_at, :datetime
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class User < ActiveRecord::Base; end
|
22
|
+
100.times { User.create }
|
23
|
+
end
|
data/lib/stackflame.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "stackflame/version"
|
2
|
+
require "stackprof"
|
3
|
+
|
4
|
+
class Stackflame
|
5
|
+
def self.profile(mode: :cpu, &block)
|
6
|
+
stackflame = self.new
|
7
|
+
stackflame.run(mode, &block)
|
8
|
+
stackflame.open_flamegraph
|
9
|
+
end
|
10
|
+
|
11
|
+
def run(mode, &block)
|
12
|
+
result = StackProf.run(mode: mode, raw: true, &block)
|
13
|
+
|
14
|
+
File.open(temp_js_path, 'w') do |f|
|
15
|
+
StackProf::Report.new(result).print_flamegraph(f)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def open_flamegraph
|
20
|
+
if system("which osascript > /dev/null")
|
21
|
+
# NOTE: `open` can't open path with query string
|
22
|
+
`osascript -e 'open location "#{flamegraph_path}"'`
|
23
|
+
else
|
24
|
+
puts "This OS is not supported. Please open: #{flamegraph_path}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def flamegraph_path
|
31
|
+
viewer_path = File.expand_path('../../vendor/viewer.html', __FILE__)
|
32
|
+
"file://#{viewer_path}?data=#{temp_js_path}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def temp_js_path
|
36
|
+
return @js_path if @js_path
|
37
|
+
|
38
|
+
temp = `mktemp /tmp/stackflame-XXXXXXXX`.strip
|
39
|
+
@js_path = "#{temp}.js"
|
40
|
+
`mv #{temp} #{@js_path}`
|
41
|
+
@js_path
|
42
|
+
end
|
43
|
+
end
|
data/stackflame.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'stackflame/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "stackflame"
|
8
|
+
spec.version = Stackflame::VERSION
|
9
|
+
spec.authors = ["Takashi Kokubun"]
|
10
|
+
spec.email = ["takashikkbn@gmail.com"]
|
11
|
+
spec.summary = %q{Stackflame provides a simple API to deal with Flamegraph of stackprof.}
|
12
|
+
spec.description = %q{Stackflame provides a simple API to deal with Flamegraph of stackprof.}
|
13
|
+
spec.homepage = "https://github.com/k0kubun/stackflame"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "stackprof", "~> 0.2.7"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "activerecord", "~> 4.1"
|
26
|
+
spec.add_development_dependency "sqlite3"
|
27
|
+
spec.add_development_dependency "pry"
|
28
|
+
end
|
@@ -0,0 +1,357 @@
|
|
1
|
+
var guessGem = function(frame) {
|
2
|
+
var split = frame.split('/gems/');
|
3
|
+
if(split.length == 1) {
|
4
|
+
split = frame.split('/app/');
|
5
|
+
if(split.length == 1) {
|
6
|
+
split = frame.split('/lib/');
|
7
|
+
} else {
|
8
|
+
return split[split.length-1].split('/')[0]
|
9
|
+
}
|
10
|
+
|
11
|
+
split = split[Math.max(split.length-2,0)].split('/');
|
12
|
+
return split[split.length-1].split(':')[0];
|
13
|
+
}
|
14
|
+
else
|
15
|
+
{
|
16
|
+
return split[split.length -1].split('/')[0].split('-', 2)[0];
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
var color = function() {
|
21
|
+
var r = parseInt(205 + Math.random() * 50);
|
22
|
+
var g = parseInt(Math.random() * 230);
|
23
|
+
var b = parseInt(Math.random() * 55);
|
24
|
+
return "rgb(" + r + "," + g + "," + b + ")";
|
25
|
+
}
|
26
|
+
|
27
|
+
// http://stackoverflow.com/a/7419630
|
28
|
+
var rainbow = function(numOfSteps, step) {
|
29
|
+
// This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distiguishable vibrant markers in Google Maps and other apps.
|
30
|
+
// Adam Cole, 2011-Sept-14
|
31
|
+
// HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
|
32
|
+
var r, g, b;
|
33
|
+
var h = step / numOfSteps;
|
34
|
+
var i = ~~(h * 6);
|
35
|
+
var f = h * 6 - i;
|
36
|
+
var q = 1 - f;
|
37
|
+
switch(i % 6){
|
38
|
+
case 0: r = 1, g = f, b = 0; break;
|
39
|
+
case 1: r = q, g = 1, b = 0; break;
|
40
|
+
case 2: r = 0, g = 1, b = f; break;
|
41
|
+
case 3: r = 0, g = q, b = 1; break;
|
42
|
+
case 4: r = f, g = 0, b = 1; break;
|
43
|
+
case 5: r = 1, g = 0, b = q; break;
|
44
|
+
}
|
45
|
+
var c = "#" + ("00" + (~ ~(r * 255)).toString(16)).slice(-2) + ("00" + (~ ~(g * 255)).toString(16)).slice(-2) + ("00" + (~ ~(b * 255)).toString(16)).slice(-2);
|
46
|
+
return (c);
|
47
|
+
}
|
48
|
+
|
49
|
+
// http://stackoverflow.com/questions/1960473/unique-values-in-an-array
|
50
|
+
var getUnique = function(orig) {
|
51
|
+
var o = {}, a = []
|
52
|
+
for (var i = 0; i < orig.length; i++) o[orig[i]] = 1
|
53
|
+
for (var e in o) a.push(e)
|
54
|
+
return a
|
55
|
+
}
|
56
|
+
|
57
|
+
function flamegraph(data) {
|
58
|
+
var maxX = 0;
|
59
|
+
var maxY = 0;
|
60
|
+
var minY = 10000;
|
61
|
+
$.each(data, function(){
|
62
|
+
maxX = Math.max(maxX, this.x + this.width);
|
63
|
+
maxY = Math.max(maxY, this.y);
|
64
|
+
minY = Math.min(minY, this.y);
|
65
|
+
});
|
66
|
+
|
67
|
+
// normalize Y
|
68
|
+
if (minY > 0) {
|
69
|
+
$.each(data, function(){
|
70
|
+
this.y -= minY
|
71
|
+
})
|
72
|
+
maxY -= minY
|
73
|
+
minY = 0
|
74
|
+
}
|
75
|
+
|
76
|
+
var margin = {top: 10, right: 10, bottom: 10, left: 10}
|
77
|
+
var width = $(window).width() - 200 - margin.left - margin.right;
|
78
|
+
var height = $(window).height() * 0.70 - margin.top - margin.bottom;
|
79
|
+
var height2 = $(window).height() * 0.30 - 60 - margin.top - margin.bottom;
|
80
|
+
|
81
|
+
$('.flamegraph').width(width + margin.left + margin.right).height(height + margin.top + margin.bottom);
|
82
|
+
$('.zoom').width(width + margin.left + margin.right).height(height2 + margin.top + margin.bottom);
|
83
|
+
|
84
|
+
var xScale = d3.scale.linear()
|
85
|
+
.domain([0, maxX])
|
86
|
+
.range([0, width]);
|
87
|
+
|
88
|
+
var xScale2 = d3.scale.linear()
|
89
|
+
.domain([0, maxX])
|
90
|
+
.range([0, width])
|
91
|
+
|
92
|
+
var yScale = d3.scale.linear()
|
93
|
+
.domain([0, maxY])
|
94
|
+
.range([0,height]);
|
95
|
+
|
96
|
+
var yScale2 = d3.scale.linear()
|
97
|
+
.domain([0, maxY])
|
98
|
+
.range([0,height2]);
|
99
|
+
|
100
|
+
var zoomXRatio = 1
|
101
|
+
var zoomed = function() {
|
102
|
+
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + (zoomXRatio*d3.event.scale) + "," + d3.event.scale + ")");
|
103
|
+
|
104
|
+
var x = xScale.domain(), y = yScale.domain()
|
105
|
+
brush.extent([ [x[0]/zoomXRatio, y[0]], [x[1]/zoomXRatio, y[1]] ])
|
106
|
+
if (x[1] == maxX && y[1] == maxY)
|
107
|
+
brush.clear()
|
108
|
+
svg2.select('g.brush').call(brush)
|
109
|
+
}
|
110
|
+
|
111
|
+
var zoom = d3.behavior.zoom().x(xScale).y(yScale).scaleExtent([1, 14]).on('zoom', zoomed)
|
112
|
+
|
113
|
+
var svg2 = d3.select('.zoom').append('svg').attr('width', '100%').attr('height', '100%').append('svg:g')
|
114
|
+
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
115
|
+
.append('g').attr('class', 'graph')
|
116
|
+
|
117
|
+
var svg = d3.select(".flamegraph")
|
118
|
+
.append("svg")
|
119
|
+
.attr("width", "100%")
|
120
|
+
.attr("height", "100%")
|
121
|
+
.attr("pointer-events", "all")
|
122
|
+
.append('svg:g')
|
123
|
+
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
124
|
+
.call(zoom)
|
125
|
+
.append('svg:g').attr('class', 'graph');
|
126
|
+
|
127
|
+
// so zoom works everywhere
|
128
|
+
svg.append("rect")
|
129
|
+
.attr("x",function(d) { return xScale(0); })
|
130
|
+
.attr("y",function(d) { return yScale(0);})
|
131
|
+
.attr("width", function(d){return xScale(maxX);})
|
132
|
+
.attr("height", yScale(maxY))
|
133
|
+
.attr("fill", "white");
|
134
|
+
|
135
|
+
var samplePercentRaw = function(samples, exclusive) {
|
136
|
+
var ret = [samples, ((samples / maxX) * 100).toFixed(2)]
|
137
|
+
if (exclusive)
|
138
|
+
ret = ret.concat([exclusive, ((exclusive / maxX) * 100).toFixed(2)])
|
139
|
+
return ret;
|
140
|
+
}
|
141
|
+
|
142
|
+
var samplePercent = function(samples, exclusive) {
|
143
|
+
var info = samplePercentRaw(samples, exclusive)
|
144
|
+
var samplesPct = info[1], exclusivePct = info[3]
|
145
|
+
var ret = " (" + samples + " sample" + (samples == 1 ? "" : "s") + " - " + samplesPct + "%) ";
|
146
|
+
if (exclusive)
|
147
|
+
ret += " (" + exclusive + " exclusive - " + exclusivePct + "%) ";
|
148
|
+
return ret;
|
149
|
+
}
|
150
|
+
|
151
|
+
var info = {};
|
152
|
+
|
153
|
+
var mouseover = function(d) {
|
154
|
+
var i = info[d.frame_id];
|
155
|
+
var shortFile = d.file.replace(/^.+\/(gems|app|lib|config|jobs)/, '$1')
|
156
|
+
var data = samplePercentRaw(i.samples.length, d.topFrame ? d.topFrame.exclusiveCount : 0)
|
157
|
+
|
158
|
+
$('.info')
|
159
|
+
.css('background-color', i.color)
|
160
|
+
.find('.frame').text(d.frame).end()
|
161
|
+
.find('.file').text(shortFile).end()
|
162
|
+
.find('.samples').text(data[0] + ' samples ('+data[1]+'%)').end()
|
163
|
+
.find('.exclusive').text('')
|
164
|
+
|
165
|
+
if (data[3])
|
166
|
+
$('.info .exclusive').text(data[2] + ' exclusive ('+data[3]+'%)')
|
167
|
+
|
168
|
+
d3.selectAll(i.nodes)
|
169
|
+
.attr('opacity',0.5);
|
170
|
+
};
|
171
|
+
|
172
|
+
var mouseout = function(d) {
|
173
|
+
var i = info[d.frame_id];
|
174
|
+
$('.info').css('background-color', 'none').find('.frame, .file, .samples, .exclusive').text('')
|
175
|
+
|
176
|
+
d3.selectAll(i.nodes)
|
177
|
+
.attr('opacity',1);
|
178
|
+
};
|
179
|
+
|
180
|
+
// assign some colors, analyze samples per gem
|
181
|
+
var gemStats = {}
|
182
|
+
var topFrames = {}
|
183
|
+
var lastFrame = {frame: 'd52e04d-df28-41ed-a215-b6ec840a8ea5', x: -1}
|
184
|
+
|
185
|
+
$.each(data, function(){
|
186
|
+
var gem = guessGem(this.file);
|
187
|
+
var stat = gemStats[gem];
|
188
|
+
this.gemName = gem
|
189
|
+
|
190
|
+
if(!stat) {
|
191
|
+
gemStats[gem] = stat = {name: gem, samples: [], frames: [], nodes:[]};
|
192
|
+
}
|
193
|
+
|
194
|
+
stat.frames.push(this.frame_id);
|
195
|
+
for(var j=0; j < this.width; j++){
|
196
|
+
stat.samples.push(this.x + j);
|
197
|
+
}
|
198
|
+
// This assumes the traversal is in order
|
199
|
+
if (lastFrame.x != this.x) {
|
200
|
+
var topFrame = topFrames[lastFrame.frame_id]
|
201
|
+
if (!topFrame) {
|
202
|
+
topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0}
|
203
|
+
}
|
204
|
+
topFrame.exclusiveCount += 1;
|
205
|
+
lastFrame.topFrame = topFrame;
|
206
|
+
}
|
207
|
+
lastFrame = this;
|
208
|
+
|
209
|
+
});
|
210
|
+
|
211
|
+
var topFrame = topFrames[lastFrame.frame_id]
|
212
|
+
if (!topFrame) {
|
213
|
+
topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0}
|
214
|
+
}
|
215
|
+
topFrame.exclusiveCount += 1;
|
216
|
+
lastFrame.topFrame = topFrame;
|
217
|
+
|
218
|
+
var totalGems = 0;
|
219
|
+
$.each(gemStats, function(k,stat){
|
220
|
+
totalGems++;
|
221
|
+
stat.samples = getUnique(stat.samples);
|
222
|
+
});
|
223
|
+
|
224
|
+
var gemsSorted = $.map(gemStats, function(v, k){ return v })
|
225
|
+
gemsSorted.sort(function(a, b){ return b.samples.length - a.samples.length })
|
226
|
+
|
227
|
+
var currentIndex = 0;
|
228
|
+
$.each(gemsSorted, function(k,stat){
|
229
|
+
stat.color = rainbow(totalGems, currentIndex);
|
230
|
+
currentIndex += 1;
|
231
|
+
|
232
|
+
for(var x=0; x < stat.frames.length; x++) {
|
233
|
+
info[stat.frames[x]] = {nodes: [], samples: [], color: stat.color};
|
234
|
+
}
|
235
|
+
});
|
236
|
+
|
237
|
+
function drawData(svg, data, xScale, yScale, mini) {
|
238
|
+
svg.selectAll("g.flames")
|
239
|
+
.data(data)
|
240
|
+
.enter()
|
241
|
+
.append("g")
|
242
|
+
.attr('class', 'flames')
|
243
|
+
.each(function(d){
|
244
|
+
gemStats[d.gemName].nodes.push(this)
|
245
|
+
|
246
|
+
var r = d3.select(this)
|
247
|
+
.append("rect")
|
248
|
+
.attr("x",function(d) { return xScale(d.x); })
|
249
|
+
.attr("y",function(d) { return yScale(maxY - d.y);})
|
250
|
+
.attr("width", function(d){return xScale(d.width);})
|
251
|
+
.attr("height", yScale(1))
|
252
|
+
.attr("fill", function(d){
|
253
|
+
var i = info[d.frame_id];
|
254
|
+
if(!i) {
|
255
|
+
info[d.frame_id] = i = {nodes: [], samples: [], color: color()};
|
256
|
+
}
|
257
|
+
i.nodes.push(this);
|
258
|
+
if (!mini)
|
259
|
+
for(var j=0; j < d.width; j++){
|
260
|
+
i.samples.push(d.x + j);
|
261
|
+
}
|
262
|
+
return i.color;
|
263
|
+
})
|
264
|
+
|
265
|
+
if (!mini)
|
266
|
+
r
|
267
|
+
.on("mouseover", mouseover)
|
268
|
+
.on("mouseout", mouseout);
|
269
|
+
|
270
|
+
if (!mini)
|
271
|
+
d3.select(this)
|
272
|
+
.append('foreignObject')
|
273
|
+
.classed('label-body', true)
|
274
|
+
.attr("x",function(d) { return xScale(d.x); })
|
275
|
+
.attr("y",function(d) { return yScale(maxY - d.y);})
|
276
|
+
.attr("width", function(d){return xScale(d.width);})
|
277
|
+
.attr("height", yScale(1))
|
278
|
+
.attr("line-height", yScale(1))
|
279
|
+
.attr("font-size", yScale(0.42) + 'px')
|
280
|
+
.attr('pointer-events', 'none')
|
281
|
+
.append('xhtml:span')
|
282
|
+
.style("height", yScale(1))
|
283
|
+
.classed('label', true)
|
284
|
+
.text(function(d){ return d.frame })
|
285
|
+
});
|
286
|
+
}
|
287
|
+
|
288
|
+
drawData(svg, data, xScale, yScale, 0)
|
289
|
+
drawData(svg2, data, xScale2, yScale2, 1)
|
290
|
+
|
291
|
+
var brushed = function(){
|
292
|
+
if (brush.empty()) {
|
293
|
+
svg.attr('transform', '')
|
294
|
+
zoomXRatio = 1
|
295
|
+
zoom.scale(1).translate([0,0])
|
296
|
+
svg.selectAll('.label-body')
|
297
|
+
.attr('transform', 'scale(1,1)')
|
298
|
+
.attr("x",function(d) { return xScale(d.x)*zoomXRatio; })
|
299
|
+
.attr("width", function(d){return xScale(d.width)*zoomXRatio;})
|
300
|
+
} else {
|
301
|
+
var e = brush.extent()
|
302
|
+
var x = [e[0][0],e[1][0]], y = [e[0][1],e[1][1]]
|
303
|
+
|
304
|
+
xScale.domain([0, maxX])
|
305
|
+
yScale.domain([0, maxY])
|
306
|
+
|
307
|
+
var w = width, h = height2
|
308
|
+
var dx = xScale2(1.0*x[1]-x[0]), dy = yScale2(1.0*y[1]-y[0])
|
309
|
+
var sx = w/dx, sy = h/dy
|
310
|
+
var trlx = -xScale(x[0])*sx, trly = -yScale(y[0])*sy
|
311
|
+
var transform = "translate(" + trlx + ',' + trly + ")" + " scale(" + sx + ',' + sy + ")"
|
312
|
+
|
313
|
+
zoomXRatio = sx/sy
|
314
|
+
|
315
|
+
svg.selectAll('.label-body')
|
316
|
+
.attr("x",function(d) { return xScale(d.x)*zoomXRatio; })
|
317
|
+
.attr("width", function(d){return xScale(d.width)*zoomXRatio;})
|
318
|
+
.attr('transform', function(d){
|
319
|
+
var x = xScale(d.x)
|
320
|
+
return "scale("+(1.0/zoomXRatio)+",1)"
|
321
|
+
})
|
322
|
+
|
323
|
+
svg.attr("transform", transform)
|
324
|
+
zoom.translate([trlx, trly]).scale(sy)
|
325
|
+
}
|
326
|
+
}
|
327
|
+
|
328
|
+
var brush = d3.svg.brush()
|
329
|
+
.x(xScale2)
|
330
|
+
.y(yScale2)
|
331
|
+
.on("brush", brushed);
|
332
|
+
|
333
|
+
svg2.append("g")
|
334
|
+
.attr("class", "brush")
|
335
|
+
.call(brush)
|
336
|
+
|
337
|
+
// Samples may overlap on the same line
|
338
|
+
for (var r in info) {
|
339
|
+
if (info[r].samples) {
|
340
|
+
info[r].samples = getUnique(info[r].samples);
|
341
|
+
}
|
342
|
+
};
|
343
|
+
|
344
|
+
// render the legend
|
345
|
+
$.each(gemsSorted, function(k,gem){
|
346
|
+
var data = samplePercentRaw(gem.samples.length)
|
347
|
+
var node = $("<div class='"+gem.name+"'></div>")
|
348
|
+
.css("background-color", gem.color)
|
349
|
+
.html("<span style='float: right'>" + data[0] + 'x<br>' + data[1] + '%' + '</span>' + '<div class="name">'+gem.name+'<br> </div>');
|
350
|
+
|
351
|
+
node.on('mouseenter mouseleave', function(e){
|
352
|
+
d3.selectAll(gemStats[gem.name].nodes).classed('highlighted', e.type == 'mouseenter')
|
353
|
+
})
|
354
|
+
|
355
|
+
$('.legend').append(node);
|
356
|
+
});
|
357
|
+
}
|
data/vendor/viewer.html
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>flamegraph</title>
|
4
|
+
<style>
|
5
|
+
body {
|
6
|
+
margin: 0;
|
7
|
+
padding: 0;
|
8
|
+
font-family: Monaco, "Liberation Mono", Courier, monospace;
|
9
|
+
font-size: 10pt;
|
10
|
+
}
|
11
|
+
.info {
|
12
|
+
display: block;
|
13
|
+
height: 40px;
|
14
|
+
margin: 3px 6px;
|
15
|
+
margin-right: 206px;
|
16
|
+
padding: 3px 6px;
|
17
|
+
line-height: 18px;
|
18
|
+
}
|
19
|
+
.legend {
|
20
|
+
display: block;
|
21
|
+
float: right;
|
22
|
+
width: 195px;
|
23
|
+
max-height: 100%;
|
24
|
+
overflow-y: scroll;
|
25
|
+
}
|
26
|
+
.legend > div {
|
27
|
+
padding: 6px;
|
28
|
+
clear: right;
|
29
|
+
}
|
30
|
+
.legend > div span {
|
31
|
+
opacity: 0.75;
|
32
|
+
display: block;
|
33
|
+
text-align: right;
|
34
|
+
}
|
35
|
+
.legend > div .name {
|
36
|
+
max-width: 70%;
|
37
|
+
word-wrap: break-word;
|
38
|
+
}
|
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
|
+
</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
|
+
<script src="flamegraph.js"></script>
|
61
|
+
</head>
|
62
|
+
<body>
|
63
|
+
<div class="legend"></div>
|
64
|
+
<div class="flamegraph"></div>
|
65
|
+
<div class="info">
|
66
|
+
<div style="float: right; text-align: right">
|
67
|
+
<div class="samples"></div>
|
68
|
+
<div class="exclusive"></div>
|
69
|
+
</div>
|
70
|
+
<div class="frame"></div>
|
71
|
+
<div class="file"></div>
|
72
|
+
</div>
|
73
|
+
<div class="zoom"></div>
|
74
|
+
<script type="text/javascript">
|
75
|
+
var queryDict = {}
|
76
|
+
location.search.substr(1).split("&").forEach(function(item) {queryDict[item.split("=")[0]] = decodeURIComponent(item.split("=")[1])})
|
77
|
+
|
78
|
+
if (queryDict.data) {
|
79
|
+
s = document.createElement('script')
|
80
|
+
s.setAttribute('src', queryDict.data)
|
81
|
+
document.body.appendChild(s)
|
82
|
+
}
|
83
|
+
</script>
|
84
|
+
</body>
|
85
|
+
</html>
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stackflame
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Takashi Kokubun
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: stackprof
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.7
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.7
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activerecord
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Stackflame provides a simple API to deal with Flamegraph of stackprof.
|
98
|
+
email:
|
99
|
+
- takashikkbn@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- Gemfile
|
106
|
+
- LICENSE.txt
|
107
|
+
- README.md
|
108
|
+
- Rakefile
|
109
|
+
- example/ar_profile.rb
|
110
|
+
- lib/stackflame.rb
|
111
|
+
- lib/stackflame/version.rb
|
112
|
+
- stackflame.gemspec
|
113
|
+
- vendor/flamegraph.js
|
114
|
+
- vendor/viewer.html
|
115
|
+
homepage: https://github.com/k0kubun/stackflame
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
metadata: {}
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 2.2.2
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: Stackflame provides a simple API to deal with Flamegraph of stackprof.
|
139
|
+
test_files: []
|