servo: Merge #10694 - Add a method for dumping profiles as self-contained HTML w/ timeline visualization (from fitzgen:profile-traces-to-file); r=SimonSapin
authorNick Fitzgerald <fitzgen@gmail.com>
Thu, 28 Apr 2016 02:47:35 -0700
changeset 338649 283b27837c58c61c6ad917abba9688d545be456b
parent 338648 9ccf2775ea06047572036265d68efebb93c5c0e8
child 338650 8ae346b2cc146dfc0e5551d997443d7efdc9b9a4
push id31307
push usergszorc@mozilla.com
push dateSat, 04 Feb 2017 00:59:06 +0000
treeherdermozilla-central@94079d43835f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersSimonSapin
servo: Merge #10694 - Add a method for dumping profiles as self-contained HTML w/ timeline visualization (from fitzgen:profile-traces-to-file); r=SimonSapin 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 Gantt-chart style timeline. Example output HTML file: http://media.fitzgeraldnick.com/dumping-grounds/trace-reddit.html Mostly I made this because I wanted to see what kind of data the profiler has, and thought that this might be useful for others as well. I'm happy to add tests if we can figure out how to integrate them into CI, but I'm not sure how that would work. Thoughts? Source-Repo: https://github.com/servo/servo Source-Revision: b8e2fa58d61a4d77b67efa09a437ba6beb68e30e
servo/components/profile/Cargo.toml
servo/components/profile/lib.rs
servo/components/profile/time.rs
servo/components/profile/trace-dump-epilogue-1.html
servo/components/profile/trace-dump-epilogue-2.html
servo/components/profile/trace-dump-prologue-1.html
servo/components/profile/trace-dump-prologue-2.html
servo/components/profile/trace-dump.css
servo/components/profile/trace-dump.js
servo/components/profile/trace_dump.rs
servo/components/profile_traits/time.rs
servo/components/servo/Cargo.lock
servo/components/servo/lib.rs
servo/components/util/opts.rs
servo/ports/cef/Cargo.lock
servo/ports/gonk/Cargo.lock
servo/tests/unit/profile/time.rs
--- a/servo/components/profile/Cargo.toml
+++ b/servo/components/profile/Cargo.toml
@@ -11,15 +11,18 @@ path = "lib.rs"
 [dependencies]
 profile_traits = {path = "../profile_traits"}
 plugins = {path = "../plugins"}
 util = {path = "../util"}
 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]
 task_info = {path = "../../support/rust-task_info"}
 
 [target.'cfg(target_os = "linux")'.dependencies]
 regex = "0.1.55"
--- a/servo/components/profile/lib.rs
+++ b/servo/components/profile/lib.rs
@@ -2,33 +2,38 @@
  * 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/. */
 
 #![feature(alloc_jemalloc)]
 #![feature(box_syntax)]
 #![feature(iter_arith)]
 #![feature(plugin)]
 #![plugin(plugins)]
+#![feature(custom_derive)]
+#![plugin(serde_macros)]
 
 #![deny(unsafe_code)]
 
 #[allow(unused_extern_crates)]
 #[cfg(not(target_os = "windows"))]
 extern crate alloc_jemalloc;
 extern crate hbs_pow;
 extern crate ipc_channel;
 extern crate libc;
 #[macro_use]
 extern crate log;
 #[macro_use]
 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;
 extern crate util;
 
 #[allow(unsafe_code)]
 mod heartbeats;
 #[allow(unsafe_code)]
 pub mod mem;
 pub mod time;
+pub mod trace_dump;
--- a/servo/components/profile/time.rs
+++ b/servo/components/profile/time.rs
@@ -7,20 +7,23 @@
 use heartbeats;
 use ipc_channel::ipc::{self, IpcReceiver};
 use profile_traits::energy::{energy_interval_ms, read_energy_uj};
 use profile_traits::time::{ProfilerCategory, ProfilerChan, ProfilerMsg, TimerMetadata};
 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;
 
 pub trait Formattable {
     fn format(&self) -> String;
 }
 
 impl Formattable for Option<TimerMetadata> {
@@ -120,35 +123,40 @@ impl Formattable for ProfilerCategory {
 
 type ProfilerBuckets = BTreeMap<(ProfilerCategory, Option<TimerMetadata>), Vec<f64>>;
 
 // back end of the profiler that handles data aggregation and performance metrics
 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) => {
                 let chan = chan.clone();
                 spawn_named("Time profiler timer".to_owned(), move || {
                     loop {
                         thread::sleep(duration_from_seconds(period));
                         if chan.send(ProfilerMsg::Print).is_err() {
                             break;
                         }
                     }
                 });
                 // 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();
                 });
             }
             None => {
                 // No-op to handle messages when the time profiler is inactive.
                 spawn_named("Time profiler".to_owned(), move || {
                     loop {
                         match port.recv() {
@@ -201,21 +209,22 @@ impl Profiler {
                     start_energy = end_energy;
                 }
             });
         }
 
         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,
         }
     }
 
     pub fn start(&mut self) {
         while let Ok(msg) = self.port.recv() {
            if !self.handle_msg(msg) {
                break
            }
@@ -230,16 +239,19 @@ impl Profiler {
 
         self.buckets.insert(k, vec!(t));
     }
 
     fn handle_msg(&mut self, msg: ProfilerMsg) -> bool {
         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);
             },
             ProfilerMsg::Print => if let Some(ProfilerMsg::Time(..)) = self.last_msg {
                 // only print if more data has arrived since the last printout
                 self.print_buckets();
             },
             ProfilerMsg::Exit => {
new file mode 100644
--- /dev/null
+++ b/servo/components/profile/trace-dump-epilogue-1.html
@@ -0,0 +1,3 @@
+      ];
+    </script>
+    <script type="text/javascript">
new file mode 100644
--- /dev/null
+++ b/servo/components/profile/trace-dump-epilogue-2.html
@@ -0,0 +1,4 @@
+      //# sourceURL=trace-dump.js
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/servo/components/profile/trace-dump-prologue-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <style>
new file mode 100644
--- /dev/null
+++ b/servo/components/profile/trace-dump-prologue-2.html
@@ -0,0 +1,5 @@
+    </style>
+  </head>
+  <body>
+    <script>
+      window.TRACES = [
new file mode 100644
--- /dev/null
+++ b/servo/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;
+}
new file mode 100644
--- /dev/null
+++ b/servo/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();
new file mode 100644
--- /dev/null
+++ b/servo/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();
+}
--- a/servo/components/profile_traits/time.rs
+++ b/servo/components/profile_traits/time.rs
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 extern crate time as std_time;
 
 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,
     pub incremental: TimerMetadataReflowType,
 }
 
 #[derive(Clone, Deserialize, Serialize)]
 pub struct ProfilerChan(pub IpcSender<ProfilerMsg>);
@@ -30,17 +30,17 @@ pub enum ProfilerMsg {
     Time((ProfilerCategory, Option<TimerMetadata>), (u64, u64), (u64, u64)),
     /// Message used to force print the profiling metrics
     Print,
     /// Tells the profiler to shut down.
     Exit,
 }
 
 #[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,
     LayoutStyleRecalc,
     LayoutTextShaping,
     LayoutRestyleDamagePropagation,
     LayoutNonIncrementalReset,
     LayoutSelectorMatch,
@@ -73,23 +73,23 @@ pub enum ProfilerCategory {
     ScriptTimerEvent,
     ScriptStylesheetLoad,
     ScriptUpdateReplacedElement,
     ScriptWebSocketEvent,
     ScriptWorkerEvent,
     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,
 }
 
 pub fn profile<T, F>(category: ProfilerCategory,
                      meta: Option<TimerMetadata>,
                      profiler_chan: ProfilerChan,
@@ -118,9 +118,8 @@ pub fn send_profile_data(category: Profi
                          start_time: u64,
                          end_time: u64,
                          start_energy: u64,
                          end_energy: u64) {
     profiler_chan.send(ProfilerMsg::Time((category, meta),
                                          (start_time, end_time),
                                          (start_energy, end_energy)));
 }
-
--- a/servo/components/servo/Cargo.lock
+++ b/servo/components/servo/Cargo.lock
@@ -1647,16 +1647,19 @@ version = "0.0.1"
 dependencies = [
  "hbs-pow 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.2.2 (git+https://github.com/servo/ipc-channel)",
  "libc 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "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",
 ]
 
 [[package]]
 name = "profile_tests"
 version = "0.0.1"
--- a/servo/components/servo/lib.rs
+++ b/servo/components/servo/lib.rs
@@ -111,17 +111,18 @@ impl Browser {
 
         // Get both endpoints of a special channel for communication between
         // the client window and the compositor. This channel is unique because
         // messages to client may need to pump a platform-specific event loop
         // to deliver the message.
         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)
         });
 
         let (webrender, webrender_api_sender) = if opts::get().use_webrender {
             let mut resource_path = resources_dir_path();
             resource_path.push("shaders");
--- a/servo/components/util/opts.rs
+++ b/servo/components/util/opts.rs
@@ -12,17 +12,17 @@ use num_cpus;
 use prefs::{self, PrefValue};
 use resource_files::set_resources_path;
 use std::cmp;
 use std::default::Default;
 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};
 
 /// Global flags for Servo, currently set on the command line.
 #[derive(Clone, Deserialize, Serialize)]
 pub struct Opts {
     pub is_running_problem_test: bool,
@@ -45,16 +45,20 @@ pub struct Opts {
     /// The ratio of device pixels per px at the default scale. If unspecified, will use the
     /// platform default setting.
     pub device_pixels_per_px: Option<f32>,
 
     /// `None` to disable the time profiler or `Some` with an interval in seconds to enable it and
     /// 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>,
 
     /// The number of threads to use for layout (`-y`). Defaults to 1, which results in a recursive
     /// sequential algorithm.
     pub layout_threads: usize,
 
@@ -464,16 +468,17 @@ pub fn default_opts() -> Opts {
     Opts {
         is_running_problem_test: false,
         url: Some(Url::parse("about:blank").unwrap()),
         paint_threads: 1,
         gpu_painting: false,
         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,
         userscripts: None,
         user_stylesheets: Vec::new(),
         output_file: None,
         replace_surrogates: false,
         gc_profile: false,
@@ -524,16 +529,19 @@ pub fn from_cmdline_args(args: &[String]
     let mut opts = Options::new();
     opts.optflag("c", "cpu", "CPU painting (default)");
     opts.optflag("g", "gpu", "GPU painting");
     opts.optopt("o", "output", "Output file", "output.png");
     opts.optopt("s", "size", "Size of tiles", "512");
     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");
     opts.optflag("i", "nonincremental-layout", "Enable to turn off incremental layout.");
     opts.optflag("", "no-ssl", "Disables ssl certificate verification.");
     opts.optflagopt("", "userscripts",
                     "Uses userscripts in resources/user-agent-js, or a specified full path", "");
     opts.optmulti("", "user-stylesheet",
@@ -651,16 +659,25 @@ pub fn from_cmdline_args(args: &[String]
         None => cmp::max(num_cpus::get() * 3 / 4, 1),
     };
 
     // If only the flag is present, default to a 5 second period for both profilers.
     let time_profiler_period = opt_match.opt_default("p", "5").map(|period| {
         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)))
     });
 
     let gpu_painting = !FORCE_CPU_PAINTING && opt_match.opt_present("g");
 
     let mut layout_threads: usize = match opt_match.opt_str("y") {
         Some(layout_threads_str) => layout_threads_str.parse()
@@ -750,16 +767,17 @@ pub fn from_cmdline_args(args: &[String]
     let opts = Opts {
         is_running_problem_test: is_running_problem_test,
         url: Some(url),
         paint_threads: paint_threads,
         gpu_painting: gpu_painting,
         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,
         userscripts: opt_match.opt_default("userscripts", ""),
         user_stylesheets: user_stylesheets,
         output_file: opt_match.opt_str("o"),
         replace_surrogates: debug_options.replace_surrogates,
         gc_profile: debug_options.gc_profile,
--- a/servo/ports/cef/Cargo.lock
+++ b/servo/ports/cef/Cargo.lock
@@ -1525,16 +1525,19 @@ version = "0.0.1"
 dependencies = [
  "hbs-pow 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.2.2 (git+https://github.com/servo/ipc-channel)",
  "libc 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "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",
 ]
 
 [[package]]
 name = "profile_traits"
 version = "0.0.1"
--- a/servo/ports/gonk/Cargo.lock
+++ b/servo/ports/gonk/Cargo.lock
@@ -1508,16 +1508,19 @@ version = "0.0.1"
 dependencies = [
  "hbs-pow 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.2.2 (git+https://github.com/servo/ipc-channel)",
  "libc 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "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",
 ]
 
 [[package]]
 name = "profile_traits"
 version = "0.0.1"
--- a/servo/tests/unit/profile/time.rs
+++ b/servo/tests/unit/profile/time.rs
@@ -2,14 +2,14 @@
  * 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 profile::time;
 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);
     assert!(true, "Can tell the profiler thread to exit");
 }