Skip to content

Commit

Permalink
turn LRUCache into tile-specific TileCache
Browse files Browse the repository at this point in the history
LRUCache was only being used for tiles and recently we started adding
some non-standard behaviour including storing multiple values for a
single key. This pr:

- replaces `number` key with `OverscaledTileID` so that we can enforce
  the type and enforce tile id wrapping
- moves expiry logic into cache and gets rid of `cacheTimers` in
  `SourceCache`
  • Loading branch information
ansis committed Apr 18, 2018
1 parent c9f94d4 commit a4eed2f
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 320 deletions.
48 changes: 9 additions & 39 deletions src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { create as createSource } from './source';

import Tile from './tile';
import { Event, ErrorEvent, Evented } from '../util/evented';
import Cache from '../util/lru_cache';
import TileCache from './tile_cache';
import Coordinate from '../geo/coordinate';
import { keysDifference } from '../util/util';
import EXTENT from '../data/extent';
Expand Down Expand Up @@ -44,7 +44,7 @@ class SourceCache extends Evented {
_sourceErrored: boolean;
_tiles: {[any]: Tile};
_prevLng: number | void;
_cache: Cache<Tile>;
_cache: TileCache;
_timers: {[any]: TimeoutID};
_cacheTimers: {[any]: TimeoutID};
_maxTileCacheSize: ?number;
Expand Down Expand Up @@ -86,7 +86,7 @@ class SourceCache extends Evented {
this._source = createSource(id, options, dispatcher, this);

this._tiles = {};
this._cache = new Cache(0, this._unloadTile.bind(this));
this._cache = new TileCache(0, this._unloadTile.bind(this));
this._timers = {};
this._cacheTimers = {};
this._maxTileCacheSize = null;
Expand Down Expand Up @@ -208,7 +208,7 @@ class SourceCache extends Evented {
return;
}

this._resetCache();
this._cache.reset();

for (const i in this._tiles) {
this._reloadTile(i, 'reloading');
Expand Down Expand Up @@ -366,9 +366,9 @@ class SourceCache extends Evented {
retain[id] = parent;
return tile;
}
if (this._cache.has(id)) {
if (this._cache.has(parent)) {
retain[id] = parent;
return this._cache.get(id);
return this._cache.get(parent);
}
}
}
Expand Down Expand Up @@ -616,13 +616,9 @@ class SourceCache extends Evented {
return tile;


tile = this._cache.getAndRemove((tileID.wrapped().key: any));
tile = this._cache.getAndRemove(tileID);
if (tile) {
if (this._cacheTimers[tileID.key]) {
clearTimeout(this._cacheTimers[tileID.key]);
delete this._cacheTimers[tileID.key];
this._setTileReloadTimer(tileID.key, tile);
}
this._setTileReloadTimer(tileID.key, tile);
// set the tileID because the cached tile could have had a different wrap value
tile.tileID = tileID;
}
Expand Down Expand Up @@ -658,21 +654,6 @@ class SourceCache extends Evented {
}
}

_setCacheInvalidationTimer(id: string | number, tile: Tile) {
if (id in this._cacheTimers) {
clearTimeout(this._cacheTimers[id]);
delete this._cacheTimers[id];
}

const expiryTimeout = tile.getExpiryTimeout();
if (expiryTimeout) {
this._cacheTimers[id] = setTimeout(() => {
this._cache.remove((id: any));
delete this._cacheTimers[id];
}, expiryTimeout);
}
}

/**
* Remove a tile, given its id, from the pyramid
* @private
Expand All @@ -693,10 +674,7 @@ class SourceCache extends Evented {
return;

if (tile.hasData()) {
tile.tileID = tile.tileID.wrapped();
const wrappedId = tile.tileID.key;
this._cache.add((wrappedId: any), tile);
this._setCacheInvalidationTimer(wrappedId, tile);
this._cache.add(tile.tileID, tile, tile.getExpiryTimeout());
} else {
tile.aborted = true;
this._abortTile(tile);
Expand All @@ -714,14 +692,6 @@ class SourceCache extends Evented {
for (const id in this._tiles)
this._removeTile(id);

this._resetCache();
}

_resetCache() {
for (const id in this._cacheTimers)
clearTimeout(this._cacheTimers[id]);

this._cacheTimers = {};
this._cache.reset();
}

Expand Down
184 changes: 184 additions & 0 deletions src/source/tile_cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// @flow

import { OverscaledTileID } from './tile_id';
import type Tile from './tile';

/**
* A [least-recently-used cache](http://en.wikipedia.org/wiki/Cache_algorithms)
* with hash lookup made possible by keeping a list of keys in parallel to
* an array of dictionary of values
*
* @private
*/
class TileCache {
max: number;
data: {[key: number | string]: Array<{ value: Tile, timeout: ?TimeoutID}>};
order: Array<number>;
onRemove: (element: Tile) => void;
/**
* @param {number} max number of permitted values
* @param {Function} onRemove callback called with items when they expire
*/
constructor(max: number, onRemove: (element: Tile) => void) {
this.max = max;
this.onRemove = onRemove;
this.reset();
}

/**
* Clear the cache
*
* @returns {TileCache} this cache
* @private
*/
reset() {
for (const key in this.data) {
for (const removedData of this.data[key]) {
if (removedData.timeout) clearTimeout(removedData.timeout);
this.onRemove(removedData.value);
}
}

this.data = {};
this.order = [];

return this;
}

/**
* Add a key, value combination to the cache, trimming its size if this pushes
* it over max length.
*
* @param {OverscaledTileID} tileID lookup key for the item
* @param {*} data any value
*
* @returns {TileCache} this cache
* @private
*/
add(tileID: OverscaledTileID, data: Tile, expiryTimeout: number | void) {
const key = tileID.wrapped().key;
if (this.data[key] === undefined) {
this.data[key] = [];
}

const dataWrapper = {
value: data,
timeout: undefined
};

if (expiryTimeout !== undefined) {
dataWrapper.timeout = setTimeout(() => {
this.remove(tileID, dataWrapper);
}, expiryTimeout);
}

this.data[key].push(dataWrapper);
this.order.push(key);

if (this.order.length > this.max) {
const removedData = this._getAndRemoveByKey(this.order[0]);
if (removedData) this.onRemove(removedData);
}

return this;
}

/**
* Determine whether the value attached to `key` is present
*
* @param {OverscaledTileID} tileID the key to be looked-up
* @returns {boolean} whether the cache has this value
* @private
*/
has(tileID: OverscaledTileID): boolean {
return tileID.wrapped().key in this.data;
}

/**
* Get the value attached to a specific key and remove data from cache.
* If the key is not found, returns `null`
*
* @param {OverscaledTileID} tileID the key to look up
* @returns {*} the data, or null if it isn't found
* @private
*/
getAndRemove(tileID: OverscaledTileID): ?Tile {
if (!this.has(tileID)) { return null; }
return this._getAndRemoveByKey(tileID.wrapped().key);
}

/*
* Get and remove the value with the specified key.
*/
_getAndRemoveByKey(key: number): ?Tile {
const data = this.data[key].shift();
if (data.timeout) clearTimeout(data.timeout);

if (this.data[key].length === 0) {
delete this.data[key];
}
this.order.splice(this.order.indexOf(key), 1);

return data.value;
}

/**
* Get the value attached to a specific key without removing data
* from the cache. If the key is not found, returns `null`
*
* @param {OverscaledTileID} tileID the key to look up
* @returns {*} the data, or null if it isn't found
* @private
*/
get(tileID: OverscaledTileID): ?Tile {
if (!this.has(tileID)) { return null; }

const data = this.data[tileID.wrapped().key][0];
return data.value;
}

/**
* Remove a key/value combination from the cache.
*
* @param {OverscaledTileID} tileID the key for the pair to delete
* @param {Tile} value If a value is provided, remove that exact version of the value.
* @returns {TileCache} this cache
* @private
*/
remove(tileID: OverscaledTileID, value: ?{ value: Tile, timeout: ?TimeoutID}) {
if (!this.has(tileID)) { return this; }
const key = tileID.wrapped().key;

const dataIndex = value === undefined ? 0 : this.data[key].indexOf(value);
const data = this.data[key][dataIndex];
this.data[key].splice(dataIndex, 1);
if (data.timeout) clearTimeout(data.timeout);
if (this.data[key].length === 0) {
delete this.data[key];
}
this.onRemove(data.value);
this.order.splice(this.order.indexOf(key), 1);

return this;
}

/**
* Change the max size of the cache.
*
* @param {number} max the max size of the cache
* @returns {TileCache} this cache
* @private
*/
setMaxSize(max: number): TileCache {
this.max = max;

while (this.order.length > this.max) {
const removedData = this._getAndRemoveByKey(this.order[0]);
if (removedData) this.onRemove(removedData);
}

return this;
}
}

export default TileCache;
Loading

0 comments on commit a4eed2f

Please sign in to comment.