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";
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",
"resource://gre/modules/NetUtil.jsm");
@ -55,6 +58,70 @@ XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
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
* accessing them if already cached.
@ -134,6 +201,33 @@ this.PageThumbs = {
}.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.
* @param aWindow The DOM window to capture a thumbnail from.
@ -177,35 +271,39 @@ this.PageThumbs = {
let channel = aBrowser.docShell.currentDocumentChannel;
let originalURL = channel.originalURI.spec;
this.capture(aBrowser.contentWindow, function (aInputStream) {
let telemetryStoreTime = new Date();
TaskUtils.spawn((function task() {
let isSuccess = true;
try {
let blob = yield this.captureToBlob(aBrowser.contentWindow);
let buffer = yield TaskUtils.readBlob(blob);
function finish(aSuccessful) {
if (aSuccessful) {
Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
.add(new Date() - telemetryStoreTime);
let telemetryStoreTime = new Date();
yield PageThumbsStorage.writeData(url, new Uint8Array(buffer));
// We've been redirected. Create a copy of the current thumbnail for
// the redirect source. We need to do this because:
//
// 1) Users can drag any kind of links onto the newtab page. If those
// links redirect to a different URL then we want to be able to
// provide thumbnails for both of them.
//
// 2) The newtab page should actually display redirect targets, only.
// Because of bug 559175 this information can get lost when using
// Sync and therefore also redirect sources appear on the newtab
// page. We also want thumbnails for those.
if (url != originalURL)
PageThumbsStorage.copy(url, originalURL);
Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
.add(new Date() - telemetryStoreTime);
// We've been redirected. Create a copy of the current thumbnail for
// the redirect source. We need to do this because:
//
// 1) Users can drag any kind of links onto the newtab page. If those
// links redirect to a different URL then we want to be able to
// provide thumbnails for both of them.
//
// 2) The newtab page should actually display redirect targets, only.
// Because of bug 559175 this information can get lost when using
// 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);
}
if (aCallback)
aCallback(aSuccessful);
} catch (_) {
isSuccess = false;
}
PageThumbsStorage.write(url, aInputStream, finish);
});
if (aCallback) {
aCallback(isSuccess);
}
}).bind(this));
},
/**
@ -314,55 +412,99 @@ this.PageThumbs = {
};
this.PageThumbsStorage = {
getDirectory: function Storage_getDirectory(aCreate = true) {
return FileUtils.getDir("ProfLD", [THUMBNAIL_DIRECTORY], aCreate);
// The path for the storage
_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) {
if (typeof aURL != "string") {
throw new TypeError("Expecting a string");
}
let hash = this._calculateMD5Hash(aURL);
return hash + ".png";
},
getFileForURL: function Storage_getFileForURL(aURL) {
let file = this.getDirectory();
file.append(this.getLeafNameForURL(aURL));
return file;
getFilePathForURL: function Storage_getFilePathForURL(aURL) {
return OS.Path.join(this.path, this.getLeafNameForURL(aURL));
},
write: function Storage_write(aURL, aDataStream, aCallback) {
let file = this.getFileForURL(aURL);
let fos = FileUtils.openSafeFileOutputStream(file);
NetUtil.asyncCopy(aDataStream, fos, function (aResult) {
FileUtils.closeSafeFileOutputStream(fos);
aCallback(Components.isSuccessCode(aResult));
});
/**
* Write the contents of a thumbnail, off the main thread.
*
* @param {string} aURL The url for which to store a thumbnail.
* @param {string} aData The data to store in the thumbnail, as
* an ArrayBuffer. This array buffer is neutered and cannot be
* 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) {
let sourceFile = this.getFileForURL(aSourceURL);
let targetFile = this.getFileForURL(aTargetURL);
try {
sourceFile.copyTo(targetFile.parent, targetFile.leafName);
} catch (e) {
/* We might not be permitted to write to the file. */
}
this.ensurePath();
let sourceFile = this.getFilePathForURL(aSourceURL);
let targetFile = this.getFilePathForURL(aTargetURL);
return PageThumbsWorker.post("copy", [sourceFile, targetFile]);
},
/**
* Remove a single thumbnail, off the main thread.
*
* @return {Promise}
*/
remove: function Storage_remove(aURL) {
let file = this.getFileForURL(aURL);
PageThumbsWorker.postMessage({type: "removeFile", path: file.path});
return PageThumbsWorker.post("remove", [this.getFilePathForURL(aURL)]);
},
/**
* Remove all thumbnails, off the main thread.
*
* @return {Promise}
*/
wipe: function Storage_wipe() {
let dir = this.getDirectory(false);
dir.followLinks = false;
try {
dir.remove(true);
} catch (e) {
/* The directory might not exist or we're not permitted to remove it. */
}
return PageThumbsWorker.post("wipe", [this.path]);
},
_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
* possible (because ProfD might be on a different file system than
* 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() {
let local = FileUtils.getDir("ProfLD", [THUMBNAIL_DIRECTORY], true);
let roaming = FileUtils.getDir("ProfD", [THUMBNAIL_DIRECTORY]);
if (!roaming.equals(local)) {
PageThumbsWorker.postMessage({
type: "moveOrDeleteAllThumbnails",
from: roaming.path,
to: local.path
});
}
migrateToVersion3: function Migrator_migrateToVersion3(
local = OS.Constants.Path.localProfileDir,
roaming = OS.Constants.Path.profileDir) {
PageThumbsWorker.post(
"moveOrDeleteAllThumbnails",
[OS.Path.join(roaming, THUMBNAIL_DIRECTORY),
OS.Path.join(local, THUMBNAIL_DIRECTORY)]
);
}
};
@ -484,60 +628,46 @@ let PageThumbsExpiration = {
}
},
expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep, aCallback) {
PageThumbsWorker.postMessage({
type: "expireFilesInDirectory",
minChunkSize: EXPIRATION_MIN_CHUNK_SIZE,
path: PageThumbsStorage.getDirectory().path,
filesToKeep: [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)]
}, aCallback);
expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
let path = this.path;
let keep = [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)];
let msg = [
PageThumbsStorage.path,
keep,
EXPIRATION_MIN_CHUNK_SIZE
];
return PageThumbsWorker.post(
"expireFilesInDirectory",
msg
);
}
};
/**
* 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: [],
/**
* Get the worker, spawning it if necessary.
* Code of the worker is in companion file PageThumbsWorker.js
*/
get _worker() {
delete this._worker;
this._worker = new ChromeWorker("resource://gre/modules/PageThumbsWorker.js");
this._worker.addEventListener("message", this);
return this._worker;
},
/**
* Post a message to the dedicated thread, registering a callback
* to be executed once the reply has been received.
*
* 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 PageThumbsWorker = (function() {
let worker = new PromiseWorker("resource://gre/modules/PageThumbsWorker.js",
OS.Shared.LOG.bind("PageThumbs"));
return {
post: function post(...args) {
let promise = worker.post.apply(worker, args);
return promise.then(
null,
function onError(error) {
// Decode any serialized error
if (error instanceof PromiseWorker.WorkerError) {
throw OS.File.Error.fromMsg(error.data);
} else {
throw error;
}
}
);
}
};
})();
let PageThumbsHistoryObserver = {
onDeleteURI: function Thumbnails_onDeleteURI(aURI, aGUID) {

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

@ -27,6 +27,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"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.
@ -73,8 +75,8 @@ Protocol.prototype = {
*/
newChannel: function Proto_newChannel(aURI) {
let {url} = parseURI(aURI);
let file = PageThumbsStorage.getFileForURL(url);
let fileuri = Services.io.newFileURI(file);
let file = PageThumbsStorage.getFilePathForURL(url);
let fileuri = Services.io.newFileURI(new FileUtils.File(file));
return Services.io.newChannelFromURI(fileuri);
},

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

@ -13,45 +13,62 @@
importScripts("resource://gre/modules/osfile.jsm");
let PageThumbsWorker = {
handleMessage: function Worker_handleMessage(aEvent) {
let msg = aEvent.data;
let data = {result: null, data: null};
let File = OS.File;
let Type = OS.Shared.Type;
switch (msg.type) {
case "removeFile":
data.result = this.removeFile(msg);
break;
case "expireFilesInDirectory":
data.result = this.expireFilesInDirectory(msg);
break;
case "moveOrDeleteAllThumbnails":
data.result = this.moveOrDeleteAllThumbnails(msg);
break;
default:
data.result = false;
data.detail = "message not understood";
break;
}
/**
* Communications with the controller.
*
* Accepts messages:
* {fun:function_name, args:array_of_arguments_or_null}
*
* Sends messages:
* {ok: result} / {fail: serialized_form_of_OS.File.Error}
*/
self.onmessage = function onmessage(msg) {
let data = msg.data;
let id = data.id;
let result;
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 {
OS.File.remove(msg.path);
OS.File.remove(path);
return true;
} catch (e) {
return false;
}
},
expireFilesInDirectory: function Worker_expireFilesInDirectory(msg) {
let entries = this.getFileEntriesInDirectory(msg.path, msg.filesToKeep);
let limit = Math.max(msg.minChunkSize, Math.round(entries.length / 2));
expireFilesInDirectory:
function Agent_expireFilesInDirectory(path, filesToKeep, minChunkSize) {
let entries = this.getFileEntriesInDirectory(path, filesToKeep);
let limit = Math.max(minChunkSize, Math.round(entries.length / 2));
for (let entry of entries) {
this.removeFile(entry);
this.remove(entry.path);
// Check if we reached the limit of files to remove.
if (--limit <= 0) {
@ -63,9 +80,13 @@ let PageThumbsWorker = {
},
getFileEntriesInDirectory:
function Worker_getFileEntriesInDirectory(aPath, aSkipFiles) {
let skip = new Set(aSkipFiles);
let iter = new OS.File.DirectoryIterator(aPath);
function Agent_getFileEntriesInDirectory(path, skipFiles) {
let iter = new OS.File.DirectoryIterator(path);
if (!iter.exists()) {
return [];
}
let skip = new Set(skipFiles);
return [entry
for (entry in iter)
@ -73,36 +94,74 @@ let PageThumbsWorker = {
},
moveOrDeleteAllThumbnails:
function Worker_moveOrDeleteAllThumbnails(msg) {
if (!OS.File.exists(msg.from))
function Agent_moveOrDeleteAllThumbnails(pathFrom, pathTo) {
OS.File.makeDir(pathTo, {ignoreExisting: true});
if (pathFrom == pathTo) {
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 to = OS.Path.join(msg.to, entry.name);
let from = OS.Path.join(pathFrom, entry.name);
let to = OS.Path.join(pathTo, entry.name);
try {
OS.File.move(from, to, {noOverwrite: true, noCopy: true});
} catch (e) {
OS.File.remove(from);
try {
OS.File.move(from, to, {noOverwrite: true, noCopy: true});
} catch (e) {
OS.File.remove(from);
}
}
}
iter.close();
try {
OS.File.removeEmptyDir(msg.from);
OS.File.removeEmptyDir(pathFrom);
} catch (e) {
// This could fail if there's something in
// the folder we're not permitted to remove.
}
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 URL3 = URL + "#3";
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
let tmp = {};
Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader)
@ -18,13 +15,13 @@ const {EXPIRATION_MIN_CHUNK_SIZE, PageThumbsExpiration} = tmp;
function runTests() {
// Create three thumbnails.
createDummyThumbnail(URL1);
yield createDummyThumbnail(URL1);
ok(thumbnailExists(URL1), "first thumbnail created");
createDummyThumbnail(URL2);
yield createDummyThumbnail(URL2);
ok(thumbnailExists(URL2), "second thumbnail created");
createDummyThumbnail(URL3);
yield createDummyThumbnail(URL3);
ok(thumbnailExists(URL3), "third thumbnail created");
// Remove the third thumbnail.
@ -45,10 +42,11 @@ function runTests() {
// Create some more files than the min chunk size.
let urls = [];
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");
// Make sure our dummy thumbnails aren't expired too early.
@ -71,16 +69,30 @@ function runTests() {
}
function createDummyThumbnail(aURL) {
let file = PageThumbsStorage.getFileForURL(aURL);
let fos = FileUtils.openSafeFileOutputStream(file);
let data = "dummy";
fos.write(data, data.length);
FileUtils.closeSafeFileOutputStream(fos);
info("Creating dummy thumbnail for " + aURL);
let dummy = new Uint8Array(10);
for (let i = 0; i < 10; ++i) {
dummy[i] = i;
}
PageThumbsStorage.writeData(aURL, dummy).then(
function onSuccess() {
info("createDummyThumbnail succeeded");
executeSoon(next);
},
function onFailure(error) {
ok(false, "createDummyThumbnail failed " + error);
}
);
}
function expireThumbnails(aKeep) {
PageThumbsExpiration.expireThumbnails(aKeep, function () {
executeSoon(next);
});
PageThumbsExpiration.expireThumbnails(aKeep).then(
function onSuccess() {
info("expireThumbnails succeeded");
executeSoon(next);
},
function onFailure(error) {
ok(false, "expireThumbnails failed " + error);
}
);
}

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

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

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

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

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

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