<!DOCTYPE HTML>
<html lang="en">
<head>
  <meta charset="utf8">
  <title>Test for getRepeatId()</title>
  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <script type="application/javascript" src="head.js"></script>
  <!-- Any copyright is dedicated to the Public Domain.
     - http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for render perf</p>
<div id="output"></div>

<script type="text/javascript">
"use strict";

// To analyze the profile results:
// > ./mach mochitest test_render_perf.html
// Then open https://profiler.firefox.com and drag the json file printed at the end of
// this test

const NUM_MESSAGES = 4000;
const NUM_STREAMING = 100;
const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
Services.prefs.setIntPref("devtools.hud.loglimit", NUM_MESSAGES);
const WebConsoleWrapper = browserRequire("devtools/client/webconsole/webconsole-wrapper");
const actions =
  browserRequire("devtools/client/webconsole/actions/index");
const EventEmitter = browserRequire("devtools/shared/event-emitter");
const testPackets = Array.from({length: NUM_MESSAGES}).map((el, id) => ({
  "from": "server1.conn4.child1/consoleActor2",
  "type": "consoleAPICall",
  "message": {
    "arguments": [
      "foobar",
      `${id % 2 === 0 ? "Even" : "Odd"} text`,
      id,
    ],
    "columnNumber": 1,
    "counter": null,
    "filename": "file:///test.html",
    "functionName": "",
    "groupName": "",
    "level": "log",
    "lineNumber": 1,
    "private": false,
    "styles": [],
    "timeStamp": 1455064271115 + id,
    "timer": null,
    "workerType": "none",
    "category": "webdev",
  },
}));
const lastPacket = testPackets.pop();

function startMeasure(label) {
  const startLabel = label + "start";
  performance.mark(startLabel);
  return {
    stop: (clear = true) => {
      performance.measure(label, startLabel);
      const entries = performance.getEntriesByName(label);
      if (clear) {
        performance.clearMeasures(label);
      }
      return entries[entries.length - 1];
    },
  };
}

function wait(time) {
  return new Promise(resolve => setTimeout(resolve, time));
}

async function addAllMessages(wrapper) {
  testPackets.forEach((packet) => wrapper.dispatchMessageAdd(packet));
  // Only wait for the last packet to minimize work.
  await wrapper.dispatchMessageAdd(lastPacket, true);
  await new Promise(resolve => requestAnimationFrame(resolve));
}

async function addMessage(wrapper, message) {
  await wrapper.dispatchMessageAdd(message, true);
}

function getTimes(times) {
  times = times.sort();
  const totalTime = times.reduce((sum, t) => sum + t);
  const avg = totalTime / times.length;
  const median = times.length % 2 !== 0
    ? times[Math.floor(times.length / 2)]
    : (times[(times.length / 2) - 1] + times[times.length / 2]) / 2;
  return {avg, median};
}

async function clearMessages(wrapper) {
  wrapper.dispatchMessagesClear();
  await new Promise(resolve => requestAnimationFrame(resolve));
}

async function testBulkLogging(wrapper) {
  await clearMessages(wrapper);
  const bulkTimes = [];
  const iterations = 5;
  for (let i = 0; i < iterations; i++) {
    const measure = startMeasure("bulk log");
    await addAllMessages(wrapper);
    const {duration} = measure.stop();

    info(`took ${duration} ms to render bulk messages (iteration ${i})`);
    bulkTimes.push(duration);

    // Do not clear the store on last iteration so the next test can use the messages.
    if (i !== iterations - 1) {
      // Wait before clearing messages so those events are more spotable on the profile.
      await wait(500);
      await clearMessages(wrapper);
      await wait(500);
    }
  }
  const {avg, median} = getTimes(bulkTimes);

  info(`BULK: On average, it took ${avg} ms (median ${median} ms) ` +
       `to render ${NUM_MESSAGES} messages`);
}

async function testFiltering(wrapper) {
  const measureFilter = startMeasure("filtering");

  const measureFilterOff = startMeasure("filtering off");
  await wrapper.getStore().dispatch(actions.filterToggle("log"));
  const measureFilterOffEntry = measureFilterOff.stop();
  info(`Filter toggle time (off): ${measureFilterOffEntry.duration}`);

  const measureFilterOn = startMeasure("filtering on");
  await wrapper.getStore().dispatch(actions.filterToggle("log"));
  const measureFilterOnEntry = measureFilterOn.stop();
  info(`Filter toggle time (on): ${measureFilterOnEntry.duration}`);
  measureFilter.stop();
}

async function testStreamLogging(wrapper, clear = true) {
  const streamMeasureLabel = "stream" + (clear === false ? " and prune" : "");
  const streamMeasure = startMeasure(streamMeasureLabel);
  if (clear === true) {
    await clearMessages(wrapper);
  }

  const measureLabel = "stream - add single message";
  for (let i = 0; i < NUM_STREAMING; i++) {
    const measure = startMeasure(measureLabel);
    await addMessage(wrapper, testPackets[i]);
    measure.stop(false);
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  const streamTimes = performance.getEntriesByName(measureLabel)
    .map(entry => entry.duration);
  performance.clearMeasures(measureLabel);
  const { avg, median } = getTimes(streamTimes);
  info(`STREAMING${clear === false ? " AND PRUNING" : ""}: On average, ` +
    `it took ${avg} ms (median ${median} ms) for each message`);
  streamMeasure.stop();
}

window.onload = async function() {
  // This test does costly work multiple times to have better performance data.
  // It doesn't run in automation
  SimpleTest.requestLongerTimeout(3);

  const wrapper = new WebConsoleWrapper(
    document.getElementById("output"),
    {hud: EventEmitter.decorate({proxy: {}}), focus: () => {}},
    {},
    null,
    document,
  );
  wrapper.init();

  // From https://github.com/firefox-devtools/profiler/blob/b73eb73df04c7df51464fa50eeadef3dc7f5d4e2/docs/gecko-profile-format.md#L21
  const settings = {
    entries: 100000000,
    interval: 1,
    features: ["js"],
    threads: ["GeckoMain"],
  };
  Services.profiler.StartProfiler(
    settings.entries,
    settings.interval,
    settings.features,
    settings.threads,
  );
  info("Profiler has started");

  await wait(500);

  await testBulkLogging(wrapper);

  await wait(500);

  await testFiltering(wrapper);

  await wait(500);

  // first pass, without clearing the store.
  await testStreamLogging(wrapper, false);

  await wait(500);
  // second pass, with an empty store.
  await testStreamLogging(wrapper, true);

  ok(true, "Tests finished");

  const file = new FileUtils.File(PathUtils.join(PathUtils.tempDir, `test_render_perf_${Date.now()}.json`));
  Services.profiler.dumpProfileToFile(file.path);
  Services.profiler.StopProfiler();

  info(`

SAVING PROFILE: ${file.path}

To upload the profile, run the following commands:

  gzip ${file.path}
  curl -O https://raw.githubusercontent.com/firefox-devtools/profiler-server/master/tools/decode_jwt_payload.py
  curl 'https://api.profiler.firefox.com/compressed-store' --compressed --data-binary @${file.path}.gz --header 'Accept: application/vnd.firefox-profiler+json;version=1' \\
    | python3 decode_jwt_payload.py \\
    | awk '{print "Hosted at: https://profiler.firefox.com/public/"$1}'


`);

  SimpleTest.finish();
};
</script>
</body>
</html>
