diff options
author | mtomasz@chromium.org <mtomasz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-21 07:23:47 +0000 |
---|---|---|
committer | mtomasz@chromium.org <mtomasz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-21 07:23:47 +0000 |
commit | 8937251d3fd50990d8879fbba0766d744d057c77 (patch) | |
tree | b7a86cb93c90292b391f82a30d25b5bbbae5098a | |
parent | fb9c50ddca2aee9b52e3bf91b742eeb5a51b2772 (diff) | |
download | chromium_src-8937251d3fd50990d8879fbba0766d744d057c77.zip chromium_src-8937251d3fd50990d8879fbba0766d744d057c77.tar.gz chromium_src-8937251d3fd50990d8879fbba0766d744d057c77.tar.bz2 |
Introduce a priority queue to the image loader.
Image loader used to start loading all requests at once, which caused long loading times in some scenarios. Eg. while images in the mosaic view are being loaded in the background, switching to the next image in the slide view is slow. It may happen that there are hundreds of pending images (for mosaic view preload) which have to be handled before the one is important (slide view).
To fix this issue (1) priorities were introduces, (2) tasks are queued, (3) number of tasks performed in parallel is limited to 5.
In practice, it gives a guarantee that there will be at most 5 tasks with lower priority executed before a task with higher priority. Taking into account an average resizing time, which is around 200ms, this gives a guarantee that on average, the image with the highest priority will be handled in the worst case with 1 second of delay.
Along the way, the selected images in the mosaic view have higher priority than other tiles. Also, background thumbnail generation for copying images has now very low priority to minimize load of important images.
TEST=Tested manually.
BUG=239237
Review URL: https://chromiumcodereview.appspot.com/14623021
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@201245 0039d316-1c4b-4281-b951-d872f2087c98
6 files changed, 176 insertions, 20 deletions
diff --git a/chrome/browser/resources/file_manager/js/file_manager.js b/chrome/browser/resources/file_manager/js/file_manager.js index ab8239e..700d4e0 100644 --- a/chrome/browser/resources/file_manager/js/file_manager.js +++ b/chrome/browser/resources/file_manager/js/file_manager.js @@ -1392,7 +1392,8 @@ DialogType.isModal = function(type) { undefined, // Media type. FileType.isOnDrive(url) ? ThumbnailLoader.UseEmbedded.USE_EMBEDDED : - ThumbnailLoader.UseEmbedded.NO_EMBEDDED); + ThumbnailLoader.UseEmbedded.NO_EMBEDDED, + 10); // Very low priority. thumbnailLoader_.loadDetachedImage(function(success) {}); }); } diff --git a/chrome/browser/resources/file_manager/js/media/media_util.js b/chrome/browser/resources/file_manager/js/media/media_util.js index d039f1a3..db09006 100644 --- a/chrome/browser/resources/file_manager/js/media/media_util.js +++ b/chrome/browser/resources/file_manager/js/media/media_util.js @@ -16,15 +16,17 @@ * @param {string=} opt_mediaType Media type. * @param {ThumbnailLoader.UseEmbedded=} opt_useEmbedded If to use embedded * jpeg thumbnail if available. Default: USE_EMBEDDED. + * @param {number=} opt_priority Priority, the highest is 0. default: 2. * @constructor */ -function ThumbnailLoader( - url, opt_loaderType, opt_metadata, opt_mediaType, opt_useEmbedded) { +function ThumbnailLoader(url, opt_loaderType, opt_metadata, opt_mediaType, + opt_useEmbedded, opt_priority) { opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED; this.mediaType_ = opt_mediaType || FileType.getMediaType(url); this.loaderType_ = opt_loaderType || ThumbnailLoader.LoaderType.IMAGE; this.metadata_ = opt_metadata; + this.priority_ = (opt_priority !== undefined) ? opt_priority : 2; if (!opt_metadata) { this.thumbnailUrl_ = url; // Use the URL directly. @@ -155,8 +157,10 @@ ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, if (this.fallbackUrl_) { new ThumbnailLoader(this.fallbackUrl_, this.loaderType_, - null, - this.mediaType_). + null, // No metadata. + this.mediaType_, + undefined, // Default value for use-embedded. + this.priority_). load(box, fillMode, opt_onSuccess); } else { box.setAttribute('generic-thumbnail', this.mediaType_); @@ -180,6 +184,7 @@ ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, cache: true, + priority: this.priority_, timestamp: modificationTime }, function() { if (opt_optimizationMode == @@ -261,6 +266,7 @@ ThumbnailLoader.prototype.loadDetachedImage = function(callback) { { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, cache: true, + priority: this.priority_, timestamp: modificationTime }); }; diff --git a/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js b/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js index 25303d7..0d3d85d 100644 --- a/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js +++ b/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js @@ -1715,6 +1715,9 @@ Mosaic.Tile.prototype.init = function(metadata, onImageMeasured) { this.markUnloaded(); this.left_ = null; // Mark as not laid out. + // Set higher priority for the selected elements to load them first. + var priority = this.getAttribute('selected') ? 1 : 2; + // Use embedded thumbnails on Drive, since they have higher resolution. this.thumbnailLoader_ = new ThumbnailLoader( this.getItem().getUrl(), @@ -1723,7 +1726,8 @@ Mosaic.Tile.prototype.init = function(metadata, onImageMeasured) { undefined, // Media type. FileType.isOnDrive(this.getItem().getUrl()) ? ThumbnailLoader.UseEmbedded.USE_EMBEDDED : - ThumbnailLoader.UseEmbedded.NO_EMBEDDED); + ThumbnailLoader.UseEmbedded.NO_EMBEDDED, + priority); var setDimensions = function(width, height) { if (width > height) { diff --git a/chrome/browser/resources/image_loader/OWNERS b/chrome/browser/resources/image_loader/OWNERS new file mode 100644 index 0000000..4eab6e4 --- /dev/null +++ b/chrome/browser/resources/image_loader/OWNERS @@ -0,0 +1 @@ +mtomasz@chromium.org diff --git a/chrome/browser/resources/image_loader/client.js b/chrome/browser/resources/image_loader/client.js index 5a1e3c1..8d80ec7 100644 --- a/chrome/browser/resources/image_loader/client.js +++ b/chrome/browser/resources/image_loader/client.js @@ -269,7 +269,6 @@ ImageLoader.Client.Cache.prototype.evictCache_ = function(size) { */ ImageLoader.Client.Cache.prototype.saveImage = function( key, data, opt_timestamp) { - // If the image is currently in cache, then remove it. if (this.images_[key]) this.removeImage(key); diff --git a/chrome/browser/resources/image_loader/image_loader.js b/chrome/browser/resources/image_loader/image_loader.js index 1502d92..0758d24 100644 --- a/chrome/browser/resources/image_loader/image_loader.js +++ b/chrome/browser/resources/image_loader/image_loader.js @@ -21,6 +21,13 @@ var ImageLoader = function() { */ this.cache_ = new ImageLoader.Cache(); + /** + * Manages pending requests and runs them in order of priorities. + * @type {ImageLoader.Worker} + * @private + */ + this.worker_ = new ImageLoader.Worker(); + chrome.fileBrowserPrivate.requestLocalFileSystem(function(filesystem) { // TODO(mtomasz): Handle. }); @@ -63,9 +70,10 @@ ImageLoader.prototype.onMessage_ = function(senderId, request, callback) { } return false; // No callback calls. } else { - // Start a task. - this.requests_[requestId] = - new ImageLoader.Request(this.cache_, request, callback); + // Create a request task and add it to the worker (queue). + var requestTask = new ImageLoader.Request(this.cache_, request, callback); + this.requests_[requestId] = requestTask; + this.worker_.add(requestTask); return true; // Request will call the callback. } }; @@ -230,17 +238,56 @@ ImageLoader.Request = function(cache, request, callback) { */ this.context_ = this.canvas_.getContext('2d'); - // Process the request. Try to load from cache. If it fails, then download. + /** + * Callback to be called once downloading is finished. + * @type {function()} + * @private + */ + this.downloadCallback_ = null; +}; + +/** + * Returns priority of the request. The higher priority, the faster it will + * be handled. The highest priority is 0. The default one is 2. + * + * @return {number} Priority. + */ +ImageLoader.Request.prototype.getPriority = function() { + return (this.request_.priority !== undefined) ? this.request_.priority : 2; +}; + +/** + * Tries to load the image from cache if exists and sends the response. + * + * @param {function()} onSuccess Success callback. + * @param {function()} onFailure Failure callback. + */ +ImageLoader.Request.prototype.loadFromCacheAndProcess = function( + onSuccess, onFailure) { this.loadFromCache_( - this.sendImageData_.bind(this), - function() { // Failure, not in cache. - this.downloadOriginal_(this.onImageLoad_.bind(this), - this.onImageError_.bind(this)); - }.bind(this)); + function(data) { // Found in cache. + this.sendImageData_(data); + onSuccess(); + }.bind(this), + onFailure); // Not found in cache. +}; + +/** + * Tries to download the image, resizes and sends the response. + * @param {function()} callback Completion callback. + */ +ImageLoader.Request.prototype.downloadAndProcess = function(callback) { + if (this.downloadCallback_) + throw new Error('Downloading already started.'); + + this.downloadCallback_ = callback; + this.downloadOriginal_(this.onImageLoad_.bind(this), + this.onImageError_.bind(this)); }; /** * Fetches the image from the persistent cache. + * * @param {function()} onSuccess Success callback. * @param {function()} onFailure Failure callback. * @private @@ -270,6 +317,7 @@ ImageLoader.Request.prototype.loadFromCache_ = function(onSuccess, onFailure) { /** * Saves the image to the persistent cache. + * * @param {string} data The image's data. * @private */ @@ -287,6 +335,7 @@ ImageLoader.Request.prototype.saveToCache_ = function(data) { /** * Downloads an image directly or for remote resources using the XmlHttpRequest. + * * @param {function()} onSuccess Success callback. * @param {function()} onFailure Failure callback. * @private @@ -296,7 +345,7 @@ ImageLoader.Request.prototype.downloadOriginal_ = function( this.image_.onload = onSuccess; this.image_.onerror = onFailure; - if (window.harness || !this.request_.url.match(/^https?:/)) { + if (!this.request_.url.match(/^https?:/)) { // Download directly. this.image_.src = this.request_.url; return; @@ -363,24 +412,28 @@ ImageLoader.Request.prototype.sendImageData_ = function(data) { * Handler, when contents are loaded into the image element. Performs resizing * and finalizes the request process. * + * @param {function()} callback Completion callback. * @private */ -ImageLoader.Request.prototype.onImageLoad_ = function() { +ImageLoader.Request.prototype.onImageLoad_ = function(callback) { ImageLoader.resize(this.image_, this.canvas_, this.request_); this.sendImage_(); this.cleanup_(); + this.downloadCallback_(); }; /** * Handler, when loading of the image fails. Sends a failure response and * finalizes the request process. * + * @param {function()} callback Completion callback. * @private */ -ImageLoader.Request.prototype.onImageError_ = function() { +ImageLoader.Request.prototype.onImageError_ = function(callback) { this.sendResponse_({status: 'error', taskId: this.request_.taskId}); this.cleanup_(); + this.downloadCallback_(); }; /** @@ -388,6 +441,10 @@ ImageLoader.Request.prototype.onImageError_ = function() { */ ImageLoader.Request.prototype.cancel = function() { this.cleanup_(); + + // If downloading has started, then call the callback. + if (this.downloadCallback_) + this.downloadCallback_(); }; /** @@ -412,6 +469,94 @@ ImageLoader.Request.prototype.cleanup_ = function() { }; /** + * Worker for requests. Fetches requests from a queue and processes them + * synchronously, taking into account priorities. The highest priority is 0. + */ +ImageLoader.Worker = function() { + /** + * List of pending requests. + * @type {ImageLoader.Request} + * @private + */ + this.pendingRequests_ = []; + + /** + * List of requests being processed. + * @type {ImageLoader.Request} + * @private + */ + this.activeRequests_ = []; +}; + +/** + * Maximum requests to be run in parallel. + * @type {number} + * @const + */ +ImageLoader.Worker.MAXIMUM_IN_PARALLEL = 5; + +/** + * Adds a request to the internal priority queue and executes it when requests + * with higher priorities are finished. If the result is cached, then it is + * processed immediately. + * + * @param {ImageLoader.Request} request Request object. + */ +ImageLoader.Worker.prototype.add = function(request) { + request.loadFromCacheAndProcess(function() { }, function() { + // Not found in cache, then add to pending requests. + this.pendingRequests_.push(request); + + // Sort requests by priorities. + this.pendingRequests_.sort(function(a, b) { + return a.getPriority() - b.getPriority(); + }); + + // Handle the most important requests (if possible). + this.continue_(); + }.bind(this)); +}; + +/** + * Processes pending requests from the queue. There is no guarantee that + * all of the tasks will be processed at once. + * + * @private + */ +ImageLoader.Worker.prototype.continue_ = function() { + for (var index = 0; index < this.pendingRequests_.length; index++) { + var request = this.pendingRequests_[index]; + + // Run only up to MAXIMUM_IN_PARALLEL in the same time. + if (Object.keys(this.activeRequests_).length == + ImageLoader.Worker.MAXIMUM_IN_PARALLEL) { + return; + } + + delete this.pendingRequests_.splice(index, 1); + this.activeRequests_.push(request); + + request.downloadAndProcess(this.finish_.bind(this, request)); + } +}; + +/** + * Handles finished requests. + * + * @param {ImageLoader.Request} request Finished request. + * @private + */ +ImageLoader.Worker.prototype.finish_ = function(request) { + var index = this.activeRequests_.indexOf(request); + if (index < 0) + console.warn('Request not found.'); + delete this.activeRequests_.splice(index, 1); + + // Handle the most important requests (if possible). + this.continue_(); +}; + +/** * Persistent cache storing images in an indexed database on the hard disk. * @constructor */ @@ -460,7 +605,7 @@ ImageLoader.Cache.DB_NAME = 'image-loader'; * @type {number} * @const */ -ImageLoader.Cache.DB_VERSION = 8; +ImageLoader.Cache.DB_VERSION = 11; /** * Memory limit for images data in bytes. |