| /* |
| !!! THIS FILE IS COPIED FROM https://github.com/fitzgen/servo-trace-dump !!! |
| Make sure to upstream changes, or they will get lost! |
| */ |
| /* 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 https://mozilla.org/MPL/2.0/. */ |
| "use strict"; |
| |
| (function (exports, window) { |
| |
| /*** State ******************************************************************/ |
| |
| const COLORS = exports.COLORS = [ |
| "#0088cc", |
| "#5b5fff", |
| "#b82ee5", |
| "#ed2655", |
| "#f13c00", |
| "#d97e00", |
| "#2cbb0f", |
| "#0072ab", |
| ]; |
| |
| // A class containing the cleaned up trace state. |
| const State = exports.State = (function () { |
| return class State { |
| constructor(rawTraces, windowWidth) { |
| // The traces themselves. |
| this.traces = null; |
| |
| // Only display traces that take at least this long. Default is .1 ms. |
| this.minimumTraceTime = 100000; |
| |
| // 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 = windowWidth; |
| |
| // 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(rawTraces); |
| } |
| |
| // Clean up and massage the trace data. |
| initialize(rawTraces) { |
| this.traces = rawTraces.filter(t => t.endTime - t.startTime >= this.minimumTraceTime); |
| |
| 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; |
| } |
| |
| // Translate pixels into nanoseconds. |
| pxToNs(px) { |
| return px / this.windowWidth * this.maxTime; |
| } |
| |
| // Translate nanoseconds into pixels. |
| nsToPx(ns) { |
| return ns / this.maxTime * this.windowWidth |
| } |
| |
| // Translate nanoseconds into pixels in the zoomed viewport region. |
| nsToSelectionPx(ns) { |
| return ns / (this.endSelection - this.startSelection) * this.windowWidth; |
| } |
| |
| // Update the start selection to the given position's time. |
| updateStartSelection(position) { |
| this.startSelection = clamp(this.pxToNs(position), |
| this.minTime, |
| this.endSelection); |
| } |
| |
| // Update the end selection to the given position's time. |
| updateEndSelection(position) { |
| this.endSelection = clamp(this.pxToNs(position), |
| this.startSelection, |
| this.maxTime); |
| } |
| |
| // Move the start and end selection by the given delta movement. |
| moveSelection(movement) { |
| let delta = clamp(this.pxToNs(movement), |
| -this.startSelection, |
| this.maxTime - this.endSelection); |
| |
| this.startSelection += delta; |
| this.endSelection += delta; |
| } |
| |
| // Widen or narrow the selection based on the given zoom. |
| zoomSelection(zoom) { |
| const increment = this.maxTime / 1000; |
| |
| this.startSelection = clamp(this.startSelection - zoom * increment, |
| this.minTime, |
| this.endSelection); |
| |
| this.endSelection = clamp(this.endSelection + zoom * increment, |
| this.startSelection, |
| this.maxTime); |
| } |
| |
| // Get the set of traces that overlap the current selection. |
| getTracesInSelection() { |
| const tracesInSelection = []; |
| |
| for (let i = 0; i < state.traces.length; i++) { |
| let trace = state.traces[i]; |
| |
| if (trace.endTime < state.startSelection) { |
| continue; |
| } |
| |
| if (trace.startTime > state.endSelection) { |
| break; |
| } |
| |
| tracesInSelection.push(trace); |
| } |
| |
| return tracesInSelection; |
| } |
| }; |
| }()); |
| |
| /*** Utilities **************************************************************/ |
| |
| // Ensure that min <= value <= max |
| const clamp = exports.clamp = (value, min, max) => { |
| return Math.max(Math.min(value, max), min); |
| }; |
| |
| // Get the closest power of ten to the given number. |
| const closestPowerOfTen = exports.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. |
| const selectIncrement = exports.selectIncrement = (range, maxTicks) => { |
| let increment = closestPowerOfTen(range / 10); |
| while (range / increment > maxTicks) { |
| increment *= 2; |
| } |
| return increment; |
| }; |
| |
| /*** Window Specific Code ***************************************************/ |
| |
| if (!window) { |
| return; |
| } |
| |
| // XXX: Everything below here relies on the presence of `window`! Try to |
| // minimize this code and factor out the parts that don't explicitly need |
| // `window` or `document` as much as possible. We can't easily test code that |
| // relies upon `window`. |
| |
| const state = exports.state = new State(window.TRACES, window.innerWidth); |
| |
| /*** Initial Persistent Element Creation ************************************/ |
| |
| window.document.body.innerHTML = ""; |
| |
| const sliderContainer = window.document.createElement("div"); |
| sliderContainer.id = "slider"; |
| window.document.body.appendChild(sliderContainer); |
| |
| const leftGrabby = window.document.createElement("span"); |
| leftGrabby.className = "grabby"; |
| sliderContainer.appendChild(leftGrabby); |
| |
| const sliderViewport = window.document.createElement("span"); |
| sliderViewport.id = "slider-viewport"; |
| sliderContainer.appendChild(sliderViewport); |
| |
| const rightGrabby = window.document.createElement("span"); |
| rightGrabby.className = "grabby"; |
| sliderContainer.appendChild(rightGrabby); |
| |
| const tracesContainer = window.document.createElement("div"); |
| tracesContainer.id = "traces"; |
| window.document.body.appendChild(tracesContainer); |
| |
| /*** Listeners *************************************************************/ |
| |
| // Run the given function and render afterwards. |
| const withRender = fn => function () { |
| fn.apply(null, arguments); |
| 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 => { |
| if (state.grabbingSlider) { |
| state.moveSelection(event.movementX); |
| event.preventDefault(); |
| render(); |
| } else if (state.grabbingLeft) { |
| state.updateStartSelection(event.clientX); |
| event.preventDefault(); |
| render(); |
| } else if (state.grabbingRight) { |
| state.updateEndSelection(event.clientX); |
| event.preventDefault(); |
| render(); |
| } |
| }); |
| |
| sliderContainer.addEventListener("wheel", withRender(event => { |
| state.zoomSelection(event.deltaY); |
| })); |
| |
| /*** Rendering **************************************************************/ |
| |
| // Create a function that calls the given function `fn` only once per animation |
| // frame. |
| const oncePerAnimationFrame = fn => { |
| let animationId = null; |
| return () => { |
| if (animationId !== null) { |
| return; |
| } |
| |
| animationId = window.requestAnimationFrame(() => { |
| fn(); |
| animationId = null; |
| }); |
| }; |
| }; |
| |
| // Only call the given function once per window width resize. |
| const 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`. |
| const render = oncePerAnimationFrame(() => { |
| renderSlider(); |
| renderTraces(); |
| }); |
| |
| // Render the slider at the top of the screen. |
| const renderSlider = () => { |
| let selectionDelta = state.endSelection - state.startSelection; |
| |
| // -6px because of the 3px width of each grabby. |
| sliderViewport.style.width = state.nsToPx(selectionDelta) - 6 + "px"; |
| leftGrabby.style.marginLeft = state.nsToPx(state.startSelection) + "px"; |
| rightGrabby.style.rightMargin = state.nsToPx(state.maxTime - state.endSelection) + "px"; |
| |
| renderSliderTicks(); |
| }; |
| |
| // Render the ticks along the slider overview. |
| const renderSliderTicks = oncePerWindowWidth(() => { |
| let oldTicks = Array.from(window.document.querySelectorAll(".slider-tick")); |
| for (let tick of oldTicks) { |
| tick.remove(); |
| } |
| |
| let increment = selectIncrement(state.maxTime, 20); |
| let px = state.nsToPx(increment); |
| let ms = 0; |
| for (let i = 0; i < state.windowWidth; i += px) { |
| let tick = window.document.createElement("div"); |
| tick.className = "slider-tick"; |
| tick.textContent = ms + " ms"; |
| tick.style.left = i + "px"; |
| window.document.body.appendChild(tick); |
| ms += increment / 1000000; |
| } |
| }); |
| |
| // Render the individual traces. |
| const renderTraces = () => { |
| renderTracesTicks(); |
| |
| let tracesToRender = state.getTracesInSelection(); |
| |
| // Ensure that we have exactly enough trace row 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. |
| const 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 = state.nsToPx(increment); |
| 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 = state.nsToSelectionPx(time - state.startSelection) + "px"; |
| tracesContainer.appendChild(tick); |
| |
| time += increment; |
| } |
| }; |
| |
| // Create the DOM structure for an individual trace. |
| const makeTraceTemplate = () => { |
| let outer = window.document.createElement("div"); |
| outer.className = "outer"; |
| |
| let inner = window.document.createElement("div"); |
| inner.className = "inner"; |
| |
| let tooltip = window.document.createElement("div"); |
| tooltip.className = "tooltip"; |
| |
| let header = window.document.createElement("h3"); |
| header.className = "header"; |
| tooltip.appendChild(header); |
| |
| let duration = window.document.createElement("h4"); |
| duration.className = "duration"; |
| tooltip.appendChild(duration); |
| |
| let pairs = window.document.createElement("dl"); |
| |
| let timeStartLabel = window.document.createElement("dt"); |
| timeStartLabel.textContent = "Start:" |
| pairs.appendChild(timeStartLabel); |
| |
| let timeStartValue = window.document.createElement("dd"); |
| timeStartValue.className = "start"; |
| pairs.appendChild(timeStartValue); |
| |
| let timeEndLabel = window.document.createElement("dt"); |
| timeEndLabel.textContent = "End:" |
| pairs.appendChild(timeEndLabel); |
| |
| let timeEndValue = window.document.createElement("dd"); |
| timeEndValue.className = "end"; |
| pairs.appendChild(timeEndValue); |
| |
| let urlLabel = window.document.createElement("dt"); |
| urlLabel.textContent = "URL:"; |
| pairs.appendChild(urlLabel); |
| |
| let urlValue = window.document.createElement("dd"); |
| urlValue.className = "url"; |
| pairs.appendChild(urlValue); |
| |
| let iframeLabel = window.document.createElement("dt"); |
| iframeLabel.textContent = "iframe?"; |
| pairs.appendChild(iframeLabel); |
| |
| let iframeValue = window.document.createElement("dd"); |
| iframeValue.className = "iframe"; |
| pairs.appendChild(iframeValue); |
| |
| let incrementalLabel = window.document.createElement("dt"); |
| incrementalLabel.textContent = "Incremental?"; |
| pairs.appendChild(incrementalLabel); |
| |
| let incrementalValue = window.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. |
| const renderTrace = (trace, elem) => { |
| let inner = elem.querySelector(".inner"); |
| inner.style.width = state.nsToSelectionPx(trace.endTime - trace.startTime) + "px"; |
| inner.style.marginLeft = state.nsToSelectionPx(trace.startTime - state.startSelection) + "px"; |
| |
| let category = trace.category; |
| 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(); |
| |
| }(typeof exports === "object" ? exports : window, |
| typeof window === "object" ? window : null)); |