diff options
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.html | 694 |
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> |