зеркало из 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";
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Загрузка…
Ссылка в новой задаче