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, '&').replace(/</g, '<').replace(/>/g, '>'); 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