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

Add caching to sampleTerrain #6284

Merged
merged 13 commits into from
Mar 16, 2018
Merged

Add caching to sampleTerrain #6284

merged 13 commits into from
Mar 16, 2018

Conversation

hpinkos
Copy link
Contributor

@hpinkos hpinkos commented Feb 27, 2018

  • Added LRUCache, a data structure for managing a ordered list based on what was most recently accessed. It has a DoublyLinkedList for keeping track of the order, and a dictionary for fast lookups. It has a capacity option so the size doesn't grow beyond the capacity, and an expiration option to clean out old entries when LRUCache.prune is called
  • Added DoublyLinkedList.addFront for pushing nodes to the front of the list
  • Added DoublyLinkedList.moveToFront for moving an existing node to the front of the list
  • Added DoublyLinkedList.removeAfter for removing all nodes including and after a given index
  • Updated sampleTerrain so it stores recently fetched tiles in an LRUCache. It sets the cache size limit to 256 entries, and sets the cache entries to expire and get cleared out after 10 seconds without being accessed.

@cesium-concierge
Copy link

Signed CLA is on file.

@hpinkos, thanks for the pull request! Maintainers, we have a signed CLA from @hpinkos, so you can review this at any time.

⚠️ I noticed that CHANGES.md has not been updated. If this change updates the public API in any way, fixes a bug, or makes any non-trivial update, please add a bullet point to CHANGES.md and comment on this pull request so we know it was updated. For more info, see the Pull Request Guidelines.


I am a bot who helps you make Cesium awesome! Contributions to my configuration are welcome.

🌍 🌎 🌏

@hpinkos
Copy link
Contributor Author

hpinkos commented Feb 27, 2018

Here's some sample code I used for testing this:

var worldTerrain = Cesium.createWorldTerrain({
    requestWaterMask: true,
    requestVertexNormals: true
});

var viewer = new Cesium.Viewer('cesiumContainer', {
    terrainProvider: worldTerrain
});

Sandcastle.addToolbarButton('Sample', function() {
    Cesium.sampleTerrainMostDetailed(worldTerrain, [Cesium.Cartographic.fromDegrees(40, 40)]);
});

Sandcastle.addToolbarButton('Sample random', function() {
    Cesium.sampleTerrainMostDetailed(worldTerrain, [Cesium.Cartographic.fromDegrees(Math.random() * 80, Math.random() * 80)]);
});

@hpinkos
Copy link
Contributor Author

hpinkos commented Mar 9, 2018

This is ready for review

@hpinkos
Copy link
Contributor Author

hpinkos commented Mar 15, 2018

Could someone review this please? @bagnell or @lilleyse maybe?
This change is related to #6214. #6214 calls sampleTerrainMostDetailed to get the height relative to terrain, and this change keeps sampleTerrain from issuing an http request each time it's called.

@lilleyse
Copy link
Contributor

I'll start taking a look.


var i;
var node = this.head;
for (i = 0; i < startIndex; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Style suggestions:
Remove var i; and declare it in the loop.
Change i++ to ++i

requestAnimationFrame,
DeveloperError,
DoublyLinkedList) {
'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

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

Fix indentation for includes.

'./defineProperties',
'./getTimestamp',
'./requestAnimationFrame',
'./DeveloperError',
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor point, but requestAnimationFrame isn't in alphabetical order.

requestPromise = tileRequest.terrainProvider.requestTileGeometry(tileRequest.x, tileRequest.y, tileRequest.level);
cache.set(cacheKey, requestPromise);
}
var tilePromise = when(requestPromise)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is when( needed?

], function(
LRUCache,
getTimestamp) {
'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

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

Fix indentation for includes.

* @alias AssociativeArray
* @constructor
*/
function LRUCache(capacity, expiration) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually not sure.. in Cesium is the preferred style to put the constructor first always or okay to have helpers above it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, good to know.

It looks like DoublyLinkedList was fixed but not LRUCache.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whoops! All fixed now

* A cache for storing key-value pairs
* @param {Number} [capacity] The capacity of the cache. If undefined, the size will be unlimited.
* @param {Number} [expiration] The number of milliseconds before an item in the cache expires and will be discarded when LRUCache.prune is called. If undefined, items do not expire.
* @alias AssociativeArray
Copy link
Contributor

Choose a reason for hiding this comment

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

AssociativeArray -> LRUCache

* A cache for storing key-value pairs
* @param {Number} [capacity] The capacity of the cache. If undefined, the size will be unlimited.
* @param {Number} [expiration] The number of milliseconds before an item in the cache expires and will be discarded when LRUCache.prune is called. If undefined, items do not expire.
* @alias AssociativeArray
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this class be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It probably doesn't have to be, but it isn't really useful in the Cesium API so I decided to make it private

prune(cache);

if (cache.length > 0) {
requestAnimationFrame(loop);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure that it's good practice to call requestAnimationFrame here or anywhere besides the main render loop in CesiumWidget. I don't think it will break anything, but it may request a render that isn't desired.

At the same time, I'm not sure what the alternative would be. Is there any way for the lru cache to be owned by something with access to frame state?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I originally had Scene.update tell sampleTerrain to call cache.checkExpiration, but that results in that function being called every frame. This way, LRUCache can manage it's own expiration and doesn't execute the loop when there's nothing in the cache.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that LRUCache is cleaner when it manages itself, I'm just worried about the calls to requestAnimationFrame.

Would it be not completely terrible to call prune after each set call? Or maybe use setTimeout if it doesn't need to prune every frame?

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer what @hpinkos has done here. It will not trigger a Cesium render, it will simply let LRUcache does it's processing all at once on the next browser (not Cesium) render

Copy link
Contributor

Choose a reason for hiding this comment

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

My concern is that it will trigger a browser render unnecessarily. Or maybe it won't... maybe that's going to happen every frame regardless. But I still think requestAnimationFrame really only belongs inside the main render loop since that's where we "ask" for a frame. But if this is the cleanest way to get the job done, then I suppose it's ok.

Copy link
Contributor

@lilleyse lilleyse Mar 31, 2018

Choose a reason for hiding this comment

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

Two week later update. I read up on requestAnimationFrame more and it seems that it is purely a callback aggregator and it won't force a render like I thought might be happening. Sorry if this is already common knowledge...

I still think its nice that requestAnimationFrame is called in just one place, but technically it should be fine to call in multiple.

https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animation-frames

Copy link
Contributor

Choose a reason for hiding this comment

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

For the render loop, I agree incompletely, there should only be one requestAnimationFrame call. For anything we want to have happen independent of the render loop, multiple requestAnimationFrame make total sense. I think we need to continue to decouple things from our actual webgl render loop and move them one level up into requestAnimationFrame .

});

it('set throws with undefined key', function() {
var associativeArray = new LRUCache();
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to cache.

@hpinkos
Copy link
Contributor Author

hpinkos commented Mar 15, 2018

@lilleyse ready for another look

@hpinkos
Copy link
Contributor Author

hpinkos commented Mar 15, 2018

@lilleyse anything else?

@lilleyse
Copy link
Contributor

Nope that's it, thanks @hpinkos.

@lilleyse lilleyse merged commit 11bd15f into master Mar 16, 2018
@lilleyse lilleyse deleted the cache-sampleterrain branch March 16, 2018 13:16
@pjcozzi
Copy link
Contributor

pjcozzi commented Mar 31, 2018

I know the public API didn't change, but this improves performance for some cases, right? Update CHANGES.md?

'use strict';

var cache = new LRUCache(256, 10000);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this was discussed and this is the best solution - if so please point me to it, but this is not how I originally suggested to implement this.

We generally want to centralize all the time-related cache logic so I suggested that we use the render loop to flush any caches just like we do for the shader cache. Are we sure this setInterval approach is better? I think explicit and consistent control of caches in our render loop (but still based on time stamp) would be more cohesive.

Copy link
Contributor

Choose a reason for hiding this comment

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

There was some discussion here: #6284 (comment)

Since sampleTerrain doesn't have access to frameState some of the options were:

  • Scene edits a global property in sampleTerrain that tells it to flush the cache.
  • sampleTerrain uses requestAnimationFrame to hook into the render loop to flush the cache, making sampleTerrain self contained.
  • sampleTerrain uses setInterval instead of requestAnimationFrame.

One benefit of the third approach (which I guess is opposite your original suggestion) is that sampleTerrain is not tied to the render loop at all, making it possible to use in Node.js or wherever else the Cesium engine isn't actually running.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I did not think about the standalone use case. I'm still not sure that a standalone API would implicitly cache and flush like this (imagine if an OpenGL driver did thinks like this); the user would likely have more fine-grained control and would be able to pass in a cache policy or whatever.

Still not convinced that having random caches with random set intervals is a good design in Cesium, but proceed as you see fit.

Copy link
Contributor

Choose a reason for hiding this comment

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

Still not convinced that having random caches with random set intervals is a good design in Cesium

I think everyone agrees with this sentiment. In my opinion what Cesium really needs is a shared centralized cache (just like we also need a centralized worker pool).

@hpinkos
Copy link
Contributor Author

hpinkos commented Apr 2, 2018

Do we want to revert this change? I don't think I need it for the things I'm working on after all. @pjcozzi @lilleyse @mramato

@lilleyse
Copy link
Contributor

lilleyse commented Apr 2, 2018

Personally I'm fine with keeping it as it may still be useful for other cases, and the caching system can be changed whenever we are ready. But if anyone wants to revert that sounds fine too.

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.

5 participants