stackprof-webnav 0.1.0 → 1.0.0

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 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
+ }