diff options
Diffstat (limited to 'js/src/devtools/gc-ubench/ui.js')
-rw-r--r-- | js/src/devtools/gc-ubench/ui.js | 700 |
1 files changed, 700 insertions, 0 deletions
diff --git a/js/src/devtools/gc-ubench/ui.js b/js/src/devtools/gc-ubench/ui.js new file mode 100644 index 0000000000..4905f97904 --- /dev/null +++ b/js/src/devtools/gc-ubench/ui.js @@ -0,0 +1,700 @@ +/* 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/. */ + +var stroke = { + gcslice: "rgb(255,100,0)", + minor: "rgb(0,255,100)", + initialMajor: "rgb(180,60,255)", +}; + +var numSamples = 500; + +var gHistogram = new Map(); // {ms: count} +var gHistory = new FrameHistory(numSamples); +var gPerf = new PerfTracker(); + +var latencyGraph; +var memoryGraph; +var ctx; +var memoryCtx; + +var loadState = "(init)"; // One of '(active)', '(inactive)', '(N/A)' +var testState = "idle"; // One of 'idle' or 'running'. +var enabled = { trackingSizes: false }; + +var gMemory = performance.mozMemory?.gc || performance.mozMemory || {}; + +var Firefox = class extends Host { + start_turn() { + // Handled by Gecko. + } + + end_turn() { + // Handled by Gecko. + } + + suspend(duration) { + // Not used; requestAnimationFrame takes its place. + throw new Error("unimplemented"); + } + + get minorGCCount() { + return gMemory.minorGCCount; + } + get majorGCCount() { + return gMemory.majorGCCount; + } + get GCSliceCount() { + return gMemory.sliceCount; + } + get gcBytes() { + return gMemory.zone.gcBytes; + } + get gcAllocTrigger() { + return gMemory.zone.gcAllocTrigger; + } + + features = { + haveMemorySizes: 'gcBytes' in gMemory, + haveGCCounts: 'majorGCCount' in gMemory, + }; +}; + +var gHost = new Firefox(); + +function parse_units(v) { + if (!v.length) { + 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; +} + +var Graph = class { + constructor(ctx) { + this.ctx = ctx; + + var { height } = ctx.canvas; + this.layout = { + xAxisLabel_Y: height - 20, + }; + } + + xpos(index) { + return index * 2; + } + + clear() { + const { width, height } = this.ctx.canvas; + this.ctx.clearRect(0, 0, width, height); + } + + drawScale(delay) { + this.drawHBar(delay, `${delay}ms`, "rgb(150,150,150)"); + } + + draw60fps() { + this.drawHBar(1000 / 60, "60fps", "#00cf61", 25); + } + + draw30fps() { + this.drawHBar(1000 / 30, "30fps", "#cf0061", 25); + } + + drawAxisLabels(x_label, y_label) { + const ctx = this.ctx; + const { 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(); + } + + drawFrame() { + const ctx = this.ctx; + const { 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(); + } +}; + +var LatencyGraph = class extends Graph { + constructor(ctx) { + super(ctx); + console.log(this.ctx); + } + + ypos(delay) { + const { height } = this.ctx.canvas; + + const r = height + 100 - Math.log(delay) * 64; + if (r < 5) { + return 5; + } + return r; + } + + drawHBar(delay, label, color = "rgb(0,0,0)", label_offset = 0) { + const 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)"; + } + + draw() { + const 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 (let i = 0; i < numSamples; i++) { + ctx.lineTo(this.xpos(i), this.ypos(gHistory.delays[i])); + if (gHistory.delays[i] >= worst) { + worst = gHistory.delays[i]; + worstpos = i; + } + } + ctx.stroke(); + + // Draw vertical lines marking minor and major GCs + if (gHost.features.haveGCCounts) { + ctx.strokeStyle = stroke.gcslice; + let idx = sampleIndex % numSamples; + const count = { + major: gHistory.majorGCs[idx], + minor: 0, + slice: gHistory.slices[idx], + }; + for (let i = 0; i < numSamples; i++) { + idx = (sampleIndex + i) % numSamples; + const isMajorStart = count.major < gHistory.majorGCs[idx]; + if (count.slice < gHistory.slices[idx]) { + if (isMajorStart) { + ctx.strokeStyle = stroke.initialMajor; + } + ctx.beginPath(); + ctx.moveTo(this.xpos(idx), 0); + ctx.lineTo(this.xpos(idx), this.layout.xAxisLabel_Y); + ctx.stroke(); + if (isMajorStart) { + ctx.strokeStyle = stroke.gcslice; + } + } + count.major = gHistory.majorGCs[idx]; + count.slice = gHistory.slices[idx]; + } + + ctx.strokeStyle = stroke.minor; + idx = sampleIndex % numSamples; + count.minor = gHistory.minorGCs[idx]; + for (let i = 0; i < numSamples; i++) { + idx = (sampleIndex + i) % numSamples; + if (count.minor < gHistory.minorGCs[idx]) { + ctx.beginPath(); + ctx.moveTo(this.xpos(idx), 0); + ctx.lineTo(this.xpos(idx), 20); + ctx.stroke(); + } + count.minor = gHistory.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(gHistory.delays[where]), + 5, + 0, + Math.PI * 2, + true + ); + ctx.fill(); + ctx.fillStyle = "rgb(0,0,0)"; + + this.drawAxisLabels("Time", "Pause between frames (log scale)"); + } +}; + +var MemoryGraph = class extends Graph { + constructor(ctx) { + super(ctx); + this.worstEver = this.bestEver = gHost.gcBytes; + this.limit = Math.max(this.worstEver, gHost.gcAllocTrigger); + } + + ypos(size) { + const { height } = this.ctx.canvas; + + const range = this.limit - this.bestEver; + const percent = (size - this.bestEver) / range; + + return (1 - percent) * height * 0.9 + 20; + } + + drawHBar(size, label, color = "rgb(150,150,150)") { + const ctx = this.ctx; + + const 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)"; + } + + draw() { + const ctx = this.ctx; + + this.clear(); + this.drawFrame(); + + var worst = 0, + worstpos = 0; + for (let i = 0; i < numSamples; i++) { + if (gHistory.gcBytes[i] >= worst) { + worst = gHistory.gcBytes[i]; + worstpos = i; + } + if (gHistory.gcBytes[i] < this.bestEver) { + this.bestEver = gHistory.gcBytes[i]; + } + } + + if (this.worstEver < worst) { + this.worstEver = worst; + this.limit = Math.max(this.worstEver, gHost.gcAllocTrigger); + } + + this.drawHBar( + this.bestEver, + `${format_bytes(this.bestEver)} min`, + "#00cf61" + ); + this.drawHBar( + this.worstEver, + `${format_bytes(this.worstEver)} max`, + "#cc1111" + ); + this.drawHBar( + gHost.gcAllocTrigger, + `${format_bytes(gHost.gcAllocTrigger)} trigger`, + "#cc11cc" + ); + + ctx.fillStyle = "rgb(255,0,0)"; + if (worst) { + ctx.fillText( + format_bytes(worst), + this.xpos(worstpos) - 10, + this.ypos(worst) - 14 + ); + } + + ctx.beginPath(); + var where = sampleIndex % numSamples; + ctx.arc( + this.xpos(where), + this.ypos(gHistory.gcBytes[where]), + 5, + 0, + Math.PI * 2, + true + ); + ctx.fill(); + + ctx.beginPath(); + for (let i = 0; i < numSamples; i++) { + if (i == (sampleIndex + 1) % numSamples) { + ctx.moveTo(this.xpos(i), this.ypos(gHistory.gcBytes[i])); + } else { + ctx.lineTo(this.xpos(i), this.ypos(gHistory.gcBytes[i])); + } + if (i == where) { + ctx.stroke(); + } + } + ctx.stroke(); + + this.drawAxisLabels("Time", "Heap Memory Usage"); + } +}; + +function onUpdateDisplayChanged() { + const do_graph = document.getElementById("do-graph"); + if (do_graph.checked) { + window.requestAnimationFrame(handler); + gHistory.resume(); + } else { + gHistory.pause(); + } + update_load_state_indicator(); +} + +function onDoLoadChange() { + const do_load = document.getElementById("do-load"); + gLoadMgr.paused = !do_load.checked; + console.log(`load paused: ${gLoadMgr.paused}`); + update_load_state_indicator(); +} + +var previous = 0; +function handler(timestamp) { + if (gHistory.is_stopped()) { + return; + } + + const completed = gLoadMgr.tick(timestamp); + if (completed) { + end_test(timestamp, gLoadMgr.lastActive); + if (!gLoadMgr.stopped()) { + start_test(); + } + update_load_display(); + } + + if (testState == "running") { + document.getElementById("test-progress").textContent = + (gLoadMgr.currentLoadRemaining(timestamp) / 1000).toFixed(1) + " sec"; + } + + const delay = gHistory.on_frame(timestamp); + + update_histogram(gHistogram, delay); + + latencyGraph.draw(); + if (memoryGraph) { + memoryGraph.draw(); + } + window.requestAnimationFrame(handler); +} + +// For interactive debugging. +// +// ['a', 'b', 'b', 'b', 'c', 'c'] => ['a', 'b x 3', 'c x 2'] +function summarize(arr) { + if (!arr.length) { + return []; + } + + var result = []; + var run_start = 0; + var prev = arr[0]; + for (let 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 reset_draw_state() { + gHistory.reset(); +} + +function onunload() { + gLoadMgr.deactivateLoad(); +} + +function onload() { + // The order of `tests` is currently based on their asynchronous load + // order, rather than the listed order. Rearrange by extracting the test + // names from their filenames, which is kind of gross. + _tests = tests; + tests = new Map(); + foreach_test_file(fn => { + // "benchmarks/foo.js" => "foo" + const name = fn.split(/\//)[1].split(/\./)[0]; + tests.set(name, _tests.get(name)); + }); + _tests = undefined; + + gLoadMgr = new AllocationLoadManager(tests); + + // Load initial test duration. + duration_changed(); + + // Load initial garbage size. + garbage_piles_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. + gLoadMgr.setActiveLoad(gLoadMgr.getByName("noAllocation")); + update_load_display(); + document.getElementById("test-selection").value = "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 (!gHost.features.haveMemorySizes) { + document.getElementById("memgraph-disabled").style.display = "block"; + document.getElementById("track-sizes-div").style.display = "none"; + } + + trackHeapSizes(document.getElementById("track-sizes").checked); + + update_load_state_indicator(); + gHistory.start(); + + // Start drawing. + reset_draw_state(); + window.requestAnimationFrame(handler); +} + +function run_one_test() { + start_test_cycle([gLoadMgr.activeLoad().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. + const duration = gLoadMgr.testDurationMS / 1000; + const mutators = tests_to_run.map(name => new SingleMutatorSequencer(gLoadMgr.getByName(name), gPerf, duration)); + const sequencer = new ChainSequencer(mutators); + gLoadMgr.startSequencer(sequencer); + testState = "running"; + gHistogram.clear(); + reset_draw_state(); +} + +function update_load_state_indicator() { + if ( + !gLoadMgr.load_running() || + gLoadMgr.activeLoad().name == "noAllocation" + ) { + loadState = "(none)"; + } else if (gHistory.is_stopped() || gLoadMgr.paused) { + loadState = "(inactive)"; + } else { + loadState = "(active)"; + } + document.getElementById("load-running").textContent = loadState; +} + +function start_test() { + console.log(`Running test: ${gLoadMgr.activeLoad().name}`); + document.getElementById("test-selection").value = gLoadMgr.activeLoad().name; + update_load_state_indicator(); +} + +function end_test(timestamp, load) { + document.getElementById("test-progress").textContent = "(not running)"; + report_test_result(load, gHistogram); + gHistogram.clear(); + console.log(`Ending test ${load.name}`); + if (gLoadMgr.stopped()) { + testState = "idle"; + } + update_load_state_indicator(); + reset_draw_state(); +} + +function compute_test_spark_histogram(histogram) { + const percents = compute_spark_histogram_percents(histogram); + + var sparks = "▁▂▃▄▅▆▇█"; + var colors = [ + "#aaaa00", + "#007700", + "#dd0000", + "#ff0000", + "#ff0000", + "#ff0000", + "#ff0000", + "#ff0000", + ]; + var line = ""; + for (let i = 0; i < percents.length; ++i) { + var spark = sparks.charAt(parseInt(percents[i] * sparks.length)); + line += `<span style="color:${colors[i]}">${spark}</span>`; + } + return line; +} + +function report_test_result(load, 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_num(load.garbagePerFrame)},${format_num( + load.garbagePiles + )})`; + resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${ + load.name + }${params} - ${load.description}`; + resultList.appendChild(resultElem); +} + +function update_load_display() { + const garbage = gLoadMgr.activeLoad() + ? gLoadMgr.activeLoad().garbagePerFrame + : parse_units(gDefaultGarbagePerFrame); + document.getElementById("garbage-per-frame").value = format_num(garbage); + const piles = gLoadMgr.activeLoad() + ? gLoadMgr.activeLoad().garbagePiles + : parse_units(gDefaultGarbagePiles); + document.getElementById("garbage-piles").value = format_num(piles); + update_load_state_indicator(); +} + +function duration_changed() { + var durationInput = document.getElementById("test-duration"); + gLoadMgr.testDurationMS = parseInt(durationInput.value) * 1000; + console.log( + `Updated test duration to: ${gLoadMgr.testDurationMS / 1000} seconds` + ); +} + +function onLoadChange() { + var select = document.getElementById("test-selection"); + console.log(`Switching to test: ${select.value}`); + gLoadMgr.setActiveLoad(gLoadMgr.getByName(select.value)); + update_load_display(); + gHistogram.clear(); + reset_draw_state(); +} + +function garbage_piles_changed() { + const input = document.getElementById("garbage-piles"); + const value = parse_units(input.value); + if (isNaN(value)) { + update_load_display(); + return; + } + + if (gLoadMgr.load_running()) { + gLoadMgr.change_garbagePiles(value); + console.log( + `Updated garbage-piles to ${gLoadMgr.activeLoad().garbagePiles} items` + ); + } + gHistogram.clear(); + reset_draw_state(); +} + +function garbage_per_frame_changed() { + const input = document.getElementById("garbage-per-frame"); + var value = parse_units(input.value); + if (isNaN(value)) { + update_load_display(); + return; + } + if (gLoadMgr.load_running()) { + gLoadMgr.change_garbagePerFrame = value; + console.log( + `Updated garbage-per-frame to ${ + gLoadMgr.activeLoad().garbagePerFrame + } items` + ); + } +} + +function trackHeapSizes(track) { + enabled.trackingSizes = track && gHost.features.haveMemorySizes; + + var canvas = document.getElementById("memgraph"); + + if (enabled.trackingSizes) { + canvas.style.display = "block"; + memoryGraph = new MemoryGraph(canvas.getContext("2d")); + } else { + canvas.style.display = "none"; + memoryGraph = null; + } +} |