Commit 07b3d32a authored by hirono@chromium.org's avatar hirono@chromium.org

Previously the caches are stored in the UI class.

The CL moves the caches in Gallery items to:

 * Simplify UI class.
 * Share the cache different UI classes.
 * Move loading logic into the items in future patches.

BUG=391643
TEST=manually

Review URL: https://codereview.chromium.org/420743002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@285890 0039d316-1c4b-4281-b951-d872f2087c98
parent 61641f5a
......@@ -834,7 +834,7 @@ util.AppCache.cleanup_ = function(map) {
if (map.hasOwnProperty(key))
keys.push(key);
}
keys.sort(function(a, b) { return map[a].expire > map[b].expire });
keys.sort(function(a, b) { return map[a].expire > map[b].expire; });
var cutoff = Date.now();
......
......@@ -28,6 +28,22 @@ function GalleryDataModel() {
this.metadataCache_ = null;
}
/**
* Maximum number of full size image cache.
* @type {number}
* @const
* @private
*/
GalleryDataModel.MAX_FULL_IMAGE_CACHE_ = 3;
/**
* Maximum number of screen size image cache.
* @type {number}
* @const
* @private
*/
GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_ = 5;
GalleryDataModel.prototype = {
__proto__: cr.ui.ArrayDataModel.prototype
};
......@@ -133,6 +149,47 @@ GalleryDataModel.prototype.saveItem = function(item, canvas, overwrite) {
}.bind(this));
};
/**
* Evicts image caches in the items.
* @param {Gallery.Item} currentSelectedItem Current selected item.
*/
GalleryDataModel.prototype.evictCache = function(currentSelectedItem) {
// Sort the item by the last accessed date.
var sorted = this.slice().sort(function(a, b) {
return b.getLastAccessedDate() - a.getLastAccessedDate();
});
// Evict caches.
var contentCacheCount = 0;
var screenCacheCount = 0;
for (var i = 0; i < sorted.length; i++) {
if (sorted[i].contentImage) {
if (++contentCacheCount > GalleryDataModel.MAX_FULL_IMAGE_CACHE_) {
if (sorted[i].contentImage.parentNode) {
console.error('The content image has a parent node.');
} else {
// Force to free the buffer of the canvas by assinng zero size.
sorted[i].contentImage.width = 0;
sorted[i].contentImage.height = 0;
sorted[i].contentImage = null;
}
}
}
if (sorted[i].screenImage) {
if (++screenCacheCount > GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_) {
if (sorted[i].screenImage.parentNode) {
console.error('The screen image has a parent node.');
} else {
// Force to free the buffer of the canvas by assinng zero size.
sorted[i].screenImage.width = 0;
sorted[i].screenImage.height = 0;
sorted[i].screenImage = null;
}
}
}
}
};
/**
* Gallery for viewing and editing image files.
*
......@@ -646,6 +703,10 @@ Gallery.prototype.getSingleSelectedItem = function() {
Gallery.prototype.onSelection_ = function() {
this.updateSelectionAndState_();
this.updateShareMenu_();
var currentItem = this.getSelectedItems()[0];
if (currentItem)
currentItem.touch();
this.dataModel_.evictCache();
};
/**
......
......@@ -27,9 +27,33 @@ Gallery.Item = function(entry, metadata, metadataCache, original) {
/**
* @type {MetadataCache}
* @private
*/
this.metadataCache_ = metadataCache;
/**
* The content cache is used for prefetching the next image when going through
* the images sequentially. The real life photos can be large (18Mpix = 72Mb
* pixel array) so we want only the minimum amount of caching.
* @type {Canvas}
*/
this.screenImage = null;
/**
* We reuse previously generated screen-scale images so that going back to a
* recently loaded image looks instant even if the image is not in the content
* cache any more. Screen-scale images are small (~1Mpix) so we can afford to
* cache more of them.
* @type {Canvas}
*/
this.contentImage = null;
/**
* Last accessed date to be used for selecting items whose cache are evicted.
* @type {number}
*/
this.lastAccessed_ = Date.now();
/**
* @type {boolean}
* @private
......@@ -90,6 +114,22 @@ Gallery.Item.prototype.getFileName = function() {
*/
Gallery.Item.prototype.isOriginal = function() { return this.original_; };
/**
* Obtains the last accessed date.
* @return {number} Last accessed date.
*/
Gallery.Item.prototype.getLastAccessedDate = function() {
return this.lastAccessed_;
};
/**
* Updates the last accessed date.
*/
Gallery.Item.prototype.touch = function() {
this.lastAccessed_ = Date.now();
};
// TODO: Localize?
/**
* @type {string} Suffix for a edited copy file name.
......
......@@ -26,16 +26,6 @@ function ImageView(container, viewport) {
// when the selection changes.
this.prefetchLoader_ = new ImageUtil.ImageLoader(this.document_);
// The content cache is used for prefetching the next image when going
// through the images sequentially. The real life photos can be large
// (18Mpix = 72Mb pixel array) so we want only the minimum amount of caching.
this.contentCache_ = new ImageView.Cache(2);
// We reuse previously generated screen-scale images so that going back to
// a recently loaded image looks instant even if the image is not in
// the content cache any more. Screen-scale images are small (~1Mpix)
// so we can afford to cache more of them.
this.screenCache_ = new ImageView.Cache(5);
this.contentCallbacks_ = [];
/**
......@@ -284,19 +274,15 @@ ImageView.prototype.load =
var self = this;
this.contentEntry_ = entry;
this.contentItem_ = item;
this.contentRevision_ = -1;
// Cache has to be evicted in advance, so the returned cached image is not
// evicted later by the prefetched image.
this.contentCache_.evictLRU();
var cached = this.contentCache_.getItem(this.contentEntry_);
var cached = item.contentImage;
if (cached) {
displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
false /* no preview */, cached);
} else {
var cachedScreen = this.screenCache_.getItem(this.contentEntry_);
var cachedScreen = item.screenImage;
var imageWidth = metadata.media && metadata.media.width ||
metadata.drive && metadata.drive.imageWidth;
var imageHeight = metadata.media && metadata.media.height ||
......@@ -409,33 +395,12 @@ ImageView.prototype.load =
* @param {number} delay Image load delay in ms.
*/
ImageView.prototype.prefetch = function(item, delay) {
var self = this;
var entry = item.getEntry();
function prefetchDone(canvas) {
if (canvas.width)
self.contentCache_.putItem(entry, canvas);
}
var cached = this.contentCache_.getItem(entry);
if (cached) {
prefetchDone(cached);
} else if (FileType.getMediaType(entry) === 'image') {
// Evict the LRU item before we allocate the new canvas to avoid unneeded
// strain on memory.
this.contentCache_.evictLRU();
this.prefetchLoader_.load(item, prefetchDone, delay);
}
};
/**
* Renames the current image.
* @param {FileEntry} newEntry The new image Entry.
*/
ImageView.prototype.changeEntry = function(newEntry) {
this.contentCache_.renameItem(this.contentEntry_, newEntry);
this.screenCache_.renameItem(this.contentEntry_, newEntry);
this.contentEntry_ = newEntry;
if (item.contentImage)
return;
this.prefetchLoader_.load(item, function(canvas) {
if (canvas.width && canvas.height && !item.contentImage)
item.contentImage = canvas;
}, delay);
};
/**
......@@ -495,8 +460,8 @@ ImageView.prototype.replaceContent_ = function(
this.container_.appendChild(this.contentCanvas_);
this.contentCanvas_.classList.add('fullres');
this.contentCache_.putItem(this.contentEntry_, this.contentCanvas_, true);
this.screenCache_.putItem(this.contentEntry_, this.screenImage_);
this.contentItem_.contentImage = this.contentCanvas_;
this.contentItem_.screenImage = this.screenImage_;
// TODO(kaznacheev): It is better to pass screenImage_ as it is usually
// much smaller than contentCanvas_ and still contains the entire image.
......@@ -688,103 +653,6 @@ ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
return effect.getSafeInterval();
};
/**
* Generic cache with a limited capacity and LRU eviction.
* @param {number} capacity Maximum number of cached item.
* @constructor
*/
ImageView.Cache = function(capacity) {
this.capacity_ = capacity;
this.map_ = {};
this.order_ = [];
};
/**
* Fetches the item from the cache.
* @param {FileEntry} entry The entry.
* @return {Object} The cached item.
*/
ImageView.Cache.prototype.getItem = function(entry) {
return this.map_[entry.toURL()];
};
/**
* Puts the item into the cache.
*
* @param {FileEntry} entry The entry.
* @param {Object} item The item object.
* @param {boolean=} opt_keepLRU True if the LRU order should not be modified.
*/
ImageView.Cache.prototype.putItem = function(entry, item, opt_keepLRU) {
var pos = this.order_.indexOf(entry.toURL());
if ((pos >= 0) !== (entry.toURL() in this.map_))
throw new Error('Inconsistent cache state');
if (entry.toURL() in this.map_) {
if (!opt_keepLRU) {
// Move to the end (most recently used).
this.order_.splice(pos, 1);
this.order_.push(entry.toURL());
}
} else {
this.evictLRU();
this.order_.push(entry.toURL());
}
if ((pos >= 0) && (item !== this.map_[entry.toURL()]))
this.deleteItem_(this.map_[entry.toURL()]);
this.map_[entry.toURL()] = item;
if (this.order_.length > this.capacity_)
throw new Error('Exceeded cache capacity');
};
/**
* Evicts the least recently used items.
*/
ImageView.Cache.prototype.evictLRU = function() {
if (this.order_.length === this.capacity_) {
var url = this.order_.shift();
this.deleteItem_(this.map_[url]);
delete this.map_[url];
}
};
/**
* Changes the Entry.
* @param {FileEntry} oldEntry The old Entry.
* @param {FileEntry} newEntry The new Entry.
*/
ImageView.Cache.prototype.renameItem = function(oldEntry, newEntry) {
if (util.isSameEntry(oldEntry, newEntry))
return; // No need to rename.
var pos = this.order_.indexOf(oldEntry.toURL());
if (pos < 0)
return; // Not cached.
this.order_[pos] = newEntry.toURL();
this.map_[newEntry.toURL()] = this.map_[oldEntry.toURL()];
delete this.map_[oldEntry.toURL()];
};
/**
* Disposes an object.
*
* @param {Object} item The item object.
* @private
*/
ImageView.Cache.prototype.deleteItem_ = function(item) {
// Trick to reduce memory usage without waiting for gc.
if (item instanceof HTMLCanvasElement) {
// If the canvas is being used somewhere else (eg. displayed on the screen),
// it will be cleared.
item.width = 0;
item.height = 0;
}
};
/* Transition effects */
/**
......
......@@ -37,7 +37,6 @@ function SlideMode(container, content, toolbar, prompt,
this.onSelectionBound_ = this.onSelection_.bind(this);
this.onSpliceBound_ = this.onSplice_.bind(this);
this.onContentBound_ = this.onContentChange_.bind(this);
// Unique numeric key, incremented per each load attempt used to discard
// old attempts. This can happen especially when changing selection fast or
......@@ -256,7 +255,6 @@ SlideMode.prototype.enter = function(
this.selectionModel_.addEventListener('change', this.onSelectionBound_);
this.dataModel_.addEventListener('splice', this.onSpliceBound_);
this.dataModel_.addEventListener('content', this.onContentBound_);
ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
this.ribbon_.enable();
......@@ -314,7 +312,6 @@ SlideMode.prototype.enter = function(
// Register handlers.
this.selectionModel_.addEventListener('change', this.onSelectionBound_);
this.dataModel_.addEventListener('splice', this.onSpliceBound_);
this.dataModel_.addEventListener('content', this.onContentBound_);
this.touchHandlers_.enabled = true;
// Wait 1000ms after the animation is done, then prefetch the next image.
......@@ -342,7 +339,6 @@ SlideMode.prototype.leave = function(zoomToRect, callback) {
this.selectionModel_.removeEventListener(
'change', this.onSelectionBound_);
this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
this.dataModel_.removeEventListener('content', this.onContentBound_);
this.ribbon_.disable();
this.active_ = false;
if (this.savedSelection_)
......@@ -995,17 +991,6 @@ SlideMode.prototype.saveCurrentImage_ = function(callback) {
});
};
/**
* Update caches when the selected item has been renamed.
* @param {Event} event Event.
* @private
*/
SlideMode.prototype.onContentChange_ = function(event) {
var newEntry = event.item.getEntry();
if (!util.isSameEntry(newEntry, event.oldEntry))
this.imageView_.changeEntry(newEntry);
};
/**
* Flash 'Saved' label briefly to indicate that the image has been saved.
* @private
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment