Home | History | Annotate | Download | only in image_loader
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Client used to connect to the remote ImageLoader extension. Client class runs
      9  * in the extension, where the client.js is included (eg. Files.app).
     10  * It sends remote requests using IPC to the ImageLoader class and forwards
     11  * its responses.
     12  *
     13  * Implements cache, which is stored in the calling extension.
     14  *
     15  * @constructor
     16  */
     17 function ImageLoaderClient() {
     18   /**
     19    * Hash array with active tasks.
     20    * @type {Object}
     21    * @private
     22    */
     23   this.tasks_ = {};
     24 
     25   /**
     26    * @type {number}
     27    * @private
     28    */
     29   this.lastTaskId_ = 0;
     30 
     31   /**
     32    * LRU cache for images.
     33    * @type {ImageLoaderClient.Cache}
     34    * @private
     35    */
     36   this.cache_ = new ImageLoaderClient.Cache();
     37 }
     38 
     39 /**
     40  * Image loader's extension id.
     41  * @const
     42  * @type {string}
     43  */
     44 ImageLoaderClient.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp';
     45 
     46 /**
     47  * Returns a singleton instance.
     48  * @return {Client} Client instance.
     49  */
     50 ImageLoaderClient.getInstance = function() {
     51   if (!ImageLoaderClient.instance_)
     52     ImageLoaderClient.instance_ = new ImageLoaderClient();
     53   return ImageLoaderClient.instance_;
     54 };
     55 
     56 /**
     57  * Records binary metrics. Counts for true and false are stored as a histogram.
     58  * @param {string} name Histogram's name.
     59  * @param {boolean} value True or false.
     60  */
     61 ImageLoaderClient.recordBinary = function(name, value) {
     62   chrome.metricsPrivate.recordValue(
     63       { metricName: 'ImageLoader.Client.' + name,
     64         type: 'histogram-linear',
     65         min: 1,  // According to histogram.h, this should be 1 for enums.
     66         max: 2,  // Maximum should be exclusive.
     67         buckets: 3 },  // Number of buckets: 0, 1 and overflowing 2.
     68       value ? 1 : 0);
     69 };
     70 
     71 /**
     72  * Records percent metrics, stored as a histogram.
     73  * @param {string} name Histogram's name.
     74  * @param {number} value Value (0..100).
     75  */
     76 ImageLoaderClient.recordPercentage = function(name, value) {
     77   chrome.metricsPrivate.recordPercentage('ImageLoader.Client.' + name,
     78                                          Math.round(value));
     79 };
     80 
     81 /**
     82  * Sends a message to the Image Loader extension.
     83  * @param {Object} request Hash array with request data.
     84  * @param {function(Object)=} opt_callback Response handling callback.
     85  *     The response is passed as a hash array.
     86  * @private
     87  */
     88 ImageLoaderClient.sendMessage_ = function(request, opt_callback) {
     89   opt_callback = opt_callback || function(response) {};
     90   var sendMessage = chrome.runtime ? chrome.runtime.sendMessage :
     91                                      chrome.extension.sendMessage;
     92   sendMessage(ImageLoaderClient.EXTENSION_ID, request, opt_callback);
     93 };
     94 
     95 /**
     96  * Handles a message from the remote image loader and calls the registered
     97  * callback to pass the response back to the requester.
     98  *
     99  * @param {Object} message Response message as a hash array.
    100  * @private
    101  */
    102 ImageLoaderClient.prototype.handleMessage_ = function(message) {
    103   if (!(message.taskId in this.tasks_)) {
    104     // This task has been canceled, but was already fetched, so it's result
    105     // should be discarded anyway.
    106     return;
    107   }
    108 
    109   var task = this.tasks_[message.taskId];
    110 
    111   // Check if the task is still valid.
    112   if (task.isValid())
    113     task.accept(message);
    114 
    115   delete this.tasks_[message.taskId];
    116 };
    117 
    118 /**
    119  * Loads and resizes and image. Use opt_isValid to easily cancel requests
    120  * which are not valid anymore, which will reduce cpu consumption.
    121  *
    122  * @param {string} url Url of the requested image.
    123  * @param {function} callback Callback used to return response.
    124  * @param {Object=} opt_options Loader options, such as: scale, maxHeight,
    125  *     width, height and/or cache.
    126  * @param {function=} opt_isValid Function returning false in case
    127  *     a request is not valid anymore, eg. parent node has been detached.
    128  * @return {?number} Remote task id or null if loaded from cache.
    129  */
    130 ImageLoaderClient.prototype.load = function(
    131     url, callback, opt_options, opt_isValid) {
    132   opt_options = opt_options || {};
    133   opt_isValid = opt_isValid || function() { return true; };
    134 
    135   // Record cache usage.
    136   ImageLoaderClient.recordPercentage('Cache.Usage', this.cache_.getUsage());
    137 
    138   // Cancel old, invalid tasks.
    139   var taskKeys = Object.keys(this.tasks_);
    140   for (var index = 0; index < taskKeys.length; index++) {
    141     var taskKey = taskKeys[index];
    142     var task = this.tasks_[taskKey];
    143     if (!task.isValid()) {
    144       // Cancel this task since it is not valid anymore.
    145       this.cancel(taskKey);
    146       delete this.tasks_[taskKey];
    147     }
    148   }
    149 
    150   // Replace the extension id.
    151   var sourceId = chrome.i18n.getMessage('@@extension_id');
    152   var targetId = ImageLoaderClient.EXTENSION_ID;
    153 
    154   url = url.replace('filesystem:chrome-extension://' + sourceId,
    155                     'filesystem:chrome-extension://' + targetId);
    156 
    157   // Try to load from cache, if available.
    158   var cacheKey = ImageLoaderClient.Cache.createKey(url, opt_options);
    159   if (opt_options.cache) {
    160     // Load from cache.
    161     ImageLoaderClient.recordBinary('Cached', 1);
    162     var cachedData = this.cache_.loadImage(cacheKey, opt_options.timestamp);
    163     if (cachedData) {
    164       ImageLoaderClient.recordBinary('Cache.HitMiss', 1);
    165       callback({status: 'success', data: cachedData});
    166       return null;
    167     } else {
    168       ImageLoaderClient.recordBinary('Cache.HitMiss', 0);
    169     }
    170   } else {
    171     // Remove from cache.
    172     ImageLoaderClient.recordBinary('Cached', 0);
    173     this.cache_.removeImage(cacheKey);
    174   }
    175 
    176   // Not available in cache, performing a request to a remote extension.
    177   var request = opt_options;
    178   this.lastTaskId_++;
    179   var task = {isValid: opt_isValid};
    180   this.tasks_[this.lastTaskId_] = task;
    181 
    182   request.url = url;
    183   request.taskId = this.lastTaskId_;
    184   request.timestamp = opt_options.timestamp;
    185 
    186   ImageLoaderClient.sendMessage_(
    187       request,
    188       function(result) {
    189         // Save to cache.
    190         if (result.status == 'success' && opt_options.cache)
    191           this.cache_.saveImage(cacheKey, result.data, opt_options.timestamp);
    192         callback(result);
    193       }.bind(this));
    194   return request.taskId;
    195 };
    196 
    197 /**
    198  * Cancels the request.
    199  * @param {number} taskId Task id returned by ImageLoaderClient.load().
    200  */
    201 ImageLoaderClient.prototype.cancel = function(taskId) {
    202   ImageLoaderClient.sendMessage_({taskId: taskId, cancel: true});
    203 };
    204 
    205 /**
    206  * Least Recently Used (LRU) cache implementation to be used by
    207  * Client class. It has memory constraints, so it will never
    208  * exceed specified memory limit defined in MEMORY_LIMIT.
    209  *
    210  * @constructor
    211  */
    212 ImageLoaderClient.Cache = function() {
    213   this.images_ = [];
    214   this.size_ = 0;
    215 };
    216 
    217 /**
    218  * Memory limit for images data in bytes.
    219  *
    220  * @const
    221  * @type {number}
    222  */
    223 ImageLoaderClient.Cache.MEMORY_LIMIT = 20 * 1024 * 1024;  // 20 MB.
    224 
    225 /**
    226  * Creates a cache key.
    227  *
    228  * @param {string} url Image url.
    229  * @param {Object=} opt_options Loader options as a hash array.
    230  * @return {string} Cache key.
    231  */
    232 ImageLoaderClient.Cache.createKey = function(url, opt_options) {
    233   opt_options = opt_options || {};
    234   return JSON.stringify({url: url,
    235                          orientation: opt_options.orientation,
    236                          scale: opt_options.scale,
    237                          width: opt_options.width,
    238                          height: opt_options.height,
    239                          maxWidth: opt_options.maxWidth,
    240                          maxHeight: opt_options.maxHeight});
    241 };
    242 
    243 /**
    244  * Evicts the least used elements in cache to make space for a new image.
    245  *
    246  * @param {number} size Requested size.
    247  * @private
    248  */
    249 ImageLoaderClient.Cache.prototype.evictCache_ = function(size) {
    250   // Sort from the most recent to the oldest.
    251   this.images_.sort(function(a, b) {
    252     return b.lastLoadTimestamp - a.lastLoadTimestamp;
    253   });
    254 
    255   while (this.images_.length > 0 &&
    256          (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < size)) {
    257     var entry = this.images_.pop();
    258     this.size_ -= entry.data.length;
    259   }
    260 };
    261 
    262 /**
    263  * Saves an image in the cache.
    264  *
    265  * @param {string} key Cache key.
    266  * @param {string} data Image data.
    267  * @param {number=} opt_timestamp Last modification timestamp. Used to detect
    268  *     if the cache entry becomes out of date.
    269  */
    270 ImageLoaderClient.Cache.prototype.saveImage = function(
    271     key, data, opt_timestamp) {
    272   // If the image is currently in cache, then remove it.
    273   if (this.images_[key])
    274     this.removeImage(key);
    275 
    276   if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < data.length) {
    277     ImageLoaderClient.recordBinary('Evicted', 1);
    278     this.evictCache_(data.length);
    279   } else {
    280     ImageLoaderClient.recordBinary('Evicted', 0);
    281   }
    282 
    283   if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ >= data.length) {
    284     this.images_[key] = {lastLoadTimestamp: Date.now(),
    285                          timestamp: opt_timestamp ? opt_timestamp : null,
    286                          data: data};
    287     this.size_ += data.length;
    288   }
    289 };
    290 
    291 /**
    292  * Loads an image from the cache (if available) or returns null.
    293  *
    294  * @param {string} key Cache key.
    295  * @param {number=} opt_timestamp Last modification timestamp. If different
    296  *     that the one in cache, then the entry will be invalidated.
    297  * @return {?string} Data of the loaded image or null.
    298  */
    299 ImageLoaderClient.Cache.prototype.loadImage = function(key, opt_timestamp) {
    300   if (!(key in this.images_))
    301     return null;
    302 
    303   var entry = this.images_[key];
    304   entry.lastLoadTimestamp = Date.now();
    305 
    306   // Check if the image in cache is up to date. If not, then remove it and
    307   // return null.
    308   if (entry.timestamp != opt_timestamp) {
    309     this.removeImage(key);
    310     return null;
    311   }
    312 
    313   return entry.data;
    314 };
    315 
    316 /**
    317  * Returns cache usage.
    318  * @return {number} Value in percent points (0..100).
    319  */
    320 ImageLoaderClient.Cache.prototype.getUsage = function() {
    321   return this.size_ / ImageLoaderClient.Cache.MEMORY_LIMIT * 100.0;
    322 };
    323 
    324 /**
    325  * Removes the image from the cache.
    326  * @param {string} key Cache key.
    327  */
    328 ImageLoaderClient.Cache.prototype.removeImage = function(key) {
    329   if (!(key in this.images_))
    330     return;
    331 
    332   var entry = this.images_[key];
    333   this.size_ -= entry.data.length;
    334   delete this.images_[key];
    335 };
    336 
    337 // Helper functions.
    338 
    339 /**
    340  * Loads and resizes and image. Use opt_isValid to easily cancel requests
    341  * which are not valid anymore, which will reduce cpu consumption.
    342  *
    343  * @param {string} url Url of the requested image.
    344  * @param {Image} image Image node to load the requested picture into.
    345  * @param {Object} options Loader options, such as: orientation, scale,
    346  *     maxHeight, width, height and/or cache.
    347  * @param {function=} onSuccess Callback for success.
    348  * @param {function=} onError Callback for failure.
    349  * @param {function=} opt_isValid Function returning false in case
    350  *     a request is not valid anymore, eg. parent node has been detached.
    351  * @return {?number} Remote task id or null if loaded from cache.
    352  */
    353 ImageLoaderClient.loadToImage = function(
    354     url, image, options, onSuccess, onError, opt_isValid) {
    355   var callback = function(result) {
    356     if (result.status == 'error') {
    357       onError();
    358       return;
    359     }
    360     image.src = result.data;
    361     onSuccess();
    362   };
    363 
    364   return ImageLoaderClient.getInstance().load(
    365       url, callback, options, opt_isValid);
    366 };
    367