blob: 7cf2a25811df53676d9c5045625bd397ed2a7f7b [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ==========//
// Constants //
// ==========//
var CALL_TYPE = {
PATH : "PATH",
IMAGE : "IMAGE",
GET_DATA : "GET_DATA",
PUT_DATA : "PUT_DATA"
}
var SHAPE_TYPE = {
STROKE_PATH : "STROKE_PATH",
FILL_CONVEX_PATH : "FILL_CONVEX_PATH",
FILL_NON_CONVEX_PATH : "FILL_NON_CONVEX_PATH",
STROKE_RECT : "STROKE_RECT",
FILL_RECT : "FILL_RECT",
STROKE_TEXT : "STROKE_TEXT",
FILL_TEXT : "FILL_TEXT"
}
var SHAPE_STYLE = {
COLOR : "COLOR",
LINEAR_GRADIENT : "LINEAR_GRADIENT",
RADIAL_GRADIENT: "RADIAL_GRADIENT",
PATTERN : "PATTERN"
}
var IMAGE_TYPE = {
DRAW_SVG_IMAGE : "DRAW_SVG_IMAGE",
DRAW_PNG_IMAGE : "DRAW_PNG_IMAGE"
}
var NUMBER_OF_FRAME_PER_ESTIMATE = 3;
var NUMBER_OF_BLANK_FRAMES_BETWEEN_MEASUREMENTS = 5;
// The max frames per second (fps) should be less than 60 because the rendering
// speed should be the limiting factor, not screen refresh rate.
var MAX_FRAMES_PER_SECOND_FOR_MEASUREMENTS = 30;
var MIN_FRAMES_PER_SECOND_FOR_MEASUREMENTS = 10;
var TARGET_FPS = 20;
var TARGET_TPF = 1000 / TARGET_FPS;
// ================//
// State Variables //
// ================//
var recording_helper = new RecordingHelper();
recording_helper.restartRecordingTime();
// list of list of DrawParameter's to describe the each frame to draw
var list_of_frame_parameters = [];
// list of PerformanceMeasurement
var performance_measurements = [];
var frame_index = 0;
var blank_counter = 0;
// =============================//
// Main rendering loop function //
// =============================//
function drawAndRecord(context) {
if (frame_index >= list_of_frame_parameters.length) return;
if (blank_counter > 0) {
recording_helper.restartRecordingTime();
blank_counter--;
return;
}
var newMeasurement = false;
if (blank_counter < 0) {
if (frame_index == 0
&& recording_helper.getNumberOfFramesRecorded() == 0) {
newMeasurement = true;
} else {
var tpf = recording_helper.getAverageTimePerFrame();
var fps = recording_helper.getAverageFramesPerSecond();
if (fps > MAX_FRAMES_PER_SECOND_FOR_MEASUREMENTS) {
// too fast, increase quantity and try again
var new_quantity = quantityForTarget(
list_of_frame_parameters[frame_index][0].quantity,
tpf, TARGET_TPF);
list_of_frame_parameters[frame_index][0].quantity = new_quantity;
// reset total bounding box area values
for (var i = 0; i<list_of_frame_parameters[frame_index].length; i++) {
list_of_frame_parameters[frame_index][i].resetBoundingBoxArea();
}
newMeasurement = true;
} else if (fps < MIN_FRAMES_PER_SECOND_FOR_MEASUREMENTS) {
// too slow, move on to the next measurement
console.log("Too slow - moving on to next measurement.");
frame_index++;
newMeasurement = true;
}
if (recording_helper.getNumberOfFramesRecorded() >=
NUMBER_OF_FRAME_PER_ESTIMATE && newMeasurement == false) {
if (fps < MAX_FRAMES_PER_SECOND_FOR_MEASUREMENTS) {
// Redering speed, not refresh rate is the limiting factor
console.log("Recording: frame_index = " +
frame_index + ",\n\ttfp = "+ tpf +
"\n\tfps = " + fps);
performance_measurements.push(
new PerformanceMeasurement(
tpf, fps,
list_of_frame_parameters[frame_index]));
}
frame_index++;
newMeasurement = true;
}
}
if (newMeasurement) {
recording_helper.restartRecordingTime();
blank_counter = NUMBER_OF_BLANK_FRAMES_BETWEEN_MEASUREMENTS;
if (frame_index >= list_of_frame_parameters.length) {
console.log(performance_measurements);
drawCSVString(getCSVString(performance_measurements));
return;
}
}
}
if (blank_counter == 0) {
recording_helper.restartRecordingTime();
blank_counter--;
}
DrawParameters(context, list_of_frame_parameters[frame_index]);
recording_helper.recordFrame();
}
// =====================//
// Data storage classes //
// =====================//
function DrawSize(width, height) {
this.width = Math.round(width);
this.height = Math.round(height);
}
function DrawParameter(
call_type, draw_sub_type, size, shadow_blur, quantity, fill_style) {
this.call_type = call_type; // path, image, put or get image data
this.draw_sub_type = draw_sub_type; // image type or path type
this.fill_style = fill_style; // for paths only
this.shadow_blur = shadow_blur;
// Target size of side of square bounding box. Actuall size can differ.
// total_bounding_box_area is returned by draw call and represents actuall
// area of the call.
this.size = size;
this.total_bounding_box_area = 0; // computed elsewhere
this.total_shadow_bounding_box_area = 0; // computed elsewhere
this.total_bounding_box_perimeter = 0; // computed elsewhere
this.resetBoundingBoxArea = function() {
this.total_bounding_box_area = 0;
this.total_shadow_bounding_box_area = 0;
this.total_bounding_box_perimeter = 0;
}
this.quantity = quantity;
}
function PerformanceMeasurement(tpf, fps, parameters) {
this.tpf = tpf; // time per frame
this.fps = fps; // frames per second
this.parameters = parameters; // DrawParameter object
}
// ===========================================================//
// Utility functions for generating lists of frame parameters //
// ===========================================================//
function generatePathFrameParameters() {
var path_sizes = [20, 50, 200, 300, 400];
var quantities = [10, 20, 50, 100, 500, 1000, 2000];
var shadow_blurs = [0, 1, 10, 20];
// Only draw combinations that contain shadows with this probability
// to reduce the number of permutations.
var shadowedPermutationProbability = 0.1;
var list_of_frame_parameters = [];
for (shape_type in SHAPE_TYPE) {
for (var size_i = 0; size_i < path_sizes.length; size_i++) {
for (var qty_i = 0; qty_i < quantities.length; qty_i++) {
for (var sh_i = 0; sh_i < shadow_blurs.length; sh_i++) {
for (fill_style in SHAPE_STYLE) {
if (shadow_blurs[sh_i] == 0
|| getTrueWithProbability(shadowedPermutationProbability)) {
list_of_frame_parameters.push([
new DrawParameter(CALL_TYPE.PATH, shape_type,
path_sizes[size_i], shadow_blurs[sh_i],
quantities[qty_i], fill_style)]);
}
}
}
}
}
}
return list_of_frame_parameters;
}
function generateImageFrameParameters() {
var png_image_sizes = [5, 10, 20, 50, 90];
var svg_image_sizes = [5, 20, 50, 90, 200, 300];
var png_quantities = [10, 50, 100, 500, 1000];
var svg_quantities = [10, 50, 100, 200, 500];
var shadow_blurs = [0, 1, 5, 10];
// Only draw combinations that contain shadows with this probability
// to reduce the number of permutations.
var shadowedPermutationProbability = 0.1;
var list_of_frame_parameters = [];
for (var size_i = 0; size_i < png_image_sizes.length; size_i++) {
for (var qty_i = 0; qty_i < png_quantities.length; qty_i++) {
for (var sh_i = 0; sh_i < shadow_blurs.length; sh_i++) {
if (shadow_blurs[sh_i] == 0
|| getTrueWithProbability(shadowedPermutationProbability)) {
list_of_frame_parameters.push([
new DrawParameter(CALL_TYPE.IMAGE,
IMAGE_TYPE.DRAW_PNG_IMAGE,
png_image_sizes[size_i],
shadow_blurs[sh_i],
png_quantities[qty_i], "")]);
}
}
}
}
for (var size_i = 0; size_i < svg_image_sizes.length; size_i++) {
for (var qty_i = 0; qty_i < svg_quantities.length; qty_i++) {
for (var sh_i = 0; sh_i < shadow_blurs.length; sh_i++) {
if (shadow_blurs[sh_i] == 0
|| getTrueWithProbability(shadowedPermutationProbability)) {
list_of_frame_parameters.push([
new DrawParameter(CALL_TYPE.IMAGE,
IMAGE_TYPE.DRAW_SVG_IMAGE,
svg_image_sizes[size_i],
shadow_blurs[sh_i],
svg_quantities[qty_i], "")]);
}
}
}
}
return list_of_frame_parameters;
}
function generatePutDataFrameParameters(context) {
var sizes = [20, 50, 100, 200, 300];
var quantities = [10, 50, 100, 500, 1000, 2000];
var list_of_frame_parameters = [];
for (var size_i = 0; size_i < sizes.length; size_i++) {
for (var qty_i = 0; qty_i < quantities.length; qty_i++) {
// Call putImageData so it aquires and caches image data of
// the correct dimensions
putImageData(context, sizes[size_i]);
list_of_frame_parameters.push([
new DrawParameter(CALL_TYPE.PUT_DATA,
"",
sizes[size_i],
0,
quantities[qty_i], "")]);
}
}
return list_of_frame_parameters;
}
function generateGetDataFrameParameters(context) {
var sizes = [20, 50, 100, 200, 300];
var quantities = [10, 50, 100, 500, 1000, 2000];
var list_of_frame_parameters = [];
for (var size_i = 0; size_i < sizes.length; size_i++) {
for (var qty_i = 0; qty_i < quantities.length; qty_i++) {
list_of_frame_parameters.push([
new DrawParameter(CALL_TYPE.GET_DATA,
"",
sizes[size_i],
0,
quantities[qty_i], "")]);
}
}
return list_of_frame_parameters;
}
// Draw each DrawParameter in the parameter_list on the canvas context,
// updates their total_bounding_box_area, total_bounding_box_perimeter
// and total_shadow_bounding_box_area fields.
function DrawParameters(context, parameter_list) {
for (var i = 0; i < parameter_list.length; i++) {
var p = parameter_list[i];
p.resetBoundingBoxArea();
for (var count = 0; count < p.quantity; count++) {
var draw_size;
switch (p.call_type) {
case CALL_TYPE.PATH:
setShapeStyle(context, p.fill_style);
setShadeStyle(context, p.shadow_blur);
draw_size = drawShape(context, p.draw_sub_type, p.size);
break;
case CALL_TYPE.IMAGE:
setShadeStyle(context, p.shadow_blur);
draw_size = drawImageType(context, p.draw_sub_type, p.size);
break;
case CALL_TYPE.GET_DATA:
draw_size = getImageData(context, p.size);
break;
case CALL_TYPE.PUT_DATA:
draw_size = putImageData(context, p.size);
break;
default:
draw_size = new DrawSize(0, 0);
}
p.total_bounding_box_area +=
draw_size.width * draw_size.height;
p.total_bounding_box_perimeter +=
(2*draw_size.width) + (2*draw_size.height);
if (p.call_type == CALL_TYPE.PATH || p.call_type == CALL_TYPE.IMAGE) {
p.total_shadow_bounding_box_area +=
(draw_size.width+2*p.shadow_blur) *
(draw_size.height+2*p.shadow_blur);
}
}
}
}
// =======================================================================//
// Utility functions to make a CSV string that encodes the generated data //
// =======================================================================//
function getDataFrame(performance_measurements) {
var data = {};
// shadow_blur * total bounding box area
data["SHADOW_BLUR_TIMES_AREA"] = [];
// shadow_blur^2 * total bounding box area
data["SHADOW_BLUR_SQUARED_TIMES_AREA"] = [];
// shadow_blur^2 * total shadow bounding box area
data["SHADOW_BLUR_SQUARED_TIMES_SHADOW_AREA"] = [];
// the number of draw calls that have a shadow
data["NUM_SHADOWS"] = [];
data["CALL_TYPE"] = [];
for (var i = 0; i < performance_measurements.length; i++) {
var performance_measurement = performance_measurements[i];
for (var j = 0; j < performance_measurement.parameters.length; j++) {
var p = performance_measurement.parameters[j];
var call_type = p.call_type + "_" + p.draw_sub_type;
var call_type_area = p.call_type + "_" + p.draw_sub_type + "_AREA";
data[call_type] = [];
data[call_type_area] = [];
if (p.call_type == CALL_TYPE.PATH) {
data[p.fill_style] = [];
data[p.fill_style + "_AREA"] = [];
}
}
}
for (var i = 0; i < performance_measurements.length; i++) {
var performance_measurement = performance_measurements[i];
for (var j = 0; j < performance_measurement.parameters.length; j++) {
var p = performance_measurement.parameters[j];
for (var key in data) {
data[key].push(0);
}
var call_type = p.call_type + "_" + p.draw_sub_type;
var call_type_area = p.call_type + "_" + p.draw_sub_type + "_AREA";
data[call_type][i] += p.quantity;
data[call_type_area][i] += p.total_bounding_box_area;
if (p.call_type == CALL_TYPE.PATH) {
data[p.fill_style][i] += p.quantity;
data[p.fill_style + "_AREA"][i] += p.total_bounding_box_area;
}
if (p.call_type == CALL_TYPE.PATH || p.call_type == CALL_TYPE.IMAGE) {
data["NUM_SHADOWS"][i] += (p.shadow_blur > 0.0) ? p.quantity : 0;
data["SHADOW_BLUR_TIMES_AREA"][i] +=
p.total_bounding_box_area * p.shadow_blur;
data["SHADOW_BLUR_SQUARED_TIMES_AREA"][i] +=
p.total_bounding_box_area * p.shadow_blur * p.shadow_blur;
data["SHADOW_BLUR_SQUARED_TIMES_SHADOW_AREA"][i] +=
p.total_shadow_bounding_box_area * p.shadow_blur * p.shadow_blur;
}
data["CALL_TYPE"][i] = p.call_type;
}
}
return data;
}
function getCSVString(performance_measurements) {
var csv_string = "";
var data_frame = getDataFrame(performance_measurements);
csv_string += "TIME_PER_FRAME, FRAMES_PER_SECOND"
for (var key in data_frame) {
csv_string += ", " + key;
}
csv_string += "\n";
for (var i = 0; i < performance_measurements.length; i++) {
var performance_measurement = performance_measurements[i];
csv_string += performance_measurement.tpf;
csv_string += ", " + performance_measurement.fps;
for (var key in data_frame) {
csv_string += ", " + data_frame[key][i];
}
csv_string += "\n";
}
return csv_string;
}
function drawCSVString(csv_string) {
var div = document.getElementById("csv_string");
div.innerHTML = csv_string;
}
// =========================================================================//
// Utility functions to set canvas parameters and perform canvas draw calls //
// =========================================================================//
function drawShape(context, shape_type, size) {
context.lineWidth = 3;
switch (shape_type) {
case SHAPE_TYPE.STROKE_PATH:
var radius = size/2;
context.beginPath();
context.ellipse(400, 400, radius, radius, 0, 0, 2*Math.PI, false);
context.stroke();
return new DrawSize(size, size);
case SHAPE_TYPE.FILL_CONVEX_PATH:
var radius = size/2;
context.beginPath();
context.ellipse(400, 400, radius, radius, 0, 0, 2*Math.PI, false);
context.fill();
return new DrawSize(size, size);
case SHAPE_TYPE.FILL_NON_CONVEX_PATH:
var radius = size/2;
context.beginPath();
context.lineTo(400, 400);
context.arc(400, 400, radius, 0, Math.PI*3/2, false);
context.lineTo(this.x, this.y);
context.fill();
return new DrawSize(size, size);
case SHAPE_TYPE.STROKE_RECT:
context.beginPath();
context.rect(100, 100, size, size);
context.stroke();
return new DrawSize(size, size);
case SHAPE_TYPE.FILL_RECT:
context.beginPath();
context.rect(100, 100, size, size);
context.fill();
return new DrawSize(size, size);
case SHAPE_TYPE.STROKE_TEXT:
context.font = size + "px Georgia";
context.strokeText("M", 40, 500);
return new DrawSize(context.measureText("M").width, size);
case SHAPE_TYPE.FILL_TEXT:
context.font = size + "px Georgia";
context.fillText("M", 40, 500);
return new DrawSize(context.measureText("M").width, size);
default:
console.log("Invalid SHAPE_TYPE: " + shape_type);
return new DrawSize(0, 0);
}
}
var svg_image = document.getElementById("svg_image_element");
var png_image = document.getElementById("png_image_element");
function drawImageType(context, image_type, size) {
switch (image_type) {
case IMAGE_TYPE.DRAW_SVG_IMAGE:
context.drawImage(svg_image, 50, 50, size, size);
return new DrawSize(size, size);
case IMAGE_TYPE.DRAW_PNG_IMAGE:
if (size > png_image.width) {
throw "In drawImageType (png) - size too big ";
}
if (size > png_image.height) {
throw "In drawImageType (png) - size too big ";
}
context.drawImage(png_image, 0, 0, size, size, 50, 50, size, size);
return new DrawSize(size, size);
}
}
function setShapeStyle(context, shape_style, area) {
var gradient = "";
switch (shape_style) {
case SHAPE_STYLE.LINEAR_GRADIENT:
var gradient = context.createLinearGradient(0, 0, 800, 600);
gradient.addColorStop(0, "blue");
gradient.addColorStop(1, "white");
context.fillStyle = gradient;
context.strokeStyle = gradient;
break;
case SHAPE_STYLE.COLOR:
context.fillStyle = "blue";
context.strokeStyle = "blue";
break;
case SHAPE_STYLE.RADIAL_GRADIENT:
var gradient =
context.createRadialGradient(400, 300, 100, 400, 300, 800);
gradient.addColorStop(0, "blue");
gradient.addColorStop(1, "white");
context.fillStyle = gradient;
context.strokeStyle = gradient;
break;
case SHAPE_STYLE.PATTERN:
var pattern = context.createPattern(png_image,"repeat");
context.fillStyle = pattern;
context.strokeStyle = pattern;
break;
default:
console.log("Invalid SHAPE_STYLE: " + shape_style);
break;
}
}
function setShadeStyle(context, shadow_blur) {
context.shadowBlur = shadow_blur;
context.shadowColor= "black";
}
function getImageData(context, size) {
var image = context.getImageData(10,10, size, size);
return new DrawSize(size, size);
}
var image_data_cache = {};
function putImageData(context, size) {
if (!(size in image_data_cache)) {
// Cache a data source so that subsequent calls with the same size don't
// need to generate a new data source of the appropriate size
image_data_cache[size] = context.createImageData(size, size);
}
context.putImageData(image_data_cache[size], 10, 10);
return new DrawSize(size, size);
}
// ================//
// Other utilities //
// ================//
function RecordingHelper() {
this.num_frames_since_restart = 0;
this.recording_start_time = Date.now();
this.restartRecordingTime = function() {
this.num_frames_since_restart = 0;
this.recording_start_time = Date.now();
}
this.recordFrame = function() {
this.num_frames_since_restart += 1;
}
this.getAverageTimePerFrame = function() {
var time_interval = Date.now() - this.recording_start_time;
return time_interval / this.num_frames_since_restart;
}
this.getAverageFramesPerSecond = function() {
return 1000 / this.getAverageTimePerFrame();
}
this.getNumberOfFramesRecorded = function() {
return this.num_frames_since_restart;
}
}
function getTrueWithProbability(probability) {
return Math.random() < probability;
}
// Given the current quantity of a call, the current time per frame and target
// time per frame, return a suggested quantity that should yield a rendering
// speed closer to the target time per frame assuming that the time per frame
// is linearly proportional to the quantity.
function quantityForTarget(current_quantity, current_tpf, target_tpf) {
if (current_quantity <= 0) return 1;
return Math.round(current_quantity * target_tpf / current_tpf);
}