Home | History | Annotate | Download | only in svgui
      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 (a] chromium.org>
     27 
     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';
     31 
     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;
     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.
     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 = [ ];
     53 };
     54 
     55 TLThread.prototype.duration_ms =
     56 function() {
     57   return this.endms - this.startms;
     58 };
     59 
     60 TLThread.prototype.AddEvent =
     61 function(e) {
     62   this.events.push(e);
     63 };
     64 
     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;
     71 };
     72 
     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;
     81 }
     82 
     83 function HTMLEscape(str) {
     84   return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
     85 }
     86 
     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   }
    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 
    143 function JSONLoader(json_urls) {
    144   this.urls_to_load = json_urls;
    145   this.script_element = null;
    146 }
    147 
    148 JSONLoader.prototype.IsFinishedLoading =
    149 function() { return this.urls_to_load.length == 0; };
    150 
    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;
    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.
    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();
    171 };
    172 
    173 var loader = null;
    174 
    175 function loadJSON(json_urls) {
    176   loader = new JSONLoader(json_urls);
    177   if (!loader.IsFinishedLoading())
    178     loader.LoadNext();
    179 }
    180 
    181 var traceline = new Traceline();
    182 
    183 // Called from the JSON with the log event array.
    184 function 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.
    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 = [ ];
    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.
    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);
    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 
    276 Traceline.prototype.Render =
    277 function() { this.RenderSVG(); };
    278 
    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   }
    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 
    311 function 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 
    326 function 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 
    342 Traceline.prototype.RenderSVG =
    343 function() {
    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.
    378 Traceline.prototype.RenderSVGCreateDOM =
    379 function(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 
    496 Traceline.prototype.RenderSVGCreateThreadNames =
    497 function() {
    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 
    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 = [ ];
    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 
    629 Traceline.prototype.RenderSVGRenderScene =
    630 function(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