diff --git a/src/tilecache.js b/src/tilecache.js index 9578cc91..12060295 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -225,21 +225,17 @@ prepareForRendering(drawer) { const supportedTypes = drawer.getRequiredDataFormats(); - if (drawer.options.usePrivateCache && drawer.options.preloadCache) { - return this.prepareInternalCacheAsync(drawer).then(_ => { - // if not internal copy and we have no data, or we are ready to render, exit - if (!this.loaded || supportedTypes.includes(this.type)) { - return this.await(); - } - - return this.transformTo(supportedTypes); - }); - } - + let selfPromise; if (!this.loaded || supportedTypes.includes(this.type)) { - return this.await(); + selfPromise = this.await(); + } else { + selfPromise = this.transformTo(supportedTypes); } - return this.transformTo(supportedTypes); + + if (drawer.options.usePrivateCache && drawer.options.preloadCache) { + return selfPromise.then(_ => this.prepareInternalCacheAsync(drawer)); + } + return selfPromise; } /** @@ -265,7 +261,9 @@ $.console.assert(this._tRef, "Data Create called from invalidation routine needs tile reference!"); const transformedData = drawer.dataCreate(this, this._tRef); $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); - internalCache = this[DRAWER_INTERNAL_CACHE][drawer.getId()] = new $.InternalCacheRecord(transformedData, (data) => drawer.dataFree(data)); + const drawerID = drawer.getId(); + internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, + drawerID, (data) => drawer.dataFree(data)); return internalCache.await(); } @@ -290,7 +288,10 @@ $.console.assert(this._tRef, "Data Create called from drawing loop needs tile reference!"); const transformedData = drawer.dataCreate(this, this._tRef); $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); - internalCache = this[DRAWER_INTERNAL_CACHE][drawer.getId()] = new $.InternalCacheRecord(transformedData, (data) => drawer.dataFree(data)); + + const drawerID = drawer.getId(); + internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, + drawerID, (data) => drawer.dataFree(data)); return internalCache; } @@ -676,9 +677,10 @@ * @private */ $.InternalCacheRecord = class { - constructor(data, onDestroy) { + constructor(data, type, onDestroy) { this.tstamp = $.now(); this._ondestroy = onDestroy; + this._type = type; if (data instanceof $.Promise) { this._promise = data; @@ -706,7 +708,7 @@ * @returns {string} */ get type() { - return "__internal_cache__"; + return this._type; } /** @@ -1167,11 +1169,15 @@ */ clearDrawerInternalCache(drawer) { const drawerId = drawer.getId(); - for (let zombie in this._zombiesLoaded) { - this._zombiesLoaded[zombie].destroyInternalCache(drawerId); + for (let zombie of this._zombiesLoaded) { + if (zombie) { + zombie.destroyInternalCache(drawerId); + } } - for (let cache in this._tilesLoaded) { - this._tilesLoaded[cache].destroyInternalCache(drawerId); + for (let cache of this._cachesLoaded) { + if (cache) { + cache.destroyInternalCache(drawerId); + } } } diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index 9f965866..73b06aa0 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -92,21 +92,6 @@ this.testEvents = new OpenSeadragon.EventSource(); } - getType() { - return "test-cache-drawer"; - } - - // Make test use private cache - get defaultOptions() { - return { - usePrivateCache: true - }; - } - - getSupportedDataFormats() { - return [T_C, T_E]; - } - static isSupported() { return true; } @@ -128,12 +113,16 @@ } } + dataFree(data) { + this.testEvents.raiseEvent('free-data'); + } + canRotate() { return true; } destroy() { - //noop + this.destroyInternalCache(); } setImageSmoothingEnabled(imageSmoothingEnabled){ @@ -149,6 +138,60 @@ } } + OpenSeadragon.SyncInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { + + getType() { + return "test-cache-drawer-sync"; + } + + getSupportedDataFormats() { + return [T_C, T_E]; + } + + // Make test use private cache + get defaultOptions() { + return { + usePrivateCache: true, + preloadCache: false, + }; + } + + dataCreate(cache, tile) { + this.testEvents.raiseEvent('create-data'); + return cache.data; + } + } + + OpenSeadragon.AsnycInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { + + getType() { + return "test-cache-drawer-async"; + } + + getSupportedDataFormats() { + return [T_A]; + } + + // Make test use private cache + get defaultOptions() { + return { + usePrivateCache: true, + preloadCache: true, + }; + } + + dataCreate(cache, tile) { + this.testEvents.raiseEvent('create-data'); + return cache.getDataAs(T_C, true); + } + + dataFree(data) { + super.dataFree(data); + // Be nice and truly destroy the data copy + OpenSeadragon.convertor.destroy(data, T_C); + } + } + OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource { supports( data, url ){ @@ -184,14 +227,6 @@ $('
').appendTo("#qunit-fixture"); testLog.reset(); - - viewer = OpenSeadragon({ - id: 'example', - prefixUrl: '/build/openseadragon/images/', - maxImageCacheCount: 200, //should be enough to fit test inside the cache - springStiffness: 100, // Faster animation = faster tests - drawer: 'test-cache-drawer', - }); OpenSeadragon.ImageLoader.prototype.addJob = originalJob; // Reset counters @@ -313,201 +348,234 @@ done(); }); - //Tile API and cache interaction - // QUnit.test('Tile: basic rendering & test setup', function(test) { - // const done = test.async(); - // - // const tileCache = viewer.tileCache; - // const drawer = viewer.drawer; - // - // let testTileCalled = false; - // drawer.testEvents.addHandler('test-tile', e => { - // testTileCalled = true; - // test.ok(e.dataToDraw, "Tile data is ready to be drawn"); - // }); - // - // viewer.addHandler('open', async () => { - // await viewer.waitForFinishedJobsForTest(); - // await sleep(1); // necessary to make space for a draw call - // - // test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); - // test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); - // test.ok(testTileCalled, "Drawer tested at least one tile."); - // - // test.ok(typeAtoB > 1, "At least one conversion was triggered."); - // test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer."); - // - // for (let tile of tileCache._tilesLoaded) { - // const cache = tile.getCache(); - // test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache."); - // - // const internalCache = cache.getCacheForRendering(drawer, tile); - // test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E."); - // test.ok(internalCache.loaded, "Internal cache ready."); - // } - // - // done(); - // }); - // viewer.open([ - // {isTestSource: true}, - // {isTestSource: true}, - // ]); - // }); + // Tile API and cache interaction + QUnit.test('Tile: basic rendering & test setup (sync drawer)', function(test) { + const done = test.async(); - // QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) { - // const done = test.async(); - // - // const tileCache = viewer.tileCache; - // const drawer = viewer.drawer; - // - // let testTileCalled = false; - // - // let _currentTestVal = undefined; - // let previousTestValue = undefined; - // drawer.testEvents.addHandler('test-tile', e => { - // test.ok(e.dataToDraw, "Tile data is ready to be drawn"); - // if (_currentTestVal !== undefined) { - // testTileCalled = true; - // test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data."); - // } - // }); - // - // function testDrawingRoutine(value) { - // _currentTestVal = value; - // viewer.world.needsDraw(); - // viewer.world.draw(); - // previousTestValue = value; - // _currentTestVal = undefined; - // } - // - // viewer.addHandler('open', async () => { - // await viewer.waitForFinishedJobsForTest(); - // await sleep(1); // necessary to make space for a draw call - // - // // Test simple data set -> creates main cache - // - // let testHandler = async e => { - // // data comes in as T_A - // test.equal(typeDtoA, 0, "No conversion needed to get type A."); - // test.equal(typeCtoA, 0, "No conversion needed to get type A."); - // - // const data = await e.getData(T_A); - // test.equal(data, 1, "Copy: creation of a working cache."); - // e.tile.__TEST_PROCESSED = true; - // - // // Test value 2 since we set T_C no need to convert - // await e.setData(2, T_C); - // test.notOk(e.outdated(), "Event is still valid."); - // }; - // - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(true); - // await sleep(1); // necessary to make space for internal updates - // testDrawingRoutine(2); - // - // //test for each level only single cache was processed - // const processedLevels = {}; - // for (let tile of tileCache._tilesLoaded) { - // const level = tile.level; - // - // if (tile.__TEST_PROCESSED) { - // test.ok(!processedLevels[level], "Only single tile processed per level."); - // processedLevels[level] = true; - // delete tile.__TEST_PROCESSED; - // } - // - // const origCache = tile.getCache(tile.originalCacheKey); - // test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache."); - // test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache."); - // - // const cache = tile.getCache(); - // test.equal(cache.type, T_C, "Main Cache Updated (suite 1)"); - // test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)"); - // - // const internalCache = cache.getCacheForRendering(drawer, tile); - // test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E."); - // test.ok(internalCache.loaded, "Internal cache ready."); - // } - // - // // Test that basic scenario with reset data false starts from the main cache data of previous round - // const modificationConstant = 50; - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // const data = await e.getData(T_B); - // test.equal(data, previousTestValue + 2, "C -> A -> B conversion happened."); - // await e.setData(data + modificationConstant, T_B); - // console.log(data + modificationConstant); - // test.notOk(e.outdated(), "Event is still valid."); - // }; - // console.log(previousTestValue, modificationConstant) - // - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(false); - // await sleep(1); // necessary to make space for a draw call - // // We set data as TB - there is T_C -> T_A -> T_B -> T_C conversion round - // let newValue = previousTestValue + modificationConstant + 3; - // testDrawingRoutine(newValue); - // - // newValue--; // intenrla cache performed +1 conversion, but here we have main cache with one step less - // for (let tile of tileCache._tilesLoaded) { - // const cache = tile.getCache(); - // test.equal(cache.type, T_B, "Main Cache Updated (suite 2)."); - // test.equal(cache.data, newValue, "Main Cache Updated (suite 2)."); - // } - // - // // Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // const data = await e.getData(T_B); - // test.equal(data, 1, "Copy: creation of a working cache."); - // await e.setData(-8, T_E); - // e.resetData(); - // }; - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(true); - // await sleep(1); // necessary to make space for a draw call - // testDrawingRoutine(2); // Value +2 rendering from original data - // - // for (let tile of tileCache._tilesLoaded) { - // const origCache = tile.getCache(tile.originalCacheKey); - // test.ok(tile.getCache() === origCache, "Main cache is now original cache."); - // } - // - // // Now force main cache creation that differs - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // await e.setData(41, T_B); - // }; - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(true); - // - // // Now test whether data reset works, even with non-original data - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // const data = await e.getData(T_B); - // test.equal(data, 42, "Copy: 41 + 1."); - // await e.setData(data, T_E); - // e.resetData(); - // }; - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(false); - // await sleep(1); // necessary to make space for a draw call - // testDrawingRoutine(42); - // - // for (let tile of tileCache._tilesLoaded) { - // const origCache = tile.getCache(tile.originalCacheKey); - // test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh."); - // test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh."); - // } - // - // test.ok(testTileCalled, "Drawer tested at least one tile."); - // done(); - // }); - // viewer.open([ - // {isTestSource: true}, - // {isTestSource: true}, - // ]); - // }); + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-sync', + }); + + const tileCache = viewer.tileCache; + const drawer = viewer.drawer; + + let testTileCalled = false; + let countFreeCalled = 0; + let countCreateCalled = 0; + drawer.testEvents.addHandler('test-tile', e => { + testTileCalled = true; + test.ok(e.dataToDraw, "Tile data is ready to be drawn"); + }); + drawer.testEvents.addHandler('create-data', e => { + countCreateCalled++; + }); + drawer.testEvents.addHandler('free-data', e => { + countFreeCalled++; + }); + + viewer.addHandler('open', async () => { + await viewer.waitForFinishedJobsForTest(); + await sleep(1); // necessary to make space for a draw call + + test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); + test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); + test.ok(testTileCalled, "Drawer tested at least one tile."); + + test.ok(typeAtoB > 1, "At least one conversion was triggered."); + test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer."); + + for (let tile of tileCache._tilesLoaded) { + const cache = tile.getCache(); + test.equal(cache.type, T_C, "Cache data was affected, the drawer supports only T_C since there is no way to get to T_E."); + + const internalCache = cache.getDataForRendering(drawer, tile); + test.equal(internalCache.type, viewer.drawer.getId(), "Sync conversion routine means T_C is also internal since dataCreate only creates data. However, internal cache keeps type of the drawer ID."); + test.ok(internalCache.loaded, "Internal cache ready."); + } + + test.ok(countCreateCalled > 0, "Internal cache creation called."); + viewer.drawer.destroyInternalCache(); + test.equal(countCreateCalled, countFreeCalled, "Free called as many times as create."); + + done(); + }); + viewer.open([ + {isTestSource: true}, + {isTestSource: true}, + ]); + }); + + QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) { + const done = test.async(); + + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-async', + }); + const tileCache = viewer.tileCache; + const drawer = viewer.drawer; + + let testTileCalled = false; + + let _currentTestVal = undefined; + let previousTestValue = undefined; + drawer.testEvents.addHandler('test-tile', e => { + test.ok(e.dataToDraw, "Tile data is ready to be drawn"); + if (_currentTestVal !== undefined) { + testTileCalled = true; + test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data."); + } + }); + + function testDrawingRoutine(value) { + _currentTestVal = value; + viewer.world.needsDraw(); + viewer.world.draw(); + _currentTestVal = undefined; + } + + viewer.addHandler('open', async () => { + await viewer.waitForFinishedJobsForTest(); + await sleep(1); // necessary to make space for a draw call + + // Test simple data set -> creates main cache + + let testHandler = async e => { + // data comes in as T_A + test.equal(typeDtoA, 0, "No conversion needed to get type A."); + test.equal(typeCtoA, 0, "No conversion needed to get type A."); + + const data = await e.getData(T_A); + test.equal(data, 1, "Copy: creation of a working cache."); + e.tile.__TEST_PROCESSED = true; + + // Test value 2 since we set T_C no need to convert + await e.setData(2, T_C); + test.notOk(e.outdated(), "Event is still valid."); + }; + + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(true); + + //test for each level only single cache was processed + const processedLevels = {}; + for (let tile of tileCache._tilesLoaded) { + const level = tile.level; + + if (tile.__TEST_PROCESSED) { + test.ok(!processedLevels[level], "Only single tile processed per level."); + processedLevels[level] = true; + delete tile.__TEST_PROCESSED; + } + + const origCache = tile.getCache(tile.originalCacheKey); + test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache."); + test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache."); + + const cache = tile.getCache(); + test.equal(cache.type, T_A, "Main Cache Converted T_C -> T_A (drawer supports type A) (suite 1)"); + test.equal(cache.data, 3, "Conversion step increases plugin-stored value 2 to 3"); + + const internalCache = cache.getDataForRendering(drawer, tile); + test.equal(internalCache.type, viewer.drawer.getId(), "Internal cache has type of the drawer ID."); + test.ok(internalCache.loaded, "Internal cache ready."); + } + // Internal cache will have value 5: main cache is 3, type is T_A, + testDrawingRoutine(5); // internal cache transforms to T_C: two steps, TA->TB->TC 3+2 + + // Test that basic scenario with reset data false starts from the main cache data of previous round + const modificationConstant = 50; + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + const data = await e.getData(T_B); + test.equal(data, 4, "A -> B conversion happened, we started from value 3 in the main cache."); + await e.setData(data + modificationConstant, T_B); + test.notOk(e.outdated(), "Event is still valid."); + }; + + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(false); + + // We set data as TB - there is required T_A: T_B -> T_C -> T_A conversion round on the main cache + let newValue = modificationConstant + 4 + 2; + // and there is still requirement of T_C on internal data, +2 steps + testDrawingRoutine(newValue + 2); + + for (let tile of tileCache._tilesLoaded) { + const cache = tile.getCache(); + test.equal(cache.type, T_A, "Main Cache Updated (suite 2)."); + test.equal(cache.data, newValue, "Main Cache Updated (suite 2)."); + } + + // Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + const data = await e.getData(T_B); + test.equal(data, 1, "Copy: creation of a working cache."); + await e.setData(-8, T_E); + e.resetData(); + }; + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(true); + await sleep(1); // necessary to make space for a draw call + testDrawingRoutine(2); // Value +2 rendering from original data + + for (let tile of tileCache._tilesLoaded) { + const origCache = tile.getCache(tile.originalCacheKey); + test.ok(tile.getCache() === origCache, "Main cache is now original cache."); + } + + // Now force main cache creation that differs + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + await e.setData(41, T_B); + }; + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(true); + + // Now test whether data reset works, even with non-original data + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + const data = await e.getData(T_B); + test.equal(data, 44, "Copy: 41 +2 (previous request invalidate ends at T_A) + 1 (we request type B)."); + await e.setData(data, T_E); // there is no way to convert T_E -> T_A, this would throw an error + e.resetData(); // reset data will revert to original cache + }; + viewer.addHandler('tile-invalidated', testHandler); + + // The data will be 45 since no change has been made: + // last main cache set was 41 T_B, supported T_A = +2 + // and internal requirement T_C = +2 + const checkNotCalled = e => { + test.ok(false, "Create data must not be called when there is no change!"); + }; + drawer.testEvents.addHandler('create-data', checkNotCalled); + + await viewer.world.requestInvalidate(false); + testDrawingRoutine(45); + + for (let tile of tileCache._tilesLoaded) { + const origCache = tile.getCache(tile.originalCacheKey); + test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh."); + test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh."); + } + + test.ok(testTileCalled, "Drawer tested at least one tile."); + viewer.destroy(); + done(); + }); + viewer.open([ + {isTestSource: true}, + {isTestSource: true}, + ]); + }); //Tile API and cache interaction QUnit.test('Tile API Cache Interaction', function(test) { @@ -632,6 +700,14 @@ QUnit.test('Zombie Cache', function(test) { const done = test.async(); + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-sync', + }); + //test jobs by coverage: fail if cached coverage not fully re-stored without jobs let jobCounter = 0, coverage = undefined; OpenSeadragon.ImageLoader.prototype.addJob = function (options) { @@ -711,6 +787,14 @@ QUnit.test('Zombie Cache Replace Item', function(test) { const done = test.async(); + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-sync', + }); + let jobCounter = 0, coverage = undefined; OpenSeadragon.ImageLoader.prototype.addJob = function (options) { jobCounter++;