Redesign working cache: it is now owned by the event, not a tile. Tests are not yet updated.

This commit is contained in:
Aiosa 2024-11-13 14:35:50 +01:00
parent e059b8982e
commit 541fe2e4df
10 changed files with 320 additions and 292 deletions

View File

@ -33,7 +33,6 @@
*/ */
(function( $ ){ (function( $ ){
let _workingCacheIdDealer = 0;
/** /**
* @class Tile * @class Tile
@ -252,16 +251,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @private * @private
*/ */
this._caches = {}; this._caches = {};
/**
* Static Working Cache key to keep cached object (for swapping) when executing modifications.
* Uses unique ID to prevent sharing between other tiles:
* - if some tile initiates processing, all other tiles usually are skipped if they share the data
* - if someone tries to bypass sharing and process all tiles that share data, working caches would collide
* Note that $.now() is not sufficient, there might be tile created in the same millisecond.
* @member {String}
* @private
*/
this._wcKey = `w${_workingCacheIdDealer++}://` + this.originalCacheKey;
/** /**
* Processing flag, exempt the tile from removal when there are ongoing updates * Processing flag, exempt the tile from removal when there are ongoing updates
* @member {Boolean|Number} * @member {Boolean|Number}
@ -273,14 +262,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @private * @private
*/ */
this.lastProcess = 0; this.lastProcess = 0;
/**
* Transforming flag, exempt the tile from any processing since it is being transformed to a drawer-compatible
* format. This process cannot be paused and the tile cannot be touched during the process. Used for tile-locking
* in the data invalidation routine.
* @member {Boolean|Number}
* @private
*/
this.transforming = false;
}; };
/** @lends OpenSeadragon.Tile.prototype */ /** @lends OpenSeadragon.Tile.prototype */
@ -420,7 +401,7 @@ $.Tile.prototype = {
* @returns {?Image} * @returns {?Image}
*/ */
getImage: function() { getImage: function() {
$.console.error("[Tile.getImage] property has been deprecated. Use [Tile.getData] instead."); $.console.error("[Tile.getImage] property has been deprecated. Use 'tile-invalidated' routine event instead.");
//this method used to ensure the underlying data model conformed to given type - convert instead of getData() //this method used to ensure the underlying data model conformed to given type - convert instead of getData()
const cache = this.getCache(this.cacheKey); const cache = this.getCache(this.cacheKey);
if (!cache) { if (!cache) {
@ -445,10 +426,10 @@ $.Tile.prototype = {
/** /**
* Get the CanvasRenderingContext2D instance for tile image data drawn * Get the CanvasRenderingContext2D instance for tile image data drawn
* onto Canvas if enabled and available * onto Canvas if enabled and available
* @returns {?CanvasRenderingContext2D} * @returns {CanvasRenderingContext2D|undefined}
*/ */
getCanvasContext: function() { getCanvasContext: function() {
$.console.error("[Tile.getCanvasContext] property has been deprecated. Use [Tile.getData] instead."); $.console.error("[Tile.getCanvasContext] property has been deprecated. Use 'tile-invalidated' routine event instead.");
//this method used to ensure the underlying data model conformed to given type - convert instead of getData() //this method used to ensure the underlying data model conformed to given type - convert instead of getData()
const cache = this.getCache(this.cacheKey); const cache = this.getCache(this.cacheKey);
if (!cache) { if (!cache) {
@ -464,7 +445,7 @@ $.Tile.prototype = {
* @type {CanvasRenderingContext2D} * @type {CanvasRenderingContext2D}
*/ */
get context2D() { get context2D() {
$.console.error("[Tile.context2D] property has been deprecated. Use [Tile.getData] instead."); $.console.error("[Tile.context2D] property has been deprecated. Use 'tile-invalidated' routine event instead.");
return this.getCanvasContext(); return this.getCanvasContext();
}, },
@ -473,9 +454,12 @@ $.Tile.prototype = {
* @deprecated * @deprecated
*/ */
set context2D(value) { set context2D(value) {
$.console.error("[Tile.context2D] property has been deprecated. Use [Tile.setData] within dedicated update event instead."); $.console.error("[Tile.context2D] property has been deprecated. Use 'tile-invalidated' routine event instead.");
this.setData(value, "context2d"); const cache = this._caches[this.cacheKey];
this.updateRenderTarget(); if (cache) {
this.removeCache(this.cacheKey);
}
this.addCache(this.cacheKey, value, 'context2d', true, false);
}, },
/** /**
@ -510,128 +494,12 @@ $.Tile.prototype = {
}, },
/** /**
* Get the data to render for this tile. If no conversion is necessary, get a reference. Else, get a copy * Cache key for main cache that is 'cache-equal', but different from original cache key
* of the data as desired type. This means that data modification _might_ be reflected on the tile, but * @return {string}
* it is not guaranteed. Use tile.setData() to ensure changes are reflected.
* @param {string} type data type to require
* @return {OpenSeadragon.Promise<*>} data in the desired type, or resolved promise with udnefined if the
* associated cache object is out of its lifespan
*/
getData: function(type) {
if (!this.tiledImage) {
return $.Promise.resolve(); //async can access outside its lifetime
}
return this._getOrCreateWorkingCacheData(type);
},
/**
* Restore the original data data for this tile
* @param {boolean} freeIfUnused if true, restoration frees cache along the way of the tile lifecycle
*/
restore: function(freeIfUnused = true) {
if (!this.tiledImage) {
return; //async context can access the tile outside its lifetime
}
this.__restoreRequestedFree = freeIfUnused;
if (this.originalCacheKey !== this.cacheKey) {
this.__restore = true;
}
// Somebody has called restore on this tile, make sure we delete working cache in case there was some
this.removeCache(this._wcKey, true);
},
/**
* Set main cache data
* @param {*} value
* @param {?string} type data type to require
* @return {OpenSeadragon.Promise<*>}
*/
setData: function(value, type) {
if (!this.tiledImage) {
return Promise.resolve(); //async context can access the tile outside its lifetime
}
let cache = this.getCache(this._wcKey);
if (!cache) {
this._getOrCreateWorkingCacheData(undefined);
cache = this.getCache(this._wcKey);
}
return cache.setDataAs(value, type);
},
/**
* Optimizazion: prepare target cache for subsequent use in rendering, and perform updateRenderTarget()
* The main idea of this function is that it must be ASYNCHRONOUS since there might be additional processing
* happening due to underlying drawer requirements.
* @return {OpenSeadragon.Promise<?>}
* @private * @private
*/ */
updateRenderTargetWithDataTransform: function (drawerId, supportedFormats, usePrivateCache, processTimestamp) { buildDistinctMainCacheKey: function () {
// Now, if working cache exists, we set main cache to the working cache --> prepare return this.cacheKey === this.originalCacheKey ? "mod://" + this.originalCacheKey : this.cacheKey;
let cache = this.getCache(this._wcKey);
if (cache) {
return cache.prepareForRendering(drawerId, supportedFormats, usePrivateCache).then(c => {
if (c && processTimestamp && this.processing === processTimestamp) {
this.updateRenderTarget();
}
});
}
// If we requested restore, perform now
if (this.__restore) {
cache = this.getCache(this.originalCacheKey);
this.tiledImage._tileCache.restoreTilesThatShareOriginalCache(
this, cache, this.__restoreRequestedFree
);
this.__restore = false;
return cache.prepareForRendering(drawerId, supportedFormats, usePrivateCache).then((c) => {
if (c && processTimestamp && this.processing === processTimestamp) {
this.updateRenderTarget();
}
});
}
cache = this.getCache();
return cache.prepareForRendering(drawerId, supportedFormats, usePrivateCache);
},
/**
* Resolves render target: changes might've been made to the rendering pipeline:
* - working cache is set: make sure main cache will be replaced
* - working cache is unset: make sure main cache either gets updated to original data or stays (based on this.__restore)
*
* The main idea of this function is that it is SYNCHRONOUS, e.g. can perform in-place cache swap to update
* before any rendering occurs.
* @private
*/
updateRenderTarget: function (_allowTileNotLoaded = false) {
// Check if we asked for restore, and make sure we set it to false since we update the whole cache state
const requestedRestore = this.__restore;
this.__restore = false;
// Now, if working cache exists, we set main cache to the working cache, since it has been updated
// if restore() was called last, then working cache was deleted (does not exist)
const cache = this.getCache(this._wcKey);
if (cache) {
let newCacheKey = this.cacheKey === this.originalCacheKey ? "mod://" + this.originalCacheKey : this.cacheKey;
this.tiledImage._tileCache.consumeCache({
tile: this,
victimKey: this._wcKey,
consumerKey: newCacheKey,
tileAllowNotLoaded: _allowTileNotLoaded
});
this.cacheKey = newCacheKey;
} else if (requestedRestore) {
// If we requested restore, perform now
this.tiledImage._tileCache.restoreTilesThatShareOriginalCache(
this, this.getCache(this.originalCacheKey), this.__restoreRequestedFree
);
}
// If transforming was set, we finished: drawer transform always finishes with updateRenderTarget()
this.transforming = false;
}, },
/** /**
@ -648,10 +516,10 @@ $.Tile.prototype = {
}, },
/** /**
* Set tile cache, possibly multiple with custom key * Create tile cache for given data object. NOTE: if the existing cache already exists,
* @param {string} key cache key, must be unique (we recommend re-using this.cacheTile * data parameter is ignored and inherited from the existing cache object.
* value and extend it with some another unique content, by default overrides the existing *
* main cache used for drawing, if not existing. * @param {string} key cache key, if unique, new cache object is created, else existing cache attached
* @param {*} data this data will be IGNORED if cache already exists; therefore if * @param {*} data this data will be IGNORED if cache already exists; therefore if
* `typeof data === 'function'` holds (both async and normal functions), the data is called to obtain * `typeof data === 'function'` holds (both async and normal functions), the data is called to obtain
* 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.
@ -663,7 +531,8 @@ $.Tile.prototype = {
* @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to. * @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to.
*/ */
addCache: function(key, data, type = undefined, setAsMain = false, _safely = true) { addCache: function(key, data, type = undefined, setAsMain = false, _safely = true) {
if (!this.tiledImage) { const tiledImage = this.tiledImage;
if (!tiledImage) {
return null; //async can access outside its lifetime return null; //async can access outside its lifetime
} }
@ -679,35 +548,83 @@ $.Tile.prototype = {
type = $.convertor.guessType(data); type = $.convertor.guessType(data);
} }
const writesToRenderingCache = key === this.cacheKey; const overwritesMainCache = key === this.cacheKey;
if (writesToRenderingCache && _safely) { if (_safely && (overwritesMainCache || setAsMain)) {
// Need to get the supported type for rendering out of the active drawer. // Need to get the supported type for rendering out of the active drawer.
const supportedTypes = this.tiledImage.viewer.drawer.getSupportedDataFormats(); const supportedTypes = tiledImage.viewer.drawer.getSupportedDataFormats();
const conversion = $.convertor.getConversionPath(type, supportedTypes); const conversion = $.convertor.getConversionPath(type, supportedTypes);
$.console.assert(conversion, "[Tile.addCache] data was set for the default tile cache we are unable" + $.console.assert(conversion, "[Tile.addCache] data was set for the default tile cache we are unable" +
"to render. Make sure OpenSeadragon.convertor was taught to convert to (one of): " + type); `to render. Make sure OpenSeadragon.convertor was taught to convert ${type} to (one of): ${conversion.toString()}`);
} }
const cachedItem = this.tiledImage._tileCache.cacheTile({ const cachedItem = tiledImage._tileCache.cacheTile({
data: data, data: data,
dataType: type, dataType: type,
tile: this, tile: this,
cacheKey: key, cacheKey: key,
//todo consider caching this on a tiled image level cutoff: tiledImage.source.getClosestLevel(),
cutoff: this.__cutoff || this.tiledImage.source.getClosestLevel(),
}); });
const havingRecord = this._caches[key]; const havingRecord = this._caches[key];
if (havingRecord !== cachedItem) { if (havingRecord !== cachedItem) {
this._caches[key] = cachedItem; this._caches[key] = cachedItem;
if (havingRecord) {
havingRecord.removeTile(this);
tiledImage._tileCache.safeUnloadCache(havingRecord);
}
} }
// Update cache key if differs and main requested // Update cache key if differs and main requested
if (!writesToRenderingCache && setAsMain) { if (!overwritesMainCache && setAsMain) {
this._updateMainCacheKey(key); this._updateMainCacheKey(key);
} }
return cachedItem; return cachedItem;
}, },
/**
* Add cache object to the tile
*
* @param {string} key cache key, if unique, new cache object is created, else existing cache attached
* @param {OpenSeadragon.CacheRecord} cache the cache object to attach to this tile
* @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
* @returns {OpenSeadragon.CacheRecord|null} - Returns cache parameter reference if attached.
*/
setCache(key, cache, setAsMain = false, _safely = true) {
const tiledImage = this.tiledImage;
if (!tiledImage) {
return null; //async can access outside its lifetime
}
const overwritesMainCache = key === this.cacheKey;
if (_safely) {
$.console.assert(cache instanceof $.CacheRecord, "[Tile.setCache] cache must be a CacheRecord object!");
if (overwritesMainCache || setAsMain) {
// Need to get the supported type for rendering out of the active drawer.
const supportedTypes = tiledImage.viewer.drawer.getSupportedDataFormats();
const conversion = $.convertor.getConversionPath(cache.type, supportedTypes);
$.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" +
`to render. Make sure OpenSeadragon.convertor was taught to convert ${cache.type} to (one of): ${conversion.toString()}`);
}
}
const havingRecord = this._caches[key];
if (havingRecord !== cache) {
this._caches[key] = cache;
if (havingRecord) {
havingRecord.removeTile(this);
tiledImage._tileCache.safeUnloadCache(havingRecord);
}
}
// Update cache key if differs and main requested
if (!overwritesMainCache && setAsMain) {
this._updateMainCacheKey(key);
}
return cache;
},
/** /**
* Sets the main cache key for this tile and * Sets the main cache key for this tile and
* performs necessary updates * performs necessary updates
@ -717,36 +634,11 @@ $.Tile.prototype = {
_updateMainCacheKey: function(value) { _updateMainCacheKey: function(value) {
let ref = this._caches[this._cKey]; let ref = this._caches[this._cKey];
if (ref) { if (ref) {
// make sure we free drawer internal cache // make sure we free drawer internal cache if people change cache key externally
// todo make sure this is really needed even after refactoring
ref.destroyInternalCache(); ref.destroyInternalCache();
} }
this._cKey = value; this._cKey = value;
// we do not trigger redraw, this is handled within cache
// as drawers request data for drawing
},
/**
* Initializes working cache if it does not exist.
* @param {string|undefined} type initial cache type to create
* @return {OpenSeadragon.Promise<?>} data-awaiting promise with the cache data
* @private
*/
_getOrCreateWorkingCacheData: function (type) {
const cache = this.getCache(this._wcKey);
if (!cache) {
const targetCopyKey = this.__restore ? this.originalCacheKey : this.cacheKey;
const origCache = this.getCache(targetCopyKey);
if (!origCache) {
$.console.error("[Tile::getData] There is no cache available for tile with key %s", targetCopyKey);
}
// Here ensure type is defined, rquired by data callbacks
type = type || origCache.type;
// Here we use extensively ability to call addCache with callback: working cache is created only if not
// already in memory (=> shared).
return this.addCache(this._wcKey, () => origCache.getDataAs(type, true), type, false, false).await();
}
return cache.getDataAs(type, false);
}, },
/** /**
@ -761,12 +653,15 @@ $.Tile.prototype = {
* Free tile cache. Removes by default the cache record if no other tile uses it. * Free tile cache. Removes by default the cache record if no other tile uses it.
* @param {string} key cache key, required * @param {string} key cache key, required
* @param {boolean} [freeIfUnused=true] set to false if zombie should be created * @param {boolean} [freeIfUnused=true] set to false if zombie should be created
* @return {OpenSeadragon.CacheRecord|undefined} reference to the cache record if it was removed,
* undefined if removal was refused to perform (e.g. does not exist, it is an original data target etc.)
*/ */
removeCache: function(key, freeIfUnused = true) { removeCache: function(key, freeIfUnused = true) {
if (!this._caches[key]) { const deleteTarget = this._caches[key];
if (!deleteTarget) {
// try to erase anyway in case the cache got stuck in memory // try to erase anyway in case the cache got stuck in memory
this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, true); this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, true);
return; return undefined;
} }
const currentMainKey = this.cacheKey, const currentMainKey = this.cacheKey,
@ -776,7 +671,7 @@ $.Tile.prototype = {
if (!sameBuiltinKeys && originalDataKey === key) { if (!sameBuiltinKeys && originalDataKey === key) {
$.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!", $.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!",
"If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData()."); "If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData().");
return; return undefined;
} }
if (currentMainKey === key) { if (currentMainKey === key) {
@ -786,13 +681,14 @@ $.Tile.prototype = {
} else { } else {
$.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!", $.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!",
"If you want to remove the main cache, first set different cache as main with tile.addCache()"); "If you want to remove the main cache, first set different cache as main with tile.addCache()");
return; return undefined;
} }
} }
if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, false)) { if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, false)) {
//if we managed to free tile from record, we are sure we decreased cache count //if we managed to free tile from record, we are sure we decreased cache count
delete this._caches[key]; delete this._caches[key];
} }
return deleteTarget;
}, },
/** /**
@ -826,8 +722,8 @@ $.Tile.prototype = {
// the sketch canvas to the top and left and we must use negative coordinates to repaint it // the sketch canvas to the top and left and we must use negative coordinates to repaint it
// to the main canvas. In that case, some browsers throw: // to the main canvas. In that case, some browsers throw:
// INDEX_SIZE_ERR: DOM Exception 1: Index or size was negative, or greater than the allowed value. // INDEX_SIZE_ERR: DOM Exception 1: Index or size was negative, or greater than the allowed value.
var x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2)); const x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2));
var y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2)); const y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2));
return new $.Point(x, y).minus( return new $.Point(x, y).minus(
this.position this.position
.times($.pixelDensityRatio) .times($.pixelDensityRatio)

View File

@ -163,7 +163,7 @@
/** /**
* Access of the data by drawers, synchronous function. Should always access a valid main cache, e.g. * Access of the data by drawers, synchronous function. Should always access a valid main cache, e.g.
* cache swap performed on working cache (consumeCache()) must be synchronous such that cache is always * cache swap performed on working cache (replaceCache()) must be synchronous such that cache is always
* ready to render, and swaps atomically between render calls. * ready to render, and swaps atomically between render calls.
* *
* @param {OpenSeadragon.DrawerBase} drawer drawer reference which requests the data: the drawer * @param {OpenSeadragon.DrawerBase} drawer drawer reference which requests the data: the drawer
@ -190,8 +190,9 @@
let internalCache = this[DRAWER_INTERNAL_CACHE]; let internalCache = this[DRAWER_INTERNAL_CACHE];
internalCache = internalCache && internalCache[drawer.getId()]; internalCache = internalCache && internalCache[drawer.getId()];
if (keepInternalCopy && !internalCache) { if (keepInternalCopy && !internalCache) {
$.console.warn("Attempt to render %s that is not prepared with drawer requesting " + $.console.warn("Attempt to render cache that is not prepared for current drawer " +
"internal cache! This might introduce artifacts.", this.toString()); "supported format: the preparation should've happened after tile processing has finished.",
this, tileToDraw);
this.prepareForRendering(drawer.getId(), supportedTypes, keepInternalCopy) this.prepareForRendering(drawer.getId(), supportedTypes, keepInternalCopy)
.then(() => this._triggerNeedsDraw()); .then(() => this._triggerNeedsDraw());
@ -211,8 +212,10 @@
} }
if (!supportedTypes.includes(internalCache.type)) { if (!supportedTypes.includes(internalCache.type)) {
$.console.warn("Attempt to render %s that is not prepared for current drawer " + $.console.warn("Attempt to render cache that is not prepared for current drawer " +
"supported format: the preparation should've happened after tile processing has finished.", this.toString()); "supported format: the preparation should've happened after tile processing has finished.",
Object.entries(this[DRAWER_INTERNAL_CACHE]),
this, tileToDraw);
internalCache.transformTo(supportedTypes.length > 1 ? supportedTypes : supportedTypes[0]) internalCache.transformTo(supportedTypes.length > 1 ? supportedTypes : supportedTypes[0])
.then(() => this._triggerNeedsDraw()); .then(() => this._triggerNeedsDraw());
@ -226,8 +229,8 @@
* @private * @private
* @param drawerId * @param drawerId
* @param supportedTypes * @param supportedTypes
* @param keepInternalCopy * @param keepInternalCopy if a drawer requests internal copy, it means it can only use
* given cache for itself, cannot be shared -> initialize privately
* @return {OpenSeadragon.Promise<OpenSeadragon.SimpleCacheRecord|OpenSeadragon.CacheRecord> | null} * @return {OpenSeadragon.Promise<OpenSeadragon.SimpleCacheRecord|OpenSeadragon.CacheRecord> | null}
* reference to the cache processed for drawer rendering requirements, or null on error * reference to the cache processed for drawer rendering requirements, or null on error
*/ */
@ -330,10 +333,12 @@
* Conversion requires tile references: * Conversion requires tile references:
* keep the most 'up to date' ref here. It is called and managed automatically. * keep the most 'up to date' ref here. It is called and managed automatically.
* @param {OpenSeadragon.Tile} ref * @param {OpenSeadragon.Tile} ref
* @return {OpenSeadragon.CacheRecord} self reference for builder pattern
* @private * @private
*/ */
withTileReference(ref) { withTileReference(ref) {
this._tRef = ref; this._tRef = ref;
return this;
} }
/** /**
@ -642,9 +647,11 @@
* Must be called before transformTo or setDataAs. To keep * Must be called before transformTo or setDataAs. To keep
* compatible api with CacheRecord where tile refs are known. * compatible api with CacheRecord where tile refs are known.
* @param {OpenSeadragon.Tile} referenceTile reference tile for conversion * @param {OpenSeadragon.Tile} referenceTile reference tile for conversion
* @return {OpenSeadragon.SimpleCacheRecord} self reference for builder pattern
*/ */
withTileReference(referenceTile) { withTileReference(referenceTile) {
this._temporaryTileRef = referenceTile; this._temporaryTileRef = referenceTile;
return this;
} }
/** /**
@ -908,51 +915,92 @@
} }
/** /**
* Consume cache by another cache * Inject new cache to the system
* @param {Object} options
* @param {OpenSeadragon.Tile} options.tile - Reference tile. All tiles sharing original data will be affected.
* @param {OpenSeadragon.CacheRecord} options.cache - Cache that will be injected.
* @param {String} options.targetKey - The target cache key to inhabit. Can replace existing cache.
* @param {Boolean} options.setAsMainCache - If true, tiles main cache gets updated to consumerKey.
* Otherwise, if consumerKey==tile.cacheKey the cache is set as main too.
* @param {Boolean} options.tileAllowNotLoaded - if true, tile that is not loaded is also processed,
* this is internal parameter used in tile-loaded completion routine, as we need to prepare tile but
* it is not yet loaded and cannot be marked as so (otherwise the system would think it is ready)
* @private
*/
injectCache(options) {
const targetKey = options.targetKey,
tile = options.tile;
if (!options.tileAllowNotLoaded && !tile.loaded && !tile.loading) {
$.console.warn("Attempt to inject cache on tile in invalid state: this is probably a bug!");
return;
}
const consumer = this._cachesLoaded[targetKey];
if (consumer) {
// We need to avoid async execution here: replace consumer instead of overwriting the data.
const iterateTiles = [...consumer._tiles]; // unloadCacheForTile() will modify the array, use a copy
for (let tile of iterateTiles) {
this.unloadCacheForTile(tile, targetKey, true, false);
}
}
if (this._cachesLoaded[targetKey]) {
$.console.error("The inject routine should've freed cache!");
}
const cache = options.cache;
this._cachesLoaded[targetKey] = cache;
// Update cache: add the new cache, we must add since we removed above with unloadCacheForTile()
for (let t of tile.getCache(tile.originalCacheKey)._tiles) { // grab all cache-equal tiles
t.setCache(targetKey, cache, options.setAsMainCache, false);
}
}
/**
* Replace cache (and update tile references) by another cache
* @param {Object} options * @param {Object} options
* @param {OpenSeadragon.Tile} options.tile - The tile to own ot add record for the cache. * @param {OpenSeadragon.Tile} options.tile - The tile to own ot add record for the cache.
* @param {String} options.victimKey - Cache that will be erased. In fact, the victim _replaces_ consumer, * @param {String} options.victimKey - Cache that will be erased. In fact, the victim _replaces_ consumer,
* inheriting its tiles and key. * inheriting its tiles and key.
* @param {String} options.consumerKey - The cache that consumes the victim. In fact, it gets destroyed and * @param {String} options.consumerKey - The cache that consumes the victim. In fact, it gets destroyed and
* replaced by victim, which inherits all its metadata. * replaced by victim, which inherits all its metadata.
* @param {Boolean} options.setAsMainCache - If true, tiles main cache gets updated to consumerKey.
* Otherwise, if consumerKey==tile.cacheKey the cache is set as main too.
* @param {Boolean} options.tileAllowNotLoaded - if true, tile that is not loaded is also processed, * @param {Boolean} options.tileAllowNotLoaded - if true, tile that is not loaded is also processed,
* this is internal parameter used in tile-loaded completion routine, as we need to prepare tile but * this is internal parameter used in tile-loaded completion routine, as we need to prepare tile but
* it is not yet loaded and cannot be marked as so (otherwise the system would think it is ready) * it is not yet loaded and cannot be marked as so (otherwise the system would think it is ready)
* @private * @private
*/ */
consumeCache(options) { replaceCache(options) {
const victim = this._cachesLoaded[options.victimKey], const victimKey = options.victimKey,
consumerKey = options.consumerKey,
victim = this._cachesLoaded[victimKey],
tile = options.tile; tile = options.tile;
if (!victim || (!options.tileAllowNotLoaded && !tile.loaded && !tile.loading)) { if (!victim || (!options.tileAllowNotLoaded && !tile.loaded && !tile.loading)) {
$.console.warn("Attempt to consume non-existent cache: this is probably a bug!"); $.console.warn("Attempt to consume cache on tile in invalid state: this is probably a bug!");
return; return;
} }
const consumer = this._cachesLoaded[options.consumerKey]; const consumer = this._cachesLoaded[consumerKey];
let tiles = [...tile.getCache()._tiles];
if (consumer) { if (consumer) {
// We need to avoid async execution here: replace consumer instead of overwriting the data. // We need to avoid async execution here: replace consumer instead of overwriting the data.
const iterateTiles = [...consumer._tiles]; // unloadCacheForTile() will modify the array, use a copy const iterateTiles = [...consumer._tiles]; // unloadCacheForTile() will modify the array, use a copy
for (let tile of iterateTiles) { for (let tile of iterateTiles) {
this.unloadCacheForTile(tile, options.consumerKey, true, false); this.unloadCacheForTile(tile, consumerKey, true, false);
} }
} }
if (this._cachesLoaded[options.consumerKey]) { if (this._cachesLoaded[consumerKey]) {
console.error("The routine should've freed cache!"); $.console.error("The consume routine should've freed cache!");
} }
// Just swap victim to become new consumer // Just swap victim to become new consumer
const resultCache = this.renameCache({ const resultCache = this.renameCache({
oldCacheKey: options.victimKey, oldCacheKey: victimKey,
newCacheKey: options.consumerKey newCacheKey: consumerKey
}); });
if (resultCache) { if (resultCache) {
// Only one cache got working item, other caches were idle: update cache: add the new cache // Only one cache got working item, other caches were idle: update cache: add the new cache
// we can add since we removed above with unloadCacheForTile() // we must add since we removed above with unloadCacheForTile()
for (let tile of tiles) { for (let t of tile.getCache(tile.originalCacheKey)._tiles) { // grab all cache-equal tiles
if (tile !== options.tile) { t.setCache(consumerKey, resultCache, options.setAsMainCache, false);
tile.addCache(options.consumerKey, resultCache.data, resultCache.type, true, false);
}
} }
} }
} }
@ -967,11 +1015,13 @@
*/ */
restoreTilesThatShareOriginalCache(tile, originalCache, freeIfUnused) { restoreTilesThatShareOriginalCache(tile, originalCache, freeIfUnused) {
for (let t of originalCache._tiles) { for (let t of originalCache._tiles) {
this.unloadCacheForTile(t, t.cacheKey, freeIfUnused, false); if (t.cacheKey !== t.originalCacheKey) {
this.unloadCacheForTile(t, t.cacheKey, freeIfUnused, true);
delete t._caches[t.cacheKey]; delete t._caches[t.cacheKey];
t.cacheKey = t.originalCacheKey; t.cacheKey = t.originalCacheKey;
} }
} }
}
_freeOldRecordRoutine(theTile, cutoff) { _freeOldRecordRoutine(theTile, cutoff) {
let insertionIndex = this._tilesLoaded.length, let insertionIndex = this._tilesLoaded.length,
@ -1089,6 +1139,25 @@
return this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey]; return this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey];
} }
/**
* Delete cache safely from the system if it is not needed
* @param {OpenSeadragon.CacheRecord} cache
*/
safeUnloadCache(cache) {
if (cache && !cache._destroyed && cache.getTileCount() < 1) {
for (let i in this._zombiesLoaded) {
const c = this._zombiesLoaded[i];
if (c === cache) {
delete this._zombiesLoaded[i];
c.destroy();
return;
}
}
$.console.error("Attempt to delete an orphan cache that is not in zombie list: this could be a bug!", cache);
cache.destroy();
}
}
/** /**
* Delete cache record for a given til * Delete cache record for a given til
* @param {OpenSeadragon.Tile} tile * @param {OpenSeadragon.Tile} tile

View File

@ -1179,7 +1179,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
ajaxHeaders = {}; ajaxHeaders = {};
} }
if (!$.isPlainObject(ajaxHeaders)) { if (!$.isPlainObject(ajaxHeaders)) {
console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); $.console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
return; return;
} }
@ -1881,32 +1881,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {OpenSeadragon.Tile} tile * @param {OpenSeadragon.Tile} tile
*/ */
_tryFindTileCacheRecord: function(tile) { _tryFindTileCacheRecord: function(tile) {
let record = this._tileCache.getCacheRecord(tile.cacheKey); let record = this._tileCache.getCacheRecord(tile.originalCacheKey);
if (!record) { if (!record) {
return false; return false;
} }
tile.loading = true;
// if we find existing record, check the original data of existing tile of this record this._setTileLoaded(tile, record.data, null, null, record.type);
let baseTile = record._tiles[0];
if (!baseTile) {
// zombie cache -> revive, it's okay to use current tile as state inherit point since there is no state
baseTile = tile;
}
// Setup tile manually, data can be null -> we already have existing cache to share, share also caches
tile.tiledImage = this;
tile.addCache(baseTile.originalCacheKey, null, record.type, false, false);
if (baseTile.cacheKey !== baseTile.originalCacheKey) {
tile.addCache(baseTile.cacheKey, null, record.type, true, false);
}
tile.hasTransparency = tile.hasTransparency || this.source.hasTransparency(
undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData
);
tile.loading = false;
tile.loaded = true;
return true; return true;
}, },
@ -2154,7 +2135,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
function markTileAsReady() { function markTileAsReady() {
tile.lastProcess = false; tile.lastProcess = false;
tile.processing = false; tile.processing = false;
tile.transforming = false;
const fallbackCompletion = getCompletionCallback(); const fallbackCompletion = getCompletionCallback();
@ -2185,16 +2165,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
resolver = resolve; resolver = resolve;
}), }),
get image() { get image() {
$.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile.getData()' instead."); $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile-invalidated' event to modify data instead.");
return data; return data;
}, },
get data() { get data() {
$.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile.getData()' instead."); $.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile-invalidated' event to modify data instead.");
return data; return data;
}, },
getCompletionCallback: function () { getCompletionCallback: function () {
$.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: " + $.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: " +
"use async event handlers instead, execution order is deducted by addHandler(...) priority"); "use async event handlers instead, execution order is deducted by addHandler(...) priority argument.");
return getCompletionCallback(); return getCompletionCallback();
}, },
}).catch(() => { }).catch(() => {
@ -2207,8 +2187,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
const updatePromise = _this.viewer.world.requestTileInvalidateEvent([tile], now, false, true); const updatePromise = _this.viewer.world.requestTileInvalidateEvent([tile], now, false, true);
updatePromise.then(markTileAsReady); updatePromise.then(markTileAsReady);
} else { } else {
// In case we did not succeed in tile restoration, request invalidation // Tile-invalidated not called on each tile, but only on tiles with new data! Verify we share the main cache
// Tile-loaded not called on each tile, but only on tiles with new data! Verify we share the main cache
const origCache = tile.getCache(tile.originalCacheKey); const origCache = tile.getCache(tile.originalCacheKey);
for (let t of origCache._tiles) { for (let t of origCache._tiles) {
@ -2218,11 +2197,18 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
// add reference also to the main cache, no matter what the other tile state has // add reference also to the main cache, no matter what the other tile state has
// completion of the invaldate event should take care of all such tiles // completion of the invaldate event should take care of all such tiles
const targetMainCache = t.getCache(); const targetMainCache = t.getCache();
tile.addCache(t.cacheKey, () => { tile.setCache(t.cacheKey, targetMainCache, true, false);
$.console.error("Attempt to share main cache with existing tile should not trigger data getter!");
return targetMainCache.data;
}, targetMainCache.type, true, false);
break; break;
} else if (t.processing) {
console.log("ENCOUNTERED LOADING TILE!!!");
let internval = setInterval(() => {
if (t.processing) {
clearInterval(internval);
console.log("FINISHED!!!!!");
markTileAsReady();
}
}, 500);
return;
} }
} }
markTileAsReady(); markTileAsReady();

View File

@ -1135,7 +1135,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
ajaxHeaders = {}; ajaxHeaders = {};
} }
if (!$.isPlainObject(ajaxHeaders)) { if (!$.isPlainObject(ajaxHeaders)) {
console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); $.console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
return; return;
} }
if (propagate === undefined) { if (propagate === undefined) {

View File

@ -276,7 +276,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
// We allow re-execution on tiles that are in process but have too low processing timestamp, // We allow re-execution on tiles that are in process but have too low processing timestamp,
// which must be solved by ensuring subsequent data calls in the suddenly outdated processing // which must be solved by ensuring subsequent data calls in the suddenly outdated processing
// pipeline take no effect. // pipeline take no effect.
if (!tile || (!_allowTileUnloaded && !tile.loaded) || tile.transforming) { if (!tile || (!_allowTileUnloaded && !tile.loaded)) {
continue; continue;
} }
const tileCache = tile.getCache(tile.originalCacheKey); const tileCache = tile.getCache(tile.originalCacheKey);
@ -308,20 +308,96 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
const drawerId = this.viewer.drawer.getId(); const drawerId = this.viewer.drawer.getId();
const jobList = tileList.map(tile => { const jobList = tileList.map(tile => {
if (restoreTiles) { const tiledImage = tile.tiledImage;
tile.restore(); const originalCache = tile.getCache(tile.originalCacheKey);
let workingCache = null;
const getWorkingCacheData = (type) => {
if (workingCache) {
return workingCache.getDataAs(type, false);
} }
const targetCopyKey = restoreTiles ? tile.originalCacheKey : tile.cacheKey;
const origCache = tile.getCache(targetCopyKey);
if (!origCache) {
$.console.error("[Tile::getData] There is no cache available for tile with key %s", targetCopyKey);
return $.Promise.reject();
}
// Here ensure type is defined, rquired by data callbacks
type = type || origCache.type;
workingCache = new $.CacheRecord().withTileReference(tile);
return origCache.getDataAs(type, true).then(data => {
workingCache.addTile(tile, data, type);
return workingCache.data;
});
};
const setWorkingCacheData = (value, type) => {
if (!workingCache) {
workingCache = new $.CacheRecord().withTileReference(tile);
workingCache.addTile(tile, value, type);
} else {
workingCache.setDataAs(value, type);
}
};
const atomicCacheSwap = () => {
if (workingCache) {
let newCacheKey = tile.buildDistinctMainCacheKey();
tiledImage._tileCache.injectCache({
tile: tile,
cache: workingCache,
targetKey: newCacheKey,
setAsMainCache: true,
tileAllowNotLoaded: false //todo what if called from load event?
});
} else if (restoreTiles) {
// If we requested restore, perform now
tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, tile.getCache(tile.originalCacheKey), true);
}
};
//todo docs
return eventTarget.raiseEventAwaiting('tile-invalidated', { return eventTarget.raiseEventAwaiting('tile-invalidated', {
tile: tile, tile: tile,
tiledImage: tile.tiledImage, tiledImage: tiledImage,
}, tile.getCache(tile.originalCacheKey)).then(cacheKey => { outdated: () => originalCache.__invStamp !== tStamp,
if (cacheKey.__invStamp === tStamp) { getData: getWorkingCacheData,
// asynchronous finisher setData: setWorkingCacheData,
tile.transforming = tStamp; resetData: () => {
return tile.updateRenderTargetWithDataTransform(drawerId, supportedFormats, keepInternalCacheCopy, tStamp).then(() => { workingCache.destroy();
cacheKey.__invStamp = null; workingCache = null;
}
}).then(_ => {
if (originalCache.__invStamp === tStamp) {
if (workingCache) {
return workingCache.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(c => {
if (c && originalCache.__invStamp === tStamp) {
atomicCacheSwap();
originalCache.__invStamp = null;
}
}); });
} }
// If we requested restore, perform now
if (restoreTiles) {
const freshOriginalCacheRef = tile.getCache(tile.originalCacheKey);
tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, freshOriginalCacheRef, true);
return freshOriginalCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then((c) => {
if (c && originalCache.__invStamp === tStamp) {
atomicCacheSwap();
originalCache.__invStamp = null;
}
});
}
const freshMainCacheRef = tile.getCache();
return freshMainCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(() => {
originalCache.__invStamp = null;
});
} else if (workingCache) {
workingCache.destroy();
workingCache = null;
}
return null; return null;
}).catch(e => { }).catch(e => {
$.console.error("Update routine error:", e); $.console.error("Update routine error:", e);
@ -332,7 +408,6 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
for (let tile of markedTiles) { for (let tile of markedTiles) {
tile.lastProcess = false; tile.lastProcess = false;
tile.processing = false; tile.processing = false;
tile.transforming = false;
} }
this.draw(); this.draw();
}); });

View File

@ -807,7 +807,6 @@ async function processTile(tile) {
console.log("Selected tile", tile); console.log("Selected tile", tile);
await Promise.all([ await Promise.all([
updateCanvas(document.getElementById("tile-original"), tile, tile.originalCacheKey), updateCanvas(document.getElementById("tile-original"), tile, tile.originalCacheKey),
updateCanvas(document.getElementById("tile-working"), tile, tile._wcKey),
updateCanvas(document.getElementById("tile-main"), tile, tile.cacheKey), updateCanvas(document.getElementById("tile-main"), tile, tile.cacheKey),
]); ]);
} }

View File

@ -73,7 +73,6 @@
<div style="display: flex"> <div style="display: flex">
<div id="tile-original"></div> <div id="tile-original"></div>
<div id="tile-working"></div>
<div id="tile-main"></div> <div id="tile-main"></div>
</div> </div>
</section> </section>

View File

@ -56,15 +56,15 @@
setOptions(this, options); setOptions(this, options);
async function applyFilters(e) { async function applyFilters(e) {
const tile = e.tile, const tiledImage = e.tiledImage,
tiledImage = e.tiledImage,
processors = getFiltersProcessors(self, tiledImage); processors = getFiltersProcessors(self, tiledImage);
if (processors.length === 0) { if (processors.length === 0) {
return; return;
} }
const contextCopy = await tile.getData('context2d'); const contextCopy = await e.getData('context2d');
if (!contextCopy) return;
if (contextCopy.canvas.width === 0) { if (contextCopy.canvas.width === 0) {
debugger; debugger;
@ -79,7 +79,7 @@
await processors[i](contextCopy); await processors[i](contextCopy);
} }
await tile.setData(contextCopy, 'context2d'); await e.setData(contextCopy, 'context2d');
} catch (e) { } catch (e) {
// pass, this is error caused by canvas being destroyed & replaced // pass, this is error caused by canvas being destroyed & replaced
} }

View File

@ -89,9 +89,9 @@
<textarea id="scriptInput" rows="25" cols="120" placeholder="" style="height: 470px"> <textarea id="scriptInput" rows="25" cols="120" placeholder="" style="height: 470px">
// window.pluginA must be defined! draw small gradient square // window.pluginA must be defined! draw small gradient square
window.pluginA = async function(e) { window.pluginA = async function(e) {
const tile = e.tile; const ctx = await e.getData('context2d');
const ctx = await tile.getData('context2d');
if (ctx) {
const gradient = ctx.createLinearGradient(0, 0, 50, 50); const gradient = ctx.createLinearGradient(0, 0, 50, 50);
gradient.addColorStop(0, 'blue'); gradient.addColorStop(0, 'blue');
gradient.addColorStop(0.5, 'green'); gradient.addColorStop(0.5, 'green');
@ -99,15 +99,18 @@ window.pluginA = async function(e) {
ctx.fillStyle = gradient; ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 50, 50); ctx.fillRect(0, 0, 50, 50);
await tile.setData(ctx, 'context2d'); await e.setData(ctx, 'context2d');
}
}; };
// window.pluginB must be defined! overlay with color opacity 40% // window.pluginB must be defined! overlay with color opacity 40%
window.pluginB = async function(e) { window.pluginB = async function(e) {
const tile = e.tile; const ctx = await e.getData('context2d');
const ctx = await tile.getData('context2d'), canvas = ctx.canvas; if (ctx) {
const canvas = ctx.canvas;
ctx.fillStyle = "rgba(156, 0, 26, 0.4)"; ctx.fillStyle = "rgba(156, 0, 26, 0.4)";
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
await tile.setData(ctx, 'context2d'); await tile.setData(ctx, 'context2d');
}
}; };
// higher number = earlier execution // higher number = earlier execution
window.orderPluginA = 1; window.orderPluginA = 1;

View File

@ -25,18 +25,19 @@
function getPluginCode(overlayColor = "rgba(0,0,255,0.5)") { function getPluginCode(overlayColor = "rgba(0,0,255,0.5)") {
return async function(e) { return async function(e) {
const tile = e.tile; const ctx = await e.getData('context2d');
const ctx = await tile.getData('context2d'), canvas = ctx.canvas; if (ctx) {
const canvas = ctx.canvas;
ctx.fillStyle = overlayColor; ctx.fillStyle = overlayColor;
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
await tile.setData(ctx, 'context2d'); await e.setData(ctx, 'context2d');
}
}; };
} }
function getResetTileDataCode() { function getResetTileDataCode() {
return async function(e) { return async function(e) {
const tile = e.tile; e.resetData();
tile.restore();
}; };
} }