Add ICU message format support
[chromium-blink-merge.git] / tools / traceline / svgui / traceline.js
blob33cc2dfa388b18386181f154d95057de0dff1ab4
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.
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
26 // Dean McNamee <deanm@chromium.org>
28 // Man... namespaces are such a pain.
29 var svgNS = 'http://www.w3.org/2000/svg';
30 var xhtmlNS = 'http://www.w3.org/1999/xhtml';
32 function 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;
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.
45 function 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 = [ ];
55 TLThread.prototype.duration_ms =
56 function() {
57   return this.endms - this.startms;
60 TLThread.prototype.AddEvent =
61 function(e) {
62   this.events.push(e);
65 TLThread.prototype.toString =
66 function() {
67   var res = "TLThread -- id: " + this.id + " name: " + this.name +
68             " startms: " + this.startms + " endms: " + this.endms +
69             " parent: " + this.parent;
70   return res;
73 // A TLEvent represents a single logged event that happened on a thread.
74 function 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;
83 function HTMLEscape(str) {
84   return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
87 TLEvent.prototype.toString =
88 function() {
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   }
126   return res;
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.
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.
143 function JSONLoader(json_urls) {
144   this.urls_to_load = json_urls;
145   this.script_element = null;
148 JSONLoader.prototype.IsFinishedLoading =
149 function() { return this.urls_to_load.length == 0; };
151 // Start loading of the next JSON URL.
152 JSONLoader.prototype.LoadNext =
153 function() {
154   var sc = document.createElementNS(
155       'http://www.w3.org/1999/xhtml', 'script');
156   this.script_element = sc;
158   sc.setAttribute("src", this.urls_to_load[0]);
159   document.getElementsByTagNameNS(xhtmlNS, 'body')[0].appendChild(sc);
162 // Callback counterpart to load_next, should be called when the script element
163 // is finished loading.  Returns the URL that was just loaded.
164 JSONLoader.prototype.DoneLoading =
165 function() {
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();
173 var loader = null;
175 function loadJSON(json_urls) {
176   loader = new JSONLoader(json_urls);
177   if (!loader.IsFinishedLoading())
178     loader.LoadNext();
181 var traceline = new Traceline();
183 // Called from the JSON with the log event array.
184 function parseEvents(json) {
185   loader.DoneLoading();
187   var done = loader.IsFinishedLoading();
188   if (!done)
189     loader.LoadNext();
191   traceline.ProcessJSON(json);
193   if (done)
194     traceline.Render();
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.
199 function 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 = [ ];
204   // Keep a mapping of where in the list of threads a set starts...
205   this.thread_set_indexes = [ ];
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 = { };
211   // The last event time of all of our events.
212   this.endms = 0;
214   // Constants for SVG rendering...
215   this.kThreadHeightPx = 16;
216   this.kTimelineWidthPx = 1008;
219 // Called to add another set of data into the traceline.
220 Traceline.prototype.ProcessJSON =
221 function(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);
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 = { };
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]);
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;
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     }
250     var thread = this.threads[this.threads_by_id[e.thread]];
251     thread.AddEvent(e);
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;
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   }
276 Traceline.prototype.Render =
277 function() { this.RenderSVG(); };
279 Traceline.prototype.RenderText =
280 function() {
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   }
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.
311 function SVGSceneLine(msg, klass, x1, y1, x2, y2) {
312   this.type = SVGSceneLine;
313   this.msg = msg;
314   this.klass = klass;
316   this.x1 = x1;
317   this.y1 = y1;
318   this.x2 = x2;
319   this.y2 = y2;
321   this.hittest = function(startms, dur) {
322     return true;
323   };
326 function SVGSceneRect(msg, klass, x, y, width, height) {
327   this.type = SVGSceneRect;
328   this.msg = msg;
329   this.klass = klass;
331   this.x = x;
332   this.y = y;
333   this.width = width;
334   this.height = height;
336   this.hittest = function(startms, dur) {
337     return this.x <= (startms + dur) &&
338            (this.x + this.width) >= startms;
339   };
342 Traceline.prototype.RenderSVG =
343 function() {
344   var threadnames = this.RenderSVGCreateThreadNames();
345   var scene = this.RenderSVGCreateScene();
347   var curzoom = 8;
349   // The height is static after we've created the scene
350   var dom = this.RenderSVGCreateDOM(threadnames, scene.height);
352   dom.zoom(curzoom);
354   dom.attach();
356   var draw = (function(obj) {
357     return function(scroll, total) {
358       var startms = (scroll / total) * obj.endms;
360       var start = (new Date).getTime();
361       var count = obj.RenderSVGRenderScene(dom, scene, startms, curzoom);
362       var total = (new Date).getTime() - start;
364       dom.infoareadiv.innerHTML =
365           'Scene render of ' + count + ' nodes took: ' + total + ' ms';
366     };
367   })(this, dom, scene);
369   // Paint the initial paint with no scroll
370   draw(0, 1);
372   // Hook us up to repaint on scrolls.
373   dom.redraw = draw;
377 // Create all of the DOM elements for the SVG scene.
378 Traceline.prototype.RenderSVGCreateDOM =
379 function(threadnames, svgheight) {
381   // Total div holds the container and the info area.
382   var totaldiv = document.createElementNS(xhtmlNS, 'div');
384   // Container holds the thread names, SVG element, and fake scroll bar.
385   var container = document.createElementNS(xhtmlNS, 'div');
386   container.className = 'container';
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';
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   }
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';
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);
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';
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);
421   var infoareadiv = document.createElementNS(xhtmlNS, 'div');
422   infoareadiv.className = 'infoareadiv';
423   infoareadiv.innerHTML = 'Hover an event...';
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);
436   svgdiv.appendChild(svg);
437   svgdiv.appendChild(fakescrolldiv);
439   container.appendChild(threadnamesdiv);
440   container.appendChild(svgdiv);
442   totaldiv.appendChild(container);
443   totaldiv.appendChild(infoareadiv);
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   };
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);
493   return obj;
496 Traceline.prototype.RenderSVGCreateThreadNames =
497 function() {
498   // This names is the list to show along the left hand size.
499   var threadnames = [ ];
501   for (var i = 0, il = this.threads.length; i < il; ++i) {
502     var thread = this.threads[i];
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     }
514     threadnames.push(thread.name);
515   }
517   return threadnames;
520 Traceline.prototype.RenderSVGCreateScene =
521 function() {
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 = [ ];
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.
530   var lasty = 0;
531   for (var i = 0, il = this.threads.length; i < il; ++i) {
532     var thread = this.threads[i];
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     }
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));
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];
557       var y = 2 + lasty;
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       }
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       }
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       }
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;
588       var width = dur;
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         }
604         pushdown[slot] = e.ms + dur;
605         y += slot * 4;
606         break;
607       }
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));
615       // If there is a "parentevent", we want to make a line there.
616       // TODO
617     }
619     lasty += this.kThreadHeightPx;
620   }
622   return {
623     'scene': scene,
624     'width': this.endms + 2,
625     'height': lasty,
626   };
629 Traceline.prototype.RenderSVGRenderScene =
630 function(dom, scene, startms, curzoom) {
631   var stuff = scene.scene;
632   var svg = dom.svg;
634   var count = 0;
636   // Remove everything from the DOM.
637   while (svg.firstChild)
638     svg.removeChild(svg.firstChild);
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');
644   var dur = this.kTimelineWidthPx / curzoom;
646   function min(a, b) {
647     return a < b ? a : b;
648   }
650   function max(a, b) {
651     return a > b ? a : b;
652   }
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   }
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;
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     }
686     ++count;
687   }
689   // Append the 'g' element on after we've build it.
690   svg.appendChild(svgg);
692   return count;