1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
|
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO
// - spacial partitioning of the data so that we don't have to scan the
// entire scene every time we render.
// - properly clip the SVG elements when they render, right now we are just
// letting them go negative or off the screen. This might give us a little
// bit better performance?
// - make the lines for thread creation work again. Figure out a better UI
// than these lines, because they can be a bit distracting.
// - Implement filters, so that you can filter on specific event types, etc.
// - Make the callstack box collapsable or scrollable or something, it takes
// up a lot of screen realestate now.
// - Figure out better ways to preserve screen realestate.
// - Make the thread bar heights configurable, figure out a better way to
// handle overlapping events (the pushdown code).
// - "Sticky" info, so you can click on something, and it will stay. Now
// if you need to scroll the page you usually lose the info because you
// will mouse over something else on your way to scrolling.
// - Help / legend
// - Loading indicator / debug console.
// - OH MAN BETTER COLORS PLEASE
//
// Dean McNamee <deanm@chromium.org>
// Man... namespaces are such a pain.
var svgNS = 'http://www.w3.org/2000/svg';
var xhtmlNS = 'http://www.w3.org/1999/xhtml';
function toHex(num) {
var str = "";
var table = "0123456789abcdef";
for (var i = 0; i < 8; ++i) {
str = table.charAt(num & 0xf) + str;
num >>= 4;
}
return str;
}
// a TLThread represents information about a thread in the traceline data.
// A thread has a list of all events that happened on that thread, the start
// and end time of the thread, the thread id, and name, etc.
function TLThread(id, startms, endms) {
this.id = id;
// Default the name to the thread id, but if the application uses
// thread naming, we might see a THREADNAME event later and update.
this.name = "thread_" + id;
this.startms = startms;
this.endms = endms;
this.events = [ ];
};
TLThread.prototype.duration_ms =
function() {
return this.endms - this.startms;
};
TLThread.prototype.AddEvent =
function(e) {
this.events.push(e);
};
TLThread.prototype.toString =
function() {
var res = "TLThread -- id: " + this.id + " name: " + this.name +
" startms: " + this.startms + " endms: " + this.endms +
" parent: " + this.parent;
return res;
};
// A TLEvent represents a single logged event that happened on a thread.
function TLEvent(e) {
this.eventtype = e['eventtype'];
this.thread = toHex(e['thread']);
this.cpu = toHex(e['cpu']);
this.ms = e['ms'];
this.done = e['done'];
this.e = e;
}
function HTMLEscape(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
TLEvent.prototype.toString =
function() {
var res = "<b>ms:</b> " + this.ms + " " +
"<b>event:</b> " + this.eventtype + " " +
"<b>thread:</b> " + this.thread + " " +
"<b>cpu:</b> " + this.cpu + "<br/>";
if ('ldrinfo' in this.e) {
res += "<b>ldrinfo:</b> " + this.e['ldrinfo'] + "<br/>";
}
if ('done' in this.e && this.e['done'] > 0) {
res += "<b>done:</b> " + this.e['done'] + " ";
res += "<b>duration:</b> " + (this.e['done'] - this.ms) + "<br/>";
}
if ('syscall' in this.e) {
res += "<b>syscall:</b> " + this.e['syscall'];
if ('syscallname' in this.e) {
res += " <b>syscallname:</b> " + this.e['syscallname'];
}
if ('retval' in this.e) {
res += " <b>retval:</b> " + this.e['retval'];
}
res += "<br/>"
}
if ('func_addr' in this.e) {
res += "<b>func_addr:</b> " + toHex(this.e['func_addr']);
if ('func_addr_name' in this.e) {
res += " <b>func_addr_name:</b> " + HTMLEscape(this.e['func_addr_name']);
}
res += "<br/>"
}
if ('stacktrace' in this.e) {
var stack = this.e['stacktrace'];
res += "<b>stacktrace:</b><br/>";
for (var i = 0; i < stack.length; ++i) {
res += "0x" + toHex(stack[i][0]) + " - " +
HTMLEscape(stack[i][1]) + "<br/>";
}
}
return res;
}
// The trace logger dumps all log events to a simple JSON array. We delay
// and background load the JSON, since it can be large. When the JSON is
// loaded, parseEvents(...) is called and passed the JSON data. To make
// things easier, we do a few passes on the data to group them together by
// thread, gather together some useful pieces of data in a single place,
// and form more of a structure out of the data. We also build links
// between related events, for example a thread creating a new thread, and
// the new thread starting to run. This structure is fairly close to what
// we want to represent in the interface.
// Delay load the JSON data. We want to display the order in the order it was
// passed to us. Since we have no way of correlating the json callback to
// which script element it was called on, we load them one at a time.
function JSONLoader(json_urls) {
this.urls_to_load = json_urls;
this.script_element = null;
}
JSONLoader.prototype.IsFinishedLoading =
function() { return this.urls_to_load.length == 0; };
// Start loading of the next JSON URL.
JSONLoader.prototype.LoadNext =
function() {
var sc = document.createElementNS(
'http://www.w3.org/1999/xhtml', 'script');
this.script_element = sc;
sc.setAttribute("src", this.urls_to_load[0]);
document.getElementsByTagNameNS(xhtmlNS, 'body')[0].appendChild(sc);
};
// Callback counterpart to load_next, should be called when the script element
// is finished loading. Returns the URL that was just loaded.
JSONLoader.prototype.DoneLoading =
function() {
// Remove the script element from the DOM.
this.script_element.parentNode.removeChild(this.script_element);
this.script_element = null;
// Return the URL that had just finished loading.
return this.urls_to_load.shift();
};
var loader = null;
function loadJSON(json_urls) {
loader = new JSONLoader(json_urls);
if (!loader.IsFinishedLoading())
loader.LoadNext();
}
var traceline = new Traceline();
// Called from the JSON with the log event array.
function parseEvents(json) {
loader.DoneLoading();
var done = loader.IsFinishedLoading();
if (!done)
loader.LoadNext();
traceline.ProcessJSON(json);
if (done)
traceline.Render();
}
// The Traceline class represents our entire state, all of the threads from
// all sets of data, all of the events, DOM elements, etc.
function Traceline() {
// The array of threads that existed in the program. Hopefully in order
// they were created. This includes all threads from all sets of data.
this.threads = [ ];
// Keep a mapping of where in the list of threads a set starts...
this.thread_set_indexes = [ ];
// Map a thread id to the index in the threads array. A thread ID is the
// unique ID from the OS, along with our set id of which data file we were.
this.threads_by_id = { };
// The last event time of all of our events.
this.endms = 0;
// Constants for SVG rendering...
this.kThreadHeightPx = 16;
this.kTimelineWidthPx = 1008;
}
// Called to add another set of data into the traceline.
Traceline.prototype.ProcessJSON =
function(json_data) {
// Keep track of which threads belong to which sets of data...
var set_id = this.thread_set_indexes.length;
this.thread_set_indexes.push(this.threads.length);
// TODO make this less hacky. Used to connect related events, like creating
// a thread and then having that thread run (two separate events which are
// related but come in at different times, etc).
var tiez = { };
// Run over the data, building TLThread's and TLEvents, and doing some
// processing to put things in an easier to display form...
for (var i = 0, il = json_data.length; i < il; ++i) {
var e = new TLEvent(json_data[i]);
// Create a unique identifier for a thread by using the id of this data
// set, so that they are isolated from other sets of data with the same
// thread id, etc. TODO don't overwrite the original...
e.thread = set_id + '_' + e.thread;
// If this is the first event ever seen on this thread, create a new
// thread object and add it to our lists of threads.
if (!(e.thread in this.threads_by_id)) {
var end_ms = e.done ? e.done : e.ms;
var new_thread = new TLThread(e.thread, e.ms, end_ms);
this.threads_by_id[new_thread.id] = this.threads.length;
this.threads.push(new_thread);
}
var thread = this.threads[this.threads_by_id[e.thread]];
thread.AddEvent(e);
// Keep trace of the time of the last event seen.
var end_ms = e.done ? e.done : e.ms;
if (end_ms > this.endms) this.endms = end_ms;
if (end_ms > thread.endms) thread.endms = end_ms;
switch(e.eventtype) {
case 'EVENT_TYPE_THREADNAME':
thread.name = e.e['threadname'];
break;
case 'EVENT_TYPE_CREATETHREAD':
tiez[e.e['eventid']] = e;
break;
case 'EVENT_TYPE_THREADBEGIN':
var pei = e.e['parenteventid'];
if (pei in tiez) {
e.parentevent = tiez[pei];
tiez[pei].childevent = e;
}
break;
}
}
};
Traceline.prototype.Render =
function() { this.RenderSVG(); };
Traceline.prototype.RenderText =
function() {
var z = document.getElementsByTagNameNS(xhtmlNS, 'body')[0];
for (var i = 0, il = this.threads.length; i < il; ++i) {
var p = document.createElementNS(
'http://www.w3.org/1999/xhtml', 'p');
p.innerHTML = this.threads[i].toString();
z.appendChild(p);
}
};
// Oh man, so here we go. For two reasons, I implement my own scrolling
// system. First off, is that in order to scale, we want to have as little
// on the DOM as possible. This means not having off-screen elements in the
// DOM, as this slows down everything. This comes at a cost of more expensive
// scrolling performance since you have to re-render the scene. The second
// reason is a bug I stumbled into:
// https://bugs.webkit.org/show_bug.cgi?id=21968
// This means that scrolling an SVG element doesn't really work properly
// anyway. So what the code does is this. We have our layout that looks like:
// [ thread names ] [ svg timeline ]
// [ scroll bar ]
// We make a fake scrollbar, which doesn't actually have the SVG inside of it,
// we want for when this scrolls, with some debouncing, and then when it has
// scrolled we rerender the scene. This means that the SVG element is never
// scrolled, and coordinates are always at 0. We keep the scene in millisecond
// units which also helps for zooming. We do our own hit testing and decide
// what needs to be renderer, convert from milliseconds to SVG pixels, and then
// draw the update into the static SVG element... Y coordinates are still
// always in pixels (since we aren't paging along the Y axis), but this might
// be something to fix up later.
function SVGSceneLine(msg, klass, x1, y1, x2, y2) {
this.type = SVGSceneLine;
this.msg = msg;
this.klass = klass;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.hittest = function(startms, dur) {
return true;
};
}
function SVGSceneRect(msg, klass, x, y, width, height) {
this.type = SVGSceneRect;
this.msg = msg;
this.klass = klass;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.hittest = function(startms, dur) {
return this.x <= (startms + dur) &&
(this.x + this.width) >= startms;
};
}
Traceline.prototype.RenderSVG =
function() {
var threadnames = this.RenderSVGCreateThreadNames();
var scene = this.RenderSVGCreateScene();
var curzoom = 8;
// The height is static after we've created the scene
var dom = this.RenderSVGCreateDOM(threadnames, scene.height);
dom.zoom(curzoom);
dom.attach();
var draw = (function(obj) {
return function(scroll, total) {
var startms = (scroll / total) * obj.endms;
var start = (new Date).getTime();
var count = obj.RenderSVGRenderScene(dom, scene, startms, curzoom);
var total = (new Date).getTime() - start;
dom.infoareadiv.innerHTML =
'Scene render of ' + count + ' nodes took: ' + total + ' ms';
};
})(this, dom, scene);
// Paint the initial paint with no scroll
draw(0, 1);
// Hook us up to repaint on scrolls.
dom.redraw = draw;
};
// Create all of the DOM elements for the SVG scene.
Traceline.prototype.RenderSVGCreateDOM =
function(threadnames, svgheight) {
// Total div holds the container and the info area.
var totaldiv = document.createElementNS(xhtmlNS, 'div');
// Container holds the thread names, SVG element, and fake scroll bar.
var container = document.createElementNS(xhtmlNS, 'div');
container.className = 'container';
// This is the div that holds the thread names along the left side, this is
// done in HTML for easier/better text support than SVG.
var threadnamesdiv = document.createElementNS(xhtmlNS, 'div');
threadnamesdiv.className = 'threadnamesdiv';
// Add all of the names into the div, these are static and don't update.
for (var i = 0, il = threadnames.length; i < il; ++i) {
var div = document.createElementNS(xhtmlNS, 'div');
div.className = 'threadnamediv';
div.appendChild(document.createTextNode(threadnames[i]));
threadnamesdiv.appendChild(div);
}
// SVG div goes along the right side, it holds the SVG element and our fake
// scroll bar.
var svgdiv = document.createElementNS(xhtmlNS, 'div');
svgdiv.className = 'svgdiv';
// The SVG element, static width, and we will update the height after we've
// walked through how many threads we have and know the size.
var svg = document.createElementNS(svgNS, 'svg');
svg.setAttributeNS(null, 'height', svgheight);
svg.setAttributeNS(null, 'width', this.kTimelineWidthPx);
// The fake scroll div is an outer div with a fixed size with a scroll.
var fakescrolldiv = document.createElementNS(xhtmlNS, 'div');
fakescrolldiv.className = 'fakescrolldiv';
// Fatty is inside the fake scroll div to give us the size we want to scroll.
var fattydiv = document.createElementNS(xhtmlNS, 'div');
fattydiv.className = 'fattydiv';
fakescrolldiv.appendChild(fattydiv);
var infoareadiv = document.createElementNS(xhtmlNS, 'div');
infoareadiv.className = 'infoareadiv';
infoareadiv.innerHTML = 'Hover an event...';
// Set the SVG mouseover handler to write the data to the infoarea.
svg.addEventListener('mouseover', (function(infoarea) {
return function(e) {
if ('msg' in e.target && e.target.msg) {
infoarea.innerHTML = e.target.msg;
}
e.stopPropagation(); // not really needed, but might as well.
};
})(infoareadiv), true);
svgdiv.appendChild(svg);
svgdiv.appendChild(fakescrolldiv);
container.appendChild(threadnamesdiv);
container.appendChild(svgdiv);
totaldiv.appendChild(container);
totaldiv.appendChild(infoareadiv);
var widthms = Math.floor(this.endms + 2);
// Make member variables out of the things we want to 'export', things that
// will need to be updated each time we redraw the scene.
var obj = {
// The root of our piece of the DOM.
'totaldiv': totaldiv,
// We will want to listen for scrolling on the fakescrolldiv
'fakescrolldiv': fakescrolldiv,
// The SVG element will of course need updating.
'svg': svg,
// The area we update with the info on mouseovers.
'infoareadiv': infoareadiv,
// Called when we detected new scroll a should redraw
'redraw': function() { },
'attached': false,
'attach': function() {
document.getElementsByTagNameNS(xhtmlNS, 'body')[0].appendChild(
this.totaldiv);
this.attached = true;
},
// The fatty div will have it's width adjusted based on the zoom level and
// the duration of the graph, to get the scrolling correct for the size.
'zoom': function(curzoom) {
var width = widthms * curzoom;
fattydiv.style.width = width + 'px';
},
'detach': function() {
this.totaldiv.parentNode.removeChild(this.totaldiv);
this.attached = false;
},
};
// Watch when we get scroll events on the fake scrollbar and debounce. We
// need to give it a pointer to use in the closer to call this.redraw();
fakescrolldiv.addEventListener('scroll', (function(theobj) {
var seqnum = 0;
return function(e) {
seqnum = (seqnum + 1) & 0xffff;
window.setTimeout((function(myseqnum) {
return function() {
if (seqnum == myseqnum) {
theobj.redraw(e.target.scrollLeft, e.target.scrollWidth);
}
};
})(seqnum), 100);
};
})(obj), false);
return obj;
};
Traceline.prototype.RenderSVGCreateThreadNames =
function() {
// This names is the list to show along the left hand size.
var threadnames = [ ];
for (var i = 0, il = this.threads.length; i < il; ++i) {
var thread = this.threads[i];
// TODO make this not so stupid...
if (i != 0) {
for (var j = 0; j < this.thread_set_indexes.length; j++) {
if (i == this.thread_set_indexes[j]) {
threadnames.push('------');
break;
}
}
}
threadnames.push(thread.name);
}
return threadnames;
};
Traceline.prototype.RenderSVGCreateScene =
function() {
// This scene is just a list of SVGSceneRect and SVGSceneLine, in no great
// order. In the future they should be structured to make range checking
// faster.
var scene = [ ];
// Remember, for now, Y (height) coordinates are still in pixels, since we
// don't zoom or scroll in this direction. X coordinates are milliseconds.
var lasty = 0;
for (var i = 0, il = this.threads.length; i < il; ++i) {
var thread = this.threads[i];
// TODO make this not so stupid...
if (i != 0) {
for (var j = 0; j < this.thread_set_indexes.length; j++) {
if (i == this.thread_set_indexes[j]) {
lasty += this.kThreadHeightPx;
break;
}
}
}
// For this thread, create the background thread (blue band);
scene.push(new SVGSceneRect(null,
'thread',
thread.startms,
1 + lasty,
thread.duration_ms(),
this.kThreadHeightPx - 2));
// Now create all of the events...
var pushdown = [ 0, 0, 0, 0 ];
for (var j = 0, jl = thread.events.length; j < jl; ++j) {
var e = thread.events[j];
var y = 2 + lasty;
// TODO this is a hack just so that we know the correct why position
// so we can create the threadline...
if (e.childevent) {
e.marky = y;
}
// Handle events that we want to represent as lines and not event blocks,
// right now this is only thread creation. We map an event back to its
// "parent" event, and now lets add a line to represent that.
if (e.parentevent) {
var eparent = e.parentevent;
var msg = eparent.toString() + '<br/>' + e.toString();
scene.push(
new SVGSceneLine(msg, 'eventline',
eparent.ms, eparent.marky + 5, e.ms, lasty + 5));
}
// We get negative done values (well, really, it was 0 and then made
// relative to start time) when a syscall never returned...
var dur = 0;
if ('done' in e.e && e.e['done'] > 0) {
dur = e.e['done'] - e.ms;
}
// TODO skip short events for now, but eventually we should figure out
// a way to control this from the UI, etc.
if (dur < 0.2)
continue;
var width = dur;
// Try to find an available horizontal slot for our event.
for (var z = 0; z < pushdown.length; ++z) {
var found = false;
var slot = z;
if (pushdown[z] < e.ms) {
found = true;
}
if (!found) {
if (z != pushdown.length - 1)
continue;
slot = Math.floor(Math.random() * pushdown.length);
alert('blah');
}
pushdown[slot] = e.ms + dur;
y += slot * 4;
break;
}
// Create the event
klass = e.e.waiting ? 'eventwaiting' : 'event';
scene.push(
new SVGSceneRect(e.toString(), klass, e.ms, y, width, 3));
// If there is a "parentevent", we want to make a line there.
// TODO
}
lasty += this.kThreadHeightPx;
}
return {
'scene': scene,
'width': this.endms + 2,
'height': lasty,
};
};
Traceline.prototype.RenderSVGRenderScene =
function(dom, scene, startms, curzoom) {
var stuff = scene.scene;
var svg = dom.svg;
var count = 0;
// Remove everything from the DOM.
while (svg.firstChild)
svg.removeChild(svg.firstChild);
// Don't actually need this, but you can't transform on an svg element,
// so it's nice to have a <g> around for transforms...
var svgg = document.createElementNS(svgNS, 'g');
var dur = this.kTimelineWidthPx / curzoom;
function min(a, b) {
return a < b ? a : b;
}
function max(a, b) {
return a > b ? a : b;
}
function timeToPixel(x) {
// TODO(deanm): This clip is a bit shady.
var x = min(max(Math.floor(x*curzoom), -100), 2000);
return (x == 0 ? 1 : x);
}
for (var i = 0, il = stuff.length; i < il; ++i) {
var thing = stuff[i];
if (!thing.hittest(startms, startms+dur))
continue;
if (thing.type == SVGSceneRect) {
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttributeNS(null, 'class', thing.klass)
rect.setAttributeNS(null, 'x', timeToPixel(thing.x - startms));
rect.setAttributeNS(null, 'y', thing.y);
rect.setAttributeNS(null, 'width', timeToPixel(thing.width));
rect.setAttributeNS(null, 'height', thing.height);
rect.msg = thing.msg;
svgg.appendChild(rect);
} else if (thing.type == SVGSceneLine) {
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttributeNS(null, 'class', thing.klass)
line.setAttributeNS(null, 'x1', timeToPixel(thing.x1 - startms));
line.setAttributeNS(null, 'y1', thing.y1);
line.setAttributeNS(null, 'x2', timeToPixel(thing.x2 - startms));
line.setAttributeNS(null, 'y2', thing.y2);
line.msg = thing.msg;
svgg.appendChild(line);
}
++count;
}
// Append the 'g' element on after we've build it.
svg.appendChild(svgg);
return count;
};
|