gecko-dev/servo/components/profile/trace-dump.js

565 строки
18 KiB
JavaScript

/*
!!! 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 http://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));