зеркало из https://github.com/mozilla/gecko-dev.git
565 строки
18 KiB
JavaScript
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));
|