Commit 2cb36e7c authored by yawano's avatar yawano Committed by Commit bot

Add cache size and number of prefetch tasks limitations to list thumbnail loader.

BUG=438050
TEST=out/Release/browser_tests --gtest_filter=FileManagerJsTest.ListThumbnailLoader

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

Cr-Commit-Position: refs/heads/master@{#314757}
parent 4d353255
...@@ -14,9 +14,8 @@ ...@@ -14,9 +14,8 @@
* The following list is a todo list for this class. This list will be deleted * The following list is a todo list for this class. This list will be deleted
* after all of them are implemented. * after all of them are implemented.
* * Done: Fetch thumbnails with range based priority control. * * Done: Fetch thumbnails with range based priority control.
* * Implement cache size limitation. * * Done: Implement cache size limitation.
* * Modest queueing for low priority thumbnail fetches (i.e. not to use up IO * * Done: Modest queueing for low priority thumbnail fetches.
* by low priority tasks).
* * Handle other event types of FileListModel, e.g. sort. * * Handle other event types of FileListModel, e.g. sort.
* * Change ThumbnailLoader to directly return dataUrl. * * Change ThumbnailLoader to directly return dataUrl.
* * Handle file types for which generic images are used. * * Handle file types for which generic images are used.
...@@ -66,12 +65,12 @@ function ListThumbnailLoader( ...@@ -66,12 +65,12 @@ function ListThumbnailLoader(
this.active_ = {}; this.active_ = {};
/** /**
* @type {Object<string, !Object>} * @type {LRUCache<!Object>}
* @private * @private
* *
* TODO(yawano) Add size limitation to the cache. * TODO(yawano): After ThumbnailData class is created, type this with it.
*/ */
this.cache_ = {}; this.cache_ = new LRUCache(ListThumbnailLoader.CACHE_SIZE);
/** /**
* @type {number} * @type {number}
...@@ -92,7 +91,7 @@ function ListThumbnailLoader( ...@@ -92,7 +91,7 @@ function ListThumbnailLoader(
*/ */
this.cursor_ = 0; this.cursor_ = 0;
// TODO(yawano) Handle other event types of FileListModel, e.g. sort. // TODO(yawano): Handle other event types of FileListModel, e.g. sort.
this.dataModel_.addEventListener('splice', this.onSplice_.bind(this)); this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
} }
...@@ -104,6 +103,19 @@ ListThumbnailLoader.prototype.__proto__ = cr.EventTarget.prototype; ...@@ -104,6 +103,19 @@ ListThumbnailLoader.prototype.__proto__ = cr.EventTarget.prototype;
*/ */
ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS = 5; ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS = 5;
/**
* Number of prefetch requests.
* @const {number}
*/
ListThumbnailLoader.NUM_OF_PREFETCH = 10;
/**
* Cache size. Cache size must be larger than sum of high priority range size
* and number of prefetch tasks.
* @const {number}
*/
ListThumbnailLoader.CACHE_SIZE = 100;
/** /**
* An event handler for splice event of data model. When list is changed, start * An event handler for splice event of data model. When list is changed, start
* to rescan items. * to rescan items.
...@@ -111,13 +123,6 @@ ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS = 5; ...@@ -111,13 +123,6 @@ ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS = 5;
* @param {!Event} event Event * @param {!Event} event Event
*/ */
ListThumbnailLoader.prototype.onSplice_ = function(event) { ListThumbnailLoader.prototype.onSplice_ = function(event) {
// Delete thumbnails of removed items from cache.
for (var i = 0; i < event.removed.length; i++) {
var removedItem = event.removed[i];
if (this.cache_[removedItem.toURL()])
delete this.cache_[removedItem.toURL()];
}
this.cursor_ = this.beginIndex_; this.cursor_ = this.beginIndex_;
this.continue_(); this.continue_();
} }
...@@ -146,7 +151,9 @@ ListThumbnailLoader.prototype.setHighPriorityRange = function( ...@@ -146,7 +151,9 @@ ListThumbnailLoader.prototype.setHighPriorityRange = function(
* @return {Object} If the thumbnail is not in cache, this returns null. * @return {Object} If the thumbnail is not in cache, this returns null.
*/ */
ListThumbnailLoader.prototype.getThumbnailFromCache = function(entry) { ListThumbnailLoader.prototype.getThumbnailFromCache = function(entry) {
return this.cache_[entry.toURL()] || null; // Since we want to evict cache based on high priority range, we use peek here
// instead of get.
return this.cache_.peek(entry.toURL()) || null;
} }
/** /**
...@@ -156,7 +163,8 @@ ListThumbnailLoader.prototype.continue_ = function() { ...@@ -156,7 +163,8 @@ ListThumbnailLoader.prototype.continue_ = function() {
// If tasks are running full or all items are scanned, do nothing. // If tasks are running full or all items are scanned, do nothing.
if (!(Object.keys(this.active_).length < if (!(Object.keys(this.active_).length <
ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS) || ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS) ||
!(this.cursor_ < this.dataModel_.length)) { !(this.cursor_ < this.dataModel_.length) ||
!(this.cursor_ < this.endIndex_ + ListThumbnailLoader.NUM_OF_PREFETCH)) {
return; return;
} }
...@@ -164,7 +172,7 @@ ListThumbnailLoader.prototype.continue_ = function() { ...@@ -164,7 +172,7 @@ ListThumbnailLoader.prototype.continue_ = function() {
// If the entry is a directory, already in cache or fetching, skip it. // If the entry is a directory, already in cache or fetching, skip it.
if (entry.isDirectory || if (entry.isDirectory ||
this.cache_[entry.toURL()] || this.cache_.get(entry.toURL()) ||
this.active_[entry.toURL()]) { this.active_[entry.toURL()]) {
this.continue_(); this.continue_();
return; return;
...@@ -188,7 +196,7 @@ ListThumbnailLoader.prototype.enqueue_ = function(entry) { ...@@ -188,7 +196,7 @@ ListThumbnailLoader.prototype.enqueue_ = function(entry) {
task.fetch().then(function(thumbnail) { task.fetch().then(function(thumbnail) {
delete this.active_[thumbnail.fileUrl]; delete this.active_[thumbnail.fileUrl];
this.cache_[thumbnail.fileUrl] = thumbnail; this.cache_.put(thumbnail.fileUrl, thumbnail);
this.dispatchThumbnailLoaded_(thumbnail); this.dispatchThumbnailLoaded_(thumbnail);
this.continue_(); this.continue_();
}.bind(this)); }.bind(this));
...@@ -200,7 +208,7 @@ ListThumbnailLoader.prototype.enqueue_ = function(entry) { ...@@ -200,7 +208,7 @@ ListThumbnailLoader.prototype.enqueue_ = function(entry) {
* @param {Object} thumbnail Thumbnail. * @param {Object} thumbnail Thumbnail.
*/ */
ListThumbnailLoader.prototype.dispatchThumbnailLoaded_ = function(thumbnail) { ListThumbnailLoader.prototype.dispatchThumbnailLoaded_ = function(thumbnail) {
// TODO(yawano) Create ThumbnailLoadedEvent class. // TODO(yawano): Create ThumbnailLoadedEvent class.
var event = new Event('thumbnailLoaded'); var event = new Event('thumbnailLoaded');
event.fileUrl = thumbnail.fileUrl; event.fileUrl = thumbnail.fileUrl;
event.dataUrl = thumbnail.dataUrl; event.dataUrl = thumbnail.dataUrl;
...@@ -230,7 +238,7 @@ ListThumbnailLoader.Task = function( ...@@ -230,7 +238,7 @@ ListThumbnailLoader.Task = function(
/** /**
* Fetches thumbnail. * Fetches thumbnail.
* TODO(yawano) Add error handling. * TODO(yawano): Add error handling.
* *
* @return {!Promise} A promise which is resolved when thumbnail is fetched. * @return {!Promise} A promise which is resolved when thumbnail is fetched.
*/ */
...@@ -239,7 +247,7 @@ ListThumbnailLoader.Task.prototype.fetch = function() { ...@@ -239,7 +247,7 @@ ListThumbnailLoader.Task.prototype.fetch = function() {
this.metadataCache_.getOne(this.entry_, this.metadataCache_.getOne(this.entry_,
'thumbnail|filesystem|external|media', 'thumbnail|filesystem|external|media',
function(metadata) { function(metadata) {
// TODO(yawano) Change ThumbnailLoader to directly return data url of // TODO(yawano): Change ThumbnailLoader to directly return data url of
// an image. // an image.
var box = this.document_.createElement('div'); var box = this.document_.createElement('div');
...@@ -251,7 +259,7 @@ ListThumbnailLoader.Task.prototype.fetch = function() { ...@@ -251,7 +259,7 @@ ListThumbnailLoader.Task.prototype.fetch = function() {
ThumbnailLoader.FillMode.FIT, ThumbnailLoader.FillMode.FIT,
ThumbnailLoader.OptimizationMode.DISCARD_DETACHED, ThumbnailLoader.OptimizationMode.DISCARD_DETACHED,
function(image, transform) { function(image, transform) {
// TODO(yawano) Transform an image if necessary. // TODO(yawano): Transform an image if necessary.
var canvas = this.document_.createElement('canvas'); var canvas = this.document_.createElement('canvas');
canvas.width = image.width; canvas.width = image.width;
canvas.height = image.height; canvas.height = image.height;
...@@ -259,7 +267,7 @@ ListThumbnailLoader.Task.prototype.fetch = function() { ...@@ -259,7 +267,7 @@ ListThumbnailLoader.Task.prototype.fetch = function() {
var context = canvas.getContext('2d'); var context = canvas.getContext('2d');
context.drawImage(image, 0, 0); context.drawImage(image, 0, 0);
// TODO(yawano) Create ThumbnailData class. // TODO(yawano): Create ThumbnailData class.
resolve({ resolve({
fileUrl: this.entry_.toURL(), fileUrl: this.entry_.toURL(),
dataUrl: canvas.toDataURL('image/jpeg', 0.5), dataUrl: canvas.toDataURL('image/jpeg', 0.5),
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
<script src="../../../../webui/resources/js/cr/event_target.js"></script> <script src="../../../../webui/resources/js/cr/event_target.js"></script>
<script src="../../../../webui/resources/js/cr/ui.js"></script> <script src="../../../../webui/resources/js/cr/ui.js"></script>
<script src="../../../../webui/resources/js/cr/ui/array_data_model.js"></script> <script src="../../../../webui/resources/js/cr/ui/array_data_model.js"></script>
<script src="../../common/js/lru_cache.js"></script>
<script src="../../common/js/mock_entry.js"></script> <script src="../../common/js/mock_entry.js"></script>
<script src="../../common/js/unittest_util.js"></script> <script src="../../common/js/unittest_util.js"></script>
<script src="directory_contents.js"></script> <script src="directory_contents.js"></script>
......
...@@ -52,9 +52,12 @@ var entry2 = new MockEntry(fileSystem, '/Test2.jpg'); ...@@ -52,9 +52,12 @@ var entry2 = new MockEntry(fileSystem, '/Test2.jpg');
var entry3 = new MockEntry(fileSystem, '/Test3.jpg'); var entry3 = new MockEntry(fileSystem, '/Test3.jpg');
var entry4 = new MockEntry(fileSystem, '/Test4.jpg'); var entry4 = new MockEntry(fileSystem, '/Test4.jpg');
var entry5 = new MockEntry(fileSystem, '/Test5.jpg'); var entry5 = new MockEntry(fileSystem, '/Test5.jpg');
var entry6 = new MockEntry(fileSystem, '/Test6.jpg');
function setUp() { function setUp() {
ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS = 2; ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS = 2;
ListThumbnailLoader.NUM_OF_PREFETCH = 1;
ListThumbnailLoader.CACHE_SIZE = 5;
MockThumbnailLoader.setTestImageDataUrl(generateSampleImageDataUrl(document)); MockThumbnailLoader.setTestImageDataUrl(generateSampleImageDataUrl(document));
getOneCallbacks = {}; getOneCallbacks = {};
...@@ -81,6 +84,14 @@ function resolveGetOneCallback(url) { ...@@ -81,6 +84,14 @@ function resolveGetOneCallback(url) {
delete getOneCallbacks[url]; delete getOneCallbacks[url];
} }
function areEntriesInCache(entries) {
for (var i = 0; i < entries.length; i++) {
if (null === listThumbnailLoader.getThumbnailFromCache(entries[i]))
return false;
}
return true;
}
/** /**
* Story test for list thumbnail loader. * Story test for list thumbnail loader.
*/ */
...@@ -142,14 +153,6 @@ function testStory(callback) { ...@@ -142,14 +153,6 @@ function testStory(callback) {
!!getOneCallbacks[entry4.toURL()] && !!getOneCallbacks[entry4.toURL()] &&
Object.keys(getOneCallbacks).length === 2; Object.keys(getOneCallbacks).length === 2;
}); });
}).then(function() {
// Cache is deleted when the item is removed from the list.
var result = fileListModel.splice(2, 1); // Remove Test2.jpg.
// Fail to fetch thumbnail from cache.
return waitUntil(function() {
return listThumbnailLoader.getThumbnailFromCache(entry2) === null;
});
}), callback); }), callback);
} }
...@@ -167,3 +170,54 @@ function testRangeIsAtTheEndOfList() { ...@@ -167,3 +170,54 @@ function testRangeIsAtTheEndOfList() {
assertEquals('filesystem:volume-id/Test5.jpg', assertEquals('filesystem:volume-id/Test5.jpg',
Object.keys(getOneCallbacks)[0]); Object.keys(getOneCallbacks)[0]);
} }
function testCache(callback) {
ListThumbnailLoader.NUM_OF_MAX_ACTIVE_TASKS = 5;
// Set high priority range to 0 - 2.
listThumbnailLoader.setHighPriorityRange(0, 2);
fileListModel.push(entry1, entry2, entry3, entry4, entry5, entry6);
resolveGetOneCallback(entry1.toURL());
// In this test case, entry 3 is resolved earlier than entry 2.
resolveGetOneCallback(entry3.toURL());
resolveGetOneCallback(entry2.toURL());
assertEquals(0, Object.keys(getOneCallbacks).length);
reportPromise(waitUntil(function() {
return areEntriesInCache([entry3, entry2, entry1]);
}).then(function() {
// Move high priority range to 1 - 3.
listThumbnailLoader.setHighPriorityRange(1, 3);
resolveGetOneCallback(entry4.toURL());
assertEquals(0, Object.keys(getOneCallbacks).length);
return waitUntil(function() {
return areEntriesInCache([entry4, entry3, entry2, entry1]);
});
}).then(function() {
// Move high priority range to 4 - 6.
listThumbnailLoader.setHighPriorityRange(4, 6);
resolveGetOneCallback(entry5.toURL());
resolveGetOneCallback(entry6.toURL());
assertEquals(0, Object.keys(getOneCallbacks).length);
return waitUntil(function() {
return areEntriesInCache([entry6, entry5, entry4, entry3, entry2]);
});
}).then(function() {
// Move high priority range to 3 - 5.
listThumbnailLoader.setHighPriorityRange(3, 5);
assertEquals(0, Object.keys(getOneCallbacks).length);
assertTrue(areEntriesInCache([entry6, entry5, entry4, entry3, entry2]));
// Move high priority range to 0 - 2.
listThumbnailLoader.setHighPriorityRange(0, 2);
resolveGetOneCallback(entry1.toURL());
assertEquals(0, Object.keys(getOneCallbacks).length);
return waitUntil(function() {
return areEntriesInCache([entry3, entry2, entry1, entry6, entry5]);
});
}), callback);
}
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