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 16, 2018
1 parent 132bfd7 commit ee32a0b
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 188 deletions.
44 changes: 7 additions & 37 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 Cache from './tile_cache';
import Coordinate from '../geo/coordinate';
import { keysDifference } from '../util/util';
import EXTENT from '../data/extent';
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
98 changes: 63 additions & 35 deletions src/util/lru_cache.js → src/source/tile_cache.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
// @flow

import { OverscaledTileID } from './tile_id';

/**
* 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 LRUCache<T> {
class TileCache<T> {
max: number;
data: {[key: string]: Array<T>};
order: Array<string>;
data: {[key: number | string]: Array<{ value: T, timeout: ?TimeoutID}>};
order: Array<number>;
onRemove: (element: T) => void;
/**
* @param {number} max number of permitted values
Expand All @@ -25,13 +27,14 @@ class LRUCache<T> {
/**
* Clear the cache
*
* @returns {LRUCache} this cache
* @returns {TileCache} this cache
* @private
*/
reset() {
for (const key in this.data) {
for (const removedData of this.data[key]) {
this.onRemove(removedData);
if (removedData.timeout) clearTimeout(removedData.timeout);
this.onRemove(removedData.value);
}
}

Expand All @@ -45,21 +48,34 @@ class LRUCache<T> {
* Add a key, value combination to the cache, trimming its size if this pushes
* it over max length.
*
* @param {string} key lookup key for the item
* @param {OverscaledTileID} key lookup key for the item
* @param {*} data any value
*
* @returns {LRUCache} this cache
* @returns {TileCache} this cache
* @private
*/
add(key: string, data: T) {
add(tileID: OverscaledTileID, data: T, expiryTimeout: number | void) {
const key = tileID.wrapped().key;
if (this.data[key] === undefined) {
this.data[key] = [];
}
this.data[key].push(data);

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.getAndRemove(this.order[0]);
const removedData = this._getAndRemoveByKey(this.order[0]);
if (removedData) this.onRemove(removedData);
}

Expand All @@ -69,75 +85,87 @@ class LRUCache<T> {
/**
* Determine whether the value attached to `key` is present
*
* @param {string} key the key to be looked-up
* @param {OverscaledTileID} key the key to be looked-up
* @returns {boolean} whether the cache has this value
* @private
*/
has(key: string): boolean {
return key in this.data;
has(tileID: OverscaledTileID): boolean {
return tileID.wrapped().key in this.data;
}

/**
* List all keys in the cache
*
* @returns {Array<string>} an array of keys in this cache.
* @returns {Array<number>} an array of keys in this cache.
* @private
*/
keys(): Array<string> {
keys(): Array<number> {
return this.order;
}

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

/*
* Get and remove the value with the specified key.
*/
_getAndRemoveByKey(key: number): ?T {
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;
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 {string} key the key to look up
* @param {OverscaledTileID} key the key to look up
* @returns {*} the data, or null if it isn't found
* @private
*/
get(key: string): ?T {
if (!this.has(key)) { return null; }
get(tileID: OverscaledTileID): ?T {
if (!this.has(tileID)) { return null; }

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

/**
* Remove a key/value combination from the cache.
*
* @param {string} key the key for the pair to delete
* @returns {LRUCache} this cache
* @param {OverscaledTileID} key the key for the pair to delete
* @param {T} value If a value is provided, remove that exact version of the value.
* @returns {TileCache} this cache
* @private
*/
remove(key: string) {
if (!this.has(key)) { return this; }

const data = this.data[key].pop();
remove(tileID: OverscaledTileID, value: ?{ value: T, 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);
this.onRemove(data.value);
this.order.splice(this.order.indexOf(key), 1);

return this;
Expand All @@ -147,19 +175,19 @@ class LRUCache<T> {
* Change the max size of the cache.
*
* @param {number} max the max size of the cache
* @returns {LRUCache} this cache
* @returns {TileCache} this cache
* @private
*/
setMaxSize(max: number): LRUCache<T> {
setMaxSize(max: number): TileCache<T> {
this.max = max;

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

return this;
}
}

export default LRUCache;
export default TileCache;
15 changes: 6 additions & 9 deletions test/unit/source/source_cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,9 @@ test('SourceCache#addTile', (t) => {
sourceCache._setTileReloadTimer = (id) => {
sourceCache._timers[id] = setTimeout(() => {}, 0);
};
sourceCache._setCacheInvalidationTimer = (id) => {
sourceCache._cacheTimers[id] = setTimeout(() => {}, 0);
};
sourceCache._loadTile = (tile, callback) => {
tile.state = 'loaded';
tile.getExpiryTimeout = () => time;
tile.getExpiryTimeout = () => 1000 * 60;
sourceCache._setTileReloadTimer(tileID.key, tile);
callback();
};
Expand All @@ -142,22 +139,22 @@ test('SourceCache#addTile', (t) => {

const id = tileID.key;
t.notOk(sourceCache._timers[id]);
t.notOk(sourceCache._cacheTimers[id]);
t.notOk(sourceCache._cache.has(tileID));

sourceCache._addTile(tileID);

t.ok(sourceCache._timers[id]);
t.notOk(sourceCache._cacheTimers[id]);
t.notOk(sourceCache._cache.has(tileID));

sourceCache._removeTile(tileID.key);

t.notOk(sourceCache._timers[id]);
t.ok(sourceCache._cacheTimers[id]);
t.ok(sourceCache._cache.has(tileID));

sourceCache._addTile(tileID);

t.ok(sourceCache._timers[id]);
t.notOk(sourceCache._cacheTimers[id]);
t.notOk(sourceCache._cache.has(tileID));

t.end();
});
Expand Down Expand Up @@ -1366,7 +1363,7 @@ test('SourceCache#findLoadedParent', (t) => {
sourceCache.updateCacheSize(tr);

const tile = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512, 22);
sourceCache._cache.add(tile.tileID.key, tile);
sourceCache._cache.add(tile.tileID, tile);

const retain = {};
const expectedRetain = {};
Expand Down
Loading

0 comments on commit ee32a0b

Please sign in to comment.