/* -*- Mode: js2; indent-tabs-mode: nil; js2-basic-offset: 2; -*- */

/* 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/. */

"use strict";

importScripts("ProgressReporter.js");

var gProfiles = [];

var partialTaskData = {};

var gNextProfileID = 0;

var gLogLines = [];

var gDebugLog = false;
var gDebugTrace = false;
// Use for verbose tracing, otherwise use log
function PROFILDERTRACE(msg) {
  if (gDebugTrace)
    PROFILERLOG(msg);
}
function PROFILERLOG(msg) {
  if (gDebugLog) {
    msg = "Cleo: " + msg;
    //if (window.dump)
    //  window.dump(msg + "\n");
  }
}
function PROFILERERROR(msg) {
  msg = "Cleo: " + msg;
  //if (window.dump)
  //  window.dump(msg + "\n");
}

// http://stackoverflow.com/a/2548133
function endsWith(str, suffix) {
      return str.indexOf(suffix, this.length - suffix.length) !== -1;
};

// https://bugzilla.mozilla.org/show_bug.cgi?id=728780
if (!String.prototype.startsWith) {
  String.prototype.startsWith =
    function(s) { return this.lastIndexOf(s, 0) === 0; }
}

// functions for which lr is unconditionally valid.  These are
// largely going to be atomics and other similar functions
// that don't touch lr.  This is currently populated with
// some functions from bionic, largely via manual inspection
// of the assembly in e.g.
// http://androidxref.com/source/xref/bionic/libc/arch-arm/syscalls/
var sARMFunctionsWithValidLR = [
  "__atomic_dec",
  "__atomic_inc",
  "__atomic_cmpxchg",
  "__atomic_swap",
  "__atomic_dec",
  "__atomic_inc",
  "__atomic_cmpxchg",
  "__atomic_swap",
  "__futex_syscall3",
  "__futex_wait",
  "__futex_wake",
  "__futex_syscall3",
  "__futex_wait",
  "__futex_wake",
  "__futex_syscall4",
  "__ioctl",
  "__brk",
  "__wait4",
  "epoll_wait",
  "fsync",
  "futex",
  "nanosleep",
  "pause",
  "sched_yield",
  "syscall"
];

function log() {
  var z = [];
  for (var i = 0; i < arguments.length; ++i)
    z.push(arguments[i]);
  gLogLines.push(z.join(" "));
}

self.onmessage = function (msg) {
  try {
    var requestID = msg.data.requestID;
    var task = msg.data.task;
    var taskData = msg.data.taskData;
    if (!taskData &&
        (["chunkedStart", "chunkedChunk", "chunkedEnd"].indexOf(task) == -1)) {
      taskData = partialTaskData[requestID];
      delete partialTaskData[requestID];
    }
    PROFILERLOG("Start task: " + task);

    gLogLines = [];

    switch (task) {
      case "initWorker":
        gDebugLog = taskData.debugLog;
        gDebugTrace = taskData.debugTrace;
        PROFILERLOG("Init logging in parserWorker");
        return;
      case "chunkedStart":
        partialTaskData[requestID] = null;
        break;
      case "chunkedChunk":
        if (partialTaskData[requestID] === null)
          partialTaskData[requestID] = msg.data.chunk;
        else
          partialTaskData[requestID] = partialTaskData[requestID].concat(msg.data.chunk);
        break;
      case "chunkedEnd":
        break;
      case "parseRawProfile":
        parseRawProfile(requestID, msg.data.params, taskData);
        break;
      case "updateFilters":
        updateFilters(requestID, taskData.profileID, taskData.filters);
        break;
      case "updateViewOptions":
        updateViewOptions(requestID, taskData.profileID, taskData.options);
        break;
      case "getSerializedProfile":
        getSerializedProfile(requestID, taskData.profileID, taskData.complete);
        break;
      case "calculateHistogramData":
        calculateHistogramData(requestID, taskData.profileID);
        break;
      case "calculateDiagnosticItems":
        calculateDiagnosticItems(requestID, taskData.profileID, taskData.meta);
        break;
      default:
        sendError(requestID, "Unknown task " + task);
        break;
    }
    PROFILERLOG("Complete task: " + task);
  } catch (e) {
    PROFILERERROR("Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")");
    sendError(requestID, "Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")");
  }
}

function sendError(requestID, error) {
  // support sendError(msg)
  if (error == null) {
    error = requestID;
    requestID = null;
  }

  self.postMessage({
    requestID: requestID,
    type: "error",
    error: error,
    log: gLogLines
  });
}

function sendProgress(requestID, progress) {
  self.postMessage({
    requestID: requestID,
    type: "progress",
    progress: progress
  });
}

function sendFinished(requestID, result) {
  self.postMessage({
    requestID: requestID,
    type: "finished",
    result: result,
    log: gLogLines
  });
}

function bucketsBySplittingArray(array, maxCostPerBucket, costOfElementCallback) {
  var buckets = [];
  var currentBucket = [];
  var currentBucketCost = 0;
  for (var i = 0; i < array.length; i++) {
    var element = array[i];
    var costOfCurrentElement = costOfElementCallback ? costOfElementCallback(element) : 1;
    if (currentBucketCost + costOfCurrentElement > maxCostPerBucket) {
      buckets.push(currentBucket);
      currentBucket = [];
      currentBucketCost = 0;
    }
    currentBucket.push(element);
    currentBucketCost += costOfCurrentElement;
  }
  buckets.push(currentBucket);
  return buckets;
}

function sendFinishedInChunks(requestID, result, maxChunkCost, costOfElementCallback) {
  if (result.length === undefined || result.slice === undefined)
    throw new Error("Can't slice result into chunks");
  self.postMessage({
    requestID: requestID,
    type: "finishedStart"
  });
  var chunks = bucketsBySplittingArray(result, maxChunkCost, costOfElementCallback);
  for (var i = 0; i < chunks.length; i++) {
    self.postMessage({
      requestID: requestID,
      type: "finishedChunk",
      chunk: chunks[i]
    });
  }
  self.postMessage({
    requestID: requestID,
    type: "finishedEnd",
    log: gLogLines
  });
}

function makeSample(frames, extraInfo) {
  return {
    frames: frames,
    extraInfo: extraInfo
  };
}

function cloneSample(sample) {
  return makeSample(sample.frames.slice(0), sample.extraInfo);
}

function parseRawProfile(requestID, params, rawProfile) {
  var progressReporter = new ProgressReporter();
  progressReporter.addListener(function (r) {
    sendProgress(requestID, r.getProgress());
  });
  progressReporter.begin("Parsing...");

  var symbolicationTable = {};
  var symbols = [];
  var symbolIndices = {};
  var resources = {};
  var functions = [];
  var functionIndices = {};
  var samples = [];
  var meta = {};
  var armIncludePCIndex = {};

  if (rawProfile == null) {
    throw "rawProfile is null";
  }

  if (typeof rawProfile == "string" && rawProfile[0] == "{") {
    // rawProfile is a JSON string.
    rawProfile = JSON.parse(rawProfile);
    if (rawProfile === null) {
      throw "rawProfile couldn't not successfully be parsed using JSON.parse. Make sure that the profile is a valid JSON encoding.";
    }
  }

  if (rawProfile.profileJSON && !rawProfile.profileJSON.meta && rawProfile.meta) {
    rawProfile.profileJSON.meta = rawProfile.meta;
  }

  if (typeof rawProfile == "object") {
    switch (rawProfile.format || null) {
      case "profileStringWithSymbolicationTable,1":
        symbolicationTable = rawProfile.symbolicationTable;
        parseProfileString(rawProfile.profileString);
        break;
      case "profileJSONWithSymbolicationTable,1":
        symbolicationTable = rawProfile.symbolicationTable;
        parseProfileJSON(rawProfile.profileJSON);
        break;
      default:
        parseProfileJSON(rawProfile);
    }
  } else {
    parseProfileString(rawProfile);
  }

  if (params.profileId) {
    meta.profileId = params.profileId;
  }

  function cleanFunctionName(functionName) {
    var ignoredPrefix = "non-virtual thunk to ";
    if (functionName.startsWith(ignoredPrefix))
      return functionName.substr(ignoredPrefix.length);
    return functionName;
  }

  function resourceNameForAddon(addon) {
    if (!addon)
      return "";

    var iconHTML = "";
    if (addon.iconURL)
      iconHTML = "<img src=\"" + addon.iconURL + "\" style='width:12px; height:12px;'> "
    return iconHTML + " " + (/@jetpack$/.exec(addon.id) ? "Jetpack: " : "") + addon.name;
  }

  function addonWithID(addonID) {
    return firstMatch(meta.addons, function addonHasID(addon) {
      return addon.id.toLowerCase() == addonID.toLowerCase();
    })
  }

  function resourceNameForAddonWithID(addonID) {
    return resourceNameForAddon(addonWithID(addonID));
  }

  function findAddonForChromeURIHost(host) {
    return firstMatch(meta.addons, function addonUsesChromeURIHost(addon) {
      return addon.chromeURIHosts && addon.chromeURIHosts.indexOf(host) != -1;
    });
  }

  function ensureResource(name, resourceDescription) {
    if (!(name in resources)) {
      resources[name] = resourceDescription;
    }
    return name;
  }

  function resourceNameFromLibrary(library) {
    return ensureResource("lib_" + library, {
      type: "library",
      name: library
    });
  }

  function getAddonForScriptURI(url, host) {
    if (!meta || !meta.addons)
      return null;

    if (url.startsWith("resource:") && endsWith(host, "-at-jetpack")) {
      // Assume this is a jetpack url
      var jetpackID = host.substring(0, host.length - 11) + "@jetpack";
      return addonWithID(jetpackID);
    }

    if (url.startsWith("file:///") && url.indexOf("/extensions/") != -1) {
      var unpackedAddonNameMatch = /\/extensions\/(.*?)\//.exec(url);
      if (unpackedAddonNameMatch)
        return addonWithID(decodeURIComponent(unpackedAddonNameMatch[1]));
      return null;
    }

    if (url.startsWith("jar:file:///") && url.indexOf("/extensions/") != -1) {
      var packedAddonNameMatch = /\/extensions\/(.*?).xpi/.exec(url);
      if (packedAddonNameMatch)
        return addonWithID(decodeURIComponent(packedAddonNameMatch[1]));
      return null;
    }

    if (url.startsWith("chrome://")) {
      var chromeURIMatch = /chrome\:\/\/(.*?)\//.exec(url);
      if (chromeURIMatch)
        return findAddonForChromeURIHost(chromeURIMatch[1]);
      return null;
    }

    return null;
  }

  function resourceNameFromURI(url) {
    if (!url)
      return ensureResource("unknown", {type: "unknown", name: "<unknown>"});

    var match = /^(.*):\/\/(.*?)\//.exec(url);

    if (!match) {
      // Can this happen? If so, we should change the regular expression above.
      return ensureResource("url_" + url, {type: "url", name: url});
    }

    var urlRoot = match[0];
    var protocol = match[1];
    var host = match[2];

    var addon = getAddonForScriptURI(url, host);
    if (addon) {
      return ensureResource("addon_" + addon.id, {
        type: "addon",
        name: addon.name,
        addonID: addon.id,
        icon: addon.iconURL
      });
    }

    if (protocol.startsWith("http")) {
      return ensureResource("webhost_" + host, {
        type: "webhost",
        name: host,
        icon: urlRoot + "favicon.ico"
      });
    }

    if (protocol.startsWith("file")) {
      return ensureResource("file_" + host, {
        type: "file",
        name: host
      });
    }

    return ensureResource("otherhost_" + host, {
      type: "otherhost",
      name: host
    });
  }

  function parseScriptFile(url) {
     var match = /([^\/]*)$/.exec(url);
     if (match && match[1])
       return match[1];

     return url;
  }

  // JS File information sometimes comes with multiple URIs which are chained
  // with " -> ". We only want the last URI in this list.
  function getRealScriptURI(url) {
    if (url) {
      var urls = url.split(" -> ");
      return urls[urls.length - 1];
    }
    return url;
  }

  function getFunctionInfo(fullName) {

    function getCPPFunctionInfo(fullName) {
      var match =
        /^(.*) \(in ([^\)]*)\) (\+ [0-9]+)$/.exec(fullName) ||
        /^(.*) \(in ([^\)]*)\) (\(.*:.*\))$/.exec(fullName) ||
        /^(.*) \(in ([^\)]*)\)$/.exec(fullName);

      if (!match)
        return null;

      return {
        functionName: cleanFunctionName(match[1]),
        libraryName: resourceNameFromLibrary(match[2]),
        lineInformation: match[3] || "",
        isRoot: false,
        isJSFrame: false
      };
    }

    function getJSFunctionInfo(fullName) {
      var jsMatch =
        /^(.*) \((.*):([0-9]+)\)$/.exec(fullName) ||
        /^()(.*):([0-9]+)$/.exec(fullName);

      if (!jsMatch)
        return null;

      var functionName = jsMatch[1] || "<Anonymous>";
      var scriptURI = getRealScriptURI(jsMatch[2]);
      var lineNumber = jsMatch[3];
      var scriptFile = parseScriptFile(scriptURI);
      var resourceName = resourceNameFromURI(scriptURI);

      return {
        functionName: functionName + "() @ " + scriptFile + ":" + lineNumber,
        libraryName: resourceName,
        lineInformation: "",
        isRoot: false,
        isJSFrame: true,
        scriptLocation: {
          scriptURI: scriptURI,
          lineInformation: lineNumber
        }
      };
    }

    function getFallbackFunctionInfo(fullName) {
      return {
        functionName: cleanFunctionName(fullName),
        libraryName: "",
        lineInformation: "",
        isRoot: fullName == "(root)",
        isJSFrame: false
      };
    }

    return getCPPFunctionInfo(fullName) ||
           getJSFunctionInfo(fullName) ||
           getFallbackFunctionInfo(fullName);
  }

  function indexForFunction(symbol, info) {
    var resolve = info.functionName + "__" + info.libraryName;
    if (resolve in functionIndices)
      return functionIndices[resolve];
    var newIndex = functions.length;
    info.symbol = symbol;
    functions[newIndex] = info;
    functionIndices[resolve] = newIndex;
    return newIndex;
  }

  function parseSymbol(symbol) {
    var info = getFunctionInfo(symbol);
    //dump("Parse symbol: " + symbol + "\n");
    return {
      symbolName: symbol,
      functionName: info.functionName,
      functionIndex: indexForFunction(symbol, info),
      lineInformation: info.lineInformation,
      isRoot: info.isRoot,
      isJSFrame: info.isJSFrame,
      scriptLocation: info.scriptLocation
    };
  }

  function translatedSymbol(symbol) {
    return symbolicationTable[symbol] || symbol;
  }

  function indexForSymbol(symbol) {
    if (symbol in symbolIndices)
      return symbolIndices[symbol];
    var newIndex = symbols.length;
    symbols[newIndex] = parseSymbol(translatedSymbol(symbol));
    symbolIndices[symbol] = newIndex;
    return newIndex;
  }

  function clearRegExpLastMatch() {
    /./.exec(" ");
  }

  function shouldIncludeARMLRForPC(pcIndex) {
    if (pcIndex in armIncludePCIndex)
      return armIncludePCIndex[pcIndex];

    var pcName = symbols[pcIndex].functionName;
    var include = sARMFunctionsWithValidLR.indexOf(pcName) != -1;
    armIncludePCIndex[pcIndex] = include;
    return include;
  }

  function parseProfileString(data) {
    var extraInfo = {};
    var lines = data.split("\n");
    var sample = null;
    for (var i = 0; i < lines.length; ++i) {
      var line = lines[i];
      if (line.length < 2 || line[1] != '-') {
        // invalid line, ignore it
        continue;
      }
      var info = line.substring(2);
      switch (line[0]) {
      //case 'l':
      //  // leaf name
      //  if ("leafName" in extraInfo) {
      //    extraInfo.leafName += ":" + info;
      //  } else {
      //    extraInfo.leafName = info;
      //  }
      //  break;
      case 'm':
        // marker
        if (!("marker" in extraInfo)) {
          extraInfo.marker = [];
        }
        extraInfo.marker.push(info);
        break;
      case 's':
        // sample
        var sampleName = info;
        sample = makeSample([indexForSymbol(sampleName)], extraInfo);
        samples.push(sample);
        extraInfo = {}; // reset the extra info for future rounds
        break;
      case 'c':
      case 'l':
        // continue sample
        if (sample) { // ignore the case where we see a 'c' before an 's'
          sample.frames.push(indexForSymbol(info));
        }
        break;
      case 'L':
        // continue sample; this is an ARM LR record.  Stick it before the
        // PC if it's one of the functions where we know LR is good.
        if (sample && sample.frames.length > 1) {
          var pcIndex = sample.frames[sample.frames.length - 1];
          if (shouldIncludeARMLRForPC(pcIndex)) {
            sample.frames.splice(-1, 0, indexForSymbol(info));
          }
        }
        break;
      case 't':
        // time
        if (sample) {
          sample.extraInfo["time"] = parseFloat(info);
        }
        break;
      case 'r':
        // responsiveness
        if (sample) {
          sample.extraInfo["responsiveness"] = parseFloat(info);
        }
        break;
      }
      progressReporter.setProgress((i + 1) / lines.length);
    }
  }

  function parseProfileJSON(profile) {
    // Thread 0 will always be the main thread of interest
    // TODO support all the thread in the profile
    var profileSamples = null;
    meta = profile.meta || {};
    if (params.appendVideoCapture) {
      meta.videoCapture = {
        src: params.appendVideoCapture,
      };
    }
    // Support older format that aren't thread aware
    if (profile.threads != null) {
      profileSamples = profile.threads[0].samples;
    } else {
      profileSamples = profile;
    }
    var rootSymbol = null;
    var insertCommonRoot = false;
    var frameStart = {};
    meta.frameStart = frameStart;
    for (var j = 0; j < profileSamples.length; j++) {
      var sample = profileSamples[j];
      var indicedFrames = [];
      if (!sample) {
        // This sample was filtered before saving
        samples.push(null);
        progressReporter.setProgress((j + 1) / profileSamples.length);
        continue;
      }
      for (var k = 0; sample.frames && k < sample.frames.length; k++) {
        var frame = sample.frames[k];
        var pcIndex;
        if (frame.location !== undefined) {
          pcIndex = indexForSymbol(frame.location);
        } else {
          pcIndex = indexForSymbol(frame);
        }

        if (frame.lr !== undefined && shouldIncludeARMLRForPC(pcIndex)) {
          indicedFrames.push(indexForSymbol(frame.lr));
        }

        indicedFrames.push(pcIndex);
      }
      if (indicedFrames.length >= 1) {
        if (rootSymbol && rootSymbol != indicedFrames[0]) {
          insertCommonRoot = true;
        }
        rootSymbol = rootSymbol || indicedFrames[0];
      }
      if (sample.extraInfo == null) {
        sample.extraInfo = {};
      }
      if (sample.responsiveness) {
        sample.extraInfo["responsiveness"] = sample.responsiveness;
      }
      if (sample.marker) {
        sample.extraInfo["marker"] = sample.marker;
      }
      if (sample.time) {
        sample.extraInfo["time"] = sample.time;
      }
      if (sample.frameNumber) {
        sample.extraInfo["frameNumber"] = sample.frameNumber;
        //dump("Got frame number: " + sample.frameNumber + "\n");
        frameStart[sample.frameNumber] = samples.length;
      }
      samples.push(makeSample(indicedFrames, sample.extraInfo));
      progressReporter.setProgress((j + 1) / profileSamples.length);
    }
    if (insertCommonRoot) {
      var rootIndex = indexForSymbol("(root)");
      for (var i = 0; i < samples.length; i++) {
        var sample = samples[i];
        if (!sample) continue;
        // If length == 0 then the sample was filtered when saving the profile
        if (sample.frames.length >= 1 && sample.frames[0] != rootIndex)
          sample.frames.unshift(rootIndex)
      }
    }
  }

  progressReporter.finish();
  // Don't increment the profile ID now because (1) it's buggy
  // and (2) for now there's no point in storing each profile
  // here if we're storing them in the local storage.
  //var profileID = gNextProfileID++;
  var profileID = gNextProfileID;
  gProfiles[profileID] = JSON.parse(JSON.stringify({
    meta: meta,
    symbols: symbols,
    functions: functions,
    resources: resources,
    allSamples: samples
  }));
  clearRegExpLastMatch();
  sendFinished(requestID, {
    meta: meta,
    numSamples: samples.length,
    profileID: profileID,
    symbols: symbols,
    functions: functions,
    resources: resources
  });
}

function getSerializedProfile(requestID, profileID, complete) {
  var profile = gProfiles[profileID];
  var symbolicationTable = {};
  if (complete || !profile.filterSettings.mergeFunctions) {
    for (var symbolIndex in profile.symbols) {
      symbolicationTable[symbolIndex] = profile.symbols[symbolIndex].symbolName;
    }
  } else {
    for (var functionIndex in profile.functions) {
      var f = profile.functions[functionIndex];
      symbolicationTable[functionIndex] = f.symbol;
    }
  }
  var serializedProfile = JSON.stringify({
    format: "profileJSONWithSymbolicationTable,1",
    meta: profile.meta,
    profileJSON: complete ? profile.allSamples : profile.filteredSamples,
    symbolicationTable: symbolicationTable
  });
  sendFinished(requestID, serializedProfile);
}

function TreeNode(name, parent, startCount) {
  this.name = name;
  this.children = [];
  this.counter = startCount;
  this.parent = parent;
}
TreeNode.prototype.getDepth = function TreeNode__getDepth() {
  if (this.parent)
    return this.parent.getDepth() + 1;
  return 0;
};
TreeNode.prototype.findChild = function TreeNode_findChild(name) {
  for (var i = 0; i < this.children.length; i++) {
    var child = this.children[i];
    if (child.name == name)
      return child;
  }
  return null;
}
// path is an array of strings which is matched to our nodes' names.
// Try to walk path in our own tree and return the last matching node. The
// length of the match can be calculated by the caller by comparing the
// returned node's depth with the depth of the path's start node.
TreeNode.prototype.followPath = function TreeNode_followPath(path) {
  if (path.length == 0)
    return this;

  var matchingChild = this.findChild(path[0]);
  if (!matchingChild)
    return this;

  return matchingChild.followPath(path.slice(1));
};
TreeNode.prototype.incrementCountersInParentChain = function TreeNode_incrementCountersInParentChain() {
  this.counter++;
  if (this.parent)
    this.parent.incrementCountersInParentChain();
};

function convertToCallTree(samples, isReverse) {
  function areSamplesMultiroot(samples) {
    var previousRoot;
    for (var i = 0; i < samples.length; ++i) {
      if (!previousRoot) {
        previousRoot = samples[i].frames[0];
        continue;
      }
      if (previousRoot != samples[i].frames[0]) {
        return true;
      }
    }
    return false;
  }
  samples = samples.filter(function noNullSamples(sample) {
    return sample != null;
  });
  if (samples.length == 0)
    return new TreeNode("(empty)", null, 0);
  var firstRoot = null;
  for (var i = 0; i < samples.length; ++i) {
    firstRoot = samples[i].frames[0];
    break;
  }
  if (firstRoot == null) {
    return new TreeNode("(all filtered)", null, 0);
  }
  var multiRoot = areSamplesMultiroot(samples);
  var treeRoot = new TreeNode((isReverse || multiRoot) ? "(total)" : firstRoot, null, 0);
  for (var i = 0; i < samples.length; ++i) {
    var sample = samples[i];
    var callstack = sample.frames.slice(0);
    callstack.shift();
    if (isReverse)
      callstack.reverse();
    var deepestExistingNode = treeRoot.followPath(callstack);
    var remainingCallstack = callstack.slice(deepestExistingNode.getDepth());
    deepestExistingNode.incrementCountersInParentChain();
    var node = deepestExistingNode;
    for (var j = 0; j < remainingCallstack.length; ++j) {
      var frame = remainingCallstack[j];
      var child = new TreeNode(frame, node, 1);
      node.children.push(child);
      node = child;
    }
  }
  return treeRoot;
}

function filterByJank(samples, filterThreshold) {
  return samples.map(function nullNonJank(sample) {
    if (!sample ||
        !("responsiveness" in sample.extraInfo) ||
        sample.extraInfo["responsiveness"] < filterThreshold)
      return null;
    return sample;
  });
}

function filterBySymbol(samples, symbolOrFunctionIndex) {
  return samples.map(function filterSample(origSample) {
    if (!origSample)
      return null;
    var sample = cloneSample(origSample);
    for (var i = 0; i < sample.frames.length; i++) {
      if (symbolOrFunctionIndex == sample.frames[i]) {
        sample.frames = sample.frames.slice(i);
        return sample;
      }
    }
    return null; // no frame matched; filter out complete sample
  });
}

function filterByCallstackPrefix(samples, symbols, functions, callstack, appliesToJS, useFunctions) {
  var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) {
      return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot);
    } : function isJSSymbolOrRoot(symbolIndex) {
      return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot);
    };
  return samples.map(function filterSample(sample) {
    if (!sample)
      return null;
    if (sample.frames.length < callstack.length)
      return null;
    for (var i = 0, j = 0; j < callstack.length; i++) {
      if (i >= sample.frames.length)
        return null;
      if (appliesToJS && !isJSFrameOrRoot(sample.frames[i]))
        continue;
      if (sample.frames[i] != callstack[j])
        return null;
      j++;
    }
    return makeSample(sample.frames.slice(i - 1), sample.extraInfo);
  });
}

function filterByCallstackPostfix(samples, symbols, functions, callstack, appliesToJS, useFunctions) {
  var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) {
      return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot);
    } : function isJSSymbolOrRoot(symbolIndex) {
      return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot);
    };
  return samples.map(function filterSample(sample) {
    if (!sample)
      return null;
    if (sample.frames.length < callstack.length)
      return null;
    for (var i = 0, j = 0; j < callstack.length; i++) {
      if (i >= sample.frames.length)
        return null;
      if (appliesToJS && !isJSFrameOrRoot(sample.frames[sample.frames.length - i - 1]))
        continue;
      if (sample.frames[sample.frames.length - i - 1] != callstack[j])
        return null;
      j++;
    }
    var newFrames = sample.frames.slice(0, sample.frames.length - i + 1);
    return makeSample(newFrames, sample.extraInfo);
  });
}

function chargeNonJSToCallers(samples, symbols, functions, useFunctions) {
  var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) {
      return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot);
    } : function isJSSymbolOrRoot(symbolIndex) {
      return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot);
    };
  samples = samples.slice(0);
  for (var i = 0; i < samples.length; ++i) {
    var sample = samples[i];
    if (!sample)
      continue;
    var newFrames = sample.frames.filter(isJSFrameOrRoot);
    if (!newFrames.length) {
      samples[i] = null;
    } else {
      samples[i].frames = newFrames;
    }
  }
  return samples;
}

function filterByName(samples, symbols, functions, filterName, useFunctions) {
  function getSymbolOrFunctionName(index, useFunctions) {
    if (useFunctions) {
      if (!(index in functions))
        return "";
      return functions[index].functionName;
    }
    if (!(index in symbols))
      return "";
    return symbols[index].symbolName;
  }
  function getLibraryName(index, useFunctions) {
    if (useFunctions) {
      if (!(index in functions))
        return "";
      return functions[index].libraryName;
    }
    if (!(index in symbols))
      return "";
    return symbols[index].libraryName;
  }
  samples = samples.slice(0);
  filterName = filterName.toLowerCase();
  calltrace_it: for (var i = 0; i < samples.length; ++i) {
    var sample = samples[i];
    if (!sample)
      continue;
    var callstack = sample.frames;
    for (var j = 0; j < callstack.length; ++j) { 
      var symbolOrFunctionName = getSymbolOrFunctionName(callstack[j], useFunctions);
      var libraryName = getLibraryName(callstack[j], useFunctions);
      if (symbolOrFunctionName.toLowerCase().indexOf(filterName) != -1 || 
          libraryName.toLowerCase().indexOf(filterName) != -1) {
        continue calltrace_it;
      }
    }
    samples[i] = null;
  }
  return samples;
}

function discardLineLevelInformation(samples, symbols, functions) {
  var data = samples;
  var filteredData = [];
  for (var i = 0; i < data.length; i++) {
    if (!data[i]) {
      filteredData.push(null);
      continue;
    }
    filteredData.push(cloneSample(data[i]));
    var frames = filteredData[i].frames;
    for (var j = 0; j < frames.length; j++) {
      if (!(frames[j] in symbols))
        continue;
      frames[j] = symbols[frames[j]].functionIndex;
    }
  }
  return filteredData;
}

function mergeUnbranchedCallPaths(root) {
  var mergedNames = [root.name];
  var node = root;
  while (node.children.length == 1 && node.counter == node.children[0].counter) {
    node = node.children[0];
    mergedNames.push(node.name);
  }
  if (node != root) {
    // Merge path from root to node into root.
    root.children = node.children;
    root.mergedNames = mergedNames;
    //root.name = clipText(root.name, 50) + " to " + this._clipText(node.name, 50);
  }
  for (var i = 0; i < root.children.length; i++) {
    mergeUnbranchedCallPaths(root.children[i]);
  }
}

function FocusedFrameSampleFilter(focusedSymbol) {
  this._focusedSymbol = focusedSymbol;
}
FocusedFrameSampleFilter.prototype = {
  filter: function FocusedFrameSampleFilter_filter(samples, symbols, functions, useFunctions) {
    return filterBySymbol(samples, this._focusedSymbol);
  }
};

function FocusedCallstackPrefixSampleFilter(focusedCallstack, appliesToJS) {
  this._focusedCallstackPrefix = focusedCallstack;
  this._appliesToJS = appliesToJS;
}
FocusedCallstackPrefixSampleFilter.prototype = {
  filter: function FocusedCallstackPrefixSampleFilter_filter(samples, symbols, functions, useFunctions) {
    return filterByCallstackPrefix(samples, symbols, functions, this._focusedCallstackPrefix, this._appliesToJS, useFunctions);
  }
};

function FocusedCallstackPostfixSampleFilter(focusedCallstack, appliesToJS) {
  this._focusedCallstackPostfix = focusedCallstack;
  this._appliesToJS = appliesToJS;
}
FocusedCallstackPostfixSampleFilter.prototype = {
  filter: function FocusedCallstackPostfixSampleFilter_filter(samples, symbols, functions, useFunctions) {
    return filterByCallstackPostfix(samples, symbols, functions, this._focusedCallstackPostfix, this._appliesToJS, useFunctions);
  }
};

function RangeSampleFilter(start, end) {
  this._start = start;
  this._end = end;
}
RangeSampleFilter.prototype = {
  filter: function RangeSampleFilter_filter(samples, symbols, functions) {
    return samples.slice(this._start, this._end);
  }
}

function unserializeSampleFilters(filters) {
  return filters.map(function (filter) {
    switch (filter.type) {
      case "FocusedFrameSampleFilter":
        return new FocusedFrameSampleFilter(filter.focusedSymbol);
      case "FocusedCallstackPrefixSampleFilter":
        return new FocusedCallstackPrefixSampleFilter(filter.focusedCallstack, filter.appliesToJS);
      case "FocusedCallstackPostfixSampleFilter":
        return new FocusedCallstackPostfixSampleFilter(filter.focusedCallstack, filter.appliesToJS);
      case "RangeSampleFilter":
        return new RangeSampleFilter(filter.start, filter.end);
      case "PluginView":
        return null;
      default:
        throw new Error("Unknown filter");
    }
  })
}

var gJankThreshold = 50 /* ms */;

function updateFilters(requestID, profileID, filters) {
  var profile = gProfiles[profileID];
  var samples = profile.allSamples;
  var symbols = profile.symbols;
  var functions = profile.functions;

  if (filters.mergeFunctions) {
    samples = discardLineLevelInformation(samples, symbols, functions);
  }
  if (filters.nameFilter) {
    try {
      samples = filterByName(samples, symbols, functions, filters.nameFilter, filters.mergeFunctions);
    } catch (e) {
      dump("Could not filer by name: " + e + "\n");
    }
  }
  samples = unserializeSampleFilters(filters.sampleFilters).reduce(function (filteredSamples, currentFilter) {
    if (currentFilter===null) return filteredSamples;
    return currentFilter.filter(filteredSamples, symbols, functions, filters.mergeFunctions);
  }, samples);
  if (filters.jankOnly) {
    samples = filterByJank(samples, gJankThreshold);
  }
  if (filters.javascriptOnly) {
    samples = chargeNonJSToCallers(samples, symbols, functions, filters.mergeFunctions);
  }

  gProfiles[profileID].filterSettings = filters;
  gProfiles[profileID].filteredSamples = samples;
  sendFinishedInChunks(requestID, samples, 40000,
                       function (sample) { return sample ? sample.frames.length : 1; });
}

function updateViewOptions(requestID, profileID, options) {
  var profile = gProfiles[profileID];
  var samples = profile.filteredSamples;
  var symbols = profile.symbols;
  var functions = profile.functions;

  var treeData = convertToCallTree(samples, options.invertCallstack);
  if (options.mergeUnbranched)
    mergeUnbranchedCallPaths(treeData);
  sendFinished(requestID, treeData);
}

// The responsiveness threshold (in ms) after which the sample shuold become
// completely red in the histogram.
var kDelayUntilWorstResponsiveness = 1000;

function calculateHistogramData(requestID, profileID) {

  function getStepColor(step) {
    if (step.extraInfo && "responsiveness" in step.extraInfo) {
      var res = step.extraInfo.responsiveness;
      var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness));
      return "rgb(" + redComponent + ",0,0)";
    }

    return "rgb(0,0,0)";
  }

  var profile = gProfiles[profileID];
  var data = profile.filteredSamples;
  var histogramData = [];
  var maxHeight = 0;
  for (var i = 0; i < data.length; ++i) {
    if (!data[i])
      continue;
    var value = data[i].frames.length;
    if (maxHeight < value)
      maxHeight = value;
  }
  maxHeight += 1;
  var nextX = 0;
  // The number of data items per histogramData rects.
  // Except when seperated by a marker.
  // This is used to cut down the number of rects, since
  // there's no point in having more rects then pixels
  var samplesPerStep = Math.max(1, Math.floor(data.length / 2000));
  var frameStart = {};
  for (var i = 0; i < data.length; i++) {
    var step = data[i];
    if (!step) {
      // Add a gap for the sample that was filtered out.
      nextX += 1 / samplesPerStep;
      continue;
    }
    nextX = Math.ceil(nextX);
    var value = step.frames.length / maxHeight;
    var frames = step.frames;
    var currHistogramData = histogramData[histogramData.length-1];
    if (step.extraInfo && "marker" in step.extraInfo) {
      // A new marker boundary has been discovered.
      histogramData.push({
        frames: "marker",
        x: nextX,
        width: 2,
        value: 1,
        marker: step.extraInfo.marker,
        color: "fuchsia"
      });
      nextX += 2;
      histogramData.push({
        frames: [step.frames],
        x: nextX,
        width: 1,
        value: value,
        color: getStepColor(step),
      });
      nextX += 1;
    } else if (currHistogramData != null &&
      currHistogramData.frames.length < samplesPerStep &&
      !(step.extraInfo && "frameNumber" in step.extraInfo)) {
      currHistogramData.frames.push(step.frames);
      // When merging data items take the average:
      currHistogramData.value =
        (currHistogramData.value * (currHistogramData.frames.length - 1) + value) /
        currHistogramData.frames.length;
      // Merge the colors? For now we keep the first color set.
    } else {
      // A new name boundary has been discovered.
      currHistogramData = {
        frames: [step.frames],
        x: nextX,
        width: 1,
        value: value,
        color: getStepColor(step),
      };
      if (step.extraInfo && "frameNumber" in step.extraInfo) {
        currHistogramData.frameNumber = step.extraInfo.frameNumber;
        frameStart[step.extraInfo.frameNumber] = histogramData.length;
      }
      histogramData.push(currHistogramData);
      nextX += 1;
    }
  }
  sendFinished(requestID, { histogramData: histogramData, frameStart: frameStart, widthSum: Math.ceil(nextX) });
}

var diagnosticList = [
  // *************** Known bugs first (highest priority)
  {
    image: "io.png",
    title: "Main Thread IO - Bug 765135 - TISCreateInputSourceList",
    check: function(frames, symbols, meta) {

      if (!stepContains('TISCreateInputSourceList', frames, symbols))
        return false;

      return stepContains('__getdirentries64', frames, symbols) 
          || stepContains('__read', frames, symbols) 
          || stepContains('__open', frames, symbols) 
          || stepContains('stat$INODE64', frames, symbols)
          ;
    },
  },

  {
    image: "js.png",
    title: "Bug 772916 - Gradients are slow on mobile",
    bugNumber: "772916",
    check: function(frames, symbols, meta) {

      return stepContains('PaintGradient', frames, symbols)
          && stepContains('ClientTiledLayerBuffer::PaintThebesSingleBufferDraw', frames, symbols)
          ;
    },
  },
  {
    image: "cache.png",
    title: "Bug 717761 - Main thread can be blocked by IO on the cache thread",
    bugNumber: "717761",
    check: function(frames, symbols, meta) {

      return stepContains('nsCacheEntryDescriptor::GetStoragePolicy', frames, symbols)
          ;
    },
  },
  {
    image: "js.png",
    title: "Web Content Shutdown Notification",
    check: function(frames, symbols, meta) {

      return stepContains('nsAppStartup::Quit', frames, symbols)
          && stepContains('nsDocShell::FirePageHideNotification', frames, symbols)
          ;
    },
  },
  {
    image: "js.png",
    title: "Bug 789193 - AMI_startup() takes 200ms on startup",
    bugNumber: "789193",
    check: function(frames, symbols, meta) {

      return stepContains('AMI_startup()', frames, symbols)
          ;
    },
  },
  {
    image: "js.png",
    title: "Bug 818296 - [Shutdown] js::NukeCrossCompartmentWrappers takes up 300ms on shutdown",
    bugNumber: "818296",
    check: function(frames, symbols, meta) {
      return stepContains('js::NukeCrossCompartmentWrappers', frames, symbols)
          && (stepContains('WindowDestroyedEvent', frames, symbols) || stepContains('DoShutdown', frames, symbols))
          ;
    },
  },
  {
    image: "js.png",
    title: "Bug 818274 - [Shutdown] Telemetry takes ~10ms on shutdown",
    bugNumber: "818274",
    check: function(frames, symbols, meta) {
      return stepContains('TelemetryPing.js', frames, symbols)
          ;
    },
  },
  {
    image: "plugin.png",
    title: "Bug 818265 - [Shutdown] Plug-in shutdown takes ~90ms on shutdown",
    bugNumber: "818265",
    check: function(frames, symbols, meta) {
      return stepContains('PluginInstanceParent::Destroy', frames, symbols)
          ;
    },
  },
  {
    image: "snapshot.png",
    title: "Bug 720575 - Make thumbnailing faster and/or asynchronous",
    bugNumber: "720575",
    check: function(frames, symbols, meta) {
      return stepContains('Thumbnails_capture()', frames, symbols)
          ;
    },
  },

  {
    image: "js.png",
    title: "Bug 789185 - LoginManagerStorage_mozStorage.init() takes 300ms on startup ",
    bugNumber: "789185",
    check: function(frames, symbols, meta) {

      return stepContains('LoginManagerStorage_mozStorage.prototype.init()', frames, symbols)
          ;
    },
  },

  {
    image: "js.png",
    title: "JS - Bug 767070 - Text selection performance is bad on android",
    bugNumber: "767070",
    check: function(frames, symbols, meta) {

      if (!stepContains('FlushPendingNotifications', frames, symbols))
        return false;

      return stepContains('sh_', frames, symbols)
          && stepContains('browser.js', frames, symbols)
          ;
    },
  },

  {
    image: "js.png",
    title: "JS - Bug 765930 - Reader Mode: Optimize readability check",
    bugNumber: "765930",
    check: function(frames, symbols, meta) {

      return stepContains('Readability.js', frames, symbols)
          ;
    },
  },

  // **************** General issues
  {
    image: "js.png",
    title: "JS is triggering a sync reflow",
    check: function(frames, symbols, meta) {
      return symbolSequence(['js::RunScript','layout::DoReflow'], frames, symbols) ||
             symbolSequence(['js::RunScript','layout::Flush'], frames, symbols)
          ;
    },
  },

  {
    image: "gc.png",
    title: "Garbage Collection Slice",
    canMergeWithGC: false,
    check: function(frames, symbols, meta, step) {
      var slice = findGCSlice(frames, symbols, meta, step);

      if (slice) {
        var gcEvent = findGCEvent(frames, symbols, meta, step);
        //dump("found event matching diagnostic\n");
        //dump(JSON.stringify(gcEvent) + "\n");
        return true;
      }
      return false;
    },
    details: function(frames, symbols, meta, step) {
      var slice = findGCSlice(frames, symbols, meta, step);
      if (slice) {
        return "" +
          "Reason: " + slice.reason + "\n" +
          "Slice: " + slice.slice + "\n" +
          "Pause: " + slice.pause + " ms";
      }
      return null;
    },
    onclickDetails: function(frames, symbols, meta, step) {
      var gcEvent = findGCEvent(frames, symbols, meta, step);
      if (gcEvent) {
        return JSON.stringify(gcEvent);
      } else {
        return null;
      }
    },
  },
  {
    image: "cc.png",
    title: "Cycle Collect",
    check: function(frames, symbols, meta, step) {
      var ccEvent = findCCEvent(frames, symbols, meta, step);

      if (ccEvent) {
        return true;
      }
      return false;
    },
    details: function(frames, symbols, meta, step) {
      var ccEvent = findCCEvent(frames, symbols, meta, step);
      if (ccEvent) {
        return "" +
          "Duration: " + ccEvent.duration + " ms\n" +
          "Suspected: " + ccEvent.suspected;
      }
      return null;
    },
    onclickDetails: function(frames, symbols, meta, step) {
      var ccEvent = findCCEvent(frames, symbols, meta, step);
      if (ccEvent) {
        return JSON.stringify(ccEvent);
      } else {
        return null;
      }
    },
  },
  {
    image: "gc.png",
    title: "Garbage Collection",
    canMergeWithGC: false,
    check: function(frames, symbols, meta) {
      return stepContainsRegEx(/.*Collect.*Runtime.*Invocation.*/, frames, symbols)
          || stepContains('GarbageCollectNow', frames, symbols) // Label
          || stepContains('JS_GC(', frames, symbols) // Label
          || stepContains('CycleCollect__', frames, symbols) // Label
          ;
    },
  },
  {
    image: "cc.png",
    title: "Cycle Collect",
    check: function(frames, symbols, meta) {
      return stepContains('nsCycleCollector::Collect', frames, symbols)
          || stepContains('CycleCollect__', frames, symbols) // Label
          || stepContains('nsCycleCollectorRunner::Collect', frames, symbols) // Label
          ;
    },
  },
  {
    image: "plugin.png",
    title: "Sync Plugin Constructor",
    check: function(frames, symbols, meta) {
      return stepContains('CallPPluginInstanceConstructor', frames, symbols) 
          || stepContains('CallPCrashReporterConstructor', frames, symbols) 
          || stepContains('PPluginModuleParent::CallNP_Initialize', frames, symbols)
          || stepContains('GeckoChildProcessHost::SyncLaunch', frames, symbols)
          ;
    },
  },
  {
    image: "text.png",
    title: "Font Loading",
    check: function(frames, symbols, meta) {
      return stepContains('gfxFontGroup::BuildFontList', frames, symbols);
    },
  },
  {
    image: "io.png",
    title: "Main Thread IO!",
    check: function(frames, symbols, meta) {
      return stepContains('__getdirentries64', frames, symbols) 
          || stepContains('__open', frames, symbols) 
          || stepContains('NtFlushBuffersFile', frames, symbols) 
          || stepContains('storage:::Statement::ExecuteStep', frames, symbols) 
          || stepContains('__unlink', frames, symbols) 
          || stepContains('fsync', frames, symbols) 
          || stepContains('stat$INODE64', frames, symbols)
          ;
    },
  },
];

function hasJSFrame(frames, symbols) {
  for (var i = 0; i < frames.length; i++) {
    if (symbols[frames[i]].isJSFrame === true) {
      return true;
    }
  }
  return false;
}
function findCCEvent(frames, symbols, meta, step) {
  if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
    return null;

  var time = step.extraInfo.time;

  for (var i = 0; i < meta.gcStats.ccEvents.length; i++) {
    var ccEvent = meta.gcStats.ccEvents[i];
    if (ccEvent.start_timestamp <= time && ccEvent.end_timestamp >= time) {
      //dump("JSON: " + js_beautify(JSON.stringify(ccEvent)) + "\n");
      return ccEvent;
    }
  }

  return null;
}
function findGCEvent(frames, symbols, meta, step) {
  if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
    return null;

  var time = step.extraInfo.time;

  for (var i = 0; i < meta.gcStats.gcEvents.length; i++) {
    var gcEvent = meta.gcStats.gcEvents[i];
    if (!gcEvent.slices)
      continue;
    for (var j = 0; j < gcEvent.slices.length; j++) {
      var slice = gcEvent.slices[j];
      if (slice.start_timestamp <= time && slice.end_timestamp >= time) {
        return gcEvent;
      }
    }
  }

  return null;
}
function findGCSlice(frames, symbols, meta, step) {
  if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
    return null;

  var time = step.extraInfo.time;

  for (var i = 0; i < meta.gcStats.gcEvents.length; i++) {
    var gcEvent = meta.gcStats.gcEvents[i];
    if (!gcEvent.slices)
      continue;
    for (var j = 0; j < gcEvent.slices.length; j++) {
      var slice = gcEvent.slices[j];
      if (slice.start_timestamp <= time && slice.end_timestamp >= time) {
        return slice;
      }
    }
  }

  return null;
}
function stepContains(substring, frames, symbols) {
  for (var i = 0; frames && i < frames.length; i++) {
    if (!(frames[i] in symbols))
      continue;
    var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
    if (frameSym.indexOf(substring) != -1) {
      return true;
    }
  }
  return false;
}
function stepContainsRegEx(regex, frames, symbols) {
  for (var i = 0; frames && i < frames.length; i++) {
    if (!(frames[i] in symbols))
      continue;
    var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
    if (regex.exec(frameSym)) {
      return true;
    }
  }
  return false;
}
function symbolSequence(symbolsOrder, frames, symbols) {
  var symbolIndex = 0;
  for (var i = 0; frames && i < frames.length; i++) {
    if (!(frames[i] in symbols))
      continue;
    var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
    var substring = symbolsOrder[symbolIndex];
    if (frameSym.indexOf(substring) != -1) {
      symbolIndex++;
      if (symbolIndex == symbolsOrder.length) {
        return true;
      }
    }
  }
  return false;
}
function firstMatch(array, matchFunction) {
  for (var i = 0; i < array.length; i++) {
    if (matchFunction(array[i]))
      return array[i];
  }
  return undefined;
}

function calculateDiagnosticItems(requestID, profileID, meta) {
  /*
  if (!histogramData || histogramData.length < 1) {
    sendFinished(requestID, []);
    return;
  }*/

  var profile = gProfiles[profileID];
  //var symbols = profile.symbols;
  var symbols = profile.functions;
  var data = profile.filteredSamples;

  var lastStep = data[data.length-1];
  var widthSum = data.length;
  var pendingDiagnosticInfo = null;

  var diagnosticItems = [];

  function finishPendingDiagnostic(endX) {
    if (!pendingDiagnosticInfo)
      return;

    var diagnostic = pendingDiagnosticInfo.diagnostic;
    var currDiagnostic = {
      x: pendingDiagnosticInfo.x / widthSum,
      width: (endX - pendingDiagnosticInfo.x) / widthSum,
      imageFile: pendingDiagnosticInfo.diagnostic.image,
      title: pendingDiagnosticInfo.diagnostic.title,
      details: pendingDiagnosticInfo.details,
      onclickDetails: pendingDiagnosticInfo.onclickDetails
    };

    if (!currDiagnostic.onclickDetails && diagnostic.bugNumber) {
      currDiagnostic.onclickDetails = "bug " + diagnostic.bugNumber;
    }

    diagnosticItems.push(currDiagnostic);

    pendingDiagnosticInfo = null;
  }

/*
  dump("meta: " + meta.gcStats + "\n");
  if (meta && meta.gcStats) {
    dump("GC Stats: " + JSON.stringify(meta.gcStats) + "\n");
  }
*/

  data.forEach(function diagnoseStep(step, x) {
    if (step) {
      var frames = step.frames;

      var diagnostic = firstMatch(diagnosticList, function (diagnostic) {
        return diagnostic.check(frames, symbols, meta, step);
      });
    }

    if (!diagnostic) {
      finishPendingDiagnostic(x);
      return;
    }

    var details = diagnostic.details ? diagnostic.details(frames, symbols, meta, step) : null;

    if (pendingDiagnosticInfo) {
      // We're already inside a diagnostic range.
      if (diagnostic == pendingDiagnosticInfo.diagnostic && pendingDiagnosticInfo.details == details) {
        // We're still inside the same diagnostic.
        return;
      }

      // We have left the old diagnostic and found a new one. Finish the old one.
      finishPendingDiagnostic(x);
    }

    pendingDiagnosticInfo = {
      diagnostic: diagnostic,
      x: x,
      details: details,
      onclickDetails: diagnostic.onclickDetails ? diagnostic.onclickDetails(frames, symbols, meta, step) : null
    };
  });
  if (pendingDiagnosticInfo)
    finishPendingDiagnostic(data.length);

  sendFinished(requestID, diagnosticItems);
}
