diff --git a/.idea/WebWorldWind.iml b/.idea/WebWorldWind.iml index 68a7f7796..afa798668 100644 --- a/.idea/WebWorldWind.iml +++ b/.idea/WebWorldWind.iml @@ -3,6 +3,7 @@ + diff --git a/functionalTest/shapes/SurfaceShapePerformance.html b/functionalTest/shapes/SurfaceShapePerformance.html new file mode 100644 index 000000000..16dc9f1fa --- /dev/null +++ b/functionalTest/shapes/SurfaceShapePerformance.html @@ -0,0 +1,78 @@ + + + + + + + + + + + Surface Shape Performance Functional Test + + + + +
+
+ + +
+ + Browser Does Not Support HTML5 + +
+ + +
+ + +
+

+ Surface Shape Performance Tests +

+

+ Reload the page between tests. Once the test has been completed, a statistical summary of the frame + times and a distribution of the frame times grouped by five second durations is displayed. Below the + frame time distribution plot, a text area with the actual frame times is displayed as well as a + button which automates copying of the data to the clipboard. +

+
+
+

Available Tests:

+ + + +
+
+ + +
+
+ +
+
+
+ +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/functionalTest/shapes/SurfaceShapePerformance.js b/functionalTest/shapes/SurfaceShapePerformance.js new file mode 100644 index 000000000..aac7526c9 --- /dev/null +++ b/functionalTest/shapes/SurfaceShapePerformance.js @@ -0,0 +1,280 @@ +requirejs([ + '../../src/WorldWind', + '../util/Util' + ], + function (WorldWind, Util) { + "use strict"; + + // Define shape counts for testing + var dynamicShapeCount = 100; // of each type of shape (circle, ellipse, etc.) + var staticShapeCount = 1000; // of each type of shape (circle, ellipse, etc.) + + // Set the base url to use the root images directory + WorldWind.configuration.baseUrl = "../../"; + + // Initialize Globe + var wwd = Util.initializeLowResourceWorldWindow("globe"); + var testLayer = new WorldWind.RenderableLayer("Test Layer"); + wwd.addLayer(testLayer); + var util = new Util(wwd); + + util.setStatusMessage("Globe loaded, ready for testing..."); + + // Navigation Moves for Testing + var moveZero = { + latitude: { + goal: wwd.navigator.lookAtLocation.latitude, + step: Number.MAX_VALUE + }, + longitude: { + goal: wwd.navigator.lookAtLocation.longitude, + step: Number.MAX_VALUE + }, + tilt: { + goal: 0, + step: Number.MAX_VALUE + }, + heading: { + goal: 0, + step: Number.MAX_VALUE + }, + range: { + goal: wwd.navigator.range, + step: Number.MAX_VALUE + } + }; + + var moveOne = { + range: { + goal: 8e5, + step: 5e4 + }, + tilt: { + goal: 75, + step: 0.75 + }, + onComplete: function () { + util.setStatusMessage("First move complete..."); + } + }; + + var moveTwo = { + heading: { + goal: 90, + step: 0.75 + }, + onComplete: function () { + util.setStatusMessage("Second move complete"); + } + }; + + var moveThree = { + latitude: { + goal: wwd.navigator.lookAtLocation.latitude, + step: 1 + }, + longitude: { + goal: -70, + step: 0.5 + }, + onComplete: function () { + util.stopMetricCapture(); + util.setStatusMessage("Move Complete"); + + // output metrics + var stats = util.frameStats.map(function (frameStat) { + return frameStat.frameTime; + }); + stats.shift(); + stats.pop(); + util.setOutputMessage(Util.generateResultsSummary(stats, "Frame Times")); + stats = util.frameStats.map(function (frameStat) { + return frameStat.layerRenderingTime; + }); + } + }; + + // Utility functions + + // This is a pseudo random number generator which allows a seed, providing repeatable conditions + var nextFloat = function (seed) { + seed = seed % 2147483647; + return function () { + seed = seed * 16807 % 2147483647; + return (seed - 1) / 2147483646; + }; + }; + // Assign the PRNG to the function which will be used by the testing for content generation + var rand = nextFloat(1234); + + var generateShapeAttributes = function () { + var sa = new WorldWind.ShapeAttributes(); + var r = rand(); + + sa.drawInterior = false; + sa.drawOutline = false; + + if (r < 0.3333) { + sa.drawInterior = true; + sa.interiorColor = new WorldWind.Color(rand(), rand(), rand(), rand()); + } else if (r < 0.6666) { + sa.drawOutline = true; + sa.outlineColor = new WorldWind.Color(rand(), rand(), rand(), rand()); + } else { + sa.drawInterior = true; + sa.interiorColor = new WorldWind.Color(rand(), rand(), rand(), rand()); + sa.drawOutline = true; + sa.outlineColor = new WorldWind.Color(rand(), rand(), rand(), rand()); + } + + return sa; + }; + + var generateLocation = function () { + var lat = 180 * rand() - 90; + var lon = 360 * rand() - 180; + + return new WorldWind.Location(lat, lon); + }; + + var generateShapes = function (count) { + var sa, shape, sector, radius, topLocation, lat, lon, locations, minorAxis, majorAxis, heading, i, j; + + // Surface Circles + for (i = 0; i < count; i++) { + sa = generateShapeAttributes(); + radius = rand() * 1000000; + shape = new WorldWind.SurfaceCircle(generateLocation(), radius, sa); + testLayer.addRenderable(shape); + } + + // Surface Ellipse + for (i = 0; i < count; i++) { + sa = generateShapeAttributes(); + majorAxis = rand() * 1000000; + minorAxis = rand() * 500000; + heading = rand() * 360; + shape = new WorldWind.SurfaceEllipse(generateLocation(), majorAxis, minorAxis, heading, sa); + testLayer.addRenderable(shape); + } + + // Surface Polygon + for (i = 0; i < count; i++) { + sa = generateShapeAttributes(); + topLocation = generateLocation(); + locations = []; + locations.push(topLocation); + for (j = 0; j < 2; j++) { + lat = Math.min(90, Math.max(-90, topLocation.latitude - rand() * 8)); + lon = Math.min(180, Math.max(-180, topLocation.longitude - rand() * 8)); + locations.push(new WorldWind.Location(lat, lon)); + } + shape = new WorldWind.SurfacePolygon(locations, sa); + testLayer.addRenderable(shape); + } + + // Surface Polyline + for (i = 0; i < count; i++) { + sa = generateShapeAttributes(); + topLocation = generateLocation(); + locations = []; + locations.push(topLocation); + for (j = 0; j < 2; j++) { + lat = Math.min(90, Math.max(-90, topLocation.latitude - rand() * 8)); + lon = Math.min(180, Math.max(-180, topLocation.longitude - rand() * 8)); + locations.push(new WorldWind.Location(lat, lon)); + } + shape = new WorldWind.SurfacePolyline(locations, sa); + testLayer.addRenderable(shape); + } + + // Surface Rectangle + for (i = 0; i < count; i++) { + sa = generateShapeAttributes(); + majorAxis = 1000000 * rand(); + minorAxis = 500000 * rand(); + heading = 360 * rand(); + shape = new WorldWind.SurfaceRectangle(generateLocation(), majorAxis, minorAxis, heading, sa); + testLayer.addRenderable(shape); + } + + // Surface Sector + for (i = 0; i < count; i++) { + sa = generateShapeAttributes(); + topLocation = generateLocation(); + majorAxis = 8 * rand(); + minorAxis = 4 * rand(); + sector = new WorldWind.Sector(topLocation.latitude, topLocation.latitude + majorAxis, + topLocation.longitude, topLocation.longitude + minorAxis); + shape = new WorldWind.SurfaceSector(sector, sa); + testLayer.addRenderable(shape); + } + + wwd.redraw(); + }; + + // Click Event Callbacks + var onMoveClick = function () { + if (!util.isMoving()) { + util.startMetricCapture(); + util.move([moveZero, moveOne, moveTwo, moveThree]); + } + }; + + var onStaticShapesClick = function () { + generateShapes(1000); + wwd.redraw(); + onMoveClick(); + }; + + var onDynamicShapesClick = function () { + generateShapes(100); + + var onRedrawMoveShapes = function (worldwindow, stage) { + if (stage === WorldWind.AFTER_REDRAW) { + var idx = worldwindow.layers.indexOf(testLayer); + + if (idx >= 0) { + var count = worldwindow.layers[idx].renderables.length; + + for (var i = 0; i < count; i++) { + var shape = worldwindow.layers[idx].renderables[i]; + + if (shape instanceof WorldWind.SurfaceCircle || shape instanceof WorldWind.SurfaceEllipse + || shape instanceof WorldWind.SurfaceRectangle) { + var center = shape.center; + center.latitude += rand() * 0.5 - 0.25; + center.longitude += rand() * 0.5 - 0.25; + shape.center = center; + } else if (shape instanceof WorldWind.SurfacePolygon + || shape instanceof WorldWind.SurfacePolyline) { + var boundaries = shape.boundaries; + boundaries[0].latitude += rand() * 0.5 - 0.25; + boundaries[0].longitude += rand() * 0.5 - 0.25; + shape.boundaries = boundaries; + } else if (shape instanceof WorldWind.SurfaceSector) { + var sector = shape.sector; + sector.minLongitude += rand() * 0.5 - 0.25; + sector.minLatitude += rand() * 0.5 - 0.25; + shape.sector = sector; + } + + } + } + } + }; + + wwd.redrawCallbacks.push(onRedrawMoveShapes); + wwd.redraw(); + onMoveClick(); + }; + + // Click Event Assignments + var navigateButton = document.getElementById("navigate-button"); + var staticShapesButton = document.getElementById("static-button"); + var dynamicShapesButton = document.getElementById("dynamic-button"); + + navigateButton.addEventListener("click", onMoveClick); + staticShapesButton.addEventListener("click", onStaticShapesClick); + dynamicShapesButton.addEventListener("click", onDynamicShapesClick); + }); diff --git a/functionalTest/util/Util.js b/functionalTest/util/Util.js new file mode 100644 index 000000000..e74a21248 --- /dev/null +++ b/functionalTest/util/Util.js @@ -0,0 +1,380 @@ +define([ + '../../src/WorldWind', + '../../src/util/WWUtil' + ], + function (WorldWind, WWUtil) { + "use strict"; + + var Util = function (worldwindow) { + + this.wwd = worldwindow; + + this.moveQueue = []; + + this.frameStats = []; + + this.captureMetrics = false; + + this.wwd.redrawCallbacks.push(this.onRedrawMove()); + + this.wwd.redrawCallbacks.push(this.onRedrawMetricCapture()); + + this.statusOutput = document.getElementById("status-output"); + + this.resultsOutput = document.getElementById("results-output"); + }; + + Util.initializeLowResourceWorldWindow = function (canvasId) { + var wwd = new WorldWind.WorldWindow(canvasId); + wwd.globe.elevationModel.removeAllCoverages(); // Don't want delays associated with parsing and changing terrain + var bmnglayer = new WorldWind.BMNGOneImageLayer(); + bmnglayer.minActiveAltitude = 0; + wwd.addLayer(bmnglayer); // Don't want any imaging processing delays + wwd.redraw(); + + return wwd; + }; + + Util.prototype.isMoving = function () { + return this.moveQueue.length > 0; + }; + + Util.prototype.setStatusMessage = function (value) { + Util.setElementValue(this.statusOutput, value); + }; + + Util.prototype.setOutputMessage = function (value) { + Util.setElementValue(this.resultsOutput, value); + }; + + Util.prototype.appendOutputMessage = function (value) { + this.resultsOutput.appendChild(document.createElement("hr")); + Util.appendElementValue(this.resultsOutput, value); + }; + + Util.setElementValue = function (element, value) { + var children = element.childNodes; + + // remove existing nodes + for (var c = 0; c < children.length; c++) { + element.removeChild(children[c]); + } + + Util.appendElementValue(element, value); + }; + + Util.appendElementValue = function (element, value) { + if (typeof value === "string") { + value = document.createElement("h3").appendChild(document.createTextNode(value)); + } + + element.appendChild(value); + }; + + Util.prototype.onRedrawMove = function () { + + var self = this; + + return function (worldwindow, stage) { + + if (self.moveQueue.length > 0 && stage === WorldWind.AFTER_REDRAW) { + + var range, tilt, heading, latitude, longitude, endStates = self.moveQueue[0]; + + if (endStates.range && !endStates.range.complete) { + range = Util.calculateNextValue(worldwindow.navigator.range, endStates.range.goal, endStates.range.step); + if (typeof range === "number") { + worldwindow.navigator.range = range; + } else { + endStates.range.complete = true; + } + } + + if (endStates.tilt && !endStates.tilt.complete) { + tilt = Util.calculateNextValue(worldwindow.navigator.tilt, endStates.tilt.goal, endStates.tilt.step); + if (typeof tilt === "number") { + worldwindow.navigator.tilt = tilt; + } else { + endStates.tilt.complete = true; + } + } + + if (endStates.heading && !endStates.heading.complete) { + heading = Util.calculateNextValue(worldwindow.navigator.heading, endStates.heading.goal, endStates.heading.step); + if (typeof heading === "number") { + worldwindow.navigator.heading = heading; + } else { + endStates.heading.complete = true; + } + } + + if (endStates.latitude && !endStates.latitude.complete) { + latitude = Util.calculateNextValue(worldwindow.navigator.lookAtLocation.latitude, + endStates.latitude.goal, endStates.latitude.step); + if (typeof latitude === "number") { + worldwindow.navigator.lookAtLocation.latitude = latitude; + } else { + endStates.latitude.complete = true; + } + } + + if (endStates.longitude && !endStates.longitude.complete) { + longitude = Util.calculateNextValue(worldwindow.navigator.lookAtLocation.longitude, + endStates.longitude.goal, endStates.longitude.step); + if (typeof longitude === "number") { + worldwindow.navigator.lookAtLocation.longitude = longitude; + } else { + endStates.longitude.complete = true; + } + } + + // Check to see if all the end states of this move have been completed + var keys = Object.getOwnPropertyNames(endStates); + for (var i = 0; i < keys.length; i++) { + + // skip the callback property + if (keys[i] === "onComplete") { + continue; + } + + if (!endStates[keys[i]].complete) { + // not done yet, don't complete the move yet + worldwindow.redraw(); + return; + } + } + + // Remove the move from the queue and invoke the completion listener if provided + self.moveQueue.shift(); + if (endStates.onComplete) { + endStates.onComplete(); + } + worldwindow.redraw(); + } + }; + }; + + Util.prototype.onRedrawMetricCapture = function () { + + var self = this; + + return function (worldwindow, stage) { + + if (self.captureMetrics && stage === WorldWind.AFTER_REDRAW) { + + // Copy all of the individual frame metrics + var frameStatistics = {}; + + frameStatistics.frameTime = worldwindow.frameStatistics.frameTime; + frameStatistics.tesselationTime = worldwindow.frameStatistics.tessellationTime; + frameStatistics.layerRenderingTime = worldwindow.frameStatistics.layerRenderingTime; + frameStatistics.orderedRenderingTime = worldwindow.frameStatistics.orderedRenderingTime; + frameStatistics.terrainTileCount = worldwindow.frameStatistics.terrainTileCount; + frameStatistics.imageTileCount = worldwindow.frameStatistics.imageTileCount; + frameStatistics.renderedTileCount = worldwindow.frameStatistics.renderedTileCount; + frameStatistics.tileUpdateCount = worldwindow.frameStatistics.tileUpdateCount; + frameStatistics.textureLoadCount = worldwindow.frameStatistics.textureLoadCount; + frameStatistics.vboLoadCount = worldwindow.frameStatistics.vboLoadCount; + + self.frameStats.push(frameStatistics); + } + }; + }; + + Util.prototype.move = function (endStates) { + + if (Array.isArray(endStates)) { + endStates.forEach(function (endState) { + this.moveQueue.push(endState); + }, this); + } else { + this.moveQueue.push(endStates); + } + + this.wwd.redraw(); + }; + + Util.prototype.startMetricCapture = function () { + this.frameStats = []; + this.captureMetrics = true; + }; + + Util.prototype.stopMetricCapture = function () { + this.captureMetrics = false; + }; + + Util.generateResultsSummary = function (dataArray, title) { + var min = Util.calculateMin(dataArray), max = Util.calculateMax(dataArray), average, stddev, bins; + + // calculate average + average = Util.calculateAverage(dataArray); + + // calculate standard deviation + stddev = Util.calculateStdDev(dataArray); + + // bin the data on 5 second bins + bins = Util.binValues(dataArray, 5); + + // create the html elements displaying the data + var encDiv = document.createElement("div"); + var titleEl = document.createElement("h2"); + titleEl.appendChild(document.createTextNode(title)); + encDiv.appendChild(titleEl); + var list = document.createElement("ul"); + encDiv.appendChild(list); + var li = document.createElement("li"); + list.appendChild(li); + li.appendChild(document.createTextNode("Average: " + average)); + li = document.createElement("li"); + list.appendChild(li); + li.appendChild(document.createTextNode("Standard Deviation: " + stddev)); + li = document.createElement("li"); + list.appendChild(li); + li.appendChild(document.createTextNode("Max: " + max)); + li = document.createElement("li"); + list.appendChild(li); + li.appendChild(document.createTextNode("Min: " + min)); + + encDiv.appendChild(Util.generateTextOutput(dataArray)); + + return encDiv; + }; + + Util.calculateMax = function (values) { + var max = -Number.MAX_VALUE; + + values.forEach(function (value) { + max = Math.max(max, value); + }); + + return max; + }; + + Util.calculateMin = function (values) { + var min = Number.MAX_VALUE; + + values.forEach(function (value) { + min = Math.min(min, value); + }); + + return min; + }; + + Util.calculateAverage = function (values) { + var total = 0; + values.forEach(function (value) { + total += value; + }); + + return total / values.length; + }; + + Util.calculateStdDev = function (values) { + var avg = Util.calculateAverage(values), total = 0, diff; + + values.forEach(function (value) { + diff = value - avg; + total += diff * diff; + }); + + return Math.sqrt(total / values.length); + }; + + Util.binValues = function (values, binSize) { + var i, idx, min = Util.calculateMin(values), max = Util.calculateMax(values), binRange = (max - min), + len = values.length, binCount = Math.ceil(binRange / binSize), bins; + + bins = new Array(binCount + 1); + + WWUtil.fillArray(bins, 0); + for (i = 0; i < len; i++) { + idx = Math.floor((values[i] - min) / binSize); // TODO check if this should be floor or round + bins[idx]++; + } + + return bins; + }; + + Util.generateSimplePlot = function (data, sizeX, sizeY, reverseData) { + var canvas = document.createElement("canvas"), xScale, yScale, i, x, y, ctx, max = Util.calculateMax(data), + min = Util.calculateMin(data), dataPoints = data.length; + + // setup canvas element + canvas.setAttribute("width", sizeX); + canvas.setAttribute("height", sizeY); + ctx = canvas.getContext("2d"); + + // setup scaling values + xScale = sizeX / dataPoints; + yScale = sizeY / (max - min); + + // plot data + x = 0; + y = data[0] * yScale; + if (reverseData) { + y = sizeY - y; + } + ctx.beginPath(); + ctx.moveTo(x, y); + + for (i = 1; i < dataPoints; i++) { + x = xScale * i; + y = data[i] * yScale; + if (reverseData) { + y = sizeY - y; + } + ctx.lineTo(x, y); + } + ctx.stroke(); + + return canvas; + }; + + Util.generateTextOutput = function (data) { + var textOutput = document.createElement("textarea"); + textOutput.value = data.join(","); + textOutput.setAttribute("id", "text-data-output"); + + var copyToClipboardButton = document.createElement("button"); + copyToClipboardButton.appendChild(document.createTextNode("Copy to Clipboard")); + copyToClipboardButton.addEventListener("click", function () { + textOutput.select(); + + try { + var successful = document.execCommand('copy'); + var msg = successful ? 'successful' : 'unsuccessful'; + console.log('Copying text command was ' + msg); + } catch (err) { + console.log('Oops, unable to copy'); + } + }); + + var outputDiv = document.createElement("div"); + outputDiv.appendChild(textOutput); + var buttonDiv = document.createElement("div"); + buttonDiv.appendChild(copyToClipboardButton); + + var div = document.createElement("div"); + div.appendChild(outputDiv); + div.appendChild(buttonDiv); + + return div; + }; + + // Internal use only + Util.calculateNextValue = function (currentValue, goal, step) { + var diff = currentValue - goal; + + if (Math.abs(diff) > 0.1) { + if (diff < 0) { + return Math.abs(diff) > step ? currentValue + step : goal; + } else { + return Math.abs(diff) > step ? currentValue - step : goal; + } + } else { + return null; + } + }; + + return Util; +});