aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--components/profile/Cargo.toml3
-rw-r--r--components/profile/lib.rs5
-rw-r--r--components/profile/time.rs18
-rw-r--r--components/profile/trace-dump-epilogue-1.html3
-rw-r--r--components/profile/trace-dump-epilogue-2.html4
-rw-r--r--components/profile/trace-dump-prologue-1.html5
-rw-r--r--components/profile/trace-dump-prologue-2.html5
-rw-r--r--components/profile/trace-dump.css100
-rw-r--r--components/profile/trace-dump.js504
-rw-r--r--components/profile/trace_dump.rs79
-rw-r--r--components/profile_traits/time.rs9
-rw-r--r--components/servo/Cargo.lock3
-rw-r--r--components/servo/lib.rs3
-rw-r--r--components/util/opts.rs20
-rw-r--r--ports/cef/Cargo.lock3
-rw-r--r--ports/gonk/Cargo.lock3
-rw-r--r--tests/unit/profile/time.rs2
17 files changed, 758 insertions, 11 deletions
diff --git a/components/profile/Cargo.toml b/components/profile/Cargo.toml
index 1088725a8b5..ca7a2dc25cb 100644
--- a/components/profile/Cargo.toml
+++ b/components/profile/Cargo.toml
@@ -16,6 +16,9 @@ ipc-channel = {git = "https://github.com/servo/ipc-channel"}
hbs-pow = "0.2"
log = "0.3.5"
libc = "0.2"
+serde = "0.7"
+serde_json = "0.7"
+serde_macros = "0.7"
time = "0.1.12"
[target.'cfg(target_os = "macos")'.dependencies]
diff --git a/components/profile/lib.rs b/components/profile/lib.rs
index 5885d85588d..da2d03c0a8d 100644
--- a/components/profile/lib.rs
+++ b/components/profile/lib.rs
@@ -7,6 +7,8 @@
#![feature(iter_arith)]
#![feature(plugin)]
#![plugin(plugins)]
+#![feature(custom_derive)]
+#![plugin(serde_macros)]
#![deny(unsafe_code)]
@@ -22,6 +24,8 @@ extern crate log;
extern crate profile_traits;
#[cfg(target_os = "linux")]
extern crate regex;
+extern crate serde;
+extern crate serde_json;
#[cfg(target_os = "macos")]
extern crate task_info;
extern crate time as std_time;
@@ -32,3 +36,4 @@ mod heartbeats;
#[allow(unsafe_code)]
pub mod mem;
pub mod time;
+pub mod trace_dump;
diff --git a/components/profile/time.rs b/components/profile/time.rs
index 6a1152a2d72..fa02cf51f3d 100644
--- a/components/profile/time.rs
+++ b/components/profile/time.rs
@@ -12,10 +12,13 @@ use profile_traits::time::{TimerMetadataReflowType, TimerMetadataFrameType};
use std::borrow::ToOwned;
use std::cmp::Ordering;
use std::collections::BTreeMap;
+use std::fs;
use std::io::{self, Write};
+use std::path;
use std::time::Duration;
use std::{thread, f64};
use std_time::precise_time_ns;
+use trace_dump::TraceDump;
use util::thread::spawn_named;
use util::time::duration_from_seconds;
@@ -125,10 +128,11 @@ pub struct Profiler {
pub port: IpcReceiver<ProfilerMsg>,
buckets: ProfilerBuckets,
pub last_msg: Option<ProfilerMsg>,
+ trace: Option<TraceDump>,
}
impl Profiler {
- pub fn create(period: Option<f64>) -> ProfilerChan {
+ pub fn create(period: Option<f64>, file_path: Option<String>) -> ProfilerChan {
let (chan, port) = ipc::channel().unwrap();
match period {
Some(period) => {
@@ -143,7 +147,11 @@ impl Profiler {
});
// Spawn the time profiler.
spawn_named("Time profiler".to_owned(), move || {
- let mut profiler = Profiler::new(port);
+ let trace = file_path.as_ref()
+ .map(path::Path::new)
+ .map(fs::File::create)
+ .map(|res| TraceDump::new(res.unwrap()));
+ let mut profiler = Profiler::new(port, trace);
profiler.start();
});
}
@@ -206,11 +214,12 @@ impl Profiler {
profiler_chan
}
- pub fn new(port: IpcReceiver<ProfilerMsg>) -> Profiler {
+ pub fn new(port: IpcReceiver<ProfilerMsg>, trace: Option<TraceDump>) -> Profiler {
Profiler {
port: port,
buckets: BTreeMap::new(),
last_msg: None,
+ trace: trace,
}
}
@@ -235,6 +244,9 @@ impl Profiler {
match msg.clone() {
ProfilerMsg::Time(k, t, e) => {
heartbeats::maybe_heartbeat(&k.0, t.0, t.1, e.0, e.1);
+ if let Some(ref mut trace) = self.trace {
+ trace.write_one(&k, t, e);
+ }
let ms = (t.1 - t.0) as f64 / 1000000f64;
self.find_or_insert(k, ms);
},
diff --git a/components/profile/trace-dump-epilogue-1.html b/components/profile/trace-dump-epilogue-1.html
new file mode 100644
index 00000000000..401f8301ee1
--- /dev/null
+++ b/components/profile/trace-dump-epilogue-1.html
@@ -0,0 +1,3 @@
+ ];
+ </script>
+ <script type="text/javascript">
diff --git a/components/profile/trace-dump-epilogue-2.html b/components/profile/trace-dump-epilogue-2.html
new file mode 100644
index 00000000000..8380010909d
--- /dev/null
+++ b/components/profile/trace-dump-epilogue-2.html
@@ -0,0 +1,4 @@
+ //# sourceURL=trace-dump.js
+ </script>
+ </body>
+</html>
diff --git a/components/profile/trace-dump-prologue-1.html b/components/profile/trace-dump-prologue-1.html
new file mode 100644
index 00000000000..7638b8d678a
--- /dev/null
+++ b/components/profile/trace-dump-prologue-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <style>
diff --git a/components/profile/trace-dump-prologue-2.html b/components/profile/trace-dump-prologue-2.html
new file mode 100644
index 00000000000..e82f833abd6
--- /dev/null
+++ b/components/profile/trace-dump-prologue-2.html
@@ -0,0 +1,5 @@
+ </style>
+ </head>
+ <body>
+ <script>
+ window.TRACES = [
diff --git a/components/profile/trace-dump.css b/components/profile/trace-dump.css
new file mode 100644
index 00000000000..75f9b93f8c3
--- /dev/null
+++ b/components/profile/trace-dump.css
@@ -0,0 +1,100 @@
+/* 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/. */
+
+body, html {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+ position: relative;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+}
+
+#slider {
+ height: 50px;
+ background-color: rgba(210, 210, 210, .5);
+ overflow: hidden;
+ box-shadow: 0px 0px 5px #999;
+ z-index: 10;
+}
+
+#slider-viewport {
+ background-color: rgba(255, 255, 255, .8);
+ min-width: 5px;
+ cursor: grab;
+ display: inline-block;
+ height: 100%;
+}
+
+.grabby {
+ background-color: #000;
+ width: 3px;
+ cursor: ew-resize;
+ height: 100%;
+ display: inline-block;
+}
+
+.slider-tick {
+ position: absolute;
+ height: 50px;
+ top: 0;
+ color: #000;
+ border-left: 1px solid #444;
+}
+
+.traces-tick {
+ position: absolute;
+ height: 100%;
+ top: 50px;
+ color: #aaa;
+ border-left: 1px solid #ddd;
+ z-index: -1;
+ overflow: hidden;
+ padding-top: calc(50% - .5em);
+}
+
+#traces {
+ flex: 1;
+ overflow-x: hidden;
+ overflow-y: auto;
+ flex-direction: column;
+}
+
+.outer {
+ flex: 1;
+ margin: 0;
+ padding: 0;
+}
+
+.outer:hover {
+ background-color: rgba(255, 255, 200, .7);
+}
+
+.inner {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ height: 100%;
+ color: white;
+ min-width: 1px;
+ text-align: center;
+}
+
+.tooltip {
+ display: none;
+}
+
+.outer:hover > .tooltip {
+ display: block;
+ position: absolute;
+ top: 50px;
+ right: 20px;
+ background-color: rgba(255, 255, 200, .7);
+ min-width: 20em;
+ padding: 1em;
+}
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();
diff --git a/components/profile/trace_dump.rs b/components/profile/trace_dump.rs
new file mode 100644
index 00000000000..8f53d4a9e37
--- /dev/null
+++ b/components/profile/trace_dump.rs
@@ -0,0 +1,79 @@
+/* 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/. */
+
+//! A module for writing time profiler traces out to a self contained HTML file.
+
+use profile_traits::time::{ProfilerCategory, TimerMetadata};
+use serde_json::{self};
+use std::fs;
+use std::io::Write;
+
+/// An RAII class for writing the HTML trace dump.
+pub struct TraceDump {
+ file: fs::File,
+}
+
+#[derive(Debug, Serialize)]
+struct TraceEntry {
+ category: ProfilerCategory,
+ metadata: Option<TimerMetadata>,
+
+ #[serde(rename = "startTime")]
+ start_time: u64,
+
+ #[serde(rename = "endTime")]
+ end_time: u64,
+
+ #[serde(rename = "startEnergy")]
+ start_energy: u64,
+
+ #[serde(rename = "endEnergy")]
+ end_energy: u64,
+}
+
+impl TraceDump {
+ /// Create a new TraceDump and write the prologue of the HTML file out to
+ /// disk.
+ pub fn new(mut file: fs::File) -> TraceDump {
+ write_prologue(&mut file);
+ TraceDump { file: file }
+ }
+
+ /// Write one trace to the trace dump file.
+ pub fn write_one(&mut self,
+ category: &(ProfilerCategory, Option<TimerMetadata>),
+ time: (u64, u64),
+ energy: (u64, u64)) {
+ let entry = TraceEntry {
+ category: category.0,
+ metadata: category.1.clone(),
+ start_time: time.0,
+ end_time: time.1,
+ start_energy: energy.0,
+ end_energy: energy.1,
+ };
+ serde_json::to_writer(&mut self.file, &entry).unwrap();
+ writeln!(&mut self.file, ",").unwrap();
+ }
+}
+
+impl Drop for TraceDump {
+ /// Write the epilogue of the trace dump HTML file out to disk on
+ /// destruction.
+ fn drop(&mut self) {
+ write_epilogue(&mut self.file);
+ }
+}
+
+fn write_prologue(file: &mut fs::File) {
+ writeln!(file, "{}", include_str!("./trace-dump-prologue-1.html")).unwrap();
+ writeln!(file, "{}", include_str!("./trace-dump.css")).unwrap();
+ writeln!(file, "{}", include_str!("./trace-dump-prologue-2.html")).unwrap();
+}
+
+fn write_epilogue(file: &mut fs::File) {
+ writeln!(file, "{}", include_str!("./trace-dump-epilogue-1.html")).unwrap();
+ writeln!(file, "{}", include_str!("./trace-dump.js")).unwrap();
+ writeln!(file, "{}", include_str!("./trace-dump-epilogue-2.html")).unwrap();
+}
diff --git a/components/profile_traits/time.rs b/components/profile_traits/time.rs
index 203d2953065..aa01c68efb8 100644
--- a/components/profile_traits/time.rs
+++ b/components/profile_traits/time.rs
@@ -8,7 +8,7 @@ use energy::read_energy_uj;
use ipc_channel::ipc::IpcSender;
use self::std_time::precise_time_ns;
-#[derive(PartialEq, Clone, PartialOrd, Eq, Ord, Deserialize, Serialize)]
+#[derive(PartialEq, Clone, PartialOrd, Eq, Ord, Debug, Deserialize, Serialize)]
pub struct TimerMetadata {
pub url: String,
pub iframe: TimerMetadataFrameType,
@@ -35,7 +35,7 @@ pub enum ProfilerMsg {
}
#[repr(u32)]
-#[derive(PartialEq, Clone, PartialOrd, Eq, Ord, Deserialize, Serialize, Debug, Hash)]
+#[derive(PartialEq, Clone, Copy, PartialOrd, Eq, Ord, Deserialize, Serialize, Debug, Hash)]
pub enum ProfilerCategory {
Compositing,
LayoutPerform,
@@ -78,13 +78,13 @@ pub enum ProfilerCategory {
ApplicationHeartbeat,
}
-#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub enum TimerMetadataFrameType {
RootWindow,
IFrame,
}
-#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub enum TimerMetadataReflowType {
Incremental,
FirstReflow,
@@ -123,4 +123,3 @@ pub fn send_profile_data(category: ProfilerCategory,
(start_time, end_time),
(start_energy, end_energy)));
}
-
diff --git a/components/servo/Cargo.lock b/components/servo/Cargo.lock
index 6f9538ee228..6f3a16c415c 100644
--- a/components/servo/Cargo.lock
+++ b/components/servo/Cargo.lock
@@ -1652,6 +1652,9 @@ dependencies = [
"plugins 0.0.1",
"profile_traits 0.0.1",
"regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_macros 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"task_info 0.0.1",
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
"util 0.0.1",
diff --git a/components/servo/lib.rs b/components/servo/lib.rs
index 47c66b97590..42e6706500c 100644
--- a/components/servo/lib.rs
+++ b/components/servo/lib.rs
@@ -116,7 +116,8 @@ impl Browser {
let (compositor_proxy, compositor_receiver) =
window.create_compositor_channel();
let supports_clipboard = window.supports_clipboard();
- let time_profiler_chan = profile_time::Profiler::create(opts.time_profiler_period);
+ let time_profiler_chan = profile_time::Profiler::create(opts.time_profiler_period,
+ opts.time_profiler_trace_path.clone());
let mem_profiler_chan = profile_mem::Profiler::create(opts.mem_profiler_period);
let devtools_chan = opts.devtools_port.map(|port| {
devtools::start_server(port)
diff --git a/components/util/opts.rs b/components/util/opts.rs
index a828925c88e..8ca76e87a20 100644
--- a/components/util/opts.rs
+++ b/components/util/opts.rs
@@ -17,7 +17,7 @@ use std::env;
use std::fs;
use std::fs::File;
use std::io::{self, Read, Write};
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicBool, ATOMIC_BOOL_INIT, Ordering};
use url::{self, Url};
@@ -50,6 +50,10 @@ pub struct Opts {
/// cause it to produce output on that interval (`-p`).
pub time_profiler_period: Option<f64>,
+ /// When the profiler is enabled, this is an optional path to dump a self-contained HTML file
+ /// visualizing the traces as a timeline.
+ pub time_profiler_trace_path: Option<String>,
+
/// `None` to disable the memory profiler or `Some` with an interval in seconds to enable it
/// and cause it to produce output on that interval (`-m`).
pub mem_profiler_period: Option<f64>,
@@ -469,6 +473,7 @@ pub fn default_opts() -> Opts {
tile_size: 512,
device_pixels_per_px: None,
time_profiler_period: None,
+ time_profiler_trace_path: None,
mem_profiler_period: None,
layout_threads: 1,
nonincremental_layout: false,
@@ -529,6 +534,9 @@ pub fn from_cmdline_args(args: &[String]) -> ArgumentParsingResult {
opts.optopt("", "device-pixel-ratio", "Device pixels per px", "");
opts.optopt("t", "threads", "Number of paint threads", "1");
opts.optflagopt("p", "profile", "Profiler flag and output interval", "10");
+ opts.optflagopt("", "profiler-trace-path",
+ "Path to dump a self-contained HTML timeline of profiler traces",
+ "");
opts.optflagopt("m", "memory-profile", "Memory profiler flag and output interval", "10");
opts.optflag("x", "exit", "Exit after load flag");
opts.optopt("y", "layout-threads", "Number of threads to use for layout", "1");
@@ -656,6 +664,15 @@ pub fn from_cmdline_args(args: &[String]) -> ArgumentParsingResult {
period.parse().unwrap_or_else(|err| args_fail(&format!("Error parsing option: -p ({})", err)))
});
+ if let Some(ref time_profiler_trace_path) = opt_match.opt_str("profiler-trace-path") {
+ let mut path = PathBuf::from(time_profiler_trace_path);
+ path.pop();
+ if let Err(why) = fs::create_dir_all(&path) {
+ error!("Couldn't create/open {:?}: {:?}",
+ Path::new(time_profiler_trace_path).to_string_lossy(), why);
+ }
+ }
+
let mem_profiler_period = opt_match.opt_default("m", "5").map(|period| {
period.parse().unwrap_or_else(|err| args_fail(&format!("Error parsing option: -m ({})", err)))
});
@@ -755,6 +772,7 @@ pub fn from_cmdline_args(args: &[String]) -> ArgumentParsingResult {
tile_size: tile_size,
device_pixels_per_px: device_pixels_per_px,
time_profiler_period: time_profiler_period,
+ time_profiler_trace_path: opt_match.opt_str("profiler-trace-path"),
mem_profiler_period: mem_profiler_period,
layout_threads: layout_threads,
nonincremental_layout: nonincremental_layout,
diff --git a/ports/cef/Cargo.lock b/ports/cef/Cargo.lock
index 6873ea8ad00..62fdf7ec7f8 100644
--- a/ports/cef/Cargo.lock
+++ b/ports/cef/Cargo.lock
@@ -1530,6 +1530,9 @@ dependencies = [
"plugins 0.0.1",
"profile_traits 0.0.1",
"regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_macros 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"task_info 0.0.1",
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
"util 0.0.1",
diff --git a/ports/gonk/Cargo.lock b/ports/gonk/Cargo.lock
index 919951afc87..a628302e16b 100644
--- a/ports/gonk/Cargo.lock
+++ b/ports/gonk/Cargo.lock
@@ -1513,6 +1513,9 @@ dependencies = [
"plugins 0.0.1",
"profile_traits 0.0.1",
"regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_macros 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"task_info 0.0.1",
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
"util 0.0.1",
diff --git a/tests/unit/profile/time.rs b/tests/unit/profile/time.rs
index b3156f40f87..b48e60dc423 100644
--- a/tests/unit/profile/time.rs
+++ b/tests/unit/profile/time.rs
@@ -7,7 +7,7 @@ use profile_traits::time::ProfilerMsg;
#[test]
fn time_profiler_smoke_test() {
- let chan = time::Profiler::create(None);
+ let chan = time::Profiler::create(None, None);
assert!(true, "Can create the profiler thread");
chan.send(ProfilerMsg::Exit);