<!-- 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>
