stackprof-webnav 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: d7397a6a9f217b90faf1e1ea5ab1a2bb3b616ffbf678d7953ab1a27c6f69b165
4
- data.tar.gz: 75dec19185527cecb7a7b7cc5cf1ee1eedb0f9865f27a65eb44f8fe174337e25
2
+ SHA1:
3
+ metadata.gz: 44d5eca8d093b0c0830b858dc66cdb3bcbf05fa6
4
+ data.tar.gz: cea99af6d10a337743fad39da1d4475bae8c44ea
5
5
  SHA512:
6
- metadata.gz: fd15f2fa5bf7461ac0d001d67c1e9629f0238a20bc930141dc357c4f6b8290f4483443d47c7e5715e007db85290c07b256a738d518170ca7259fb0baf78c2ec4
7
- data.tar.gz: 630b5916db9902f8df747286519fcfc82f653ee373ee8abe8af99654bf2a05b3e3dfdba8ede11664ea9801a08792618171203b4bff629deb176289e1b3fb52f8
6
+ metadata.gz: d68d970fab0ef478ae8cbcac41adf9998ae8995c6b1824871bd2756b0e9afc78e140319975daeb6b228773e56e4a8cb5f606dc2fcbf0751a7ca5ce7ceee5434b
7
+ data.tar.gz: 5d2dda6a5f15bbd10a4fcf5686c5f8760419512d7f0db99026621aa15824727a36a33d0effc53a947255e339f81fedcf32e21386bf25c2b9a6fe77670320bfbb
data/.gitignore CHANGED
@@ -15,3 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ tags
19
+ .DS_Store
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ script: bundle exec rspec
3
+ before_install:
4
+ - "sudo apt-get install graphviz"
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in stackprof-webnav.gemspec
4
4
  gemspec
5
+
6
+ gem 'pry'
data/README.md CHANGED
@@ -1,16 +1,16 @@
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
- ![main screenshot][main-screenshot]
7
+ ![][https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/directory.png?raw=true]
8
+
9
+ ![][https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/overview.png?raw=true]
10
10
 
11
- ![method screenshot][method-screenshot]
11
+ ![][https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/callgraph.png?raw=true]
12
12
 
13
- ![file screenshot][file-screenshot]
13
+ ![][https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/flamegraph.png?raw=true]
14
14
 
15
15
  ## Usage
16
16
 
@@ -19,22 +19,31 @@ Provides a web ui to inspect stackprof dumps.
19
19
  $ gem install stackprof-webnav
20
20
  ```
21
21
 
22
- ### Pass a dump/URI to it
22
+ ### Run the server
23
+
24
+ ```bash
25
+ $ stackprof-webnav
26
+ ```
27
+
28
+ By default it will list all files in the current directory. You can click in the
29
+ web interface to try to open any file as a dump.
30
+
31
+ Additionally, you can list another directory by passing the `-d` flag:
32
+
33
+ ```bash
34
+ $ stackprof-webnav -d /my/folder/with/dumps
35
+ ```
36
+
37
+ Or launch it with a dump preselected:
38
+
23
39
  ```bash
24
40
  $ stackprof-webnav -f /path/to/stackprof.dump
25
- $ stackprof-webnav -u http://path/to/stackprof.dump
26
- $ stackprof-webnav -b http://amazon/s3/bucketlisting.xml
27
41
  ```
28
42
 
29
43
  See [stackprof gem][create-dump] homepage to learn how to create dumps.
30
- See [amazon s3 API docs][list-bucket-contents] to see the URI format for S3 bucket listings.
31
44
 
32
45
  ### Profit
33
- Open the browser at localhost:9292. If you've used the -f or -u form, you can navigate the dump. If you've used the -b form, you'll see a listing of the keys in the bucket -- click on one that is a dump to browse through it.
34
-
35
- ## Caveats
36
- - no tests, this gem was created for my personal usage in a hack stream,
37
- bugs may occur
46
+ Open the browser at localhost:9292
38
47
 
39
48
  ## Contributing
40
49
 
@@ -45,7 +54,3 @@ Open the browser at localhost:9292. If you've used the -f or -u form, you can na
45
54
  5. Create new Pull Request
46
55
 
47
56
  [create-dump]: https://github.com/tmm1/stackprof#getting-started
48
- [main-screenshot]: https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/main.png?raw=true
49
- [method-screenshot]: https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/method.png?raw=true
50
- [file-screenshot]: https://github.com/alisnic/stackprof-webnav/blob/master/screenshots/file.png?raw=true
51
- [list-bucket-contents]: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html
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
@@ -8,17 +8,15 @@ options = {
8
8
  }
9
9
 
10
10
  parser = OptionParser.new(ARGV) do |o|
11
- o.banner = "Usage: stackprof-webnav [-f localfile.dump]|[-u http://path/to/file.dump]|[-b http://path/to/s3/bucket/listing] [-p NUMBER]"
11
+ o.banner = "Usage: stackprof-webnav [-f localfile.dump]|[-d directory]|[-p NUMBER]"
12
12
  o.on('-f [LOCALFILE]', 'Local file path to dump') {|filepath| options[:filepath] = filepath }
13
- o.on('-u [URI]', 'URI path to dump') {|uri| options[:uri] = uri }
14
- o.on('-b [URI]', 'URI path to Amazon S3 bucket listing') {|bucket| options[:bucket] = bucket}
13
+ o.on('-d [DIRECTORY]', 'path to a directory with dumps') {|directory| options[:directory] = directory}
15
14
  o.on('-p [PORT]', 'Server port') {|port| options[:port] = port }
16
15
  end
17
16
 
18
17
  parser.parse!
19
- parser.abort(parser.help) unless [:filepath, :uri, :bucket].any? {|key| options.key?(key)}
20
18
 
21
19
  server = StackProf::Webnav::Server
22
20
  server.cmd_options = options
23
21
 
24
- Rack::Handler.get('webrick').run server.new, :Port => options[:port]
22
+ Rack::Handler.pick(['thin', 'webrick']).run server.new, :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
@@ -34,25 +34,6 @@ module StackProf
34
34
  end
35
35
  end
36
36
 
37
- def listing_dumps
38
- Server.report_dump_listing += "/" unless Server.report_dump_listing.end_with?("/")
39
- xml_data = Net::HTTP.get(URI.parse(Server.report_dump_listing))
40
- if xml_data
41
- doc = REXML::Document.new(xml_data)
42
- dumps = []
43
- doc.elements.each('ListBucketResult/Contents') do |ele|
44
- dumps << {
45
- :key => ele.elements["Key"].text,
46
- :date => ele.elements["LastModified"].text,
47
- :size => number_with_delimiter(ele.elements["Size"].text.to_i),
48
- :uri => Server.report_dump_listing + ele.elements["Key"].text
49
- }
50
- end
51
- end
52
- dumps.sort_by! { |hash| hash[:date] }
53
- dumps.reverse!
54
- end
55
-
56
37
  def method_info name
57
38
  name = /#{Regexp.escape name}/ unless Regexp === name
58
39
  frames = report.frames.select do |frame, info|
@@ -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
+ }