diff options
author | Nick Fitzgerald <fitzgen@gmail.com> | 2016-04-16 11:48:23 -0700 |
---|---|---|
committer | Nick Fitzgerald <fitzgen@gmail.com> | 2016-04-27 18:35:17 -0700 |
commit | 9fbb5c720eb89947720fa745aa08fa4fed7143b4 (patch) | |
tree | de0dbc67ce18b2dbb6b2960a90fccbb69b31ffb7 /components/profile/trace-dump.js | |
parent | 311dd0f930b9e8e90d08151f1956e2da25737d8a (diff) | |
download | servo-9fbb5c720eb89947720fa745aa08fa4fed7143b4.tar.gz servo-9fbb5c720eb89947720fa745aa08fa4fed7143b4.zip |
Add a method for dumping self-contained HTML timeline profiles
This commit adds the `--profiler-trace-path` flag. When combined with `-p` to
enable profiling, it dumps a profile as a self-contained HTML file to the given
path. The profile visualizes the traced operations as a gant-chart style
timeline.
Diffstat (limited to 'components/profile/trace-dump.js')
-rw-r--r-- | components/profile/trace-dump.js | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/components/profile/trace-dump.js b/components/profile/trace-dump.js new file mode 100644 index 00000000000..a663f89880f --- /dev/null +++ b/components/profile/trace-dump.js @@ -0,0 +1,504 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*** State *******************************************************************/ + +window.COLORS = [ + "#0088cc", + "#5b5fff", + "#b82ee5", + "#ed2655", + "#f13c00", + "#d97e00", + "#2cbb0f", + "#0072ab", +]; + +window.MIN_TRACE_TIME = 100000; // .1 ms + +// A class containing the cleaned up trace state. +window.State = (function () { + return class { + constructor() { + // The traces themselves. + this.traces = null; + + // Maximimum and minimum times seen in traces. These get normalized to be + // relative to 0, so after initialization minTime is always 0. + this.minTime = Infinity; + this.maxTime = 0; + + // The current start and end of the viewport selection. + this.startSelection = 0; + this.endSelection = 0; + + // The current width of the window. + this.windowWidth = window.innerWidth; + + // Whether the user is actively grabbing the left or right grabby, or the + // viewport slider. + this.grabbingLeft = false; + this.grabbingRight = false; + this.grabbingSlider = false; + + // Maps category labels to a persistent color so that they are always + // rendered the same color. + this.colorIndex = 0; + this.categoryToColor = Object.create(null); + + this.initialize(); + } + + // Clean up and massage the trace data. + initialize() { + this.traces = TRACES.filter(t => t.endTime - t.startTime >= MIN_TRACE_TIME); + window.TRACES = null; + + this.traces.sort((t1, t2) => { + let cmp = t1.startTime - t2.startTime; + if (cmp !== 0) { + return cmp; + } + + return t1.endTime - t2.endTime; + }); + + this.findMinTime(); + this.normalizeTimes(); + this.removeIdleTime(); + this.findMaxTime(); + + this.startSelection = 3 * this.maxTime / 8; + this.endSelection = 5 * this.maxTime / 8; + } + + // Find the minimum timestamp. + findMinTime() { + this.minTime = this.traces.reduce((min, t) => Math.min(min, t.startTime), + Infinity); + } + + // Find the maximum timestamp. + findMaxTime() { + this.maxTime = this.traces.reduce((max, t) => Math.max(max, t.endTime), + 0); + } + + // Normalize all times to be relative to the minTime and then reset the + // minTime to 0. + normalizeTimes() { + for (let i = 0; i < this.traces.length; i++) { + let trace = this.traces[i]; + trace.startTime -= this.minTime; + trace.endTime -= this.minTime; + } + this.minTime = 0; + } + + // Remove idle time between traces. It isn't useful to see and makes + // visualizing the data more difficult. + removeIdleTime() { + let totalIdleTime = 0; + let lastEndTime = null; + + for (let i = 0; i < this.traces.length; i++) { + let trace = this.traces[i]; + + if (lastEndTime !== null && trace.startTime > lastEndTime) { + totalIdleTime += trace.startTime - lastEndTime; + } + + lastEndTime = trace.endTime; + + trace.startTime -= totalIdleTime; + trace.endTime -= totalIdleTime; + } + } + + // Get the color for the given category, or assign one if no such color + // exists yet. + getColorForCategory(category) { + let result = this.categoryToColor[category]; + if (!result) { + result = COLORS[this.colorIndex++ % COLORS.length]; + this.categoryToColor[category] = result; + } + return result; + } + }; +}()); + +window.state = new State(); + +/*** Utilities ****************************************************************/ + +// Get the closest power of ten to the given number. +window.closestPowerOfTen = n => { + let powerOfTen = 1; + let diff = Math.abs(n - powerOfTen); + + while (true) { + let nextPowerOfTen = powerOfTen * 10; + let nextDiff = Math.abs(n - nextPowerOfTen); + + if (nextDiff > diff) { + return powerOfTen; + } + + diff = nextDiff; + powerOfTen = nextPowerOfTen; + } +}; + +// Select the tick increment for the given range size and maximum number of +// ticks to show for that range. +window.selectIncrement = (range, maxTicks) => { + let increment = closestPowerOfTen(range / 10); + while (range / increment > maxTicks) { + increment *= 2; + } + return increment; +}; + +// Get the category name for the given trace. +window.traceCategory = trace => { + return Object.keys(trace.category)[0]; +}; + +/*** Initial Persistent Element Creation **************************************/ + +document.body.innerHTML = ""; + +window.sliderContainer = document.createElement("div"); +sliderContainer.id = "slider"; +document.body.appendChild(sliderContainer); + +window.leftGrabby = document.createElement("span"); +leftGrabby.className = "grabby"; +sliderContainer.appendChild(leftGrabby); + +window.sliderViewport = document.createElement("span"); +sliderViewport.id = "slider-viewport"; +sliderContainer.appendChild(sliderViewport); + +window.rightGrabby = document.createElement("span"); +rightGrabby.className = "grabby"; +sliderContainer.appendChild(rightGrabby); + +window.tracesContainer = document.createElement("div"); +tracesContainer.id = "traces"; +document.body.appendChild(tracesContainer); + +/*** Listeners ***************************************************************/ + +// Run the given function and render afterwards. +window.withRender = fn => (...args) => { + fn(...args); + render(); +}; + +window.addEventListener("resize", withRender(() => { + state.windowWidth = window.innerWidth; +})); + +window.addEventListener("mouseup", () => { + state.grabbingSlider = state.grabbingLeft = state.grabbingRight = false; +}); + +leftGrabby.addEventListener("mousedown", () => { + state.grabbingLeft = true; +}); + +rightGrabby.addEventListener("mousedown", () => { + state.grabbingRight = true; +}); + +sliderViewport.addEventListener("mousedown", () => { + state.grabbingSlider = true; +}); + +window.addEventListener("mousemove", event => { + let ratio = event.clientX / state.windowWidth; + let relativeTime = ratio * state.maxTime; + let absTime = state.minTime + relativeTime; + absTime = Math.min(state.maxTime, absTime); + absTime = Math.max(state.minTime, absTime); + + if (state.grabbingSlider) { + let delta = event.movementX / state.windowWidth * state.maxTime; + if (delta < 0) { + delta = Math.max(-state.startSelection, delta); + } else { + delta = Math.min(state.maxTime - state.endSelection, delta); + } + + state.startSelection += delta; + state.endSelection += delta; + render(); + } else if (state.grabbingLeft) { + state.startSelection = Math.min(absTime, state.endSelection); + render(); + } else if (state.grabbingRight) { + state.endSelection = Math.max(absTime, state.startSelection); + render(); + } +}); + +sliderContainer.addEventListener("wheel", withRender(event => { + let increment = state.maxTime / 1000; + + state.startSelection -= event.deltaY * increment + state.startSelection = Math.max(0, state.startSelection); + state.startSelection = Math.min(state.startSelection, state.endSelection); + + state.endSelection += event.deltaY * increment; + state.endSelection = Math.min(state.maxTime, state.endSelection); + state.endSelection = Math.max(state.startSelection, state.endSelection); +})); + +/*** Rendering ***************************************************************/ + +// Create a function that calls the given function `fn` only once per animation +// frame. +window.oncePerAnimationFrame = fn => { + let animationId = null; + return () => { + if (animationId !== null) { + return; + } + + animationId = requestAnimationFrame(() => { + fn(); + animationId = null; + }); + }; +}; + +// Only call the given function once per window width resize. +window.oncePerWindowWidth = fn => { + let lastWidth = null; + return () => { + if (state.windowWidth !== lastWidth) { + fn(); + lastWidth = state.windowWidth; + } + }; +}; + +// Top level entry point for rendering. Renders the current `window.state`. +window.render = oncePerAnimationFrame(() => { + renderSlider(); + renderTraces(); +}); + +// Render the slider at the top of the screen. +window.renderSlider = () => { + let selectionDelta = state.endSelection - state.startSelection; + + leftGrabby.style.marginLeft = (state.startSelection / state.maxTime) * state.windowWidth + "px"; + + // -6px because of the 3px width of each grabby. + sliderViewport.style.width = (selectionDelta / state.maxTime) * state.windowWidth - 6 + "px"; + + rightGrabby.style.rightMargin = (state.maxTime - state.endSelection) / state.maxTime + * state.windowWidth + "px"; + + renderSliderTicks(); +}; + +// Render the ticks along the slider overview. +window.renderSliderTicks = oncePerWindowWidth(() => { + let oldTicks = Array.from(document.querySelectorAll(".slider-tick")); + for (let tick of oldTicks) { + tick.remove(); + } + + let increment = selectIncrement(state.maxTime, 20); + let px = increment / state.maxTime * state.windowWidth; + let ms = 0; + for (let i = 0; i < state.windowWidth; i += px) { + let tick = document.createElement("div"); + tick.className = "slider-tick"; + tick.textContent = ms + " ms"; + tick.style.left = i + "px"; + document.body.appendChild(tick); + ms += increment / 1000000; + } +}); + +// Render the individual traces. +window.renderTraces = () => { + renderTracesTicks(); + + let tracesToRender = []; + for (let i = 0; i < state.traces.length; i++) { + let trace = state.traces[i]; + + if (trace.endTime < state.startSelection || trace.startTime > state.endSelection) { + continue; + } + + tracesToRender.push(trace); + } + + // Ensure that we have enouch traces elements. If we have more elements than + // traces we are going to render, then remove some. If we have fewer elements + // than traces we are going to render, then add some. + let rows = Array.from(tracesContainer.querySelectorAll(".outer")); + while (rows.length > tracesToRender.length) { + rows.pop().remove(); + } + while (rows.length < tracesToRender.length) { + let elem = makeTraceTemplate(); + tracesContainer.appendChild(elem); + rows.push(elem); + } + + for (let i = 0; i < tracesToRender.length; i++) { + renderTrace(tracesToRender[i], rows[i]); + } +}; + +// Render the ticks behind the traces. +window.renderTracesTicks = () => { + let oldTicks = Array.from(tracesContainer.querySelectorAll(".traces-tick")); + for (let tick of oldTicks) { + tick.remove(); + } + + let selectionDelta = state.endSelection - state.startSelection; + let increment = selectIncrement(selectionDelta, 10); + let px = increment / selectionDelta * state.windowWidth; + let offset = state.startSelection % increment; + let time = state.startSelection - offset + increment; + + while (time < state.endSelection) { + let tick = document.createElement("div"); + tick.className = "traces-tick"; + tick.textContent = Math.round(time / 1000000) + " ms"; + tick.style.left = (time - state.startSelection) / selectionDelta * state.windowWidth + "px"; + tracesContainer.appendChild(tick); + + time += increment; + } +}; + +// Create the DOM structure for an individual trace. +window.makeTraceTemplate = () => { + let outer = document.createElement("div"); + outer.className = "outer"; + + let inner = document.createElement("div"); + inner.className = "inner"; + + let tooltip = document.createElement("div"); + tooltip.className = "tooltip"; + + let header = document.createElement("h3"); + header.className = "header"; + tooltip.appendChild(header); + + let duration = document.createElement("h4"); + duration.className = "duration"; + tooltip.appendChild(duration); + + let pairs = document.createElement("dl"); + + let timeStartLabel = document.createElement("dt"); + timeStartLabel.textContent = "Start:" + pairs.appendChild(timeStartLabel); + + let timeStartValue = document.createElement("dd"); + timeStartValue.className = "start"; + pairs.appendChild(timeStartValue); + + let timeEndLabel = document.createElement("dt"); + timeEndLabel.textContent = "End:" + pairs.appendChild(timeEndLabel); + + let timeEndValue = document.createElement("dd"); + timeEndValue.className = "end"; + pairs.appendChild(timeEndValue); + + let urlLabel = document.createElement("dt"); + urlLabel.textContent = "URL:"; + pairs.appendChild(urlLabel); + + let urlValue = document.createElement("dd"); + urlValue.className = "url"; + pairs.appendChild(urlValue); + + let iframeLabel = document.createElement("dt"); + iframeLabel.textContent = "iframe?"; + pairs.appendChild(iframeLabel); + + let iframeValue = document.createElement("dd"); + iframeValue.className = "iframe"; + pairs.appendChild(iframeValue); + + let incrementalLabel = document.createElement("dt"); + incrementalLabel.textContent = "Incremental?"; + pairs.appendChild(incrementalLabel); + + let incrementalValue = document.createElement("dd"); + incrementalValue.className = "incremental"; + pairs.appendChild(incrementalValue); + + tooltip.appendChild(pairs); + outer.appendChild(tooltip); + outer.appendChild(inner); + return outer; +}; + +// Render `trace` into the given `elem`. We reuse the trace elements and modify +// them with the new trace that will populate this particular `elem` rather than +// clearing the DOM out and rebuilding it from scratch. Its a bit of a +// performance win when there are a lot of traces being rendered. Funnily +// enough, iterating over the complete set of traces hasn't been a performance +// problem at all and the bottleneck seems to be purely rendering the subset of +// traces we wish to show. +window.renderTrace = (trace, elem) => { + let inner = elem.querySelector(".inner"); + inner.style.width = (trace.endTime - trace.startTime) / (state.endSelection - state.startSelection) + * state.windowWidth + "px"; + inner.style.marginLeft = (trace.startTime - state.startSelection) + / (state.endSelection - state.startSelection) + * state.windowWidth + "px"; + + let category = traceCategory(trace); + inner.textContent = category; + inner.style.backgroundColor = state.getColorForCategory(category); + + let header = elem.querySelector(".header"); + header.textContent = category; + + let duration = elem.querySelector(".duration"); + duration.textContent = (trace.endTime - trace.startTime) / 1000000 + " ms"; + + let timeStartValue = elem.querySelector(".start"); + timeStartValue.textContent = trace.startTime / 1000000 + " ms"; + + let timeEndValue = elem.querySelector(".end"); + timeEndValue.textContent = trace.endTime / 1000000 + " ms"; + + if (trace.metadata) { + let urlValue = elem.querySelector(".url"); + urlValue.textContent = trace.metadata.url; + urlValue.removeAttribute("hidden"); + + let iframeValue = elem.querySelector(".iframe"); + iframeValue.textContent = trace.metadata.iframe.RootWindow ? "No" : "Yes"; + iframeValue.removeAttribute("hidden"); + + let incrementalValue = elem.querySelector(".incremental"); + incrementalValue.textContent = trace.metadata.incremental.Incremental ? "Yes" : "No"; + incrementalValue.removeAttribute("hidden"); + } else { + elem.querySelector(".url").setAttribute("hidden", ""); + elem.querySelector(".iframe").setAttribute("hidden", ""); + elem.querySelector(".incremental").setAttribute("hidden", ""); + } +}; + +render(); |