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.
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
13 * Implements cache, which is stored in the calling extension.
17 function ImageLoaderClient() {
19 * Hash array with active tasks.
32 * LRU cache for images.
33 * @type {ImageLoaderClient.Cache}
36 this.cache_
= new ImageLoaderClient
.Cache();
40 * Image loader's extension id.
44 ImageLoaderClient
.EXTENSION_ID
= 'pmfjbimdmchhbnneeidfognadeopoehp';
47 * Returns a singleton instance.
48 * @return {Client} Client instance.
50 ImageLoaderClient
.getInstance = function() {
51 if (!ImageLoaderClient
.instance_
)
52 ImageLoaderClient
.instance_
= new ImageLoaderClient();
53 return ImageLoaderClient
.instance_
;
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.
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.
72 * Records percent metrics, stored as a histogram.
73 * @param {string} name Histogram's name.
74 * @param {number} value Value (0..100).
76 ImageLoaderClient
.recordPercentage = function(name
, value
) {
77 chrome
.metricsPrivate
.recordPercentage('ImageLoader.Client.' + name
,
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.
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
);
96 * Handles a message from the remote image loader and calls the registered
97 * callback to pass the response back to the requester.
99 * @param {Object} message Response message as a hash array.
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.
109 var task
= this.tasks_
[message
.taskId
];
111 // Check if the task is still valid.
113 task
.accept(message
);
115 delete this.tasks_
[message
.taskId
];
119 * Loads and resizes and image. Use opt_isValid to easily cancel requests
120 * which are not valid anymore, which will reduce cpu consumption.
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.
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; };
135 // Record cache usage.
136 ImageLoaderClient
.recordPercentage('Cache.Usage', this.cache_
.getUsage());
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
];
150 // Replace the extension id.
151 var sourceId
= chrome
.i18n
.getMessage('@@extension_id');
152 var targetId
= ImageLoaderClient
.EXTENSION_ID
;
154 url
= url
.replace('filesystem:chrome-extension://' + sourceId
,
155 'filesystem:chrome-extension://' + targetId
);
157 // Try to load from cache, if available.
158 var cacheKey
= ImageLoaderClient
.Cache
.createKey(url
, opt_options
);
159 if (opt_options
.cache
) {
161 ImageLoaderClient
.recordBinary('Cached', 1);
162 var cachedData
= this.cache_
.loadImage(cacheKey
, opt_options
.timestamp
);
164 ImageLoaderClient
.recordBinary('Cache.HitMiss', 1);
165 callback({status
: 'success', data
: cachedData
});
168 ImageLoaderClient
.recordBinary('Cache.HitMiss', 0);
171 // Remove from cache.
172 ImageLoaderClient
.recordBinary('Cached', 0);
173 this.cache_
.removeImage(cacheKey
);
176 // Not available in cache, performing a request to a remote extension.
177 var request
= opt_options
;
179 var task
= {isValid
: opt_isValid
};
180 this.tasks_
[this.lastTaskId_
] = task
;
183 request
.taskId
= this.lastTaskId_
;
184 request
.timestamp
= opt_options
.timestamp
;
186 ImageLoaderClient
.sendMessage_(
190 if (result
.status
== 'success' && opt_options
.cache
)
191 this.cache_
.saveImage(cacheKey
, result
.data
, opt_options
.timestamp
);
194 return request
.taskId
;
198 * Cancels the request.
199 * @param {number} taskId Task id returned by ImageLoaderClient.load().
201 ImageLoaderClient
.prototype.cancel = function(taskId
) {
202 ImageLoaderClient
.sendMessage_({taskId
: taskId
, cancel
: true});
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.
212 ImageLoaderClient
.Cache = function() {
218 * Memory limit for images data in bytes.
223 ImageLoaderClient
.Cache
.MEMORY_LIMIT
= 20 * 1024 * 1024; // 20 MB.
226 * Creates a cache key.
228 * @param {string} url Image url.
229 * @param {Object=} opt_options Loader options as a hash array.
230 * @return {string} Cache key.
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
});
244 * Evicts the least used elements in cache to make space for a new image.
246 * @param {number} size Requested size.
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
;
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
;
263 * Saves an image in the cache.
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.
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
);
276 if (ImageLoaderClient
.Cache
.MEMORY_LIMIT
- this.size_
< data
.length
) {
277 ImageLoaderClient
.recordBinary('Evicted', 1);
278 this.evictCache_(data
.length
);
280 ImageLoaderClient
.recordBinary('Evicted', 0);
283 if (ImageLoaderClient
.Cache
.MEMORY_LIMIT
- this.size_
>= data
.length
) {
284 this.images_
[key
] = {lastLoadTimestamp
: Date
.now(),
285 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 {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.
353 ImageLoaderClient
.loadToImage = function(
354 url
, image
, options
, onSuccess
, onError
, opt_isValid
) {
355 var callback = function(result
) {
356 if (result
.status
== 'error') {
360 image
.src
= result
.data
;
364 return ImageLoaderClient
.getInstance().load(
365 url
, callback
, options
, opt_isValid
);