stackprof-webnav 0.0.2 → 1.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 +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +2 -0
- data/Procfile +1 -0
- data/README.md +27 -16
- data/Rakefile +24 -0
- data/bin/stackprof-webnav +8 -10
- data/lib/stackprof-webnav/dump.rb +41 -0
- data/lib/stackprof-webnav/presenter.rb +2 -1
- data/lib/stackprof-webnav/public/css/application.css +7 -0
- data/lib/stackprof-webnav/{css → public/css}/code.css +0 -0
- data/lib/stackprof-webnav/{css → public/css}/foundation.min.css +0 -0
- data/lib/stackprof-webnav/{css → public/css}/normalize.css +0 -0
- data/lib/stackprof-webnav/public/flamegraph.js +983 -0
- data/lib/stackprof-webnav/server.rb +109 -35
- data/lib/stackprof-webnav/version.rb +1 -1
- data/lib/stackprof-webnav/views/error.haml +8 -0
- data/lib/stackprof-webnav/views/file.haml +7 -4
- data/lib/stackprof-webnav/views/flamegraph.haml +77 -0
- data/lib/stackprof-webnav/views/graph.haml +4 -0
- data/lib/stackprof-webnav/views/index.haml +18 -0
- data/lib/stackprof-webnav/views/invalid_dump.haml +4 -0
- data/lib/stackprof-webnav/views/layout.haml +5 -4
- data/lib/stackprof-webnav/views/method.haml +11 -8
- data/lib/stackprof-webnav/views/overview.haml +17 -3
- data/screenshots/callgraph.png +0 -0
- data/screenshots/directory.png +0 -0
- data/screenshots/file.png +0 -0
- data/screenshots/flamegraph.png +0 -0
- data/screenshots/method.png +0 -0
- data/screenshots/overview.png +0 -0
- data/spec/fixtures/test-raw.dump +0 -0
- data/spec/fixtures/test.dump +0 -0
- data/spec/helpers.rb +17 -0
- data/spec/integration_spec.rb +79 -0
- data/spec/spec_helper.rb +15 -0
- data/stackprof-webnav.gemspec +8 -4
- metadata +104 -25
- data/lib/stackprof-webnav/css/application.css +0 -13
- data/screenshots/main.png +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c1dc3f5aab4a2ed125868d17bf4657933d657f5d9a697813d182cae1ba1b293e
|
4
|
+
data.tar.gz: ec484153a1283461143792401a681687c812a0a5698866954fba5b576d87d8c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a65aaa8975d84e9917af3327c389eb0243060480fbb9f3bf99e017fef3c947f2fd4ac7ccdfba785ac3149ccc7e107cd24bb7a665ca9f40cbc3fc2d9c428df8d5
|
7
|
+
data.tar.gz: b92cfeffddfec601b8f6412e2f3123c298c407bad8a7256d6485f5a97999e934430ea14f0802ad20ff5681042bc50f0c51f5c75c2cfc84daa387de677274c547
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: bundle exec stackprof-webnav -b <url_to_S3_bucket> -p $PORT
|
data/README.md
CHANGED
@@ -1,16 +1,20 @@
|
|
1
1
|
# StackProf Web navigator
|
2
2
|
|
3
|
-
__WARNING__: early version, no tests, may have bugs.
|
4
|
-
|
5
3
|
Provides a web ui to inspect stackprof dumps.
|
6
4
|
|
7
5
|
## Screenshots
|
8
6
|
|
9
|
-

|
8
|
+
|
9
|
+

|
10
|
+
|
11
|
+

|
12
|
+
|
13
|
+

|
10
14
|
|
11
|
-

|
12
16
|
|
13
|
-

|
14
18
|
|
15
19
|
## Usage
|
16
20
|
|
@@ -19,22 +23,32 @@ Provides a web ui to inspect stackprof dumps.
|
|
19
23
|
$ gem install stackprof-webnav
|
20
24
|
```
|
21
25
|
|
22
|
-
###
|
26
|
+
### Run the server
|
27
|
+
|
23
28
|
```bash
|
24
|
-
$ stackprof-webnav
|
25
|
-
|
29
|
+
$ stackprof-webnav
|
30
|
+
```
|
31
|
+
|
32
|
+
By default it will list all files in the current directory. You can click in the
|
33
|
+
web interface to try to open any file as a dump.
|
34
|
+
|
35
|
+
Additionally, you can list another directory by passing the `-d` flag:
|
36
|
+
|
37
|
+
```bash
|
38
|
+
$ stackprof-webnav -d /my/folder/with/dumps
|
39
|
+
```
|
40
|
+
|
41
|
+
Or launch it with a dump preselected:
|
42
|
+
|
43
|
+
```bash
|
44
|
+
$ stackprof-webnav -f /path/to/stackprof.dump
|
26
45
|
```
|
27
|
-
If the argument passed does not exist locally, it is assumed to be a URI and is treated as such.
|
28
46
|
|
29
47
|
See [stackprof gem][create-dump] homepage to learn how to create dumps.
|
30
48
|
|
31
49
|
### Profit
|
32
50
|
Open the browser at localhost:9292
|
33
51
|
|
34
|
-
## Caveats
|
35
|
-
- no tests, this gem was created for my personal usage in a hack stream,
|
36
|
-
bugs may occur
|
37
|
-
|
38
52
|
## Contributing
|
39
53
|
|
40
54
|
1. Fork it ( http://github.com/<my-github-username>/stackprof-webnav/fork )
|
@@ -44,6 +58,3 @@ Open the browser at localhost:9292
|
|
44
58
|
5. Create new Pull Request
|
45
59
|
|
46
60
|
[create-dump]: https://github.com/tmm1/stackprof#getting-started
|
47
|
-
[main-screenshot]: https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/main.png?raw=true
|
48
|
-
[method-screenshot]: https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/method.png?raw=true
|
49
|
-
[file-screenshot]: https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/file.png?raw=true
|
data/Rakefile
CHANGED
@@ -1 +1,25 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
require "stackprof"
|
3
|
+
require "rack/test"
|
4
|
+
|
5
|
+
task :make_dump do
|
6
|
+
class DummyA
|
7
|
+
def work
|
8
|
+
sleep 0.01
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class DummyB
|
13
|
+
def work
|
14
|
+
DummyA.new.work
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
StackProf.run(mode: :cpu, out: 'spec/fixtures/test.dump') do
|
19
|
+
1000.times { DummyB.new.work }
|
20
|
+
end
|
21
|
+
|
22
|
+
StackProf.run(mode: :cpu, raw: true, out: 'spec/fixtures/test-raw.dump') do
|
23
|
+
1000.times { DummyB.new.work }
|
24
|
+
end
|
25
|
+
end
|
data/bin/stackprof-webnav
CHANGED
@@ -1,26 +1,24 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'optparse'
|
3
3
|
require 'stackprof-webnav'
|
4
|
+
require 'rack'
|
4
5
|
|
5
6
|
options = {
|
7
|
+
:addr => "127.0.0.1",
|
6
8
|
:port => 9292
|
7
9
|
}
|
8
10
|
|
9
11
|
parser = OptionParser.new(ARGV) do |o|
|
10
|
-
o.banner = "Usage: stackprof-webnav
|
12
|
+
o.banner = "Usage: stackprof-webnav [-f localfile.dump]|[-d directory]|[-o ADDR]|[-p NUMBER]"
|
13
|
+
o.on('-f [LOCALFILE]', 'Local file path to dump') {|filepath| options[:filepath] = filepath }
|
14
|
+
o.on('-d [DIRECTORY]', 'path to a directory with dumps') {|directory| options[:directory] = directory}
|
15
|
+
o.on('-o [ADDR]', 'Server addr bind') {|addr| options[:addr] = addr }
|
11
16
|
o.on('-p [PORT]', 'Server port') {|port| options[:port] = port }
|
12
17
|
end
|
13
18
|
|
14
19
|
parser.parse!
|
15
|
-
parser.abort(parser.help) if ARGV.empty?
|
16
20
|
|
17
|
-
file = ARGV.pop
|
18
21
|
server = StackProf::Webnav::Server
|
22
|
+
server.cmd_options = options
|
19
23
|
|
20
|
-
|
21
|
-
server.report_dump_path = File.expand_path(file)
|
22
|
-
else
|
23
|
-
server.report_dump_url = file
|
24
|
-
end
|
25
|
-
|
26
|
-
server.run! options[:port]
|
24
|
+
Rack::Handler.pick(['thin', 'webrick']).run server.new, :Host => options[:addr], :Port => options[:port]
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Dump
|
2
|
+
attr_reader :path
|
3
|
+
attr_accessor :flamegraph_json, :graph_data
|
4
|
+
def initialize(path)
|
5
|
+
@path = path
|
6
|
+
end
|
7
|
+
|
8
|
+
def checksum
|
9
|
+
@checksum ||= Digest::SHA1.file(@path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def content
|
13
|
+
@content ||= File.open(@path).read
|
14
|
+
end
|
15
|
+
|
16
|
+
def path=(new_path)
|
17
|
+
@path = new_path
|
18
|
+
check_checksum!
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_checksum!
|
22
|
+
return unless @checksum
|
23
|
+
|
24
|
+
if Digest::SHA1.file(@path) != checksum
|
25
|
+
puts "\n\nFile reloaded"
|
26
|
+
@checksum, @content = nil, nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def flame_graph_path
|
31
|
+
@path + ".#{checksum}.flames.json"
|
32
|
+
end
|
33
|
+
|
34
|
+
def graph_path
|
35
|
+
@path + ".#{checksum}.digraph.dot"
|
36
|
+
end
|
37
|
+
|
38
|
+
def graph_image_path
|
39
|
+
@path + ".#{checksum}.graph.png"
|
40
|
+
end
|
41
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'better_errors'
|
2
2
|
require 'stringio'
|
3
|
+
require 'rexml/document'
|
3
4
|
|
4
5
|
module StackProf
|
5
6
|
module Webnav
|
@@ -52,7 +53,7 @@ module StackProf
|
|
52
53
|
private
|
53
54
|
|
54
55
|
def percent value
|
55
|
-
"%2.2f
|
56
|
+
"%2.2f%%" % (value*100)
|
56
57
|
end
|
57
58
|
|
58
59
|
def callers frame, info
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,983 @@
|
|
1
|
+
if (typeof Element.prototype.matches !== 'function') {
|
2
|
+
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.webkitMatchesSelector || function matches(selector) {
|
3
|
+
var element = this
|
4
|
+
var elements = (element.document || element.ownerDocument).querySelectorAll(selector)
|
5
|
+
var index = 0
|
6
|
+
|
7
|
+
while (elements[index] && elements[index] !== element) {
|
8
|
+
++index
|
9
|
+
}
|
10
|
+
|
11
|
+
return Boolean(elements[index])
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
if (typeof Element.prototype.closest !== 'function') {
|
16
|
+
Element.prototype.closest = function closest(selector) {
|
17
|
+
var element = this
|
18
|
+
|
19
|
+
while (element && element.nodeType === 1) {
|
20
|
+
if (element.matches(selector)) {
|
21
|
+
return element
|
22
|
+
}
|
23
|
+
|
24
|
+
element = element.parentNode
|
25
|
+
}
|
26
|
+
|
27
|
+
return null
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
if (typeof Object.assign !== 'function') {
|
32
|
+
(function() {
|
33
|
+
Object.assign = function(target) {
|
34
|
+
'use strict'
|
35
|
+
// We must check against these specific cases.
|
36
|
+
if (target === undefined || target === null) {
|
37
|
+
throw new TypeError('Cannot convert undefined or null to object')
|
38
|
+
}
|
39
|
+
|
40
|
+
var output = Object(target)
|
41
|
+
for (var index = 1; index < arguments.length; index++) {
|
42
|
+
var source = arguments[index]
|
43
|
+
if (source !== undefined && source !== null) {
|
44
|
+
for (var nextKey in source) {
|
45
|
+
if (source.hasOwnProperty(nextKey)) {
|
46
|
+
output[nextKey] = source[nextKey]
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
return output
|
52
|
+
}
|
53
|
+
})()
|
54
|
+
}
|
55
|
+
|
56
|
+
function EventSource() {
|
57
|
+
var self = this
|
58
|
+
|
59
|
+
self.eventListeners = {}
|
60
|
+
}
|
61
|
+
|
62
|
+
EventSource.prototype.on = function(name, callback) {
|
63
|
+
var self = this
|
64
|
+
|
65
|
+
var listeners = self.eventListeners[name]
|
66
|
+
if (!listeners)
|
67
|
+
listeners = self.eventListeners[name] = []
|
68
|
+
listeners.push(callback)
|
69
|
+
}
|
70
|
+
|
71
|
+
EventSource.prototype.dispatch = function(name, data) {
|
72
|
+
var self = this
|
73
|
+
|
74
|
+
var listeners = self.eventListeners[name] || []
|
75
|
+
listeners.forEach(function(c) {
|
76
|
+
requestAnimationFrame(function() { c(data) })
|
77
|
+
})
|
78
|
+
}
|
79
|
+
|
80
|
+
function CanvasView(canvas) {
|
81
|
+
var self = this
|
82
|
+
|
83
|
+
self.canvas = canvas
|
84
|
+
}
|
85
|
+
|
86
|
+
CanvasView.prototype.setDimensions = function(width, height) {
|
87
|
+
var self = this
|
88
|
+
|
89
|
+
if (self.resizeRequestID)
|
90
|
+
cancelAnimationFrame(self.resizeRequestID)
|
91
|
+
|
92
|
+
self.resizeRequestID = requestAnimationFrame(self.setDimensionsNow.bind(self, width, height))
|
93
|
+
}
|
94
|
+
|
95
|
+
CanvasView.prototype.setDimensionsNow = function(width, height) {
|
96
|
+
var self = this
|
97
|
+
|
98
|
+
if (width === self.width && height === self.height)
|
99
|
+
return
|
100
|
+
|
101
|
+
self.width = width
|
102
|
+
self.height = height
|
103
|
+
|
104
|
+
self.canvas.style.width = width
|
105
|
+
self.canvas.style.height = height
|
106
|
+
|
107
|
+
var ratio = window.devicePixelRatio || 1
|
108
|
+
self.canvas.width = width * ratio
|
109
|
+
self.canvas.height = height * ratio
|
110
|
+
|
111
|
+
var ctx = self.canvas.getContext('2d')
|
112
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
113
|
+
ctx.scale(ratio, ratio)
|
114
|
+
|
115
|
+
self.repaintNow()
|
116
|
+
}
|
117
|
+
|
118
|
+
CanvasView.prototype.paint = function() {
|
119
|
+
}
|
120
|
+
|
121
|
+
CanvasView.prototype.scheduleRepaint = function() {
|
122
|
+
var self = this
|
123
|
+
|
124
|
+
if (self.repaintRequestID)
|
125
|
+
return
|
126
|
+
|
127
|
+
self.repaintRequestID = requestAnimationFrame(function() {
|
128
|
+
self.repaintRequestID = null
|
129
|
+
self.repaintNow()
|
130
|
+
})
|
131
|
+
}
|
132
|
+
|
133
|
+
CanvasView.prototype.repaintNow = function() {
|
134
|
+
var self = this
|
135
|
+
|
136
|
+
self.canvas.getContext('2d').clearRect(0, 0, self.width, self.height)
|
137
|
+
self.paint()
|
138
|
+
|
139
|
+
if (self.repaintRequestID) {
|
140
|
+
cancelAnimationFrame(self.repaintRequestID)
|
141
|
+
self.repaintRequestID = null
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
function Flamechart(canvas, data, dataRange, info) {
|
146
|
+
var self = this
|
147
|
+
|
148
|
+
CanvasView.call(self, canvas)
|
149
|
+
EventSource.call(self)
|
150
|
+
|
151
|
+
self.canvas = canvas
|
152
|
+
self.data = data
|
153
|
+
self.dataRange = dataRange
|
154
|
+
self.info = info
|
155
|
+
|
156
|
+
self.viewport = {
|
157
|
+
x: dataRange.minX,
|
158
|
+
y: dataRange.minY,
|
159
|
+
width: dataRange.maxX - dataRange.minX,
|
160
|
+
height: dataRange.maxY - dataRange.minY,
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
Flamechart.prototype = Object.create(CanvasView.prototype)
|
165
|
+
Flamechart.prototype.constructor = Flamechart
|
166
|
+
Object.assign(Flamechart.prototype, EventSource.prototype)
|
167
|
+
|
168
|
+
Flamechart.prototype.xScale = function(x) {
|
169
|
+
var self = this
|
170
|
+
return self.widthScale(x - self.viewport.x)
|
171
|
+
}
|
172
|
+
|
173
|
+
Flamechart.prototype.yScale = function(y) {
|
174
|
+
var self = this
|
175
|
+
return self.heightScale(y - self.viewport.y)
|
176
|
+
}
|
177
|
+
|
178
|
+
Flamechart.prototype.widthScale = function(width) {
|
179
|
+
var self = this
|
180
|
+
return width * self.width / self.viewport.width
|
181
|
+
}
|
182
|
+
|
183
|
+
Flamechart.prototype.heightScale = function(height) {
|
184
|
+
var self = this
|
185
|
+
return height * self.height / self.viewport.height
|
186
|
+
}
|
187
|
+
|
188
|
+
Flamechart.prototype.frameRect = function(f) {
|
189
|
+
return {
|
190
|
+
x: f.x,
|
191
|
+
y: f.y,
|
192
|
+
width: f.width,
|
193
|
+
height: 1,
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
Flamechart.prototype.dataToCanvas = function(r) {
|
198
|
+
var self = this
|
199
|
+
|
200
|
+
return {
|
201
|
+
x: self.xScale(r.x),
|
202
|
+
y: self.yScale(r.y),
|
203
|
+
width: self.widthScale(r.width),
|
204
|
+
height: self.heightScale(r.height),
|
205
|
+
}
|
206
|
+
}
|
207
|
+
|
208
|
+
Flamechart.prototype.setViewport = function(viewport) {
|
209
|
+
var self = this
|
210
|
+
|
211
|
+
if (self.viewport.x === viewport.x &&
|
212
|
+
self.viewport.y === viewport.y &&
|
213
|
+
self.viewport.width === viewport.width &&
|
214
|
+
self.viewport.height === viewport.height)
|
215
|
+
return
|
216
|
+
|
217
|
+
self.viewport = viewport
|
218
|
+
|
219
|
+
self.scheduleRepaint()
|
220
|
+
|
221
|
+
self.dispatch('viewportchanged', { current: viewport })
|
222
|
+
}
|
223
|
+
|
224
|
+
Flamechart.prototype.paint = function(opacity, frames, gemName) {
|
225
|
+
var self = this
|
226
|
+
|
227
|
+
var ctx = self.canvas.getContext('2d')
|
228
|
+
|
229
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'
|
230
|
+
|
231
|
+
if (self.showLabels) {
|
232
|
+
ctx.textBaseline = 'middle'
|
233
|
+
ctx.font = '11px ' + getComputedStyle(this.canvas).fontFamily
|
234
|
+
// W tends to be one of the widest characters (and if the font is truly
|
235
|
+
// fixed-width then any character will do).
|
236
|
+
var characterWidth = ctx.measureText('WWWW').width / 4
|
237
|
+
}
|
238
|
+
|
239
|
+
if (typeof opacity === 'undefined')
|
240
|
+
opacity = 1
|
241
|
+
|
242
|
+
frames = frames || self.data
|
243
|
+
|
244
|
+
var blocksByColor = {}
|
245
|
+
|
246
|
+
frames.forEach(function(f) {
|
247
|
+
if (gemName && f.gemName !== gemName)
|
248
|
+
return
|
249
|
+
|
250
|
+
var r = self.dataToCanvas(self.frameRect(f))
|
251
|
+
|
252
|
+
if (r.x >= self.width ||
|
253
|
+
r.y >= self.height ||
|
254
|
+
(r.x + r.width) <= 0 ||
|
255
|
+
(r.y + r.height) <= 0) {
|
256
|
+
return
|
257
|
+
}
|
258
|
+
|
259
|
+
var i = self.info[f.frame_id]
|
260
|
+
var color = colorString(i.color, opacity)
|
261
|
+
var colorBlocks = blocksByColor[color]
|
262
|
+
if (!colorBlocks)
|
263
|
+
colorBlocks = blocksByColor[color] = []
|
264
|
+
colorBlocks.push({ rect: r, text: f.frame })
|
265
|
+
})
|
266
|
+
|
267
|
+
var textBlocks = []
|
268
|
+
|
269
|
+
Object.keys(blocksByColor).forEach(function(color) {
|
270
|
+
ctx.fillStyle = color
|
271
|
+
|
272
|
+
blocksByColor[color].forEach(function(block) {
|
273
|
+
if (opacity < 1)
|
274
|
+
ctx.clearRect(block.rect.x, block.rect.y, block.rect.width, block.rect.height)
|
275
|
+
|
276
|
+
ctx.fillRect(block.rect.x, block.rect.y, block.rect.width, block.rect.height)
|
277
|
+
|
278
|
+
if (block.rect.width > 4 && block.rect.height > 4)
|
279
|
+
ctx.strokeRect(block.rect.x, block.rect.y, block.rect.width, block.rect.height)
|
280
|
+
|
281
|
+
if (!self.showLabels || block.rect.width / characterWidth < 4)
|
282
|
+
return
|
283
|
+
|
284
|
+
textBlocks.push(block)
|
285
|
+
})
|
286
|
+
})
|
287
|
+
|
288
|
+
ctx.fillStyle = '#000'
|
289
|
+
textBlocks.forEach(function(block) {
|
290
|
+
var text = block.text
|
291
|
+
var textRect = Object.assign({}, block.rect)
|
292
|
+
textRect.x += 1
|
293
|
+
textRect.width -= 2
|
294
|
+
if (textRect.width < text.length * characterWidth * 0.75)
|
295
|
+
text = centerTruncate(block.text, Math.floor(textRect.width / characterWidth))
|
296
|
+
ctx.fillText(text, textRect.x, textRect.y + textRect.height / 2, textRect.width)
|
297
|
+
})
|
298
|
+
}
|
299
|
+
|
300
|
+
Flamechart.prototype.frameAtPoint = function(x, y) {
|
301
|
+
var self = this
|
302
|
+
|
303
|
+
return self.data.find(function(d) {
|
304
|
+
var r = self.dataToCanvas(self.frameRect(d))
|
305
|
+
|
306
|
+
return r.x <= x
|
307
|
+
&& r.x + r.width >= x
|
308
|
+
&& r.y <= y
|
309
|
+
&& r.y + r.height >= y
|
310
|
+
})
|
311
|
+
}
|
312
|
+
|
313
|
+
function MainFlamechart(canvas, data, dataRange, info) {
|
314
|
+
var self = this
|
315
|
+
|
316
|
+
Flamechart.call(self, canvas, data, dataRange, info)
|
317
|
+
|
318
|
+
self.showLabels = true
|
319
|
+
|
320
|
+
self.canvas.addEventListener('mousedown', self.onMouseDown.bind(self))
|
321
|
+
self.canvas.addEventListener('mousemove', self.onMouseMove.bind(self))
|
322
|
+
self.canvas.addEventListener('mouseout', self.onMouseOut.bind(self))
|
323
|
+
self.canvas.addEventListener('wheel', self.onWheel.bind(self))
|
324
|
+
}
|
325
|
+
|
326
|
+
MainFlamechart.prototype = Object.create(Flamechart.prototype)
|
327
|
+
|
328
|
+
MainFlamechart.prototype.setDimensionsNow = function(width, height) {
|
329
|
+
var self = this
|
330
|
+
|
331
|
+
var viewport = Object.assign({}, self.viewport)
|
332
|
+
viewport.height = height / 16
|
333
|
+
self.setViewport(viewport)
|
334
|
+
|
335
|
+
CanvasView.prototype.setDimensionsNow.call(self, width, height)
|
336
|
+
}
|
337
|
+
|
338
|
+
MainFlamechart.prototype.onMouseDown = function(e) {
|
339
|
+
var self = this
|
340
|
+
|
341
|
+
if (e.button !== 0)
|
342
|
+
return
|
343
|
+
|
344
|
+
captureMouse({
|
345
|
+
mouseup: self.onMouseUp.bind(self),
|
346
|
+
mousemove: self.onMouseMove.bind(self),
|
347
|
+
})
|
348
|
+
|
349
|
+
var clientRect = self.canvas.getBoundingClientRect()
|
350
|
+
var currentX = e.clientX - clientRect.left
|
351
|
+
var currentY = e.clientY - clientRect.top
|
352
|
+
|
353
|
+
self.dragging = true
|
354
|
+
self.dragInfo = {
|
355
|
+
mouse: { x: currentX, y: currentY },
|
356
|
+
viewport: { x: self.viewport.x, y: self.viewport.y },
|
357
|
+
}
|
358
|
+
|
359
|
+
e.preventDefault()
|
360
|
+
}
|
361
|
+
|
362
|
+
MainFlamechart.prototype.onMouseUp = function(e) {
|
363
|
+
var self = this
|
364
|
+
|
365
|
+
if (!self.dragging)
|
366
|
+
return
|
367
|
+
|
368
|
+
releaseCapture()
|
369
|
+
|
370
|
+
self.dragging = false
|
371
|
+
e.preventDefault()
|
372
|
+
}
|
373
|
+
|
374
|
+
MainFlamechart.prototype.onMouseMove = function(e) {
|
375
|
+
var self = this
|
376
|
+
|
377
|
+
var clientRect = self.canvas.getBoundingClientRect()
|
378
|
+
var currentX = e.clientX - clientRect.left
|
379
|
+
var currentY = e.clientY - clientRect.top
|
380
|
+
|
381
|
+
if (self.dragging) {
|
382
|
+
var viewport = Object.assign({}, self.viewport)
|
383
|
+
viewport.x = self.dragInfo.viewport.x - (currentX - self.dragInfo.mouse.x) * viewport.width / self.width
|
384
|
+
viewport.y = self.dragInfo.viewport.y - (currentY - self.dragInfo.mouse.y) * viewport.height / self.height
|
385
|
+
viewport.x = Math.min(self.dataRange.maxX - viewport.width, Math.max(self.dataRange.minX, viewport.x))
|
386
|
+
viewport.y = Math.min(self.dataRange.maxY - viewport.height, Math.max(self.dataRange.minY, viewport.y))
|
387
|
+
self.setViewport(viewport)
|
388
|
+
return
|
389
|
+
}
|
390
|
+
|
391
|
+
var frame = self.frameAtPoint(currentX, currentY)
|
392
|
+
self.setHoveredFrame(frame)
|
393
|
+
}
|
394
|
+
|
395
|
+
MainFlamechart.prototype.onMouseOut = function() {
|
396
|
+
var self = this
|
397
|
+
|
398
|
+
if (self.dragging)
|
399
|
+
return
|
400
|
+
|
401
|
+
self.setHoveredFrame(null)
|
402
|
+
}
|
403
|
+
|
404
|
+
MainFlamechart.prototype.onWheel = function(e) {
|
405
|
+
var self = this
|
406
|
+
|
407
|
+
var deltaX = e.deltaX
|
408
|
+
var deltaY = e.deltaY
|
409
|
+
|
410
|
+
if (e.deltaMode == WheelEvent.prototype.DOM_DELTA_LINE) {
|
411
|
+
deltaX *= 11
|
412
|
+
deltaY *= 11
|
413
|
+
}
|
414
|
+
|
415
|
+
if (e.shiftKey) {
|
416
|
+
if ('webkitDirectionInvertedFromDevice' in e) {
|
417
|
+
if (e.webkitDirectionInvertedFromDevice)
|
418
|
+
deltaY *= -1
|
419
|
+
} else if (/Mac OS X/.test(navigator.userAgent)) {
|
420
|
+
// Assume that most Mac users have "Scroll direction: Natural" enabled.
|
421
|
+
deltaY *= -1
|
422
|
+
}
|
423
|
+
|
424
|
+
var mouseWheelZoomSpeed = 1 / 120
|
425
|
+
self.handleZoomGesture(Math.pow(1.2, -(deltaY || deltaX) * mouseWheelZoomSpeed), e.offsetX)
|
426
|
+
e.preventDefault()
|
427
|
+
return
|
428
|
+
}
|
429
|
+
|
430
|
+
var viewport = Object.assign({}, self.viewport)
|
431
|
+
viewport.x += deltaX * viewport.width / (self.dataRange.maxX - self.dataRange.minX)
|
432
|
+
viewport.x = Math.min(self.dataRange.maxX - viewport.width, Math.max(self.dataRange.minX, viewport.x))
|
433
|
+
viewport.y += (deltaY / 8) * viewport.height / (self.dataRange.maxY - self.dataRange.minY)
|
434
|
+
viewport.y = Math.min(self.dataRange.maxY - viewport.height, Math.max(self.dataRange.minY, viewport.y))
|
435
|
+
self.setViewport(viewport)
|
436
|
+
e.preventDefault()
|
437
|
+
}
|
438
|
+
|
439
|
+
MainFlamechart.prototype.handleZoomGesture = function(zoom, originX) {
|
440
|
+
var self = this
|
441
|
+
|
442
|
+
var viewport = Object.assign({}, self.viewport)
|
443
|
+
var ratioX = originX / self.width
|
444
|
+
|
445
|
+
var newWidth = Math.min(viewport.width / zoom, self.dataRange.maxX - self.dataRange.minX)
|
446
|
+
viewport.x = Math.max(self.dataRange.minX, viewport.x + (viewport.width - newWidth) * ratioX)
|
447
|
+
viewport.width = Math.min(newWidth, self.dataRange.maxX - viewport.x)
|
448
|
+
|
449
|
+
self.setViewport(viewport)
|
450
|
+
}
|
451
|
+
|
452
|
+
MainFlamechart.prototype.setHoveredFrame = function(frame) {
|
453
|
+
var self = this
|
454
|
+
|
455
|
+
if (frame === self.hoveredFrame)
|
456
|
+
return
|
457
|
+
|
458
|
+
var previous = self.hoveredFrame
|
459
|
+
self.hoveredFrame = frame
|
460
|
+
|
461
|
+
self.dispatch('hoveredframechanged', { previous: previous, current: self.hoveredFrame })
|
462
|
+
}
|
463
|
+
|
464
|
+
function OverviewFlamechart(container, viewportOverlay, data, dataRange, info) {
|
465
|
+
var self = this
|
466
|
+
|
467
|
+
Flamechart.call(self, container.querySelector('.overview'), data, dataRange, info)
|
468
|
+
|
469
|
+
self.container = container
|
470
|
+
|
471
|
+
self.showLabels = false
|
472
|
+
|
473
|
+
self.viewportOverlay = viewportOverlay
|
474
|
+
|
475
|
+
self.canvas.addEventListener('mousedown', self.onMouseDown.bind(self))
|
476
|
+
self.viewportOverlay.addEventListener('mousedown', self.onOverlayMouseDown.bind(self))
|
477
|
+
}
|
478
|
+
|
479
|
+
OverviewFlamechart.prototype = Object.create(Flamechart.prototype)
|
480
|
+
|
481
|
+
OverviewFlamechart.prototype.setViewportOverlayRect = function(r) {
|
482
|
+
var self = this
|
483
|
+
|
484
|
+
self.viewportOverlayRect = r
|
485
|
+
|
486
|
+
r = self.dataToCanvas(r)
|
487
|
+
r.width = Math.max(2, r.width)
|
488
|
+
r.height = Math.max(2, r.height)
|
489
|
+
|
490
|
+
if ('transform' in self.viewportOverlay.style) {
|
491
|
+
self.viewportOverlay.style.transform = 'translate(' + r.x + 'px, ' + r.y + 'px) scale(' + r.width + ', ' + r.height + ')'
|
492
|
+
} else {
|
493
|
+
self.viewportOverlay.style.left = r.x
|
494
|
+
self.viewportOverlay.style.top = r.y
|
495
|
+
self.viewportOverlay.style.width = r.width
|
496
|
+
self.viewportOverlay.style.height = r.height
|
497
|
+
}
|
498
|
+
}
|
499
|
+
|
500
|
+
OverviewFlamechart.prototype.onMouseDown = function(e) {
|
501
|
+
var self = this
|
502
|
+
|
503
|
+
captureMouse({
|
504
|
+
mouseup: self.onMouseUp.bind(self),
|
505
|
+
mousemove: self.onMouseMove.bind(self),
|
506
|
+
})
|
507
|
+
|
508
|
+
self.dragging = true
|
509
|
+
self.dragStartX = e.clientX - self.canvas.getBoundingClientRect().left
|
510
|
+
|
511
|
+
self.handleDragGesture(e)
|
512
|
+
|
513
|
+
e.preventDefault()
|
514
|
+
}
|
515
|
+
|
516
|
+
OverviewFlamechart.prototype.onMouseUp = function(e) {
|
517
|
+
var self = this
|
518
|
+
|
519
|
+
if (!self.dragging)
|
520
|
+
return
|
521
|
+
|
522
|
+
releaseCapture()
|
523
|
+
|
524
|
+
self.dragging = false
|
525
|
+
|
526
|
+
self.handleDragGesture(e)
|
527
|
+
|
528
|
+
e.preventDefault()
|
529
|
+
}
|
530
|
+
|
531
|
+
OverviewFlamechart.prototype.onMouseMove = function(e) {
|
532
|
+
var self = this
|
533
|
+
|
534
|
+
if (!self.dragging)
|
535
|
+
return
|
536
|
+
|
537
|
+
self.handleDragGesture(e)
|
538
|
+
|
539
|
+
e.preventDefault()
|
540
|
+
}
|
541
|
+
|
542
|
+
OverviewFlamechart.prototype.handleDragGesture = function(e) {
|
543
|
+
var self = this
|
544
|
+
|
545
|
+
var clientRect = self.canvas.getBoundingClientRect()
|
546
|
+
var currentX = e.clientX - clientRect.left
|
547
|
+
var currentY = e.clientY - clientRect.top
|
548
|
+
|
549
|
+
if (self.dragCurrentX === currentX)
|
550
|
+
return
|
551
|
+
|
552
|
+
self.dragCurrentX = currentX
|
553
|
+
|
554
|
+
var minX = Math.min(self.dragStartX, self.dragCurrentX)
|
555
|
+
var maxX = Math.max(self.dragStartX, self.dragCurrentX)
|
556
|
+
|
557
|
+
var rect = Object.assign({}, self.viewportOverlayRect)
|
558
|
+
rect.x = minX / self.width * self.viewport.width + self.viewport.x
|
559
|
+
rect.width = Math.max(self.viewport.width / 1000, (maxX - minX) / self.width * self.viewport.width)
|
560
|
+
|
561
|
+
rect.y = Math.max(self.viewport.y, Math.min(self.viewport.height - self.viewport.y, currentY / self.height * self.viewport.height + self.viewport.y - rect.height / 2))
|
562
|
+
|
563
|
+
self.setViewportOverlayRect(rect)
|
564
|
+
self.dispatch('overlaychanged', { current: self.viewportOverlayRect })
|
565
|
+
}
|
566
|
+
|
567
|
+
OverviewFlamechart.prototype.onOverlayMouseDown = function(e) {
|
568
|
+
var self = this
|
569
|
+
|
570
|
+
captureMouse({
|
571
|
+
mouseup: self.onOverlayMouseUp.bind(self),
|
572
|
+
mousemove: self.onOverlayMouseMove.bind(self),
|
573
|
+
})
|
574
|
+
|
575
|
+
self.overlayDragging = true
|
576
|
+
self.overlayDragInfo = {
|
577
|
+
mouse: { x: e.clientX, y: e.clientY },
|
578
|
+
rect: Object.assign({}, self.viewportOverlayRect),
|
579
|
+
}
|
580
|
+
self.viewportOverlay.classList.add('moving')
|
581
|
+
|
582
|
+
self.handleOverlayDragGesture(e)
|
583
|
+
|
584
|
+
e.preventDefault()
|
585
|
+
}
|
586
|
+
|
587
|
+
OverviewFlamechart.prototype.onOverlayMouseUp = function(e) {
|
588
|
+
var self = this
|
589
|
+
|
590
|
+
if (!self.overlayDragging)
|
591
|
+
return
|
592
|
+
|
593
|
+
releaseCapture()
|
594
|
+
|
595
|
+
self.overlayDragging = false
|
596
|
+
self.viewportOverlay.classList.remove('moving')
|
597
|
+
|
598
|
+
self.handleOverlayDragGesture(e)
|
599
|
+
|
600
|
+
e.preventDefault()
|
601
|
+
}
|
602
|
+
|
603
|
+
OverviewFlamechart.prototype.onOverlayMouseMove = function(e) {
|
604
|
+
var self = this
|
605
|
+
|
606
|
+
if (!self.overlayDragging)
|
607
|
+
return
|
608
|
+
|
609
|
+
self.handleOverlayDragGesture(e)
|
610
|
+
|
611
|
+
e.preventDefault()
|
612
|
+
}
|
613
|
+
|
614
|
+
OverviewFlamechart.prototype.handleOverlayDragGesture = function(e) {
|
615
|
+
var self = this
|
616
|
+
|
617
|
+
var deltaX = (e.clientX - self.overlayDragInfo.mouse.x) / self.width * self.viewport.width
|
618
|
+
var deltaY = (e.clientY - self.overlayDragInfo.mouse.y) / self.height * self.viewport.height
|
619
|
+
|
620
|
+
var rect = Object.assign({}, self.overlayDragInfo.rect)
|
621
|
+
rect.x += deltaX
|
622
|
+
rect.y += deltaY
|
623
|
+
rect.x = Math.max(self.viewport.x, Math.min(self.viewport.x + self.viewport.width - rect.width, rect.x))
|
624
|
+
rect.y = Math.max(self.viewport.y, Math.min(self.viewport.y + self.viewport.height - rect.height, rect.y))
|
625
|
+
|
626
|
+
self.setViewportOverlayRect(rect)
|
627
|
+
self.dispatch('overlaychanged', { current: self.viewportOverlayRect })
|
628
|
+
}
|
629
|
+
|
630
|
+
function FlamegraphView(data, info, sortedGems) {
|
631
|
+
var self = this
|
632
|
+
|
633
|
+
self.data = data
|
634
|
+
self.info = info
|
635
|
+
|
636
|
+
self.dataRange = self.computeDataRange()
|
637
|
+
|
638
|
+
self.mainChart = new MainFlamechart(document.querySelector('.flamegraph'), data, self.dataRange, info)
|
639
|
+
self.overview = new OverviewFlamechart(document.querySelector('.overview-container'), document.querySelector('.overview-viewport-overlay'), data, self.dataRange, info)
|
640
|
+
self.infoElement = document.querySelector('.info')
|
641
|
+
|
642
|
+
self.mainChart.on('hoveredframechanged', self.onHoveredFrameChanged.bind(self))
|
643
|
+
self.mainChart.on('viewportchanged', self.onViewportChanged.bind(self))
|
644
|
+
self.overview.on('overlaychanged', self.onOverlayChanged.bind(self))
|
645
|
+
|
646
|
+
var legend = document.querySelector('.legend')
|
647
|
+
self.renderLegend(legend, sortedGems)
|
648
|
+
|
649
|
+
legend.addEventListener('mousemove', self.onLegendMouseMove.bind(self))
|
650
|
+
legend.addEventListener('mouseout', self.onLegendMouseOut.bind(self))
|
651
|
+
|
652
|
+
window.addEventListener('resize', self.updateDimensions.bind(self))
|
653
|
+
|
654
|
+
self.updateDimensions()
|
655
|
+
}
|
656
|
+
|
657
|
+
FlamegraphView.prototype.updateDimensions = function() {
|
658
|
+
var self = this
|
659
|
+
|
660
|
+
var margin = {top: 10, right: 10, bottom: 10, left: 10}
|
661
|
+
var width = window.innerWidth - 200 - margin.left - margin.right
|
662
|
+
var mainChartHeight = Math.ceil(window.innerHeight * 0.80) - margin.top - margin.bottom
|
663
|
+
var overviewHeight = Math.floor(window.innerHeight * 0.20) - 60 - margin.top - margin.bottom
|
664
|
+
|
665
|
+
self.mainChart.setDimensions(width + margin.left + margin.right, mainChartHeight + margin.top + margin.bottom)
|
666
|
+
self.overview.setDimensions(width + margin.left + margin.right, overviewHeight + margin.top + margin.bottom)
|
667
|
+
self.overview.setViewportOverlayRect(self.mainChart.viewport)
|
668
|
+
}
|
669
|
+
|
670
|
+
FlamegraphView.prototype.computeDataRange = function() {
|
671
|
+
var self = this
|
672
|
+
|
673
|
+
var range = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
674
|
+
self.data.forEach(function(d) {
|
675
|
+
range.minX = Math.min(range.minX, d.x)
|
676
|
+
range.minY = Math.min(range.minY, d.y)
|
677
|
+
range.maxX = Math.max(range.maxX, d.x + d.width)
|
678
|
+
range.maxY = Math.max(range.maxY, d.y + 1)
|
679
|
+
})
|
680
|
+
|
681
|
+
return range
|
682
|
+
}
|
683
|
+
|
684
|
+
FlamegraphView.prototype.onHoveredFrameChanged = function(data) {
|
685
|
+
var self = this
|
686
|
+
|
687
|
+
self.updateInfo(data.current)
|
688
|
+
|
689
|
+
if (data.previous)
|
690
|
+
self.repaintFrames(1, self.info[data.previous.frame_id].frames)
|
691
|
+
|
692
|
+
if (data.current)
|
693
|
+
self.repaintFrames(0.5, self.info[data.current.frame_id].frames)
|
694
|
+
}
|
695
|
+
|
696
|
+
FlamegraphView.prototype.repaintFrames = function(opacity, frames) {
|
697
|
+
var self = this
|
698
|
+
|
699
|
+
self.mainChart.paint(opacity, frames)
|
700
|
+
self.overview.paint(opacity, frames)
|
701
|
+
}
|
702
|
+
|
703
|
+
FlamegraphView.prototype.updateInfo = function(frame) {
|
704
|
+
var self = this
|
705
|
+
|
706
|
+
if (!frame) {
|
707
|
+
self.infoElement.style.backgroundColor = ''
|
708
|
+
self.infoElement.querySelector('.frame').textContent = ''
|
709
|
+
self.infoElement.querySelector('.file').textContent = ''
|
710
|
+
self.infoElement.querySelector('.samples').textContent = ''
|
711
|
+
self.infoElement.querySelector('.exclusive').textContent = ''
|
712
|
+
return
|
713
|
+
}
|
714
|
+
|
715
|
+
var i = self.info[frame.frame_id]
|
716
|
+
var shortFile = frame.file.replace(/^.+\/(gems|app|lib|config|jobs)/, '$1')
|
717
|
+
var sData = self.samplePercentRaw(i.samples.length, frame.topFrame ? frame.topFrame.exclusiveCount : 0)
|
718
|
+
|
719
|
+
self.infoElement.style.backgroundColor = colorString(i.color, 1)
|
720
|
+
self.infoElement.querySelector('.frame').textContent = frame.frame
|
721
|
+
self.infoElement.querySelector('.file').textContent = shortFile
|
722
|
+
self.infoElement.querySelector('.samples').textContent = sData[0] + ' samples (' + sData[1] + '%)'
|
723
|
+
if (sData[3])
|
724
|
+
self.infoElement.querySelector('.exclusive').textContent = sData[2] + ' exclusive (' + sData[3] + '%)'
|
725
|
+
else
|
726
|
+
self.infoElement.querySelector('.exclusive').textContent = ''
|
727
|
+
}
|
728
|
+
|
729
|
+
FlamegraphView.prototype.samplePercentRaw = function(samples, exclusive) {
|
730
|
+
var self = this
|
731
|
+
|
732
|
+
var ret = [samples, ((samples / self.dataRange.maxX) * 100).toFixed(2)]
|
733
|
+
if (exclusive)
|
734
|
+
ret = ret.concat([exclusive, ((exclusive / self.dataRange.maxX) * 100).toFixed(2)])
|
735
|
+
return ret
|
736
|
+
}
|
737
|
+
|
738
|
+
FlamegraphView.prototype.onViewportChanged = function(data) {
|
739
|
+
var self = this
|
740
|
+
|
741
|
+
self.overview.setViewportOverlayRect(data.current)
|
742
|
+
}
|
743
|
+
|
744
|
+
FlamegraphView.prototype.onOverlayChanged = function(data) {
|
745
|
+
var self = this
|
746
|
+
|
747
|
+
self.mainChart.setViewport(data.current)
|
748
|
+
}
|
749
|
+
|
750
|
+
FlamegraphView.prototype.renderLegend = function(element, sortedGems) {
|
751
|
+
var self = this
|
752
|
+
|
753
|
+
var fragment = document.createDocumentFragment()
|
754
|
+
|
755
|
+
sortedGems.forEach(function(gem) {
|
756
|
+
var sData = self.samplePercentRaw(gem.samples.length)
|
757
|
+
var node = document.createElement('div')
|
758
|
+
node.className = 'legend-gem'
|
759
|
+
node.setAttribute('data-gem-name', gem.name)
|
760
|
+
node.style.backgroundColor = colorString(gem.color, 1)
|
761
|
+
|
762
|
+
var span = document.createElement('span')
|
763
|
+
span.style.float = 'right'
|
764
|
+
span.textContent = sData[0] + 'x'
|
765
|
+
span.appendChild(document.createElement('br'))
|
766
|
+
span.appendChild(document.createTextNode(sData[1] + '%'))
|
767
|
+
node.appendChild(span)
|
768
|
+
|
769
|
+
var name = document.createElement('div')
|
770
|
+
name.className = 'name'
|
771
|
+
name.textContent = gem.name
|
772
|
+
name.appendChild(document.createElement('br'))
|
773
|
+
name.appendChild(document.createTextNode('\u00a0'))
|
774
|
+
node.appendChild(name)
|
775
|
+
|
776
|
+
fragment.appendChild(node)
|
777
|
+
})
|
778
|
+
|
779
|
+
element.appendChild(fragment)
|
780
|
+
}
|
781
|
+
|
782
|
+
FlamegraphView.prototype.onLegendMouseMove = function(e) {
|
783
|
+
var self = this
|
784
|
+
|
785
|
+
var gemElement = e.target.closest('.legend-gem')
|
786
|
+
var gemName = gemElement.getAttribute('data-gem-name')
|
787
|
+
|
788
|
+
if (self.hoveredGemName === gemName)
|
789
|
+
return
|
790
|
+
|
791
|
+
if (self.hoveredGemName) {
|
792
|
+
self.mainChart.paint(1, null, self.hoveredGemName)
|
793
|
+
self.overview.paint(1, null, self.hoveredGemName)
|
794
|
+
}
|
795
|
+
|
796
|
+
self.hoveredGemName = gemName
|
797
|
+
|
798
|
+
self.mainChart.paint(0.5, null, self.hoveredGemName)
|
799
|
+
self.overview.paint(0.5, null, self.hoveredGemName)
|
800
|
+
}
|
801
|
+
|
802
|
+
FlamegraphView.prototype.onLegendMouseOut = function() {
|
803
|
+
var self = this
|
804
|
+
|
805
|
+
if (!self.hoveredGemName)
|
806
|
+
return
|
807
|
+
|
808
|
+
self.mainChart.paint(1, null, self.hoveredGemName)
|
809
|
+
self.overview.paint(1, null, self.hoveredGemName)
|
810
|
+
self.hoveredGemName = null
|
811
|
+
}
|
812
|
+
|
813
|
+
var capturingListeners = null
|
814
|
+
function captureMouse(listeners) {
|
815
|
+
if (capturingListeners)
|
816
|
+
releaseCapture()
|
817
|
+
|
818
|
+
for (var name in listeners)
|
819
|
+
document.addEventListener(name, listeners[name], true)
|
820
|
+
capturingListeners = listeners
|
821
|
+
}
|
822
|
+
|
823
|
+
function releaseCapture() {
|
824
|
+
if (!capturingListeners)
|
825
|
+
return
|
826
|
+
|
827
|
+
for (var name in capturingListeners)
|
828
|
+
document.removeEventListener(name, capturingListeners[name], true)
|
829
|
+
capturingListeners = null
|
830
|
+
}
|
831
|
+
|
832
|
+
function guessGem(frame) {
|
833
|
+
var split = frame.split('/gems/')
|
834
|
+
if (split.length === 1) {
|
835
|
+
split = frame.split('/app/')
|
836
|
+
if (split.length === 1) {
|
837
|
+
split = frame.split('/lib/')
|
838
|
+
} else {
|
839
|
+
return split[split.length - 1].split('/')[0]
|
840
|
+
}
|
841
|
+
|
842
|
+
split = split[Math.max(split.length - 2, 0)].split('/')
|
843
|
+
return split[split.length - 1].split(':')[0]
|
844
|
+
}
|
845
|
+
else
|
846
|
+
{
|
847
|
+
return split[split.length - 1].split('/')[0].split('-', 2)[0]
|
848
|
+
}
|
849
|
+
}
|
850
|
+
|
851
|
+
function color() {
|
852
|
+
var r = parseInt(205 + Math.random() * 50)
|
853
|
+
var g = parseInt(Math.random() * 230)
|
854
|
+
var b = parseInt(Math.random() * 55)
|
855
|
+
return [r, g, b]
|
856
|
+
}
|
857
|
+
|
858
|
+
// http://stackoverflow.com/a/7419630
|
859
|
+
function rainbow(numOfSteps, step) {
|
860
|
+
// 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.
|
861
|
+
// Adam Cole, 2011-Sept-14
|
862
|
+
// HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
|
863
|
+
var r, g, b
|
864
|
+
var h = step / numOfSteps
|
865
|
+
var i = ~~(h * 6)
|
866
|
+
var f = h * 6 - i
|
867
|
+
var q = 1 - f
|
868
|
+
switch (i % 6) {
|
869
|
+
case 0: r = 1, g = f, b = 0; break
|
870
|
+
case 1: r = q, g = 1, b = 0; break
|
871
|
+
case 2: r = 0, g = 1, b = f; break
|
872
|
+
case 3: r = 0, g = q, b = 1; break
|
873
|
+
case 4: r = f, g = 0, b = 1; break
|
874
|
+
case 5: r = 1, g = 0, b = q; break
|
875
|
+
}
|
876
|
+
return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]
|
877
|
+
}
|
878
|
+
|
879
|
+
function colorString(color, opacity) {
|
880
|
+
if (typeof opacity === 'undefined')
|
881
|
+
opacity = 1
|
882
|
+
return 'rgba(' + color.join(',') + ',' + opacity + ')'
|
883
|
+
}
|
884
|
+
|
885
|
+
// http://stackoverflow.com/questions/1960473/unique-values-in-an-array
|
886
|
+
function getUnique(orig) {
|
887
|
+
var o = {}
|
888
|
+
for (var i = 0; i < orig.length; i++) o[orig[i]] = 1
|
889
|
+
return Object.keys(o)
|
890
|
+
}
|
891
|
+
|
892
|
+
function centerTruncate(text, maxLength) {
|
893
|
+
var charactersToKeep = maxLength - 1
|
894
|
+
if (charactersToKeep <= 0)
|
895
|
+
return ''
|
896
|
+
if (text.length <= charactersToKeep)
|
897
|
+
return text
|
898
|
+
|
899
|
+
var prefixLength = Math.ceil(charactersToKeep / 2)
|
900
|
+
var suffixLength = charactersToKeep - prefixLength
|
901
|
+
var prefix = text.substr(0, prefixLength)
|
902
|
+
var suffix = suffixLength > 0 ? text.substr(-suffixLength) : ''
|
903
|
+
|
904
|
+
return [prefix, '\u2026', suffix].join('')
|
905
|
+
}
|
906
|
+
|
907
|
+
function flamegraph(data) {
|
908
|
+
var info = {}
|
909
|
+
data.forEach(function(d) {
|
910
|
+
var i = info[d.frame_id]
|
911
|
+
if (!i)
|
912
|
+
info[d.frame_id] = i = {frames: [], samples: [], color: color()}
|
913
|
+
i.frames.push(d)
|
914
|
+
for (var j = 0; j < d.width; j++) {
|
915
|
+
i.samples.push(d.x + j)
|
916
|
+
}
|
917
|
+
})
|
918
|
+
|
919
|
+
// Samples may overlap on the same line
|
920
|
+
for (var r in info) {
|
921
|
+
if (info[r].samples) {
|
922
|
+
info[r].samples = getUnique(info[r].samples)
|
923
|
+
}
|
924
|
+
}
|
925
|
+
|
926
|
+
// assign some colors, analyze samples per gem
|
927
|
+
var gemStats = {}
|
928
|
+
var topFrames = {}
|
929
|
+
var lastFrame = {frame: 'd52e04d-df28-41ed-a215-b6ec840a8ea5', x: -1}
|
930
|
+
|
931
|
+
data.forEach(function(d) {
|
932
|
+
var gem = guessGem(d.file)
|
933
|
+
var stat = gemStats[gem]
|
934
|
+
d.gemName = gem
|
935
|
+
|
936
|
+
if (!stat) {
|
937
|
+
gemStats[gem] = stat = {name: gem, samples: [], frames: []}
|
938
|
+
}
|
939
|
+
|
940
|
+
stat.frames.push(d.frame_id)
|
941
|
+
for (var j = 0; j < d.width; j++) {
|
942
|
+
stat.samples.push(d.x + j)
|
943
|
+
}
|
944
|
+
// This assumes the traversal is in order
|
945
|
+
if (lastFrame.x !== d.x) {
|
946
|
+
var topFrame = topFrames[lastFrame.frame_id]
|
947
|
+
if (!topFrame) {
|
948
|
+
topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0}
|
949
|
+
}
|
950
|
+
topFrame.exclusiveCount += 1
|
951
|
+
lastFrame.topFrame = topFrame
|
952
|
+
}
|
953
|
+
lastFrame = d
|
954
|
+
})
|
955
|
+
|
956
|
+
var topFrame = topFrames[lastFrame.frame_id]
|
957
|
+
if (!topFrame) {
|
958
|
+
topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0}
|
959
|
+
}
|
960
|
+
topFrame.exclusiveCount += 1
|
961
|
+
lastFrame.topFrame = topFrame
|
962
|
+
|
963
|
+
var totalGems = 0
|
964
|
+
for (var k in gemStats) {
|
965
|
+
totalGems++
|
966
|
+
gemStats[k].samples = getUnique(gemStats[k].samples)
|
967
|
+
}
|
968
|
+
|
969
|
+
var gemsSorted = Object.keys(gemStats).map(function(k) { return gemStats[k] })
|
970
|
+
gemsSorted.sort(function(a, b) { return b.samples.length - a.samples.length })
|
971
|
+
|
972
|
+
var currentIndex = 0
|
973
|
+
gemsSorted.forEach(function(stat) {
|
974
|
+
stat.color = rainbow(totalGems, currentIndex)
|
975
|
+
currentIndex += 1
|
976
|
+
|
977
|
+
for (var x = 0; x < stat.frames.length; x++) {
|
978
|
+
info[stat.frames[x]].color = stat.color
|
979
|
+
}
|
980
|
+
})
|
981
|
+
|
982
|
+
new FlamegraphView(data, info, gemsSorted)
|
983
|
+
}
|