<!DOCTYPE html>
<html>
<!--
Copyright 2016 the V8 project authors. All rights reserved.  Use of this source
code is governed by a BSD-style license that can be found in the LICENSE file.
-->

<head>
<meta charset="utf-8">
<title>V8 Parse Processor</title>
<style>
  html {
    font-family: monospace;
  }

  .parse {
    background-color: red;
    border: 1px red solid;
  }

  .preparse {
    background-color: orange;
    border: 1px orange solid;
  }

  .resolution {
    background-color: green;
    border: 1px green solid;
  }

  .execution {
    background-color: black;
    border-left: 2px black solid;
    z-index: -1;
  }

  .script {
    margin-top: 1em;
    overflow: visible;
    clear: both;
      border-top: 2px black dotted;
  }
  .script h3 {
    height: 20px;
    margin-bottom: 0.5em;
    white-space: nowrap;
  }

  .script-details {
    float: left;
  }

  .chart {
    float: left;
    margin-right: 2em;
  }

  .funktion-list {
    float: left;
    height: 400px;
  }

  .funktion-list > ul {
    height: 80%;
    overflow-y: scroll;
  }

  .funktion {
  }

  .script-size {
    display: inline-flex;
    background-color: #505050;
    border-radius: 3px;
    padding: 3px;
    margin: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-decoration: none;
    color: white;
    transition: auto ease-in-out 0.8s;
    max-width: 500px;
  }
  .script-size:hover {
    max-width: 100000px !important;
    transition: auto ease-in-out 0.8s;
  }
  .script-size.eval {
    background-color: #ee6300fc;
  }
  .script-size.streaming {
    background-color: #008aff;
  }
  .script-size.deserialized {
    background-color: #1fad00fc;
  }

  .script-details {
    padding-right: 5px;
    margin-right: 4px;
  }
  /* all but the last need a border  */
  .script-details:nth-last-child(n+2) {
    border-right: 1px white solid;
  }

  .script-details.id {
    min-width: 2em;
    text-align: right;
  }
</style>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script type="module">

import { ParseProcessor, kSecondsToMillis, BYTES, PERCENT } from "./parse-processor.mjs";

google.charts.load('current', {packages: ['corechart']});

function $(query) {
  return document.querySelector(query);
}

window.addEventListener('DOMContentLoaded', (event) => {
  $("#uploadInput").focus();
});

document.loadFile = function() {
  let files = $('#uploadInput').files;

  let file = files[0];
  let reader = new FileReader();

  reader.onload = function(evt) {
    const kTimerName = 'parse log file';
    console.time(kTimerName);
    let parseProcessor = new ParseProcessor();
    parseProcessor.processString(this.result);
    console.timeEnd(kTimerName);
    renderParseResults(parseProcessor);
    document.parseProcessor = parseProcessor;
  }
  reader.readAsText(file);
}

function createNode(tag, classNames) {
  let node = document.createElement(tag);
  if (classNames) {
    if (Array.isArray(classNames)) {
      node.classList.add(...classNames);
    } else {
      node.className = classNames;
    }
  }
  return node;
}

function div(...args) {
  return createNode('div', ...args);
}

function h1(string) {
  let node = createNode('h1');
  node.appendChild(text(string));
  return node;
}

function h3(string, ...args) {
  let node = createNode('h3', ...args);
  if (string) node.appendChild(text(string));
  return node;
}

function a(href, string, ...args) {
  let link = createNode('a', ...args);
  if (href.length) link.href = href;
  if (string) link.appendChild(text(string));
  return link;
}

function text(string) {
  return document.createTextNode(string);
}

function delay(t) {
  return new Promise(resolve => setTimeout(resolve, t));
}

function renderParseResults(parseProcessor) {
  let result = $('#result');
  // clear out all existing result pages;
  result.innerHTML = '';
  const start = parseProcessor.firstEventTimestamp;
  const end = parseProcessor.lastEventTimestamp;
  renderScript(result, parseProcessor.totalScript, start, end);
  // Build up the graphs lazily to keep the page responsive.
  parseProcessor.scripts.forEach(
      script => renderScript(result, script, start, end));
  renderScriptSizes(parseProcessor);
  // Install an intersection observer to lazily load the graphs when the script
  // div becomes visible for the first time.
  var io = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.intersectionRatio == 0) return;
      console.assert(!entry.target.querySelector('.graph'));
      let target = entry.target;
      appendGraph(target.script, target, start, end);
      observer.unobserve(entry.target);
    });
  }, {rootMargin: '400px'});
  document.querySelectorAll('.script').forEach(div => io.observe(div));
}

const kTimeFactor = 10;
const kHeight = 20;
const kFunktionTopOffset = 50;

function renderScript(result, script, start, end) {
  // Filter out empty scripts.
  if (script.isEmpty() || script.lastParseEvent == 0) return;

  let scriptDiv = div('script');
  scriptDiv.script = script;

  let scriptTitle = h3();
  let anchor = a("", 'Script #' + script.id);
  anchor.name = "script"+script.id
  scriptTitle.appendChild(anchor);
  scriptDiv.appendChild(scriptTitle);
  if (script.file) scriptTitle.appendChild(a(script.file, script.file));
  let summary = createNode('pre', 'script-details');
  summary.appendChild(text(script.summary));
  scriptDiv.appendChild(summary);
  result.appendChild(scriptDiv);
}

function renderScriptSizes(parseProcessor) {
  let scriptsDiv = $('#scripts');
  parseProcessor.scripts.forEach(
    script => {
      let scriptDiv = a(`#script${script.id}`, '', 'script-size');
      let scriptId = div('script-details');
      scriptId.classList.add('id');
      scriptId.innerText = `id=${script.id}`;
      scriptDiv.appendChild(scriptId);
      let scriptSize = div('script-details');
      scriptSize.innerText = BYTES(script.bytesTotal);
      scriptDiv.appendChild(scriptSize);
      let scriptUrl = div('script-details');
      if (script.isEval) {
        scriptUrl.innerText = "eval";
        scriptDiv.classList.add('eval');
      } else {
        scriptUrl.innerText = script.file.split("/").pop();
      }
      if (script.isStreamingCompiled ) {
        scriptDiv.classList.add('streaming');
      } else if (script.deserializationTimestamp > 0) {
        scriptDiv.classList.add('deserialized');
      }
      scriptDiv.appendChild(scriptUrl);
      scriptDiv.style.maxWidth = `${script.bytesTotal * 0.001}px`;
      scriptsDiv.appendChild(scriptDiv);
    });
}

const kMaxTime = 120 * kSecondsToMillis;
// Resolution of the graphs
const kTimeIncrement = 1;
const kSelectionTimespan = 2;
// TODO(cbruni): support compilation cache hit.
class Series {
  constructor(metricName, description, color, lineStyle, isArea=false) {
    this.metricName = metricName;
    this.description = description;
    this.color = color;
    this.lineStyle = lineStyle;
    this.isArea = isArea;
  }
}
const series = [
    new Series('firstParseEvent', 'Any Parse', '#4D4D4D', undefined, true),
    new Series('execution', '1st Exec', '#fff700',undefined, true),
    new Series('firstCompileEvent', 'Any Compile', '#5DA5DA', undefined, true),

    new Series('compile', 'Eager Compile', '#FAA43A'),
    new Series('lazyCompile', 'Lazy Compile','#FAA43A', 'dash'),

    new Series('parse', 'Parsing', '#F17CB0'),
    new Series('preparse', 'Preparse', '#B2912F'),
    new Series('resolution', 'Preparse with Var. Resolution', '#B276B2'),

    new Series('deserialization', 'Deserialization', '#DECF3F'),

    new Series('baseline', 'Baseline', '#606611', 'dash'),
    new Series('optimize', 'Optimize', '#F15854'),
];
const metricNames = series.map(each => each.metricName);
// Display cumulative values (useuful for bytes).
const kCumulative = true;
// Include durations in the graphs.
const kUseDuration = false;


function appendGraph(script, parentNode, start, end) {
  const timerLabel = 'graph script=' + script.id;
  // TODO(cbruni): add support for network events

  console.time(timerLabel);
  let data = new google.visualization.DataTable();
  data.addColumn('number', 'Duration');
  // The series are interleave bytes processed, time spent and thus have two
  // different vAxes.
  let seriesOptions = [];
  series.forEach(series => {
    // Add the bytes column.
    data.addColumn('number', series.description);
    let options = {targetAxisIndex: 0, color: series.color};
    if (series.isArea) options.type = 'area';
    if (series.lineStyle === 'dash') options.lineDashStyle = [4, 4];
    seriesOptions.push(options)
    // Add the time column.
    if (kUseDuration) {
      data.addColumn('number', series.description + ' Duration');
      seriesOptions.push(
          {targetAxisIndex: 1, color: series.color, lineDashStyle: [3, 2]});
    }
  });

  const maxTime = Math.min(kMaxTime, end);
  console.time('metrics');
  let metricValues =
    script.getAccumulatedTimeMetrics(metricNames , 0, maxTime, kTimeIncrement,
        kCumulative, kUseDuration);
  console.timeEnd('metrics');
  // Make sure that the series added to the graph matches the returned values.
  console.assert(metricValues[0].length == seriesOptions.length + 1);
  data.addRows(metricValues);

  let options = {
    explorer: {
      actions: ['dragToZoom', 'rightClickToReset'],
      maxZoomIn: 0.01
    },
    hAxis: {
      format: '#,###.##s'
    },
    vAxes: {
      0: {title: 'Bytes Touched', format: 'short'},
      1: {title: 'Duration', format: '#,###ms'}
    },
    height: 400,
    width: 1000,
    chartArea: {left: 70, top: 0, right: 160, height: "90%"},
    // The first series should be a area chart (total bytes touched),
    series: seriesOptions,
    // everthing else is a line.
    seriesType: 'line'
  };
  let graphNode = createNode('div', 'chart');
  let listNode = createNode('div', 'funktion-list');
  parentNode.appendChild(graphNode);
  parentNode.appendChild(listNode);
  let chart = new google.visualization.ComboChart(graphNode);
  google.visualization.events.addListener(chart, 'select',
      () => selectGraphPointHandler(chart, data, script, parentNode));
  chart.draw(data, options);
  // Add event listeners
  console.timeEnd(timerLabel);
}

function selectGraphPointHandler(chart, data, script, parentNode) {
  let selection = chart.getSelection();
  if (selection.length <= 0) return;
  // Display a list of funktions with events at the given time.
  let {row, column} = selection[0];
  if (row === null|| column === null) return;
  const kEntrySize = kUseDuration ? 2 : 1;
  let [metric, description] = series[((column-1)/ kEntrySize) | 0];
  let time = data.getValue(row, 0);
  let funktions = script.getFunktionsAtTime(
        time * kSecondsToMillis, kSelectionTimespan, metric);
  let oldList = parentNode.querySelector('.funktion-list');
  parentNode.replaceChild(
      createFunktionList(metric, description, time, funktions), oldList);
}

function createFunktionList(metric, description, time, funktions) {
  let container = createNode('div', 'funktion-list');
  container.appendChild(h3('Changes of "' + description + '" at ' +
        time + 's: ' + funktions.length));
  let listNode = createNode('ul');
  funktions.forEach(funktion => {
    let node = createNode('li', 'funktion');
    node.funktion = funktion;
    node.appendChild(text(funktion.toString(false) + " "));
    let script = funktion.script;
    if (script) {
      node.appendChild(a("#script" + script.id, "in script " + script.id));
    }
    listNode.appendChild(node);
  });
  container.appendChild(listNode);
  return container;
}
</script>
</head>

<body>
  <h1>BEHOLD, THIS IS PARSEROR!</h1>

  <h2>Usage</h2>
  Run your script with <code>--log-function-events</code> and upload <code>v8.log</code> on this page:<br/>
  <code>/path/to/d8 --log-function-events your_script.js</code>

  <h2>Data</h2>
  <form name="fileForm">
    <p>
      <input id="uploadInput" type="file" name="files" onchange="loadFile();" accept=".log"> trace entries: <span id="count">0</span>
    </p>
  </form>


  <h2>Scripts</h2>
  <div id="scripts"></div>

  <h2>Result</h2>
  <div id="result"></div>
</body>

</html>