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
Merged

Request Scheduler Prioritization #3397

merged 13 commits into from Jan 25, 2016

Conversation

lilleyse
Copy link
Contributor

@lilleyse lilleyse commented Jan 7, 2016

Continuation of #3371 for #3241

I added support for prioritizing requests based on distance. While we want something more generic in the future, I would like some feedback on the approach I'm taking.

The request scheduler uses information about the previous frame to determine the number of requests allowed for each server during the given frame. resetBudgets will sort by distance any requests that couldn't be made last frame and estimate new budgets. When a request is made, it can give a yes/no answer by either returning a promise or undefined, as it did before. The request can be sent immediately and not defer until the end of the frame. There may be cases where the view changes drastically between frames and the estimates from the previous frame are off, but this may not be a problem. If it is, I can introduce the concept of stealing from budgets like in JobScheduler.

I had to modify a lot of functions in order to send the distance with each request. I considered generating the distance using the x, y, level, and tiling scheme instead, which would make the code cleaner but has its own limitations.

@@ -58,6 +58,8 @@

/*global jasmineRequire,jasmine,exports,specs*/

Cesium.RequestScheduler.prioritize = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A lot of the specs fail if prioritization is enabled because it lags behind a frame. So I added this here for simplicity sake, but it's probably a bad place...

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like an OK place to me, I don't think there is a beforeAll that applies to all suites.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, add a comment about why this line is here.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 7, 2016

I considered generating the distance using the x, y, level, and tiling scheme instead, which would make the code cleaner but has its own limitations.

This won't be general enough because, for example, we want to prioritize all requests like glTF assets and their external resources.

@@ -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.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 7, 2016

It would be good to test this with something like the DC buildings and Bing imagery, and see how it compares to before 3D Tiles requests were prioritized. http://cesiumjs.org/WashingtonDC/

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).

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 7, 2016

That's all my comments.

@lilleyse
Copy link
Contributor Author

Updated.

I created a Request class that is like the priority object mentioned in these comments, but also stores additional information about the request and is used inside RequestScheduler.

I added the ability to defer a request, so if there aren't any open slots on the current frame it will still return a promise that will be resolved once an opening appears. This is useful for all the requests that aren't imagery/terrain/3d-tiles related. I went through the codebase looking for loading functions like loadArrayBuffer, loadJson, etc and changed them to use the RequestScheduler. @mramato suggested the possibility of doing the inverse approach, and having loadWithXhr call RequestScheduler.

I haven't tested with the Washington DC buildings yet because I'm having some problems converting the tileset, but I hope to see it working soon. Also CHANGES.md still needs to be updated, but I'm waiting until this is final.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 14, 2016

@lilleyse can you merge 3d-tiles into this?

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 14, 2016

@mramato suggested the possibility of doing the inverse approach, and having loadWithXhr call RequestScheduler.

This was my gut too, but I'm open to other approaches. Will have a look now.

*/
this.distance = defaultValue(options.distance, 0.0);

// Helper members for RequestScheduler
Copy link
Contributor

Choose a reason for hiding this comment

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

Mark these as @private since they are being used like C#'s internal.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 14, 2016

I know this was already merged into 3d-tiles, but the implementation of RequestScheduler.hasAvailableRequests is too slow and allocates memory. This is an important function that is called on each tile during tree traversal. Perhaps we can implement some caching (that the caller could store) to avoid the memory allocation and maybe even implicit hash lookup. I dunno, it might not be perfectly straightforward since, for example, a 3D tileset can reference tiles on many different servers. Perhaps there is a fast approximation or coarse-grained check we can use.

* @returns {Promise.<Object>} A Promise for the requested data.
*/
RequestScheduler.request = function(url, requestFunction, parameters) {
return RequestScheduler.throttleRequest(new Request({
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps this should have a type of "OTHER", "USER", "CUSTOM", or something like that, which could be useful for setting priorities and avoiding starvation.

@@ -1010,7 +1014,7 @@ define([
setCachedGltf(model, cachedGltf);
gltfCache[cacheKey] = cachedGltf;

loadArrayBuffer(url, options.headers).then(function(arrayBuffer) {
RequestScheduler.request(url, loadArrayBuffer, options.headers).then(function(arrayBuffer) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I mentioned this in a past review, but Model may be making requests on behalf of 3D Tiles so I think we should allow an override for the request type via a @private options parameter on the constructor.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 14, 2016

One simple way would be for each content provider to store its server.

Perhaps each tile would store a reference to a small object that is a new RequestsAvailable (or something like that) class. At initialization, they would ask RequestScheduler for this object based on their url, then they can check it instead of the slow path currently in RequestScheduler. Under-the-hood, RequestScheduler could do something similar.

The idea is that if we know a request won't go through this frame, we want to learn that as quickly as possible and bail on tree traversal. Likewise, we want requests that may go through to be queued up as quickly as possible with minimal scheduler overhead.

var removeFunction = removeFromProcessingQueue(tiles3D, tile);
when(tile.processingPromise).then(addToProcessingQueue(tiles3D, tile)).otherwise(removeFunction);
when(tile.readyPromise).then(removeFunction).otherwise(removeFunction);
}
}

function hasAvailableRequests(tiles3D) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this line is wrong:

return RequestScheduler.hasAvailableRequests(tiles3D._url)

This assumes that the tile's content has the same base path as tileset.json, right? They could be on two separate servers.

@lilleyse
Copy link
Contributor Author

Updated.

@@ -231,7 +237,19 @@ define([
url = proxy.getURL(url);
}

var promise = RequestScheduler.throttleRequest(url, loadImage);
// TODO : request used to be a boolean called throttleRequest. Continue to handle that case until it is deprecated.
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 TODO and friends, and submit an issue for the deprecation.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 18, 2016

Just those comments.

Are you happy with how this worked for the DC dataset?

if (!tile.isContentUnloaded()) {
var stats = tiles3D._statistics;
++stats.numberOfPendingRequests;
addLoadProgressEvent(tiles3D);
Copy link
Contributor

Choose a reason for hiding this comment

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

This call keeps track of the number of requests so it is possible that it could increase the number of pending requests here even though the scheduler doesn't make the request.

At one point we talked about adding an event (or promise or whatever) that fires when the request is actually made (or not). Perhaps we should use that to call addLoadProgressEvent.

Note that this should all happen before events are raised at the end of the frame with frameState.afterRender.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would also be fine (perhaps even prefer) raising this event once per frame instead of per tile. The former is probably better for a UI, and the later for debugging.

@lilleyse
Copy link
Contributor Author

I cleaned up the code and have been testing with the DC dataset. When testing with prioritization enabled vs. disabled, there are some small differences but its hard to say that one method is actually better than another. It's easier to notice the benefits in contrived cases where the maximum requests is set to some low number. Usually both 3D Tiles and Imagery/Terrain servers have enough open slots to do their work with or without budgeting.

I also tested the difference for when Cesium3DTileset checks if there are available requests vs. always sending requests (slower, but creates accurate budgets). Like above, its hard to see an actual visual difference. I started working on the starvation issue, but in order to do it correctly accounting for distance and the distance ranges of other budgets, it just seemed too complicated for little gain.

@lilleyse
Copy link
Contributor Author

This is ready from my end.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 22, 2016

Looks good. Please cherry pick the commits for a PR into master and update the roadmap as we discussed offline.

@lilleyse
Copy link
Contributor Author

Is this ready to be merged here?

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 25, 2016

Yes.

pjcozzi added a commit that referenced this pull request Jan 25, 2016
@pjcozzi pjcozzi merged commit 7e0d85a into 3d-tiles Jan 25, 2016
@pjcozzi pjcozzi deleted the request-scheduler branch January 25, 2016 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants