Add cache tests, add more robust CacheRecord creation/deletion logics. Zombies now do not replace data, prevents also potential memory leak.

This commit is contained in:
Aiosa 2024-10-18 14:38:04 +02:00
parent bf25e2f069
commit 1e47bd6add
3 changed files with 153 additions and 114 deletions

View File

@ -637,7 +637,8 @@ $.Tile.prototype = {
* the data item: this is an optimization to load data only when necessary. * the data item: this is an optimization to load data only when necessary.
* @param {string} [type=undefined] data type, will be guessed if not provided (not recommended), * @param {string} [type=undefined] data type, will be guessed if not provided (not recommended),
* if data is a callback the type is a mandatory field, not setting it results in undefined behaviour * if data is a callback the type is a mandatory field, not setting it results in undefined behaviour
* @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey * @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey,
* no effect if key === this.cacheKey
* @param [_safely=true] private * @param [_safely=true] private
* @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to. * @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to.
*/ */

View File

@ -361,17 +361,20 @@
* Free all the data and call data destructors if defined. * Free all the data and call data destructors if defined.
*/ */
destroy() { destroy() {
delete this._conversionJobQueue; if (!this._destroyed) {
this._destroyed = true; delete this._conversionJobQueue;
this._destroyed = true;
// make sure this gets destroyed even if loaded=false // make sure this gets destroyed even if loaded=false
if (this.loaded) { if (this.loaded) {
this._destroySelfUnsafe(this._data, this._type); this._destroySelfUnsafe(this._data, this._type);
} else { } else {
const oldType = this._type; const oldType = this._type;
this._promise.then(x => this._destroySelfUnsafe(x, oldType)); this._promise.then(x => this._destroySelfUnsafe(x, oldType));
}
this.loaded = false;
} }
this.loaded = false;
} }
_destroySelfUnsafe(data, type) { _destroySelfUnsafe(data, type) {
@ -392,7 +395,7 @@
/** /**
* Add tile dependency on this record * Add tile dependency on this record
* @param tile * @param tile
* @param data * @param data can be null|undefined => optimization, will skip data initialization and just adds tile reference
* @param type * @param type
*/ */
addTile(tile, data, type) { addTile(tile, data, type) {
@ -402,30 +405,45 @@
$.console.assert(tile, '[CacheRecord.addTile] tile is required'); $.console.assert(tile, '[CacheRecord.addTile] tile is required');
// first come first served, data for existing tiles is NOT overridden // first come first served, data for existing tiles is NOT overridden
if (this._tiles.length < 1) { if (data !== undefined && data !== null && this._tiles.length < 1) {
// Since we IGNORE new data if already initialized, we support 'data getter' // Since we IGNORE new data if already initialized, we support 'data getter'
if (typeof data === 'function') { if (typeof data === 'function') {
data = data(); data = data();
} }
// If we receive async callback, we consume the async state // in case we attempt to write to existing data object
if (data instanceof $.Promise) { if (this.type && this._promise) {
this._promise = data.then(d => { if (data instanceof $.Promise) {
this._data = d; this._promise = data.then(d => {
this.loaded = true; this._overwriteData(d, type);
return d; });
}); } else {
this._data = null; this._overwriteData(data, type);
}
} else { } else {
this._promise = $.Promise.resolve(data); // If we receive async callback, we consume the async state
this._data = data; if (data instanceof $.Promise) {
this.loaded = true; this._promise = data.then(d => {
} this._data = d;
this.loaded = true;
return d;
});
this._data = null;
} else {
this._promise = $.Promise.resolve(data);
this._data = data;
this.loaded = true;
}
this._type = type; this._type = type;
}
this._tiles.push(tile); this._tiles.push(tile);
} else if (!this._tiles.includes(tile)) { } else if (!this._tiles.includes(tile) && this.type && this._promise) {
// here really check we are loaded, since if optimization allows sending no data and we add tile without
// proper initialization it is a bug
this._tiles.push(tile); this._tiles.push(tile);
} else {
$.console.warn("Tile %s caching attempt without data argument on uninitialized cache entry!", tile);
} }
} }
@ -496,7 +514,7 @@
*/ */
_overwriteData(data, type) { _overwriteData(data, type) {
if (this._destroyed) { if (this._destroyed) {
//we take ownership of the data, destroy //we have received the ownership of the data, destroy it too since we are destroyed
$.convertor.destroy(data, type); $.convertor.destroy(data, type);
return $.Promise.resolve(); return $.Promise.resolve();
} }
@ -772,7 +790,7 @@
let cacheKey = options.cacheKey || theTile.cacheKey; let cacheKey = options.cacheKey || theTile.cacheKey;
let cacheRecord = this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey]; let cacheRecord = this._cachesLoaded[cacheKey];
if (!cacheRecord) { if (!cacheRecord) {
if (options.data === undefined) { if (options.data === undefined) {
$.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " + $.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " +
@ -780,15 +798,26 @@
options.data = options.image; options.data = options.image;
} }
//allow anything but undefined, null, false (other values mean the data was set, for example '0') cacheRecord = this._zombiesLoaded[cacheKey];
const validData = options.data !== undefined && options.data !== null && options.data !== false; if (cacheRecord) {
$.console.assert( validData, "[TileCache.cacheTile] options.data is required to create an CacheRecord" ); // zombies should not be (yet) destroyed, but if we encounter one...
cacheRecord = this._cachesLoaded[cacheKey] = new $.CacheRecord(); if (cacheRecord._destroyed) {
this._cachesLoadedCount++; cacheRecord.revive();
} else if (cacheRecord._destroyed) { } else {
cacheRecord.revive(); // if zombie ready, do not overwrite its data
delete this._zombiesLoaded[cacheKey]; delete options.data;
this._zombiesLoadedCount--; }
delete this._zombiesLoaded[cacheKey];
this._zombiesLoadedCount--;
this._cachesLoaded[cacheKey] = cacheRecord;
this._cachesLoadedCount++;
} else {
//allow anything but undefined, null, false (other values mean the data was set, for example '0')
const validData = options.data !== undefined && options.data !== null && options.data !== false;
$.console.assert( validData, "[TileCache.cacheTile] options.data is required to create an CacheRecord" );
cacheRecord = this._cachesLoaded[cacheKey] = new $.CacheRecord();
this._cachesLoadedCount++;
}
} }
if (!options.dataType) { if (!options.dataType) {

View File

@ -97,6 +97,11 @@
springStiffness: 100 // Faster animation = faster tests springStiffness: 100 // Faster animation = faster tests
}); });
OpenSeadragon.ImageLoader.prototype.addJob = originalJob; OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
// Reset counters
typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
}, },
afterEach: function () { afterEach: function () {
if (viewer && viewer.close) { if (viewer && viewer.close) {
@ -130,7 +135,9 @@
tile0._caches[tile0.cacheKey] = cache.cacheTile({ tile0._caches[tile0.cacheKey] = cache.cacheTile({
tile: tile0, tile: tile0,
tiledImage: fakeTiledImage0 tiledImage: fakeTiledImage0,
data: 3,
dataType: T_A
}); });
tile0._cacheSize++; tile0._cacheSize++;
@ -138,7 +145,9 @@
tile1._caches[tile1.cacheKey] = cache.cacheTile({ tile1._caches[tile1.cacheKey] = cache.cacheTile({
tile: tile1, tile: tile1,
tiledImage: fakeTiledImage1 tiledImage: fakeTiledImage1,
data: 55,
dataType: T_B
}); });
tile1._cacheSize++; tile1._cacheSize++;
assert.equal(cache.numTilesLoaded(), 2, 'tile count after second cache'); assert.equal(cache.numTilesLoaded(), 2, 'tile count after second cache');
@ -178,7 +187,9 @@
tile0._caches[tile0.cacheKey] = cache.cacheTile({ tile0._caches[tile0.cacheKey] = cache.cacheTile({
tile: tile0, tile: tile0,
tiledImage: fakeTiledImage0 tiledImage: fakeTiledImage0,
data: 55,
dataType: T_B
}); });
tile0._cacheSize++; tile0._cacheSize++;
@ -186,7 +197,9 @@
tile1._caches[tile1.cacheKey] = cache.cacheTile({ tile1._caches[tile1.cacheKey] = cache.cacheTile({
tile: tile1, tile: tile1,
tiledImage: fakeTiledImage0 tiledImage: fakeTiledImage0,
data: 55,
dataType: T_B
}); });
tile1._cacheSize++; tile1._cacheSize++;
@ -194,7 +207,9 @@
tile2._caches[tile2.cacheKey] = cache.cacheTile({ tile2._caches[tile2.cacheKey] = cache.cacheTile({
tile: tile2, tile: tile2,
tiledImage: fakeTiledImage0 tiledImage: fakeTiledImage0,
data: 55,
dataType: T_B
}); });
tile2._cacheSize++; tile2._cacheSize++;
@ -570,78 +585,72 @@
test.equal(c12.getTileCount(), 2, "The original data cache still has only two tiles attached."); test.equal(c12.getTileCount(), 2, "The original data cache still has only two tiles attached.");
test.equal(tile12.getCacheSize(), 2, "Related tile cache has also two caches."); test.equal(tile12.getCacheSize(), 2, "Related tile cache has also two caches.");
//TODO fix test from here //add and delete cache nothing changes (+1 destroy T_C)
test.ok("TODO: FIX TEST SUITE FOR NEW CACHE SYSTEM"); tile00.addCache("my_custom_cache2", 128, T_C);
// tile00.removeCache("my_custom_cache2");
// //add and delete cache nothing changes test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
// tile00.addCache("my_custom_cache2", 128, T_C); test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items.");
// tile00.removeCache("my_custom_cache2"); test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
// test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
// test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items."); //delete cache as a zombie (+0 destroy)
// test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects."); tile00.addCache("my_custom_cache2", 17, T_D);
// //direct access shoes correct value although we set key!
// //delete cache as a zombie const myCustomCache2Data = tile00.getCache("my_custom_cache2").data;
// tile00.addCache("my_custom_cache2", 17, T_C); test.equal(myCustomCache2Data, 17, "Previously defined cache does not intervene.");
// //direct access shoes correct value although we set key! test.equal(tileCache.numCachesLoaded(), 6, "The cache size is 6.");
// const myCustomCache2Data = tile00.getCache("my_custom_cache2").data; //keep zombie
// test.equal(myCustomCache2Data, 17, "Previously defined cache does not intervene."); tile00.removeCache("my_custom_cache2", false);
// test.equal(tileCache.numCachesLoaded(), 6, "The cache size is 6."); test.equal(tileCache.numCachesLoaded(), 6, "The cache is 5 + 1 zombie, no change.");
// //keep zombie test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
// tile00.removeCache("my_custom_cache2", false); test.equal(tileCache._zombiesLoadedCount, 1, "One zombie.");
// test.equal(tileCache.numCachesLoaded(), 6, "The cache is 5 + 1 zombie, no change.");
// test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects."); //revive zombie
// tile01.addCache("my_custom_cache2", 18, T_D);
// //revive zombie const myCustomCache2OtherData = tile01.getCache("my_custom_cache2").data;
// tile01.addCache("my_custom_cache2", 18, T_C); test.equal(myCustomCache2OtherData, myCustomCache2Data, "Caches are equal because revived.");
// const myCustomCache2OtherData = tile01.getCache("my_custom_cache2").data; test.equal(tileCache._cachesLoadedCount, 6, "Zombie revived, original state restored.");
// test.equal(myCustomCache2OtherData, myCustomCache2Data, "Caches are equal because revived."); test.equal(tileCache._zombiesLoadedCount, 0, "No zombies.");
// //again, keep zombie
// tile01.removeCache("my_custom_cache2", false); //again, keep zombie
// tile01.removeCache("my_custom_cache2", false);
// //first create additional cache so zombie is not the youngest
// tile01.addCache("some weird cache", 11, T_A); //first create additional cache so zombie is not the youngest
// test.ok(tile01.cacheKey === tile01.originalCacheKey, "Custom cache does not touch tile cache keys."); tile01.addCache("some weird cache", 11, T_A);
// test.ok(tile01.cacheKey === tile01.originalCacheKey, "Custom cache does not touch tile cache keys.");
// //insertion aadditional cache clears the zombie first although it is not the youngest one
// test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items."); //insertion aadditional cache clears the zombie first although it is not the youngest one
// test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items.");
// //Test CAP test.equal(tileCache._cachesLoadedCount, 6, "New cache created -> 5+1.");
// tileCache._maxCacheItemCount = 7; test.equal(tileCache._zombiesLoadedCount, 1, "One zombie remains.");
//
// //does not trigger insertion - deletion, since we setData to cache that already exists, 43 value ignored //Test CAP
// tile12.setData(43, T_B, true); tileCache._maxCacheItemCount = 7;
// test.notEqual(tile12.cacheKey, tile12.originalCacheKey, "Original cache key differs.");
// test.equal(theTileKey, tile12.originalCacheKey, "Original cache key preserved."); // Zombie destroyed before other caches (+1 destroy T_D)
// test.equal(tileCache.numCachesLoaded(), 7, "The cache has still 7 items."); tile12.setData(43, T_B);
// //we called SET DATA with preserve=true on tile12 which was sharing cache with tile00, new cache is also shared test.notEqual(tile12.cacheKey, tile12.originalCacheKey, "Original cache key differs.");
// test.equal(tile00.originalCacheKey, tile12.originalCacheKey, "Original cache key matches between tiles."); test.equal(theTileKey, tile12.originalCacheKey, "Original cache key preserved.");
// test.equal(tile00.cacheKey, tile12.cacheKey, "Modified cache key matches between tiles."); test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items.");
// test.equal(tile12.getCache().data, 42, "The value is not 43 as setData triggers cache share!"); test.equal(tileCache._zombiesLoadedCount, 0, "One zombie sacrificed, preferred over living cache.");
// test.notOk([tile00, tile01, tile10, tile11, tile12].find(x => !x.loaded), "All tiles sill loaded since zombie was sacrificed.");
// //triggers insertion - deletion of zombie cache 'my_custom_cache2'
// tile00.addCache("trigger-max-cache-handler", 5, T_C); // test destructors called as expected
// //reset CAP test.equal(destroyA, 0, "No destructors for A called.");
// tileCache._maxCacheItemCount = OpenSeadragon.DEFAULT_SETTINGS.maxImageCacheCount; test.equal(destroyB, 0, "No destructors for B called.");
// test.equal(destroyC, 1, "One destruction for C called.");
// //try to revive zombie will fail: the zombie was deleted, we will find 18 test.equal(destroyD, 1, "One destruction for D called.");
// tile01.addCache("my_custom_cache2", 18, T_C); test.equal(destroyE, 0, "No destructors for E called.");
// const myCustomCache2RecreatedData = tile01.getCache("my_custom_cache2").data;
// test.notEqual(myCustomCache2RecreatedData, myCustomCache2Data, "Caches are not equal because created.");
// test.equal(myCustomCache2RecreatedData, 18, "Cache data is actually as set to 18."); //try to revive zombie will fail: the zombie was deleted, we will find new vaue there
// test.equal(tileCache.numCachesLoaded(), 8, "The cache has now 8 items."); tile01.addCache("my_custom_cache2", -849613, T_C);
// const myCustomCache2RecreatedData = tile01.getCache("my_custom_cache2").data;
// test.notEqual(myCustomCache2RecreatedData, myCustomCache2Data, "Caches are not equal because zombie was killed.");
// //delete cache bound to other tiles, this tile has 4 caches: test.equal(myCustomCache2RecreatedData, -849613, "Cache data is actually as set to 18.");
// // cacheKey: shared, originalCacheKey: shared, <custom cache key>, <custom cache key> test.equal(tileCache.numCachesLoaded(), 7, "The cache has still 7 items.");
// // note that cacheKey is shared because we called setData on two items that both create MOD cache
// tileCache.unloadTile(tile00, true, tileCache._tilesLoaded.indexOf(tile00)); // some tile has been selected as a sacrifice since we triggered cap control
// test.equal(tileCache.numCachesLoaded(), 6, "The cache has now 8-2 items."); test.ok([tile00, tile01, tile10, tile11, tile12].find(x => !x.loaded), "One tile has been sacrificed.");
// test.equal(tileCache.numTilesLoaded(), 4, "One tile removed.");
// test.equal(c00.getTileCount(), 1, "The cache has still tile12 left.");
//
// //now test tile destruction as zombie
//
// //now test tile cache sharing
done(); done();
})(); })();
}); });