Bug 753768 - Moving Page Thumbs I/O fully off the main thread;r=ttaubert

This commit is contained in:
David Rajchenbach-Teller 2013-03-27 16:35:56 +01:00
Родитель d2147038eb
Коммит eec7ada17b
7 изменённых файлов: 401 добавлений и 197 удалений

Просмотреть файл

@ -27,7 +27,10 @@ const THUMBNAIL_DIRECTORY = "thumbnails";
*/ */
const THUMBNAIL_BG_COLOR = "#fff"; const THUMBNAIL_BG_COLOR = "#fff";
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm"); "resource://gre/modules/NetUtil.jsm");
@ -55,6 +58,70 @@ XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
return converter; return converter;
}); });
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
/**
* Utilities for dealing with promises and Task.jsm
*/
const TaskUtils = {
/**
* Add logging to a promise.
*
* @param {Promise} promise
* @return {Promise} A promise behaving as |promise|, but with additional
* logging in case of uncaught error.
*/
captureErrors: function captureErrors(promise) {
return promise.then(
null,
function onError(reason) {
Cu.reportError("Uncaught asynchronous error: " + reason + " at\n"
+ reason.stack + "\n");
throw reason;
}
);
},
/**
* Spawn a new Task from a generator.
*
* This function behaves as |Task.spawn|, with the exception that it
* adds logging in case of uncaught error. For more information, see
* the documentation of |Task.jsm|.
*
* @param {generator} gen Some generator.
* @return {Promise} A promise built from |gen|, with the same semantics
* as |Task.spawn(gen)|.
*/
spawn: function spawn(gen) {
return this.captureErrors(Task.spawn(gen));
},
/**
* Read the bytes from a blob, asynchronously.
*
* @return {Promise}
* @resolve {ArrayBuffer} In case of success, the bytes contained in the blob.
* @reject {DOMError} In case of error, the underlying DOMError.
*/
readBlob: function readBlob(blob) {
let deferred = Promise.defer();
let reader = Cc["@mozilla.org/files/filereader;1"].createInstance(Ci.nsIDOMFileReader);
reader.onloadend = function onloadend() {
if (reader.readyState != Ci.nsIDOMFileReader.DONE) {
deferred.reject(reader.error);
} else {
deferred.resolve(reader.result);
}
};
reader.readAsArrayBuffer(blob);
return deferred.promise;
}
};
/** /**
* Singleton providing functionality for capturing web page thumbnails and for * Singleton providing functionality for capturing web page thumbnails and for
* accessing them if already cached. * accessing them if already cached.
@ -134,6 +201,33 @@ this.PageThumbs = {
}.bind(this), Ci.nsIThread.DISPATCH_NORMAL); }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
}, },
/**
* Captures a thumbnail for the given window.
*
* @param aWindow The DOM window to capture a thumbnail from.
* @return {Promise}
* @resolve {Blob} The thumbnail, as a Blob.
*/
captureToBlob: function PageThumbs_captureToBlob(aWindow) {
if (!this._prefEnabled()) {
return null;
}
let canvas = this._createCanvas();
this.captureToCanvas(aWindow, canvas);
let deferred = Promise.defer();
let type = this.contentType;
// Fetch the canvas data on the next event loop tick so that we allow
// some event processing in between drawing to the canvas and encoding
// its data. We want to block the UI as short as possible. See bug 744100.
canvas.toBlob(function asBlob(blob) {
deferred.resolve(blob, type);
});
return deferred.promise;
},
/** /**
* Captures a thumbnail from a given window and draws it to the given canvas. * Captures a thumbnail from a given window and draws it to the given canvas.
* @param aWindow The DOM window to capture a thumbnail from. * @param aWindow The DOM window to capture a thumbnail from.
@ -177,35 +271,39 @@ this.PageThumbs = {
let channel = aBrowser.docShell.currentDocumentChannel; let channel = aBrowser.docShell.currentDocumentChannel;
let originalURL = channel.originalURI.spec; let originalURL = channel.originalURI.spec;
this.capture(aBrowser.contentWindow, function (aInputStream) { TaskUtils.spawn((function task() {
let telemetryStoreTime = new Date(); let isSuccess = true;
try {
let blob = yield this.captureToBlob(aBrowser.contentWindow);
let buffer = yield TaskUtils.readBlob(blob);
function finish(aSuccessful) { let telemetryStoreTime = new Date();
if (aSuccessful) { yield PageThumbsStorage.writeData(url, new Uint8Array(buffer));
Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
.add(new Date() - telemetryStoreTime);
// We've been redirected. Create a copy of the current thumbnail for Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
// the redirect source. We need to do this because: .add(new Date() - telemetryStoreTime);
//
// 1) Users can drag any kind of links onto the newtab page. If those // We've been redirected. Create a copy of the current thumbnail for
// links redirect to a different URL then we want to be able to // the redirect source. We need to do this because:
// provide thumbnails for both of them. //
// // 1) Users can drag any kind of links onto the newtab page. If those
// 2) The newtab page should actually display redirect targets, only. // links redirect to a different URL then we want to be able to
// Because of bug 559175 this information can get lost when using // provide thumbnails for both of them.
// Sync and therefore also redirect sources appear on the newtab //
// page. We also want thumbnails for those. // 2) The newtab page should actually display redirect targets, only.
if (url != originalURL) // Because of bug 559175 this information can get lost when using
PageThumbsStorage.copy(url, originalURL); // Sync and therefore also redirect sources appear on the newtab
// page. We also want thumbnails for those.
if (url != originalURL) {
yield PageThumbsStorage.copy(url, originalURL);
} }
} catch (_) {
if (aCallback) isSuccess = false;
aCallback(aSuccessful);
} }
if (aCallback) {
PageThumbsStorage.write(url, aInputStream, finish); aCallback(isSuccess);
}); }
}).bind(this));
}, },
/** /**
@ -314,55 +412,99 @@ this.PageThumbs = {
}; };
this.PageThumbsStorage = { this.PageThumbsStorage = {
getDirectory: function Storage_getDirectory(aCreate = true) { // The path for the storage
return FileUtils.getDir("ProfLD", [THUMBNAIL_DIRECTORY], aCreate); _path: null,
get path() {
if (!this._path) {
this._path = OS.Path.join(OS.Constants.Path.localProfileDir, THUMBNAIL_DIRECTORY);
}
return this._path;
},
ensurePath: function Storage_ensurePath() {
// Create the directory (ignore any error if the directory
// already exists). As all writes are done from the PageThumbsWorker
// thread, which serializes its operations, this ensures that
// future operations can proceed without having to check whether
// the directory exists.
return PageThumbsWorker.post("makeDir",
[this.path, {ignoreExisting: true}]).then(
null,
function onError(aReason) {
Components.utils.reportError("Could not create thumbnails directory" + aReason);
});
}, },
getLeafNameForURL: function Storage_getLeafNameForURL(aURL) { getLeafNameForURL: function Storage_getLeafNameForURL(aURL) {
if (typeof aURL != "string") {
throw new TypeError("Expecting a string");
}
let hash = this._calculateMD5Hash(aURL); let hash = this._calculateMD5Hash(aURL);
return hash + ".png"; return hash + ".png";
}, },
getFileForURL: function Storage_getFileForURL(aURL) { getFilePathForURL: function Storage_getFilePathForURL(aURL) {
let file = this.getDirectory(); return OS.Path.join(this.path, this.getLeafNameForURL(aURL));
file.append(this.getLeafNameForURL(aURL));
return file;
}, },
write: function Storage_write(aURL, aDataStream, aCallback) { /**
let file = this.getFileForURL(aURL); * Write the contents of a thumbnail, off the main thread.
let fos = FileUtils.openSafeFileOutputStream(file); *
* @param {string} aURL The url for which to store a thumbnail.
NetUtil.asyncCopy(aDataStream, fos, function (aResult) { * @param {string} aData The data to store in the thumbnail, as
FileUtils.closeSafeFileOutputStream(fos); * an ArrayBuffer. This array buffer is neutered and cannot be
aCallback(Components.isSuccessCode(aResult)); * reused after the copy.
}); *
* @return {Promise}
*/
writeData: function Storage_write(aURL, aData) {
let path = this.getFilePathForURL(aURL);
this.ensurePath();
let msg = [
path,
aData,
{
tmpPath: path + ".tmp",
bytes: aData.byteLength,
flush: false /*thumbnails do not require the level of guarantee provided by flush*/
}];
return PageThumbsWorker.post("writeAtomic", msg,
msg /*we don't want that message garbage-collected,
as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level
memory tricks to enforce zero-copy*/);
}, },
/**
* Copy a thumbnail, off the main thread.
*
* @param {string} aSourceURL The url of the thumbnail to copy.
* @param {string} aTargetURL The url of the target thumbnail.
*
* @return {Promise}
*/
copy: function Storage_copy(aSourceURL, aTargetURL) { copy: function Storage_copy(aSourceURL, aTargetURL) {
let sourceFile = this.getFileForURL(aSourceURL); this.ensurePath();
let targetFile = this.getFileForURL(aTargetURL); let sourceFile = this.getFilePathForURL(aSourceURL);
let targetFile = this.getFilePathForURL(aTargetURL);
try { return PageThumbsWorker.post("copy", [sourceFile, targetFile]);
sourceFile.copyTo(targetFile.parent, targetFile.leafName);
} catch (e) {
/* We might not be permitted to write to the file. */
}
}, },
/**
* Remove a single thumbnail, off the main thread.
*
* @return {Promise}
*/
remove: function Storage_remove(aURL) { remove: function Storage_remove(aURL) {
let file = this.getFileForURL(aURL); return PageThumbsWorker.post("remove", [this.getFilePathForURL(aURL)]);
PageThumbsWorker.postMessage({type: "removeFile", path: file.path});
}, },
/**
* Remove all thumbnails, off the main thread.
*
* @return {Promise}
*/
wipe: function Storage_wipe() { wipe: function Storage_wipe() {
let dir = this.getDirectory(false); return PageThumbsWorker.post("wipe", [this.path]);
dir.followLinks = false;
try {
dir.remove(true);
} catch (e) {
/* The directory might not exist or we're not permitted to remove it. */
}
}, },
_calculateMD5Hash: function Storage_calculateMD5Hash(aValue) { _calculateMD5Hash: function Storage_calculateMD5Hash(aValue) {
@ -423,18 +565,20 @@ let PageThumbsStorageMigrator = {
* try to move the old thumbnails to their new location. If that's not * try to move the old thumbnails to their new location. If that's not
* possible (because ProfD might be on a different file system than * possible (because ProfD might be on a different file system than
* ProfLD) we'll just discard them. * ProfLD) we'll just discard them.
*
* @param {string*} local The path to the local profile directory.
* Used for testing. Default argument is good for all non-testing uses.
* @param {string*} roaming The path to the roaming profile directory.
* Used for testing. Default argument is good for all non-testing uses.
*/ */
migrateToVersion3: function Migrator_migrateToVersion3() { migrateToVersion3: function Migrator_migrateToVersion3(
let local = FileUtils.getDir("ProfLD", [THUMBNAIL_DIRECTORY], true); local = OS.Constants.Path.localProfileDir,
let roaming = FileUtils.getDir("ProfD", [THUMBNAIL_DIRECTORY]); roaming = OS.Constants.Path.profileDir) {
PageThumbsWorker.post(
if (!roaming.equals(local)) { "moveOrDeleteAllThumbnails",
PageThumbsWorker.postMessage({ [OS.Path.join(roaming, THUMBNAIL_DIRECTORY),
type: "moveOrDeleteAllThumbnails", OS.Path.join(local, THUMBNAIL_DIRECTORY)]
from: roaming.path, );
to: local.path
});
}
} }
}; };
@ -484,60 +628,46 @@ let PageThumbsExpiration = {
} }
}, },
expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep, aCallback) { expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
PageThumbsWorker.postMessage({ let path = this.path;
type: "expireFilesInDirectory", let keep = [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)];
minChunkSize: EXPIRATION_MIN_CHUNK_SIZE, let msg = [
path: PageThumbsStorage.getDirectory().path, PageThumbsStorage.path,
filesToKeep: [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)] keep,
}, aCallback); EXPIRATION_MIN_CHUNK_SIZE
];
return PageThumbsWorker.post(
"expireFilesInDirectory",
msg
);
} }
}; };
/** /**
* Interface to a dedicated thread handling I/O * Interface to a dedicated thread handling I/O
*/ */
let PageThumbsWorker = {
/**
* A (fifo) queue of callbacks registered for execution
* upon completion of calls to the worker.
*/
_callbacks: [],
/** let PageThumbsWorker = (function() {
* Get the worker, spawning it if necessary. let worker = new PromiseWorker("resource://gre/modules/PageThumbsWorker.js",
* Code of the worker is in companion file PageThumbsWorker.js OS.Shared.LOG.bind("PageThumbs"));
*/ return {
get _worker() { post: function post(...args) {
delete this._worker; let promise = worker.post.apply(worker, args);
this._worker = new ChromeWorker("resource://gre/modules/PageThumbsWorker.js"); return promise.then(
this._worker.addEventListener("message", this); null,
return this._worker; function onError(error) {
}, // Decode any serialized error
if (error instanceof PromiseWorker.WorkerError) {
/** throw OS.File.Error.fromMsg(error.data);
* Post a message to the dedicated thread, registering a callback } else {
* to be executed once the reply has been received. throw error;
* }
* See PageThumbsWorker.js for the format of messages and replies. }
* );
* @param {*} message A JSON message. }
* @param {Function=} callback An optional callback. };
*/ })();
postMessage: function Worker_postMessage(message, callback) {
this._callbacks.push(callback);
this._worker.postMessage(message);
},
/**
* Handle a message from the dedicated thread.
*/
handleEvent: function Worker_handleEvent(aEvent) {
let callback = this._callbacks.shift();
if (callback)
callback(aEvent.data);
}
};
let PageThumbsHistoryObserver = { let PageThumbsHistoryObserver = {
onDeleteURI: function Thumbnails_onDeleteURI(aURI, aGUID) { onDeleteURI: function Thumbnails_onDeleteURI(aURI, aGUID) {

Просмотреть файл

@ -27,6 +27,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm"); "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
/** /**
* Implements the thumbnail protocol handler responsible for moz-page-thumb: URIs. * Implements the thumbnail protocol handler responsible for moz-page-thumb: URIs.
@ -73,8 +75,8 @@ Protocol.prototype = {
*/ */
newChannel: function Proto_newChannel(aURI) { newChannel: function Proto_newChannel(aURI) {
let {url} = parseURI(aURI); let {url} = parseURI(aURI);
let file = PageThumbsStorage.getFileForURL(url); let file = PageThumbsStorage.getFilePathForURL(url);
let fileuri = Services.io.newFileURI(file); let fileuri = Services.io.newFileURI(new FileUtils.File(file));
return Services.io.newChannelFromURI(fileuri); return Services.io.newChannelFromURI(fileuri);
}, },

Просмотреть файл

@ -13,45 +13,62 @@
importScripts("resource://gre/modules/osfile.jsm"); importScripts("resource://gre/modules/osfile.jsm");
let PageThumbsWorker = { let File = OS.File;
handleMessage: function Worker_handleMessage(aEvent) { let Type = OS.Shared.Type;
let msg = aEvent.data;
let data = {result: null, data: null};
switch (msg.type) { /**
case "removeFile": * Communications with the controller.
data.result = this.removeFile(msg); *
break; * Accepts messages:
case "expireFilesInDirectory": * {fun:function_name, args:array_of_arguments_or_null}
data.result = this.expireFilesInDirectory(msg); *
break; * Sends messages:
case "moveOrDeleteAllThumbnails": * {ok: result} / {fail: serialized_form_of_OS.File.Error}
data.result = this.moveOrDeleteAllThumbnails(msg); */
break; self.onmessage = function onmessage(msg) {
default: let data = msg.data;
data.result = false; let id = data.id;
data.detail = "message not understood"; let result;
break; if (!(data.fun in Agent)) {
} throw new Error("Cannot find method " + data.fun);
}
try {
result = Agent[data.fun].apply(Agent, data.args);
} catch (ex if ex instanceof StopIteration) {
// StopIteration cannot be serialized automatically
self.postMessage({StopIteration: true, id: id});
return;
} catch (ex if ex instanceof OS.File.Error) {
// Instances of OS.File.Error know how to serialize themselves
// (deserialization ensures that we end up with OS-specific
// instances of |OS.File.Error|)
self.postMessage({fail: OS.File.Error.toMsg(ex), id:id});
return;
}
// Other exceptions do not, and should be propagated through DOM's
// built-in mechanism for uncaught errors, although this mechanism
// may lose interesting information.
self.postMessage({ok: result, id:id});
};
self.postMessage(data);
},
removeFile: function Worker_removeFile(msg) { let Agent = {
remove: function Agent_removeFile(path) {
try { try {
OS.File.remove(msg.path); OS.File.remove(path);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
} }
}, },
expireFilesInDirectory: function Worker_expireFilesInDirectory(msg) { expireFilesInDirectory:
let entries = this.getFileEntriesInDirectory(msg.path, msg.filesToKeep); function Agent_expireFilesInDirectory(path, filesToKeep, minChunkSize) {
let limit = Math.max(msg.minChunkSize, Math.round(entries.length / 2)); let entries = this.getFileEntriesInDirectory(path, filesToKeep);
let limit = Math.max(minChunkSize, Math.round(entries.length / 2));
for (let entry of entries) { for (let entry of entries) {
this.removeFile(entry); this.remove(entry.path);
// Check if we reached the limit of files to remove. // Check if we reached the limit of files to remove.
if (--limit <= 0) { if (--limit <= 0) {
@ -63,9 +80,13 @@ let PageThumbsWorker = {
}, },
getFileEntriesInDirectory: getFileEntriesInDirectory:
function Worker_getFileEntriesInDirectory(aPath, aSkipFiles) { function Agent_getFileEntriesInDirectory(path, skipFiles) {
let skip = new Set(aSkipFiles); let iter = new OS.File.DirectoryIterator(path);
let iter = new OS.File.DirectoryIterator(aPath); if (!iter.exists()) {
return [];
}
let skip = new Set(skipFiles);
return [entry return [entry
for (entry in iter) for (entry in iter)
@ -73,36 +94,74 @@ let PageThumbsWorker = {
}, },
moveOrDeleteAllThumbnails: moveOrDeleteAllThumbnails:
function Worker_moveOrDeleteAllThumbnails(msg) { function Agent_moveOrDeleteAllThumbnails(pathFrom, pathTo) {
if (!OS.File.exists(msg.from)) OS.File.makeDir(pathTo, {ignoreExisting: true});
if (pathFrom == pathTo) {
return true; return true;
}
let iter = new OS.File.DirectoryIterator(pathFrom);
if (iter.exists()) {
for (let entry in iter) {
if (entry.isDir || entry.isSymLink) {
continue;
}
let iter = new OS.File.DirectoryIterator(msg.from);
for (let entry in iter) {
if (entry.isDir || entry.isSymLink) {
continue;
}
let from = OS.Path.join(msg.from, entry.name); let from = OS.Path.join(pathFrom, entry.name);
let to = OS.Path.join(msg.to, entry.name); let to = OS.Path.join(pathTo, entry.name);
try { try {
OS.File.move(from, to, {noOverwrite: true, noCopy: true}); OS.File.move(from, to, {noOverwrite: true, noCopy: true});
} catch (e) { } catch (e) {
OS.File.remove(from); OS.File.remove(from);
}
} }
} }
iter.close(); iter.close();
try { try {
OS.File.removeEmptyDir(msg.from); OS.File.removeEmptyDir(pathFrom);
} catch (e) { } catch (e) {
// This could fail if there's something in // This could fail if there's something in
// the folder we're not permitted to remove. // the folder we're not permitted to remove.
} }
return true; return true;
} },
writeAtomic: function Agent_writeAtomic(path, buffer, options) {
return File.writeAtomic(path,
buffer,
options);
},
makeDir: function Agent_makeDir(path, options) {
return File.makeDir(path, options);
},
copy: function Agent_copy(source, dest) {
return File.copy(source, dest);
},
wipe: function Agent_wipe(path) {
let iterator = new File.DirectoryIterator(path);
try {
for (let entry in iterator) {
try {
File.remove(entry.path);
} catch (ex) {
// If a file cannot be removed, we should still continue.
// This can happen at least for any of the following reasons:
// - access denied;
// - file has been removed recently during a previous wipe
// and the file system has not flushed that yet (yes, this
// can happen under Windows);
// - file has been removed by the user or another process.
}
}
} finally {
iterator.close();
}
}
}; };
self.onmessage = PageThumbsWorker.handleMessage.bind(PageThumbsWorker);

Просмотреть файл

@ -6,9 +6,6 @@ const URL1 = URL + "#1";
const URL2 = URL + "#2"; const URL2 = URL + "#2";
const URL3 = URL + "#3"; const URL3 = URL + "#3";
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
let tmp = {}; let tmp = {};
Cc["@mozilla.org/moz/jssubscript-loader;1"] Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader) .getService(Ci.mozIJSSubScriptLoader)
@ -18,13 +15,13 @@ const {EXPIRATION_MIN_CHUNK_SIZE, PageThumbsExpiration} = tmp;
function runTests() { function runTests() {
// Create three thumbnails. // Create three thumbnails.
createDummyThumbnail(URL1); yield createDummyThumbnail(URL1);
ok(thumbnailExists(URL1), "first thumbnail created"); ok(thumbnailExists(URL1), "first thumbnail created");
createDummyThumbnail(URL2); yield createDummyThumbnail(URL2);
ok(thumbnailExists(URL2), "second thumbnail created"); ok(thumbnailExists(URL2), "second thumbnail created");
createDummyThumbnail(URL3); yield createDummyThumbnail(URL3);
ok(thumbnailExists(URL3), "third thumbnail created"); ok(thumbnailExists(URL3), "third thumbnail created");
// Remove the third thumbnail. // Remove the third thumbnail.
@ -45,10 +42,11 @@ function runTests() {
// Create some more files than the min chunk size. // Create some more files than the min chunk size.
let urls = []; let urls = [];
for (let i = 0; i < EXPIRATION_MIN_CHUNK_SIZE + 10; i++) { for (let i = 0; i < EXPIRATION_MIN_CHUNK_SIZE + 10; i++) {
urls.push(URL + "#dummy" + i); let url = URL + "#dummy" + i;
urls.push(url);
yield createDummyThumbnail(url);
} }
urls.forEach(createDummyThumbnail);
ok(urls.every(thumbnailExists), "all dummy thumbnails created"); ok(urls.every(thumbnailExists), "all dummy thumbnails created");
// Make sure our dummy thumbnails aren't expired too early. // Make sure our dummy thumbnails aren't expired too early.
@ -71,16 +69,30 @@ function runTests() {
} }
function createDummyThumbnail(aURL) { function createDummyThumbnail(aURL) {
let file = PageThumbsStorage.getFileForURL(aURL); info("Creating dummy thumbnail for " + aURL);
let fos = FileUtils.openSafeFileOutputStream(file); let dummy = new Uint8Array(10);
for (let i = 0; i < 10; ++i) {
let data = "dummy"; dummy[i] = i;
fos.write(data, data.length); }
FileUtils.closeSafeFileOutputStream(fos); PageThumbsStorage.writeData(aURL, dummy).then(
function onSuccess() {
info("createDummyThumbnail succeeded");
executeSoon(next);
},
function onFailure(error) {
ok(false, "createDummyThumbnail failed " + error);
}
);
} }
function expireThumbnails(aKeep) { function expireThumbnails(aKeep) {
PageThumbsExpiration.expireThumbnails(aKeep, function () { PageThumbsExpiration.expireThumbnails(aKeep).then(
executeSoon(next); function onSuccess() {
}); info("expireThumbnails succeeded");
executeSoon(next);
},
function onFailure(error) {
ok(false, "expireThumbnails failed " + error);
}
);
} }

Просмотреть файл

@ -22,16 +22,16 @@ function runTests() {
yield createThumbnail(); yield createThumbnail();
// Make sure Storage.copy() updates an existing file. // Make sure Storage.copy() updates an existing file.
PageThumbsStorage.copy(URL, URL_COPY); yield PageThumbsStorage.copy(URL, URL_COPY);
let copy = PageThumbsStorage.getFileForURL(URL_COPY); let copy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
let mtime = copy.lastModifiedTime -= 60; let mtime = copy.lastModifiedTime -= 60;
PageThumbsStorage.copy(URL, URL_COPY); yield PageThumbsStorage.copy(URL, URL_COPY);
isnot(PageThumbsStorage.getFileForURL(URL_COPY).lastModifiedTime, mtime, isnot(new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY)).lastModifiedTime, mtime,
"thumbnail file was updated"); "thumbnail file was updated");
let file = PageThumbsStorage.getFileForURL(URL); let file = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL));
let fileCopy = PageThumbsStorage.getFileForURL(URL_COPY); let fileCopy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
// Clear the browser history. Retry until the files are gone because Windows // Clear the browser history. Retry until the files are gone because Windows
// locks them sometimes. // locks them sometimes.

Просмотреть файл

@ -13,9 +13,6 @@ Cc["@mozilla.org/moz/jssubscript-loader;1"]
.loadSubScript("resource://gre/modules/PageThumbs.jsm", tmp); .loadSubScript("resource://gre/modules/PageThumbs.jsm", tmp);
let {PageThumbsStorageMigrator} = tmp; let {PageThumbsStorageMigrator} = tmp;
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gDirSvc", XPCOMUtils.defineLazyServiceGetter(this, "gDirSvc",
"@mozilla.org/file/directory_service;1", "nsIProperties"); "@mozilla.org/file/directory_service;1", "nsIProperties");
@ -55,7 +52,7 @@ function runTests() {
writeDummyFile(file, "no-overwrite-plz"); writeDummyFile(file, "no-overwrite-plz");
// Kick off thumbnail storage migration. // Kick off thumbnail storage migration.
PageThumbsStorageMigrator.migrateToVersion3(); PageThumbsStorageMigrator.migrateToVersion3(localProfile.path);
ok(true, "migration finished"); ok(true, "migration finished");
// Wait until the first thumbnail was moved to its new location. // Wait until the first thumbnail was moved to its new location.

Просмотреть файл

@ -4,7 +4,9 @@
let tmp = {}; let tmp = {};
Cu.import("resource://gre/modules/PageThumbs.jsm", tmp); Cu.import("resource://gre/modules/PageThumbs.jsm", tmp);
Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp); Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp);
let {PageThumbs, PageThumbsStorage, SessionStore} = tmp; Cu.import("resource://gre/modules/FileUtils.jsm", tmp);
Cu.import("resource://gre/modules/osfile.jsm", tmp);
let {PageThumbs, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp;
Cu.import("resource://gre/modules/PlacesUtils.jsm"); Cu.import("resource://gre/modules/PlacesUtils.jsm");
@ -45,7 +47,9 @@ let TestRunner = {
*/ */
next: function () { next: function () {
try { try {
TestRunner._iter.next(); let value = TestRunner._iter.next();
if (value && typeof value.then == "function")
value.then(next);
} catch (e if e instanceof StopIteration) { } catch (e if e instanceof StopIteration) {
finish(); finish();
} }
@ -85,10 +89,10 @@ function navigateTo(aURI) {
* @param aElement The DOM element to listen on. * @param aElement The DOM element to listen on.
* @param aCallback The function to call when the load event was dispatched. * @param aCallback The function to call when the load event was dispatched.
*/ */
function whenLoaded(aElement, aCallback) { function whenLoaded(aElement, aCallback = next) {
aElement.addEventListener("load", function onLoad() { aElement.addEventListener("load", function onLoad() {
aElement.removeEventListener("load", onLoad, true); aElement.removeEventListener("load", onLoad, true);
executeSoon(aCallback || next); executeSoon(aCallback);
}, true); }, true);
} }
@ -143,7 +147,7 @@ function retrieveImageDataForURL(aURL, aCallback) {
* @param aURL The url associated to the thumbnail. * @param aURL The url associated to the thumbnail.
*/ */
function thumbnailExists(aURL) { function thumbnailExists(aURL) {
let file = PageThumbsStorage.getFileForURL(aURL); let file = new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
return file.exists() && file.fileSize; return file.exists() && file.fileSize;
} }
@ -211,13 +215,13 @@ function addVisits(aPlaceInfo, aCallback) {
* @param [optional] aCallback * @param [optional] aCallback
* Function to be invoked on completion. * Function to be invoked on completion.
*/ */
function whenFileExists(aURL, aCallback) { function whenFileExists(aURL, aCallback = next) {
let callback = aCallback; let callback = aCallback;
if (!thumbnailExists(aURL)) { if (!thumbnailExists(aURL)) {
callback = function () whenFileExists(aURL, aCallback); callback = function () whenFileExists(aURL, aCallback);
} }
executeSoon(callback || next); executeSoon(callback);
} }
/** /**