diff options
Diffstat (limited to 'tools/traceline/svgui/traceline.js')
-rwxr-xr-x | tools/traceline/svgui/traceline.js | 684 |
1 files changed, 684 insertions, 0 deletions
diff --git a/tools/traceline/svgui/traceline.js b/tools/traceline/svgui/traceline.js new file mode 100755 index 0000000..c804f1f --- /dev/null +++ b/tools/traceline/svgui/traceline.js @@ -0,0 +1,684 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO +// - spacial partitioning of the data so that we don't have to scan the +// entire scene every time we render. +// - properly clip the SVG elements when they render, right now we are just +// letting them go negative or off the screen. This might give us a little +// bit better performance? +// - make the lines for thread creation work again. Figure out a better UI +// than these lines, because they can be a bit distracting. +// - Implement filters, so that you can filter on specific event types, etc. +// - Make the callstack box collapsable or scrollable or something, it takes +// up a lot of screen realestate now. +// - Figure out better ways to preserve screen realestate. +// - Make the thread bar heights configurable, figure out a better way to +// handle overlapping events (the pushdown code). +// - "Sticky" info, so you can click on something, and it will stay. Now +// if you need to scroll the page you usually lose the info because you +// will mouse over something else on your way to scrolling. +// - Help / legend +// - Loading indicator / debug console. +// - OH MAN BETTER COLORS PLEASE +// +// Dean McNamee <deanm@chromium.org> + +// Man... namespaces are such a pain. +var svgNS = 'http://www.w3.org/2000/svg'; +var xhtmlNS = 'http://www.w3.org/1999/xhtml'; + +function toHex(num) { + var str = ""; + var table = "0123456789abcdef"; + for (var i = 0; i < 8; ++i) { + str = table.charAt(num & 0xf) + str; + num >>= 4; + } + return str; +} + +// a TLThread represents information about a thread in the traceline data. +// A thread has a list of all events that happened on that thread, the start +// and end time of the thread, the thread id, and name, etc. +function TLThread(id, startms, endms) { + this.id = id; + // Default the name to the thread id, but if the application uses + // thread naming, we might see a THREADNAME event later and update. + this.name = "thread_" + id; + this.startms = startms; + this.endms = endms; + this.events = [ ]; +}; + +TLThread.prototype.duration_ms = +function() { + return this.endms - this.startms; +}; + +TLThread.prototype.AddEvent = +function(e) { + this.events.push(e); +}; + +TLThread.prototype.toString = +function() { + var res = "TLThread -- id: " + this.id + " name: " + this.name + + " startms: " + this.startms + " endms: " + this.endms + + " parent: " + this.parent; + return res; +}; + +// A TLEvent represents a single logged event that happened on a thread. +function TLEvent(e) { + this.eventtype = e['eventtype']; + this.thread = toHex(e['thread']); + this.cpu = toHex(e['cpu']); + this.ms = e['ms']; + this.e = e; +} + +function HTMLEscape(str) { + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); +} + +TLEvent.prototype.toString = +function() { + var res = "<b>ms:</b> " + this.ms + " " + + "<b>event:</b> " + this.eventtype + " " + + "<b>thread:</b> " + this.thread + " " + + "<b>cpu:</b> " + this.cpu + "<br/>"; + if ('ldrinfo' in this.e) { + res += "<b>ldrinfo:</b> " + this.e['ldrinfo'] + "<br/>"; + } + if ('done' in this.e && this.e['done'] > 0) { + res += "<b>done:</b> " + this.e['done'] + " "; + res += "<b>duration:</b> " + (this.e['done'] - this.ms) + "<br/>"; + } + if ('syscall' in this.e) { + res += "<b>syscall:</b> " + this.e['syscall']; + if ('syscallname' in this.e) { + res += " <b>syscallname:</b> " + this.e['syscallname']; + } + if ('retval' in this.e) { + res += " <b>retval:</b> " + this.e['retval']; + } + res += "<br/>" + } + if ('func_addr' in this.e) { + res += "<b>func_addr:</b> " + toHex(this.e['func_addr']); + if ('func_addr_name' in this.e) { + res += " <b>func_addr_name:</b> " + HTMLEscape(this.e['func_addr_name']); + } + res += "<br/>" + } + if ('stacktrace' in this.e) { + var stack = this.e['stacktrace']; + res += "<b>stacktrace:</b><br/>"; + for (var i = 0; i < stack.length; ++i) { + res += "0x" + toHex(stack[i][0]) + " - " + + HTMLEscape(stack[i][1]) + "<br/>"; + } + } + + return res; +} + +// The trace logger dumps all log events to a simple JSON array. We delay +// and background load the JSON, since it can be large. When the JSON is +// loaded, parseEvents(...) is called and passed the JSON data. To make +// things easier, we do a few passes on the data to group them together by +// thread, gather together some useful pieces of data in a single place, +// and form more of a structure out of the data. We also build links +// between related events, for example a thread creating a new thread, and +// the new thread starting to run. This structure is fairly close to what +// we want to represent in the interface. + +// Delay load the JSON data. We want to display the order in the order it was +// passed to us. Since we have no way of correlating the json callback to +// which script element it was called on, we load them one at a time. + +function JSONLoader(json_urls) { + this.urls_to_load = json_urls; + this.script_element = null; +} + +JSONLoader.prototype.IsFinishedLoading = +function() { return this.urls_to_load.length == 0; }; + +// Start loading of the next JSON URL. +JSONLoader.prototype.LoadNext = +function() { + var sc = document.createElementNS( + 'http://www.w3.org/1999/xhtml', 'script'); + this.script_element = sc; + + sc.setAttribute("src", this.urls_to_load[0]); + document.getElementsByTagNameNS(xhtmlNS, 'body')[0].appendChild(sc); +}; + +// Callback counterpart to load_next, should be called when the script element +// is finished loading. Returns the URL that was just loaded. +JSONLoader.prototype.DoneLoading = +function() { + // Remove the script element from the DOM. + this.script_element.parentNode.removeChild(this.script_element); + this.script_element = null; + // Return the URL that had just finished loading. + return this.urls_to_load.shift(); +}; + +var loader = null; + +function loadJSON(json_urls) { + loader = new JSONLoader(json_urls); + if (!loader.IsFinishedLoading()) + loader.LoadNext(); +} + +var traceline = new Traceline(); + +// Called from the JSON with the log event array. +function parseEvents(json) { + loader.DoneLoading(); + + var done = loader.IsFinishedLoading(); + if (!done) + loader.LoadNext(); + + traceline.ProcessJSON(json); + + if (done) + traceline.Render(); +} + +// The Traceline class represents our entire state, all of the threads from +// all sets of data, all of the events, DOM elements, etc. +function Traceline() { + // The array of threads that existed in the program. Hopefully in order + // they were created. This includes all threads from all sets of data. + this.threads = [ ]; + + // Keep a mapping of where in the list of threads a set starts... + this.thread_set_indexes = [ ]; + + // Map a thread id to the index in the threads array. A thread ID is the + // unique ID from the OS, along with our set id of which data file we were. + this.threads_by_id = { }; + + // The last event time of all of our events. + this.endms = 0; + + // Constants for SVG rendering... + this.kThreadHeightPx = 16; + this.kTimelineWidthPx = 1008; +} + +// Called to add another set of data into the traceline. +Traceline.prototype.ProcessJSON = +function(json_data) { + // Keep track of which threads belong to which sets of data... + var set_id = this.thread_set_indexes.length; + this.thread_set_indexes.push(this.threads.length); + + // TODO make this less hacky. Used to connect related events, like creating + // a thread and then having that thread run (two separate events which are + // related but come in at different times, etc). + var tiez = { }; + + // Run over the data, building TLThread's and TLEvents, and doing some + // processing to put things in an easier to display form... + for (var i = 0, il = json_data.length; i < il; ++i) { + var e = new TLEvent(json_data[i]); + + // Create a unique identifier for a thread by using the id of this data + // set, so that they are isolated from other sets of data with the same + // thread id, etc. TODO don't overwrite the original... + e.thread = set_id + '_' + e.thread; + + // If this is the first event ever seen on this thread, create a new + // thread object and add it to our lists of threads. + if (!(e.thread in this.threads_by_id)) { + var new_thread = new TLThread(e.thread, e.ms, e.ms); + this.threads_by_id[new_thread.id] = this.threads.length; + this.threads.push(new_thread); + } + + var thread = this.threads[this.threads_by_id[e.thread]]; + thread.AddEvent(e); + + // Keep trace of the time of the last event seen. + if (e.ms > this.endms) this.endms = e.ms; + if (e.ms > thread.endms) thread.endms = e.ms; + + switch(e.eventtype) { + case 'EVENT_TYPE_THREADNAME': + thread.name = e.e['threadname']; + break; + case 'EVENT_TYPE_CREATETHREAD': + tiez[e.e['eventid']] = e; + break; + case 'EVENT_TYPE_THREADBEGIN': + var pei = e.e['parenteventid']; + if (pei in tiez) { + e.parentevent = tiez[pei]; + tiez[pei].childevent = e; + } + break; + } + } +}; + +Traceline.prototype.Render = +function() { this.RenderSVG(); }; + +Traceline.prototype.RenderText = +function() { + var z = document.getElementsByTagNameNS(xhtmlNS, 'body')[0]; + for (var i = 0, il = this.threads.length; i < il; ++i) { + var p = document.createElementNS( + 'http://www.w3.org/1999/xhtml', 'p'); + p.innerHTML = this.threads[i].toString(); + z.appendChild(p); + } +}; + +// Oh man, so here we go. For two reasons, I implement my own scrolling +// system. First off, is that in order to scale, we want to have as little +// on the DOM as possible. This means not having off-screen elements in the +// DOM, as this slows down everything. This comes at a cost of more expensive +// scrolling performance since you have to re-render the scene. The second +// reason is a bug I stumbled into: +// https://bugs.webkit.org/show_bug.cgi?id=21968 +// This means that scrolling an SVG element doesn't really work properly +// anyway. So what the code does is this. We have our layout that looks like: +// [ thread names ] [ svg timeline ] +// [ scroll bar ] +// We make a fake scrollbar, which doesn't actually have the SVG inside of it, +// we want for when this scrolls, with some debouncing, and then when it has +// scrolled we rerender the scene. This means that the SVG element is never +// scrolled, and coordinates are always at 0. We keep the scene in millisecond +// units which also helps for zooming. We do our own hit testing and decide +// what needs to be renderer, convert from milliseconds to SVG pixels, and then +// draw the update into the static SVG element... Y coordinates are still +// always in pixels (since we aren't paging along the Y axis), but this might +// be something to fix up later. + +function SVGSceneLine(msg, klass, x1, y1, x2, y2) { + this.type = SVGSceneLine; + this.msg = msg; + this.klass = klass; + + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + + this.hittest = function(startms, dur) { + return true; + }; +} + +function SVGSceneRect(msg, klass, x, y, width, height) { + this.type = SVGSceneRect; + this.msg = msg; + this.klass = klass; + + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + this.hittest = function(startms, dur) { + return this.x <= (startms + dur) && + (this.x + this.width) >= startms; + }; +} + +Traceline.prototype.RenderSVG = +function() { + var threadnames = this.RenderSVGCreateThreadNames(); + var scene = this.RenderSVGCreateScene(); + + var curzoom = 8; + + // The height is static after we've created the scene + var dom = this.RenderSVGCreateDOM(threadnames, scene.height); + + dom.zoom(curzoom); + + dom.attach(); + + var draw = (function(obj) { + return function(scroll, total) { + var startms = (scroll / total) * obj.endms; + + var start = (new Date).getTime(); + var count = obj.RenderSVGRenderScene(dom, scene, startms, curzoom); + var total = (new Date).getTime() - start; + + dom.infoareadiv.innerHTML = + 'Scene render of ' + count + ' nodes took: ' + total + ' ms'; + }; + })(this, dom, scene); + + // Paint the initial paint with no scroll + draw(0, 1); + + // Hook us up to repaint on scrolls. + dom.redraw = draw; +}; + + +// Create all of the DOM elements for the SVG scene. +Traceline.prototype.RenderSVGCreateDOM = +function(threadnames, svgheight) { + + // Total div holds the container and the info area. + var totaldiv = document.createElementNS(xhtmlNS, 'div'); + + // Container holds the thread names, SVG element, and fake scroll bar. + var container = document.createElementNS(xhtmlNS, 'div'); + container.className = 'container'; + + // This is the div that holds the thread names along the left side, this is + // done in HTML for easier/better text support than SVG. + var threadnamesdiv = document.createElementNS(xhtmlNS, 'div'); + threadnamesdiv.className = 'threadnamesdiv'; + + // Add all of the names into the div, these are static and don't update. + for (var i = 0, il = threadnames.length; i < il; ++i) { + var div = document.createElementNS(xhtmlNS, 'div'); + div.className = 'threadnamediv'; + div.appendChild(document.createTextNode(threadnames[i])); + threadnamesdiv.appendChild(div); + } + + // SVG div goes along the right side, it holds the SVG element and our fake + // scroll bar. + var svgdiv = document.createElementNS(xhtmlNS, 'div'); + svgdiv.className = 'svgdiv'; + + // The SVG element, static width, and we will update the height after we've + // walked through how many threads we have and know the size. + var svg = document.createElementNS(svgNS, 'svg'); + svg.setAttributeNS(null, 'height', svgheight); + svg.setAttributeNS(null, 'width', this.kTimelineWidthPx); + + // The fake scroll div is an outer div with a fixed size with a scroll. + var fakescrolldiv = document.createElementNS(xhtmlNS, 'div'); + fakescrolldiv.className = 'fakescrolldiv'; + + // Fatty is inside the fake scroll div to give us the size we want to scroll. + var fattydiv = document.createElementNS(xhtmlNS, 'div'); + fattydiv.className = 'fattydiv'; + fakescrolldiv.appendChild(fattydiv); + + var infoareadiv = document.createElementNS(xhtmlNS, 'div'); + infoareadiv.className = 'infoareadiv'; + infoareadiv.innerHTML = 'Hover an event...'; + + // Set the SVG mouseover handler to write the data to the infoarea. + svg.addEventListener('mouseover', (function(infoarea) { + return function(e) { + if ('msg' in e.target && e.target.msg) { + infoarea.innerHTML = e.target.msg; + } + e.stopPropagation(); // not really needed, but might as well. + }; + })(infoareadiv), true); + + + svgdiv.appendChild(svg); + svgdiv.appendChild(fakescrolldiv); + + container.appendChild(threadnamesdiv); + container.appendChild(svgdiv); + + totaldiv.appendChild(container); + totaldiv.appendChild(infoareadiv); + + var widthms = Math.floor(this.endms + 2); + // Make member variables out of the things we want to 'export', things that + // will need to be updated each time we redraw the scene. + var obj = { + // The root of our piece of the DOM. + 'totaldiv': totaldiv, + // We will want to listen for scrolling on the fakescrolldiv + 'fakescrolldiv': fakescrolldiv, + // The SVG element will of course need updating. + 'svg': svg, + // The area we update with the info on mouseovers. + 'infoareadiv': infoareadiv, + // Called when we detected new scroll a should redraw + 'redraw': function() { }, + 'attached': false, + 'attach': function() { + document.getElementsByTagNameNS(xhtmlNS, 'body')[0].appendChild( + this.totaldiv); + this.attached = true; + }, + // The fatty div will have it's width adjusted based on the zoom level and + // the duration of the graph, to get the scrolling correct for the size. + 'zoom': function(curzoom) { + var width = widthms * curzoom; + fattydiv.style.width = width + 'px'; + }, + 'detach': function() { + this.totaldiv.parentNode.removeChild(this.totaldiv); + this.attached = false; + }, + }; + + // Watch when we get scroll events on the fake scrollbar and debounce. We + // need to give it a pointer to use in the closer to call this.redraw(); + fakescrolldiv.addEventListener('scroll', (function(theobj) { + var seqnum = 0; + return function(e) { + seqnum = (seqnum + 1) & 0xffff; + window.setTimeout((function(myseqnum) { + return function() { + if (seqnum == myseqnum) { + theobj.redraw(e.target.scrollLeft, e.target.scrollWidth); + } + }; + })(seqnum), 100); + }; + })(obj), false); + + return obj; +}; + +Traceline.prototype.RenderSVGCreateThreadNames = +function() { + // This names is the list to show along the left hand size. + var threadnames = [ ]; + + for (var i = 0, il = this.threads.length; i < il; ++i) { + var thread = this.threads[i]; + + // TODO make this not so stupid... + if (i != 0) { + for (var j = 0; j < this.thread_set_indexes.length; j++) { + if (i == this.thread_set_indexes[j]) { + threadnames.push('------'); + break; + } + } + } + + threadnames.push(thread.name); + } + + return threadnames; +}; + +Traceline.prototype.RenderSVGCreateScene = +function() { + // This scene is just a list of SVGSceneRect and SVGSceneLine, in no great + // order. In the future they should be structured to make range checking + // faster. + var scene = [ ]; + + // Remember, for now, Y (height) coordinates are still in pixels, since we + // don't zoom or scroll in this direction. X coordinates are milliseconds. + + var lasty = 0; + for (var i = 0, il = this.threads.length; i < il; ++i) { + var thread = this.threads[i]; + + // TODO make this not so stupid... + if (i != 0) { + for (var j = 0; j < this.thread_set_indexes.length; j++) { + if (i == this.thread_set_indexes[j]) { + lasty += this.kThreadHeightPx; + break; + } + } + } + + // For this thread, create the background thread (blue band); + scene.push(new SVGSceneRect(null, + 'thread', + thread.startms, + 1 + lasty, + thread.duration_ms(), + this.kThreadHeightPx - 2)); + + // Now create all of the events... + var pushdown = [ 0, 0, 0, 0 ]; + for (var j = 0, jl = thread.events.length; j < jl; ++j) { + var e = thread.events[j]; + + var y = 2 + lasty; + + // TODO this is a hack just so that we know the correct why position + // so we can create the threadline... + if (e.childevent) { + e.marky = y; + } + + // Handle events that we want to represent as lines and not event blocks, + // right now this is only thread creation. We map an event back to its + // "parent" event, and now lets add a line to represent that. + if (e.parentevent) { + var eparent = e.parentevent; + var msg = eparent.toString() + '<br/>' + e.toString(); + scene.push( + new SVGSceneLine(msg, 'eventline', + eparent.ms, eparent.marky + 5, e.ms, lasty + 5)); + } + + // We get negative done values (well, really, it was 0 and then made + // relative to start time) when a syscall never returned... + var dur = 0; + if ('done' in e.e && e.e['done'] > 0) { + dur = e.e['done'] - e.ms; + } + + // TODO skip short events for now, but eventually we should figure out + // a way to control this from the UI, etc. + if (dur < 0.2) + continue; + + var width = dur; + + // Try to find an available horizontal slot for our event. + for (var z = 0; z < pushdown.length; ++z) { + var found = false; + var slot = z; + if (pushdown[z] < e.ms) { + found = true; + } + if (!found) { + if (z != pushdown.length - 1) + continue; + slot = Math.floor(Math.random() * pushdown.length); + alert('blah'); + } + + pushdown[slot] = e.ms + dur; + y += slot * 4; + break; + } + + + // Create the event + klass = e.e.waiting ? 'eventwaiting' : 'event'; + scene.push( + new SVGSceneRect(e.toString(), klass, e.ms, y, width, 3)); + + // If there is a "parentevent", we want to make a line there. + // TODO + } + + lasty += this.kThreadHeightPx; + } + + return { + 'scene': scene, + 'width': this.endms + 2, + 'height': lasty, + }; +}; + +Traceline.prototype.RenderSVGRenderScene = +function(dom, scene, startms, curzoom) { + var stuff = scene.scene; + var svg = dom.svg; + + var count = 0; + + // Remove everything from the DOM. + while (svg.firstChild) + svg.removeChild(svg.firstChild); + + // Don't actually need this, but you can't transform on an svg element, + // so it's nice to have a <g> around for transforms... + var svgg = document.createElementNS(svgNS, 'g'); + + var dur = this.kTimelineWidthPx / curzoom; + + function timeToPixel(x) { + var x = Math.floor(x*curzoom); + return (x == 0 ? 1 : x); + } + + for (var i = 0, il = stuff.length; i < il; ++i) { + var thing = stuff[i]; + if (!thing.hittest(startms, startms+dur)) + continue; + + + if (thing.type == SVGSceneRect) { + var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttributeNS(null, 'class', thing.klass) + // TODO timeToPixel could be negative, clamp it at 0 + rect.setAttributeNS(null, 'x', timeToPixel(thing.x - startms)); + rect.setAttributeNS(null, 'y', thing.y); + // TODO thing.width can be larger than our current view, clamp it. + rect.setAttributeNS(null, 'width', timeToPixel(thing.width)); + rect.setAttributeNS(null, 'height', thing.height); + rect.msg = thing.msg; + svgg.appendChild(rect); + } else if (thing.type == SVGSceneLine) { + var line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + line.setAttributeNS(null, 'class', thing.klass) + // TODO timeToPixel could be negative, clamp it at 0 + line.setAttributeNS(null, 'x1', timeToPixel(thing.x1 - startms)); + line.setAttributeNS(null, 'y1', thing.y1); + line.setAttributeNS(null, 'x2', timeToPixel(thing.x2 - startms)); + line.setAttributeNS(null, 'y2', thing.y2); + line.msg = thing.msg; + svgg.appendChild(line); + } + + ++count; + } + + // Append the 'g' element on after we've build it. + svg.appendChild(svgg); + + return count; +}; |