aboutsummaryrefslogtreecommitdiffstats
path: root/components/profile/trace-dump.js
diff options
context:
space:
mode:
authorNick Fitzgerald <fitzgen@gmail.com>2016-04-16 11:48:23 -0700
committerNick Fitzgerald <fitzgen@gmail.com>2016-04-27 18:35:17 -0700
commit9fbb5c720eb89947720fa745aa08fa4fed7143b4 (patch)
treede0dbc67ce18b2dbb6b2960a90fccbb69b31ffb7 /components/profile/trace-dump.js
parent311dd0f930b9e8e90d08151f1956e2da25737d8a (diff)
downloadservo-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.js504
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();