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

Re-request expired resources #3944

Merged
merged 15 commits into from
Jan 24, 2017
4 changes: 2 additions & 2 deletions bench/benchmarks/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ function preloadAssets(stylesheet, callback) {

function getTile(url, callback) {
ajax.getArrayBuffer(url, (err, response) => {
assets.tiles[url] = response;
callback(err, response);
assets.tiles[url] = response.data;
callback(err, response.data);
});
}

Expand Down
4 changes: 4 additions & 0 deletions js/source/raster_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class RasterTileSource extends Evented {
return callback(err);
}

tile.setExpiryData(img);
delete img.cacheControl;
delete img.expires;

const gl = this.map.painter.gl;
tile.texture = this.map.painter.getTileTexture(img.width);
if (tile.texture) {
Expand Down
67 changes: 52 additions & 15 deletions js/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class SourceCache extends Evented {

this._tiles = {};
this._cache = new Cache(0, this.unloadTile.bind(this));
this._timers = {};
this._cacheTimers = {};

this._isIdRenderable = this._isIdRenderable.bind(this);
}
Expand Down Expand Up @@ -133,21 +135,25 @@ class SourceCache extends Evented {
reload() {
this._cache.reset();
for (const i in this._tiles) {
const tile = this._tiles[i];

// The difference between "loading" tiles and "reloading" tiles is
// that "reloading" tiles are "renderable". Therefore, a "loading"
// tile cannot become a "reloading" tile without first becoming
// a "loaded" tile.
if (tile.state !== 'loading') {
tile.state = 'reloading';
}
this.reloadTile(i, 'reloading');
}
}

reloadTile(id, state) {
const tile = this._tiles[id];

this.loadTile(this._tiles[i], this._tileLoaded.bind(this, this._tiles[i]));
// The difference between "loading" tiles and "reloading" or "expired"
// tiles is that "reloading"/"expired" tiles are "renderable".
// Therefore, a "loading" tile cannot become a "reloading" tile without
// first becoming a "loaded" tile.
if (tile.state !== 'loading') {
tile.state = state;
}

this.loadTile(tile, this._tileLoaded.bind(this, tile, id));
}

_tileLoaded(tile, err) {
_tileLoaded(tile, id, err) {
if (err) {
tile.state = 'errored';
this._source.fire('error', {tile: tile, error: err});
Expand All @@ -156,10 +162,10 @@ class SourceCache extends Evented {

tile.sourceCache = this;
tile.timeAdded = new Date().getTime();

this._setTileReloadTimer(id, tile);
this._source.fire('data', {tile: tile, coord: tile.coord, dataType: 'tile'});

// HACK this is nescessary to fix https://github.com/mapbox/mapbox-gl-js/issues/2986
// HACK this is necessary to fix https://github.com/mapbox/mapbox-gl-js/issues/2986
if (this.map) this.map.painter.tileExtentVAO.vao = null;
}

Expand Down Expand Up @@ -404,14 +410,19 @@ class SourceCache extends Evented {
tile = this._cache.get(wrapped.id);
if (tile) {
tile.redoPlacement(this._source);
if (this._cacheTimers[wrapped.id]) {
clearTimeout(this._cacheTimers[wrapped.id]);
this._cacheTimers[wrapped.id] = undefined;
this._setTileReloadTimer(wrapped.id, tile);
}
}
}

if (!tile) {
const zoom = coord.z;
const overscaling = zoom > this._source.maxzoom ? Math.pow(2, zoom - this._source.maxzoom) : 1;
tile = new Tile(wrapped, this._source.tileSize * overscaling, this._source.maxzoom);
this.loadTile(tile, this._tileLoaded.bind(this, tile));
this.loadTile(tile, this._tileLoaded.bind(this, tile, coord.id));
}

tile.uses++;
Expand All @@ -421,6 +432,26 @@ class SourceCache extends Evented {
return tile;
}

_setTileReloadTimer(id, tile) {
const tileExpires = tile.getExpiry();
if (tileExpires) {
this._timers[id] = setTimeout(() => {
this.reloadTile(id, 'expired');
this._timers[id] = undefined;
}, tileExpires - new Date().getTime());
}
}

_setCacheInvalidationTimer(id, tile) {
const tileExpires = tile.getExpiry();
if (tileExpires) {
this._cacheTimers[id] = setTimeout(() => {
this._cache.remove(id);
this._cacheTimers[id] = undefined;
}, tileExpires - new Date().getTime());
}
}

/**
* Remove a tile, given its id, from the pyramid
* @param {string|number} id tile id
Expand All @@ -434,13 +465,19 @@ class SourceCache extends Evented {

tile.uses--;
delete this._tiles[id];
if (this._timers[id]) {
clearTimeout(this._timers[id]);
this._timers[id] = undefined;
}
this._source.fire('data', { tile: tile, coord: tile.coord, dataType: 'tile' });

if (tile.uses > 0)
return;

if (tile.hasData()) {
this._cache.add(tile.coord.wrapped().id, tile);
const wrappedId = tile.coord.wrapped().id;
this._cache.add(wrappedId, tile);
this._setCacheInvalidationTimer(wrappedId, tile);
} else {
tile.aborted = true;
this.abortTile(tile);
Expand Down
21 changes: 20 additions & 1 deletion js/source/tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class Tile {
this.tileSize = size;
this.sourceMaxZoom = sourceMaxZoom;
this.buckets = {};
this.expires = null;
this.cacheControl = null;

// `this.state` must be one of
//
Expand All @@ -38,6 +40,7 @@ class Tile {
// - `reloading`: Tile data has been loaded and is being updated. Tile can be rendered.
// - `unloaded`: Tile data has been deleted.
// - `errored`: Tile data was not loaded because of an error.
// - `expired`: Tile data was previously loaded, but has expired per its HTTP headers and is in the process of refreshing.
this.state = 'loading';
}

Expand Down Expand Up @@ -194,7 +197,23 @@ class Tile {
}

hasData() {
return this.state === 'loaded' || this.state === 'reloading';
return this.state === 'loaded' || this.state === 'reloading' || this.state === 'expired';
}

setExpiryData(data) {
if (data.cacheControl) this.cacheControl = data.cacheControl;
if (data.expires) this.expires = data.expires;
}

getExpiry() {
if (this.cacheControl) {
// Cache-Control headers set max age (in seconds) from the time of request
const parsedCC = util.parseCacheControl(this.cacheControl);
if (parsedCC['max-age']) return this.timeAdded + parsedCC['max-age'] * 1000;
} else if (this.expires) {
// Expires headers set absolute expiration times
return new Date(this.expires).getTime();
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion js/source/vector_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class VectorTileSource extends Evented {
showCollisionBoxes: this.map.showCollisionBoxes
};

if (!tile.workerID) {
if (!tile.workerID || tile.state === 'expired') {
tile.workerID = this.dispatcher.send('loadTile', params, done.bind(this));
} else if (tile.state === 'loading') {
// schedule tile reloading after it has been loaded
Expand All @@ -86,6 +86,7 @@ class VectorTileSource extends Evented {
return callback(err);
}

tile.setExpiryData(data);
tile.loadVectorData(data, this.map.painter);

if (tile.redoWhenDone) {
Expand Down
14 changes: 10 additions & 4 deletions js/source/vector_tile_worker_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,13 @@ class VectorTileWorkerSource {
workerTile.parse(vectorTile, this.layerIndex, this.actor, (err, result, transferrables) => {
if (err) return callback(err);

const cacheControl = {};
if (vectorTile.expires) cacheControl.expires = vectorTile.expires;
if (vectorTile.cacheControl) cacheControl.cacheControl = vectorTile.cacheControl;

// Not transferring rawTileData because the worker needs to retain its copy.
callback(null,
util.extend({rawTileData: vectorTile.rawData}, result),
util.extend({rawTileData: vectorTile.rawData}, result, cacheControl),
transferrables);
});

Expand Down Expand Up @@ -163,10 +167,12 @@ class VectorTileWorkerSource {
loadVectorData(params, callback) {
const xhr = ajax.getArrayBuffer(params.url, done.bind(this));
return function abort () { xhr.abort(); };
function done(err, arrayBuffer) {
function done(err, response) {
if (err) { return callback(err); }
const vectorTile = new vt.VectorTile(new Protobuf(arrayBuffer));
vectorTile.rawData = arrayBuffer;
const vectorTile = new vt.VectorTile(new Protobuf(response.data));
vectorTile.rawData = response.data;
vectorTile.cacheControl = response.cacheControl;
vectorTile.expires = response.expires;
callback(err, vectorTile);
}
}
Expand Down
4 changes: 2 additions & 2 deletions js/symbol/glyph_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ class GlyphSource {
const rangeName = `${range * 256}-${range * 256 + 255}`;
const url = glyphUrl(fontstack, rangeName, this.url);

ajax.getArrayBuffer(url, (err, data) => {
const glyphs = !err && new Glyphs(new Protobuf(data));
ajax.getArrayBuffer(url, (err, response) => {
const glyphs = !err && new Glyphs(new Protobuf(response.data));
for (let i = 0; i < loading[range].length; i++) {
loading[range][i](err, range, glyphs);
}
Expand Down
12 changes: 9 additions & 3 deletions js/util/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ exports.getArrayBuffer = function(url, callback) {
return callback(new Error('http status 200 returned without content.'));
}
if (xhr.status >= 200 && xhr.status < 300 && xhr.response) {
callback(null, xhr.response);
callback(null, {
data: xhr.response,
cacheControl: xhr.getResponseHeader('Cache-Control'),
expires: xhr.getResponseHeader('Expires')
});
} else {
callback(new Error(xhr.statusText));
}
Expand Down Expand Up @@ -66,8 +70,10 @@ exports.getImage = function(url, callback) {
callback(null, img);
URL.revokeObjectURL(img.src);
};
const blob = new window.Blob([new Uint8Array(imgData)], { type: 'image/png' });
img.src = imgData.byteLength ? URL.createObjectURL(blob) : transparentPngUrl;
const blob = new window.Blob([new Uint8Array(imgData.data)], { type: 'image/png' });
img.cacheControl = imgData.cacheControl;
img.expires = imgData.expires;
img.src = imgData.data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl;
});
};

Expand Down
18 changes: 18 additions & 0 deletions js/util/lru_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,24 @@ class LRUCache<T> {
return data;
}

/**
* Remove a key/value combination from the cache.
*
* @param {string} key the key for the pair to delete
* @returns {LRUCache} this cache
* @private
*/
remove(key: string) {
if (!this.has(key)) { return this; }

const data = this.data[key];
delete this.data[key];
this.onRemove(data);
this.order.splice(this.order.indexOf(key), 1);

return this;
}

/**
* Change the max size of the cache.
*
Expand Down
27 changes: 27 additions & 0 deletions js/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,30 @@ exports.sphericalToCartesian = function(spherical: Array<number>): Array<number>
r * Math.cos(polar)
];
};

/**
* Parses data from 'Cache-Control' headers.
*
* @param cacheControl Value of 'Cache-Control' header
* @return object containing parsed header info.
*/

exports.parseCacheControl = function(cacheControl: string): Object {
// Taken from [Wreck](https://github.com/hapijs/wreck)
const re = /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g;

const header = {};
cacheControl.replace(re, ($0, $1, $2, $3) => {
const value = $2 || $3;
header[$1] = value ? value.toLowerCase() : true;
return '';
});

if (header['max-age']) {
const maxAge = parseInt(header['max-age'], 10);
if (isNaN(maxAge)) delete header['max-age'];
else header['max-age'] = maxAge;
}

return header;
};
Loading