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(): boolean=} 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({
    235     url: url,
    236     orientation: opt_options.orientation,
    237     scale: opt_options.scale,
    238     width: opt_options.width,
    239     height: opt_options.height,
    240     maxWidth: opt_options.maxWidth,
    241     maxHeight: opt_options.maxHeight});
    242 };
    243 
    244 /**
    245  * Evicts the least used elements in cache to make space for a new image.
    246  *
    247  * @param {number} size Requested size.
    248  * @private
    249  */
    250 ImageLoaderClient.Cache.prototype.evictCache_ = function(size) {
    251   // Sort from the most recent to the oldest.
    252   this.images_.sort(function(a, b) {
    253     return b.lastLoadTimestamp - a.lastLoadTimestamp;
    254   });
    255 
    256   while (this.images_.length > 0 &&
    257          (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < size)) {
    258     var entry = this.images_.pop();
    259     this.size_ -= entry.data.length;
    260   }
    261 };
    262 
    263 /**
    264  * Saves an image in the cache.
    265  *
    266  * @param {string} key Cache key.
    267  * @param {string} data Image data.
    268  * @param {number=} opt_timestamp Last modification timestamp. Used to detect
    269  *     if the cache entry becomes out of date.
    270  */
    271 ImageLoaderClient.Cache.prototype.saveImage = function(
    272     key, data, opt_timestamp) {
    273   // If the image is currently in cache, then remove it.
    274   if (this.images_[key])
    275     this.removeImage(key);
    276 
    277   if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < data.length) {
    278     ImageLoaderClient.recordBinary('Evicted', 1);
    279     this.evictCache_(data.length);
    280   } else {
    281     ImageLoaderClient.recordBinary('Evicted', 0);
    282   }
    283 
    284   if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ >= data.length) {
    285     this.images_[key] = {
    286       lastLoadTimestamp: Date.now(),
    287       timestamp: opt_timestamp ? opt_timestamp : null,
    288       data: data
    289     };
    290     this.size_ += data.length;
    291   }
    292 };
    293 
    294 /**
    295  * Loads an image from the cache (if available) or returns null.
    296  *
    297  * @param {string} key Cache key.
    298  * @param {number=} opt_timestamp Last modification timestamp. If different
    299  *     that the one in cache, then the entry will be invalidated.
    300  * @return {?string} Data of the loaded image or null.
    301  */
    302 ImageLoaderClient.Cache.prototype.loadImage = function(key, opt_timestamp) {
    303   if (!(key in this.images_))
    304     return null;
    305 
    306   var entry = this.images_[key];
    307   entry.lastLoadTimestamp = Date.now();
    308 
    309   // Check if the image in cache is up to date. If not, then remove it and
    310   // return null.
    311   if (entry.timestamp != opt_timestamp) {
    312     this.removeImage(key);
    313     return null;
    314   }
    315 
    316   return entry.data;
    317 };
    318 
    319 /**
    320  * Returns cache usage.
    321  * @return {number} Value in percent points (0..100).
    322  */
    323 ImageLoaderClient.Cache.prototype.getUsage = function() {
    324   return this.size_ / ImageLoaderClient.Cache.MEMORY_LIMIT * 100.0;
    325 };
    326 
    327 /**
    328  * Removes the image from the cache.
    329  * @param {string} key Cache key.
    330  */
    331 ImageLoaderClient.Cache.prototype.removeImage = function(key) {
    332   if (!(key in this.images_))
    333     return;
    334 
    335   var entry = this.images_[key];
    336   this.size_ -= entry.data.length;
    337   delete this.images_[key];
    338 };
    339 
    340 // Helper functions.
    341 
    342 /**
    343  * Loads and resizes and image. Use opt_isValid to easily cancel requests
    344  * which are not valid anymore, which will reduce cpu consumption.
    345  *
    346  * @param {string} url Url of the requested image.
    347  * @param {Image} image Image node to load the requested picture into.
    348  * @param {Object} options Loader options, such as: orientation, scale,
    349  *     maxHeight, width, height and/or cache.
    350  * @param {function} onSuccess Callback for success.
    351  * @param {function} onError Callback for failure.
    352  * @param {function=} opt_isValid Function returning false in case
    353  *     a request is not valid anymore, eg. parent node has been detached.
    354  * @return {?number} Remote task id or null if loaded from cache.
    355  */
    356 ImageLoaderClient.loadToImage = function(
    357     url, image, options, onSuccess, onError, opt_isValid) {
    358   var callback = function(result) {
    359     if (result.status == 'error') {
    360       onError();
    361       return;
    362     }
    363     image.src = result.data;
    364     onSuccess();
    365   };
    366 
    367   return ImageLoaderClient.getInstance().load(
    368       url, callback, options, opt_isValid);
    369 };
    370