зеркало из https://github.com/mozilla/gecko-dev.git
Bug 753768 - Moving Page Thumbs I/O fully off the main thread;r=ttaubert
This commit is contained in:
Родитель
d2147038eb
Коммит
eec7ada17b
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Загрузка…
Ссылка в новой задаче