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