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 +4 -4
- data/.travis.yml +5 -5
- data/Gemfile.lock +4 -1
- data/README.md +1 -0
- data/bin/stackprof +3 -2
- data/ext/stackprof/stackprof.c +154 -19
- data/lib/stackprof/flamegraph/flamegraph.js +926 -300
- data/lib/stackprof/flamegraph/viewer.html +29 -23
- data/lib/stackprof/report.rb +41 -0
- data/sample.rb +3 -3
- data/stackprof.gemspec +1 -1
- data/test/test_stackprof.rb +20 -17
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88f62f89ebff2c249b7eaabcb330f12997f09120
|
4
|
+
data.tar.gz: 629071b6584701d830b827e5d4b9b0951eaa0282
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d02684e9bd77e2b561f626a69a897e1ec2c89b870b30ef54709165cc1debb966a56127f3053d7a27e81cc7c3c62fef50865ebd5bcbfd6d2b28630add6280e479
|
7
|
+
data.tar.gz: 2d5d70aaa53080112d8f794d1d5412cd8225f451f43431f30209e42854b5de8246ff77347ef39496c2b55b447d6cf114116b81dc2eb458adea16a5d9ce03606e
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
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
|
|
data/bin/stackprof
CHANGED
@@ -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
|
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
|
data/ext/stackprof/stackprof.c
CHANGED
@@ -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
|
-
|
191
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
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.
|
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
|
-
|
379
|
+
stackprof_record_sample_for_stack(int num, int timestamp_delta)
|
344
380
|
{
|
345
|
-
int
|
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
|
-
|
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->
|
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.
|
432
|
-
|
433
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
return (c);
|
53
|
+
})()
|
47
54
|
}
|
48
55
|
|
49
|
-
|
50
|
-
var
|
51
|
-
|
52
|
-
|
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
|
58
|
-
var
|
59
|
-
|
60
|
-
var
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
73
|
-
|
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 =
|
78
|
-
var
|
79
|
-
var
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
var
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
112
|
-
|
113
|
-
var
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
var
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
879
|
+
function colorString(color, opacity) {
|
880
|
+
if (typeof opacity === 'undefined')
|
881
|
+
opacity = 1
|
882
|
+
return 'rgba(' + color.join(',') + ',' + opacity + ')'
|
883
|
+
}
|
152
884
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
166
|
-
|
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
|
-
|
169
|
-
|
170
|
-
};
|
904
|
+
return [prefix, '\u2026', suffix].join('')
|
905
|
+
}
|
171
906
|
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
177
|
-
|
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
|
-
|
186
|
-
var gem = guessGem(
|
187
|
-
var stat = gemStats[gem]
|
188
|
-
|
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: []
|
936
|
+
if (!stat) {
|
937
|
+
gemStats[gem] = stat = {name: gem, samples: [], frames: []}
|
192
938
|
}
|
193
939
|
|
194
|
-
stat.frames.push(
|
195
|
-
for(var j=0; j <
|
196
|
-
stat.samples.push(
|
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
|
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 =
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
329
|
-
|
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
|
-
|
334
|
-
|
335
|
-
|
972
|
+
var currentIndex = 0
|
973
|
+
gemsSorted.forEach(function(stat) {
|
974
|
+
stat.color = rainbow(totalGems, currentIndex)
|
975
|
+
currentIndex += 1
|
336
976
|
|
337
|
-
|
338
|
-
|
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> </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
|
-
|
356
|
-
});
|
982
|
+
new FlamegraphView(data, info, gemsSorted)
|
357
983
|
}
|