summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html')
-rw-r--r--python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html694
1 files changed, 694 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html b/python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html
new file mode 100644
index 0000000000..9daf30178b
--- /dev/null
+++ b/python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html
@@ -0,0 +1,694 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Build System Resource Usage</title>
+
+ <meta charset='utf-8'>
+ <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
+ <link rel="stylesheet" href="https://firefoxux.github.io/design-tokens/photon-colors/photon-colors.css" charset="utf-8">
+ <style>
+
+svg {
+ overflow: visible;
+}
+body {
+ background-color: var(--grey-20);
+ font-family: sans-serif;
+ padding: 30px 80px;
+ display: flex;
+ flex-direction: column;
+}
+
+h1 {
+ font-size: 2.3rem;
+ margin: 40px 0 20px;
+}
+
+.dashboard-card {
+ padding: 32px 80px;
+ background-color: var(--white-100);
+ border-radius: 8px;
+ margin: 60px 0;
+}
+
+h2 {
+ color: var(--grey-80);
+ margin-bottom: 30px;
+}
+
+.chart {
+ padding-top: 20px;
+}
+
+.grid-list ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ grid-template-columns: max-content max-content max-content;
+ gap: 15px 4px;
+}
+
+.grid-list ul li {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: subgrid;
+ place-items: baseline;
+}
+
+.grid-list ul li .label {
+ color: var(--grey-50);
+ padding-right: 40px;
+}
+
+.grid-list ul li .value {
+ justify-self: end;
+ font-size: 1.3em;
+}
+
+.axis path,
+.axis line {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+
+.grid {
+ stroke: gray;
+ stroke-dasharray: 3, 2;
+ opacity: 0.4;
+}
+
+.area {
+ fill: var(--blue-50);
+}
+
+.graphs {
+ text-anchor: end;
+}
+
+.timeline {
+ fill: var(--blue-40);
+ stroke: var(--blue-70);
+ stroke-width: 1;
+}
+
+.short {
+ fill: var(--grey-50);
+ stroke: var(--blue-70);
+ stroke-width: 1;
+}
+
+#tooltip {
+ z-index: 10;
+ position: fixed;
+ background: var(--white-100);
+ padding: 20px;
+ border-radius: 5px;
+ border: 1px solid var(--grey-20);
+}
+
+.align-self-start {
+ align-self: start;
+}
+
+/* utility-classes from Firefox Photon style guide "https://design.firefox.com/photon/"" */
+
+.shadow-10 {
+ box-shadow: 0 1px 4px var(--grey-90-a10);
+}
+
+.shadow-20 {
+ box-shadow: 0 2px 8px var(--grey-90-a10);
+}
+
+.shadow-30 {
+ box-shadow: 0 4px 16px var(--grey-90-a10);
+}
+
+.shadow-custom {
+ box-shadow: 0 8px 12px 1px rgba(29,17,51,.04),0 3px 16px 2px rgba(9,32,77,.12),0 5px 10px -3px rgba(29,17,51,.12);
+}
+
+
+ </style>
+ </head>
+ <body>
+ <script>
+var currentResources;
+
+/**
+ * Interface for a build resources JSON file.
+ */
+function BuildResources(data) {
+ if (data.version < 1 || data.version > 3) {
+ throw new Error("Unsupported version of the JSON format: " + data.version);
+ }
+
+ this.resources = [];
+
+ var cpu_fields = data.cpu_times_fields;
+ var io_fields = data.io_fields;
+ var virt_fields = data.virt_fields;
+ var swap_fields = data.swap_fields;
+
+ function convert(dest, source, sourceKey, destKey, fields) {
+ var i = 0;
+ fields.forEach(function (field) {
+ dest[destKey][field] = source[sourceKey][i];
+ i++;
+ });
+ }
+
+ var offset = data.start;
+ var cpu_times_totals = {};
+
+ cpu_fields.forEach(function (field) {
+ cpu_times_totals[field] = 0;
+ });
+
+ this.ioTotal = {};
+ var i = 0;
+ io_fields.forEach(function (field) {
+ this.ioTotal[field] = data.overall.io[i];
+ i++;
+ }.bind(this));
+
+ data.samples.forEach(function (sample) {
+ var entry = {
+ start: sample.start - offset,
+ end: sample.end - offset,
+ duration: sample.duration,
+ cpu_percent: sample.cpu_percent_mean,
+ cpu_times: {},
+ cpu_times_percents: {},
+ io: {},
+ virt: {},
+ swap: {},
+ };
+
+ convert(entry, sample, "cpu_times_sum", "cpu_times", cpu_fields);
+ convert(entry, sample, "io", "io", io_fields);
+ convert(entry, sample, "virt", "virt", virt_fields);
+ convert(entry, sample, "swap", "swap", swap_fields);
+
+ var total = 0;
+ for (var k in entry.cpu_times) {
+ cpu_times_totals[k] += entry.cpu_times[k];
+ total += entry.cpu_times[k];
+ }
+
+ for (var k in entry.cpu_times) {
+ if (total == 0) {
+ if (k == "idle") {
+ entry.cpu_times_percents[k] = 100;
+ } else {
+ entry.cpu_times_percents[k] = 0;
+ }
+ } else {
+ entry.cpu_times_percents[k] = entry.cpu_times[k] / total * 100;
+ }
+ }
+
+ this.resources.push(entry);
+ }.bind(this));
+
+ this.virt_fields = virt_fields;
+ this.cpu_times_fields = [];
+
+ // Filter out CPU fields that have no values.
+ for (var k in cpu_times_totals) {
+ var v = cpu_times_totals[k];
+ if (v) {
+ this.cpu_times_fields.push(k);
+ continue;
+ }
+
+ this.resources.forEach(function (entry) {
+ delete entry.cpu_times[k];
+ delete entry.cpu_times_percents[k];
+ });
+ }
+
+ this.offset = offset;
+ this.data = data;
+}
+
+BuildResources.prototype = Object.freeze({
+ get start() {
+ return this.data.start;
+ },
+
+ get startDate() {
+ return new Date(this.start * 1000);
+ },
+
+ get end() {
+ return this.data.end;
+ },
+
+ get endDate() {
+ return new Date(this.end * 1000);
+ },
+
+ get duration() {
+ return this.data.duration;
+ },
+
+ get sample_times() {
+ var times = [];
+ this.resources.forEach(function (sample) {
+ times.push(sample.start);
+ });
+
+ return times;
+ },
+
+ get cpuPercent() {
+ return this.data.overall.cpu_percent_mean;
+ },
+
+ get tiers() {
+ var t = [];
+
+ this.data.phases.forEach(function (e) {
+ t.push(e.name);
+ });
+
+ return t;
+ },
+
+ getTier: function (tier) {
+ for (var i = 0; i < this.data.phases.length; i++) {
+ var t = this.data.phases[i];
+
+ if (t.name == tier) {
+ return t;
+ }
+ }
+ },
+});
+
+function format_percent(d, i) {
+ return d + "%";
+}
+
+const updateChartsOnResizeWindow = () => addEventListener('resize', updateResourcesGraph);
+updateChartsOnResizeWindow();
+
+// layout spacing to set charts widths
+const marginBodyX = 8;
+const paddingBodyX = 80;
+const marginCardsX = 0;
+const paddingCardsX = 80;
+
+function updateResourcesGraph() {
+ renderResources("cpu_graph", currentResources, 400, "cpu_times_fields", "cpu_times_percents", 100, format_percent, [
+ ["nice", "#0d9fff"],
+ ["irq", "#ff0d9f"],
+ ["softirq", "#ff0d9f"],
+ ["steal", "#000000"],
+ ["guest", "#000000"],
+ ["guest_nice", "#000000"],
+ ["system", "var(--purple-80)"],
+ ["iowait", "#ff0d25"],
+ ["user", "var(--magenta-50)"],
+ ["idle", "var(--grey-20)"],
+ ]);
+ // On macos, there doesn't seem to be a combination of values that sums up to
+ // the total, so just use the percentage. Only macos has a "wired" value.
+ if ('wired' in currentResources.resources[0].virt) {
+ renderResources("mem_graph", currentResources, 200, "virt_fields", "virt", 100, format_percent, [
+ ["percent", "var(--blue-50"],
+ ]);
+ } else {
+ renderResources("mem_graph", currentResources, 200, "virt_fields", "virt", currentResources.resources[0].virt['total'], d3.format("s"), [
+ ["used", "var(--blue-50"],
+ ["buffers", "#f65c5c"],
+ ["cached", "var(--orange-50)"],
+ ["free", "var(--grey-20)"],
+ ]);
+ }
+ renderTimeline("tiers", currentResources);
+ document.getElementById("wall_time").textContent = Math.round(currentResources.duration * 100) / 100;
+ document.getElementById("start_date").textContent = currentResources.startDate.toLocaleString();
+ document.getElementById("end_date").textContent = currentResources.endDate.toLocaleString();
+ document.getElementById("cpu_percent").textContent = Math.round(currentResources.cpuPercent * 100) / 100;
+ document.getElementById("write_bytes").textContent = currentResources.ioTotal["write_bytes"];
+ document.getElementById("read_bytes").textContent = currentResources.ioTotal["read_bytes"];
+ document.getElementById("write_time").textContent = currentResources.ioTotal["write_time"];
+ document.getElementById("read_time").textContent = currentResources.ioTotal["read_time"];
+}
+
+function renderKey(key) {
+ d3.json(key, function onResource(error, response) {
+ if (error) {
+ alert("Data not available. Is the server still running?");
+ return;
+ }
+
+ currentResources = new BuildResources(response);
+ updateResourcesGraph();
+ });
+}
+
+function renderResources(id, resources, height, fields_attr, data_attr, max_value, tick_format, layers) {
+ document.getElementById(id).innerHTML = "";
+
+ const margin = {top: 20, right: 20, bottom: 20, left: 50};
+ const width = window.innerWidth - 2 * (marginBodyX + paddingBodyX + marginCardsX + paddingCardsX) - margin.left;
+ var heightChart = height - margin.top - margin.bottom;
+
+ var x = d3.scale.linear()
+ .range([0, width])
+ .domain(d3.extent(resources.resources, function (d) { return d.start; }))
+ ;
+ var y = d3.scale.linear()
+ .range([heightChart, 0])
+ .domain([0, max_value])
+ ;
+
+ var xAxis = d3.svg.axis()
+ .scale(x)
+ .orient("bottom")
+ ;
+ var yAxis = d3.svg.axis()
+ .scale(y)
+ .orient("left")
+ .tickFormat(tick_format)
+ ;
+
+ var area = d3.svg.area()
+ .x(function (d) { return x(d.start); })
+ .y0(function(d) { return y(d.y0); })
+ .y1(function(d) { return y(d.y0 + d.y); })
+ ;
+
+ var stack = d3.layout.stack()
+ .values(function (d) { return d.values; })
+ ;
+
+ // Manually control the layer order because we want it consistent and want
+ // to inject some sanity.
+ var layers = layers.filter(function (l) {
+ return resources[fields_attr].indexOf(l[0]) != -1;
+ });
+
+ // Draw a legend.
+ var legend = d3.select("#" + id)
+ .append("svg")
+ .attr("width", width + margin.left + margin.right)
+ .attr("height", 15)
+ .append("g")
+ .attr("class", "legend")
+ ;
+
+ legend.selectAll("g")
+ .data(layers)
+ .enter()
+ .append("g")
+ .each(function (d, i) {
+ var g = d3.select(this);
+ g.append("rect")
+ .attr("x", i * 100 + 20)
+ .attr("y", 0)
+ .attr("width", 10)
+ .attr("height", 10)
+ .style("fill", d[1])
+ ;
+ g.append("text")
+ .attr("x", i * 100 + 40)
+ .attr("y", 10)
+ .attr("height", 10)
+ .attr("width", 70)
+ .text(d[0])
+ ;
+ })
+ ;
+
+ var svg = d3.select("#" + id).append("svg")
+ .attr("width", width)
+ .attr("height", heightChart + margin.top + margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
+ ;
+
+ var data = stack(layers.map(function (layer) {
+ return {
+ name: layer[0],
+ color: layer[1],
+ values: resources.resources.map(function (d) {
+ return {
+ start: d.start,
+ y: d[data_attr][layer[0]],
+ };
+ }),
+ };
+ }));
+
+ var graphs = svg.selectAll(".graphs")
+ .data(data)
+ .enter().append("g")
+ .attr("class", "graphs")
+ ;
+
+ graphs.append("path")
+ .attr("class", "area")
+ .attr("d", function (d) { return area(d.values); })
+ .style("fill", function (d) { return d.color; })
+ ;
+
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + heightChart + ")")
+ .call(xAxis)
+ ;
+
+ svg.append("g")
+ .attr("class", "x grid")
+ .attr("transform", "translate(0," + heightChart + ")")
+ .call(xAxis.tickSize(-heightChart, 0, 0).tickFormat(""))
+ ;
+
+ svg.append("g")
+ .attr("class", "y axis")
+ .call(yAxis)
+ ;
+
+ svg.append("g")
+ .attr("class", "y grid")
+ .call(yAxis.tickSize(-width, 0, 0).tickFormat(""))
+ ;
+}
+
+function renderTimeline(id, resources) {
+ document.getElementById(id).innerHTML = "";
+
+ var margin = {top: 20, right: 20, bottom: 20, left: 50};
+ const width = window.innerWidth - 2 * (marginBodyX + paddingBodyX + marginCardsX + paddingCardsX) - margin.left;
+
+ var x = d3.scale.linear()
+ .range([0, width])
+ .domain(d3.extent(resources.resources, function (d) { return d.start; }))
+ ;
+ // Now we render a timeline of sorts of the tiers
+ // There is a row of rectangles that visualize divisions between the
+ // different items. We use the same x scale as the resource graph so times
+ // line up properly.
+ svg = d3.select("#" + id).append("svg")
+ .attr("width", width)
+ .attr("height", 100)
+ .append("g")
+ ;
+
+ var y = d3.scale.linear().range([10, 0]).domain([0, 1]);
+
+ resources.tiers.forEach(function (t, i) {
+ var tier = resources.getTier(t);
+
+ var x_start = x(tier.start - resources.offset);
+ var x_end = x(tier.end - resources.offset);
+
+ svg.append("rect")
+ .attr("x", x_start)
+ .attr("y", 20)
+ .attr("height", 30)
+ .attr("width", x_end - x_start)
+ .attr("class", "timeline tier")
+ .attr("tier", t)
+ ;
+ });
+
+ function getEntry(element) {
+ var tier = element.getAttribute("tier");
+
+ var entry = resources.getTier(tier);
+ entry.tier = tier;
+
+ return entry;
+ }
+
+ d3.selectAll(".timeline")
+ .on("mouseenter", function () {
+ var entry = getEntry(this);
+
+ d3.select("#tt_tier").html(entry.tier);
+ d3.select("#tt_duration").html(entry.duration || "n/a");
+ d3.select("#tt_cpu_percent").html(entry.cpu_percent_mean || "n/a");
+
+ d3.select("#tooltip").style("display", "");
+ })
+ .on("mouseleave", function () {
+ var tooltip = d3.select("#tooltip");
+ tooltip.style("display", "none");
+ })
+ .on("mousemove", function () {
+ var e = d3.event;
+ x_offset = 10;
+
+ if (e.clientX > window.innerWidth / 2) {
+ x_offset = -150;
+ }
+
+ d3.select("#tooltip")
+ .style("left", (e.clientX + x_offset) + "px")
+ .style("top", (e.clientY + 10) + "px")
+ ;
+ })
+ ;
+}
+
+function initData(data) {
+ var list = d3.select("#list");
+ // Clear the list if it wasn't already empty.
+ list.selectAll("*").remove();
+ list.style("display", "none");
+
+ if (!data) {
+ return;
+ }
+ // If the data contains a list of files, use that list.
+ // Otherwise, we expect it's directly resources info data.
+ if (Object.keys(data).length == 1 && "files" in data) {
+ if (data.files.length > 1) {
+ for (file of data.files) {
+ list.append("option").attr("value", file).text(file);
+ }
+ list.style("display", "inline");
+ }
+ renderKey(data.files[0]);
+ } else {
+ currentResources = new BuildResources(data);
+ updateResourcesGraph();
+ }
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+ var list = d3.select("#list");
+ list.on("change", function() {renderKey(this.value);})
+ d3.json("build_resources.json", function onList(error, response) {
+ initData(response);
+ });
+}, false);
+
+document.addEventListener("drop", function(event) {
+ event.preventDefault();
+ var uris = event.dataTransfer.getData("text/uri-list");
+ if (uris) {
+ var data = {
+ files: uris.split(/\r\n|\r|\n/).filter(uri => !uri.startsWith("#")),
+ };
+ initData(data);
+ }
+}, false);
+
+document.addEventListener("dragover", function(event) {
+ // prevent default to allow drop
+ event.preventDefault();
+}, false);
+
+ </script>
+ <h1>Build Resource Usage Report</h1>
+
+ <div id="tooltip" class="shadow-30" style="display: none;">
+ <div class="grid-list"><ul>
+ <li>
+ <div class="label">Tier</div>
+ <div class="value" id="tt_tier"></div>
+ </li>
+ <li>
+ <div class="label">Duration</div>
+ <div class="value" id="tt_duration"></div>
+ </li>
+ <li>
+ <div class="label">CPU %</div>
+ <div class="value" id="tt_cpu_percent"></div>
+ </li>
+ </ul></div>
+ </div>
+
+ <select id="list" style="display: none"></select>
+ <div class="dashboard-card shadow-10">
+ <h2>CPU</h2>
+ <div id="cpu_graph" class="chart"></div>
+ </div>
+ <div class="dashboard-card shadow-10">
+ <h2>Memory</h2>
+ <div id="mem_graph" class="chart"></div>
+ </div>
+ <div class="dashboard-card shadow-10">
+ <h2>Tiers</h2>
+ <div id="tiers"></div>
+ </div>
+ <div class="dashboard-card shadow-10 align-self-start">
+ <h2>Summary</h2>
+ <div id="summary" class="grid-list" style="padding-top: 20px">
+ <ul>
+ <li>
+ <div class="label">Wall Time (s)</div>
+ <div class="value" id="wall_time"></div>
+ <div class="unit">s</div>
+ </li>
+ <li>
+ <div class="label">Start Date</div>
+ <div class="value" id="start_date"></div>
+ <div class="unit"></div>
+ </li>
+ <li>
+ <div class="label">End Date</div>
+ <div class="value" id="end_date"></div>
+ <div class="unit"></div>
+ </li>
+ <li>
+ <div class="label">CPU %</div>
+ <div class="value" id="cpu_percent"></div>
+ <div class="unit">%</div>
+ </li>
+ <li>
+ <div class="label">Write Bytes</div>
+ <div class="value" id="write_bytes"></div>
+ <div class="unit">B</div>
+ </li>
+ <li>
+ <div class="label">Read Bytes</div>
+ <div class="value" id="read_bytes"></div>
+ <div class="unit">B</div>
+ </li>
+ <li>
+ <div class="label">Write Time</div>
+ <div class="value" id="write_time"></div>
+ <div class="unit"></div>
+ </li>
+ <li>
+ <div class="label">Read Time</div>
+ <div class="value" id="read_time"></div>
+ <div class="unit"></div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </body>
+</html>