js/src/devtools/gc-ubench/index.html
author Terrence Cole <terrence@mozilla.com>
Fri, 19 Dec 2014 10:03:04 -0800
changeset 233250 30f9b7719d69714d8383a9aeab8fc7bf259d17c7
child 233261 6cd496d732f7b8b58e9ba5c7410d50b284865291
permissions -rw-r--r--
Bug 1112278 - Create a GC regression benchmark; r=sfink

<html>
<head>
  <title>GC uBench</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body onload="onload()" onunload="onunload()">

<!-- Include benchmark modules. -->
<script>var tests = new Map();</script>
<script src="benchmarks/noAllocation.js"></script>
<script src="benchmarks/globalArrayNewObject.js"></script>

<!-- Harness implementation. -->
<script>
// Per-frame time sampling infra. Also GC'd: hopefully will not perturb things too badly.
var numSamples = 500;
var delays = new Array(numSamples);
var sampleIndex = 0;
var sampleTime = 16; // ms
var gHistogram = new Map(); // {ms: count}

// Draw state.
var stopped = 0;
var start;
var prev;
var ctx;

// Current test state.
var garbagePerFrame = undefined;
var garbageTotal = undefined;
var activeTest = undefined;
var testDuration = undefined; // ms
var testState = 'idle';  // One of 'idle' or 'running'.
var testStart = undefined; // ms
var testQueue = [];

function xpos(index)
{
    return index * 2;
}

function ypos(delay)
{
    var r = 525 - Math.log(delay) * 64;
    if (r < 5) return 5;
    return r;
}

function drawHBar(delay, color, label)
{
    ctx.fillStyle = color;
    ctx.strokeStyle = color;
    ctx.fillText(label, xpos(numSamples) + 4, ypos(delay) + 3);

    ctx.beginPath();
    ctx.moveTo(xpos(0), ypos(delay));
    ctx.lineTo(xpos(numSamples), ypos(delay));
    ctx.stroke();
    ctx.strokeStyle = 'rgb(0,0,0)';
    ctx.fillStyle = 'rgb(0,0,0)';
}

function drawScale(delay)
{
    drawHBar(delay, 'rgb(150,150,150)', `${delay}ms`);
}

function draw60fps() {
    drawHBar(1000/60, '#00cf61', '60fps');
}

function draw30fps() {
    drawHBar(1000/30, '#cf0061', '30fps');
}

function drawGraph()
{
    ctx.clearRect(0, 0, 1100, 550);

    drawScale(10);
    draw60fps();
    drawScale(20);
    drawScale(30);
    draw30fps();
    drawScale(50);
    drawScale(100);
    drawScale(200);
    drawScale(400);
    drawScale(800);

    var worst = 0, worstpos = 0;
    ctx.beginPath();
    for (var i = 0; i < numSamples; i++) {
        ctx.lineTo(xpos(i), ypos(delays[i]));
        if (delays[i] >= worst) {
            worst = delays[i];
            worstpos = i;
        }
    }
    ctx.stroke();

    ctx.fillStyle = 'rgb(255,0,0)';
    if (worst)
        ctx.fillText(''+worst+'ms', xpos(worstpos) - 10, ypos(worst) - 14);

    ctx.beginPath();
    var where = sampleIndex % numSamples;
    ctx.arc(xpos(where), ypos(delays[where]), 5, 0, Math.PI*2, true);
    ctx.fill();
    ctx.fillStyle = 'rgb(0,0,0)';

    ctx.fillText('Time', 550, 420);
    ctx.save();
    ctx.rotate(Math.PI/2);
    ctx.fillText('Pause between frames (log scale)', 150, -1060);
    ctx.restore();
}

function stopstart()
{
    if (stopped) {
        window.requestAnimationFrame(handler);
        prev = performance.now();
        start += prev - stopped;
        document.getElementById('stop').value = 'Pause';
        stopped = 0;
    } else {
        document.getElementById('stop').value = 'Resume';
        stopped = performance.now();
    }
}

function handler(timestamp)
{
    if (stopped)
        return;

    if (testState === 'running' && (timestamp - testStart) > testDuration)
        end_test(timestamp);

    activeTest.makeGarbage(garbagePerFrame);

    var elt = document.getElementById('data');
    var delay = timestamp - prev;
    prev = timestamp;

    // Take the histogram at 10us intervals so that we have enough resolution to capture.
    // a 16.66[666] target with adequate accuracy.
    update_histogram(gHistogram, Math.round(delay * 100));

    var t = timestamp - start;
    var newIndex = Math.round(t / sampleTime);
    while (sampleIndex < newIndex) {
        sampleIndex++;
        delays[sampleIndex % numSamples] = delay;
    }

    drawGraph();
    window.requestAnimationFrame(handler);
}

function update_histogram(histogram, delay)
{
    var current = histogram.has(delay) ? histogram.get(delay) : 0;
    histogram.set(delay, ++current);
}

function reset_draw_state()
{
    for (var i = 0; i < numSamples; i++)
        delays[i] = 0;
    start = prev = performance.now();
    sampleIndex = 0;
}

function onunload()
{
    activeTest.unload();
    activeTest = undefined;
}

function onload()
{
    // Load initial test duration.
    duration_changed();

    // Load initial garbage size.
    garbage_total_changed();
    garbage_per_frame_changed();

    // Populate the test selection dropdown.
    var select = document.getElementById("test-selection");
    for (var [name, test] of tests) {
        test.name = name;
        var option = document.createElement("option");
        option.id = name;
        option.text = name;
        option.title = test.description;
        select.add(option);
    }

    // Load the initial test.
    activeTest = tests.get('noAllocation');
    activeTest.load(garbageTotal);

    // Polyfill rAF.
    var requestAnimationFrame =
        window.requestAnimationFrame || window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
    window.requestAnimationFrame = requestAnimationFrame;

    // Acquire our canvas.
    var canvas = document.getElementById('graph');
    ctx = canvas.getContext('2d');

    // Start drawing.
    reset_draw_state();
    window.requestAnimationFrame(handler);
}

function run_one_test()
{
    start_test_cycle([activeTest.name]);
}

function run_all_tests()
{
    start_test_cycle(tests.keys());
}

function start_test_cycle(tests_to_run)
{
    // Convert from an iterable to an array for pop.
    testQueue = [];
    for (var key of tests_to_run)
        testQueue.push(key);
    testState = 'running';
    testStart = performance.now();
    gHistogram.clear();

    change_active_test(testQueue.pop());
    console.log(`Running test: ${activeTest.name}`);
    reset_draw_state();
}

function end_test(timestamp)
{
    report_test_result(activeTest, gHistogram);
    gHistogram.clear();
    console.log(`Ending test ${activeTest.name}`);
    if (testQueue.length) {
        change_active_test(testQueue.pop());
        console.log(`Running test: ${activeTest.name}`);
        testStart = timestamp;
    } else {
        testState = 'idle';
        testStart = 0;
    }
    reset_draw_state();
}

function report_test_result(test, histogram)
{
    var resultList = document.getElementById('results-display');
    var resultElem = document.createElement("div");
    var score = compute_test_score(histogram);
    var sparks = compute_test_spark_histogram(histogram);
    resultElem.innerHTML = `${score} ms/s : ${sparks} : ${test.name} - ${test.description}`;
    resultList.appendChild(resultElem);
}

// Compute a score based on the total ms we missed frames by per second.
function compute_test_score(histogram)
{
    var score = 0;
    for (var [delay, count] of histogram) {
        delay = delay / 100;
        score += Math.abs((delay - 16.66) * count);
    }
    score = score / (testDuration / 1000);
    return Math.round(score * 1000) / 1000;
}

// Build a spark-lines histogram for the test results to show with the aggregate score.
function compute_test_spark_histogram(histogram)
{
    var ranges = [
        [-99999999, 16.6],
        [16.6, 16.8],
        [16.8, 25],
        [25, 33.4],
        [33.4, 60],
        [60, 100],
        [100, 300],
        [300, 99999999],
    ];
    var rescaled = new Map();
    for (var [delay, count] of histogram) {
        delay = delay / 100;
        for (var i = 0; i < ranges.length; ++i) {
            var low = ranges[i][0];
            var high = ranges[i][1];
            if (low <= delay && delay < high) {
                update_histogram(rescaled, i);
                break;
            }
        }
    }
    var total = 0;
    for (var [i, count] of rescaled)
        total += count;
    var sparks = "▁▂▃▄▅▆▇█";
    var colors = ['#aaaa00', '#007700', '#dd0000', '#ff0000',
                  '#ff0000', '#ff0000', '#ff0000', '#ff0000'];
    var line = "";
    for (var i = 0; i < ranges.length; ++i) {
        var amt = rescaled.has(i) ? rescaled.get(i) : 0;
        var spark = sparks.charAt(parseInt(amt/total*8));
        line += `<span style="color:${colors[i]}">${spark}</span>`;
    }
    return line;
}

function reload_active_test()
{
    activeTest.unload();
    activeTest.load(garbageTotal);
}

function change_active_test(new_test_name)
{
    activeTest.unload();
    activeTest = tests.get(new_test_name);
    activeTest.load(garbageTotal);
}

function duration_changed()
{
    var durationInput = document.getElementById('test-duration');
    testDuration = parseInt(durationInput.value) * 1000;
    console.log(`Updated test duration to: ${testDuration / 1000} seconds`);
}

function testchanged()
{
    var select = document.getElementById("test-selection");
    console.log(`Switching to test: ${select.value}`);
    change_active_test(select.value);
    gHistogram.clear();
    reset_draw_state();
}

function garbage_total_changed()
{
    var mult = parseInt(document.getElementById('garbage-total-unit').value);
    var base = parseInt(document.getElementById('garbage-total-size').value);
    if (isNaN(mult) || isNaN(base))
        return;
    garbageTotal = base * mult;
    console.log(`Updated garbage-total to ${garbageTotal} items`);
    if (activeTest)
        reload_active_test();
    gHistogram.clear();
    reset_draw_state();
}

function garbage_per_frame_changed()
{
    var mult = parseInt(document.getElementById('garbage-per-frame-unit').value);
    var base = parseInt(document.getElementById('garbage-per-frame-size').value);
    if (isNaN(mult) || isNaN(base))
        return;
    garbagePerFrame = base * mult;
    console.log(`Updated garbage-per-frame to ${garbagePerFrame} items`);
}
</script>

<script>
</script>

<canvas id="graph" width="1080" height="550" style="padding-left:10px"></canvas>

<div>
    <input type="button" id="stop" value="Pause" onclick="stopstart()"></input>
</div>

<div>
    Duration: <input type="text" id="test-duration" size=3 value="8" oninput="duration_changed()"></input>s
    <input type="button" id="test-one" value="Run Test" onclick="run_one_test()"></input>
    <input type="button" id="test-all" value="Run All Tests" onclick="run_all_tests()"></input>
</div>

<div>
    Currently running test:
    <select id="test-selection" required onchange="testchanged()"></select>
</div>

<div>
    Garbage items per frame:
    <input type="text" id="garbage-per-frame-size" size=5 value="8"
           oninput="garbage_per_frame_changed()"></input>
    <select id="garbage-per-frame-unit" required>
        <option value="1"></option>
        <option value="1000" selected="selected">K</option>
        <option value="1000000">M</option>
    </select>
</div>
<div>
    Garbage heap size in items:
    <input type="text" id="garbage-total-size" size=5 value="8"
           oninput="garbage_total_changed()"></input>
    <select id="garbage-total-unit" required>
        <option value="1"></option>
        <option value="1000">K</option>
        <option value="1000000" selected="selected">M</option>
    </select>
</div>

<div id="results-Area">
    Test Results:
    <div id="results-display" style="padding-left: 10px; border: 1px solid black;"></div>
</div>

</body>
</html>