stackprof 0.2.10 → 0.2.11

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
2
  SHA1:
3
- metadata.gz: afa4ecbd5eba7e06625bb12c770da033682539f4
4
- data.tar.gz: fc0f0ad1bcc0e1d726c82437a57aa6c0beb8b3e2
3
+ metadata.gz: 88f62f89ebff2c249b7eaabcb330f12997f09120
4
+ data.tar.gz: 629071b6584701d830b827e5d4b9b0951eaa0282
5
5
  SHA512:
6
- metadata.gz: ddb8ed15ef2275231790a475a770a72753fe87aee98fc77d2c7a19d9405228f72832b76f941c84db94bf62fd5a445fb9d3b8e3521f2bc016f6dc170229d56a0e
7
- data.tar.gz: b911543ceb1e019086adda7780aa72b48dc6bede83ee99cf746b5cac557f5f9a395c5f88ac3a5f34414e1c1dc8f56c5559e770e8eb355cc9a0c9f7071bb144fe
6
+ metadata.gz: d02684e9bd77e2b561f626a69a897e1ec2c89b870b30ef54709165cc1debb966a56127f3053d7a27e81cc7c3c62fef50865ebd5bcbfd6d2b28630add6280e479
7
+ data.tar.gz: 2d5d70aaa53080112d8f794d1d5412cd8225f451f43431f30209e42854b5de8246ff77347ef39496c2b55b447d6cf114116b81dc2eb458adea16a5d9ce03606e
@@ -1,8 +1,8 @@
1
+ sudo: false
1
2
  language: ruby
2
3
  rvm:
3
- - 2.1.8
4
- - 2.2.4
5
- - 2.3.0
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3
7
+ - 2.4
6
8
  - ruby-head
7
- script: "bundle exec rake test"
8
- sudo: false
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- stackprof (0.2.9)
4
+ stackprof (0.2.11)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -22,3 +22,6 @@ DEPENDENCIES
22
22
  mocha (~> 0.14)
23
23
  rake-compiler (~> 0.9)
24
24
  stackprof!
25
+
26
+ BUNDLED WITH
27
+ 1.15.4
data/README.md CHANGED
@@ -318,6 +318,7 @@ Option | Meaning
318
318
  `interval` | mode-relative sample rate [c.f.](#sampling)
319
319
  `aggregate` | defaults: `true` - if `false` disables [aggregation](#aggregation)
320
320
  `raw` | defaults `false` - if `true` collects the extra data required by the `--flamegraph` and `--stackcollapse` report types
321
+ `save_every`| (rack middleware only) write the target file after this many requests
321
322
 
322
323
  ### todo
323
324
 
@@ -12,7 +12,8 @@ parser = OptionParser.new(ARGV) do |o|
12
12
  o.on('--limit [num]', Integer, 'Limit --text, --files, or --graphviz output to N entries'){ |n| options[:limit] = n }
13
13
  o.on('--sort-total', "Sort --text or --files output on total samples\n\n"){ options[:sort] = true }
14
14
  o.on('--method [grep]', 'Zoom into specified method'){ |f| options[:format] = :method; options[:filter] = f }
15
- o.on('--file [grep]', "Show annotated code for specified file\n\n"){ |f| options[:format] = :file; options[:filter] = f }
15
+ o.on('--file [grep]', "Show annotated code for specified file"){ |f| options[:format] = :file; options[:filter] = f }
16
+ o.on('--walk', "Walk the stacktrace interactively\n\n"){ |f| options[:walk] = true }
16
17
  o.on('--callgrind', 'Callgrind output (use with kcachegrind, stackprof-gprof2dot.py)'){ options[:format] = :callgrind }
17
18
  o.on('--graphviz', "Graphviz output (use with dot)"){ options[:format] = :graphviz }
18
19
  o.on('--node-fraction [frac]', OptionParser::DecimalNumeric, 'Drop nodes representing less than [frac] fraction of samples'){ |n| options[:node_fraction] = n }
@@ -74,7 +75,7 @@ when :stackcollapse
74
75
  when :flamegraph
75
76
  report.print_flamegraph
76
77
  when :method
77
- report.print_method(options[:filter])
78
+ options[:walk] ? report.walk_method(options[:filter]) : report.print_method(options[:filter])
78
79
  when :file
79
80
  report.print_file(options[:filter])
80
81
  when :files
@@ -20,6 +20,7 @@
20
20
  typedef struct {
21
21
  size_t total_samples;
22
22
  size_t caller_samples;
23
+ int already_accounted_in_total;
23
24
  st_table *edges;
24
25
  st_table *lines;
25
26
  } frame_data_t;
@@ -38,18 +39,27 @@ static struct {
38
39
  size_t raw_samples_capa;
39
40
  size_t raw_sample_index;
40
41
 
42
+ struct timeval last_sample_at;
43
+ int *raw_timestamp_deltas;
44
+ size_t raw_timestamp_deltas_len;
45
+ size_t raw_timestamp_deltas_capa;
46
+
41
47
  size_t overall_signals;
42
48
  size_t overall_samples;
43
49
  size_t during_gc;
50
+ size_t unrecorded_gc_samples;
44
51
  st_table *frames;
45
52
 
53
+ VALUE fake_gc_frame;
54
+ VALUE fake_gc_frame_name;
55
+ VALUE empty_string;
46
56
  VALUE frames_buffer[BUF_SIZE];
47
57
  int lines_buffer[BUF_SIZE];
48
58
  } _stackprof;
49
59
 
50
60
  static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
51
61
  static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines;
52
- static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_frames, sym_out, sym_aggregate;
62
+ static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_frames, sym_out, sym_aggregate, sym_raw_timestamp_deltas;
53
63
  static VALUE sym_gc_samples, objtracer;
54
64
  static VALUE gc_hook;
55
65
  static VALUE rb_mStackProf;
@@ -120,6 +130,10 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
120
130
  _stackprof.interval = interval;
121
131
  _stackprof.out = out;
122
132
 
133
+ if (raw) {
134
+ gettimeofday(&_stackprof.last_sample_at, NULL);
135
+ }
136
+
123
137
  return Qtrue;
124
138
  }
125
139
 
@@ -187,16 +201,24 @@ frame_i(st_data_t key, st_data_t val, st_data_t arg)
187
201
 
188
202
  rb_hash_aset(results, rb_obj_id(frame), details);
189
203
 
190
- name = rb_profile_frame_full_label(frame);
191
- rb_hash_aset(details, sym_name, name);
204
+ if (frame == _stackprof.fake_gc_frame) {
205
+ name = _stackprof.fake_gc_frame_name;
206
+ file = _stackprof.empty_string;
207
+ line = INT2FIX(0);
208
+ } else {
209
+ name = rb_profile_frame_full_label(frame);
192
210
 
193
- file = rb_profile_frame_absolute_path(frame);
194
- if (NIL_P(file))
195
- file = rb_profile_frame_path(frame);
196
- rb_hash_aset(details, sym_file, file);
211
+ file = rb_profile_frame_absolute_path(frame);
212
+ if (NIL_P(file))
213
+ file = rb_profile_frame_path(frame);
214
+ line = rb_profile_frame_first_lineno(frame);
215
+ }
197
216
 
198
- if ((line = rb_profile_frame_first_lineno(frame)) != INT2FIX(0))
217
+ rb_hash_aset(details, sym_name, name);
218
+ rb_hash_aset(details, sym_file, file);
219
+ if (line != INT2FIX(0)) {
199
220
  rb_hash_aset(details, sym_line, line);
221
+ }
200
222
 
201
223
  rb_hash_aset(details, sym_total_samples, SIZET2NUM(frame_data->total_samples));
202
224
  rb_hash_aset(details, sym_samples, SIZET2NUM(frame_data->caller_samples));
@@ -230,7 +252,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
230
252
  return Qnil;
231
253
 
232
254
  results = rb_hash_new();
233
- rb_hash_aset(results, sym_version, DBL2NUM(1.1));
255
+ rb_hash_aset(results, sym_version, DBL2NUM(1.2));
234
256
  rb_hash_aset(results, sym_mode, _stackprof.mode);
235
257
  rb_hash_aset(results, sym_interval, _stackprof.interval);
236
258
  rb_hash_aset(results, sym_samples, SIZET2NUM(_stackprof.overall_samples));
@@ -262,9 +284,23 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
262
284
  _stackprof.raw_samples_len = 0;
263
285
  _stackprof.raw_samples_capa = 0;
264
286
  _stackprof.raw_sample_index = 0;
265
- _stackprof.raw = 0;
266
287
 
267
288
  rb_hash_aset(results, sym_raw, raw_samples);
289
+
290
+ VALUE raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_timestamp_deltas_len);
291
+
292
+ for (n = 0; n < _stackprof.raw_timestamp_deltas_len; n++) {
293
+ rb_ary_push(raw_timestamp_deltas, INT2FIX(_stackprof.raw_timestamp_deltas[n]));
294
+ }
295
+
296
+ free(_stackprof.raw_timestamp_deltas);
297
+ _stackprof.raw_timestamp_deltas = NULL;
298
+ _stackprof.raw_timestamp_deltas_len = 0;
299
+ _stackprof.raw_timestamp_deltas_capa = 0;
300
+
301
+ rb_hash_aset(results, sym_raw_timestamp_deltas, raw_timestamp_deltas);
302
+
303
+ _stackprof.raw = 0;
268
304
  }
269
305
 
270
306
  if (argc == 1)
@@ -340,13 +376,12 @@ st_numtable_increment(st_table *table, st_data_t key, size_t increment)
340
376
  }
341
377
 
342
378
  void
343
- stackprof_record_sample()
379
+ stackprof_record_sample_for_stack(int num, int timestamp_delta)
344
380
  {
345
- int num, i, n;
381
+ int i, n;
346
382
  VALUE prev_frame = Qnil;
347
383
 
348
384
  _stackprof.overall_samples++;
349
- num = rb_profile_frames(0, sizeof(_stackprof.frames_buffer) / sizeof(VALUE), _stackprof.frames_buffer, _stackprof.lines_buffer);
350
385
 
351
386
  if (_stackprof.raw) {
352
387
  int found = 0;
@@ -356,7 +391,7 @@ stackprof_record_sample()
356
391
  _stackprof.raw_samples = malloc(sizeof(VALUE) * _stackprof.raw_samples_capa);
357
392
  }
358
393
 
359
- if (_stackprof.raw_samples_capa <= _stackprof.raw_samples_len + num) {
394
+ while (_stackprof.raw_samples_capa <= _stackprof.raw_samples_len + (num + 2)) {
360
395
  _stackprof.raw_samples_capa *= 2;
361
396
  _stackprof.raw_samples = realloc(_stackprof.raw_samples, sizeof(VALUE) * _stackprof.raw_samples_capa);
362
397
  }
@@ -382,6 +417,24 @@ stackprof_record_sample()
382
417
  }
383
418
  _stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)1;
384
419
  }
420
+
421
+ if (!_stackprof.raw_timestamp_deltas) {
422
+ _stackprof.raw_timestamp_deltas_capa = 100;
423
+ _stackprof.raw_timestamp_deltas = malloc(sizeof(int) * _stackprof.raw_timestamp_deltas_capa);
424
+ _stackprof.raw_timestamp_deltas_len = 0;
425
+ }
426
+
427
+ while (_stackprof.raw_timestamp_deltas_capa <= _stackprof.raw_timestamp_deltas_len + 1) {
428
+ _stackprof.raw_timestamp_deltas_capa *= 2;
429
+ _stackprof.raw_timestamp_deltas = realloc(_stackprof.raw_timestamp_deltas, sizeof(int) * _stackprof.raw_timestamp_deltas_capa);
430
+ }
431
+
432
+ _stackprof.raw_timestamp_deltas[_stackprof.raw_timestamp_deltas_len++] = timestamp_delta;
433
+ }
434
+
435
+ for (i = 0; i < num; i++) {
436
+ VALUE frame = _stackprof.frames_buffer[i];
437
+ sample_for(frame)->already_accounted_in_total = 0;
385
438
  }
386
439
 
387
440
  for (i = 0; i < num; i++) {
@@ -389,7 +442,9 @@ stackprof_record_sample()
389
442
  VALUE frame = _stackprof.frames_buffer[i];
390
443
  frame_data_t *frame_data = sample_for(frame);
391
444
 
392
- frame_data->total_samples++;
445
+ if (!frame_data->already_accounted_in_total)
446
+ frame_data->total_samples++;
447
+ frame_data->already_accounted_in_total = 1;
393
448
 
394
449
  if (i == 0) {
395
450
  frame_data->caller_samples++;
@@ -409,6 +464,68 @@ stackprof_record_sample()
409
464
 
410
465
  prev_frame = frame;
411
466
  }
467
+
468
+ if (_stackprof.raw) {
469
+ gettimeofday(&_stackprof.last_sample_at, NULL);
470
+ }
471
+ }
472
+
473
+ void
474
+ stackprof_record_sample()
475
+ {
476
+ int timestamp_delta = 0;
477
+ if (_stackprof.raw) {
478
+ struct timeval t;
479
+ gettimeofday(&t, NULL);
480
+ struct timeval diff;
481
+ timersub(&t, &_stackprof.last_sample_at, &diff);
482
+ timestamp_delta = (1000 * diff.tv_sec) + diff.tv_usec;
483
+ }
484
+ int num = rb_profile_frames(0, sizeof(_stackprof.frames_buffer) / sizeof(VALUE), _stackprof.frames_buffer, _stackprof.lines_buffer);
485
+ stackprof_record_sample_for_stack(num, timestamp_delta);
486
+ }
487
+
488
+ void
489
+ stackprof_record_gc_samples()
490
+ {
491
+ int delta_to_first_unrecorded_gc_sample = 0;
492
+ if (_stackprof.raw) {
493
+ struct timeval t;
494
+ gettimeofday(&t, NULL);
495
+ struct timeval diff;
496
+ timersub(&t, &_stackprof.last_sample_at, &diff);
497
+
498
+ // We don't know when the GC samples were actually marked, so let's
499
+ // assume that they were marked at a perfectly regular interval.
500
+ delta_to_first_unrecorded_gc_sample = (1000 * diff.tv_sec + diff.tv_usec) - (_stackprof.unrecorded_gc_samples - 1) * _stackprof.interval;
501
+ if (delta_to_first_unrecorded_gc_sample < 0) {
502
+ delta_to_first_unrecorded_gc_sample = 0;
503
+ }
504
+ }
505
+
506
+ int i;
507
+
508
+ _stackprof.frames_buffer[0] = _stackprof.fake_gc_frame;
509
+ _stackprof.lines_buffer[0] = 0;
510
+
511
+ for (i = 0; i < _stackprof.unrecorded_gc_samples; i++) {
512
+ int timestamp_delta = i == 0 ? delta_to_first_unrecorded_gc_sample : _stackprof.interval;
513
+ stackprof_record_sample_for_stack(1, timestamp_delta);
514
+ }
515
+ _stackprof.during_gc += _stackprof.unrecorded_gc_samples;
516
+ _stackprof.unrecorded_gc_samples = 0;
517
+ }
518
+
519
+ static void
520
+ stackprof_gc_job_handler(void *data)
521
+ {
522
+ static int in_signal_handler = 0;
523
+ if (in_signal_handler) return;
524
+ if (!_stackprof.running) return;
525
+
526
+ in_signal_handler++;
527
+ stackprof_record_gc_samples();
528
+ in_signal_handler--;
412
529
  }
413
530
 
414
531
  static void
@@ -427,10 +544,12 @@ static void
427
544
  stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
428
545
  {
429
546
  _stackprof.overall_signals++;
430
- if (rb_during_gc())
431
- _stackprof.during_gc++, _stackprof.overall_samples++;
432
- else
433
- rb_postponed_job_register_one(0, stackprof_job_handler, 0);
547
+ if (rb_during_gc()) {
548
+ _stackprof.unrecorded_gc_samples++;
549
+ rb_postponed_job_register_one(0, stackprof_gc_job_handler, (void*)0);
550
+ } else {
551
+ rb_postponed_job_register_one(0, stackprof_job_handler, (void*)0);
552
+ }
434
553
  }
435
554
 
436
555
  static void
@@ -524,6 +643,7 @@ Init_stackprof(void)
524
643
  S(mode);
525
644
  S(interval);
526
645
  S(raw);
646
+ S(raw_timestamp_deltas);
527
647
  S(out);
528
648
  S(frames);
529
649
  S(aggregate);
@@ -532,6 +652,21 @@ Init_stackprof(void)
532
652
  gc_hook = Data_Wrap_Struct(rb_cObject, stackprof_gc_mark, NULL, &_stackprof);
533
653
  rb_global_variable(&gc_hook);
534
654
 
655
+ _stackprof.raw_samples = NULL;
656
+ _stackprof.raw_samples_len = 0;
657
+ _stackprof.raw_samples_capa = 0;
658
+ _stackprof.raw_sample_index = 0;
659
+
660
+ _stackprof.raw_timestamp_deltas = NULL;
661
+ _stackprof.raw_timestamp_deltas_len = 0;
662
+ _stackprof.raw_timestamp_deltas_capa = 0;
663
+
664
+ _stackprof.fake_gc_frame = INT2FIX(0x9C);
665
+ _stackprof.empty_string = rb_str_new_cstr("");
666
+ _stackprof.fake_gc_frame_name = rb_str_new_cstr("(garbage collection)");
667
+ rb_global_variable(&_stackprof.fake_gc_frame_name);
668
+ rb_global_variable(&_stackprof.empty_string);
669
+
535
670
  rb_mStackProf = rb_define_module("StackProf");
536
671
  rb_define_singleton_method(rb_mStackProf, "running?", stackprof_running_p, 0);
537
672
  rb_define_singleton_method(rb_mStackProf, "run", stackprof_run, -1);
@@ -1,357 +1,983 @@
1
- var guessGem = function(frame) {
2
- var split = frame.split('/gems/');
3
- if(split.length == 1) {
4
- split = frame.split('/app/');
5
- if(split.length == 1) {
6
- split = frame.split('/lib/');
7
- } else {
8
- return split[split.length-1].split('/')[0]
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
9
  }
10
10
 
11
- split = split[Math.max(split.length-2,0)].split('/');
12
- return split[split.length-1].split(':')[0];
13
- }
14
- else
15
- {
16
- return split[split.length -1].split('/')[0].split('-', 2)[0];
11
+ return Boolean(elements[index])
17
12
  }
18
13
  }
19
14
 
20
- var color = function() {
21
- var r = parseInt(205 + Math.random() * 50);
22
- var g = parseInt(Math.random() * 230);
23
- var b = parseInt(Math.random() * 55);
24
- return "rgb(" + r + "," + g + "," + b + ")";
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
+ }
25
29
  }
26
30
 
27
- // http://stackoverflow.com/a/7419630
28
- var rainbow = function(numOfSteps, step) {
29
- // This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distiguishable vibrant markers in Google Maps and other apps.
30
- // Adam Cole, 2011-Sept-14
31
- // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
32
- var r, g, b;
33
- var h = step / numOfSteps;
34
- var i = ~~(h * 6);
35
- var f = h * 6 - i;
36
- var q = 1 - f;
37
- switch(i % 6){
38
- case 0: r = 1, g = f, b = 0; break;
39
- case 1: r = q, g = 1, b = 0; break;
40
- case 2: r = 0, g = 1, b = f; break;
41
- case 3: r = 0, g = q, b = 1; break;
42
- case 4: r = f, g = 0, b = 1; break;
43
- case 5: r = 1, g = 0, b = q; break;
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
44
52
  }
45
- var c = "#" + ("00" + (~ ~(r * 255)).toString(16)).slice(-2) + ("00" + (~ ~(g * 255)).toString(16)).slice(-2) + ("00" + (~ ~(b * 255)).toString(16)).slice(-2);
46
- return (c);
53
+ })()
47
54
  }
48
55
 
49
- // http://stackoverflow.com/questions/1960473/unique-values-in-an-array
50
- var getUnique = function(orig) {
51
- var o = {}, a = []
52
- for (var i = 0; i < orig.length; i++) o[orig[i]] = 1
53
- for (var e in o) a.push(e)
54
- return a
56
+ function EventSource() {
57
+ var self = this
58
+
59
+ self.eventListeners = {}
55
60
  }
56
61
 
57
- function flamegraph(data) {
58
- var maxX = 0;
59
- var maxY = 0;
60
- var minY = 10000;
61
- $.each(data, function(){
62
- maxX = Math.max(maxX, this.x + this.width);
63
- maxY = Math.max(maxY, this.y);
64
- minY = Math.min(minY, this.y);
65
- });
66
-
67
- // normalize Y
68
- if (minY > 0) {
69
- $.each(data, function(){
70
- this.y -= minY
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)
71
285
  })
72
- maxY -= minY
73
- minY = 0
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
74
389
  }
75
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
+
76
660
  var margin = {top: 10, right: 10, bottom: 10, left: 10}
77
- var width = $(window).width() - 200 - margin.left - margin.right;
78
- var height = $(window).height() * 0.70 - margin.top - margin.bottom;
79
- var height2 = $(window).height() * 0.30 - 60 - margin.top - margin.bottom;
80
-
81
- $('.flamegraph').width(width + margin.left + margin.right).height(height + margin.top + margin.bottom);
82
- $('.zoom').width(width + margin.left + margin.right).height(height2 + margin.top + margin.bottom);
83
-
84
- var xScale = d3.scale.linear()
85
- .domain([0, maxX])
86
- .range([0, width]);
87
-
88
- var xScale2 = d3.scale.linear()
89
- .domain([0, maxX])
90
- .range([0, width])
91
-
92
- var yScale = d3.scale.linear()
93
- .domain([0, maxY])
94
- .range([0,height]);
95
-
96
- var yScale2 = d3.scale.linear()
97
- .domain([0, maxY])
98
- .range([0,height2]);
99
-
100
- var zoomXRatio = 1
101
- var zoomed = function() {
102
- svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + (zoomXRatio*d3.event.scale) + "," + d3.event.scale + ")");
103
-
104
- var x = xScale.domain(), y = yScale.domain()
105
- brush.extent([ [x[0]/zoomXRatio, y[0]], [x[1]/zoomXRatio, y[1]] ])
106
- if (x[1] == maxX && y[1] == maxY)
107
- brush.clear()
108
- svg2.select('g.brush').call(brush)
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
109
713
  }
110
714
 
111
- var zoom = d3.behavior.zoom().x(xScale).y(yScale).scaleExtent([1, 14]).on('zoom', zoomed)
112
-
113
- var svg2 = d3.select('.zoom').append('svg').attr('width', '100%').attr('height', '100%').append('svg:g')
114
- .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
115
- .append('g').attr('class', 'graph')
116
-
117
- var svg = d3.select(".flamegraph")
118
- .append("svg")
119
- .attr("width", "100%")
120
- .attr("height", "100%")
121
- .attr("pointer-events", "all")
122
- .append('svg:g')
123
- .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
124
- .call(zoom)
125
- .append('svg:g').attr('class', 'graph');
126
-
127
- // so zoom works everywhere
128
- svg.append("rect")
129
- .attr("x",function(d) { return xScale(0); })
130
- .attr("y",function(d) { return yScale(0);})
131
- .attr("width", function(d){return xScale(maxX);})
132
- .attr("height", yScale(maxY))
133
- .attr("fill", "white");
134
-
135
- var samplePercentRaw = function(samples, exclusive) {
136
- var ret = [samples, ((samples / maxX) * 100).toFixed(2)]
137
- if (exclusive)
138
- ret = ret.concat([exclusive, ((exclusive / maxX) * 100).toFixed(2)])
139
- return ret;
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]
140
848
  }
849
+ }
141
850
 
142
- var samplePercent = function(samples, exclusive) {
143
- var info = samplePercentRaw(samples, exclusive)
144
- var samplesPct = info[1], exclusivePct = info[3]
145
- var ret = " (" + samples + " sample" + (samples == 1 ? "" : "s") + " - " + samplesPct + "%) ";
146
- if (exclusive)
147
- ret += " (" + exclusive + " exclusive - " + exclusivePct + "%) ";
148
- return ret;
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
149
875
  }
876
+ return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]
877
+ }
150
878
 
151
- var info = {};
879
+ function colorString(color, opacity) {
880
+ if (typeof opacity === 'undefined')
881
+ opacity = 1
882
+ return 'rgba(' + color.join(',') + ',' + opacity + ')'
883
+ }
152
884
 
153
- var mouseover = function(d) {
154
- var i = info[d.frame_id];
155
- var shortFile = d.file.replace(/^.+\/(gems|app|lib|config|jobs)/, '$1')
156
- var data = samplePercentRaw(i.samples.length, d.topFrame ? d.topFrame.exclusiveCount : 0)
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
+ }
157
891
 
158
- $('.info')
159
- .css('background-color', i.color)
160
- .find('.frame').text(d.frame).end()
161
- .find('.file').text(shortFile).end()
162
- .find('.samples').text(data[0] + ' samples ('+data[1]+'%)').end()
163
- .find('.exclusive').text('')
892
+ function centerTruncate(text, maxLength) {
893
+ var charactersToKeep = maxLength - 1
894
+ if (charactersToKeep <= 0)
895
+ return ''
896
+ if (text.length <= charactersToKeep)
897
+ return text
164
898
 
165
- if (data[3])
166
- $('.info .exclusive').text(data[2] + ' exclusive ('+data[3]+'%)')
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) : ''
167
903
 
168
- d3.selectAll(i.nodes)
169
- .attr('opacity',0.5);
170
- };
904
+ return [prefix, '\u2026', suffix].join('')
905
+ }
171
906
 
172
- var mouseout = function(d) {
173
- var i = info[d.frame_id];
174
- $('.info').css('background-color', 'none').find('.frame, .file, .samples, .exclusive').text('')
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
+ })
175
918
 
176
- d3.selectAll(i.nodes)
177
- .attr('opacity',1);
178
- };
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
+ }
179
925
 
180
926
  // assign some colors, analyze samples per gem
181
927
  var gemStats = {}
182
928
  var topFrames = {}
183
929
  var lastFrame = {frame: 'd52e04d-df28-41ed-a215-b6ec840a8ea5', x: -1}
184
930
 
185
- $.each(data, function(){
186
- var gem = guessGem(this.file);
187
- var stat = gemStats[gem];
188
- this.gemName = gem
931
+ data.forEach(function(d) {
932
+ var gem = guessGem(d.file)
933
+ var stat = gemStats[gem]
934
+ d.gemName = gem
189
935
 
190
- if(!stat) {
191
- gemStats[gem] = stat = {name: gem, samples: [], frames: [], nodes:[]};
936
+ if (!stat) {
937
+ gemStats[gem] = stat = {name: gem, samples: [], frames: []}
192
938
  }
193
939
 
194
- stat.frames.push(this.frame_id);
195
- for(var j=0; j < this.width; j++){
196
- stat.samples.push(this.x + j);
940
+ stat.frames.push(d.frame_id)
941
+ for (var j = 0; j < d.width; j++) {
942
+ stat.samples.push(d.x + j)
197
943
  }
198
944
  // This assumes the traversal is in order
199
- if (lastFrame.x != this.x) {
945
+ if (lastFrame.x !== d.x) {
200
946
  var topFrame = topFrames[lastFrame.frame_id]
201
947
  if (!topFrame) {
202
948
  topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0}
203
949
  }
204
- topFrame.exclusiveCount += 1;
205
- lastFrame.topFrame = topFrame;
950
+ topFrame.exclusiveCount += 1
951
+ lastFrame.topFrame = topFrame
206
952
  }
207
- lastFrame = this;
208
-
209
- });
953
+ lastFrame = d
954
+ })
210
955
 
211
956
  var topFrame = topFrames[lastFrame.frame_id]
212
957
  if (!topFrame) {
213
958
  topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0}
214
959
  }
215
- topFrame.exclusiveCount += 1;
216
- lastFrame.topFrame = topFrame;
217
-
218
- var totalGems = 0;
219
- $.each(gemStats, function(k,stat){
220
- totalGems++;
221
- stat.samples = getUnique(stat.samples);
222
- });
223
-
224
- var gemsSorted = $.map(gemStats, function(v, k){ return v })
225
- gemsSorted.sort(function(a, b){ return b.samples.length - a.samples.length })
226
-
227
- var currentIndex = 0;
228
- $.each(gemsSorted, function(k,stat){
229
- stat.color = rainbow(totalGems, currentIndex);
230
- currentIndex += 1;
231
-
232
- for(var x=0; x < stat.frames.length; x++) {
233
- info[stat.frames[x]] = {nodes: [], samples: [], color: stat.color};
234
- }
235
- });
236
-
237
- function drawData(svg, data, xScale, yScale, mini) {
238
- svg.selectAll("g.flames")
239
- .data(data)
240
- .enter()
241
- .append("g")
242
- .attr('class', 'flames')
243
- .each(function(d){
244
- gemStats[d.gemName].nodes.push(this)
245
-
246
- var r = d3.select(this)
247
- .append("rect")
248
- .attr("x",function(d) { return xScale(d.x); })
249
- .attr("y",function(d) { return yScale(maxY - d.y);})
250
- .attr("width", function(d){return xScale(d.width);})
251
- .attr("height", yScale(1))
252
- .attr("fill", function(d){
253
- var i = info[d.frame_id];
254
- if(!i) {
255
- info[d.frame_id] = i = {nodes: [], samples: [], color: color()};
256
- }
257
- i.nodes.push(this);
258
- if (!mini)
259
- for(var j=0; j < d.width; j++){
260
- i.samples.push(d.x + j);
261
- }
262
- return i.color;
263
- })
264
-
265
- if (!mini)
266
- r
267
- .on("mouseover", mouseover)
268
- .on("mouseout", mouseout);
269
-
270
- if (!mini)
271
- d3.select(this)
272
- .append('foreignObject')
273
- .classed('label-body', true)
274
- .attr("x",function(d) { return xScale(d.x); })
275
- .attr("y",function(d) { return yScale(maxY - d.y);})
276
- .attr("width", function(d){return xScale(d.width);})
277
- .attr("height", yScale(1))
278
- .attr("line-height", yScale(1))
279
- .attr("font-size", yScale(0.42) + 'px')
280
- .attr('pointer-events', 'none')
281
- .append('xhtml:span')
282
- .style("height", yScale(1))
283
- .classed('label', true)
284
- .text(function(d){ return d.frame })
285
- });
286
- }
960
+ topFrame.exclusiveCount += 1
961
+ lastFrame.topFrame = topFrame
287
962
 
288
- drawData(svg, data, xScale, yScale, 0)
289
- drawData(svg2, data, xScale2, yScale2, 1)
290
-
291
- var brushed = function(){
292
- if (brush.empty()) {
293
- svg.attr('transform', '')
294
- zoomXRatio = 1
295
- zoom.scale(1).translate([0,0])
296
- svg.selectAll('.label-body')
297
- .attr('transform', 'scale(1,1)')
298
- .attr("x",function(d) { return xScale(d.x)*zoomXRatio; })
299
- .attr("width", function(d){return xScale(d.width)*zoomXRatio;})
300
- } else {
301
- var e = brush.extent()
302
- var x = [e[0][0],e[1][0]], y = [e[0][1],e[1][1]]
303
-
304
- xScale.domain([0, maxX])
305
- yScale.domain([0, maxY])
306
-
307
- var w = width, h = height2
308
- var dx = xScale2(1.0*x[1]-x[0]), dy = yScale2(1.0*y[1]-y[0])
309
- var sx = w/dx, sy = h/dy
310
- var trlx = -xScale(x[0])*sx, trly = -yScale(y[0])*sy
311
- var transform = "translate(" + trlx + ',' + trly + ")" + " scale(" + sx + ',' + sy + ")"
312
-
313
- zoomXRatio = sx/sy
314
-
315
- svg.selectAll('.label-body')
316
- .attr("x",function(d) { return xScale(d.x)*zoomXRatio; })
317
- .attr("width", function(d){return xScale(d.width)*zoomXRatio;})
318
- .attr('transform', function(d){
319
- var x = xScale(d.x)
320
- return "scale("+(1.0/zoomXRatio)+",1)"
321
- })
322
-
323
- svg.attr("transform", transform)
324
- zoom.translate([trlx, trly]).scale(sy)
325
- }
963
+ var totalGems = 0
964
+ for (var k in gemStats) {
965
+ totalGems++
966
+ gemStats[k].samples = getUnique(gemStats[k].samples)
326
967
  }
327
968
 
328
- var brush = d3.svg.brush()
329
- .x(xScale2)
330
- .y(yScale2)
331
- .on("brush", brushed);
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 })
332
971
 
333
- svg2.append("g")
334
- .attr("class", "brush")
335
- .call(brush)
972
+ var currentIndex = 0
973
+ gemsSorted.forEach(function(stat) {
974
+ stat.color = rainbow(totalGems, currentIndex)
975
+ currentIndex += 1
336
976
 
337
- // Samples may overlap on the same line
338
- for (var r in info) {
339
- if (info[r].samples) {
340
- info[r].samples = getUnique(info[r].samples);
977
+ for (var x = 0; x < stat.frames.length; x++) {
978
+ info[stat.frames[x]].color = stat.color
341
979
  }
342
- };
343
-
344
- // render the legend
345
- $.each(gemsSorted, function(k,gem){
346
- var data = samplePercentRaw(gem.samples.length)
347
- var node = $("<div class='"+gem.name+"'></div>")
348
- .css("background-color", gem.color)
349
- .html("<span style='float: right'>" + data[0] + 'x<br>' + data[1] + '%' + '</span>' + '<div class="name">'+gem.name+'<br>&nbsp;</div>');
350
-
351
- node.on('mouseenter mouseleave', function(e){
352
- d3.selectAll(gemStats[gem.name].nodes).classed('highlighted', e.type == 'mouseenter')
353
- })
980
+ })
354
981
 
355
- $('.legend').append(node);
356
- });
982
+ new FlamegraphView(data, info, gemsSorted)
357
983
  }