experimental: render queue. rather than dumping all retrieved data into the render code immediately, queue it up and feed it in a bit at a time

also, we pause a very short time between processing sections of the queue - should help ensure things are responsive if user interacts while rendering
todo: better status bar updates while queue is being processed
This commit is contained in:
Jon Atkins 2014-03-21 04:43:33 +00:00
parent 0a9cc966cd
commit 8eeec788df

View File

@ -12,6 +12,10 @@ window.MapDataRequest = function() {
this.activeRequestCount = 0;
this.requestedTiles = {};
this.renderQueue = [];
this.renderQueueTimer = undefined;
this.renderQueuePaused = false;
this.idle = false;
@ -46,10 +50,12 @@ window.MapDataRequest = function() {
// delay before processing the queue after error==TIMEOUT requests. this is a less severe error than other errors
this.TIMEOUT_REQUEST_RUN_QUEUE_DELAY = 2;
// delay before repeating the render loop. this gives a better chance for user interaction
this.RENDER_PAUSE = 0.05; //50ms
this.REFRESH_CLOSE = 120; // refresh time to use for close views z>12 when not idle and not moving
this.REFRESH_FAR = 600; // refresh time for far views z <= 12
this.REFRESH_CLOSE = 300; // refresh time to use for close views z>12 when not idle and not moving
this.REFRESH_FAR = 900; // refresh time for far views z <= 12
this.FETCH_TO_REFRESH_FACTOR = 2; //refresh time is based on the time to complete a data fetch, times this value
// ensure we have some initial map status
@ -81,7 +87,7 @@ window.MapDataRequest.prototype.mapMoveStart = function() {
this.setStatus('paused');
this.clearTimeout();
this.pauseRenderQueue(true);
}
window.MapDataRequest.prototype.mapMoveEnd = function() {
@ -99,6 +105,7 @@ window.MapDataRequest.prototype.mapMoveEnd = function() {
if (remainingTime > this.MOVE_REFRESH) {
this.setStatus('done','Map moved, but no data updates needed');
this.refreshOnTimeout(remainingTime);
this.pauseRenderQueue(false);
return;
}
}
@ -135,8 +142,11 @@ window.MapDataRequest.prototype.refreshOnTimeout = function(seconds) {
console.log('starting map refresh in '+seconds+' seconds');
// 'this' won't be right inside the callback, so save it
var savedContext = this;
this.timer = setTimeout ( function() { savedContext.timer = undefined; savedContext.refresh(); }, seconds*1000);
// also, double setTimeout used to ensure the delay occurs after any browser-related rendering/updating/etc
var _this = this;
this.timer = setTimeout ( function() {
_this.timer = setTimeout ( function() { _this.timer = undefined; _this.refresh(); }, seconds*1000);
}, 0);
this.timerExpectedTimeoutTime = new Date().getTime() + seconds*1000;
}
@ -166,6 +176,7 @@ window.MapDataRequest.prototype.refresh = function() {
this.refreshStartTime = new Date().getTime();
this.debugTiles.reset();
this.resetRenderQueue();
// a 'set' to keep track of hard failures for tiles
this.tileErrorCount = {};
@ -251,19 +262,12 @@ window.MapDataRequest.prototype.refresh = function() {
// out zoom tiles for a detail level)
if (this.cache && this.cache.isFresh(tile_id) ) {
// data is fresh in the cache - just render it
this.debugTiles.setState(tile_id, 'cache-fresh');
this.render.processTileData (this.cache.get(tile_id));
this.pushRenderQueue(tile_id,this.cache.get(tile_id),'cache-fresh');
this.cachedTileCount += 1;
} else {
// no fresh data
// render the cached stale data, if we have it. this ensures *something* appears quickly when possible
// var old_data = this.cache && this.cache.get(tile_id);
// if (old_data) {
// this.render.processTileData (old_data);
// }
// tile needed. calculate the distance from the centre of the screen, to optimise the load order
var latCenter = (latNorth+latSouth)/2;
@ -315,37 +319,19 @@ window.MapDataRequest.prototype.refresh = function() {
window.MapDataRequest.prototype.delayProcessRequestQueue = function(seconds,isFirst) {
if (this.timer === undefined) {
var savedContext = this;
this.timer = setTimeout ( function() { savedContext.timer = undefined; savedContext.processRequestQueue(isFirst); }, seconds*1000 );
var _this = this;
this.timer = setTimeout ( function() {
_this.timer = setTimeout ( function() { _this.timer = undefined; _this.processRequestQueue(isFirst); }, seconds*1000 );
}, 0);
}
}
window.MapDataRequest.prototype.processRequestQueue = function(isFirstPass) {
// if nothing left in the queue, end the render. otherwise, send network requests
// if nothing left in the queue, finish
if (Object.keys(this.queuedTiles).length == 0) {
this.render.endRenderPass();
var endTime = new Date().getTime();
var duration = (endTime - this.refreshStartTime)/1000;
console.log('finished requesting data! (took '+duration+' seconds to complete)');
window.runHooks ('mapDataRefreshEnd', {});
var longStatus = 'Tiles: ' + this.cachedTileCount + ' cached, ' +
this.successTileCount + ' loaded, ' +
(this.staleTileCount ? this.staleTileCount + ' stale, ' : '') +
(this.failedTileCount ? this.failedTileCount + ' failed, ' : '') +
'in ' + duration + ' seconds';
// refresh timer based on time to run this pass, with a minimum of REFRESH seconds
var minRefresh = map.getZoom()>12 ? this.REFRESH_CLOSE : this.REFRESH_FAR;
var refreshTimer = Math.max(minRefresh, duration*this.FETCH_TO_REFRESH_FACTOR);
this.refreshOnTimeout(refreshTimer);
this.setStatus (this.failedTileCount ? 'errors' : this.staleTileCount ? 'out of date' : 'done', longStatus);
// we leave the renderQueue code to handle ending the render pass now
return;
}
@ -436,8 +422,7 @@ window.MapDataRequest.prototype.requeueTile = function(id, error) {
var data = this.cache ? this.cache.get(id) : undefined;
if (data) {
// we have cached data - use it, even though it's stale
this.debugTiles.setState (id, 'cache-stale');
this.render.processTileData (data);
this.pushRenderQueue(id,data,'cache-stale');
this.staleTileCount += 1;
} else {
// no cached data
@ -472,7 +457,7 @@ window.MapDataRequest.prototype.handleResponse = function (data, tiles, success)
var timeoutTiles = [];
if (!success || !data || !data.result) {
console.warn("Request.handleResponse: request failed - requeuing...");
console.warn('Request.handleResponse: request failed - requeuing...'+(data && data.error?' error: '+data.error:''));
//request failed - requeue all the tiles(?)
@ -514,9 +499,8 @@ window.MapDataRequest.prototype.handleResponse = function (data, tiles, success)
// if this tile was in the render list, render it
// (requests aren't aborted when new requests are started, so it's entirely possible we don't want to render it!)
if (id in this.queuedTiles) {
this.debugTiles.setState (id, 'ok');
this.render.processTileData (val);
this.pushRenderQueue(id,val,'ok');
delete this.queuedTiles[id];
this.successTileCount += 1;
@ -565,3 +549,109 @@ window.MapDataRequest.prototype.handleResponse = function (data, tiles, success)
//.. should this also be delayed a small amount?
this.delayProcessRequestQueue(nextQueueDelay);
}
window.MapDataRequest.prototype.resetRenderQueue = function() {
this.renderQueue = [];
if (this.renderQueueTimer) {
clearTimeout(this.renderQueueTimer);
this.renderQueueTimer = undefined;
}
this.renderQueuePaused = false;
}
window.MapDataRequest.prototype.pushRenderQueue = function (id, data, status) {
this.debugTiles.setState(id,'render-queue');
this.renderQueue.push({
id:id,
// the data in the render queue is modified as we go, so we need to copy the values of the arrays. just storing the reference would modify the data in the cache!
deleted: (data.deletedGameEntityGuids||[]).slice(0),
entities: (data.gameEntities||[]).slice(0),
status:status});
if (!this.renderQueuePaused) {
this.startQueueTimer(this.RENDER_PAUSE);
}
}
window.MapDataRequest.prototype.startQueueTimer = function(delay) {
if (this.renderQueueTimer === undefined) {
var _this = this;
this.renderQueueTimer = setTimeout( function() {
_this.renderQueueTimer = setTimeout ( function() { _this.renderQueueTimer = undefined; _this.processRenderQueue(); }, (delay||0)*1000 );
}, 0);
}
}
window.MapDataRequest.prototype.pauseRenderQueue = function(pause) {
this.renderQueuePaused = pause;
if (pause) {
if (this.renderQueueTimer) {
clearTimeout(this.renderQueueTimer);
this.renderQueueTimer = undefined;
}
} else {
if (this.renderQueue.length > 0) {
this.startQueueTimer(this.RENDER_PAUSE);
}
}
}
window.MapDataRequest.prototype.processRenderQueue = function() {
var drawEntityLimit = 500;//TODO: fine-tune - and mobile/desktop specific values?
//TODO: we don't take account of how many of the entities are actually new/removed - they
// could already be drawn and not changed. will see how it works like this...
while (drawEntityLimit > 0 && this.renderQueue.length > 0) {
var current = this.renderQueue[0];
if (current.deleted.length > 0) {
var deleteThisPass = current.deleted.splice(0,drawEntityLimit);
drawEntityLimit -= deleteThisPass.length;
this.render.processDeletedGameEntityGuids(deleteThisPass);
}
if (drawEntityLimit > 0 && current.entities.length > 0) {
var drawThisPass = current.entities.splice(0,drawEntityLimit);
drawEntityLimit -= drawThisPass.length;
this.render.processGameEntities(drawThisPass);
}
if (current.deleted.length == 0 && current.entities.length == 0) {
this.renderQueue.splice(0,1);
this.debugTiles.setState(current.id, current.status);
}
}
if (this.renderQueue.length > 0) {
this.startQueueTimer(this.RENDER_PAUSE);
} else if (Object.keys(this.queuedTiles).length == 0) {
this.render.endRenderPass();
var endTime = new Date().getTime();
var duration = (endTime - this.refreshStartTime)/1000;
console.log('finished requesting data! (took '+duration+' seconds to complete)');
window.runHooks ('mapDataRefreshEnd', {});
var longStatus = 'Tiles: ' + this.cachedTileCount + ' cached, ' +
this.successTileCount + ' loaded, ' +
(this.staleTileCount ? this.staleTileCount + ' stale, ' : '') +
(this.failedTileCount ? this.failedTileCount + ' failed, ' : '') +
'in ' + duration + ' seconds';
// refresh timer based on time to run this pass, with a minimum of REFRESH seconds
var minRefresh = map.getZoom()>12 ? this.REFRESH_CLOSE : this.REFRESH_FAR;
var refreshTimer = Math.max(minRefresh, duration*this.FETCH_TO_REFRESH_FACTOR);
this.refreshOnTimeout(refreshTimer);
this.setStatus (this.failedTileCount ? 'errors' : this.staleTileCount ? 'out of date' : 'done', longStatus);
}
}