diff options
Diffstat (limited to 'js/src/devtools/gc-ubench/harness.js')
-rw-r--r-- | js/src/devtools/gc-ubench/harness.js | 661 |
1 files changed, 661 insertions, 0 deletions
diff --git a/js/src/devtools/gc-ubench/harness.js b/js/src/devtools/gc-ubench/harness.js new file mode 100644 index 000000000..f217ff1cb --- /dev/null +++ b/js/src/devtools/gc-ubench/harness.js @@ -0,0 +1,661 @@ +// Per-frame time sampling infra. Also GC'd: hopefully will not perturb things too badly. +var numSamples = 500; +var delays = new Array(numSamples); +var gcs = new Array(numSamples); +var minorGCs = new Array(numSamples); +var gcBytes = new Array(numSamples); +var mallocBytes = new Array(numSamples); +var sampleIndex = 0; +var sampleTime = 16; // ms +var gHistogram = new Map(); // {ms: count} + +var features = { + trackingSizes: ('mozMemory' in performance), + showingGCs: ('mozMemory' in performance), +}; + +// Draw state. +var stopped = 0; +var start; +var prev; +var latencyGraph; +var memoryGraph; +var ctx; +var memoryCtx; + +// Current test state. +var activeTest = undefined; +var testDuration = undefined; // ms +var testState = 'idle'; // One of 'idle' or 'running'. +var testStart = undefined; // ms +var testQueue = []; + +// Global defaults +var globalDefaultGarbageTotal = "8M"; +var globalDefaultGarbagePerFrame = "8K"; + +function Graph(ctx) { + this.ctx = ctx; + + var { width, height } = ctx.canvas; + this.layout = { + xAxisLabel_Y: height - 20, + }; +} + +Graph.prototype.xpos = index => index * 2; + +Graph.prototype.clear = function () { + var { width, height } = this.ctx.canvas; + this.ctx.clearRect(0, 0, width, height); +}; + +Graph.prototype.drawScale = function (delay) +{ + this.drawHBar(delay, `${delay}ms`, 'rgb(150,150,150)'); +} + +Graph.prototype.draw60fps = function () { + this.drawHBar(1000/60, '60fps', '#00cf61', 25); +} + +Graph.prototype.draw30fps = function () { + this.drawHBar(1000/30, '30fps', '#cf0061', 25); +} + +Graph.prototype.drawAxisLabels = function (x_label, y_label) +{ + var ctx = this.ctx; + var { width, height } = ctx.canvas; + + ctx.fillText(x_label, width / 2, this.layout.xAxisLabel_Y); + + ctx.save(); + ctx.rotate(Math.PI/2); + var start = height / 2 - ctx.measureText(y_label).width / 2; + ctx.fillText(y_label, start, -width+20); + ctx.restore(); +} + +Graph.prototype.drawFrame = function () { + var ctx = this.ctx; + var { width, height } = ctx.canvas; + + // Draw frame to show size + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.fillStyle = 'rgb(0,0,0)'; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(width, 0); + ctx.lineTo(width, height); + ctx.lineTo(0, height); + ctx.closePath(); + ctx.stroke(); +} + +function LatencyGraph(ctx) { + Graph.call(this, ctx); + console.log(this.ctx); +} + +LatencyGraph.prototype = Object.create(Graph.prototype); + +Object.defineProperty(LatencyGraph.prototype, 'constructor', { + enumerable: false, + value: LatencyGraph }); + +LatencyGraph.prototype.ypos = function (delay) { + var { height } = this.ctx.canvas; + + var r = height + 100 - Math.log(delay) * 64; + if (r < 5) return 5; + return r; +} + +LatencyGraph.prototype.drawHBar = function (delay, label, color='rgb(0,0,0)', label_offset=0) +{ + var ctx = this.ctx; + + ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.fillText(label, this.xpos(numSamples) + 4 + label_offset, this.ypos(delay) + 3); + + ctx.beginPath(); + ctx.moveTo(this.xpos(0), this.ypos(delay)); + ctx.lineTo(this.xpos(numSamples) + label_offset, this.ypos(delay)); + ctx.stroke(); + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.fillStyle = 'rgb(0,0,0)'; +} + +LatencyGraph.prototype.draw = function () { + var ctx = this.ctx; + + this.clear(); + this.drawFrame(); + + for (var delay of [ 10, 20, 30, 50, 100, 200, 400, 800 ]) + this.drawScale(delay); + this.draw60fps(); + this.draw30fps(); + + var worst = 0, worstpos = 0; + ctx.beginPath(); + for (var i = 0; i < numSamples; i++) { + ctx.lineTo(this.xpos(i), this.ypos(delays[i])); + if (delays[i] >= worst) { + worst = delays[i]; + worstpos = i; + } + } + ctx.stroke(); + + // Draw vertical lines marking minor and major GCs + if (features.showingGCs) { + var { width, height } = ctx.canvas; + + ctx.strokeStyle = 'rgb(255,100,0)'; + var idx = sampleIndex % numSamples; + var gcCount = gcs[idx]; + for (var i = 0; i < numSamples; i++) { + idx = (sampleIndex + i) % numSamples; + if (gcCount < gcs[idx]) { + ctx.beginPath(); + ctx.moveTo(this.xpos(idx), 0); + ctx.lineTo(this.xpos(idx), this.layout.xAxisLabel_Y); + ctx.stroke(); + } + gcCount = gcs[idx]; + } + + ctx.strokeStyle = 'rgb(0,255,100)'; + idx = sampleIndex % numSamples; + gcCount = gcs[idx]; + for (var i = 0; i < numSamples; i++) { + idx = (sampleIndex + i) % numSamples; + if (gcCount < minorGCs[idx]) { + ctx.beginPath(); + ctx.moveTo(this.xpos(idx), 0); + ctx.lineTo(this.xpos(idx), 20); + ctx.stroke(); + } + gcCount = minorGCs[idx]; + } + } + + ctx.fillStyle = 'rgb(255,0,0)'; + if (worst) + ctx.fillText(`${worst.toFixed(2)}ms`, this.xpos(worstpos) - 10, this.ypos(worst) - 14); + + // Mark and label the slowest frame + ctx.beginPath(); + var where = sampleIndex % numSamples; + ctx.arc(this.xpos(where), this.ypos(delays[where]), 5, 0, Math.PI*2, true); + ctx.fill(); + ctx.fillStyle = 'rgb(0,0,0)'; + + this.drawAxisLabels('Time', 'Pause between frames (log scale)'); +} + +function MemoryGraph(ctx) { + Graph.call(this, ctx); + this.worstEver = this.bestEver = performance.mozMemory.zone.gcBytes; + this.limit = Math.max(this.worstEver, performance.mozMemory.zone.gcAllocTrigger); +} + +MemoryGraph.prototype = Object.create(Graph.prototype); + +Object.defineProperty(MemoryGraph.prototype, 'constructor', { + enumerable: false, + value: MemoryGraph }); + +MemoryGraph.prototype.ypos = function (size) { + var { height } = this.ctx.canvas; + + var range = this.limit - this.bestEver; + var percent = (size - this.bestEver) / range; + + return (1 - percent) * height * 0.9 + 20; +} + +MemoryGraph.prototype.drawHBar = function (size, label, color='rgb(150,150,150)') +{ + var ctx = this.ctx; + + var y = this.ypos(size); + + ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.fillText(label, this.xpos(numSamples) + 4, y + 3); + + ctx.beginPath(); + ctx.moveTo(this.xpos(0), y); + ctx.lineTo(this.xpos(numSamples), y); + ctx.stroke(); + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.fillStyle = 'rgb(0,0,0)'; +} + +function format_gcBytes(bytes) { + if (bytes < 4000) + return `${bytes} bytes`; + else if (bytes < 4e6) + return `${(bytes / 1024).toFixed(2)} KB`; + else if (bytes < 4e9) + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; + else + return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`; +}; + +MemoryGraph.prototype.draw = function () { + var ctx = this.ctx; + + this.clear(); + this.drawFrame(); + + var worst = 0, worstpos = 0; + for (var i = 0; i < numSamples; i++) { + if (gcBytes[i] >= worst) { + worst = gcBytes[i]; + worstpos = i; + } + if (gcBytes[i] < this.bestEver) { + this.bestEver = gcBytes[i]; + } + } + + if (this.worstEver < worst) { + this.worstEver = worst; + this.limit = Math.max(this.worstEver, performance.mozMemory.zone.gcAllocTrigger); + } + + this.drawHBar(this.bestEver, `${format_gcBytes(this.bestEver)} min`, '#00cf61'); + this.drawHBar(this.worstEver, `${format_gcBytes(this.worstEver)} max`, '#cc1111'); + this.drawHBar(performance.mozMemory.zone.gcAllocTrigger, `${format_gcBytes(performance.mozMemory.zone.gcAllocTrigger)} trigger`, '#cc11cc'); + + ctx.fillStyle = 'rgb(255,0,0)'; + if (worst) + ctx.fillText(format_gcBytes(worst), this.xpos(worstpos) - 10, this.ypos(worst) - 14); + + ctx.beginPath(); + var where = sampleIndex % numSamples; + ctx.arc(this.xpos(where), this.ypos(gcBytes[where]), 5, 0, Math.PI*2, true); + ctx.fill(); + + ctx.beginPath(); + for (var i = 0; i < numSamples; i++) { + if (i == (sampleIndex + 1) % numSamples) + ctx.moveTo(this.xpos(i), this.ypos(gcBytes[i])); + else + ctx.lineTo(this.xpos(i), this.ypos(gcBytes[i])); + if (i == where) + ctx.stroke(); + } + ctx.stroke(); + + this.drawAxisLabels('Time', 'Heap Memory Usage'); +} + +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(); + } +} + +var previous = 0; +function handler(timestamp) +{ + if (stopped) + return; + + if (testState === 'running' && (timestamp - testStart) > testDuration) + end_test(timestamp); + + if (testState == 'running') + document.getElementById("test-progress").textContent = ((testDuration - (timestamp - testStart))/1000).toFixed(1) + " sec"; + + activeTest.makeGarbage(activeTest.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++; + var idx = sampleIndex % numSamples; + delays[idx] = delay; + if (features.trackingSizes) + gcBytes[idx] = performance.mozMemory.gcBytes; + if (features.showingGCs) { + gcs[idx] = performance.mozMemory.gcNumber; + minorGCs[idx] = performance.mozMemory.minorGCCount; + } + } + + latencyGraph.draw(); + if (memoryGraph) + memoryGraph.draw(); + window.requestAnimationFrame(handler); +} + +function summarize(arr) { + if (arr.length == 0) + return []; + + var result = []; + var run_start = 0; + var prev = arr[0]; + for (var i = 1; i <= arr.length; i++) { + if (i == arr.length || arr[i] != prev) { + if (i == run_start + 1) { + result.push(arr[i]); + } else { + result.push(prev + " x " + (i - run_start)); + } + run_start = i; + } + if (i != arr.length) + prev = arr[i]; + } + + return result; +} + +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() +{ + if (activeTest) + 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. + change_active_test('noAllocation'); + + // Polyfill rAF. + var requestAnimationFrame = + window.requestAnimationFrame || window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + window.requestAnimationFrame = requestAnimationFrame; + + // Acquire our canvas. + var canvas = document.getElementById('graph'); + latencyGraph = new LatencyGraph(canvas.getContext('2d')); + + if (!performance.mozMemory) { + document.getElementById('memgraph-disabled').style.display = 'block'; + document.getElementById('track-sizes-div').style.display = 'none'; + } + + trackHeapSizes(document.getElementById('track-sizes').checked); + + // 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(); + + start_test(testQueue.shift()); + reset_draw_state(); +} + +function start_test(testName) +{ + change_active_test(testName); + console.log(`Running test: ${testName}`); + document.getElementById("test-selection").value = testName; +} + +function end_test(timestamp) +{ + document.getElementById("test-progress").textContent = "(not running)"; + report_test_result(activeTest, gHistogram); + gHistogram.clear(); + console.log(`Ending test ${activeTest.name}`); + if (testQueue.length) { + start_test(testQueue.shift()); + 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); + var params = `(${format_units(test.garbagePerFrame)},${format_units(test.garbageTotal)})`; + resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${test.name}${params} - ${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(activeTest.garbageTotal); +} + +function change_active_test(new_test_name) +{ + if (activeTest) + activeTest.unload(); + activeTest = tests.get(new_test_name); + + if (!activeTest.garbagePerFrame) + activeTest.garbagePerFrame = parse_units(activeTest.defaultGarbagePerFrame || globalDefaultGarbagePerFrame); + if (!activeTest.garbageTotal) + activeTest.garbageTotal = parse_units(activeTest.defaultGarbageTotal || globalDefaultGarbageTotal); + + document.getElementById("garbage-per-frame").value = format_units(activeTest.garbagePerFrame); + document.getElementById("garbage-total").value = format_units(activeTest.garbageTotal); + + activeTest.load(activeTest.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 test_changed() +{ + var select = document.getElementById("test-selection"); + console.log(`Switching to test: ${select.value}`); + change_active_test(select.value); + gHistogram.clear(); + reset_draw_state(); +} + +function parse_units(v) +{ + if (v.length == 0) + return NaN; + var lastChar = v[v.length - 1].toLowerCase(); + if (!isNaN(parseFloat(lastChar))) + return parseFloat(v); + var units = parseFloat(v.substr(0, v.length - 1)); + if (lastChar == "k") + return units * 1e3; + if (lastChar == "m") + return units * 1e6; + if (lastChar == "g") + return units * 1e9; + return NaN; +} + +function format_units(n) +{ + n = String(n); + if (n.length > 9 && n.substr(-9) == "000000000") + return n.substr(0, n.length - 9) + "G"; + else if (n.length > 9 && n.substr(-6) == "000000") + return n.substr(0, n.length - 6) + "M"; + else if (n.length > 3 && n.substr(-3) == "000") + return n.substr(0, n.length - 3) + "K"; + else + return String(n); +} + +function garbage_total_changed() +{ + var value = parse_units(document.getElementById('garbage-total').value); + if (isNaN(value)) + return; + if (activeTest) { + activeTest.garbageTotal = value; + console.log(`Updated garbage-total to ${activeTest.garbageTotal} items`); + reload_active_test(); + } + gHistogram.clear(); + reset_draw_state(); +} + +function garbage_per_frame_changed() +{ + var value = parse_units(document.getElementById('garbage-per-frame').value); + if (isNaN(value)) + return; + if (activeTest) { + activeTest.garbagePerFrame = value; + console.log(`Updated garbage-per-frame to ${activeTest.garbagePerFrame} items`); + } +} + +function trackHeapSizes(track) +{ + features.trackingSizes = track; + + var canvas = document.getElementById('memgraph'); + + if (features.trackingSizes) { + canvas.style.display = 'block'; + memoryGraph = new MemoryGraph(canvas.getContext('2d')); + } else { + canvas.style.display = 'none'; + memoryGraph = null; + } +} |