Bug 1406181 - Use indexedDB as the backend for storage.local.
This patch integrates the new ExtensionStorageIDB.jsm into the storage API modules
(ext-storage.js and ext-c-storage.js).
## storage.local backend selection
The IDB backend is enabled by a preference ("extensions.webextensions.useExtensStorageIDB")
which is read by the extension when is starting and stored in a property of the internal
extension object, so that it can be used in the main and the child process to ensure that
we keep using the same backend while the extension is running, even if the preference
is switched.
## storage.local data migration
When the new backend is enabled and the extension is started (or restarted), the data
stored in the storage.local file backend is migrated into the IndexedDB backend before
any extension page is loaded.
## storage.local memory usage optimization
When the IDB storage.local backend is enabled, the data stored in the underlying IndexedDB
storage is not in a single file anymore:
- data smaller than a certain threshold is compressed and stored in the sqlite file
- data bigger than the threshold is compressed and stored into separated files
The above is the standard IndexedDB behavior and it should provide better performance
than the simple File backend currently used by the API for certain usage scenarios of
the API (e.g. there is a higher chance that we can handle the storage.local API call
without loading the full file content in memory).
Besides this potential improvements (which are side-effects of using IndexedDB as a
backend), we can still introduce some additional optimization related to the memory usage
of the API in some additional API usage scenarios:
- storage.local.get API call can be fulfilled in ext-c-storage.js, so that we can spare
a good amount of memory by not serialize/deseriale the API call parameters and
results (as well as moving this data between processes using message manager events)
- when there are no storage.onChanged listeners subscribed, we can make
storage.local.set/remove/clear to use a smaller amount of memory by:
- fulfill the API call in the child process if there were no onChanged listeners
subscribed when the API call is starting to process the request
- do not retrieve the previously stored data to be able to sent the onChanged
event details (because we do not need to send any event)
- when processing the API call in the main process (because there were listeners when the
API call was starting to be processed in ext-c-storage.js), we can still try to spare
some memory in ext-storage.js if all the onChanged listeners have been unsubscribed in
the meantime (by not retrieving the previously stored data and not creating the onChanged
event details)
MozReview-Commit-ID: CHbkoqBsOQ7
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4966,16 +4966,18 @@ pref("extensions.webextensions.identity.
// Whether or not webextension themes are supported.
pref("extensions.webextensions.themes.enabled", false);
pref("extensions.webextensions.themes.icons.enabled", false);
pref("extensions.webextensions.remote", false);
// Whether or not the moz-extension resource loads are remoted. For debugging
// purposes only. Setting this to false will break moz-extension URI loading
// unless other process sandboxing and extension remoting prefs are changed.
pref("extensions.webextensions.protocol.remote", true);
+// whether or not the storage.local API should use the IndexedDB backend.
+pref("extensions.webextensions.useExtensionStorageIDB", false);
// Report Site Issue button
pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/issues/new");
#if defined(MOZ_DEV_EDITION) || defined(NIGHTLY_BUILD)
pref("extensions.webcompat-reporter.enabled", true);
#else
pref("extensions.webcompat-reporter.enabled", false);
#endif
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -43,16 +43,17 @@ Cu.import("resource://gre/modules/Servic
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
AppConstants: "resource://gre/modules/AppConstants.jsm",
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
+ ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
FileSource: "resource://gre/modules/L10nRegistry.jsm",
L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
Log: "resource://gre/modules/Log.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
NetUtil: "resource://gre/modules/NetUtil.jsm",
OS: "resource://gre/modules/osfile.jsm",
PluralForm: "resource://gre/modules/PluralForm.jsm",
@@ -92,16 +93,19 @@ const {
EventEmitter,
getUniqueId,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
XPCOMUtils.defineLazyGetter(this, "LocaleData", () => ExtensionCommon.LocaleData);
+const EXT_STORAGE_IDB_PREF = "extensions.webextensions.useExtensionStorageIDB";
+const EXT_STORAGE_IDB_PREF_DEFAULT = false;
+
// The maximum time to wait for extension shutdown blockers to complete.
const SHUTDOWN_BLOCKER_MAX_MS = 8000;
// The list of properties that themes are allowed to contain.
XPCOMUtils.defineLazyGetter(this, "allowedThemeProperties", () => {
Cu.import("resource://gre/modules/ExtensionParent.jsm");
let propertiesInBaseManifest = ExtensionParent.baseManifestProperties;
@@ -243,20 +247,27 @@ var UninstallObserver = {
onUninstalled(addon) {
let uuid = UUIDMap.get(addon.id, false);
if (!uuid) {
return;
}
if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) {
// Clear browser.local.storage
+ // TODO: we should skip the notification (and the related
+ // higher memory usage when clearing the file backend as
+ // we do for the IDB backend).
AsyncShutdown.profileChangeTeardown.addBlocker(
- `Clear Extension Storage ${addon.id}`,
+ `Clear Extension Storage ${addon.id} (File Backend)`,
ExtensionStorage.clear(addon.id));
+ AsyncShutdown.profileChangeTeardown.addBlocker(
+ `Clear Extension Storage ${addon.id} (IDB Backend)`,
+ ExtensionStorageIDB.clear(addon.id, {skipNotify: true}));
+
// Clear any IndexedDB storage created by the extension
let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
let principal = Services.scriptSecurityManager.createCodebasePrincipal(
baseURI, {});
Services.qms.clearStoragesForPrincipal(principal);
// Clear localStorage created by the extension
let storage = Services.domStorageManager.getStorage(null, principal);
@@ -1276,16 +1287,23 @@ this.Extension = class extends Extension
contentScripts: this.contentScripts,
registeredContentScripts: new Map(),
webAccessibleResources: this.webAccessibleResources.map(res => res.glob),
whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern),
localeData: this.localeData.serialize(),
permissions: this.permissions,
principal: this.principal,
optionalPermissions: this.manifest.optional_permissions,
+
+ // Used to preserve the storage.local backend that was active
+ // when the extension has been started for the entire life of the addon
+ // (may change if the addon has been restarted after the preference has been
+ // switched, and the existent storage.local data can be migrated in the new
+ // IndexedDB backend when the addon is restarting).
+ useExtensionStorageIDB: this.useExtensionStorageIDB,
};
}
get contentScripts() {
return this.manifest.content_scripts || [];
}
broadcast(msg, data) {
@@ -1471,16 +1489,23 @@ this.Extension = class extends Extension
GlobalManager.init(this);
this.policy.active = false;
this.policy = processScript.initExtension(this);
this.updatePermissions(this.startupReason);
+ // Before starting the extension, check if the indexedDB backend has been enabled,
+ // and ensure that any storage.local data stored in the file backend has been
+ // migrated to the indexedDB backend.
+ if (this.useExtensionStorageIDB) {
+ await this.migrateStorageLocalData();
+ }
+
// The "startup" Management event sent on the extension instance itself
// is emitted just before the Management "startup" event,
// and it is used to run code that needs to be executed before
// any of the "startup" listeners.
this.emit("startup", this);
Management.emit("startup", this);
await this.runManifest(this.manifest);
@@ -1499,16 +1524,73 @@ this.Extension = class extends Extension
this.cleanupGeneratedFile();
throw e;
}
this.startupPromise = null;
}
+ async migrateStorageLocalData() {
+ if (!this.hasPermission("storage")) {
+ return;
+ }
+
+ let oldStorageFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ oldStorageFile.initWithPath(ExtensionStorage.getStorageFile(this.id));
+
+ if (!oldStorageFile.exists()) {
+ return;
+ }
+
+ const isEmpty = await ExtensionStorageIDB.isEmpty(this.id);
+
+ if (!isEmpty) {
+ // Cancel the data migration is the indexedDB backend for the extension is not empty.
+ return;
+ }
+
+ let jsonFile;
+
+ try {
+ Services.console.logStringMessage(`Migrating storage.local data ${this.id}...`);
+
+ jsonFile = await ExtensionStorage.getFile(this.id);
+ } catch (err) {
+ Cu.reportError(
+ new Error(`Extension error during storage.local data migration for ${this.id}: ` +
+ `${err.message} :: ${err.stack}`)
+ );
+
+ return;
+ }
+
+ let completedWithErrors = false;
+
+ for (let [key, value] of jsonFile.data.entries()) {
+ try {
+ await ExtensionStorageIDB.set(this.id, {[key]: value});
+ } catch (err) {
+ completedWithErrors = true;
+
+ // Report the migration error and continue to migrate the remaining data.
+ Cu.reportError(
+ new Error(`Extension error during storage.local data migration for ${this.id} ` +
+ `on key '${key}': ${err.message} :: ${err.stack}`)
+ );
+ }
+ }
+
+ oldStorageFile.renameTo(null, `${oldStorageFile.leafName}.migrated`);
+
+ const msg = completedWithErrors ? "with errors" : "successfully";
+
+ Services.console.logStringMessage(`Migrating storage.local data ${this.id} completed ${msg}.`);
+ }
+
cleanupGeneratedFile() {
if (!this.cleanupFile) {
return;
}
let file = this.cleanupFile;
this.cleanupFile = null;
@@ -1646,16 +1728,25 @@ this.Extension = class extends Extension
get optionalOrigins() {
if (this._optionalOrigins == null) {
let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm).origin);
this._optionalOrigins = new MatchPatternSet(origins, {ignorePath: true});
}
return this._optionalOrigins;
}
+
+ get useExtensionStorageIDB() {
+ if (this._useExtensionStorageIDB == null) {
+ this._useExtensionStorageIDB = Services.prefs.getBoolPref(EXT_STORAGE_IDB_PREF,
+ EXT_STORAGE_IDB_PREF_DEFAULT);
+ }
+
+ return this._useExtensionStorageIDB;
+ }
};
this.Langpack = class extends ExtensionData {
constructor(addonData, startupReason) {
super(addonData.resourceURI);
this.startupData = addonData.startupData;
this.manifestCacheKey = [addonData.id, addonData.version];
}
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -573,16 +573,20 @@ class BrowserExtensionContent extends Ev
this.principal = data.principal;
this.localeData = new LocaleData(data.localeData);
this.manifest = data.manifest;
this.baseURL = data.baseURL;
this.baseURI = Services.io.newURI(data.baseURL);
+ // Preserve the same storage.local backend choosen by the extension
+ // in the main process.
+ this.useExtensionStorageIDB = data.useExtensionStorageIDB;
+
// Only used in addon processes.
this.views = new Set();
// Only used for devtools views.
this.devtoolsViews = new Set();
/* eslint-disable mozilla/balanced-listeners */
this.on("add-permissions", (ignoreEvent, permissions) => {
--- a/toolkit/components/extensions/ExtensionStorageIDB.jsm
+++ b/toolkit/components/extensions/ExtensionStorageIDB.jsm
@@ -57,47 +57,73 @@ this.ExtensionStorageIDB = {
}
}
await Promise.all(loadedPromises);
return result;
},
+ async isEmpty(extensionId) {
+ const db = await this._open(extensionId);
+ const storedKeys = await this._loadAllStoredKeys(db);
+ await db.close();
+ return storedKeys.length === 0;
+ },
+
/**
* Asynchronously sets the values of the given storage items for the
* given extension.
*
* @param {string} extensionId
* The ID of the extension for which to set storage values.
* @param {object} items
* The storage items to set. For each property in the object,
* the storage value for that property is set to its value in
* said object. Any values which are StructuredCloneHolder
* instances are deserialized before being stored.
+ * @param {object} options
+ * @param {boolean} options.skipNotify
+ * Set to true to skip the onChanged event notification
+ * (and use a lower amount of memory).
* @returns {Promise<void>}
*/
- async set(extensionId, items) {
+ async set(extensionId, items, {skipNotify} = {}) {
const db = await this._open(extensionId);
- const data = await this._loadData(db, Object.keys(items));
+ let data;
+
+ if (!skipNotify) {
+ data = await this._loadData(db, Object.keys(items));
+ }
const changes = {};
const savedPromises = [];
const store = db.objectStore(IDB_DATA_STORENAME, "readwrite");
for (let key in items) {
let value = items[key];
- changes[key] = {oldValue: data[key], newValue: value};
+
+ if (!skipNotify) {
+ changes[key] = {
+ oldValue: data[key],
+ newValue: value,
+ };
+ }
+
savedPromises.push(store.put(value, key));
}
await Promise.all(savedPromises);
- this.notifyListeners(extensionId, changes);
+ await db.close();
+
+ if (!skipNotify) {
+ this.notifyListeners(extensionId, changes);
+ }
return null;
},
/**
* Asynchronously retrieves the values for the given storage items for
* the given extension ID.
*
@@ -126,16 +152,18 @@ this.ExtensionStorageIDB = {
keys = Object.keys(keysParam);
}
const db = await this._open(extensionId);
// Load the required data (everything if keysParam is null).
const data = await this._loadData(db, keys);
+ await db.close();
+
// If keysParam is an object, use the passed value for every non existent key.
if (hasDefaults) {
for (let key of keys) {
if (!(key in data)) {
data[key] = keysParam[key];
}
}
}
@@ -146,75 +174,105 @@ this.ExtensionStorageIDB = {
/**
* Asynchronously removes the given storage items for the given
* extension ID.
*
* @param {string} extensionId
* The ID of the extension for which to remove storage values.
* @param {string|Array<string>} keys
* A string key of a list of storage items keys to remove.
+ * @param {object} options
+ * @param {boolean} options.skipNotify
+ * Set to true to skip the onChanged event notification
+ * (and use a lower amount of memory).
* @returns {Promise<void>}
*/
- async remove(extensionId, keys) {
+ async remove(extensionId, keys, {skipNotify} = {}) {
if (!keys || keys.length === 0) {
return null;
}
// Ensure that keys is an array of strings.
keys = [].concat(keys);
const db = await this._open(extensionId);
- const data = await this._loadData(db, keys);
+ let data;
+
+ if (!skipNotify) {
+ data = await this._loadData(db, keys);
+ }
const store = db.objectStore(IDB_DATA_STORENAME, "readwrite");
let changed = false;
let changes = {};
let promisesChanged = [];
for (let key of keys) {
- if (key in data) {
+ if (skipNotify) {
+ promisesChanged.push(store.delete(key));
+ } else if (key in data) {
changes[key] = {oldValue: data[key]};
promisesChanged.push(store.delete(key));
changed = true;
}
}
- if (changed) {
- await promisesChanged;
+ await promisesChanged;
+
+ await db.close();
+
+ if (!skipNotify && changed) {
this.notifyListeners(extensionId, changes);
}
return null;
},
/**
* Asynchronously clears all storage entries for the given extension
* ID.
*
* @param {string} extensionId
* The ID of the extension for which to clear storage.
+ * @param {object} options
+ * @param {boolean} options.skipNotify
+ * Set to true to skip the onChanged event notification
+ * (and use a lower amount of memory).
* @returns {Promise<void>}
*/
- async clear(extensionId) {
+ async clear(extensionId, {skipNotify} = {}) {
const db = await this._open(extensionId);
- const data = await this._loadData(db);
+
+ let data;
+
+ if (!skipNotify) {
+ data = await this._loadData(db);
+ }
let changed = false;
let changes = {};
+ const store = db.objectStore(IDB_DATA_STORENAME, "readwrite");
+
+ if (skipNotify) {
+ await store.clear();
+ await db.close();
+
+ return null;
+ }
+
for (let key in data) {
changes[key] = {oldValue: data[key]};
changed = true;
}
if (changed) {
- const store = db.objectStore(IDB_DATA_STORENAME, "readwrite");
-
await store.clear();
+ await db.close();
this.notifyListeners(extensionId, changes);
}
return null;
},
// Move these into a shared ExtensionStorageBase class?
@@ -232,9 +290,14 @@ this.ExtensionStorageIDB = {
notifyListeners(extensionId, changes) {
let listeners = this.listeners.get(extensionId);
if (listeners) {
for (let listener of listeners) {
listener(changes);
}
}
},
+
+ hasListeners(extensionId) {
+ let listeners = this.listeners.get(extensionId);
+ return listeners && listeners.size > 0;
+ },
};
--- a/toolkit/components/extensions/ext-c-storage.js
+++ b/toolkit/components/extensions/ext-c-storage.js
@@ -1,21 +1,133 @@
"use strict";
/* import-globals-from ext-c-toolkit.js */
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageIDB",
+ "resource://gre/modules/ExtensionStorageIDB.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
const storageGetHistogram = "WEBEXT_STORAGE_LOCAL_GET_MS";
const storageSetHistogram = "WEBEXT_STORAGE_LOCAL_SET_MS";
this.storage = class extends ExtensionAPI {
+ getLocalFileBackend(context, {deserialize, serialize}) {
+ return {
+ get: async function(keys) {
+ const stopwatchKey = {};
+ TelemetryStopwatch.start(storageGetHistogram, stopwatchKey);
+ try {
+ let result = await context.childManager.callParentAsyncFunction("storage.local.get", [
+ serialize(keys),
+ ]).then(deserialize);
+ TelemetryStopwatch.finish(storageGetHistogram, stopwatchKey);
+ return result;
+ } catch (e) {
+ TelemetryStopwatch.cancel(storageGetHistogram, stopwatchKey);
+ throw e;
+ }
+ },
+ set: async function(items) {
+ const stopwatchKey = {};
+ TelemetryStopwatch.start(storageSetHistogram, stopwatchKey);
+ try {
+ let result = await context.childManager.callParentAsyncFunction("storage.local.set", [
+ serialize(items),
+ ]);
+ TelemetryStopwatch.finish(storageSetHistogram, stopwatchKey);
+ return result;
+ } catch (e) {
+ TelemetryStopwatch.cancel(storageSetHistogram, stopwatchKey);
+ throw e;
+ }
+ },
+ };
+ }
+
+ getLocalIDBBackend(context, {hasParentListeners, serialize}) {
+ return {
+ get: async function(keys) {
+ const stopwatchKey = {};
+ TelemetryStopwatch.start(storageGetHistogram, stopwatchKey);
+ try {
+ let result = await ExtensionStorageIDB.get(context.extension.id, keys);
+
+ TelemetryStopwatch.finish(storageGetHistogram, stopwatchKey);
+ return result;
+ } catch (e) {
+ TelemetryStopwatch.cancel(storageGetHistogram, stopwatchKey);
+ throw e;
+ }
+ },
+ set: async function(items) {
+ const stopwatchKey = {};
+ TelemetryStopwatch.start(storageSetHistogram, stopwatchKey);
+ try {
+ const hasListeners = await hasParentListeners();
+
+ let result;
+
+ if (hasListeners) {
+ result = await context.childManager.callParentAsyncFunction("storage.local.set", [
+ serialize(items),
+ ]);
+ } else {
+ result = await ExtensionStorageIDB.set(context.extension.id, items, {
+ skipNotify: true,
+ });
+ }
+
+ TelemetryStopwatch.finish(storageSetHistogram, stopwatchKey);
+ return result;
+ } catch (e) {
+ TelemetryStopwatch.cancel(storageSetHistogram, stopwatchKey);
+ throw e;
+ }
+ },
+ remove: async function(keys) {
+ // TODO: we should also collect telemetry on storage.local.remove.
+ const hasListeners = await hasParentListeners();
+
+ let result;
+
+ if (hasListeners) {
+ result = await context.childManager.callParentAsyncFunction("storage.local.remove", [
+ serialize(keys),
+ ]);
+ } else {
+ result = await ExtensionStorageIDB.remove(context.extension.id, keys, {
+ skipNotify: true,
+ });
+ }
+
+ return result;
+ },
+ clear: async function() {
+ // TODO: we should also collect telemetry on storage.local.remove.
+ const hasListeners = await hasParentListeners();
+
+ let result;
+
+ if (hasListeners) {
+ result = await context.childManager.callParentAsyncFunction("storage.local.clear", []);
+ } else {
+ result = await ExtensionStorageIDB.clear(context.extension.id, {
+ skipNotify: true,
+ });
+ }
+
+ return result;
+ },
+ };
+ }
+
getAPI(context) {
const serialize = ExtensionStorage.serializeForContext.bind(null, context);
const deserialize = ExtensionStorage.deserializeForContext.bind(null, context);
function sanitize(items) {
// The schema validator already takes care of arrays (which are only allowed
// to contain strings). Strings and null are safe values.
if (typeof items != "object" || items === null || Array.isArray(items)) {
@@ -30,48 +142,32 @@ this.storage = class extends ExtensionAP
// So we enumerate all properties and sanitize each value individually.
let sanitized = {};
for (let [key, value] of Object.entries(items)) {
sanitized[key] = ExtensionStorage.sanitize(value, context);
}
return sanitized;
}
+ function hasParentListeners() {
+ // We spare a good amount of memory if there are no listeners around
+ // (e.g. because they have never been subscribed or they have been removed
+ // in the meantime).
+ return context.childManager.callParentAsyncFunction(
+ "storage.hasListeners", []
+ );
+ }
+
+ const localAPI = context.extension.useExtensionStorageIDB ?
+ this.getLocalIDBBackend(context, {hasParentListeners, serialize}) :
+ this.getLocalFileBackend(context, {deserialize, serialize});
+
return {
storage: {
- local: {
- get: async function(keys) {
- const stopwatchKey = {};
- TelemetryStopwatch.start(storageGetHistogram, stopwatchKey);
- try {
- let result = await context.childManager.callParentAsyncFunction("storage.local.get", [
- serialize(keys),
- ]).then(deserialize);
- TelemetryStopwatch.finish(storageGetHistogram, stopwatchKey);
- return result;
- } catch (e) {
- TelemetryStopwatch.cancel(storageGetHistogram, stopwatchKey);
- throw e;
- }
- },
- set: async function(items) {
- const stopwatchKey = {};
- TelemetryStopwatch.start(storageSetHistogram, stopwatchKey);
- try {
- let result = await context.childManager.callParentAsyncFunction("storage.local.set", [
- serialize(items),
- ]);
- TelemetryStopwatch.finish(storageSetHistogram, stopwatchKey);
- return result;
- } catch (e) {
- TelemetryStopwatch.cancel(storageSetHistogram, stopwatchKey);
- throw e;
- }
- },
- },
+ local: localAPI,
sync: {
get: function(keys) {
keys = sanitize(keys);
return context.childManager.callParentAsyncFunction("storage.sync.get", [
keys,
]);
},
--- a/toolkit/components/extensions/ext-storage.js
+++ b/toolkit/components/extensions/ext-storage.js
@@ -1,16 +1,18 @@
"use strict";
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-toolkit.js */
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
+ ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.jsm",
NativeManifests: "resource://gre/modules/NativeManifests.jsm",
});
var {
ExtensionError,
} = ExtensionUtils;
@@ -31,34 +33,104 @@ const lookupManagedStorage = async (exte
let info = await NativeManifests.lookupManifest("storage", extensionId, context);
if (info) {
return ExtensionStorage._serializableMap(info.manifest.data);
}
return null;
};
this.storage = class extends ExtensionAPI {
+ getLocalFileBackend(context) {
+ const {extension} = context;
+
+ return {
+ get: function(spec) {
+ return ExtensionStorage.get(extension.id, spec);
+ },
+ set: function(items) {
+ return ExtensionStorage.set(extension.id, items);
+ },
+ remove: function(keys) {
+ return ExtensionStorage.remove(extension.id, keys);
+ },
+ clear: function() {
+ return ExtensionStorage.clear(extension.id);
+ },
+ };
+ }
+
+ getLocalIDBBackend(context, {deserialize, hasListeners}) {
+ const {extension} = context;
+
+ return {
+ set: function(items) {
+ const setDataPromise = ExtensionStorageIDB.set(extension.id, deserialize(items), {
+ // We can still spare a good amount of memory if there are no listeners around
+ // (e.g. because they have been removed in the meantime).
+ skipNotify: !hasListeners(),
+ });
+
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ `Extension Storage IDB: Wait for extension ${extension.id} to save data`,
+ setDataPromise);
+
+ return setDataPromise;
+ },
+ remove: function(keys) {
+ const removeDataPromise = ExtensionStorageIDB.remove(extension.id, keys, {
+ // We can still spare a good amount of memory if there are no listeners around
+ // (e.g. because they have been removed in the meantime).
+ skipNotify: !hasListeners(),
+ });
+
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ `Extension Storage IDB: Wait for extension ${extension.id} to save data`,
+ removeDataPromise);
+
+ return removeDataPromise;
+ },
+ clear: function() {
+ const clearDataPromise = ExtensionStorageIDB.clear(extension.id, {
+ // We can still spare a good amount of memory if there are no listeners around
+ // (e.g. because they have been removed in the meantime).
+ skipNotify: !hasListeners(),
+ });
+
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ `Extension Storage IDB: Wait for extension ${extension.id} to save data`,
+ clearDataPromise);
+
+ return clearDataPromise;
+ },
+ };
+ }
+
getAPI(context) {
let {extension} = context;
+
+ // A StructuredCloneHolder object cannot be stored in IndexedDB like it is,
+ // we need to deserialize it.
+ const deserialize = ExtensionStorage.deserializeForContext.bind(null, context);
+
+ function hasListeners() {
+ return ExtensionStorageIDB.hasListeners(context.extension.id);
+ }
+
+ const localAPI = extension.useExtensionStorageIDB ?
+ this.getLocalIDBBackend(context, {deserialize, hasListeners}) :
+ this.getLocalFileBackend(context);
+
return {
storage: {
- local: {
- get: function(spec) {
- return ExtensionStorage.get(extension.id, spec);
- },
- set: function(items) {
- return ExtensionStorage.set(extension.id, items);
- },
- remove: function(keys) {
- return ExtensionStorage.remove(extension.id, keys);
- },
- clear: function() {
- return ExtensionStorage.clear(extension.id);
- },
- },
+ // Used in ext-c-storage.js, if there are no onChanged listeners subscribed
+ // there will no need to exchange the data between the child and main process
+ // (which will help to use less memory).
+ hasListeners,
+
+ local: localAPI,
sync: {
get: function(spec) {
enforceNoTemporaryAddon(extension.id);
return extensionStorageSync.get(extension, spec, context);
},
set: function(items) {
enforceNoTemporaryAddon(extension.id);
@@ -95,19 +167,27 @@ this.storage = class extends ExtensionAP
onChanged: new EventManager(context, "storage.onChanged", fire => {
let listenerLocal = changes => {
fire.raw(changes, "local");
};
let listenerSync = changes => {
fire.async(changes, "sync");
};
- ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
+ if (context.extension.useExtensionStorageIDB) {
+ ExtensionStorageIDB.addOnChangedListener(extension.id, listenerLocal);
+ } else {
+ ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
+ }
extensionStorageSync.addOnChangedListener(extension, listenerSync, context);
return () => {
- ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
+ if (context.extension.useExtensionStorageIDB) {
+ ExtensionStorageIDB.removeOnChangedListener(extension.id, listenerLocal);
+ } else {
+ ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
+ }
extensionStorageSync.removeOnChangedListener(extension, listenerSync);
};
}).api(),
},
};
}
};