Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request Scheduler Prioritization #3397

Merged
merged 13 commits into from
Jan 25, 2016
21 changes: 17 additions & 4 deletions Source/Core/ArcGisImageServerTerrainProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ define([
'./loadImage',
'./Math',
'./RequestScheduler',
'./RequestType',
'./TerrainProvider'
], function(
when,
Expand All @@ -30,6 +31,7 @@ define([
loadImage,
CesiumMath,
RequestScheduler,
RequestType,
TerrainProvider) {
"use strict";

Expand Down Expand Up @@ -200,11 +202,15 @@ define([
* @param {Number} x The X coordinate of the tile for which to request geometry.
* @param {Number} y The Y coordinate of the tile for which to request geometry.
* @param {Number} level The level of the tile for which to request geometry.
* @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this goes into master - or sooner - include all changes to the public API in CHANGES.md.

* or false if the request should be initiated regardless of the number of requests
* already in progress.
* @param {Number} [distance] The distance of the tile from the camera, used to prioritize requests.
* @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry. If this method
* returns undefined instead of a promise, it is an indication that too many requests are already
* pending and the request will be retried later.
*/
ArcGisImageServerTerrainProvider.prototype.requestTileGeometry = function(x, y, level) {
ArcGisImageServerTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests, distance) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to look at the rest of the code, but I wonder if throttleRequests, distance should be replaced with something like a single request object that doesn't throttle when undefined, and has a function to return the priority (distance). This would also scale nicely to segmenting 3D Tiles vs. terrain vs. imagery, etc.

At the same time, we don't want to make request attempts too heavily if they are going to be made a lot because of throttling (but there's no concern for the ones that result in an actual request).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe to start, just replace both of these with a requestPriority that can be undefined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a breaking change for some functions, but would be easy to deprecate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this is a good first step:

Or maybe to start, just replace both of these with a requestPriority that can be undefined.

But as I look at more of the code, the long-term approach could be that the priority is an object with a bunch of optional properties (distance being the most obvious), and the caller fills out as many properties as it knows. Then the request scheduler knows how to sort based on the incomplete priority objects. This would also allow things like "this request must happen right away (so perhaps we could central all requests even those that happen "immediately").

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, leaving this named distance is going to make a search and replace later pretty painful.

var rectangle = this._tilingScheme.tileXYToRectangle(x, y, level);

// Each pixel in the heightmap represents the height at the center of that
Expand All @@ -231,9 +237,16 @@ define([
url = proxy.getURL(url);
}

var promise = RequestScheduler.throttleRequest(url, loadImage);
if (!defined(promise)) {
return undefined;
var promise;

throttleRequests = defaultValue(throttleRequests, true);
if (throttleRequests) {
promise = RequestScheduler.throttleRequest(url, loadImage, RequestType.TERRAIN, distance);
if (!defined(promise)) {
return undefined;
}
} else {
promise = loadImage(url);
}

var that = this;
Expand Down
8 changes: 6 additions & 2 deletions Source/Core/CesiumTerrainProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ define([
'./OrientedBoundingBox',
'./QuantizedMeshTerrainData',
'./RequestScheduler',
'./RequestType',
'./RuntimeError',
'./TerrainProvider',
'./TileProviderError'
Expand All @@ -46,6 +47,7 @@ define([
OrientedBoundingBox,
QuantizedMeshTerrainData,
RequestScheduler,
RequestType,
RuntimeError,
TerrainProvider,
TileProviderError) {
Expand Down Expand Up @@ -477,14 +479,16 @@ define([
* @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited,
* or false if the request should be initiated regardless of the number of requests
* already in progress.
* @param {Number} [distance] The distance of the tile from the camera, used to prioritize requests.
*
* @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry. If this method
* returns undefined instead of a promise, it is an indication that too many requests are already
* pending and the request will be retried later.
*
* @exception {DeveloperError} This function must not be called before {@link CesiumTerrainProvider#ready}
* returns true.
*/
CesiumTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests) {
CesiumTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests, distance) {
//>>includeStart('debug', pragmas.debug)
if (!this._ready) {
throw new DeveloperError('requestTileGeometry must not be called before the terrain provider is ready.');
Expand Down Expand Up @@ -522,7 +526,7 @@ define([
}
throttleRequests = defaultValue(throttleRequests, true);
if (throttleRequests) {
promise = RequestScheduler.throttleRequest(url, tileLoader);
promise = RequestScheduler.throttleRequest(url, tileLoader, RequestType.TERRAIN, distance);
if (!defined(promise)) {
return undefined;
}
Expand Down
4 changes: 3 additions & 1 deletion Source/Core/EllipsoidTerrainProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,13 @@ define([
* @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited,
* or false if the request should be initiated regardless of the number of requests
* already in progress.
* @param {Number} [distance] The distance of the tile from the camera, used to prioritize requests.
*
* @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry. If this method
* returns undefined instead of a promise, it is an indication that too many requests are already
* pending and the request will be retried later.
*/
EllipsoidTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests) {
EllipsoidTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests, distance) {
return this._terrainData;
};

Expand Down
116 changes: 115 additions & 1 deletion Source/Core/RequestScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,31 @@ define([
DeveloperError) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new code in this file needs unit tests.

"use strict";

var RequestTypeBudget = function(type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not function RequestTypeBudget(type) {?

/**
* Total requests allowed this frame
*/
this.total = 0;

/**
* Total requests used this frame
*/
this.used = 0;

/**
* Type of request. Used for more fine-grained priority sorting
*/
this.type = type;
};

var activeRequestsByServer = {};
var activeRequests = 0;
var budgets = {};
var leftoverRequests = [];

var stats = {
numberOfRequestsThisFrame : 0
};

/**
* Because browsers throttle the number of parallel requests allowed to each server
Expand All @@ -32,6 +55,70 @@ define([
function RequestScheduler() {
}

function handleLeftoverRequest(url, type, distance) {
// TODO : don't create a new object, reuse instead
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address this and the other TODOs before we merge.

leftoverRequests.push({
url : url,
type : type,
distance : defaultValue(distance, 0.0)
});
}

function printStats() {
if (stats.numberOfRequestsThisFrame > 0) {
console.log('Number of requests attempted: ' + stats.numberOfRequestsThisFrame);
}

var numberOfActiveRequests = RequestScheduler.maximumRequests - RequestScheduler.getNumberOfAvailableRequests();
if (numberOfActiveRequests > 0) {
console.log('Number of active requests: ' + numberOfActiveRequests);
}
}

function distanceSortFunction(a, b) {
return a.distance - b.distance;
}

RequestScheduler.resetBudgets = function() {
// TODO : Right now, assume that requests made the previous frame will probably be issued again the current frame
// TODO : If this assumption changes, we should introduce stealing from other budgets.

if (!RequestScheduler.prioritize) {
return;
}

printStats();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this. Instead, for example, the Sandcastle example could call this each frame (or it could be commented out in the Sandcastle example).

stats.numberOfRequestsThisFrame = 0;

// Reset budget totals
for (var name in budgets) {
if (budgets.hasOwnProperty(name)) {
budgets[name].total = 0;
budgets[name].used = 0;
}
}

// Sort all requests made this frame by distance
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all? Or all leftover?

var requests = leftoverRequests;
requests.sort(distanceSortFunction);

// Allocate new budgets based on the distances of leftover requests
var availableRequests = RequestScheduler.getNumberOfAvailableRequests();
var requestsLength = requests.length;
for (var j = 0; (j < requestsLength) && (availableRequests > 0); ++j) {
var request = requests[j];
var server = RequestScheduler.getServer(request.url);
var budget = budgets[server];
var budgetAvailable = RequestScheduler.getNumberOfAvailableRequestsByServer(request.url);
if (budget.total < budgetAvailable) {
++budget.total;
--availableRequests;
}
}

requests.length = 0;
};

var pageUri = typeof document !== 'undefined' ? new Uri(document.location.href) : new Uri();

/**
Expand Down Expand Up @@ -109,6 +196,9 @@ define([
* @param {String} url The URL to request.
* @param {RequestScheduler~RequestFunction} requestFunction The actual function that
* makes the request.
* @param {RequestType} [requestType] The type of request being issued.
* @param {Number} [distance] The distance from the camera, used to prioritize requests.
*
* @returns {Promise.<Object>|undefined} Either undefined, meaning the request would exceed the maximum number of
* parallel requests, or a Promise for the requested data.
*
Expand All @@ -129,7 +219,7 @@ define([
* }
*
*/
RequestScheduler.throttleRequest = function(url, requestFunction) {
RequestScheduler.throttleRequest = function(url, requestFunction, requestType, distance) {
//>>includeStart('debug', pragmas.debug);
if (!defined(url)) {
throw new DeveloperError('url is required.');
Expand All @@ -140,16 +230,33 @@ define([
}
//>>includeEnd('debug');

++stats.numberOfRequestsThisFrame;

if (activeRequests >= RequestScheduler.maximumRequests) {
handleLeftoverRequest(url, requestType, distance);
return undefined;
}

var server = RequestScheduler.getServer(url);
var activeRequestsForServer = defaultValue(activeRequestsByServer[server], 0);
if (activeRequestsForServer >= RequestScheduler.maximumRequestsPerServer) {
handleLeftoverRequest(url, requestType, distance);
return undefined;
}

if (RequestScheduler.prioritize && defined(requestType)) {
var budget = budgets[server];
if (!defined(budget)) {
budget = new RequestTypeBudget(requestType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't two request types go to the same server?

budgets[server] = budget;
}
if (budget.used >= budget.total) {
handleLeftoverRequest(url, requestType, distance);
return undefined;
}
++budget.used;
}

++activeRequests;
activeRequestsByServer[server] = activeRequestsForServer + 1;

Expand Down Expand Up @@ -182,5 +289,12 @@ define([
*/
RequestScheduler.maximumRequests = 10;

/**
* Specifies if the request scheduler should prioritize incoming requests
* @type {Boolean}
* @default true
*/
RequestScheduler.prioritize = true;

return RequestScheduler;
});
19 changes: 19 additions & 0 deletions Source/Core/RequestType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*global define*/
define([
'../Core/freezeObject'
], function(
freezeObject) {
"use strict";

/**
* @private
*/
var RequestType = {
TERRAIN : 0,
IMAGERY : 1,
TILES3D : 2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Down the road we'll need a bit more control here, e.g., what if there are two 3D tilesets: one for a city and one for a point cloud. Cesium won't know the best priority, but the user might want to override it.

Also, what if a tile has a glTF payload, the glTF requests should be TILES3D, but we might also want to bucket them based on geometry vs. texture.

No need to change anything now, but we'll need to think these through.

OTHER : 3
};

return freezeObject(RequestType);
});
2 changes: 2 additions & 0 deletions Source/Core/TerrainProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ define([
* @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited,
* or false if the request should be initiated regardless of the number of requests
* already in progress.
* @param {Number} [distance] The distance of the tile from the camera, used to prioritize requests.
*
* @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry. If this method
* returns undefined instead of a promise, it is an indication that too many requests are already
* pending and the request will be retried later.
Expand Down
7 changes: 5 additions & 2 deletions Source/Core/VRTheWorldTerrainProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ define([
'./Math',
'./Rectangle',
'./RequestScheduler',
'./RequestType',
'./TerrainProvider',
'./TileProviderError'
], function(
Expand All @@ -35,6 +36,7 @@ define([
CesiumMath,
Rectangle,
RequestScheduler,
RequestType,
TerrainProvider,
TileProviderError) {
"use strict";
Expand Down Expand Up @@ -253,11 +255,12 @@ define([
* @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited,
* or false if the request should be initiated regardless of the number of requests
* already in progress.
* @param {Number} [distance] The distance of the tile from the camera, used to prioritize requests.
* @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry. If this method
* returns undefined instead of a promise, it is an indication that too many requests are already
* pending and the request will be retried later.
*/
VRTheWorldTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests) {
VRTheWorldTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests, distance) {
if (!this.ready) {
throw new DeveloperError('requestTileGeometry must not be called before ready returns true.');
}
Expand All @@ -274,7 +277,7 @@ define([

throttleRequests = defaultValue(throttleRequests, true);
if (throttleRequests) {
promise = RequestScheduler.throttleRequest(url, loadImage);
promise = RequestScheduler.throttleRequest(url, loadImage, RequestType.TERRAIN, distance);
if (!defined(promise)) {
return undefined;
}
Expand Down
5 changes: 3 additions & 2 deletions Source/Scene/ArcGisMapServerImageryProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,22 +578,23 @@ define([
* @param {Number} x The tile X coordinate.
* @param {Number} y The tile Y coordinate.
* @param {Number} level The tile level.
* @param {Number} [distance] The distance of the tile from the camera, used to prioritize requests.
* @returns {Promise.<Image|Canvas>|undefined} A promise for the image that will resolve when the image is available, or
* undefined if there are too many active requests to the server, and the request
* should be retried later. The resolved image may be either an
* Image or a Canvas DOM object.
*
* @exception {DeveloperError} <code>requestImage</code> must not be called before the imagery provider is ready.
*/
ArcGisMapServerImageryProvider.prototype.requestImage = function(x, y, level) {
ArcGisMapServerImageryProvider.prototype.requestImage = function(x, y, level, distance) {
//>>includeStart('debug', pragmas.debug);
if (!this._ready) {
throw new DeveloperError('requestImage must not be called before the imagery provider is ready.');
}
//>>includeEnd('debug');

var url = buildImageUrl(this, x, y, level);
return ImageryProvider.loadImage(this, url);
return ImageryProvider.loadImage(this, url, distance);
};

/**
Expand Down
Loading