blob: 3073134f2a7b0fa2c8be083069e85615efed8bc0 [file] [log] [blame]
/*
* Copyright (C) 2015-2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
Utilities.extendObject(window.benchmarkController, {
updateGraphData: function(testResult, testData, options)
{
var element = document.getElementById("test-graph-data");
element.innerHTML = "";
element._testResult = testResult;
element._options = options;
var margins = new Insets(30, 30, 30, 40);
var size = Point.elementClientSize(element);
size.y = window.innerHeight - element.offsetTop;
size = size.subtract(margins.size);
// Convert from compact JSON output to propertied data
var samplesWithProperties = {};
[Strings.json.controller, Strings.json.complexity].forEach(function(seriesName) {
var series = testData[Strings.json.samples][seriesName];
samplesWithProperties[seriesName] = series.toArray();
})
this.createTimeGraph(testResult, samplesWithProperties[Strings.json.controller], testData[Strings.json.marks], testData[Strings.json.controller], options, margins, size);
this.onTimeGraphOptionsChanged();
this._showOrHideNodes(true, "form[name=graph-type]");
document.forms["graph-type"].elements["type"] = "complexity";
this.createComplexityGraph(testResult, testData[Strings.json.controller], samplesWithProperties, options, margins, size);
this.onComplexityGraphOptionsChanged();
this.onGraphTypeChanged();
},
_addRegressionLine: function(parent, xScale, yScale, points, range, isAlongYAxis)
{
var polygon = [];
var line = []
var xRange = isAlongYAxis ? range : 0;
var yRange = isAlongYAxis ? 0 : range;
for (var i = 0; i < points.length; ++i) {
var point = points[i];
var x;
if (xRange instanceof Array)
x = xRange[0];
else
x = point[0] + xRange;
polygon.push(xScale(x), yScale(point[1] + yRange));
line.push(xScale(point[0]), yScale(point[1]));
}
for (var i = points.length - 1; i >= 0; --i) {
var point = points[i];
var x;
if (xRange instanceof Array)
x = xRange[1];
else
x = point[0] - xRange;
polygon.push(xScale(x), yScale(point[1] - yRange));
}
parent.append("polygon")
.attr("points", polygon.join(","));
parent.append("line")
.attr("x1", line[0])
.attr("y1", line[1])
.attr("x2", line[2])
.attr("y2", line[3]);
},
_addRegression: function(data, svg, xScale, yScale)
{
svg.append("circle")
.attr("cx", xScale(data.segment1[1][0]))
.attr("cy", yScale(data.segment1[1][1]))
.attr("r", 3);
this._addRegressionLine(svg, xScale, yScale, data.segment1, data.stdev);
this._addRegressionLine(svg, xScale, yScale, data.segment2, data.stdev);
},
createComplexityGraph: function(result, timeRegressions, data, options, margins, size)
{
var svg = d3.select("#test-graph-data").append("svg")
.attr("id", "complexity-graph")
.attr("class", "hidden")
.attr("width", size.width + margins.left + margins.right)
.attr("height", size.height + margins.top + margins.bottom)
.append("g")
.attr("transform", "translate(" + margins.left + "," + margins.top + ")");
var timeSamples = data[Strings.json.controller];
var xMin = 100000, xMax = 0;
if (timeRegressions) {
timeRegressions.forEach(function(regression) {
for (var i = regression.startIndex; i <= regression.endIndex; ++i) {
xMin = Math.min(xMin, timeSamples[i].complexity);
xMax = Math.max(xMax, timeSamples[i].complexity);
}
});
} else {
xMin = d3.min(timeSamples, function(s) { return s.complexity; });
xMax = d3.max(timeSamples, function(s) { return s.complexity; });
}
var xScale = d3.scale.linear()
.range([0, size.width])
.domain([xMin, xMax]);
var yScale = d3.scale.linear()
.range([size.height, 0])
.domain([1000/20, 1000/60]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.tickValues([1000/20, 1000/25, 1000/30, 1000/35, 1000/40, 1000/45, 1000/50, 1000/55, 1000/60])
.tickFormat(function(d) { return (1000 / d).toFixed(0); })
.orient("left");
// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + size.height + ")")
.call(xAxis);
// y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// time result
var mean = svg.append("g")
.attr("class", "mean complexity");
var timeResult = result[Strings.json.controller];
var yMin = yScale.domain()[0], yMax = yScale.domain()[1];
this._addRegressionLine(mean, xScale, yScale, [[timeResult.average, yMin], [timeResult.average, yMax]], timeResult.stdev, true);
// regression
this._addRegression(result[Strings.json.complexity], svg.append("g").attr("class", "regression raw"), xScale, yScale);
var bootstrapResult = result[Strings.json.complexity][Strings.json.bootstrap];
if (bootstrapResult) {
var histogram = d3.layout.histogram()
.bins(xScale.ticks(100))(bootstrapResult.data);
var yBootstrapScale = d3.scale.linear()
.range([size.height/2, 0])
.domain([0, d3.max(histogram, function(d) { return d.y; })]);
group = svg.append("g").attr("class", "bootstrap");
var bar = group.selectAll(".bar")
.data(histogram)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + xScale(d.x) + "," + yBootstrapScale(d.y) + ")"; });
bar.append("rect")
.attr("x", 1)
.attr("y", size.height/2)
.attr("width", xScale(histogram[1].x) - xScale(histogram[0].x) - 1)
.attr("height", function(d) { return size.height/2 - yBootstrapScale(d.y); });
group = group.append("g").attr("class", "median");
this._addRegressionLine(group, xScale, yScale, [[bootstrapResult.median, yMin], [bootstrapResult.median, yMax]], [bootstrapResult.confidenceLow, bootstrapResult.confidenceHigh], true);
group.append("circle")
.attr("cx", xScale(bootstrapResult.median))
.attr("cy", yScale(1000/60))
.attr("r", 5);
}
// series
group = svg.append("g")
.attr("class", "series raw")
.selectAll("line")
.data(data[Strings.json.complexity])
.enter();
group.append("line")
.attr("x1", function(d) { return xScale(d.complexity) - 3; })
.attr("x2", function(d) { return xScale(d.complexity) + 3; })
.attr("y1", function(d) { return yScale(d.frameLength) - 3; })
.attr("y2", function(d) { return yScale(d.frameLength) + 3; });
group.append("line")
.attr("x1", function(d) { return xScale(d.complexity) - 3; })
.attr("x2", function(d) { return xScale(d.complexity) + 3; })
.attr("y1", function(d) { return yScale(d.frameLength) + 3; })
.attr("y2", function(d) { return yScale(d.frameLength) - 3; });
// Cursor
var cursorGroup = svg.append("g").attr("class", "cursor hidden");
cursorGroup.append("line")
.attr("class", "x")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", yScale(yAxis.scale().domain()[0]) + 10)
.attr("y2", yScale(yAxis.scale().domain()[1]));
cursorGroup.append("line")
.attr("class", "y")
.attr("x1", xScale(xAxis.scale().domain()[0]) - 10)
.attr("x2", xScale(xAxis.scale().domain()[1]))
.attr("y1", 0)
.attr("y2", 0)
cursorGroup.append("text")
.attr("class", "label x")
.attr("x", 0)
.attr("y", yScale(yAxis.scale().domain()[0]) + 15)
.attr("baseline-shift", "-100%")
.attr("text-anchor", "middle");
cursorGroup.append("text")
.attr("class", "label y")
.attr("x", xScale(xAxis.scale().domain()[0]) - 15)
.attr("y", 0)
.attr("baseline-shift", "-30%")
.attr("text-anchor", "end");
// Area to handle mouse events
var area = svg.append("rect")
.attr("fill", "transparent")
.attr("x", 0)
.attr("y", 0)
.attr("width", size.width)
.attr("height", size.height);
area.on("mouseover", function() {
document.querySelector("#complexity-graph .cursor").classList.remove("hidden");
}).on("mouseout", function() {
document.querySelector("#complexity-graph .cursor").classList.add("hidden");
}).on("mousemove", function() {
var location = d3.mouse(this);
var location_domain = [xScale.invert(location[0]), yScale.invert(location[1])];
cursorGroup.select("line.x")
.attr("x1", location[0])
.attr("x2", location[0]);
cursorGroup.select("text.x")
.attr("x", location[0])
.text(location_domain[0].toFixed(1));
cursorGroup.select("line.y")
.attr("y1", location[1])
.attr("y2", location[1]);
cursorGroup.select("text.y")
.attr("y", location[1])
.text((1000 / location_domain[1]).toFixed(1));
});
},
createTimeGraph: function(result, samples, marks, regressions, options, margins, size)
{
var svg = d3.select("#test-graph-data").append("svg")
.attr("id", "time-graph")
.attr("width", size.width + margins.left + margins.right)
.attr("height", size.height + margins.top + margins.bottom)
.append("g")
.attr("transform", "translate(" + margins.left + "," + margins.top + ")");
// Axis scales
var x = d3.scale.linear()
.range([0, size.width])
.domain([
Math.min(d3.min(samples, function(s) { return s.time; }), 0),
d3.max(samples, function(s) { return s.time; })]);
var complexityMax = d3.max(samples, function(s) {
if (s.time > 0)
return s.complexity;
return 0;
});
var yLeft = d3.scale.linear()
.range([size.height, 0])
.domain([0, complexityMax]);
var yRight = d3.scale.linear()
.range([size.height, 0])
.domain([1000/20, 1000/60]);
// Axes
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(function(d) { return (d/1000).toFixed(0); });
var yAxisLeft = d3.svg.axis()
.scale(yLeft)
.orient("left");
var yAxisRight = d3.svg.axis()
.scale(yRight)
.tickValues([1000/20, 1000/25, 1000/30, 1000/35, 1000/40, 1000/45, 1000/50, 1000/55, 1000/60])
.tickFormat(function(d) { return (1000/d).toFixed(0); })
.orient("right");
// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("fill", "rgb(235, 235, 235)")
.attr("transform", "translate(0," + size.height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", size.width)
.attr("y", -6)
.attr("fill", "rgb(235, 235, 235)")
.style("text-anchor", "end")
.text("time");
// yLeft-axis
svg.append("g")
.attr("class", "yLeft axis")
.attr("fill", "#7ADD49")
.call(yAxisLeft)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("fill", "#7ADD49")
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(Strings.text.complexity);
// yRight-axis
svg.append("g")
.attr("class", "yRight axis")
.attr("fill", "#FA4925")
.attr("transform", "translate(" + size.width + ", 0)")
.call(yAxisRight)
.append("text")
.attr("class", "label")
.attr("x", 9)
.attr("y", -20)
.attr("fill", "#FA4925")
.attr("dy", ".71em")
.style("text-anchor", "start")
.text(Strings.text.frameRate);
// marks
var yMin = yRight(yAxisRight.scale().domain()[0]);
var yMax = yRight(yAxisRight.scale().domain()[1]);
for (var markName in marks) {
var mark = marks[markName];
var xLocation = x(mark.time);
var markerGroup = svg.append("g")
.attr("class", "marker")
.attr("transform", "translate(" + xLocation + ", 0)");
markerGroup.append("text")
.attr("transform", "translate(10, " + (yMin - 10) + ") rotate(-90)")
.style("text-anchor", "start")
.text(markName)
markerGroup.append("line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", yMin)
.attr("y2", yMax);
}
if (Strings.json.controller in result) {
var complexity = result[Strings.json.controller];
var regression = svg.append("g")
.attr("class", "complexity mean");
this._addRegressionLine(regression, x, yLeft, [[samples[0].time, complexity.average], [samples[samples.length - 1].time, complexity.average]], complexity.stdev);
}
if (Strings.json.frameLength in result) {
var frameLength = result[Strings.json.frameLength];
var regression = svg.append("g")
.attr("class", "fps mean");
this._addRegressionLine(regression, x, yRight, [[samples[0].time, 1000/frameLength.average], [samples[samples.length - 1].time, 1000/frameLength.average]], frameLength.stdev);
}
// right-target
if (options["controller"] == "adaptive") {
var targetFrameLength = 1000 / options["frame-rate"];
svg.append("line")
.attr("x1", x(0))
.attr("x2", size.width)
.attr("y1", yRight(targetFrameLength))
.attr("y2", yRight(targetFrameLength))
.attr("class", "target-fps marker");
}
// Cursor
var cursorGroup = svg.append("g").attr("class", "cursor");
cursorGroup.append("line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", yMin)
.attr("y2", yMin);
// Data
var allData = samples;
var filteredData = samples.filter(function (sample) {
return "smoothedFrameLength" in sample;
});
function addData(name, data, yCoordinateCallback, pointRadius, omitLine) {
var svgGroup = svg.append("g").attr("id", name);
if (!omitLine) {
svgGroup.append("path")
.datum(data)
.attr("d", d3.svg.line()
.x(function(d) { return x(d.time); })
.y(yCoordinateCallback));
}
svgGroup.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.time); })
.attr("cy", yCoordinateCallback)
.attr("r", pointRadius);
cursorGroup.append("circle")
.attr("class", name)
.attr("r", pointRadius + 2);
}
addData("complexity", allData, function(d) { return yLeft(d.complexity); }, 2);
addData("rawFPS", allData, function(d) { return yRight(d.frameLength); }, 1);
addData("filteredFPS", filteredData, function(d) { return yRight(d.smoothedFrameLength); }, 2);
// regressions
var regressionGroup = svg.append("g")
.attr("id", "regressions");
if (regressions) {
var complexities = [];
regressions.forEach(function (regression) {
if (!isNaN(regression.segment1[0][1]) && !isNaN(regression.segment1[1][1])) {
regressionGroup.append("line")
.attr("x1", x(regression.segment1[0][0]))
.attr("x2", x(regression.segment1[1][0]))
.attr("y1", yRight(regression.segment1[0][1]))
.attr("y2", yRight(regression.segment1[1][1]));
}
if (!isNaN(regression.segment2[0][1]) && !isNaN(regression.segment2[1][1])) {
regressionGroup.append("line")
.attr("x1", x(regression.segment2[0][0]))
.attr("x2", x(regression.segment2[1][0]))
.attr("y1", yRight(regression.segment2[0][1]))
.attr("y2", yRight(regression.segment2[1][1]));
}
// inflection point
regressionGroup.append("circle")
.attr("cx", x(regression.segment2[0][0]))
.attr("cy", yRight(regression.segment2[0][1]))
.attr("r", 3);
regressionGroup.append("line")
.attr("class", "association")
.attr("stroke-dasharray", "5, 3")
.attr("x1", x(regression.segment2[0][0]))
.attr("x2", x(regression.segment2[0][0]))
.attr("y1", yRight(regression.segment2[0][1]))
.attr("y2", yLeft(regression.complexity));
regressionGroup.append("circle")
.attr("cx", x(regression.segment1[1][0]))
.attr("cy", yLeft(regression.complexity))
.attr("r", 5);
complexities.push(regression.complexity);
});
if (complexities.length) {
var yLeftComplexities = d3.svg.axis()
.scale(yLeft)
.tickValues(complexities)
.tickSize(10)
.orient("left");
svg.append("g")
.attr("class", "complexity yLeft axis")
.call(yLeftComplexities);
}
}
// Area to handle mouse events
var area = svg.append("rect")
.attr("fill", "transparent")
.attr("x", 0)
.attr("y", 0)
.attr("width", size.width)
.attr("height", size.height);
var timeBisect = d3.bisector(function(d) { return d.time; }).right;
var statsToHighlight = ["complexity", "rawFPS", "filteredFPS"];
area.on("mouseover", function() {
document.querySelector("#time-graph .cursor").classList.remove("hidden");
document.querySelector("#test-graph nav").classList.remove("hide-data");
}).on("mouseout", function() {
document.querySelector("#time-graph .cursor").classList.add("hidden");
document.querySelector("#test-graph nav").classList.add("hide-data");
}).on("mousemove", function() {
var form = document.forms["time-graph-options"].elements;
var mx_domain = x.invert(d3.mouse(this)[0]);
var index = Math.min(timeBisect(allData, mx_domain), allData.length - 1);
var data = allData[index];
var cursor_x = x(data.time);
var cursor_y = yAxisRight.scale().domain()[1];
var ys = [yRight(yAxisRight.scale().domain()[0]), yRight(yAxisRight.scale().domain()[1])];
document.querySelector("#test-graph nav .time").textContent = (data.time / 1000).toFixed(4) + "s (" + index + ")";
statsToHighlight.forEach(function(name) {
var element = document.querySelector("#test-graph nav ." + name);
var content = "";
var data_y = null;
switch (name) {
case "complexity":
content = data.complexity;
data_y = yLeft(data.complexity);
break;
case "rawFPS":
content = (1000/data.frameLength).toFixed(2);
data_y = yRight(data.frameLength);
break;
case "filteredFPS":
if ("smoothedFrameLength" in data) {
content = (1000/data.smoothedFrameLength).toFixed(2);
data_y = yRight(data.smoothedFrameLength);
}
break;
}
element.textContent = content;
if (form[name].checked && data_y !== null) {
ys.push(data_y);
cursorGroup.select("." + name)
.attr("cx", cursor_x)
.attr("cy", data_y);
document.querySelector("#time-graph .cursor ." + name).classList.remove("hidden");
} else
document.querySelector("#time-graph .cursor ." + name).classList.add("hidden");
});
if (form["rawFPS"].checked)
cursor_y = Math.max(cursor_y, data.frameLength);
cursorGroup.select("line")
.attr("x1", cursor_x)
.attr("x2", cursor_x)
.attr("y1", Math.min.apply(null, ys))
.attr("y2", Math.max.apply(null, ys));
});
},
_showOrHideNodes: function(isShown, selector) {
var nodeList = document.querySelectorAll(selector);
if (isShown) {
for (var i = 0; i < nodeList.length; ++i)
nodeList[i].classList.remove("hidden");
} else {
for (var i = 0; i < nodeList.length; ++i)
nodeList[i].classList.add("hidden");
}
},
onComplexityGraphOptionsChanged: function() {
var form = document.forms["complexity-graph-options"].elements;
benchmarkController._showOrHideNodes(form["series-raw"].checked, "#complexity-graph .series.raw");
benchmarkController._showOrHideNodes(form["regression-time-score"].checked, "#complexity-graph .mean.complexity");
benchmarkController._showOrHideNodes(form["bootstrap-score"].checked, "#complexity-graph .bootstrap");
benchmarkController._showOrHideNodes(form["complexity-regression-aggregate-raw"].checked, "#complexity-graph .regression.raw");
},
onTimeGraphOptionsChanged: function() {
var form = document.forms["time-graph-options"].elements;
benchmarkController._showOrHideNodes(form["markers"].checked, ".marker");
benchmarkController._showOrHideNodes(form["averages"].checked, "#test-graph-data .mean");
benchmarkController._showOrHideNodes(form["complexity"].checked, "#complexity");
benchmarkController._showOrHideNodes(form["rawFPS"].checked, "#rawFPS");
benchmarkController._showOrHideNodes(form["filteredFPS"].checked, "#filteredFPS");
benchmarkController._showOrHideNodes(form["regressions"].checked, "#regressions");
},
onGraphTypeChanged: function() {
var form = document.forms["graph-type"].elements;
var testResult = document.getElementById("test-graph-data")._testResult;
var isTimeSelected = form["graph-type"].value == "time";
benchmarkController._showOrHideNodes(isTimeSelected, "#time-graph");
benchmarkController._showOrHideNodes(isTimeSelected, "form[name=time-graph-options]");
benchmarkController._showOrHideNodes(!isTimeSelected, "#complexity-graph");
benchmarkController._showOrHideNodes(!isTimeSelected, "form[name=complexity-graph-options]");
var score = "", mean = "";
if (isTimeSelected) {
score = testResult[Strings.json.score].toFixed(2);
var regression = testResult[Strings.json.controller];
mean = [
"mean: ",
regression.average.toFixed(2),
" ± ",
regression.stdev.toFixed(2),
" (",
regression.percent.toFixed(2),
"%)"];
if (regression.concern) {
mean = mean.concat([
", worst 5%: ",
regression.concern.toFixed(2)]);
}
mean = mean.join("");
} else {
var complexityRegression = testResult[Strings.json.complexity];
document.getElementById("complexity-regression-aggregate-raw").textContent = complexityRegression.complexity.toFixed(2) + ", ±" + complexityRegression.stdev.toFixed(2) + "ms";
var bootstrap = complexityRegression[Strings.json.bootstrap];
if (bootstrap) {
score = bootstrap.median.toFixed(2);
mean = [
(bootstrap.confidencePercentage * 100).toFixed(0),
"% CI: ",
bootstrap.confidenceLow.toFixed(2),
"–",
bootstrap.confidenceHigh.toFixed(2)
].join("");
}
}
sectionsManager.setSectionScore("test-graph", score, mean);
}
});