From f79da8292aba9e486ea36f3d8ba3ea8906470718 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Thu, 21 Jun 2018 17:44:42 -0400 Subject: [PATCH 1/4] transfer approximate terrain heights JSON to workers --- Source/Core/ApproximateTerrainHeights.js | 22 ++++- Source/Scene/GroundPolylinePrimitive.js | 15 --- Source/Scene/Primitive.js | 92 ++++++++++++------- .../Workers/createGroundPolylineGeometry.js | 17 ++-- Source/Workers/parseTerrainHeights.js | 14 +++ Specs/Core/ApproximateTerrainHeightsSpec.js | 14 +++ 6 files changed, 109 insertions(+), 65 deletions(-) create mode 100644 Source/Workers/parseTerrainHeights.js diff --git a/Source/Core/ApproximateTerrainHeights.js b/Source/Core/ApproximateTerrainHeights.js index fb489798b013..1a29b81c6e18 100644 --- a/Source/Core/ApproximateTerrainHeights.js +++ b/Source/Core/ApproximateTerrainHeights.js @@ -11,7 +11,8 @@ define([ './Ellipsoid', './GeographicTilingScheme', './Rectangle', - './Resource' + './Resource', + '../ThirdParty/when' ], function( buildModuleUrl, defaultValue, @@ -25,7 +26,8 @@ define([ Ellipsoid, GeographicTilingScheme, Rectangle, - Resource) { + Resource, + when) { 'use strict'; var scratchDiagonalCartesianNE = new Cartesian3(); @@ -49,20 +51,30 @@ define([ * Initializes the minimum and maximum terrain heights * @return {Promise} */ - ApproximateTerrainHeights.initialize = function(url) { + ApproximateTerrainHeights.initialize = function() { var initPromise = ApproximateTerrainHeights._initPromise; if (defined(initPromise)) { return initPromise; } - url = defaultValue(url, 'Assets/approximateTerrainHeights.json'); - ApproximateTerrainHeights._initPromise = Resource.fetchJson(buildModuleUrl(url)).then(function(json) { + ApproximateTerrainHeights._initPromise = Resource.fetchJson(buildModuleUrl('Assets/approximateTerrainHeights.json')).then(function(json) { ApproximateTerrainHeights._terrainHeights = json; }); return ApproximateTerrainHeights._initPromise; }; + /** + * Initializes the minimum and maximum terrain heights from a string. + * Used by workers to receive terrain heights from the main thread. + * + * @param {String} terrainHeightsString Stringified terrain heights JSON. + */ + ApproximateTerrainHeights.initializeFromString = function(terrainHeightsString) { + ApproximateTerrainHeights._terrainHeights = JSON.parse(terrainHeightsString); + ApproximateTerrainHeights._initPromise = when(); + }; + /** * Computes the minimum and maximum terrain heights for a given rectangle * @param {Rectangle} rectangle THe bounding rectangle diff --git a/Source/Scene/GroundPolylinePrimitive.js b/Source/Scene/GroundPolylinePrimitive.js index b00f2a4bac4e..ed84865e5c59 100644 --- a/Source/Scene/GroundPolylinePrimitive.js +++ b/Source/Scene/GroundPolylinePrimitive.js @@ -375,21 +375,6 @@ define([ return GroundPolylinePrimitive._initialized; }; - // For use with web workers. - GroundPolylinePrimitive._initializeTerrainHeightsWorker = function() { - var initPromise = GroundPolylinePrimitive._initPromise; - if (defined(initPromise)) { - return initPromise; - } - - GroundPolylinePrimitive._initPromise = ApproximateTerrainHeights.initialize('../Assets/approximateTerrainHeights.json') - .then(function() { - GroundPolylinePrimitive._initialized = true; - }); - - return GroundPolylinePrimitive._initPromise; - }; - function createShaderProgram(groundPolylinePrimitive, frameState, appearance) { var context = frameState.context; var primitive = groundPolylinePrimitive._primitive; diff --git a/Source/Scene/Primitive.js b/Source/Scene/Primitive.js index 4e471bd0179b..3931df92119e 100644 --- a/Source/Scene/Primitive.js +++ b/Source/Scene/Primitive.js @@ -1,4 +1,5 @@ define([ + '../Core/ApproximateTerrainHeights', '../Core/BoundingSphere', '../Core/Cartesian2', '../Core/Cartesian3', @@ -42,6 +43,7 @@ define([ './SceneMode', './ShadowMode' ], function( + ApproximateTerrainHeights, BoundingSphere, Cartesian2, Cartesian3, @@ -1119,57 +1121,79 @@ define([ }); } + var transferTerrainHeightsPromise; if (!defined(createGeometryTaskProcessors)) { createGeometryTaskProcessors = new Array(numberOfCreationWorkers); for (i = 0; i < numberOfCreationWorkers; i++) { createGeometryTaskProcessors[i] = new TaskProcessor('createGeometry', Number.POSITIVE_INFINITY); } + // Push tasks transferring the JSON to the workers. + transferTerrainHeightsPromise = ApproximateTerrainHeights.initialize() + .then(function() { + var terrainHeightsString = JSON.stringify(ApproximateTerrainHeights._terrainHeights); + var workerTerrainHeightsPromises = new Array(numberOfCreationWorkers); + for (i = 0; i < numberOfCreationWorkers; i++) { + workerTerrainHeightsPromises[i] = createGeometryTaskProcessors[i].scheduleTask({ + subTasks : [ + { + moduleName : 'parseTerrainHeights', + geometry : terrainHeightsString + } + ] + }); + } + return when.all(workerTerrainHeightsPromises); + }); + } else { + transferTerrainHeightsPromise = when(); } - var subTask; - subTasks = subdivideArray(subTasks, numberOfCreationWorkers); - - for (i = 0; i < subTasks.length; i++) { - var packedLength = 0; - var workerSubTasks = subTasks[i]; - var workerSubTasksLength = workerSubTasks.length; - for (j = 0; j < workerSubTasksLength; ++j) { - subTask = workerSubTasks[j]; - geometry = subTask.geometry; - if (defined(geometry.constructor.pack)) { - subTask.offset = packedLength; - packedLength += defaultValue(geometry.constructor.packedLength, geometry.packedLength); - } - } - - var subTaskTransferableObjects; - - if (packedLength > 0) { - var array = new Float64Array(packedLength); - subTaskTransferableObjects = [array.buffer]; + transferTerrainHeightsPromise.then(function() { + var subTask; + subTasks = subdivideArray(subTasks, numberOfCreationWorkers); + for (i = 0; i < subTasks.length; i++) { + var packedLength = 0; + var workerSubTasks = subTasks[i]; + var workerSubTasksLength = workerSubTasks.length; for (j = 0; j < workerSubTasksLength; ++j) { subTask = workerSubTasks[j]; geometry = subTask.geometry; if (defined(geometry.constructor.pack)) { - geometry.constructor.pack(geometry, array, subTask.offset); - subTask.geometry = array; + subTask.offset = packedLength; + packedLength += defaultValue(geometry.constructor.packedLength, geometry.packedLength); } } - } - promises.push(createGeometryTaskProcessors[i].scheduleTask({ - subTasks : subTasks[i] - }, subTaskTransferableObjects)); - } + var subTaskTransferableObjects; - primitive._state = PrimitiveState.CREATING; + if (packedLength > 0) { + var array = new Float64Array(packedLength); + subTaskTransferableObjects = [array.buffer]; - when.all(promises, function(results) { - primitive._createGeometryResults = results; - primitive._state = PrimitiveState.CREATED; - }).otherwise(function(error) { - setReady(primitive, frameState, PrimitiveState.FAILED, error); + for (j = 0; j < workerSubTasksLength; ++j) { + subTask = workerSubTasks[j]; + geometry = subTask.geometry; + if (defined(geometry.constructor.pack)) { + geometry.constructor.pack(geometry, array, subTask.offset); + subTask.geometry = array; + } + } + } + + promises.push(createGeometryTaskProcessors[i].scheduleTask({ + subTasks : subTasks[i] + }, subTaskTransferableObjects)); + } + + primitive._state = PrimitiveState.CREATING; + + when.all(promises, function(results) { + primitive._createGeometryResults = results; + primitive._state = PrimitiveState.CREATED; + }).otherwise(function(error) { + setReady(primitive, frameState, PrimitiveState.FAILED, error); + }); }); } else if (primitive._state === PrimitiveState.CREATED) { var transferableObjects = []; diff --git a/Source/Workers/createGroundPolylineGeometry.js b/Source/Workers/createGroundPolylineGeometry.js index d7f1a992a7f7..1ca74743c7b7 100644 --- a/Source/Workers/createGroundPolylineGeometry.js +++ b/Source/Workers/createGroundPolylineGeometry.js @@ -1,21 +1,16 @@ define([ '../Core/defined', - '../Core/GroundPolylineGeometry', - '../Scene/GroundPolylinePrimitive' + '../Core/GroundPolylineGeometry' ], function( defined, - GroundPolylineGeometry, - GroundPolylinePrimitive) { + GroundPolylineGeometry) { 'use strict'; function createGroundPolylineGeometry(groundPolylineGeometry, offset) { - return GroundPolylinePrimitive._initializeTerrainHeightsWorker() - .then(function() { - if (defined(offset)) { - groundPolylineGeometry = GroundPolylineGeometry.unpack(groundPolylineGeometry, offset); - } - return GroundPolylineGeometry.createGeometry(groundPolylineGeometry); - }); + if (defined(offset)) { + groundPolylineGeometry = GroundPolylineGeometry.unpack(groundPolylineGeometry, offset); + } + return GroundPolylineGeometry.createGeometry(groundPolylineGeometry); } return createGroundPolylineGeometry; diff --git a/Source/Workers/parseTerrainHeights.js b/Source/Workers/parseTerrainHeights.js new file mode 100644 index 000000000000..0a00c991c03b --- /dev/null +++ b/Source/Workers/parseTerrainHeights.js @@ -0,0 +1,14 @@ +define([ + '../Core/ApproximateTerrainHeights', + '../ThirdParty/when' + ], function( + ApproximateTerrainHeights, + when) { + 'use strict'; + + function parseTerrainHeights(terrainHeightsString) { + ApproximateTerrainHeights.initializeFromString(terrainHeightsString); + } + + return parseTerrainHeights; +}); diff --git a/Specs/Core/ApproximateTerrainHeightsSpec.js b/Specs/Core/ApproximateTerrainHeightsSpec.js index df85d432a348..ad8ad7b365c6 100644 --- a/Specs/Core/ApproximateTerrainHeightsSpec.js +++ b/Specs/Core/ApproximateTerrainHeightsSpec.js @@ -26,6 +26,20 @@ defineSuite([ }); }); + it('initializes from a JSON string', function() { + var oldInitPromise = ApproximateTerrainHeights._initPromise; + var oldHeights = ApproximateTerrainHeights._terrainHeights; + + ApproximateTerrainHeights.initializeFromString('{"6-0-0":[10.662392616271973,26.89437484741211]}'); + + var terrainHeightsJson = ApproximateTerrainHeights._terrainHeights; + expect(terrainHeightsJson).toBeDefined(); + expect(terrainHeightsJson).not.toBe(oldHeights); + + ApproximateTerrainHeights._initPromise = oldInitPromise; + ApproximateTerrainHeights._terrainHeights = oldHeights; + }); + it('getApproximateTerrainHeights computes minimum and maximum terrain heights', function() { var result = ApproximateTerrainHeights.getApproximateTerrainHeights(Rectangle.fromDegrees(-121.0, 10.0, -120.0, 11.0)); expect(result.minimumTerrainHeight).toEqualEpsilon(-476.125711887558, CesiumMath.EPSILON10); From dc1abee38cb42fb92c42fc7ff54508a632bbb2c2 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Thu, 21 Jun 2018 17:50:53 -0400 Subject: [PATCH 2/4] revert asynchronous worker changes from fe4849e --- Source/Workers/createGeometry.js | 12 +-- Source/Workers/createTaskProcessorWorker.js | 95 +++++++++------------ 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/Source/Workers/createGeometry.js b/Source/Workers/createGeometry.js index 49fb0d2ca04c..f82872a34920 100644 --- a/Source/Workers/createGeometry.js +++ b/Source/Workers/createGeometry.js @@ -1,13 +1,11 @@ define([ '../Core/defined', '../Scene/PrimitivePipeline', - '../ThirdParty/when', './createTaskProcessorWorker', 'require' ], function( defined, PrimitivePipeline, - when, createTaskProcessorWorker, require) { 'use strict'; @@ -35,7 +33,7 @@ define([ function createGeometry(parameters, transferableObjects) { var subTasks = parameters.subTasks; var length = subTasks.length; - var resultsOrPromises = new Array(length); + var results = new Array(length); for (var i = 0; i < length; i++) { var task = subTasks[i]; @@ -44,16 +42,14 @@ define([ if (defined(moduleName)) { var createFunction = getModule(moduleName); - resultsOrPromises[i] = createFunction(geometry, task.offset); + results[i] = createFunction(geometry, task.offset); } else { //Already created geometry - resultsOrPromises[i] = geometry; + results[i] = geometry; } } - return when.all(resultsOrPromises, function(results) { - return PrimitivePipeline.packCreateGeometryResults(results, transferableObjects); - }); + return PrimitivePipeline.packCreateGeometryResults(results, transferableObjects); } return createTaskProcessorWorker(createGeometry); diff --git a/Source/Workers/createTaskProcessorWorker.js b/Source/Workers/createTaskProcessorWorker.js index fc4261f59c76..ce0d12a5a249 100644 --- a/Source/Workers/createTaskProcessorWorker.js +++ b/Source/Workers/createTaskProcessorWorker.js @@ -1,29 +1,13 @@ define([ - '../ThirdParty/when', '../Core/defaultValue', '../Core/defined', '../Core/formatError' ], function( - when, defaultValue, defined, formatError) { 'use strict'; - // createXXXGeometry functions may return Geometry or a Promise that resolves to Geometry - // if the function requires access to ApproximateTerrainHeights. - // For fully synchronous functions, just wrapping the function call in a `when` Promise doesn't - // handle errors correctly, hence try-catch - function callAndWrap(workerFunction, parameters, transferableObjects) { - var resultOrPromise; - try { - resultOrPromise = workerFunction(parameters, transferableObjects); - return resultOrPromise; // errors handled by Promise - } catch (e) { - return when.reject(e); - } - } - /** * Creates an adapter function to allow a calculation function to operate as a Web Worker, * paired with TaskProcessor, to receive tasks and return results. @@ -51,53 +35,54 @@ define([ */ function createTaskProcessorWorker(workerFunction) { var postMessage; + var transferableObjects = []; + var responseMessage = { + id : undefined, + result : undefined, + error : undefined + }; return function(event) { /*global self*/ var data = event.data; - var transferableObjects = []; - var responseMessage = { - id : data.id, - result : undefined, - error : undefined - }; + transferableObjects.length = 0; + responseMessage.id = data.id; + responseMessage.error = undefined; + responseMessage.result = undefined; + + try { + responseMessage.result = workerFunction(data.parameters, transferableObjects); + } catch (e) { + if (e instanceof Error) { + // Errors can't be posted in a message, copy the properties + responseMessage.error = { + name : e.name, + message : e.message, + stack : e.stack + }; + } else { + responseMessage.error = e; + } + } - return when(callAndWrap(workerFunction, data.parameters, transferableObjects)) - .then(function(result) { - responseMessage.result = result; - }) - .otherwise(function(e) { - if (e instanceof Error) { - // Errors can't be posted in a message, copy the properties - responseMessage.error = { - name : e.name, - message : e.message, - stack : e.stack - }; - } else { - responseMessage.error = e; - } - }) - .always(function() { - if (!defined(postMessage)) { - postMessage = defaultValue(self.webkitPostMessage, self.postMessage); - } + if (!defined(postMessage)) { + postMessage = defaultValue(self.webkitPostMessage, self.postMessage); + } - if (!data.canTransferArrayBuffer) { - transferableObjects.length = 0; - } + if (!data.canTransferArrayBuffer) { + transferableObjects.length = 0; + } - try { - postMessage(responseMessage, transferableObjects); - } catch (e) { - // something went wrong trying to post the message, post a simpler - // error that we can be sure will be cloneable - responseMessage.result = undefined; - responseMessage.error = 'postMessage failed with error: ' + formatError(e) + '\n with responseMessage: ' + JSON.stringify(responseMessage); - postMessage(responseMessage); - } - }); + try { + postMessage(responseMessage, transferableObjects); + } catch (e) { + // something went wrong trying to post the message, post a simpler + // error that we can be sure will be cloneable + responseMessage.result = undefined; + responseMessage.error = 'postMessage failed with error: ' + formatError(e) + '\n with responseMessage: ' + JSON.stringify(responseMessage); + postMessage(responseMessage); + } }; } From 103a75aaa3c62b3a09ff61f4eb7edbbc5ab0aee4 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Thu, 21 Jun 2018 18:26:11 -0400 Subject: [PATCH 3/4] make sure terrain heights loaded on workers for PolylineVisualizerSpec --- Specs/DataSources/PolylineVisualizerSpec.js | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Specs/DataSources/PolylineVisualizerSpec.js b/Specs/DataSources/PolylineVisualizerSpec.js index f2080ee91fd7..c8f11e4ec860 100644 --- a/Specs/DataSources/PolylineVisualizerSpec.js +++ b/Specs/DataSources/PolylineVisualizerSpec.js @@ -54,14 +54,30 @@ defineSuite([ beforeAll(function() { scene = createScene(); - ApproximateTerrainHeights.initialize(); + // Render a primitive to confirm that asynchronous primitives are ready + var objects = new EntityCollection(); + var visualizer = new PolylineVisualizer(scene, objects); + + var polyline = new PolylineGraphics(); + polyline.positions = new ConstantProperty([Cartesian3.fromDegrees(0.0, 0.0), Cartesian3.fromDegrees(0.0, 1.0)]); + polyline.material = new ColorMaterialProperty(); + + var entity = new Entity(); + entity.polyline = polyline; + objects.add(entity); + + return pollToPromise(function() { + scene.initializeFrame(); + var isUpdated = visualizer.update(time); + scene.render(time); + return isUpdated; + }).then(function() { + visualizer.destroy(); + }); }); afterAll(function() { scene.destroyForSpecs(); - - ApproximateTerrainHeights._initPromise = undefined; - ApproximateTerrainHeights._terrainHeights = undefined; }); it('Can create and destroy', function() { From 925c011addf572027001d96ae84f26e88e0ce3d3 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Fri, 22 Jun 2018 09:14:00 -0400 Subject: [PATCH 4/4] make sure terrain heights loaded on workers for GeometryVisualizerSpec, handle possible timeout --- Specs/DataSources/GeometryVisualizerSpec.js | 40 +++++++++++++++------ Specs/DataSources/PolylineVisualizerSpec.js | 10 +++--- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Specs/DataSources/GeometryVisualizerSpec.js b/Specs/DataSources/GeometryVisualizerSpec.js index f0fd0c5e426c..f5e281cf2912 100644 --- a/Specs/DataSources/GeometryVisualizerSpec.js +++ b/Specs/DataSources/GeometryVisualizerSpec.js @@ -6,21 +6,18 @@ defineSuite([ 'Core/Color', 'Core/ColorGeometryInstanceAttribute', 'Core/JulianDate', + 'Core/Rectangle', 'Core/ShowGeometryInstanceAttribute', 'DataSources/BoundingSphereState', - 'DataSources/CallbackProperty', 'DataSources/ColorMaterialProperty', 'DataSources/ConstantPositionProperty', 'DataSources/ConstantProperty', - 'DataSources/EllipseGeometryUpdater', 'DataSources/EllipseGraphics', 'DataSources/Entity', 'DataSources/EntityCollection', 'DataSources/GridMaterialProperty', + 'DataSources/RectangleGraphics', 'DataSources/SampledProperty', - 'DataSources/StaticGeometryColorBatch', - 'DataSources/StaticGeometryPerMaterialBatch', - 'DataSources/StaticOutlineGeometryBatch', 'Scene/ClassificationType', 'Scene/GroundPrimitive', 'Scene/MaterialAppearance', @@ -37,21 +34,18 @@ defineSuite([ Color, ColorGeometryInstanceAttribute, JulianDate, + Rectangle, ShowGeometryInstanceAttribute, BoundingSphereState, - CallbackProperty, ColorMaterialProperty, ConstantPositionProperty, ConstantProperty, - EllipseGeometryUpdater, EllipseGraphics, Entity, EntityCollection, GridMaterialProperty, + RectangleGraphics, SampledProperty, - StaticGeometryColorBatch, - StaticGeometryPerMaterialBatch, - StaticOutlineGeometryBatch, ClassificationType, GroundPrimitive, MaterialAppearance, @@ -68,7 +62,31 @@ defineSuite([ beforeAll(function() { scene = createScene(); - return GroundPrimitive.initializeTerrainHeights(); + // Render a simple Primitive to ensure that asynchronous geometry workers are ready + var objects = new EntityCollection(); + var visualizer = new GeometryVisualizer(scene, objects, scene.primitives, scene.groundPrimitives); + + var rectangle = new RectangleGraphics({ + coordinates : new Rectangle(0.0, 0.0, 0.0001, 0.0001) + }); + + var entity = new Entity(); + entity.position = new ConstantPositionProperty(new Cartesian3(1234, 5678, 9101112)); + entity.rectangle = rectangle; + objects.add(entity); + + return pollToPromise(function() { + scene.initializeFrame(); + var isUpdated = visualizer.update(time); + scene.render(time); + return isUpdated; + }).then(function() { + visualizer.destroy(); + }).otherwise(function() { + // Possible that pollToPromise timed out. + // Primitive workers will continue to spin up. + visualizer.destroy(); + }); }); afterAll(function() { diff --git a/Specs/DataSources/PolylineVisualizerSpec.js b/Specs/DataSources/PolylineVisualizerSpec.js index c8f11e4ec860..6c19c321af2a 100644 --- a/Specs/DataSources/PolylineVisualizerSpec.js +++ b/Specs/DataSources/PolylineVisualizerSpec.js @@ -1,6 +1,5 @@ defineSuite([ 'DataSources/PolylineVisualizer', - 'Core/ApproximateTerrainHeights', 'Core/BoundingSphere', 'Core/Cartesian3', 'Core/Color', @@ -24,7 +23,6 @@ defineSuite([ 'Specs/pollToPromise' ], function( PolylineVisualizer, - ApproximateTerrainHeights, BoundingSphere, Cartesian3, Color, @@ -54,12 +52,12 @@ defineSuite([ beforeAll(function() { scene = createScene(); - // Render a primitive to confirm that asynchronous primitives are ready + // Render a simple Primitive to ensure that asynchronous geometry workers are ready var objects = new EntityCollection(); var visualizer = new PolylineVisualizer(scene, objects); var polyline = new PolylineGraphics(); - polyline.positions = new ConstantProperty([Cartesian3.fromDegrees(0.0, 0.0), Cartesian3.fromDegrees(0.0, 1.0)]); + polyline.positions = new ConstantProperty([Cartesian3.fromDegrees(0.0, 0.0), Cartesian3.fromDegrees(0.0, 0.001)]); polyline.material = new ColorMaterialProperty(); var entity = new Entity(); @@ -73,6 +71,10 @@ defineSuite([ return isUpdated; }).then(function() { visualizer.destroy(); + }).otherwise(function() { + // Possible that pollToPromise timed out. + // Primitive workers will continue to spin up. + visualizer.destroy(); }); });