stackprof 0.2.10 → 0.2.11

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