Skip to content

Commit

Permalink
refacto(cache): ditch Map() in Cache for simple tree (#1264)
Browse files Browse the repository at this point in the history
refacto(cache): ditch Map() in Cache for simple tree

The previous Cache was working on a single Map, which can become really
slow and big after a few minutes of navigation in iTowns.

Now, the Cache is composed of a single tree, that can contain node up
to 3 levels. It simplifies things attached to a layer, a source or
others things like a zoom value. Instead of having a complex string
composed with all those information, it now uses several key accross all
the node of the Cache.

In term of performance, the proposed implementation is 20 times faster
than the previous one.
  • Loading branch information
zarov authored Dec 6, 2019
1 parent 21fb3b3 commit 881604f
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 55 deletions.
7 changes: 3 additions & 4 deletions src/Core/Prefab/TileBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ export default function newTileGeometry(builder, params) {
const { sharableExtent, quaternion, position } = builder.computeSharableExtent(params.extent);
const south = sharableExtent.south.toFixed(6);
const bufferKey = `${builder.projection}_${params.disableSkirt ? 0 : 1}_${params.segment}`;
const geometryKey = `${bufferKey}_${params.level}_${south}`;
let promiseGeometry = Cache.get(geometryKey);
let promiseGeometry = Cache.get(bufferKey, params.level, south);

// build geometry if doesn't exist
if (!promiseGeometry) {
let resolve;
promiseGeometry = new Promise((r) => { resolve = r; });
Cache.set(geometryKey, promiseGeometry);
Cache.set(promiseGeometry, Cache.POLICIES.INFINITE, bufferKey, params.level, south);

params.extent = sharableExtent;
params.center = builder.center(params.extent).clone();
Expand Down Expand Up @@ -49,7 +48,7 @@ export default function newTileGeometry(builder, params) {
geometry._count--;
if (geometry._count == 0) {
THREE.BufferGeometry.prototype.dispose.call(geometry);
Cache.delete(bufferKey);
Cache.delete(bufferKey, params.level, south);
}
};
resolve(geometry);
Expand Down
133 changes: 85 additions & 48 deletions src/Core/Scheduler/Cache.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const data = new Map();
const stats = new Map();
let data = {};
let entry;

/**
* This is a copy of the Map object, except that it also store a value for last
Expand All @@ -13,14 +13,16 @@ const stats = new Map();
* @example
* import Cache from './Cache';
*
* Cache.set('foo', { bar: 1 }, Cache.POLICIES.TEXTURE);
* Cache.set('acme', { bar: 32 });
* Cache.set({ bar: 1 }, Cache.POLICIES.TEXTURE, 'foo');
* Cache.set({ bar: 32 }, Cache.POLICIES.INFINITE, 'foo', 'toto');
*
* Cache.get('foo');
*
* Cache.delete('foo');
*
* Cache.clear();
*
* Cache.flush();
*/
const Cache = {
/**
Expand Down Expand Up @@ -49,44 +51,73 @@ const Cache = {
* @name module:Cache.get
* @function
*
* @param {string} key
* @param {string} key1
* @param {string} [key2]
* @param {string} [key3]
*
* @return {Object}
*/
get: (key) => {
const entry = data.get(key);
const stat = stats.get(key) || stats.set(key, { hit: 0, miss: 0 });
get: (key1, key2, key3) => {
if (data[key1] == undefined) {
// eslint-disable-next-line
return;
} else if (data[key1][key2] == undefined) {
entry = data[key1];
} else if (data[key1][key2][key3] == undefined) {
entry = data[key1][key2];
} else {
entry = data[key1][key2][key3];
}

if (entry) {
stat.hit++;
if (entry.value) {
entry.lastTimeUsed = Date.now();
return entry.value;
}

stat.miss++;
},

/**
* Adds or updates an entry with a specified key. A lifetime can be added,
* by specifying a numerical value or using the {@link Cache.POLICIES}
* values. By default an entry has an infinite lifetime.
* Adds or updates an entry with specified keys (up to 3). A lifetime can be
* added, by specifying a numerical value or using the {@link
* Cache.POLICIES} values. By default an entry has an infinite lifetime.
* Caution: it overrides any existing entry already set at this/those key/s.
*
* @name module:Cache.set
* @function
*
* @param {string} key
* @param {Object} value
* @param {number} [lifetime]
* @param {number} lifetime
* @param {string} key1
* @param {string} [key2]
* @param {string} [key3]
*
* @return {Object} the added value
*/
set: (key, value, lifetime = Infinity) => {
const entry = {
set: (value, lifetime, key1, key2, key3) => {
entry = {
value,
lastTimeUsed: Date.now(),
lifetime,
};
data.set(key, entry);

if (key2 == undefined) {
data[key1] = entry;
return value;
}

if (!data[key1]) {
data[key1] = {};
}

if (key3 == undefined) {
data[key1][key2] = entry;
return value;
}

if (!data[key1][key2]) {
data[key1][key2] = {};
}

data[key1][key2][key3] = entry;

return value;
},
Expand All @@ -97,19 +128,31 @@ const Cache = {
* @name module:Cache.delete
* @function
*
* @param {string} key
*
* @return {boolean} - Confirmation that the entry has been deleted.
* @param {string} key1
* @param {string} [key2]
* @param {string} [key3]
*/
delete: key => data.delete(key),
delete: (key1, key2, key3) => {
if (data[key1] == undefined) {
throw Error('Please specify at least a key of something to delete');
} else if (data[key1][key2] == undefined) {
delete data[key1];
} else if (data[key1][key2][key3] == undefined) {
delete data[key1][key2];
} else {
delete data[key1][key2][key3];
}
},

/**
* Removes all entries of the cache.
*
* @name module:Cache.clear
* @function
*/
clear: data.clear(),
clear: () => {
data = {};
},

/**
* Flush the cache: entries that have been present for too long since the
Expand All @@ -121,32 +164,26 @@ const Cache = {
* @name module:Cache.flush
* @function
*
* @param {number} [time]
*
* @return {Object} Statistics about the flush: `before` gives the number of
* entries before flushing, `after` the number after flushing, `hit` the
* number of total successful hit on resources in the cache, and `miss` the
* number of failed hit. The hit and miss are based since the last flush,
* and are reset on every flush.
* @param {number} [time=Date.now()]
*/
flush: (time = Date.now()) => {
const before = data.size;

data.forEach((entry, key) => {
if (entry.lifetime < time - entry.lastTimeUsed) {
data.delete(key);
for (const i in data) {
if (data[i].lifetime < time - data[i].lastTimeUsed) {
delete data[i];
} else {
for (const j in data[i]) {
if (data[i][j].lifetime < time - data[i][j].lastTimeUsed) {
delete data[i][j];
} else {
for (const k in data[i][j]) {
if (data[i][j][k].lifetime < time - data[i][j][k].lastTimeUsed) {
delete data[i][j][k];
}
}
}
}
}
});

let hit = 0;
let miss = 0;
stats.forEach((stat) => {
hit += stat.hit;
miss += stat.miss;
});
stats.clear();

return { before, after: data.size, hit, miss };
}
},
};

Expand Down
5 changes: 2 additions & 3 deletions src/Provider/DataSourceProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@ export default {

// Tag to Cache data
const exTag = source.isVectorSource ? extentsDestination[i] : extSource;
const tag = `${source.uid}-${exTag.toString('-')}`;

// Get converted source data, in cache
let convertedSourceData = Cache.get(tag);
let convertedSourceData = Cache.get(source.uid, layer.id, exTag.toString('-'));

// If data isn't in cache
if (!convertedSourceData) {
Expand All @@ -85,7 +84,7 @@ export default {
.then(parsedData => layer.convert(parsedData, extDest, layer), err => error(err, source));
}
// Put converted data in cache
Cache.set(tag, convertedSourceData, Cache.POLICIES.TEXTURE);
Cache.set(convertedSourceData, Cache.POLICIES.TEXTURE, source.uid, layer.id, exTag.toString('-'));
}

// Verify some command is resolved
Expand Down

0 comments on commit 881604f

Please sign in to comment.