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