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
88 changes: 85 additions & 3 deletions Source/Core/DoublyLinkedList.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
define([
'../Core/defined',
'../Core/defineProperties'
'./defined',
'./defineProperties'
], function(
defined,
defineProperties) {
Expand Down Expand Up @@ -29,14 +29,18 @@ define([
this.next = next;
}

/**
* Adds the item to the end of the list
* @param {Object} [item]
* @return {DoublyLinkedListNode}
*/
DoublyLinkedList.prototype.add = function(item) {
var node = new DoublyLinkedListNode(item, this.tail, undefined);

if (defined(this.tail)) {
this.tail.next = node;
this.tail = node;
} else {
// Insert into empty linked list
this.head = node;
this.tail = node;
}
Expand All @@ -46,6 +50,43 @@ define([
return node;
};

/**
* Adds the item to the front of the list
* @param {Object} [item]
* @return {DoublyLinkedListNode}
*/
DoublyLinkedList.prototype.addFront = function(item) {
var node = new DoublyLinkedListNode(item, undefined, this.head);

if (defined(this.head)) {
this.head.previous = node;
this.head = node;
} else {
this.head = node;
this.tail = node;
}

++this._length;

return node;
};

/**
* Moves the given node to the front of the list
* @param {DoublyLinkedListNode} node
*/
DoublyLinkedList.prototype.moveToFront = function(node) {
if (!defined(node) || this.head === node) {
return;
}

remove(this, node);
node.next = this.head;
node.previous = undefined;
this.head.previous = node;
this.head = node;
};

function remove(list, node) {
if (defined(node.previous) && defined(node.next)) {
node.previous.next = node.next;
Expand All @@ -68,6 +109,10 @@ define([
node.previous = undefined;
}

/**
* Removes the given node from the list
* @param {DoublyLinkedListNode} node
*/
DoublyLinkedList.prototype.remove = function(node) {
if (!defined(node)) {
return;
Expand All @@ -78,6 +123,43 @@ define([
--this._length;
};

/**
* Removes all nodes after the start index (inclusive)
* @param {Number} startIndex The index of the first node to remove
*/
DoublyLinkedList.prototype.removeAfter = function(startIndex) {
var currentLength = this._length;
if (!defined(startIndex) || startIndex >= currentLength) {
return;
}

if (startIndex === 0) {
this.head = undefined;
this.tail = undefined;
this._length = 0;
return;
}

if (startIndex === currentLength - 1) {
this.remove(this.tail);
return;
}

var node = this.head;
for (var i = 0; i < startIndex; ++i) {
node = node.next;
}

node.previous.next = undefined;
this.tail = node.previous;
this._length = startIndex;
};

/**
* Moves nextNode after node
* @param {DoublyLinkedListNode} node
* @param {DoublyLinkedListNode} nextNode
*/
DoublyLinkedList.prototype.splice = function(node, nextNode) {
if (node === nextNode) {
return;
Expand Down
174 changes: 174 additions & 0 deletions Source/Core/LRUCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
define([
'./defined',
'./defineProperties',
'./getTimestamp',
'./DeveloperError',
'./DoublyLinkedList'
], function(
defined,
defineProperties,
getTimestamp,
DeveloperError,
DoublyLinkedList) {
'use strict';

/**
* 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 LRUCache
* @constructor
* @private
*/
function LRUCache(capacity, expiration) {
this._list = new DoublyLinkedList();
this._hash = {};
this._hasCapacity = defined(capacity);
this._capacity = capacity;

this._hasExpiration = defined(expiration);
this._expiration = expiration;
this._interval = undefined;
}

defineProperties(LRUCache.prototype, {
/**
* Gets the cache length
* @memberof LRUCache.prototype
* @type {Number}
* @readonly
*/
length : {
get : function() {
return this._list.length;
}
}
});

/**
* Retrieves the value associated with the provided key.
*
* @param {String|Number} key The key whose value is to be retrieved.
* @returns {Object} The associated value, or undefined if the key does not exist in the collection.
*/
LRUCache.prototype.get = function(key) {
//>>includeStart('debug', pragmas.debug);
if (typeof key !== 'string' && typeof key !== 'number') {
throw new DeveloperError('key is required to be a string or number.');
}
//>>includeEnd('debug');
var list = this._list;
var node = this._hash[key];
if (defined(node)) {
list.moveToFront(node);
var item = node.item;
item.touch();
return item.value;
}
};

/**
* Associates the provided key with the provided value. If the key already
* exists, it is overwritten with the new value.
*
* @param {String|Number} key A unique identifier.
* @param {Object} value The value to associate with the provided key.
*/
LRUCache.prototype.set = function(key, value) {
//>>includeStart('debug', pragmas.debug);
if (typeof key !== 'string' && typeof key !== 'number') {
throw new DeveloperError('key is required to be a string or number.');
}
//>>includeEnd('debug');
var hash = this._hash;
var list = this._list;

var node = hash[key];
var item;
if (!defined(node)) {
item = new Item(key, value);
node = list.addFront(item);
hash[key] = node;
if (this._hasExpiration) {
LRUCache._checkExpiration(this);
}
if (this._hasCapacity && list.length > this._capacity) {
var tail = list.tail;
delete this._hash[tail.item.key];
list.remove(tail);
}
} else {
item = node.item;
item.value = value;
item.touch();
list.moveToFront(node);
}
};

function prune(cache) {
var currentTime = getTimestamp();
var pruneAfter = currentTime - cache._expiration;

var list = cache._list;
var node = list.tail;
var index = list.length;
while (defined(node) && node.item.timestamp < pruneAfter) {
node = node.previous;
index--;
}

if (node === list.tail) {
return;
}

if (!defined(node)) {
node = list.head;
} else {
node = node.next;
}

while (defined(node)) {
delete cache._hash[node.item.key];
node = node.next;
}

list.removeAfter(index);
}

function checkExpiration(cache) {
if (defined(cache._interval)) {
return;
}
function loop() {
if (!cache._hasExpiration || cache.length === 0) {
clearInterval(cache._interval);
cache._interval = undefined;
return;
}

prune(cache);

if (cache.length === 0) {
clearInterval(cache._interval);
cache._interval = undefined;
}
}
cache._interval = setInterval(loop, 1000);
}

function Item(key, value) {
this.key = key;
this.value = value;
this.timestamp = getTimestamp();
}

Item.prototype.touch = function() {
this.timestamp = getTimestamp();
};

//exposed for testing
LRUCache._checkExpiration = checkExpiration;
LRUCache._prune = prune;

return LRUCache;
});
24 changes: 20 additions & 4 deletions Source/Core/sampleTerrain.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
define([
'../ThirdParty/when',
'./Check'
'./defined',
'./Check',
'./LRUCache'
], function(
when,
Check) {
defined,
Check,
LRUCache) {
'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).


/**
* Initiates a terrain height query for an array of {@link Cartographic} positions by
* requesting tiles from a terrain provider, sampling, and interpolating. The interpolation
Expand Down Expand Up @@ -86,8 +92,18 @@ define([
var tilePromises = [];
for (i = 0; i < tileRequests.length; ++i) {
var tileRequest = tileRequests[i];
var requestPromise = tileRequest.terrainProvider.requestTileGeometry(tileRequest.x, tileRequest.y, tileRequest.level);
var tilePromise = when(requestPromise, createInterpolateFunction(tileRequest), createMarkFailedFunction(tileRequest));
var cacheKey = tileRequest.x + '-' + tileRequest.y + '-' + tileRequest.level;
var requestPromise;
var cachedTilePromise = cache.get(cacheKey);
if (defined(cachedTilePromise)) {
requestPromise = cachedTilePromise;
} else {
requestPromise = tileRequest.terrainProvider.requestTileGeometry(tileRequest.x, tileRequest.y, tileRequest.level);
cache.set(cacheKey, requestPromise);
}
var tilePromise = requestPromise
.then(createInterpolateFunction(tileRequest))
.otherwise(createMarkFailedFunction(tileRequest));
tilePromises.push(tilePromise);
}

Expand Down
Loading