// Copyright 2017 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.

"use strict";

let codeKinds = [
    "UNKNOWN",
    "CPPPARSE",
    "CPPCOMPBC",
    "CPPCOMP",
    "CPPGC",
    "CPPEXT",
    "CPP",
    "LIB",
    "IC",
    "BC",
    "STUB",
    "BUILTIN",
    "REGEXP",
    "JSOPT",
    "JSUNOPT"
];

function resolveCodeKind(code) {
  if (!code || !code.type) {
    return "UNKNOWN";
  } else if (code.type === "CPP") {
    return "CPP";
  } else if (code.type === "SHARED_LIB") {
    return "LIB";
  } else if (code.type === "CODE") {
    if (code.kind === "LoadIC" ||
        code.kind === "StoreIC" ||
        code.kind === "KeyedStoreIC" ||
        code.kind === "KeyedLoadIC" ||
        code.kind === "LoadGlobalIC" ||
        code.kind === "Handler") {
      return "IC";
    } else if (code.kind === "BytecodeHandler") {
      return "BC";
    } else if (code.kind === "Stub") {
      return "STUB";
    } else if (code.kind === "Builtin") {
      return "BUILTIN";
    } else if (code.kind === "RegExp") {
      return "REGEXP";
    }
    console.log("Unknown CODE: '" + code.kind + "'.");
    return "CODE";
  } else if (code.type === "JS") {
    if (code.kind === "Builtin") {
      return "JSUNOPT";
    } else if (code.kind === "Opt") {
      return "JSOPT";
    } else if (code.kind === "Unopt") {
      return "JSUNOPT";
    }
  }
  console.log("Unknown code type '" + type + "'.");
}

function resolveCodeKindAndVmState(code, vmState) {
  let kind = resolveCodeKind(code);
  if (kind === "CPP") {
    if (vmState === 1) {
      kind = "CPPGC";
    } else if (vmState === 2) {
      kind = "CPPPARSE";
    } else if (vmState === 3) {
      kind = "CPPCOMPBC";
    } else if (vmState === 4) {
      kind = "CPPCOMP";
    } else if (vmState === 6) {
      kind = "CPPEXT";
    }
  }
  return kind;
}

function codeEquals(code1, code2, allowDifferentKinds = false) {
  if (!code1 || !code2) return false;
  if (code1.name !== code2.name || code1.type !== code2.type) return false;

  if (code1.type === 'CODE') {
    if (!allowDifferentKinds && code1.kind !== code2.kind) return false;
  } else if (code1.type === 'JS') {
    if (!allowDifferentKinds && code1.kind !== code2.kind) return false;
    if (code1.func !== code2.func) return false;
  }
  return true;
}

function createNodeFromStackEntry(code, codeId, vmState) {
  let name = code ? code.name : "UNKNOWN";
  let node = createEmptyNode(name);
  node.codeId = codeId;
  node.type = resolveCodeKindAndVmState(code, vmState);
  return node;
}

function childIdFromCode(codeId, code) {
  // For JavaScript function, pretend there is one instance of optimized
  // function and one instance of unoptimized function per SFI.
  // Otherwise, just compute the id from code id.
  let type = resolveCodeKind(code);
  if (type === "JSOPT") {
    return code.func * 4 + 1;
  } else if (type === "JSUNOPT") {
    return code.func * 4 + 2;
  } else {
    return codeId * 4;
  }
}

// We store list of ticks and positions within the ticks stack by
// storing flattened triplets of { tickIndex, depth, count }.
// Triplet { 123, 2, 3 } encodes positions in ticks 123, 124, 125,
// all of them at depth 2. The flattened array is used to encode
// position within the call-tree.

// The following function helps to encode such triplets.
function addFrameToFrameList(paths, pathIndex, depth) {
  // Try to combine with the previous code run.
  if (paths.length > 0 &&
      paths[paths.length - 3] + 1 === pathIndex &&
      paths[paths.length - 2] === depth) {
    paths[paths.length - 1]++;
  } else {
    paths.push(pathIndex, depth, 1);
  }
}

function findNextFrame(file, stack, stackPos, step, filter) {
  let codeId = -1;
  let code = null;
  while (stackPos >= 0 && stackPos < stack.length) {
    codeId = stack[stackPos];
    code = codeId >= 0 ? file.code[codeId] : undefined;

    if (filter) {
      let type = code ? code.type : undefined;
      let kind = code ? code.kind : undefined;
      if (filter(type, kind)) return stackPos;
    }
    stackPos += step;
  }
  return -1;
}

function addOrUpdateChildNode(parent, file, stackIndex, stackPos, ascending) {
  if (stackPos === -1) {
    // We reached the end without finding the next step.
    // If we are doing top-down call tree, update own ticks.
    if (!ascending) {
      parent.ownTicks++;
    }
    return;
  }

  let stack = file.ticks[stackIndex].s;
  console.assert(stackPos >= 0 && stackPos < stack.length);
  let codeId = stack[stackPos];
  let code = codeId >= 0 ? file.code[codeId] : undefined;
  // We found a child node.
  let childId = childIdFromCode(codeId, code);
  let child = parent.children[childId];
  if (!child) {
    let vmState = file.ticks[stackIndex].vm;
    child = createNodeFromStackEntry(code, codeId, vmState);
    child.delayedExpansion = { frameList : [], ascending };
    parent.children[childId] = child;
  }
  child.ticks++;
  addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos);
}

// This expands a tree node (direct children only).
function expandTreeNode(file, node, filter) {
  let { frameList, ascending } = node.delayedExpansion;

  let step = ascending ? 2 : -2;

  for (let i = 0; i < frameList.length; i+= 3) {
    let firstStackIndex = frameList[i];
    let depth = frameList[i + 1];
    let count = frameList[i + 2];
    for (let j = 0; j < count; j++) {
      let stackIndex = firstStackIndex + j;
      let stack = file.ticks[stackIndex].s;

      // Get to the next frame that has not been filtered out.
      let stackPos = findNextFrame(file, stack, depth + step, step, filter);
      addOrUpdateChildNode(node, file, stackIndex, stackPos, ascending);
    }
  }
  node.delayedExpansion = null;
}

function createEmptyNode(name) {
  return {
      name : name,
      codeId: -1,
      type : "CAT",
      children : [],
      ownTicks : 0,
      ticks : 0
  };
}

class RuntimeCallTreeProcessor {
  constructor() {
    this.tree = createEmptyNode("root");
    this.tree.delayedExpansion = { frameList : [], ascending : false };
  }

  addStack(file, tickIndex) {
    this.tree.ticks++;

    let stack = file.ticks[tickIndex].s;
    let i;
    for (i = 0; i < stack.length; i += 2) {
      let codeId = stack[i];
      if (codeId < 0) return;
      let code = file.code[codeId];
      if (code.type !== "CPP" && code.type !== "SHARED_LIB") {
        i -= 2;
        break;
      }
    }
    if (i < 0 || i >= stack.length) return;
    addOrUpdateChildNode(this.tree, file, tickIndex, i, false);
  }
}

class PlainCallTreeProcessor {
  constructor(filter, isBottomUp) {
    this.filter = filter;
    this.tree = createEmptyNode("root");
    this.tree.delayedExpansion = { frameList : [], ascending : isBottomUp };
    this.isBottomUp = isBottomUp;
  }

  addStack(file, tickIndex) {
    let stack = file.ticks[tickIndex].s;
    let step = this.isBottomUp ? 2 : -2;
    let start = this.isBottomUp ? 0 : stack.length - 2;

    let stackPos = findNextFrame(file, stack, start, step, this.filter);
    addOrUpdateChildNode(this.tree, file, tickIndex, stackPos, this.isBottomUp);

    this.tree.ticks++;
  }
}

function buildCategoryTreeAndLookup() {
  let root = createEmptyNode("root");
  let categories = {};
  function addCategory(name, types) {
    let n = createEmptyNode(name);
    for (let i = 0; i < types.length; i++) {
      categories[types[i]] = n;
    }
    root.children.push(n);
  }
  addCategory("JS Optimized", [ "JSOPT" ]);
  addCategory("JS Unoptimized", [ "JSUNOPT", "BC" ]);
  addCategory("IC", [ "IC" ]);
  addCategory("RegExp", [ "REGEXP" ]);
  addCategory("Other generated", [ "STUB", "BUILTIN" ]);
  addCategory("C++", [ "CPP", "LIB" ]);
  addCategory("C++/GC", [ "CPPGC" ]);
  addCategory("C++/Parser", [ "CPPPARSE" ]);
  addCategory("C++/Bytecode compiler", [ "CPPCOMPBC" ]);
  addCategory("C++/Compiler", [ "CPPCOMP" ]);
  addCategory("C++/External", [ "CPPEXT" ]);
  addCategory("Unknown", [ "UNKNOWN" ]);

  return { categories, root };
}

class CategorizedCallTreeProcessor {
  constructor(filter, isBottomUp) {
    this.filter = filter;
    let { categories, root } = buildCategoryTreeAndLookup();

    this.tree = root;
    this.categories = categories;
    this.isBottomUp = isBottomUp;
  }

  addStack(file, tickIndex) {
    let stack = file.ticks[tickIndex].s;
    let vmState = file.ticks[tickIndex].vm;
    if (stack.length === 0) return;
    let codeId = stack[0];
    let code = codeId >= 0 ? file.code[codeId] : undefined;
    let kind = resolveCodeKindAndVmState(code, vmState);
    let node = this.categories[kind];

    this.tree.ticks++;
    node.ticks++;

    let step = this.isBottomUp ? 2 : -2;
    let start = this.isBottomUp ? 0 : stack.length - 2;

    let stackPos = findNextFrame(file, stack, start, step, this.filter);
    addOrUpdateChildNode(node, file, tickIndex, stackPos, this.isBottomUp);
  }
}

class FunctionListTree {
  constructor(filter, withCategories) {
    if (withCategories) {
      let { categories, root } = buildCategoryTreeAndLookup();
      this.tree = root;
      this.categories = categories;
    } else {
      this.tree = createEmptyNode("root");
      this.categories = null;
    }

    this.codeVisited = [];
    this.filter = filter;
  }

  addStack(file, tickIndex) {
    let stack = file.ticks[tickIndex].s;
    let vmState = file.ticks[tickIndex].vm;

    this.tree.ticks++;
    let child = null;
    let tree = null;
    for (let i = stack.length - 2; i >= 0; i -= 2) {
      let codeId = stack[i];
      if (codeId < 0 || this.codeVisited[codeId]) continue;

      let code = file.code[codeId];
      if (this.filter) {
        let type = code ? code.type : undefined;
        let kind = code ? code.kind : undefined;
        if (!this.filter(type, kind)) continue;
      }
      let childId = childIdFromCode(codeId, code);
      if (this.categories) {
        let kind = resolveCodeKindAndVmState(code, vmState);
        tree = this.categories[kind];
      } else {
        tree = this.tree;
      }
      child = tree.children[childId];
      if (!child) {
        child = createNodeFromStackEntry(code, codeId, vmState);
        child.children[0] = createEmptyNode("Top-down tree");
        child.children[0].delayedExpansion =
          { frameList : [], ascending : false };
        child.children[1] = createEmptyNode("Bottom-up tree");
        child.children[1].delayedExpansion =
          { frameList : [], ascending : true };
        tree.children[childId] = child;
      }
      child.ticks++;
      child.children[0].ticks++;
      addFrameToFrameList(
          child.children[0].delayedExpansion.frameList, tickIndex, i);
      child.children[1].ticks++;
      addFrameToFrameList(
          child.children[1].delayedExpansion.frameList, tickIndex, i);
      this.codeVisited[codeId] = true;
    }
    if (child) {
      child.ownTicks++;
      console.assert(tree !== null);
      tree.ticks++;
      console.assert(tree.type === "CAT");
    }

    for (let i = 0; i < stack.length; i += 2) {
      let codeId = stack[i];
      if (codeId >= 0) this.codeVisited[codeId] = false;
    }
  }
}


class CategorySampler {
  constructor(file, bucketCount) {
    this.bucketCount = bucketCount;

    this.firstTime = file.ticks[0].tm;
    let lastTime = file.ticks[file.ticks.length - 1].tm;
    this.step = (lastTime - this.firstTime) / bucketCount;

    this.buckets = [];
    let bucket = {};
    for (let i = 0; i < codeKinds.length; i++) {
      bucket[codeKinds[i]] = 0;
    }
    for (let i = 0; i < bucketCount; i++) {
      this.buckets.push(Object.assign({ total : 0 }, bucket));
    }
  }

  addStack(file, tickIndex) {
    let { tm : timestamp, vm : vmState, s : stack } = file.ticks[tickIndex];

    let i = Math.floor((timestamp - this.firstTime) / this.step);
    if (i === this.buckets.length) i--;
    console.assert(i >= 0 && i < this.buckets.length);

    let bucket = this.buckets[i];
    bucket.total++;

    let codeId = (stack.length > 0) ? stack[0] : -1;
    let code = codeId >= 0 ? file.code[codeId] : undefined;
    let kind = resolveCodeKindAndVmState(code, vmState);
    bucket[kind]++;
  }
}

class FunctionTimelineProcessor {
  constructor(functionCodeId, filter) {
    this.functionCodeId = functionCodeId;
    this.filter = filter;
    this.blocks = [];
    this.currentBlock = null;
  }

  addStack(file, tickIndex) {
    if (!this.functionCodeId) return;

    let { tm : timestamp, vm : vmState, s : stack } = file.ticks[tickIndex];
    let functionCode = file.code[this.functionCodeId];

    // Find if the function is on the stack, and its position on the stack,
    // ignoring any filtered entries.
    let stackCode = undefined;
    let functionPosInStack = -1;
    let filteredI = 0;
    for (let i = 0; i < stack.length - 1; i += 2) {
      let codeId = stack[i];
      let code = codeId >= 0 ? file.code[codeId] : undefined;
      let type = code ? code.type : undefined;
      let kind = code ? code.kind : undefined;
      if (!this.filter(type, kind)) continue;

      // Match other instances of the same function (e.g. unoptimised, various
      // different optimised versions).
      if (codeEquals(code, functionCode, true)) {
        functionPosInStack = filteredI;
        stackCode = code;
        break;
      }
      filteredI++;
    }

    if (functionPosInStack >= 0) {
      let stackKind = resolveCodeKindAndVmState(stackCode, vmState);

      let codeIsTopOfStack = (functionPosInStack === 0);

      if (this.currentBlock !== null) {
        this.currentBlock.end = timestamp;

        if (codeIsTopOfStack === this.currentBlock.topOfStack
          && stackKind === this.currentBlock.kind) {
          // If we haven't changed the stack top or the function kind, then
          // we're happy just extending the current block and not starting
          // a new one.
          return;
        }
      }

      // Start a new block at the current timestamp.
      this.currentBlock = {
        start: timestamp,
        end: timestamp,
        code: stackCode,
        kind: stackKind,
        topOfStack: codeIsTopOfStack
      };
      this.blocks.push(this.currentBlock);
    } else {
      this.currentBlock = null;
    }
  }
}

// Generates a tree out of a ticks sequence.
// {file} is the JSON files with the ticks and code objects.
// {startTime}, {endTime} is the interval.
// {tree} is the processor of stacks.
function generateTree(
    file, startTime, endTime, tree) {
  let ticks = file.ticks;
  let i = 0;
  while (i < ticks.length && ticks[i].tm < startTime) {
    i++;
  }

  let tickCount = 0;
  while (i < ticks.length && ticks[i].tm < endTime) {
    tree.addStack(file, i);
    i++;
    tickCount++;
  }

  return tickCount;
}

function computeOptimizationStats(file,
    timeStart = -Infinity, timeEnd = Infinity) {
  function newCollection() {
    return { count : 0, functions : [], functionTable : [] };
  }
  function addToCollection(collection, code) {
    collection.count++;
    let funcData = collection.functionTable[code.func];
    if (!funcData) {
      funcData = { f : file.functions[code.func], instances : [] };
      collection.functionTable[code.func] = funcData;
      collection.functions.push(funcData);
    }
    funcData.instances.push(code);
  }

  let functionCount = 0;
  let optimizedFunctionCount = 0;
  let deoptimizedFunctionCount = 0;
  let optimizations = newCollection();
  let eagerDeoptimizations = newCollection();
  let softDeoptimizations = newCollection();
  let lazyDeoptimizations = newCollection();

  for (let i = 0; i < file.functions.length; i++) {
    let f = file.functions[i];

    // Skip special SFIs that do not correspond to JS functions.
    if (f.codes.length === 0) continue;
    if (file.code[f.codes[0]].type !== "JS") continue;

    functionCount++;
    let optimized = false;
    let deoptimized = false;

    for (let j = 0; j < f.codes.length; j++) {
      let code = file.code[f.codes[j]];
      console.assert(code.type === "JS");
      if (code.kind === "Opt") {
        optimized = true;
        if (code.tm >= timeStart && code.tm <= timeEnd) {
          addToCollection(optimizations, code);
        }
      }
      if (code.deopt) {
        deoptimized = true;
        if (code.deopt.tm >= timeStart && code.deopt.tm <= timeEnd) {
          switch (code.deopt.bailoutType) {
            case "lazy":
              addToCollection(lazyDeoptimizations, code);
              break;
            case "eager":
              addToCollection(eagerDeoptimizations, code);
              break;
            case "soft":
              addToCollection(softDeoptimizations, code);
              break;
          }
        }
      }
    }
    if (optimized) {
      optimizedFunctionCount++;
    }
    if (deoptimized) {
      deoptimizedFunctionCount++;
    }
  }

  function sortCollection(collection) {
    collection.functions.sort(
        (a, b) => a.instances.length - b.instances.length);
  }

  sortCollection(eagerDeoptimizations);
  sortCollection(lazyDeoptimizations);
  sortCollection(softDeoptimizations);
  sortCollection(optimizations);

  return {
    functionCount,
    optimizedFunctionCount,
    deoptimizedFunctionCount,
    optimizations,
    eagerDeoptimizations,
    lazyDeoptimizations,
    softDeoptimizations,
  };
}

function normalizeLeadingWhitespace(lines) {
  let regex = /^\s*/;
  let minimumLeadingWhitespaceChars = Infinity;
  for (let line of lines) {
    minimumLeadingWhitespaceChars =
        Math.min(minimumLeadingWhitespaceChars, regex.exec(line)[0].length);
  }
  for (let i = 0; i < lines.length; i++) {
    lines[i] = lines[i].substring(minimumLeadingWhitespaceChars);
  }
}
