mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
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.
This commit is contained in:
parent
311dd0f930
commit
9fbb5c720e
17 changed files with 758 additions and 11 deletions
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
3
components/profile/trace-dump-epilogue-1.html
Normal file
3
components/profile/trace-dump-epilogue-1.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
];
|
||||
</script>
|
||||
<script type="text/javascript">
|
4
components/profile/trace-dump-epilogue-2.html
Normal file
4
components/profile/trace-dump-epilogue-2.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
//# sourceURL=trace-dump.js
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
5
components/profile/trace-dump-prologue-1.html
Normal file
5
components/profile/trace-dump-prologue-1.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
5
components/profile/trace-dump-prologue-2.html
Normal file
5
components/profile/trace-dump-prologue-2.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.TRACES = [
|
100
components/profile/trace-dump.css
Normal file
100
components/profile/trace-dump.css
Normal file
|
@ -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;
|
||||
}
|
504
components/profile/trace-dump.js
Normal file
504
components/profile/trace-dump.js
Normal file
|
@ -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();
|
79
components/profile/trace_dump.rs
Normal file
79
components/profile/trace_dump.rs
Normal file
|
@ -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();
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
|
||||
|
|
3
components/servo/Cargo.lock
generated
3
components/servo/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
3
ports/cef/Cargo.lock
generated
3
ports/cef/Cargo.lock
generated
|
@ -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",
|
||||
|
|
3
ports/gonk/Cargo.lock
generated
3
ports/gonk/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue