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.
6 * Client used to connect to the remote ImageLoader extension. Client class runs
7 * in the extension, where the client.js is included (eg. Files.app).
8 * It sends remote requests using IPC to the ImageLoader class and forwards
11 * Implements cache, which is stored in the calling extension.
15 function ImageLoaderClient() {
17 * Hash array with active tasks.
30 * LRU cache for images.
31 * @type {ImageLoaderClient.Cache}
34 this.cache_ = new ImageLoaderClient.Cache();
38 * Image loader's extension id.
42 ImageLoaderClient.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp';
45 * Returns a singleton instance.
46 * @return {ImageLoaderClient} Client instance.
48 ImageLoaderClient.getInstance = function() {
49 if (!ImageLoaderClient.instance_)
50 ImageLoaderClient.instance_ = new ImageLoaderClient();
51 return ImageLoaderClient.instance_;
55 * Records binary metrics. Counts for true and false are stored as a histogram.
56 * @param {string} name Histogram's name.
57 * @param {boolean} value True or false.
59 ImageLoaderClient.recordBinary = function(name, value) {
60 chrome.metricsPrivate.recordValue(
61 { metricName: 'ImageLoader.Client.' + name,
62 type: 'histogram-linear',
63 min: 1, // According to histogram.h, this should be 1 for enums.
64 max: 2, // Maximum should be exclusive.
65 buckets: 3 }, // Number of buckets: 0, 1 and overflowing 2.
70 * Records percent metrics, stored as a histogram.
71 * @param {string} name Histogram's name.
72 * @param {number} value Value (0..100).
74 ImageLoaderClient.recordPercentage = function(name, value) {
75 chrome.metricsPrivate.recordPercentage('ImageLoader.Client.' + name,
80 * Sends a message to the Image Loader extension.
81 * @param {Object} request Hash array with request data.
82 * @param {function(Object)=} opt_callback Response handling callback.
83 * The response is passed as a hash array.
86 ImageLoaderClient.sendMessage_ = function(request, opt_callback) {
87 opt_callback = opt_callback || function(response) {};
88 chrome.runtime.sendMessage(
89 ImageLoaderClient.EXTENSION_ID, request, opt_callback);
93 * Handles a message from the remote image loader and calls the registered
94 * callback to pass the response back to the requester.
96 * @param {Object} message Response message as a hash array.
99 ImageLoaderClient.prototype.handleMessage_ = function(message) {
100 if (!(message.taskId in this.tasks_)) {
101 // This task has been canceled, but was already fetched, so it's result
102 // should be discarded anyway.
106 var task = this.tasks_[message.taskId];
108 // Check if the task is still valid.
110 task.accept(message);
112 delete this.tasks_[message.taskId];
116 * Loads and resizes and image. Use opt_isValid to easily cancel requests
117 * which are not valid anymore, which will reduce cpu consumption.
119 * @param {string} url Url of the requested image.
120 * @param {function(Object)} callback Callback used to return response.
121 * @param {Object=} opt_options Loader options, such as: scale, maxHeight,
122 * width, height and/or cache.
123 * @param {function(): boolean=} opt_isValid Function returning false in case
124 * a request is not valid anymore, eg. parent node has been detached.
125 * @return {?number} Remote task id or null if loaded from cache.
127 ImageLoaderClient.prototype.load = function(
128 url, callback, opt_options, opt_isValid) {
129 opt_options = /** @type {{cache: (boolean|undefined)}} */(opt_options || {});
130 opt_isValid = opt_isValid || function() { return true; };
132 // Record cache usage.
133 ImageLoaderClient.recordPercentage('Cache.Usage', this.cache_.getUsage());
135 // Cancel old, invalid tasks.
136 var taskKeys = Object.keys(this.tasks_);
137 for (var index = 0; index < taskKeys.length; index++) {
138 var taskKey = taskKeys[index];
139 var task = this.tasks_[taskKey];
140 if (!task.isValid()) {
141 // Cancel this task since it is not valid anymore.
142 this.cancel(parseInt(taskKey, 10));
143 delete this.tasks_[taskKey];
147 // Replace the extension id.
148 var sourceId = chrome.i18n.getMessage('@@extension_id');
149 var targetId = ImageLoaderClient.EXTENSION_ID;
151 url = url.replace('filesystem:chrome-extension://' + sourceId,
152 'filesystem:chrome-extension://' + targetId);
154 // Try to load from cache, if available.
155 var cacheKey = ImageLoaderClient.Cache.createKey(url, opt_options);
156 if (opt_options.cache) {
158 ImageLoaderClient.recordBinary('Cached', true);
159 var cachedData = this.cache_.loadImage(cacheKey, opt_options.timestamp);
161 ImageLoaderClient.recordBinary('Cache.HitMiss', true);
162 callback({status: 'success', data: cachedData});
165 ImageLoaderClient.recordBinary('Cache.HitMiss', false);
168 // Remove from cache.
169 ImageLoaderClient.recordBinary('Cached', false);
170 this.cache_.removeImage(cacheKey);
173 // Not available in cache, performing a request to a remote extension.
174 var request = opt_options;
176 var task = {isValid: opt_isValid};
177 this.tasks_[this.lastTaskId_] = task;
180 request.taskId = this.lastTaskId_;
181 request.timestamp = opt_options.timestamp;
183 ImageLoaderClient.sendMessage_(
187 if (result.status == 'success' && opt_options.cache)
188 this.cache_.saveImage(cacheKey, result.data, opt_options.timestamp);
191 return request.taskId;
195 * Cancels the request.
196 * @param {number} taskId Task id returned by ImageLoaderClient.load().
198 ImageLoaderClient.prototype.cancel = function(taskId) {
199 ImageLoaderClient.sendMessage_({taskId: taskId, cancel: true});
203 * Least Recently Used (LRU) cache implementation to be used by
204 * Client class. It has memory constraints, so it will never
205 * exceed specified memory limit defined in MEMORY_LIMIT.
209 ImageLoaderClient.Cache = function() {
215 * Memory limit for images data in bytes.
220 ImageLoaderClient.Cache.MEMORY_LIMIT = 20 * 1024 * 1024; // 20 MB.
223 * Creates a cache key.
225 * @param {string} url Image url.
226 * @param {Object=} opt_options Loader options as a hash array.
227 * @return {string} Cache key.
229 ImageLoaderClient.Cache.createKey = function(url, opt_options) {
230 opt_options = opt_options || {};
231 return JSON.stringify({
233 orientation: opt_options.orientation,
234 scale: opt_options.scale,
235 width: opt_options.width,
236 height: opt_options.height,
237 maxWidth: opt_options.maxWidth,
238 maxHeight: opt_options.maxHeight});
242 * Evicts the least used elements in cache to make space for a new image.
244 * @param {number} size Requested size.
247 ImageLoaderClient.Cache.prototype.evictCache_ = function(size) {
248 // Sort from the most recent to the oldest.
249 this.images_.sort(function(a, b) {
250 return b.lastLoadTimestamp - a.lastLoadTimestamp;
253 while (this.images_.length > 0 &&
254 (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < size)) {
255 var entry = this.images_.pop();
256 this.size_ -= entry.data.length;
261 * Saves an image in the cache.
263 * @param {string} key Cache key.
264 * @param {string} data Image data.
265 * @param {number=} opt_timestamp Last modification timestamp. Used to detect
266 * if the cache entry becomes out of date.
268 ImageLoaderClient.Cache.prototype.saveImage = function(
269 key, data, opt_timestamp) {
270 // If the image is currently in cache, then remove it.
271 if (this.images_[key])
272 this.removeImage(key);
274 if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < data.length) {
275 ImageLoaderClient.recordBinary('Evicted', true);
276 this.evictCache_(data.length);
278 ImageLoaderClient.recordBinary('Evicted', false);
281 if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ >= data.length) {
282 this.images_[key] = {
283 lastLoadTimestamp: Date.now(),
284 timestamp: opt_timestamp ? opt_timestamp : null,
287 this.size_ += data.length;
292 * Loads an image from the cache (if available) or returns null.
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.
299 ImageLoaderClient.Cache.prototype.loadImage = function(key, opt_timestamp) {
300 if (!(key in this.images_))
303 var entry = this.images_[key];
304 entry.lastLoadTimestamp = Date.now();
306 // Check if the image in cache is up to date. If not, then remove it and
308 if (entry.timestamp != opt_timestamp) {
309 this.removeImage(key);
317 * Returns cache usage.
318 * @return {number} Value in percent points (0..100).
320 ImageLoaderClient.Cache.prototype.getUsage = function() {
321 return this.size_ / ImageLoaderClient.Cache.MEMORY_LIMIT * 100.0;
325 * Removes the image from the cache.
326 * @param {string} key Cache key.
328 ImageLoaderClient.Cache.prototype.removeImage = function(key) {
329 if (!(key in this.images_))
332 var entry = this.images_[key];
333 this.size_ -= entry.data.length;
334 delete this.images_[key];
340 * Loads and resizes and image. Use opt_isValid to easily cancel requests
341 * which are not valid anymore, which will reduce cpu consumption.
343 * @param {string} url Url of the requested image.
344 * @param {HTMLImageElement} image Image node to load the requested picture
346 * @param {Object} options Loader options, such as: orientation, scale,
347 * maxHeight, width, height and/or cache.
348 * @param {function()} onSuccess Callback for success.
349 * @param {function()} onError Callback for failure.
350 * @param {function(): boolean=} opt_isValid Function returning false in case
351 * a request is not valid anymore, eg. parent node has been detached.
352 * @return {?number} Remote task id or null if loaded from cache.
354 ImageLoaderClient.loadToImage = function(
355 url, image, options, onSuccess, onError, opt_isValid) {
356 var callback = function(result) {
357 if (result.status == 'error') {
361 image.src = result.data;
365 return ImageLoaderClient.getInstance().load(
366 url, callback, options, opt_isValid);