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) { ...@@ -834,7 +834,7 @@ util.AppCache.cleanup_ = function(map) {
if (map.hasOwnProperty(key)) if (map.hasOwnProperty(key))
keys.push(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(); var cutoff = Date.now();
......
...@@ -28,6 +28,22 @@ function GalleryDataModel() { ...@@ -28,6 +28,22 @@ function GalleryDataModel() {
this.metadataCache_ = null; 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 = { GalleryDataModel.prototype = {
__proto__: cr.ui.ArrayDataModel.prototype __proto__: cr.ui.ArrayDataModel.prototype
}; };
...@@ -133,6 +149,47 @@ GalleryDataModel.prototype.saveItem = function(item, canvas, overwrite) { ...@@ -133,6 +149,47 @@ GalleryDataModel.prototype.saveItem = function(item, canvas, overwrite) {
}.bind(this)); }.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. * Gallery for viewing and editing image files.
* *
...@@ -646,6 +703,10 @@ Gallery.prototype.getSingleSelectedItem = function() { ...@@ -646,6 +703,10 @@ Gallery.prototype.getSingleSelectedItem = function() {
Gallery.prototype.onSelection_ = function() { Gallery.prototype.onSelection_ = function() {
this.updateSelectionAndState_(); this.updateSelectionAndState_();
this.updateShareMenu_(); 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) { ...@@ -27,9 +27,33 @@ Gallery.Item = function(entry, metadata, metadataCache, original) {
/** /**
* @type {MetadataCache} * @type {MetadataCache}
* @private
*/ */
this.metadataCache_ = metadataCache; 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} * @type {boolean}
* @private * @private
...@@ -90,6 +114,22 @@ Gallery.Item.prototype.getFileName = function() { ...@@ -90,6 +114,22 @@ Gallery.Item.prototype.getFileName = function() {
*/ */
Gallery.Item.prototype.isOriginal = function() { return this.original_; }; 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? // TODO: Localize?
/** /**
* @type {string} Suffix for a edited copy file name. * @type {string} Suffix for a edited copy file name.
......
...@@ -26,16 +26,6 @@ function ImageView(container, viewport) { ...@@ -26,16 +26,6 @@ function ImageView(container, viewport) {
// when the selection changes. // when the selection changes.
this.prefetchLoader_ = new ImageUtil.ImageLoader(this.document_); 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_ = []; this.contentCallbacks_ = [];
/** /**
...@@ -284,19 +274,15 @@ ImageView.prototype.load = ...@@ -284,19 +274,15 @@ ImageView.prototype.load =
var self = this; var self = this;
this.contentEntry_ = entry; this.contentItem_ = item;
this.contentRevision_ = -1; this.contentRevision_ = -1;
// Cache has to be evicted in advance, so the returned cached image is not var cached = item.contentImage;
// evicted later by the prefetched image.
this.contentCache_.evictLRU();
var cached = this.contentCache_.getItem(this.contentEntry_);
if (cached) { if (cached) {
displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL, displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
false /* no preview */, cached); false /* no preview */, cached);
} else { } else {
var cachedScreen = this.screenCache_.getItem(this.contentEntry_); var cachedScreen = item.screenImage;
var imageWidth = metadata.media && metadata.media.width || var imageWidth = metadata.media && metadata.media.width ||
metadata.drive && metadata.drive.imageWidth; metadata.drive && metadata.drive.imageWidth;
var imageHeight = metadata.media && metadata.media.height || var imageHeight = metadata.media && metadata.media.height ||
...@@ -409,33 +395,12 @@ ImageView.prototype.load = ...@@ -409,33 +395,12 @@ ImageView.prototype.load =
* @param {number} delay Image load delay in ms. * @param {number} delay Image load delay in ms.
*/ */
ImageView.prototype.prefetch = function(item, delay) { ImageView.prototype.prefetch = function(item, delay) {
var self = this; if (item.contentImage)
var entry = item.getEntry(); return;
function prefetchDone(canvas) { this.prefetchLoader_.load(item, function(canvas) {
if (canvas.width) if (canvas.width && canvas.height && !item.contentImage)
self.contentCache_.putItem(entry, canvas); item.contentImage = canvas;
} }, delay);
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;
}; };
/** /**
...@@ -495,8 +460,8 @@ ImageView.prototype.replaceContent_ = function( ...@@ -495,8 +460,8 @@ ImageView.prototype.replaceContent_ = function(
this.container_.appendChild(this.contentCanvas_); this.container_.appendChild(this.contentCanvas_);
this.contentCanvas_.classList.add('fullres'); this.contentCanvas_.classList.add('fullres');
this.contentCache_.putItem(this.contentEntry_, this.contentCanvas_, true); this.contentItem_.contentImage = this.contentCanvas_;
this.screenCache_.putItem(this.contentEntry_, this.screenImage_); this.contentItem_.screenImage = this.screenImage_;
// TODO(kaznacheev): It is better to pass screenImage_ as it is usually // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
// much smaller than contentCanvas_ and still contains the entire image. // much smaller than contentCanvas_ and still contains the entire image.
...@@ -688,103 +653,6 @@ ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) { ...@@ -688,103 +653,6 @@ ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
return effect.getSafeInterval(); 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 */ /* Transition effects */
/** /**
......
...@@ -37,7 +37,6 @@ function SlideMode(container, content, toolbar, prompt, ...@@ -37,7 +37,6 @@ function SlideMode(container, content, toolbar, prompt,
this.onSelectionBound_ = this.onSelection_.bind(this); this.onSelectionBound_ = this.onSelection_.bind(this);
this.onSpliceBound_ = this.onSplice_.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 // Unique numeric key, incremented per each load attempt used to discard
// old attempts. This can happen especially when changing selection fast or // old attempts. This can happen especially when changing selection fast or
...@@ -256,7 +255,6 @@ SlideMode.prototype.enter = function( ...@@ -256,7 +255,6 @@ SlideMode.prototype.enter = function(
this.selectionModel_.addEventListener('change', this.onSelectionBound_); this.selectionModel_.addEventListener('change', this.onSelectionBound_);
this.dataModel_.addEventListener('splice', this.onSpliceBound_); this.dataModel_.addEventListener('splice', this.onSpliceBound_);
this.dataModel_.addEventListener('content', this.onContentBound_);
ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1); ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
this.ribbon_.enable(); this.ribbon_.enable();
...@@ -314,7 +312,6 @@ SlideMode.prototype.enter = function( ...@@ -314,7 +312,6 @@ SlideMode.prototype.enter = function(
// Register handlers. // Register handlers.
this.selectionModel_.addEventListener('change', this.onSelectionBound_); this.selectionModel_.addEventListener('change', this.onSelectionBound_);
this.dataModel_.addEventListener('splice', this.onSpliceBound_); this.dataModel_.addEventListener('splice', this.onSpliceBound_);
this.dataModel_.addEventListener('content', this.onContentBound_);
this.touchHandlers_.enabled = true; this.touchHandlers_.enabled = true;
// Wait 1000ms after the animation is done, then prefetch the next image. // Wait 1000ms after the animation is done, then prefetch the next image.
...@@ -342,7 +339,6 @@ SlideMode.prototype.leave = function(zoomToRect, callback) { ...@@ -342,7 +339,6 @@ SlideMode.prototype.leave = function(zoomToRect, callback) {
this.selectionModel_.removeEventListener( this.selectionModel_.removeEventListener(
'change', this.onSelectionBound_); 'change', this.onSelectionBound_);
this.dataModel_.removeEventListener('splice', this.onSpliceBound_); this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
this.dataModel_.removeEventListener('content', this.onContentBound_);
this.ribbon_.disable(); this.ribbon_.disable();
this.active_ = false; this.active_ = false;
if (this.savedSelection_) if (this.savedSelection_)
...@@ -995,17 +991,6 @@ SlideMode.prototype.saveCurrentImage_ = function(callback) { ...@@ -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. * Flash 'Saved' label briefly to indicate that the image has been saved.
* @private * @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