Bug 1358846: Part 4 - Merge various startup information stores into a single JSON file. r=rhelmer,jonco
authorKris Maglione <maglione.k@gmail.com>
Fri, 12 May 2017 09:37:50 -0700
changeset 358140 b21481adbdca5fe92b21639ef28a3e97be0f70bd
parent 358139 bf1662f1893440f6ddb391480fd19e715e9fb727
child 358141 cf7f79a7dbfc4d5b34f8d3d7f70562d280b4cf97
push id90273
push usermaglione.k@gmail.com
push dateFri, 12 May 2017 21:30:29 +0000
treeherdermozilla-inbound@243eb19366c6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrhelmer, jonco
bugs1358846
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1358846: Part 4 - Merge various startup information stores into a single JSON file. r=rhelmer,jonco MozReview-Commit-ID: Bs8xMqzVOcl
toolkit/components/build/nsToolkitCompsModule.cpp
toolkit/mozapps/extensions/AddonManagerStartup-inlines.h
toolkit/mozapps/extensions/AddonManagerStartup.cpp
toolkit/mozapps/extensions/AddonManagerStartup.h
toolkit/mozapps/extensions/amIAddonManagerStartup.idl
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/moz.build
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
toolkit/mozapps/extensions/test/xpcshell/test_bug563256.js
toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js
toolkit/mozapps/extensions/test/xpcshell/test_bug595573.js
toolkit/mozapps/extensions/test/xpcshell/test_bug655254.js
toolkit/mozapps/extensions/test/xpcshell/test_bug659772.js
toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js
toolkit/mozapps/extensions/test/xpcshell/test_disable.js
toolkit/mozapps/extensions/test/xpcshell/test_experiment.js
toolkit/mozapps/extensions/test/xpcshell/test_hasbinarycomponents.js
toolkit/mozapps/extensions/test/xpcshell/test_install.js
toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
toolkit/mozapps/extensions/test/xpcshell/test_locked.js
toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
toolkit/mozapps/extensions/test/xpcshell/test_migrate_state_prefs.js
toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_safemode.js
toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js
toolkit/mozapps/extensions/test/xpcshell/test_startup.js
toolkit/mozapps/extensions/test/xpcshell/test_targetPlatforms.js
toolkit/mozapps/extensions/test/xpcshell/test_theme.js
toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js
toolkit/mozapps/extensions/test/xpcshell/test_update.js
toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js
toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
toolkit/mozapps/extensions/test/xpcshell/test_upgrade_strictcompat.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
toolkit/xre/nsXREDirProvider.cpp
toolkit/xre/nsXREDirProvider.h
--- a/toolkit/components/build/nsToolkitCompsModule.cpp
+++ b/toolkit/components/build/nsToolkitCompsModule.cpp
@@ -31,16 +31,17 @@
 #include "nsUrlClassifierStreamUpdater.h"
 #include "nsUrlClassifierUtils.h"
 #include "nsUrlClassifierPrefixSet.h"
 
 #include "nsBrowserStatusFilter.h"
 #include "mozilla/FinalizationWitnessService.h"
 #include "mozilla/NativeOSFileInternals.h"
 #include "mozilla/AddonContentPolicy.h"
+#include "mozilla/AddonManagerStartup.h"
 #include "mozilla/AddonPathService.h"
 
 #if defined(XP_WIN)
 #include "NativeFileWatcherWin.h"
 #else
 #include "NativeFileWatcherNotSupported.h"
 #endif // (XP_WIN)
 
@@ -121,16 +122,17 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsBrowser
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateProcessor)
 #endif
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(FinalizationWitnessService, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(NativeOSFileInternalsService)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init)
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(AddonContentPolicy)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonManagerStartup, AddonManagerStartup::GetInstance)
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebRequestListener)
 
 NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
 #if defined(MOZ_HAS_PERFSTATS)
 NS_DEFINE_NAMED_CID(NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID);
 #endif // defined (MOZ_HAS_PERFSTATS)
 
@@ -156,16 +158,17 @@ NS_DEFINE_NAMED_CID(NS_URLCLASSIFIERUTIL
 NS_DEFINE_NAMED_CID(NS_BROWSERSTATUSFILTER_CID);
 #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
 NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
 #endif
 NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID);
 NS_DEFINE_NAMED_CID(NATIVE_OSFILE_INTERNALS_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_ADDONCONTENTPOLICY_CID);
 NS_DEFINE_NAMED_CID(NS_ADDON_PATH_SERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_ADDON_MANAGER_STARTUP_CID);
 NS_DEFINE_NAMED_CID(NATIVE_FILEWATCHER_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_WEBREQUESTLISTENER_CID);
 
 static const Module::CIDEntry kToolkitCIDs[] = {
   { &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor },
 #if defined(MOZ_HAS_TERMINATOR)
   { &kNS_TOOLKIT_TERMINATOR_CID, false, nullptr, nsTerminatorConstructor },
 #endif
@@ -191,16 +194,17 @@ static const Module::CIDEntry kToolkitCI
   { &kNS_BROWSERSTATUSFILTER_CID, false, nullptr, nsBrowserStatusFilterConstructor },
 #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
   { &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor },
 #endif
   { &kFINALIZATIONWITNESSSERVICE_CID, false, nullptr, FinalizationWitnessServiceConstructor },
   { &kNATIVE_OSFILE_INTERNALS_SERVICE_CID, false, nullptr, NativeOSFileInternalsServiceConstructor },
   { &kNS_ADDONCONTENTPOLICY_CID, false, nullptr, AddonContentPolicyConstructor },
   { &kNS_ADDON_PATH_SERVICE_CID, false, nullptr, AddonPathServiceConstructor },
+  { &kNS_ADDON_MANAGER_STARTUP_CID, false, nullptr, AddonManagerStartupConstructor },
   { &kNATIVE_FILEWATCHER_SERVICE_CID, false, nullptr, NativeFileWatcherServiceConstructor },
   { &kNS_WEBREQUESTLISTENER_CID, false, nullptr, nsWebRequestListenerConstructor },
   { nullptr }
 };
 
 static const Module::ContractIDEntry kToolkitContracts[] = {
   { NS_APPSTARTUP_CONTRACTID, &kNS_TOOLKIT_APPSTARTUP_CID },
 #if defined(MOZ_HAS_TERMINATOR)
@@ -228,16 +232,17 @@ static const Module::ContractIDEntry kTo
   { NS_BROWSERSTATUSFILTER_CONTRACTID, &kNS_BROWSERSTATUSFILTER_CID },
 #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
   { NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID },
 #endif
   { FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID },
   { NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID, &kNATIVE_OSFILE_INTERNALS_SERVICE_CID },
   { NS_ADDONCONTENTPOLICY_CONTRACTID, &kNS_ADDONCONTENTPOLICY_CID },
   { NS_ADDONPATHSERVICE_CONTRACTID, &kNS_ADDON_PATH_SERVICE_CID },
+  { NS_ADDONMANAGERSTARTUP_CONTRACTID, &kNS_ADDON_MANAGER_STARTUP_CID },
   { NATIVE_FILEWATCHER_SERVICE_CONTRACTID, &kNATIVE_FILEWATCHER_SERVICE_CID },
   { NS_WEBREQUESTLISTENER_CONTRACTID, &kNS_WEBREQUESTLISTENER_CID },
   { nullptr }
 };
 
 static const mozilla::Module::CategoryEntry kToolkitCategories[] = {
   { "content-policy", NS_ADDONCONTENTPOLICY_CONTRACTID, NS_ADDONCONTENTPOLICY_CONTRACTID },
   { nullptr }
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerStartup-inlines.h
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AddonManagerStartup_inlines_h
+#define AddonManagerStartup_inlines_h
+
+#include "jsapi.h"
+#include "nsJSUtils.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Move.h"
+
+namespace mozilla {
+
+class ArrayIterElem;
+class PropertyIterElem;
+
+
+/*****************************************************************************
+ * Object iterator base classes
+ *****************************************************************************/
+
+template<class T, class PropertyType>
+class MOZ_STACK_CLASS BaseIter {
+public:
+  typedef T SelfType;
+
+  PropertyType begin() const
+  {
+    PropertyType elem(Self());
+    return Move(elem);
+  }
+
+  PropertyType end() const
+  {
+    PropertyType elem(Self());
+    return elem.End();
+  }
+
+  void* Context() const { return mContext; }
+
+protected:
+  BaseIter(JSContext* cx, JS::HandleObject object, void* context = nullptr)
+    : mCx(cx)
+    , mObject(object)
+    , mContext(context)
+  {}
+
+  const SelfType& Self() const
+  {
+    return *static_cast<const SelfType*>(this);
+  }
+  SelfType& Self()
+  {
+    return *static_cast<SelfType*>(this);
+  }
+
+  JSContext* mCx;
+
+  JS::HandleObject mObject;
+
+  void* mContext;
+};
+
+template<class T, class IterType>
+class MOZ_STACK_CLASS BaseIterElem {
+public:
+  typedef T SelfType;
+
+  explicit BaseIterElem(const IterType& iter, uint32_t index = 0)
+    : mIter(iter)
+    , mIndex(index)
+  {}
+
+  uint32_t Length() const
+  {
+    return mIter.Length();
+  }
+
+  JS::Value Value()
+  {
+    JS::RootedValue value(mIter.mCx, JS::UndefinedValue());
+
+    auto& self = Self();
+    if (!self.GetValue(&value)) {
+      JS_ClearPendingException(mIter.mCx);
+    }
+
+    return value;
+  }
+
+  SelfType& operator*() { return Self(); }
+
+  SelfType& operator++()
+  {
+    MOZ_ASSERT(mIndex < Length());
+    mIndex++;
+    return Self();
+  }
+
+  bool operator!=(const SelfType& other) const
+  {
+    return &mIter != &other.mIter || mIndex != other.mIndex;
+  }
+
+
+  SelfType End() const
+  {
+    SelfType end(mIter);
+    end.mIndex = Length();
+    return Move(end);
+  }
+
+  void* Context() const { return mIter.Context(); }
+
+protected:
+  const SelfType& Self() const
+  {
+    return *static_cast<const SelfType*>(this);
+  }
+  SelfType& Self() {
+    return *static_cast<SelfType*>(this);
+  }
+
+  const IterType& mIter;
+
+  uint32_t mIndex;
+};
+
+
+/*****************************************************************************
+ * Property iteration
+ *****************************************************************************/
+
+class MOZ_STACK_CLASS PropertyIter
+  : public BaseIter<PropertyIter, PropertyIterElem>
+{
+  friend class PropertyIterElem;
+  friend class BaseIterElem<PropertyIterElem, PropertyIter>;
+
+public:
+  PropertyIter(JSContext* cx, JS::HandleObject object, void* context = nullptr)
+    : BaseIter(cx, object, context)
+    , mIds(cx, JS::IdVector(cx))
+  {
+    if (!JS_Enumerate(cx, object, &mIds)) {
+      JS_ClearPendingException(cx);
+    }
+  }
+
+  PropertyIter(const PropertyIter& other)
+    : PropertyIter(other.mCx, other.mObject, other.mContext)
+  {}
+
+  PropertyIter& operator=(const PropertyIter& other)
+  {
+    MOZ_ASSERT(other.mObject == mObject);
+    mCx = other.mCx;
+    mContext = other.mContext;
+
+    mIds.clear();
+    if (!JS_Enumerate(mCx, mObject, &mIds)) {
+      JS_ClearPendingException(mCx);
+    }
+    return *this;
+  }
+
+  int32_t Length() const
+  {
+    return mIds.length();
+  }
+
+protected:
+  JS::Rooted<JS::IdVector> mIds;
+};
+
+class MOZ_STACK_CLASS PropertyIterElem
+  : public BaseIterElem<PropertyIterElem, PropertyIter>
+{
+  friend class BaseIterElem<PropertyIterElem, PropertyIter>;
+
+public:
+  using BaseIterElem::BaseIterElem;
+
+  PropertyIterElem(const PropertyIterElem& other)
+    : BaseIterElem(other.mIter, other.mIndex)
+  {}
+
+  jsid Id()
+  {
+    MOZ_ASSERT(mIndex < mIter.mIds.length());
+
+    return mIter.mIds[mIndex];
+  }
+
+  const nsAString& Name()
+  {
+    if(mName.isNothing()) {
+      mName.emplace();
+      mName.ref().init(mIter.mCx, Id());
+    }
+    return mName.ref();
+  }
+
+  JSContext* Cx() { return mIter.mCx; }
+
+protected:
+  bool GetValue(JS::MutableHandleValue value)
+  {
+    MOZ_ASSERT(mIndex < Length());
+    JS::Rooted<jsid> id(mIter.mCx, Id());
+
+    return JS_GetPropertyById(mIter.mCx, mIter.mObject, id, value);
+  }
+
+private:
+  Maybe<nsAutoJSString> mName;
+};
+
+
+/*****************************************************************************
+ * Array iteration
+ *****************************************************************************/
+
+class MOZ_STACK_CLASS ArrayIter
+  : public BaseIter<ArrayIter, ArrayIterElem>
+{
+  friend class ArrayIterElem;
+  friend class BaseIterElem<ArrayIterElem, ArrayIter>;
+
+public:
+  ArrayIter(JSContext* cx, JS::HandleObject object)
+    : BaseIter(cx, object)
+    , mLength(0)
+  {
+    bool isArray;
+    if (!JS_IsArrayObject(cx, object, &isArray) || !isArray) {
+      JS_ClearPendingException(cx);
+      return;
+    }
+
+    if (!JS_GetArrayLength(cx, object, &mLength)) {
+      JS_ClearPendingException(cx);
+    }
+  }
+
+  uint32_t Length() const
+  {
+    return mLength;
+  }
+
+private:
+  uint32_t mLength;
+};
+
+class MOZ_STACK_CLASS ArrayIterElem
+  : public BaseIterElem<ArrayIterElem, ArrayIter>
+{
+  friend class BaseIterElem<ArrayIterElem, ArrayIter>;
+
+public:
+  using BaseIterElem::BaseIterElem;
+
+  ArrayIterElem(const ArrayIterElem& other)
+    : BaseIterElem(other.mIter, other.mIndex)
+  {}
+
+protected:
+  bool
+  GetValue(JS::MutableHandleValue value)
+  {
+    MOZ_ASSERT(mIndex < Length());
+    return JS_GetElement(mIter.mCx, mIter.mObject, mIndex, value);
+  }
+};
+
+}
+
+#endif // AddonManagerStartup_inlines_h
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
@@ -0,0 +1,566 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AddonManagerStartup.h"
+#include "AddonManagerStartup-inlines.h"
+
+#include "jsapi.h"
+#include "js/TracingAPI.h"
+#include "xpcpublic.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/Compression.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/Unused.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsAppRunner.h"
+#include "nsIAddonInterposition.h"
+#include "nsXULAppAPI.h"
+
+#include <stdlib.h>
+
+namespace mozilla {
+
+using Compression::LZ4;
+
+#ifdef XP_WIN
+#  define READ_BINARYMODE "rb"
+#else
+#  define READ_BINARYMODE "r"
+#endif
+
+AddonManagerStartup&
+AddonManagerStartup::GetSingleton()
+{
+  static RefPtr<AddonManagerStartup> singleton;
+  if (!singleton) {
+    singleton = new AddonManagerStartup();
+    ClearOnShutdown(&singleton);
+  }
+  return *singleton;
+}
+
+AddonManagerStartup::AddonManagerStartup()
+  : mInitialized(false)
+{}
+
+
+nsIFile*
+AddonManagerStartup::ProfileDir()
+{
+  if (!mProfileDir) {
+    nsresult rv;
+
+    rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mProfileDir));
+    MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+  }
+
+  return mProfileDir;
+}
+
+NS_IMPL_ISUPPORTS(AddonManagerStartup, amIAddonManagerStartup)
+
+
+/*****************************************************************************
+ * File utils
+ *****************************************************************************/
+
+static already_AddRefed<nsIFile>
+CloneAndAppend(nsIFile* aFile, const char* name)
+{
+  nsCOMPtr<nsIFile> file;
+  aFile->Clone(getter_AddRefs(file));
+  file->AppendNative(nsDependentCString(name));
+  return file.forget();
+}
+
+static bool
+IsNormalFile(nsIFile* file)
+{
+  bool result;
+  return NS_SUCCEEDED(file->IsFile(&result)) && result;
+}
+
+static nsCString
+ReadFile(const char* path)
+{
+  nsCString result;
+
+  FILE* fd = fopen(path, READ_BINARYMODE);
+  if (!fd) {
+    return result;
+  }
+  auto cleanup = MakeScopeExit([&] () {
+    fclose(fd);
+  });
+
+  if (fseek(fd, 0, SEEK_END) != 0) {
+    return result;
+  }
+  size_t len = ftell(fd);
+  if (len <= 0 || fseek(fd, 0, SEEK_SET) != 0) {
+    return result;
+  }
+
+  result.SetLength(len);
+  size_t rd = fread(result.BeginWriting(), sizeof(char), len, fd);
+  if (rd != len) {
+    result.Truncate();
+  }
+
+  return result;
+}
+
+/**
+ * Reads the contents of a LZ4-compressed file, as stored by the OS.File
+ * module, and stores the decompressed contents in result.
+ *
+ * Returns true on success, or false on failure. A nonexistent or empty file
+ * is treated as success. A corrupt or non-LZ4 file is treated as failure.
+ */
+static bool
+ReadFileLZ4(const char* path, nsCString& result)
+{
+  static const char MAGIC_NUMBER[] = "mozLz40";
+  constexpr auto HEADER_SIZE = sizeof(MAGIC_NUMBER) + 4;
+
+  nsCString lz4 = ReadFile(path);
+  if (lz4.IsEmpty()) {
+    result.Truncate();
+    return true;
+  }
+
+  // Note: We want to include the null terminator here.
+  nsDependentCSubstring magic(MAGIC_NUMBER, sizeof(MAGIC_NUMBER));
+
+  if (lz4.Length() < HEADER_SIZE || StringHead(lz4, magic.Length()) != magic) {
+    return false;
+  }
+
+  auto size = LittleEndian::readUint32(lz4.get() + magic.Length());
+
+  if (!result.SetLength(size, fallible) ||
+      !LZ4::decompress(lz4.get() + HEADER_SIZE, result.BeginWriting(), size)) {
+    result.Truncate();
+    return false;
+  }
+
+  return true;
+}
+
+
+static bool
+ParseJSON(JSContext* cx, nsACString& jsonData, JS::MutableHandleValue result)
+{
+  NS_ConvertUTF8toUTF16 str(jsonData);
+  jsonData.Truncate();
+
+  return JS_ParseJSON(cx, str.Data(), str.Length(), result);
+}
+
+
+/*****************************************************************************
+ * JSON data handling
+ *****************************************************************************/
+
+class MOZ_STACK_CLASS WrapperBase {
+protected:
+  WrapperBase(JSContext* cx, JSObject* object)
+    : mCx(cx)
+    , mObject(cx, object)
+  {}
+
+  WrapperBase(JSContext* cx, const JS::Value& value)
+    : mCx(cx)
+    , mObject(cx)
+  {
+    if (value.isObject()) {
+      mObject = &value.toObject();
+    } else {
+      mObject = JS_NewPlainObject(cx);
+    }
+  }
+
+protected:
+  JSContext* mCx;
+  JS::RootedObject mObject;
+
+  bool GetBool(const char* name, bool defVal = false);
+
+  double GetNumber(const char* name, double defVal = 0);
+
+  nsString GetString(const char* name, const char* defVal = "");
+
+  JSObject* GetObject(const char* name);
+};
+
+bool
+WrapperBase::GetBool(const char* name, bool defVal)
+{
+  JS::RootedObject obj(mCx, mObject);
+
+  JS::RootedValue val(mCx, JS::UndefinedValue());
+  if (!JS_GetProperty(mCx, obj, name, &val)) {
+    JS_ClearPendingException(mCx);
+  }
+
+  if (val.isBoolean()) {
+    return val.toBoolean();
+  }
+  return defVal;
+}
+
+double
+WrapperBase::GetNumber(const char* name, double defVal)
+{
+  JS::RootedObject obj(mCx, mObject);
+
+  JS::RootedValue val(mCx, JS::UndefinedValue());
+  if (!JS_GetProperty(mCx, obj, name, &val)) {
+    JS_ClearPendingException(mCx);
+  }
+
+  if (val.isNumber()) {
+    return val.toNumber();
+  }
+  return defVal;
+}
+
+nsString
+WrapperBase::GetString(const char* name, const char* defVal)
+{
+  JS::RootedObject obj(mCx, mObject);
+
+  JS::RootedValue val(mCx, JS::UndefinedValue());
+  if (!JS_GetProperty(mCx, obj, name, &val)) {
+    JS_ClearPendingException(mCx);
+  }
+
+  nsString res;
+  if (val.isString()) {
+    AssignJSString(mCx, res, val.toString());
+  } else {
+    res.AppendASCII(defVal);
+  }
+  return res;
+}
+
+JSObject*
+WrapperBase::GetObject(const char* name)
+{
+  JS::RootedObject obj(mCx, mObject);
+
+  JS::RootedValue val(mCx, JS::UndefinedValue());
+  if (!JS_GetProperty(mCx, obj, name, &val)) {
+    JS_ClearPendingException(mCx);
+  }
+
+  if (val.isObject()) {
+    return &val.toObject();
+  }
+  return nullptr;
+}
+
+
+class MOZ_STACK_CLASS InstallLocation : public WrapperBase {
+public:
+  InstallLocation(JSContext* cx, const JS::Value& value);
+
+  MOZ_IMPLICIT InstallLocation(PropertyIterElem& iter)
+    : InstallLocation(iter.Cx(), iter.Value())
+  {}
+
+  InstallLocation(const InstallLocation& other)
+    : InstallLocation(other.mCx, JS::ObjectValue(*other.mObject))
+  {}
+
+  void SetChanged(bool changed)
+  {
+    JS::RootedObject obj(mCx, mObject);
+
+    JS::RootedValue val(mCx, JS::BooleanValue(changed));
+    if (!JS_SetProperty(mCx, obj, "changed", val)) {
+      JS_ClearPendingException(mCx);
+    }
+  }
+
+  PropertyIter& Addons() { return mAddonsIter.ref(); }
+
+  nsString Path() { return GetString("path"); }
+
+  bool ShouldCheckStartupModifications() { return GetBool("checkStartupModifications"); }
+
+
+private:
+  JS::RootedObject mAddonsObj;
+  Maybe<PropertyIter> mAddonsIter;
+};
+
+
+class MOZ_STACK_CLASS Addon : public WrapperBase {
+public:
+  Addon(JSContext* cx, InstallLocation& location, const nsAString& id, JSObject* object)
+    : WrapperBase(cx, object)
+    , mId(id)
+    , mLocation(location)
+  {}
+
+  MOZ_IMPLICIT Addon(PropertyIterElem& iter)
+    : WrapperBase(iter.Cx(), iter.Value())
+    , mId(iter.Name())
+    , mLocation(*static_cast<InstallLocation*>(iter.Context()))
+  {}
+
+  Addon(const Addon& other)
+    : WrapperBase(other.mCx, other.mObject)
+    , mId(other.mId)
+    , mLocation(other.mLocation)
+  {}
+
+  const nsString& Id() { return mId; }
+
+  nsString Path() { return GetString("path"); }
+
+  bool Bootstrapped() { return GetBool("bootstrapped"); }
+
+  bool Enabled() { return GetBool("enabled"); }
+
+  bool ShimsEnabled() { return GetBool("enableShims"); }
+
+  double LastModifiedTime() { return GetNumber("lastModifiedTime"); }
+
+
+  already_AddRefed<nsIFile> FullPath();
+
+  NSLocationType LocationType();
+
+  bool UpdateLastModifiedTime();
+
+
+private:
+  nsString mId;
+  InstallLocation& mLocation;
+};
+
+already_AddRefed<nsIFile>
+Addon::FullPath()
+{
+  nsString path = mLocation.Path();
+
+  nsCOMPtr<nsIFile> file;
+  NS_NewLocalFile(path, false, getter_AddRefs(file));
+  MOZ_RELEASE_ASSERT(file);
+
+  path = Path();
+  file->AppendRelativePath(path);
+
+  return file.forget();
+}
+
+NSLocationType
+Addon::LocationType()
+{
+  nsString type = GetString("type", "extension");
+  if (type.LowerCaseEqualsLiteral("theme")) {
+    return NS_SKIN_LOCATION;
+  }
+  return NS_EXTENSION_LOCATION;
+}
+
+bool
+Addon::UpdateLastModifiedTime()
+{
+  nsCOMPtr<nsIFile> file = FullPath();
+
+  bool result;
+  if (NS_FAILED(file->Exists(&result)) || !result) {
+    return true;
+  }
+
+  PRTime time;
+
+  nsCOMPtr<nsIFile> manifest = file;
+  if (!IsNormalFile(manifest)) {
+    manifest = CloneAndAppend(file, "install.rdf");
+    if (!IsNormalFile(manifest)) {
+      manifest = CloneAndAppend(file, "manifest.json");
+      if (!IsNormalFile(manifest)) {
+        return true;
+      }
+    }
+  }
+
+  if (NS_FAILED(manifest->GetLastModifiedTime(&time))) {
+    return true;
+  }
+
+  JS::RootedObject obj(mCx, mObject);
+
+  double lastModified = time;
+  JS::RootedValue value(mCx, JS::NumberValue(lastModified));
+  if (!JS_SetProperty(mCx, obj, "currentModifiedTime", value)) {
+    JS_ClearPendingException(mCx);
+  }
+
+  return lastModified != LastModifiedTime();;
+}
+
+
+InstallLocation::InstallLocation(JSContext* cx, const JS::Value& value)
+  : WrapperBase(cx, value)
+  , mAddonsObj(cx)
+  , mAddonsIter()
+{
+  mAddonsObj = GetObject("addons");
+  if (!mAddonsObj) {
+    mAddonsObj = JS_NewPlainObject(cx);
+  }
+  mAddonsIter.emplace(cx, mAddonsObj, this);
+}
+
+
+/*****************************************************************************
+ * XPC interfacing
+ *****************************************************************************/
+
+static void
+EnableShims(const nsAString& addonId)
+{
+  NS_ConvertUTF16toUTF8 id(addonId);
+
+  nsCOMPtr<nsIAddonInterposition> interposition =
+     do_GetService("@mozilla.org/addons/multiprocess-shims;1");
+
+  if (!interposition || !xpc::SetAddonInterposition(id, interposition)) {
+    return;
+  }
+
+  Unused << xpc::AllowCPOWsInAddon(id, true);
+}
+
+void
+AddonManagerStartup::AddInstallLocation(Addon& addon)
+{
+  nsCOMPtr<nsIFile> file = addon.FullPath();
+
+  nsString path;
+  if (NS_FAILED(file->GetPath(path))) {
+    return;
+  }
+
+  auto type = addon.LocationType();
+
+  if (type == NS_SKIN_LOCATION) {
+    mThemePaths.AppendElement(file);
+  } else {
+    mExtensionPaths.AppendElement(file);
+  }
+
+  if (StringTail(path, 4).LowerCaseEqualsLiteral(".xpi")) {
+    XRE_AddJarManifestLocation(type, file);
+  } else {
+    nsCOMPtr<nsIFile> manifest = CloneAndAppend(file, "chrome.manifest");
+    XRE_AddManifestLocation(type, manifest);
+  }
+}
+
+nsresult
+AddonManagerStartup::ReadStartupData(JSContext* cx, JS::MutableHandleValue locations)
+{
+  nsresult rv;
+
+  locations.set(JS::UndefinedValue());
+
+  nsCOMPtr<nsIFile> file = CloneAndAppend(ProfileDir(), "addonStartup.json.lz4");
+
+  nsCString path;
+  rv = file->GetNativePath(path);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCString data;
+  if (!ReadFileLZ4(path.get(), data)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if (data.IsEmpty() || !ParseJSON(cx, data, locations)) {
+    return NS_OK;
+  }
+
+  if (!locations.isObject()) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  JS::RootedObject locs(cx, &locations.toObject());
+  for (auto e1 : PropertyIter(cx, locs)) {
+    InstallLocation loc(e1);
+
+    if (!loc.ShouldCheckStartupModifications()) {
+      continue;
+    }
+
+    for (auto e2 : loc.Addons()) {
+      Addon addon(e2);
+
+      if (addon.Enabled() && addon.UpdateLastModifiedTime()) {
+        loc.SetChanged(true);
+      }
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+AddonManagerStartup::InitializeExtensions(JS::HandleValue locations, JSContext* cx)
+{
+  NS_ENSURE_FALSE(mInitialized, NS_ERROR_UNEXPECTED);
+  NS_ENSURE_TRUE(locations.isObject(), NS_ERROR_INVALID_ARG);
+
+  mInitialized = true;
+
+  if (!Preferences::GetBool("extensions.defaultProviders.enabled", true)) {
+    return NS_OK;
+  }
+
+  bool enableInterpositions = Preferences::GetBool("extensions.interposition.enabled", false);
+
+  JS::RootedObject locs(cx, &locations.toObject());
+  for (auto e1 : PropertyIter(cx, locs)) {
+    InstallLocation loc(e1);
+
+    for (auto e2 : loc.Addons()) {
+      Addon addon(e2);
+
+      if (!addon.Bootstrapped()) {
+        AddInstallLocation(addon);
+
+        if (enableInterpositions && addon.ShimsEnabled()) {
+          EnableShims(addon.Id());
+        }
+      }
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+AddonManagerStartup::Reset()
+{
+  MOZ_RELEASE_ASSERT(xpc::IsInAutomation());
+
+  mInitialized = false;
+
+  mExtensionPaths.Clear();
+  mThemePaths.Clear();
+
+  return NS_OK;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AddonManagerStartup_h
+#define AddonManagerStartup_h
+
+#include "amIAddonManagerStartup.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsISupports.h"
+
+#include "jsapi.h"
+
+namespace mozilla {
+
+class Addon;
+
+class AddonManagerStartup final : public amIAddonManagerStartup
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_AMIADDONMANAGERSTARTUP
+
+  AddonManagerStartup();
+
+  static AddonManagerStartup& GetSingleton();
+
+  static already_AddRefed<AddonManagerStartup> GetInstance()
+  {
+    RefPtr<AddonManagerStartup> inst = &GetSingleton();
+    return inst.forget();
+  }
+
+  const nsCOMArray<nsIFile>& ExtensionPaths()
+  {
+    return mExtensionPaths;
+  }
+
+  const nsCOMArray<nsIFile>& ThemePaths()
+  {
+    return mExtensionPaths;
+  }
+
+private:
+  void AddInstallLocation(Addon& addon);
+
+  nsIFile* ProfileDir();
+
+  nsCOMPtr<nsIFile> mProfileDir;
+
+  nsCOMArray<nsIFile> mExtensionPaths;
+  nsCOMArray<nsIFile> mThemePaths;
+
+  bool mInitialized;
+
+protected:
+  virtual ~AddonManagerStartup() = default;
+};
+
+} // namespace mozilla
+
+#define NS_ADDONMANAGERSTARTUP_CONTRACTID \
+  "@mozilla.org/addons/addon-manager-startup;1"
+
+// {17a59a6b-92b8-42e5-bce0-ab434c7a7135
+#define NS_ADDON_MANAGER_STARTUP_CID \
+{ 0x17a59a6b, 0x92b8, 0x42e5, \
+  { 0xbc, 0xe0, 0xab, 0x43, 0x4c, 0x7a, 0x71, 0x35 } }
+
+#endif // AddonManagerStartup_h
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIAddonManagerStartup.idl
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, builtinclass, uuid(01dfa47b-87e4-4135-877b-586d033e1b5d)]
+interface amIAddonManagerStartup : nsISupports
+{
+  /**
+   * Reads and parses startup data from the addonState.json.lz4 file, checks
+   * for modifications, and returns the result.
+   *
+   * Returns null for an empty or nonexistent state file, but throws for an
+   * invalid one.
+   */
+  [implicit_jscontext]
+  jsval readStartupData();
+
+  /**
+   * Initializes the chrome registry for the enabled, non-restartless add-on
+   * in the given state data.
+   */
+  [implicit_jscontext]
+  void initializeExtensions(in jsval locations);
+
+  /**
+   * Resets the internal state of the startup service, and allows
+   * initializeExtensions() to be called again. Does *not* fully unregister
+   * chrome registry locations for previously registered add-ons.
+   *
+   * NOT FOR USE OUTSIDE OF UNIT TESTS.
+   */
+  void reset();
+};
+
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -24,28 +24,33 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {EventEmitter} = Cu.import("resource://gre/modules/EventEmitter.jsm", {});
 const {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
+                                   "@mozilla.org/addons/addon-manager-startup;1",
+                                   "amIAddonManagerStartup");
 XPCOMUtils.defineLazyServiceGetter(this, "rdfService",
                                    "@mozilla.org/rdf/rdf-service;1", "nsIRDFService");
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
 
 
 XPCOMUtils.defineLazyGetter(this, "AppInfo", () => {
   let AppInfo = {};
   Cu.import("resource://testing-common/AppInfo.jsm", AppInfo);
   return AppInfo;
 });
 
+const PREF_DISABLE_SECURITY = ("security.turn_off_all_security_so_that_" +
+                               "viruses_can_take_over_this_computer");
 
 const ArrayBufferInputStream = Components.Constructor(
   "@mozilla.org/io/arraybuffer-input-stream;1",
   "nsIArrayBufferInputStream", "setData");
 
 const nsFile = Components.Constructor(
   "@mozilla.org/file/local;1",
   "nsIFile", "initWithPath");
@@ -123,69 +128,69 @@ function escaped(strings, ...values) {
       result.push(escapeXML(values[i]));
   }
 
   return result.join("");
 }
 
 
 class AddonsList {
-  constructor(extensionsINI) {
+  constructor(file) {
     this.multiprocessIncompatibleIDs = new Set();
+    this.extensions = [];
+    this.themes = [];
 
-    if (!extensionsINI.exists()) {
-      this.extensions = [];
-      this.themes = [];
+    if (!file.exists()) {
       return;
     }
 
-    let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
-                  .getService(Ci.nsIINIParserFactory);
-
-    let parser = factory.createINIParser(extensionsINI);
+    let data = aomStartup.readStartupData();
 
-    function readDirectories(section) {
-      var dirs = [];
-      var keys = parser.getKeys(section);
-      for (let key of XPCOMUtils.IterStringEnumerator(keys)) {
-        let descriptor = parser.getString(section, key);
+    for (let loc of Object.values(data)) {
+      let dir = loc.path && new nsFile(loc.path);
 
-        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-        try {
-          file.persistentDescriptor = descriptor;
-        } catch (e) {
-          // Throws if the directory doesn't exist, we can ignore this since the
-          // platform will too.
-          continue;
+      for (let [id, addon] of Object.entries(loc.addons)) {
+        if (addon.enabled && !addon.bootstrapped) {
+          let file;
+          if (dir) {
+            file = dir.clone();
+            try {
+              file.appendRelativePath(addon.path);
+            } catch (e) {
+              file = new nsFile(addon.path);
+            }
+          } else {
+            file = new nsFile(addon.path);
+          }
+
+          addon.type = addon.type || "extension";
+
+          if (addon.type == "theme") {
+            this.themes.push(file);
+          } else {
+            this.extensions.push(file);
+            if (addon.enableShims) {
+              this.multiprocessIncompatibleIDs.add(id);
+            }
+          }
         }
-        dirs.push(file);
       }
-      return dirs;
-    }
-
-    this.extensions = readDirectories("ExtensionDirs");
-    this.themes = readDirectories("ThemeDirs");
-
-    var keys = parser.getKeys("MultiprocessIncompatibleExtensions");
-    for (let key of XPCOMUtils.IterStringEnumerator(keys)) {
-      let id = parser.getString("MultiprocessIncompatibleExtensions", key);
-      this.multiprocessIncompatibleIDs.add(id);
     }
   }
 
   hasItem(type, dir, id) {
     var path = dir.clone();
     path.append(id);
 
     var xpiPath = dir.clone();
     xpiPath.append(`${id}.xpi`);
 
     return this[type].some(file => {
       if (!file.exists())
-        throw new Error(`Non-existent path found in extensions.ini: ${file.path}`);
+        throw new Error(`Non-existent path found in addonStartup.json: ${file.path}`);
 
       if (file.isDirectory())
         return file.equals(path);
       if (file.isFile())
         return file.equals(xpiPath);
       return false;
     });
   }
@@ -202,31 +207,31 @@ class AddonsList {
     return this.hasItem("extensions", dir, id);
   }
 }
 
 var AddonTestUtils = {
   addonIntegrationService: null,
   addonsList: null,
   appInfo: null,
-  extensionsINI: null,
+  addonStartup: null,
   testUnpacked: false,
   useRealCertChecks: false,
 
   init(testScope) {
     this.testScope = testScope;
 
     // Get the profile directory for tests to use.
     this.profileDir = testScope.do_get_profile();
 
     this.profileExtensions = this.profileDir.clone();
     this.profileExtensions.append("extensions");
 
-    this.extensionsINI = this.profileDir.clone();
-    this.extensionsINI.append("extensions.ini");
+    this.addonStartup = this.profileDir.clone();
+    this.addonStartup.append("addonStartup.json.lz4");
 
     // Register a temporary directory for the tests.
     this.tempDir = this.profileDir.clone();
     this.tempDir.append("temp");
     this.tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
     this.registerDirectory("TmpD", this.tempDir);
 
     // Create a replacement app directory for the tests.
@@ -549,37 +554,33 @@ var AddonTestUtils = {
 
   /**
    * Starts up the add-on manager as if it was started by the application.
    *
    * @param {boolean} [appChanged = true]
    *        An optional boolean parameter to simulate the case where the
    *        application has changed version since the last run. If not passed it
    *        defaults to true
-   * @returns {Promise}
-   *        Resolves when the add-on manager's startup has completed.
    */
-  promiseStartupManager(appChanged = true) {
+  async promiseStartupManager(appChanged = true) {
     if (this.addonIntegrationService)
       throw new Error("Attempting to startup manager that was already started.");
 
-    if (appChanged && this.extensionsINI.exists())
-      this.extensionsINI.remove(true);
+    if (appChanged && this.addonStartup.exists())
+      this.addonStartup.remove(true);
 
     this.addonIntegrationService = Cc["@mozilla.org/addons/integration;1"]
           .getService(Ci.nsIObserver);
 
     this.addonIntegrationService.observe(null, "addons-startup", null);
 
     this.emit("addon-manager-started");
 
     // Load the add-ons list as it was after extension registration
-    this.loadAddonsList();
-
-    return Promise.resolve();
+    await this.loadAddonsList(true);
   },
 
   promiseShutdownManager() {
     if (!this.addonIntegrationService)
       return Promise.resolve(false);
 
     Services.obs.notifyObservers(null, "quit-application-granted");
     return MockAsyncShutdown.hook()
@@ -599,16 +600,22 @@ var AddonTestUtils = {
         let XPIscope = Cu.import("resource://gre/modules/addons/XPIProvider.jsm", {});
         // This would be cleaner if I could get it as the rejection reason from
         // the AddonManagerInternal.shutdown() promise
         let shutdownError = XPIscope.XPIProvider._shutdownError;
 
         AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
         Cu.unload("resource://gre/modules/addons/XPIProvider.jsm");
 
+        // We need to set this in order reset the startup service, which
+        // is only possible when running in automation.
+        Services.prefs.setBoolPref(PREF_DISABLE_SECURITY, true);
+
+        aomStartup.reset();
+
         if (shutdownError)
           throw shutdownError;
 
         return true;
       });
   },
 
   promiseRestartManager(newVersion) {
@@ -616,18 +623,24 @@ var AddonTestUtils = {
       .then(() => {
         if (newVersion)
           this.appInfo.version = newVersion;
 
         return this.promiseStartupManager(!!newVersion);
       });
   },
 
-  loadAddonsList() {
-    this.addonsList = new AddonsList(this.extensionsINI);
+  async loadAddonsList(flush = false) {
+    if (flush) {
+      let XPIScope = Cu.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+      XPIScope.XPIStates.save();
+      await XPIScope.XPIStates._jsonFile._save();
+    }
+
+    this.addonsList = new AddonsList(this.addonStartup);
   },
 
   /**
    * Creates an update.rdf structure as a string using for the update data passed.
    *
    * @param {Object} data
    *        The update data as a JS object. Each property name is an add-on ID,
    *        the property value is an array of each version of the add-on. Each
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -51,16 +51,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "ProductAddonChecker",
                                   "resource://gre/modules/addons/ProductAddonChecker.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                   "resource://gre/modules/UpdateUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "isAddonPartOfE10SRollout",
                                   "resource://gre/modules/addons/E10SAddonsRollout.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+                                  "resource://gre/modules/JSONFile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils",
                                   "resource://gre/modules/LegacyExtensionsUtils.jsm");
 
 const {nsIBlocklistService} = Ci;
 XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
                                    "@mozilla.org/extensions/blocklist;1",
                                    "nsIBlocklistService");
 XPCOMUtils.defineLazyServiceGetter(this,
@@ -74,16 +76,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "AddonPolicyService",
                                    "@mozilla.org/addons/policy-service;1",
                                    "nsIAddonPolicyService");
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "AddonPathService",
                                    "@mozilla.org/addon-path-service;1",
                                    "amIAddonPathService");
+XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
+                                   "@mozilla.org/addons/addon-manager-startup;1",
+                                   "amIAddonManagerStartup");
 
 XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
   let certUtils = {};
   Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
   return certUtils;
 });
 
 XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
@@ -91,33 +96,108 @@ XPCOMUtils.defineLazyGetter(this, "IconD
   return ExtensionUtils.IconDetails;
 });
 
 Cu.importGlobalProperties(["URL"]);
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
-function getFile(descriptor) {
-  let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-  file.persistentDescriptor = descriptor;
-  return file;
+/**
+ * Returns a nsIFile instance for the given path, relative to the given
+ * base file, if provided.
+ *
+ * @param {string} path
+ *        The (possibly relative) path of the file.
+ * @param {nsIFile} [base]
+ *        An optional file to use as a base path if `path` is relative.
+ * @returns {nsIFile}
+ */
+function getFile(path, base = null) {
+  if (base) {
+    let file = base.clone();
+    try {
+      file.appendRelativePath(path);
+      return file;
+    } catch (e) {}
+  }
+  return new nsIFile(path);
+}
+
+/**
+ * Returns the modification time of the given file, or 0 if the file
+ * does not exist, or cannot be accessed.
+ *
+ * @param {nsIFile} file
+ *        The file to retrieve the modification time for.
+ * @returns {double}
+ *        The file's modification time, in seconds since the Unix epoch.
+ */
+function tryGetMtime(file) {
+  try {
+    return file.lastModifiedTime;
+  } catch (e) {
+    return 0;
+  }
+}
+
+/**
+ * Returns the path to `file` relative to the directory `dir`, or an
+ * absolute path to the file if no directory is passed, or the file is
+ * not a descendant of it.
+ *
+ * @param {nsIFile} file
+ *        The file to return a relative path to.
+ * @param {nsIFile} [dir]
+ *        If passed, return a path relative to this directory.
+ * @returns {string}
+ */
+function getRelativePath(file, dir) {
+  if (dir && dir.contains(file)) {
+    let path = file.getRelativePath(dir);
+    if (AppConstants.platform == "win") {
+      path = path.replace(/\//g, "\\");
+    }
+    return path;
+  }
+  return file.path;
+}
+
+/**
+ * Converts the given opaque descriptor string into an ordinary path
+ * string. In practice, the path string is always exactly equal to the
+ * descriptor string, but theoretically may not have been on some legacy
+ * systems.
+ *
+ * @param {string} descriptor
+ *        The opaque descriptor string to convert.
+ * @param {nsIFile} [dir]
+ *        If passed, return a path relative to this directory.
+ * @returns {string}
+ *        The file's path.
+ */
+function descriptorToPath(descriptor, dir) {
+  try {
+    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    file.persistentDescriptor = descriptor;
+    return getRelativePath(file, dir);
+  } catch (e) {
+    return null;
+  }
 }
 
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
-const PREF_INSTALL_CACHE              = "extensions.installCache";
 const PREF_XPI_STATE                  = "extensions.xpiState";
 const PREF_BOOTSTRAP_ADDONS           = "extensions.bootstrappedAddons";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
 const PREF_SKIN_SWITCHPENDING         = "extensions.dss.switchPending";
 const PREF_SKIN_TO_SELECT             = "extensions.lastSelectedSkin";
 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
 const PREF_EM_UPDATE_URL              = "extensions.update.url";
 const PREF_EM_UPDATE_BACKGROUND_URL   = "extensions.update.background.url";
-const PREF_EM_ENABLED_ADDONS          = "extensions.enabledAddons";
 const PREF_EM_EXTENSION_FORMAT        = "extensions.";
 const PREF_EM_ENABLED_SCOPES          = "extensions.enabledScopes";
 const PREF_EM_STARTUP_SCAN_SCOPES     = "extensions.startupScanScopes";
 const PREF_EM_SHOW_MISMATCH_UI        = "extensions.showMismatchUI";
 const PREF_XPI_ENABLED                = "xpinstall.enabled";
 const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
 const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
 const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
@@ -143,26 +223,34 @@ const PREF_EM_MIN_COMPAT_APP_VERSION    
 const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
 
 const PREF_CHECKCOMAT_THEMEOVERRIDE   = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion";
 
 const PREF_EM_HOTFIX_ID               = "extensions.hotfix.id";
 const PREF_EM_CERT_CHECKATTRIBUTES    = "extensions.hotfix.cert.checkAttributes";
 const PREF_EM_HOTFIX_CERTS            = "extensions.hotfix.certs.";
 
+const OBSOLETE_PREFERENCES = [
+  "extensions.bootstrappedAddons",
+  "extensions.enabledAddons",
+  "extensions.xpiState",
+  "extensions.installCache",
+];
+
 const URI_EXTENSION_UPDATE_DIALOG     = "chrome://mozapps/content/extensions/update.xul";
 const URI_EXTENSION_STRINGS           = "chrome://mozapps/locale/extensions/extensions.properties";
 
 const STRING_TYPE_NAME                = "type.%ID%.name";
 
 const DIR_EXTENSIONS                  = "extensions";
 const DIR_SYSTEM_ADDONS               = "features";
 const DIR_STAGE                       = "staged";
 const DIR_TRASH                       = "trash";
 
+const FILE_XPI_STATES                 = "addonStartup.json.lz4";
 const FILE_DATABASE                   = "extensions.json";
 const FILE_OLD_CACHE                  = "extensions.cache";
 const FILE_RDF_MANIFEST               = "install.rdf";
 const FILE_WEB_MANIFEST               = "manifest.json";
 const FILE_XPI_ADDONS_LIST            = "extensions.ini";
 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_ADDON_APP_DIR               = "XREAddonAppDir";
@@ -174,16 +262,21 @@ const KEY_APP_PROFILE                 = 
 const KEY_APP_SYSTEM_ADDONS           = "app-system-addons";
 const KEY_APP_SYSTEM_DEFAULTS         = "app-system-defaults";
 const KEY_APP_GLOBAL                  = "app-global";
 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
 const KEY_APP_SYSTEM_USER             = "app-system-user";
 const KEY_APP_TEMPORARY               = "app-temporary";
 
+const STARTUP_MTIME_SCOPES = [KEY_APP_GLOBAL,
+                              KEY_APP_SYSTEM_LOCAL,
+                              KEY_APP_SYSTEM_SHARE,
+                              KEY_APP_SYSTEM_USER];
+
 const NOTIFICATION_FLUSH_PERMISSIONS  = "flush-pending-permissions";
 const XPI_PERMISSION                  = "install";
 
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
@@ -215,16 +308,27 @@ const STATIC_BLOCKLIST_PATTERNS = [
   { creator: "Mozilla Corp.",
     level: nsIBlocklistService.STATE_BLOCKED,
     blockID: "i162" },
   { creator: "Mozilla.org",
     level: nsIBlocklistService.STATE_BLOCKED,
     blockID: "i162" }
 ];
 
+function encoded(strings, ...values) {
+  let result = [];
+
+  for (let [i, string] of strings.entries()) {
+    result.push(string);
+    if (i < values.length)
+      result.push(encodeURIComponent(values[i]));
+  }
+
+  return result.join("");
+}
 
 const BOOTSTRAP_REASONS = {
   APP_STARTUP: 1,
   APP_SHUTDOWN: 2,
   ADDON_ENABLE: 3,
   ADDON_DISABLE: 4,
   ADDON_INSTALL: 5,
   ADDON_UNINSTALL: 6,
@@ -333,34 +437,31 @@ XPCOMUtils.defineLazyPreferenceGetter(th
 
 function loadLazyObjects() {
   let uri = "resource://gre/modules/addons/XPIProviderUtils.js";
   let scope = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
     sandboxName: uri,
     wantGlobalProperties: ["TextDecoder"],
   });
 
-  let shared = {
+  Object.assign(scope, {
     ADDON_SIGNING,
     SIGNED_TYPES,
     BOOTSTRAP_REASONS,
     DB_SCHEMA,
     AddonInternal,
     XPIProvider,
     XPIStates,
     syncLoadManifestFromFile,
     isUsableAddon,
     recordAddonTelemetry,
     applyBlocklistChanges,
     flushChromeCaches,
-    canRunInSafeMode,
-  }
-
-  for (let key of Object.keys(shared))
-    scope[key] = shared[key];
+    descriptorToPath,
+  });
 
   Services.scriptloader.loadSubScript(uri, scope);
 
   for (let name of LAZY_OBJECTS) {
     delete gGlobalScope[name];
     gGlobalScope[name] = scope[name];
   }
   gLazyObjectsLoaded = true;
@@ -1669,17 +1770,17 @@ function syncLoadManifestFromFile(aFile,
  *         The file containing the resources, must be either a directory or an
  *         XPI file
  * @param  aPath
  *         The path to find the resource at, "/" separated. If aPath is empty
  *         then the uri to the root of the contained files will be returned
  * @return an nsIURI pointing at the resource
  */
 function getURIForResourceInFile(aFile, aPath) {
-  if (aFile.isDirectory()) {
+  if (aFile.exists() && aFile.isDirectory()) {
     let resource = aFile.clone();
     if (aPath)
       aPath.split("/").forEach(part => resource.append(part));
 
     return Services.io.newFileURI(resource);
   }
 
   return buildJarURI(aFile, aPath);
@@ -2081,17 +2182,19 @@ function getDirectoryEntries(aDir, aSort
     if (aSortEntries) {
       entries.sort(function(a, b) {
         return a.path > b.path ? -1 : 1;
       });
     }
 
     return entries
   } catch (e) {
-    logger.warn("Can't iterate directory " + aDir.path, e);
+    if (aDir.exists()) {
+      logger.warn("Can't iterate directory " + aDir.path, e);
+    }
     return [];
   } finally {
     if (dirEnum) {
       dirEnum.close();
     }
   }
 }
 
@@ -2106,93 +2209,191 @@ function recordAddonTelemetry(aAddon) {
       XPIProvider.setTelemetry(aAddon.id, "name", locale.name);
     if (locale.creator)
       XPIProvider.setTelemetry(aAddon.id, "creator", locale.creator);
   }
 }
 
 /**
  * The on-disk state of an individual XPI, created from an Object
- * as stored in the 'extensions.xpiState' pref.
+ * as stored in the addonStartup.json file.
  */
-function XPIState(saved) {
-  for (let [short, long] of XPIState.prototype.fields) {
-    if (short in saved) {
-      this[long] = saved[short];
-    }
-  }
-}
-
-XPIState.prototype = {
-  fields: [["d", "descriptor"],
-           ["e", "enabled"],
-           ["v", "version"],
-           ["st", "scanTime"],
-           ["mt", "manifestTime"]],
-  /**
-   * Return the last modified time, based on enabled/disabled
-   */
+const JSON_FIELDS = Object.freeze([
+  "bootstrapped",
+  "changed",
+  "dependencies",
+  "enabled",
+  "enableShims",
+  "file",
+  "hasEmbeddedWebExtension",
+  "lastModifiedTime",
+  "path",
+  "runInSafeMode",
+  "type",
+  "version",
+]);
+
+const BOOTSTRAPPED_FIELDS = Object.freeze([
+  "dependencies",
+  "hasEmbeddedWebExtension",
+  "runInSafeMode",
+  "type",
+  "version",
+]);
+
+class XPIState {
+  constructor(location, id, saved = {}) {
+    this.location = location;
+    this.id = id;
+
+    // Set default values.
+    this.type = "extension";
+    this.bootstrapped = false;
+    this.enableShims = false;
+
+    for (let prop of JSON_FIELDS) {
+      if (prop in saved) {
+        this[prop] = saved[prop];
+      }
+    }
+
+    if (saved.currentModifiedTime && saved.currentModifiedTime != this.lastModifiedTime) {
+      this.lastModifiedTime = saved.currentModifiedTime;
+      this.changed = true;
+    }
+
+    if (this.enabled) {
+      XPIProvider._addURIMapping(id, this.file);
+    }
+  }
+
+  /**
+   * Migrates an add-on's data from xpiState and bootstrappedAddons
+   * preferences, and returns an XPIState object for it.
+   *
+   * @param {XPIStateLocation} location
+   *        The location of the add-on.
+   * @param {string} id
+   *        The ID of the add-on to migrate.
+   * @param {object} state
+   *        The add-on's data from the xpiState preference.
+   * @param {object} [bootstrapped]
+   *        The add-on's data from the bootstrappedAddons preference, if
+   *        applicable.
+   */
+  static migrate(location, id, saved, bootstrapped) {
+    let data = {
+      enabled: saved.e,
+      path: descriptorToPath(saved.d, location.dir),
+      lastModifiedTime: saved.mt || saved.st,
+      version: saved.v,
+      enableShims: false,
+    };
+
+    if (bootstrapped) {
+      data.bootstrapped = true;
+      data.enabled = true;
+      data.enableShims = !bootstrapped.multiprocessCompatible;
+      data.path = descriptorToPath(bootstrapped.descriptor, location.dir);
+
+      for (let field of BOOTSTRAPPED_FIELDS) {
+        if (field in bootstrapped) {
+          data[field] = bootstrapped[field];
+        }
+      }
+    }
+
+    return new XPIState(location, id, data);
+  }
+
+  // Compatibility shim getters for legacy callers in XPIProviderUtils:
   get mtime() {
-    if (!this.enabled && ("manifestTime" in this) && this.manifestTime > this.scanTime) {
-      return this.manifestTime;
-    }
-    return this.scanTime;
-  },
-
+    return this.lastModifiedTime;
+  }
+  get active() {
+    return this.enabled;
+  }
+  get multiprocessCompatible() {
+    return !this.enableShims;
+  }
+
+
+  /**
+   * @property {string} path
+   *        The full on-disk path of the add-on.
+   */
+  get path() {
+    return this.file && this.file.path;
+  }
+  set path(path) {
+    this.file = getFile(path, this.location.dir)
+  }
+
+  /**
+   * @property {string} relativePath
+   *        The path to the add-on relative to its parent location, or
+   *        the full path if its parent location has no on-disk path.
+   */
+  get relativePath() {
+    if (this.location.dir && this.location.dir.contains(this.file)) {
+      let path = this.file.getRelativePath(this.location.dir);
+      if (AppConstants.platform == "win") {
+        path = path.replace(/\//g, "\\");
+      }
+      return path;
+    }
+    return this.path;
+  }
+
+  /**
+   * Returns a JSON-compatible representation of this add-on's state
+   * data, to be saved to addonStartup.json.
+   */
   toJSON() {
-    let json = {};
-    for (let [short, long] of XPIState.prototype.fields) {
-      if (long in this) {
-        json[short] = this[long];
-      }
+    let json = {
+      enabled: this.enabled,
+      lastModifiedTime: this.lastModifiedTime,
+      path: this.relativePath,
+      version: this.version,
+    };
+    if (this.type != "extension") {
+      json.type = this.type;
+    }
+    if (this.enableShims) {
+      json.enableShims = true;
+    }
+    if (this.bootstrapped) {
+      json.bootstrapped = true;
+      json.dependencies = this.dependencies;
+      json.runInSafeMode = this.runInSafeMode;
+      json.hasEmbeddedWebExtension = this.hasEmbeddedWebExtension;
     }
     return json;
-  },
+  }
 
   /**
    * Update the last modified time for an add-on on disk.
    * @param aFile: nsIFile path of the add-on.
    * @param aId: The add-on ID.
    * @return True if the time stamp has changed.
    */
   getModTime(aFile, aId) {
-    let changed = false;
-    let scanStarted = Cu.now();
     // Modified time is the install manifest time, if any. If no manifest
     // exists, we assume this is a packed .xpi and use the time stamp of
     // {path}
-    try {
-      // Get the install manifest update time, if any.
-      let maniFile = getManifestFileForDir(aFile);
-      let maniTime = maniFile.lastModifiedTime;
-      if (maniTime != this.manifestTime) {
-        this.manifestTime = maniTime;
-        this.scanTime = maniTime;
-        changed = true;
-      }
-    } catch (e) {
-      // No manifest
-      delete this.manifestTime;
-      try {
-        let dtime = aFile.lastModifiedTime;
-        if (dtime != this.scanTime) {
-          changed = true;
-          this.scanTime = dtime;
-        }
-      } catch (e) {
-        logger.warn("Can't get modified time of ${file}: ${e}", {file: aFile.path, e});
-        changed = true;
-        this.scanTime = 0;
-      }
-    }
-    // Record duration of file-modified check
-    XPIProvider.setTelemetry(aId, "scan_MS", Math.round(Cu.now() - scanStarted));
-
-    return changed;
-  },
+    let mtime = (tryGetMtime(getManifestFileForDir(aFile)) ||
+                 tryGetMtime(aFile));
+    if (!mtime) {
+      logger.warn("Can't get modified time of ${file}", {file: aFile.path});
+    }
+
+    this.changed = mtime != this.lastModifiedTime;
+    this.lastModifiedTime = mtime;
+    return this.changed;
+  }
 
   /**
    * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true,
    * update the last-modified time. This should probably be made async, but for now we
    * don't want to maintain parallel sync and async versions of the scan.
    * Caller is responsible for doing XPIStates.save() if necessary.
    * @param aDBAddon The DBAddonInternal for this add-on.
    * @param aUpdated The add-on was updated, so we must record new modified time.
@@ -2202,48 +2403,150 @@ XPIState.prototype = {
     // If the add-on changes from disabled to enabled, we should re-check the modified time.
     // If this is a newly found add-on, it won't have an 'enabled' field but we
     // did a full recursive scan in that case, so we don't need to do it again.
     // We don't use aDBAddon.active here because it's not updated until after restart.
     let mustGetMod = (aDBAddon.visible && !aDBAddon.disabled && !this.enabled);
 
     this.enabled = aDBAddon.visible && !aDBAddon.disabled;
     this.version = aDBAddon.version;
-    // XXX Eventually also copy bootstrap, etc.
+    this.type = aDBAddon.type;
+    this.enableShims = this.type == "extension" && !aDBAddon.multiprocessCompatible;
+
+    this.bootstrapped = !!aDBAddon.bootstrap;
+    if (this.bootstrapped) {
+      this.hasEmbeddedWebExtension = aDBAddon.hasEmbeddedWebExtension;
+      this.dependencies = aDBAddon.dependencies;
+      this.runInSafeMode = canRunInSafeMode(aDBAddon);
+    }
+
     if (aUpdated || mustGetMod) {
-      this.getModTime(new nsIFile(this.descriptor), aDBAddon.id);
-      if (this.scanTime != aDBAddon.updateDate) {
-        aDBAddon.updateDate = this.scanTime;
-        XPIDatabase.saveChanges();
-      }
-    }
-  },
-};
-
-// Constructor for an ES6 Map that knows how to convert itself into a
-// regular object for toJSON().
-function SerializableMap(arg) {
-  let m = new Map(arg);
-  m.toJSON = function() {
-    let out = {}
-    for (let [key, val] of m) {
-      out[key] = val;
-    }
-    return out;
-  };
-  return m;
+      this.getModTime(this.file, aDBAddon.id);
+      if (this.lastModifiedTime != aDBAddon.updateDate) {
+        aDBAddon.updateDate = this.lastModifiedTime;
+        if (XPIDatabase.initialized) {
+          XPIDatabase.saveChanges();
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Manages the state data for add-ons in a given install location.
+ *
+ * @param {string} name
+ *        The name of the install location (e.g., "app-profile").
+ * @param {string?} path
+ *        The on-disk path of the install location. May be null for some
+ *        locations which do not map to a specific on-disk path.
+ * @param {object} [saved = {}]
+ *        The persisted JSON state data to restore.
+ */
+class XPIStateLocation extends Map {
+  constructor(name, path, saved = {}) {
+    super();
+
+    this.name = name;
+    this.path = path || saved.path || null;
+    this.dir = this.path && new nsIFile(this.path);
+
+    for (let [id, data] of Object.entries(saved.addons || {})) {
+      let xpiState = this._addState(id, data);
+      // Make a note that this state was restored from saved data.
+      xpiState.wasRestored = true;
+    }
+  }
+
+  /**
+   * Returns a JSON-compatible representation of this location's state
+   * data, to be saved to addonStartup.json.
+   */
+  toJSON() {
+    let json = { addons: {} };
+
+    if (this.path) {
+      json.path = this.path;
+    }
+
+    if (STARTUP_MTIME_SCOPES.includes(this.name)) {
+      json.checkStartupModifications = true;
+    }
+
+    for (let [id, addon] of this.entries()) {
+      if (addon.type != "experiment") {
+        json.addons[id] = addon;
+      }
+    }
+    return json;
+  }
+
+  _addState(addonId, saved) {
+    let xpiState = new XPIState(this, addonId, saved);
+    this.set(addonId, xpiState);
+    return xpiState;
+  }
+
+  /**
+   * Adds state data for the given DB add-on to the DB.
+   *
+   * @param {DBAddon} addon
+   *        The DBAddon to add.
+   */
+  addAddon(addon) {
+    logger.debug("XPIStates adding add-on ${id} in ${location}: ${path}", addon);
+
+    let xpiState = this._addState(addon.id, {file: addon._sourceBundle});
+    xpiState.syncWithDB(addon, true);
+
+    XPIProvider.setTelemetry(addon.id, "location", this.name);
+  }
+
+  /**
+   * Adds stub state data for the local file to the DB.
+   *
+   * @param {string} addonId
+   *        The ID of the add-on represented by the given file.
+   * @param {nsIFile} file
+   *        The local file or directory containing the add-on.
+   * @returns {XPIState}
+   */
+  addFile(addonId, file) {
+    let xpiState = this._addState(addonId, {enabled: true, file: file.clone()});
+    xpiState.getModTime(xpiState.file, addonId);
+    return xpiState;
+  }
+
+  /**
+   * Migrates saved state data for the given add-on from the values
+   * stored in xpiState and bootstrappedAddons preferences, and adds it to
+   * the DB.
+   *
+   * @param {string} id
+   *        The ID of the add-on to migrate.
+   * @param {object} state
+   *        The add-on's data from the xpiState preference.
+   * @param {object} [bootstrapped]
+   *        The add-on's data from the bootstrappedAddons preference, if
+   *        applicable.
+   */
+  migrateAddon(id, state, bootstrapped) {
+    this.set(id, XPIState.migrate(this, id, state, bootstrapped));
+  }
 }
 
 /**
  * Keeps track of the state of XPI add-ons on the file system.
  */
 this.XPIStates = {
   // Map(location name -> Map(add-on ID -> XPIState))
   db: null,
 
+  _jsonFile: null,
+
   /**
    * @property {Map<string, XPIState>} sideLoadedAddons
    *        A map of new add-ons detected during install location
    *        directory scans. Keys are add-on IDs, values are XPIState
    *        objects corresponding to those add-ons.
    */
   sideLoadedAddons: new Map(),
 
@@ -2253,134 +2556,192 @@ this.XPIStates = {
       for (let location of this.db.values()) {
         count += location.size;
       }
     }
     return count;
   },
 
   /**
-   * Load extension state data from preferences.
+   * Migrates state data from the xpiState and bootstrappedAddons
+   * preferences and adds it to the DB. Returns a JSON-compatible
+   * representation of the current state of the DB.
+   *
+   * @returns {object}
+   */
+  migrateStateFromPrefs() {
+    logger.info("No addonStartup.json found. Attempting to migrate data from preferences");
+
+    let state;
+    // Try to migrate state data from old storage locations.
+    let bootstrappedAddons;
+    try {
+      state = JSON.parse(Preferences.get(PREF_XPI_STATE));
+      bootstrappedAddons = JSON.parse(Preferences.get(PREF_BOOTSTRAP_ADDONS, "{}"));
+    } catch (e) {
+      logger.warn("Error parsing extensions.xpiState and " +
+                  "extensions.bootstrappedAddons: ${error}",
+                  {error: e});
+
+    }
+
+    for (let [locName, addons] of Object.entries(state)) {
+      for (let [id, addon] of Object.entries(addons)) {
+        let loc = this.getLocation(locName);
+        if (loc) {
+          loc.migrateAddon(id, addon, bootstrappedAddons[id] || null);
+        }
+      }
+    }
+
+    // Clear out old state data.
+    for (let pref of OBSOLETE_PREFERENCES) {
+      Preferences.reset(pref);
+    }
+    OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
+                                FILE_XPI_ADDONS_LIST));
+
+    // Serialize and deserialize so we get the expected JSON data.
+    let data = JSON.parse(JSON.stringify(this));
+
+    logger.debug("Migrated data: ${}", data);
+
+    return data;
+  },
+
+  /**
+   * Load extension state data from addonStartup.json, or migrates it
+   * from legacy state preferences, if they exist.
    */
   loadExtensionState() {
-    let state = {};
-
-    // Clear out old directory state cache.
-    Preferences.reset(PREF_INSTALL_CACHE);
-
-    let cache = Preferences.get(PREF_XPI_STATE, "{}");
+    let state;
     try {
-      state = JSON.parse(cache);
+      state = aomStartup.readStartupData();
     } catch (e) {
-      logger.warn("Error parsing extensions.xpiState ${state}: ${error}",
-          {state: cache, error: e});
-    }
-    logger.debug("Loaded add-on state from prefs: ${}", state);
-    return state;
+      logger.warn("Error parsing extensions state: ${error}",
+                  {error: e});
+    }
+
+    if (!state && Preferences.has(PREF_XPI_STATE)) {
+      try {
+        state = this.migrateStateFromPrefs();
+      } catch (e) {
+        logger.warn("Error migrating extensions.xpiState and " +
+                    "extensions.bootstrappedAddons: ${error}",
+                    {error: e});
+      }
+    }
+
+    logger.debug("Loaded add-on state: ${}", state);
+    return state || {};
   },
 
   /**
    * Walk through all install locations, highest priority first,
    * comparing the on-disk state of extensions to what is stored in prefs.
    *
    * @param {bool} [ignoreSideloads = true]
    *        If true, ignore changes in scopes where we don't accept
    *        side-loads.
    *
    * @return true if anything has changed.
    */
   getInstallState(ignoreSideloads = true) {
-    let oldState = this.loadExtensionState();
+    if (!this.db) {
+      this.db = new Map();
+    }
+
+    let oldState = this.initialStateData || this.loadExtensionState();
+    this.initialStateData = oldState;
+
     let changed = false;
-    this.db = new SerializableMap();
+    let oldLocations = new Set(Object.keys(oldState));
 
     for (let location of XPIProvider.installLocations) {
+      oldLocations.delete(location.name);
+
       // The results of scanning this location.
-      let foundAddons = new SerializableMap();
+      let loc = this.getLocation(location.name, location.path || null,
+                                 oldState[location.name]);
+      changed = changed || loc.changed;
 
       // Don't bother checking scopes where we don't accept side-loads.
       if (ignoreSideloads && !(location.scope & gStartupScanScopes)) {
-        if (location.name in oldState) {
-          for (let [id, state] of Object.entries(oldState[location.name])) {
-            foundAddons.set(id, new XPIState(state));
-          }
-
-          this.db.set(location.name, foundAddons);
-          delete oldState[location.name];
-        }
+        continue;
+      }
+
+      if (location.name == KEY_APP_TEMPORARY) {
         continue;
       }
 
-      // The list of add-on like file/directory names in the install location.
-      let addons = location.getAddonLocations(!ignoreSideloads);
-
-      // What our old state thinks should be in this location.
-      let locState = {};
-      if (location.name in oldState) {
-        locState = oldState[location.name];
-        // We've seen this location.
-        delete oldState[location.name];
-      }
-
-      for (let [id, file] of addons) {
-        if (!(id in locState)) {
+      let knownIds = new Set(loc.keys());
+      for (let [id, file] of location.getAddonLocations(true)) {
+        knownIds.delete(id);
+
+        let xpiState = loc.get(id);
+        if (!xpiState) {
           logger.debug("New add-on ${id} in ${location}", {id, location: location.name});
-          let xpiState = new XPIState({d: file.persistentDescriptor});
-          changed = xpiState.getModTime(file, id) || changed;
-          foundAddons.set(id, xpiState);
-          this.sideLoadedAddons.set(id, xpiState);
+
+          changed = true;
+          xpiState = loc.addFile(id, file);
+          if (!location.isSystem) {
+            this.sideLoadedAddons.set(id, xpiState);
+          }
         } else {
-          let xpiState = new XPIState(locState[id]);
-          // We found this add-on in the file system
-          delete locState[id];
-
-          changed = xpiState.getModTime(file, id) || changed;
-
-          if (file.persistentDescriptor != xpiState.descriptor) {
-            xpiState.descriptor = file.persistentDescriptor;
+          let addonChanged = (xpiState.getModTime(file, id) ||
+                              file.path != xpiState.path);
+          xpiState.file = file.clone();
+
+          if (addonChanged) {
             changed = true;
-          }
-          if (changed) {
             logger.debug("Changed add-on ${id} in ${location}", {id, location: location.name});
           } else {
             logger.debug("Existing add-on ${id} in ${location}", {id, location: location.name});
           }
-          foundAddons.set(id, xpiState);
         }
         XPIProvider.setTelemetry(id, "location", location.name);
       }
 
       // Anything left behind in oldState was removed from the file system.
-      if (Object.keys(locState).length) {
+      for (let id of knownIds) {
+        loc.delete(id);
         changed = true;
       }
-      // If we found anything, add this location to our database.
-      if (foundAddons.size != 0) {
-        this.db.set(location.name, foundAddons);
-      }
     }
 
     // If there's anything left in oldState, an install location that held add-ons
     // was removed from the browser configuration.
-    if (Object.keys(oldState).length) {
-      changed = true;
-    }
+    changed = changed || oldLocations.size > 0;
 
     logger.debug("getInstallState changed: ${rv}, state: ${state}",
         {rv: changed, state: this.db});
     return changed;
   },
 
   /**
    * Get the Map of XPI states for a particular location.
-   * @param aLocation The name of the install location.
-   * @return Map (id -> XPIState) or null if there are no add-ons in the location.
-   */
-  getLocation(aLocation) {
-    return this.db.get(aLocation);
+   * @param name The name of the install location.
+   * @return XPIStateLocation (id -> XPIState) or null if there are no add-ons in the location.
+   */
+  getLocation(name, path, saved) {
+    let location = this.db.get(name);
+
+    if (path && location && location.path != path) {
+      location = null;
+      saved = null;
+    }
+
+    if (!location || (path && location.path != path)) {
+      let loc = XPIProvider.installLocationsByName[name];
+      if (loc) {
+        location = new XPIStateLocation(name, path || loc.path || null, saved);
+        this.db.set(name, location);
+      }
+    }
+    return location;
   },
 
   /**
    * Get the XPI state for a specific add-on in a location.
    * If the state is not in our cache, return null.
    * @param aLocation The name of the location where the add-on is installed.
    * @param aId       The add-on ID
    * @return The XPIState entry for the add-on, or null.
@@ -2389,58 +2750,98 @@ this.XPIStates = {
     let location = this.db.get(aLocation);
     return location && location.get(aId);
   },
 
   /**
    * Find the highest priority location of an add-on by ID and return the
    * location and the XPIState.
    * @param aId   The add-on ID
-   * @return [locationName, XPIState] if the add-on is found, [undefined, undefined]
-   *         if the add-on is not found.
+   * @return {XPIState?}
    */
   findAddon(aId) {
     // Fortunately the Map iterator returns in order of insertion, which is
     // also our highest -> lowest priority order.
-    for (let [name, location] of this.db) {
+    for (let location of this.db.values()) {
       if (location.has(aId)) {
-        return [name, location.get(aId)];
-      }
-    }
-    return [undefined, undefined];
+        return location.get(aId);
+      }
+    }
+    return undefined;
+  },
+
+  /**
+   * Iterates over the list of all enabled add-ons in any location.
+   */
+  * enabledAddons() {
+    for (let location of this.db.values()) {
+      for (let entry of location.values()) {
+        if (entry.enabled) {
+          yield entry;
+        }
+      }
+    }
+  },
+
+  /**
+   * Iterates over the list of all add-ons which were initially restored
+   * from the startup state cache.
+   */
+  * initialEnabledAddons() {
+    for (let addon of this.enabledAddons()) {
+      if (addon.wasRestored) {
+        yield addon;
+      }
+    }
+  },
+
+  /**
+   * Iterates over all enabled bootstrapped add-ons, in any location.
+   */
+  * bootstrappedAddons() {
+    for (let addon of this.enabledAddons()) {
+      if (addon.bootstrapped) {
+        yield addon;
+      }
+    }
   },
 
   /**
    * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
    * @param aAddon DBAddonInternal for the new add-on.
    */
   addAddon(aAddon) {
-    let location = this.db.get(aAddon.location);
-    if (!location) {
-      // First add-on in this location.
-      location = new SerializableMap();
-      this.db.set(aAddon.location, location);
-    }
-    logger.debug("XPIStates adding add-on ${id} in ${location}: ${descriptor}", aAddon);
-    let xpiState = new XPIState({d: aAddon.descriptor});
-    location.set(aAddon.id, xpiState);
-    xpiState.syncWithDB(aAddon, true);
-    XPIProvider.setTelemetry(aAddon.id, "location", aAddon.location);
+    let location = this.getLocation(aAddon._installLocation.name);
+    location.addAddon(aAddon);
   },
 
   /**
    * Save the current state of installed add-ons.
-   * XXX this *totally* should be a .json file using DeferredSave...
    */
   save() {
-    let db = new SerializableMap(this.db);
-    db.delete(TemporaryInstallLocation.name);
-
-    let cache = JSON.stringify(db);
-    Services.prefs.setCharPref(PREF_XPI_STATE, cache);
+    if (!this._jsonFile) {
+      this._jsonFile = new JSONFile({
+        path: OS.Path.join(OS.Constants.Path.profileDir, FILE_XPI_STATES),
+        finalizeAt: AddonManager.shutdown,
+        compression: "lz4",
+      })
+      this._jsonFile.data = this;
+    }
+
+    this._jsonFile.saveSoon();
+  },
+
+  toJSON() {
+    let data = {};
+    for (let [key, loc] of this.db.entries()) {
+      if (key != TemporaryInstallLocation.name && loc.size) {
+        data[key] = loc;
+      }
+    }
+    return data;
   },
 
   /**
    * Remove the XPIState for an add-on and save the new state.
    * @param aLocation  The name of the add-on location.
    * @param aId        The ID of the add-on.
    */
   removeAddon(aLocation, aId) {
@@ -2449,20 +2850,29 @@ this.XPIStates = {
     if (location) {
       location.delete(aId);
       if (location.size == 0) {
         this.db.delete(aLocation);
       }
       this.save();
     }
   },
+
+  /**
+   * Disable the XPIState for an add-on.
+   */
+  disableAddon(aId) {
+    logger.debug(`Disabling XPIState for ${aId}`);
+    let state = this.findAddon(aId);
+    if (state) {
+      state.enabled = false;
+    }
+  },
 };
 
-const hasOwnProperty = Function.call.bind({}.hasOwnProperty);
-
 this.XPIProvider = {
   get name() {
     return "XPIProvider";
   },
 
   // An array of known install locations
   installLocations: null,
   // A dictionary of known install locations by name
@@ -2476,27 +2886,23 @@ this.XPIProvider = {
   // The selected skin to be used by the application when it is restarted. This
   // will be the same as currentSkin when it is the skin to be used when the
   // application is restarted
   selectedSkin: null,
   // The value of the minCompatibleAppVersion preference
   minCompatibleAppVersion: null,
   // The value of the minCompatiblePlatformVersion preference
   minCompatiblePlatformVersion: null,
-  // A dictionary of the file descriptors for bootstrappable add-ons by ID
-  bootstrappedAddons: {},
   // A Map of active addons to their bootstrapScope by ID
   activeAddons: new Map(),
   // True if the platform could have activated extensions
   extensionsActive: false,
   // True if all of the add-ons found during startup were installed in the
   // application install location
   allAppGlobal: true,
-  // A string listing the enabled add-ons for annotating crash reports
-  enabledAddons: null,
   // Keep track of startup phases for telemetry
   runPhase: XPI_STARTING,
   // Per-addon telemetry information
   _telemetryDetails: {},
   // A Map from an add-on install to its ID
   _addonFileMap: new Map(),
   // Flag to know if ToolboxProcess.jsm has already been loaded by someone or not
   _toolboxProcessLoaded: false,
@@ -2507,41 +2913,39 @@ this.XPIProvider = {
    * Returns true if the add-on with the given ID is currently active,
    * without forcing the add-ons database to load.
    *
    * @param {string} addonId
    *        The ID of the add-on to check.
    * @returns {boolean}
    */
   addonIsActive(addonId) {
-    if (hasOwnProperty(this.bootstrappedAddons, addonId)) {
-      return true;
-    }
-
-    let [, state] = XPIStates.findAddon(addonId);
+    let state = XPIStates.findAddon(addonId);
     return state && state.enabled;
   },
 
   /**
    * Returns an array of the add-on values in `bootstrappedAddons`,
    * sorted so that all of an add-on's dependencies appear in the array
    * before itself.
    *
    * @returns {Array<object>}
    *   A sorted array of add-on objects. Each value is a copy of the
    *   corresponding value in the `bootstrappedAddons` object, with an
    *   additional `id` property, which corresponds to the key in that
    *   object, which is the same as the add-ons ID.
    */
   sortBootstrappedAddons() {
+    // Sort the list so that ordering is deterministic.
+    let list = Array.from(XPIStates.bootstrappedAddons());
+    list.sort((a, b) => String.localeCompare(a.id, b.id));
+
     let addons = {};
-
-    // Sort the list of IDs so that the ordering is deterministic.
-    for (let id of Object.keys(this.bootstrappedAddons).sort()) {
-      addons[id] = Object.assign({id}, this.bootstrappedAddons[id]);
+    for (let entry of list) {
+      addons[entry.id] = entry;
     }
 
     let res = new Set();
     let seen = new Set();
 
     let add = addon => {
       seen.add(addon.id);
 
@@ -2815,17 +3219,16 @@ this.XPIProvider = {
                                          this.defaultSkin);
       this.selectedSkin = this.currentSkin;
       this.applyThemeChange();
 
       this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION,
                                                      null);
       this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
                                                           null);
-      this.enabledAddons = "";
 
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this);
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this);
       Services.prefs.addObserver(PREF_E10S_ADDON_BLOCKLIST, this);
       Services.prefs.addObserver(PREF_E10S_ADDON_POLICY, this);
       if (!REQUIRE_SIGNING || Cu.isInAutomation)
         Services.prefs.addObserver(PREF_XPI_SIGNATURES_REQUIRED, this);
       Services.prefs.addObserver(PREF_ALLOW_NON_MPC, this);
@@ -2841,16 +3244,17 @@ this.XPIProvider = {
           BrowserToolboxProcess.on("connectionchange",
                                    this.onDebugConnectionChange.bind(this));
         } else {
           // Else, wait for it to load
           Services.obs.addObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED);
         }
       }
 
+
       let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
                                              aOldPlatformVersion);
 
       // Changes to installed extensions may have changed which theme is selected
       this.applyThemeChange();
 
       AddonManagerPrivate.markProviderSafe(this);
 
@@ -2868,18 +3272,16 @@ this.XPIProvider = {
         // UI displayed early in startup (like the compatibility UI) may have
         // caused us to cache parts of the skin or locale in memory. These must
         // be flushed to allow extension provided skins and locales to take full
         // effect
         Services.obs.notifyObservers(null, "chrome-flush-skin-caches");
         Services.obs.notifyObservers(null, "chrome-flush-caches");
       }
 
-      this.enabledAddons = Preferences.get(PREF_EM_ENABLED_ADDONS, "");
-
       if ("nsICrashReporter" in Ci &&
           Services.appinfo instanceof Ci.nsICrashReporter) {
         // Annotate the crash report with relevant add-on information.
         try {
           Services.appinfo.annotateCrashReport("Theme", this.currentSkin);
         } catch (e) { }
         try {
           Services.appinfo.annotateCrashReport("EMCheckCompatibility",
@@ -2888,25 +3290,24 @@ this.XPIProvider = {
         this.addAddonsToCrashReporter();
       }
 
       try {
         AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin");
 
         for (let addon of this.sortBootstrappedAddons()) {
           try {
-            let file = getFile(addon.descriptor);
             let reason = BOOTSTRAP_REASONS.APP_STARTUP;
             // Eventually set INSTALLED reason when a bootstrap addon
             // is dropped in profile folder and automatically installed
             if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
                             .indexOf(addon.id) !== -1)
               reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
             this.callBootstrapMethod(createAddonDetails(addon.id, addon),
-                                     file, "startup", reason);
+                                     addon.file, "startup", reason);
           } catch (e) {
             logger.error("Failed to load bootstrap addon " + addon.id + " from " +
                          addon.descriptor, e);
           }
         }
         AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end");
       } catch (e) {
         logger.error("bootstrap startup failed", e);
@@ -2920,27 +3321,25 @@ this.XPIProvider = {
           XPIProvider._closing = true;
           for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) {
             // If no scope has been loaded for this add-on then there is no need
             // to shut it down (should only happen when a bootstrapped add-on is
             // pending enable)
             if (!XPIProvider.activeAddons.has(addon.id))
               continue;
 
-            let file = getFile(addon.descriptor);
             let addonDetails = createAddonDetails(addon.id, addon);
 
             // If the add-on was pending disable then shut it down and remove it
             // from the persisted data.
             if (addon.disable) {
-              XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown",
+              XPIProvider.callBootstrapMethod(addonDetails, addon.file, "shutdown",
                                               BOOTSTRAP_REASONS.ADDON_DISABLE);
-              delete XPIProvider.bootstrappedAddons[addon.id];
             } else {
-              XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown",
+              XPIProvider.callBootstrapMethod(addonDetails, addon.file, "shutdown",
                                               BOOTSTRAP_REASONS.APP_SHUTDOWN);
             }
           }
           Services.obs.removeObserver(this, "quit-application-granted");
         }
       }, "quit-application-granted");
 
       // Detect final-ui-startup for telemetry reporting
@@ -2981,49 +3380,44 @@ this.XPIProvider = {
     this.cancelAll();
 
     // Uninstall any temporary add-ons.
     let tempLocation = XPIStates.getLocation(TemporaryInstallLocation.name);
     if (tempLocation) {
       for (let [id, addon] of tempLocation.entries()) {
         tempLocation.delete(id);
 
-        let file = getFile(addon.descriptor);
-
-        this.callBootstrapMethod(createAddonDetails(id, this.bootstrappedAddons[id]),
-                                 file, "uninstall",
+        this.callBootstrapMethod(createAddonDetails(id, addon),
+                                 addon.file, "uninstall",
                                  BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         this.unloadBootstrapScope(id);
         TemporaryInstallLocation.uninstallAddon(id);
 
-        let [locationName, ] = XPIStates.findAddon(id);
-        if (locationName) {
-          let newAddon = XPIDatabase.makeAddonLocationVisible(id, locationName);
-
-          let file = getFile(newAddon.descriptor);
+        let state = XPIStates.findAddon(id);
+        if (state) {
+          let newAddon = XPIDatabase.makeAddonLocationVisible(id, state.location.name);
+
+          let file = new nsIFile(newAddon.path);
 
           this.callBootstrapMethod(createAddonDetails(id, newAddon),
                                    file, "install",
                                    BOOTSTRAP_REASONS.ADDON_INSTALL);
         }
       }
     }
 
-    this.bootstrappedAddons = {};
     this.activeAddons.clear();
-    this.enabledAddons = null;
     this.allAppGlobal = true;
 
     // If there are pending operations then we must update the list of active
     // add-ons
     if (Preferences.get(PREF_PENDING_OPERATIONS, false)) {
       AddonManagerPrivate.recordSimpleMeasure("XPIDB_pending_ops", 1);
       XPIDatabase.updateActiveAddons();
-      Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
-                                 !XPIDatabase.writeAddonsList());
+      Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
     }
 
     this.installs = null;
     this.installLocations = null;
     this.installLocationsByName = null;
 
     // This is needed to allow xpcshell tests to simulate a restart
     this.extensionsActive = false;
@@ -3129,19 +3523,17 @@ this.XPIProvider = {
     variant.setFromVariant(aAddonIDs);
 
     // This *must* be modal as it has to block startup.
     var features = "chrome,centerscreen,dialog,titlebar,modal";
     var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
              getService(Ci.nsIWindowWatcher);
     ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
 
-    // Ensure any changes to the add-ons list are flushed to disk
-    Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
-                               !XPIDatabase.writeAddonsList());
+    Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
   },
 
   async updateSystemAddons() {
     let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
     if (!systemAddonLocation)
       return;
 
     // Don't do anything in safe mode
@@ -3307,52 +3699,30 @@ this.XPIProvider = {
         Services.obs.notifyObservers(null, "xpi-signature-changed", JSON.stringify(changes));
       })().then(null, err => {
         logger.error("XPI_verifySignature: " + err);
       })
     });
   },
 
   /**
-   * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref).
-   */
-  persistBootstrappedAddons() {
-    // Experiments are disabled upon app load, so don't persist references.
-    let filtered = {};
-    for (let id in this.bootstrappedAddons) {
-      let entry = this.bootstrappedAddons[id];
-      if (entry.type == "experiment") {
-        continue;
-      }
-
-      filtered[id] = entry;
-    }
-
-    Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
-                               JSON.stringify(filtered));
-  },
-
-  /**
    * Adds a list of currently active add-ons to the next crash report.
    */
   addAddonsToCrashReporter() {
     if (!("nsICrashReporter" in Ci) ||
         !(Services.appinfo instanceof Ci.nsICrashReporter))
       return;
 
     // In safe mode no add-ons are loaded so we should not include them in the
     // crash report
     if (Services.appinfo.inSafeMode)
       return;
 
-    let data = this.enabledAddons;
-    for (let id in this.bootstrappedAddons) {
-      data += (data ? "," : "") + encodeURIComponent(id) + ":" +
-              encodeURIComponent(this.bootstrappedAddons[id].version);
-    }
+    let data = Array.from(XPIStates.enabledAddons(),
+                          a => encoded`${a.id}:${a.version}`).join(",");
 
     try {
       Services.appinfo.annotateCrashReport("Add-ons", data);
     } catch (e) { }
 
     let TelemetrySession =
       Cu.import("resource://gre/modules/TelemetrySession.jsm", {}).TelemetrySession;
     TelemetrySession.setAddOns(data);
@@ -3446,16 +3816,17 @@ this.XPIProvider = {
                                          "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
               }
             } catch (e) {
               logger.warn("Failed to call uninstall for " + id, e);
             }
 
             try {
               location.uninstallAddon(id);
+              XPIStates.removeAddon(location.name, id);
               seenFiles.push(stageDirEntry.leafName);
             } catch (e) {
               logger.error("Failed to uninstall add-on " + id + " in " + location.name, e);
             }
             // The file check later will spot the removal and cleanup the database
             continue;
           }
         }
@@ -3514,46 +3885,48 @@ this.XPIProvider = {
           }
         }
         seenFiles.push(jsonfile.leafName);
 
         existingAddonID = addon.existingAddonID || id;
 
         var oldBootstrap = null;
         logger.debug("Processing install of " + id + " in " + location.name);
-        if (existingAddonID in this.bootstrappedAddons) {
+        let existingAddon = XPIStates.findAddon(existingAddonID);
+        if (existingAddon && existingAddon.bootstrapped) {
           try {
-            var existingAddon = getFile(this.bootstrappedAddons[existingAddonID].descriptor);
-            if (existingAddon.exists()) {
-              oldBootstrap = this.bootstrappedAddons[existingAddonID];
+            var file = existingAddon.file;
+            if (file.exists()) {
+              oldBootstrap = existingAddon;
 
               // We'll be replacing a currently active bootstrapped add-on so
               // call its uninstall method
               let newVersion = addon.version;
-              let oldVersion = oldBootstrap.version;
+              let oldVersion = existingAddon;
               let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
                                     BOOTSTRAP_REASONS.ADDON_UPGRADE :
                                     BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
 
-              this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
-                                       existingAddon, "uninstall", uninstallReason,
+              this.callBootstrapMethod(createAddonDetails(existingAddonID, existingAddon),
+                                       file, "uninstall", uninstallReason,
                                        { newVersion });
               this.unloadBootstrapScope(existingAddonID);
               flushChromeCaches();
             }
           } catch (e) {
           }
         }
 
         try {
           addon._sourceBundle = location.installAddon({
             id,
             source: stageDirEntry,
             existingAddonID
           });
+          XPIStates.addAddon(addon);
         } catch (e) {
           logger.error("Failed to install staged add-on " + id + " in " + location.name,
                 e);
           // Re-create the staged install
           new StagedAddonInstall(location, stageDirEntry, addon);
           // Make sure not to delete the cached manifest json file
           seenFiles.pop();
 
@@ -3675,16 +4048,17 @@ this.XPIProvider = {
         }
       } else if (Preferences.get(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
         continue;
       }
 
       // Install the add-on
       try {
         addon._sourceBundle = profileLocation.installAddon({ id, source: entry, action: "copy" });
+        XPIStates.addAddon(addon);
         logger.debug("Installed distribution add-on " + id);
 
         Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true)
 
         // aManifests may contain a copy of a newly installed add-on's manifest
         // and we'll have overwritten that so instead cache our install manifest
         // which will later be put into the database in processFileChanges
         if (!(KEY_APP_PROFILE in aManifests))
@@ -3738,24 +4112,19 @@ this.XPIProvider = {
 
     // Keep track of whether and why we need to open and update the database at
     // startup time.
     let updateReasons = [];
     if (aAppChanged) {
       updateReasons.push("appChanged");
     }
 
-    // Load the list of bootstrapped add-ons first so processFileChanges can
-    // modify it
-    // XXX eventually get rid of bootstrappedAddons
-    try {
-      this.bootstrappedAddons = JSON.parse(Preferences.get(PREF_BOOTSTRAP_ADDONS,
-                                           "{}"));
-    } catch (e) {
-      logger.warn("Error parsing enabled bootstrapped extensions cache", e);
+    let installChanged = XPIStates.getInstallState(aAppChanged === false);
+    if (installChanged) {
+      updateReasons.push("directoryState");
     }
 
     // First install any new add-ons into the locations, if there are any
     // changes then we must update the database with the information in the
     // install locations
     let manifests = {};
     let updated = this.processPendingFileChanges(manifests);
     if (updated) {
@@ -3773,25 +4142,16 @@ this.XPIProvider = {
     // If the application has changed then check for new distribution add-ons
     if (Preferences.get(PREF_INSTALL_DISTRO_ADDONS, true)) {
       updated = this.installDistributionAddons(manifests, aAppChanged);
       if (updated) {
         updateReasons.push("installDistributionAddons");
       }
     }
 
-    // Telemetry probe added around getInstallState() to check perf
-    let telemetryCaptureTime = Cu.now();
-    let installChanged = XPIStates.getInstallState(aAppChanged === false);
-    let telemetry = Services.telemetry;
-    telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Math.round(Cu.now() - telemetryCaptureTime));
-    if (installChanged) {
-      updateReasons.push("directoryState");
-    }
-
     let haveAnyAddons = (XPIStates.size > 0);
 
     // If the schema appears to have changed then we should update the database
     if (DB_SCHEMA != Preferences.get(PREF_DB_SCHEMA, 0)) {
       // If we don't have any add-ons, just update the pref, since we don't need to
       // write the database
       if (!haveAnyAddons) {
         logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA);
@@ -3804,34 +4164,16 @@ this.XPIProvider = {
     // If the database doesn't exist and there are add-ons installed then we
     // must update the database however if there are no add-ons then there is
     // no need to update the database.
     let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
     if (!dbFile.exists() && haveAnyAddons) {
       updateReasons.push("needNewDatabase");
     }
 
-    // XXX This will go away when we fold bootstrappedAddons into XPIStates.
-    if (updateReasons.length == 0) {
-      let bootstrapDescriptors = new Set(Object.keys(this.bootstrappedAddons)
-        .map(b => this.bootstrappedAddons[b].descriptor));
-
-      for (let location of XPIStates.db.values()) {
-        for (let state of location.values()) {
-          bootstrapDescriptors.delete(state.descriptor);
-        }
-      }
-
-      if (bootstrapDescriptors.size > 0) {
-        logger.warn("Bootstrap state is invalid (missing add-ons: "
-            + Array.from(bootstrapDescriptors).join(", ") + ")");
-        updateReasons.push("missingBootstrapAddon");
-      }
-    }
-
     // Catch and log any errors during the main startup
     try {
       let extensionListChanged = false;
       // If the database needs to be updated then open it and then update it
       // from the filesystem
       if (updateReasons.length > 0) {
         AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons);
         XPIDatabase.syncLoadDB(false);
@@ -3865,43 +4207,37 @@ this.XPIProvider = {
           logger.warn("Unable to remove old extension cache " + oldCache.path, e);
         }
       }
 
       // If the application crashed before completing any pending operations then
       // we should perform them now.
       if (extensionListChanged || hasPendingChanges) {
         this._updateActiveAddons();
+
+        // Serialize and deserialize so we get the expected JSON data.
+        let state = JSON.parse(JSON.stringify(XPIStates));
+        aomStartup.initializeExtensions(state);
         return true;
       }
 
+      aomStartup.initializeExtensions(XPIStates.initialStateData);
+
       logger.debug("No changes found");
     } catch (e) {
       logger.error("Error during startup file checks", e);
     }
 
-    // Check that the add-ons list still exists
-    let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
-                                       true);
-    // the addons list file should exist if and only if we have add-ons installed
-    if (addonsList.exists() != haveAnyAddons) {
-      logger.debug("Add-ons list is invalid, rebuilding");
-      XPIDatabase.writeAddonsList();
-    }
-
     return false;
   },
 
   _updateActiveAddons() {
     logger.debug("Updating database with changes to installed add-ons");
     XPIDatabase.updateActiveAddons();
-    Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
-                               !XPIDatabase.writeAddonsList());
-    Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
-                               JSON.stringify(this.bootstrappedAddons));
+    Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
   },
 
   /**
    * Gets an array of add-ons which were placed in a known install location
    * prior to startup of the current session, were detected by a directory scan
    * of those locations, and are currently disabled.
    *
    * @returns {Promise<Array<Addon>>}
@@ -4817,36 +5153,27 @@ this.XPIProvider = {
    *         An array of add-on IDs on which this add-on depends.
    * @param  hasEmbeddedWebExtension
    *         Boolean indicating whether the add-on has an embedded webextension.
    * @return a JavaScript scope
    */
   loadBootstrapScope(aId, aFile, aVersion, aType,
                                aMultiprocessCompatible, aRunInSafeMode,
                                aDependencies, hasEmbeddedWebExtension) {
-    // Mark the add-on as active for the crash reporter before loading
-    this.bootstrappedAddons[aId] = {
-      version: aVersion,
-      type: aType,
-      descriptor: aFile.persistentDescriptor,
-      multiprocessCompatible: aMultiprocessCompatible,
-      runInSafeMode: aRunInSafeMode,
-      dependencies: aDependencies,
-      hasEmbeddedWebExtension,
-    };
-    this.persistBootstrappedAddons();
-    this.addAddonsToCrashReporter();
-
     this.activeAddons.set(aId, {
       debugGlobal: null,
       safeWrapper: null,
       bootstrapScope: null,
       // a Symbol passed to this add-on, which it can use to identify itself
       instanceID: Symbol(aId),
     });
+
+    // Mark the add-on as active for the crash reporter before loading
+    this.addAddonsToCrashReporter();
+
     let activeAddon = this.activeAddons.get(aId);
 
     // Locales only contain chrome and can't have bootstrap scripts
     if (aType == "locale") {
       return;
     }
 
     logger.debug("Loading bootstrap scope from " + aFile.path);
@@ -4928,18 +5255,16 @@ this.XPIProvider = {
    */
   unloadBootstrapScope(aId) {
     // In case the add-on was not multiprocess-compatible, deregister
     // any interpositions for it.
     Cu.setAddonInterposition(aId, null);
     Cu.allowCPOWsInAddon(aId, false);
 
     this.activeAddons.delete(aId);
-    delete this.bootstrappedAddons[aId];
-    this.persistBootstrappedAddons();
     this.addAddonsToCrashReporter();
 
     // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been
     // initialized as otherwise, there won't be any addon globals added to it
     if (this._toolboxProcessLoaded) {
       BrowserToolboxProcess.setAddonOptions(aId, { global: null });
     }
   },
@@ -5146,16 +5471,26 @@ this.XPIProvider = {
     // If the add-on is not visible or the add-on is not changing state then
     // there is no need to do anything else
     if (!aAddon.visible || (wasDisabled == isDisabled))
       return undefined;
 
     // Flag that active states in the database need to be updated on shutdown
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
 
+    // Sync with XPIStates.
+    let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+    if (xpiState) {
+      xpiState.syncWithDB(aAddon);
+      XPIStates.save();
+    } else {
+      // There should always be an xpiState
+      logger.warn("No XPIState for ${id} in ${location}", aAddon);
+    }
+
     // Have we just gone back to the current state?
     if (isDisabled != aAddon.active) {
       AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
     } else {
       if (isDisabled) {
         var needsRestart = aPendingRestart || this.disableRequiresRestart(aAddon);
         AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
                                                needsRestart);
@@ -5177,44 +5512,17 @@ this.XPIProvider = {
           AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
         } else {
           if (aAddon.bootstrap) {
             this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
                                      BOOTSTRAP_REASONS.ADDON_ENABLE);
           }
           AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
         }
-      } else if (aAddon.bootstrap) {
-        // Something blocked the restartless add-on from enabling or disabling
-        // make sure it happens on the next startup
-        if (isDisabled) {
-          this.bootstrappedAddons[aAddon.id].disable = true;
-        } else {
-          this.bootstrappedAddons[aAddon.id] = {
-            version: aAddon.version,
-            type: aAddon.type,
-            descriptor: aAddon._sourceBundle.persistentDescriptor,
-            multiprocessCompatible: aAddon.multiprocessCompatible,
-            runInSafeMode: canRunInSafeMode(aAddon),
-            dependencies: aAddon.dependencies,
-            hasEmbeddedWebExtension: aAddon.hasEmbeddedWebExtension,
-          };
-          this.persistBootstrappedAddons();
-        }
-      }
-    }
-
-    // Sync with XPIStates.
-    let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
-    if (xpiState) {
-      xpiState.syncWithDB(aAddon);
-      XPIStates.save();
-    } else {
-      // There should always be an xpiState
-      logger.warn("No XPIState for ${id} in ${location}", aAddon);
+      }
     }
 
     // Notify any other providers that a new theme has been enabled
     if (isTheme(aAddon.type) && !isDisabled)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
 
     return isDisabled;
   },
@@ -5324,43 +5632,45 @@ this.XPIProvider = {
       }
 
       // We always send onInstalled even if a restart is required to enable
       // the revealed add-on
       AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
     }
 
     function findAddonAndReveal(aId) {
-      let [locationName, ] = XPIStates.findAddon(aId);
-      if (locationName) {
-        XPIDatabase.getAddonInLocation(aId, locationName, revealAddon);
+      let state = XPIStates.findAddon(aId);
+      if (state) {
+        XPIDatabase.getAddonInLocation(aId, state.location.name, revealAddon);
       }
     }
 
     if (!makePending) {
       if (aAddon.bootstrap) {
         if (aAddon.active) {
           this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
                                    BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         }
 
         this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall",
                                  BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+        XPIStates.disableAddon(aAddon.id);
         this.unloadBootstrapScope(aAddon.id);
         flushChromeCaches();
       }
       aAddon._installLocation.uninstallAddon(aAddon.id);
       XPIDatabase.removeAddonMetadata(aAddon);
       XPIStates.removeAddon(aAddon.location, aAddon.id);
       AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
 
       findAddonAndReveal(aAddon.id);
     } else if (aAddon.bootstrap && aAddon.active && !this.disableRequiresRestart(aAddon)) {
       this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
                                BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+      XPIStates.disableAddon(aAddon.id);
       this.unloadBootstrapScope(aAddon.id);
       XPIDatabase.updateAddonActive(aAddon, false);
     }
 
     // Notify any other providers that a new theme has been enabled
     if (isTheme(aAddon.type) && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart);
   },
@@ -5382,16 +5692,19 @@ this.XPIProvider = {
 
     XPIDatabase.setAddonProperties(aAddon, {
       pendingUninstall: false
     });
 
     if (!aAddon.visible)
       return;
 
+    XPIStates.getAddon(aAddon.location, aAddon.id).syncWithDB(aAddon);
+    XPIStates.save();
+
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
 
     // TODO hide hidden add-ons (bug 557710)
     let wrapper = aAddon.wrapper;
     AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
 
     if (aAddon.bootstrap && !aAddon.disabled && !this.enableRequiresRestart(aAddon)) {
       this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
@@ -8061,16 +8374,20 @@ class DirectoryInstallLocation {
     if (!aDirectory || !aDirectory.exists())
       return;
     if (!aDirectory.isDirectory())
       throw new Error("Location must be a directory.");
 
     this.initialized = false;
   }
 
+  get path() {
+    return this._directory && this._directory.path;
+  }
+
   /**
    * Reads a directory linked to in a file.
    *
    * @param   file
    *          The file containing the directory path
    * @return  An nsIFile object representing the linked directory.
    */
   _readDirectoryFromFile(aFile) {
@@ -8518,16 +8835,18 @@ class MutableDirectoryInstallLocation ex
       // rolling back the uninstall at this point
       try {
         recursiveRemove(trashDir);
       } catch (e) {
         logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
       }
     }
 
+    XPIStates.removeAddon(this.name, aId);
+
     delete this._IDToFileMap[aId];
   }
 }
 
 /**
  * An object which identifies a directory install location for system add-ons
  * updates.
  */
@@ -8710,17 +9029,18 @@ class SystemAddonInstallLocation extends
    * Resets the add-on set so on the next startup the default set will be used.
    */
   resetAddonSet() {
     logger.info("Removing all system add-on upgrades.");
 
     // remove everything from the pref first, if uninstall
     // fails then at least they will not be re-activated on
     // next restart.
-    SystemAddonInstallLocation._saveAddonSet({ schema: 1, addons: {} });
+    this._addonSet = { schema: 1, addons: {} };
+    SystemAddonInstallLocation._saveAddonSet(this._addonSet);
 
     // If this is running at app startup, the pref being cleared
     // will cause later stages of startup to notice that the
     // old updates are now gone.
     //
     // Updates will only be explicitly uninstalled if they are
     // removed restartlessly, for instance if they are no longer
     // part of the latest update set.
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -3,17 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // These are injected from XPIProvider.jsm
 /* globals ADDON_SIGNING, SIGNED_TYPES, BOOTSTRAP_REASONS, DB_SCHEMA,
           AddonInternal, XPIProvider, XPIStates, syncLoadManifestFromFile,
           isUsableAddon, recordAddonTelemetry, applyBlocklistChanges,
-          flushChromeCaches, canRunInSafeMode*/
+          flushChromeCaches, descriptorToPath */
 
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cr = Components.results;
 var Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -36,49 +36,48 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    Ci.nsIBlocklistService);
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "ALLOW_NON_MPC",
                                       "extensions.allow-non-mpc-extensions");
 
 Cu.import("resource://gre/modules/Log.jsm");
 const LOGGER_ID = "addons.xpi-utils";
 
-const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile");
+const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+                                       "initWithPath");
 
 // Create a new logger for use by the Addons XPI Provider Utils
 // (Requires AddonManager.jsm)
 var logger = Log.repository.getLogger(LOGGER_ID);
 
 const KEY_PROFILEDIR                  = "ProfD";
 const FILE_JSON_DB                    = "extensions.json";
-const FILE_XPI_ADDONS_LIST            = "extensions.ini";
 
 // The last version of DB_SCHEMA implemented in SQLITE
 const LAST_SQLITE_DB_SCHEMA           = 14;
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
-const PREF_EM_ENABLED_ADDONS          = "extensions.enabledAddons";
 const PREF_EM_AUTO_DISABLED_SCOPES    = "extensions.autoDisableScopes";
 const PREF_E10S_BLOCKED_BY_ADDONS     = "extensions.e10sBlockedByAddons";
 const PREF_E10S_MULTI_BLOCKED_BY_ADDONS = "extensions.e10sMultiBlockedByAddons";
 const PREF_E10S_HAS_NONEXEMPT_ADDON   = "extensions.e10s.rollout.hasAddon";
 
 const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_SYSTEM_ADDONS           = "app-system-addons";
 const KEY_APP_SYSTEM_DEFAULTS         = "app-system-defaults";
 const KEY_APP_GLOBAL                  = "app-global";
 const KEY_APP_TEMPORARY               = "app-temporary";
 
 // Properties to save in JSON file
 const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
                           "internalName", "updateURL", "updateKey", "optionsURL",
                           "optionsType", "optionsBrowserStyle", "aboutURL",
                           "defaultLocale", "visible", "active", "userDisabled",
-                          "appDisabled", "pendingUninstall", "descriptor", "installDate",
-                          "updateDate", "applyBackgroundUpdates", "bootstrap",
+                          "appDisabled", "pendingUninstall", "installDate",
+                          "updateDate", "applyBackgroundUpdates", "bootstrap", "path",
                           "skinnable", "size", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall", "hasBinaryComponents",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "multiprocessCompatible", "signedState",
                           "seen", "dependencies", "hasEmbeddedWebExtension", "mpcOptedOut",
                           "userPermissions", "icons", "iconURL", "icon64URL"];
 
 // Time to wait before async save of XPI JSON database, in milliseconds
@@ -187,33 +186,40 @@ function copyProperties(aObject, aProper
  * XPIProvider.AddonInternal created from an addon's manifest
  * @constructor
  * @param aLoaded
  *        Addon data fields loaded from JSON or the addon manifest.
  */
 function DBAddonInternal(aLoaded) {
   AddonInternal.call(this);
 
+  if (aLoaded.descriptor) {
+    if (!aLoaded.path) {
+      aLoaded.path = descriptorToPath(aLoaded.descriptor);
+    }
+    delete aLoaded.descriptor;
+  }
+
   copyProperties(aLoaded, PROP_JSON_FIELDS, this);
 
   if (!this.dependencies)
     this.dependencies = [];
   Object.freeze(this.dependencies);
 
   if (aLoaded._installLocation) {
     this._installLocation = aLoaded._installLocation;
     this.location = aLoaded._installLocation.name;
   } else if (aLoaded.location) {
     this._installLocation = XPIProvider.installLocationsByName[this.location];
   }
 
   this._key = this.location + ":" + this.id;
 
   if (!aLoaded._sourceBundle) {
-    throw new Error("Expected passed argument to contain a descriptor");
+    throw new Error("Expected passed argument to contain a path");
   }
 
   this._sourceBundle = aLoaded._sourceBundle;
 
   XPCOMUtils.defineLazyGetter(this, "pendingUpgrade", function() {
       for (let install of XPIProvider.installs) {
         if (install.state == AddonManager.STATE_INSTALLED &&
             !(install.addon.inDatabase) &&
@@ -490,21 +496,20 @@ this.XPIDatabase = {
         // When we rev the schema of the JSON database, we need to make sure we
         // force the DB to save so that the DB_SCHEMA value in the JSON file and
         // the preference are updated.
       }
       // If we got here, we probably have good data
       // Make AddonInternal instances from the loaded data and save them
       let addonDB = new Map();
       for (let loadedAddon of inputAddons.addons) {
-        loadedAddon._sourceBundle = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
         try {
-          loadedAddon._sourceBundle.persistentDescriptor = loadedAddon.descriptor;
+          loadedAddon._sourceBundle = new nsIFile(loadedAddon.path);
         } catch (e) {
-          // We can fail here when the descriptor is invalid, usually from the
+          // We can fail here when the path is invalid, usually from the
           // wrong OS
           logger.warn("Could not find source bundle for add-on " + loadedAddon.id, e);
         }
 
         let newAddon = new DBAddonInternal(loadedAddon);
         addonDB.set(newAddon._key, newAddon);
       }
       parseTimer.done();
@@ -630,72 +635,37 @@ this.XPIDatabase = {
     if (XPIStates.size == 0) {
       // No extensions installed, so we're done
       logger.debug("Rebuilding XPI database with no extensions");
       return;
     }
 
     // If there is no migration data then load the list of add-on directories
     // that were active during the last run
-    if (!this.migrateData)
-      this.activeBundles = this.getActiveBundles();
+    if (!this.migrateData) {
+      this.activeBundles = Array.from(XPIStates.initialEnabledAddons(),
+                                      addon => addon.path);
+      if (!this.activeBundles.length)
+        this.activeBundles = null;
+    }
+
 
     if (aRebuildOnError) {
       logger.warn("Rebuilding add-ons database from installed extensions.");
       try {
         XPIDatabaseReconcile.processFileChanges({}, false);
       } catch (e) {
         logger.error("Failed to rebuild XPI database from installed extensions", e);
       }
       // Make sure to update the active add-ons and add-ons list on shutdown
       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
     }
   },
 
   /**
-   * Gets the list of file descriptors of active extension directories or XPI
-   * files from the add-ons list. This must be loaded from disk since the
-   * directory service gives no easy way to get both directly. This list doesn't
-   * include themes as preferences already say which theme is currently active
-   *
-   * @return an array of persistent descriptors for the directories
-   */
-  getActiveBundles() {
-    let bundles = [];
-
-    // non-bootstrapped extensions
-    let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
-                                       true);
-
-    if (!addonsList.exists())
-      // XXX Irving believes this is broken in the case where there is no
-      // extensions.ini but there are bootstrap extensions (e.g. Android)
-      return null;
-
-    try {
-      let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
-                         .getService(Ci.nsIINIParserFactory);
-      let parser = iniFactory.createINIParser(addonsList);
-      let keys = parser.getKeys("ExtensionDirs");
-
-      while (keys.hasMore())
-        bundles.push(parser.getString("ExtensionDirs", keys.getNext()));
-    } catch (e) {
-      logger.warn("Failed to parse extensions.ini", e);
-      return null;
-    }
-
-    // Also include the list of active bootstrapped extensions
-    for (let id in XPIProvider.bootstrappedAddons)
-      bundles.push(XPIProvider.bootstrappedAddons[id].descriptor);
-
-    return bundles;
-  },
-
-  /**
    * Shuts down the database connection and releases all cached objects.
    * Return: Promise{integer} resolves / rejects with the result of the DB
    *                          flush after the database is flushed and
    *                          all cleanup is done
    */
   shutdown() {
     logger.debug("shutdown");
     if (this.initialized) {
@@ -928,29 +898,29 @@ this.XPIDatabase = {
     return _filterDB(this.addonDB, aAddon => true);
   },
 
   /**
    * Synchronously adds an AddonInternal's metadata to the database.
    *
    * @param  aAddon
    *         AddonInternal to add
-   * @param  aDescriptor
-   *         The file descriptor of the add-on
+   * @param  aPath
+   *         The file path of the add-on
    * @return The DBAddonInternal that was added to the database
    */
-  addAddonMetadata(aAddon, aDescriptor) {
+  addAddonMetadata(aAddon, aPath) {
     if (!this.addonDB) {
       AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_addMetadata",
           XPIProvider.runPhase);
       this.syncLoadDB(false);
     }
 
     let newAddon = new DBAddonInternal(aAddon);
-    newAddon.descriptor = aDescriptor;
+    newAddon.path = aPath;
     this.addonDB.set(newAddon._key, newAddon);
     if (newAddon.visible) {
       this.makeAddonVisible(newAddon);
     }
 
     this.saveChanges();
     return newAddon;
   },
@@ -958,61 +928,72 @@ this.XPIDatabase = {
   /**
    * Synchronously updates an add-on's metadata in the database. Currently just
    * removes and recreates.
    *
    * @param  aOldAddon
    *         The DBAddonInternal to be replaced
    * @param  aNewAddon
    *         The new AddonInternal to add
-   * @param  aDescriptor
-   *         The file descriptor of the add-on
+   * @param  aPath
+   *         The file path of the add-on
    * @return The DBAddonInternal that was added to the database
    */
-  updateAddonMetadata(aOldAddon, aNewAddon, aDescriptor) {
+  updateAddonMetadata(aOldAddon, aNewAddon, aPath) {
     this.removeAddonMetadata(aOldAddon);
     aNewAddon.syncGUID = aOldAddon.syncGUID;
     aNewAddon.installDate = aOldAddon.installDate;
     aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
     aNewAddon.foreignInstall = aOldAddon.foreignInstall;
     aNewAddon.seen = aOldAddon.seen;
     aNewAddon.active = (aNewAddon.visible && !aNewAddon.disabled && !aNewAddon.pendingUninstall);
 
     // addAddonMetadata does a saveChanges()
-    return this.addAddonMetadata(aNewAddon, aDescriptor);
+    return this.addAddonMetadata(aNewAddon, aPath);
   },
 
   /**
    * Synchronously removes an add-on from the database.
    *
    * @param  aAddon
    *         The DBAddonInternal being removed
    */
   removeAddonMetadata(aAddon) {
     this.addonDB.delete(aAddon._key);
     this.saveChanges();
   },
 
+  updateXPIStates(addon) {
+    let xpiState = XPIStates.getAddon(addon.location, addon.id);
+    if (xpiState) {
+      xpiState.syncWithDB(addon);
+      XPIStates.save();
+    }
+  },
+
   /**
    * Synchronously marks a DBAddonInternal as visible marking all other
    * instances with the same ID as not visible.
    *
    * @param  aAddon
    *         The DBAddonInternal to make visible
    */
   makeAddonVisible(aAddon) {
     logger.debug("Make addon " + aAddon._key + " visible");
     for (let [, otherAddon] of this.addonDB) {
       if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) {
         logger.debug("Hide addon " + otherAddon._key);
         otherAddon.visible = false;
         otherAddon.active = false;
+
+        this.updateXPIStates(otherAddon);
       }
     }
     aAddon.visible = true;
+    this.updateXPIStates(aAddon);
     this.saveChanges();
   },
 
   /**
    * Synchronously marks a given add-on ID visible in a given location,
    * instances with the same ID as not visible.
    *
    * @param  aAddon
@@ -1024,21 +1005,23 @@ this.XPIDatabase = {
     for (let [, addon] of this.addonDB) {
       if (addon.id != aId) {
         continue;
       }
       if (addon.location == aLocation) {
         logger.debug("Reveal addon " + addon._key);
         addon.visible = true;
         addon.active = true;
+        this.updateXPIStates(addon);
         result = addon;
       } else {
         logger.debug("Hide addon " + addon._key);
         addon.visible = false;
         addon.active = false;
+        this.updateXPIStates(addon);
       }
     }
     this.saveChanges();
     return result;
   },
 
   /**
    * Synchronously sets properties for an add-on.
@@ -1088,16 +1071,25 @@ this.XPIDatabase = {
   updateAddonActive(aAddon, aActive) {
     logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive);
 
     aAddon.active = aActive;
     this.saveChanges();
   },
 
   updateAddonsBlockingE10s() {
+    if (!this.addonDB) {
+      // jank-tastic! Must synchronously load DB if the theme switches from
+      // an XPI theme to a lightweight theme before the DB has loaded,
+      // because we're called from sync XPIProvider.addonChanged
+      logger.warn("Synchronous load of XPI database due to updateAddonsBlockingE10s()");
+      AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_byType", XPIProvider.runPhase);
+      this.syncLoadDB(true);
+    }
+
     let blockE10s = false;
 
     Preferences.set(PREF_E10S_HAS_NONEXEMPT_ADDON, false);
     for (let [, addon] of this.addonDB) {
       let active = (addon.visible && !addon.disabled && !addon.pendingUninstall);
 
       if (active && XPIProvider.isBlockingE10s(addon)) {
         blockE10s = true;
@@ -1136,104 +1128,16 @@ this.XPIDatabase = {
     for (let [, addon] of this.addonDB) {
       let newActive = (addon.visible && !addon.disabled && !addon.pendingUninstall);
       if (newActive != addon.active) {
         addon.active = newActive;
         this.saveChanges();
       }
     }
   },
-
-  /**
-   * Writes out the XPI add-ons list for the platform to read.
-   * @return true if the file was successfully updated, false otherwise
-   */
-  writeAddonsList() {
-    if (!this.addonDB) {
-      // force the DB to load
-      AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_writeList",
-          XPIProvider.runPhase);
-      this.syncLoadDB(true);
-    }
-    Services.appinfo.invalidateCachesOnRestart();
-
-    let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
-                                       true);
-    let enabledAddons = [];
-    let text = "[ExtensionDirs]\r\n";
-    let count = 0;
-    let fullCount = 0;
-
-    let activeAddons = _filterDB(
-      this.addonDB,
-      aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme"));
-
-    for (let row of activeAddons) {
-      text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
-      enabledAddons.push(encodeURIComponent(row.id) + ":" +
-                         encodeURIComponent(row.version));
-    }
-    fullCount += count;
-
-    // The selected skin may come from an inactive theme (the default theme
-    // when a lightweight theme is applied for example)
-    text += "\r\n[ThemeDirs]\r\n";
-
-    let activeTheme = _findAddon(
-      this.addonDB,
-      aAddon => (aAddon.type == "theme") &&
-                (aAddon.internalName == XPIProvider.selectedSkin));
-    count = 0;
-    if (activeTheme) {
-      text += "Extension" + (count++) + "=" + activeTheme.descriptor + "\r\n";
-      enabledAddons.push(encodeURIComponent(activeTheme.id) + ":" +
-                         encodeURIComponent(activeTheme.version));
-    }
-    fullCount += count;
-
-    text += "\r\n[MultiprocessIncompatibleExtensions]\r\n";
-
-    count = 0;
-    for (let row of activeAddons) {
-      if (!row.multiprocessCompatible) {
-        text += "Extension" + (count++) + "=" + row.id + "\r\n";
-      }
-    }
-
-    if (fullCount > 0) {
-      logger.debug("Writing add-ons list");
-
-      try {
-        let addonsListTmp = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST + ".tmp"],
-                                              true);
-        var fos = FileUtils.openFileOutputStream(addonsListTmp);
-        fos.write(text, text.length);
-        fos.close();
-        addonsListTmp.moveTo(addonsListTmp.parent, FILE_XPI_ADDONS_LIST);
-
-        Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(","));
-      } catch (e) {
-        logger.error("Failed to write add-ons list to profile directory", e);
-        return false;
-      }
-    } else {
-      if (addonsList.exists()) {
-        logger.debug("Deleting add-ons list");
-        try {
-          addonsList.remove(false);
-        } catch (e) {
-          logger.error("Failed to remove " + addonsList.path, e);
-          return false;
-        }
-      }
-
-      Services.prefs.clearUserPref(PREF_EM_ENABLED_ADDONS);
-    }
-    return true;
-  }
 };
 
 this.XPIDatabaseReconcile = {
   /**
    * Returns a map of ID -> add-on. When the same add-on ID exists in multiple
    * install locations the highest priority location is chosen.
    */
   flattenByID(addonMap, hideLocation) {
@@ -1316,18 +1220,17 @@ this.XPIDatabaseReconcile = {
     // If it's a new install and we haven't yet loaded the manifest then it
     // must be something dropped directly into the install location
     let isDetectedInstall = isNewInstall && !aNewAddon;
 
     // Load the manifest if necessary and sanity check the add-on ID
     try {
       if (!aNewAddon) {
         // Load the manifest from the add-on.
-        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-        file.persistentDescriptor = aAddonState.descriptor;
+        let file = new nsIFile(aAddonState.path);
         aNewAddon = syncLoadManifestFromFile(file, aInstallLocation);
       }
       // The add-on in the manifest should match the add-on ID.
       if (aNewAddon.id != aId) {
         throw new Error("Invalid addon ID: expected addon ID " + aId +
                         ", found " + aNewAddon.id + " in manifest");
       }
     } catch (e) {
@@ -1372,17 +1275,17 @@ this.XPIDatabaseReconcile = {
 
         // If we don't have an old app version then this is a new profile in
         // which case just mark any sideloaded add-ons as already seen.
         aNewAddon.seen = (aInstallLocation.name != KEY_APP_PROFILE &&
                           !aOldAppVersion);
       }
     }
 
-    return XPIDatabase.addAddonMetadata(aNewAddon, aAddonState.descriptor);
+    return XPIDatabase.addAddonMetadata(aNewAddon, aAddonState.path);
   },
 
   /**
    * Called when an add-on has been removed.
    *
    * @param  aOldAddon
    *         The AddonInternal as it appeared the last time the application
    *         ran
@@ -1413,18 +1316,17 @@ this.XPIDatabaseReconcile = {
    *         changing this add-on
    */
   updateMetadata(aInstallLocation, aOldAddon, aAddonState, aNewAddon) {
     logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
 
     try {
       // If there isn't an updated install manifest for this add-on then load it.
       if (!aNewAddon) {
-        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-        file.persistentDescriptor = aAddonState.descriptor;
+        let file = new nsIFile(aAddonState.path);
         aNewAddon = syncLoadManifestFromFile(file, aInstallLocation);
         applyBlocklistChanges(aOldAddon, aNewAddon);
 
         // Carry over any pendingUninstall state to add-ons modified directly
         // in the profile. This is important when the attempt to remove the
         // add-on in processPendingFileChanges failed and caused an mtime
         // change to the add-ons files.
         aNewAddon.pendingUninstall = aOldAddon.pendingUninstall;
@@ -1445,37 +1347,37 @@ this.XPIDatabaseReconcile = {
 
       return null;
     }
 
     // Set the additional properties on the new AddonInternal
     aNewAddon.updateDate = aAddonState.mtime;
 
     // Update the database
-    return XPIDatabase.updateAddonMetadata(aOldAddon, aNewAddon, aAddonState.descriptor);
+    return XPIDatabase.updateAddonMetadata(aOldAddon, aNewAddon, aAddonState.path);
   },
 
   /**
-   * Updates an add-on's descriptor for when the add-on has moved in the
+   * Updates an add-on's path for when the add-on has moved in the
    * filesystem but hasn't changed in any other way.
    *
    * @param  aInstallLocation
    *         The install location containing the add-on
    * @param  aOldAddon
    *         The AddonInternal as it appeared the last time the application
    *         ran
    * @param  aAddonState
    *         The new state of the add-on
    * @return a boolean indicating if flushing caches is required to complete
    *         changing this add-on
    */
-  updateDescriptor(aInstallLocation, aOldAddon, aAddonState) {
-    logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor);
-    aOldAddon.descriptor = aAddonState.descriptor;
-    aOldAddon._sourceBundle.persistentDescriptor = aAddonState.descriptor;
+  updatePath(aInstallLocation, aOldAddon, aAddonState) {
+    logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.path);
+    aOldAddon.path = aAddonState.path;
+    aOldAddon._sourceBundle = new nsIFile(aAddonState.path);
 
     return aOldAddon;
   },
 
   /**
    * Called when no change has been detected for an add-on's metadata but the
    * application has changed so compatibility may have changed.
    *
@@ -1500,27 +1402,25 @@ this.XPIDatabaseReconcile = {
   updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aOldAppVersion,
                       aOldPlatformVersion, aReloadMetadata) {
     logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name);
 
     // If updating from a version of the app that didn't support signedState
     // then fetch that property now
     if (aOldAddon.signedState === undefined && ADDON_SIGNING &&
         SIGNED_TYPES.has(aOldAddon.type)) {
-      let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-      file.persistentDescriptor = aAddonState.descriptor;
+      let file = new nsIFile(aAddonState.path);
       let manifest = syncLoadManifestFromFile(file, aInstallLocation);
       aOldAddon.signedState = manifest.signedState;
     }
 
     // May be updating from a version of the app that didn't support all the
     // properties of the currently-installed add-ons.
     if (aReloadMetadata) {
-      let file = new nsIFile()
-      file.persistentDescriptor = aAddonState.descriptor;
+      let file = new nsIFile(aAddonState.path);
       let manifest = syncLoadManifestFromFile(file, aInstallLocation);
 
       // Avoid re-reading these properties from manifest,
       // use existing addon instead.
       // TODO - consider re-scanning for targetApplications.
       let remove = ["syncGUID", "foreignInstall", "visible", "active",
                     "userDisabled", "applyBackgroundUpdates", "sourceURI",
                     "releaseNotesURI", "targetApplications"];
@@ -1567,17 +1467,17 @@ this.XPIDatabaseReconcile = {
       if (!(aInstallLocation.name in aManifests))
         return null;
       if (!(aId in aManifests[aInstallLocation.name]))
         return null;
       return aManifests[aInstallLocation.name][aId];
     };
 
     // Add-ons loaded from the database can have an uninitialized _sourceBundle
-    // if the descriptor was invalid. Swallow that error and say they don't exist.
+    // if the path was invalid. Swallow that error and say they don't exist.
     let exists = (aAddon) => {
       try {
         return aAddon._sourceBundle.exists();
       } catch (e) {
         if (e.result == Cr.NS_ERROR_NOT_INITIALIZED)
           return false;
         throw e;
       }
@@ -1624,30 +1524,31 @@ this.XPIDatabaseReconcile = {
                 XPIProvider.setTelemetry(oldAddon.id, "olderFile", {
                   mtime: xpiState.mtime,
                   oldtime: oldAddon.updateDate
                 });
               }
             }
 
             let wasDisabled = oldAddon.appDisabled;
+            let oldPath = oldAddon.path || descriptorToPath(oldAddon.descriptor);
 
             // The add-on has changed if the modification time has changed, if
             // we have an updated manifest for it, or if the schema version has
             // changed.
             //
             // Also reload the metadata for add-ons in the application directory
             // when the application version has changed.
             let newAddon = loadedManifest(installLocation, id);
             if (newAddon || oldAddon.updateDate != xpiState.mtime ||
                 (aUpdateCompatibility && (installLocation.name == KEY_APP_GLOBAL ||
                                           installLocation.name == KEY_APP_SYSTEM_DEFAULTS))) {
               newAddon = this.updateMetadata(installLocation, oldAddon, xpiState, newAddon);
-            } else if (oldAddon.descriptor != xpiState.descriptor) {
-              newAddon = this.updateDescriptor(installLocation, oldAddon, xpiState);
+            } else if (oldPath != xpiState.path) {
+              newAddon = this.updatePath(installLocation, oldAddon, xpiState);
             } else if (aUpdateCompatibility || aSchemaChange) {
               // Check compatility when the application version and/or schema
               // version has changed. A schema change also reloads metadata from
               // the manifests.
               newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState,
                                                   aOldAppVersion, aOldPlatformVersion,
                                                   aSchemaChange);
             } else {
@@ -1718,43 +1619,42 @@ this.XPIDatabaseReconcile = {
       // Hide the system add-on updates if any are invalid.
       logger.info("One or more updated system add-ons invalid, falling back to defaults.");
       hideLocation = KEY_APP_SYSTEM_ADDONS;
     }
 
     let previousVisible = this.getVisibleAddons(previousAddons);
     let currentVisible = this.flattenByID(currentAddons, hideLocation);
     let sawActiveTheme = false;
-    XPIProvider.bootstrappedAddons = {};
 
     // Pass over the new set of visible add-ons, record any changes that occured
     // during startup and call bootstrap install/uninstall scripts as necessary
     for (let [id, currentAddon] of currentVisible) {
       let previousAddon = previousVisible.get(id);
 
       // Note if any visible add-on is not in the application install location
       if (currentAddon._installLocation.name != KEY_APP_GLOBAL)
         XPIProvider.allAppGlobal = false;
 
-      let isActive = !currentAddon.disabled;
+      let isActive = !currentAddon.disabled && !currentAddon.pendingUninstall;
       let wasActive = previousAddon ? previousAddon.active : currentAddon.active
 
       if (!previousAddon) {
         // If we had a manifest for this add-on it was a staged install and
         // so wasn't something recovered from a corrupt database
         let wasStaged = !!loadedManifest(currentAddon._installLocation, id);
 
         // We might be recovering from a corrupt database, if so use the
         // list of known active add-ons to update the new add-on
         if (!wasStaged && XPIDatabase.activeBundles) {
           // For themes we know which is active by the current skin setting
           if (currentAddon.type == "theme")
             isActive = currentAddon.internalName == XPIProvider.currentSkin;
           else
-            isActive = XPIDatabase.activeBundles.indexOf(currentAddon.descriptor) != -1;
+            isActive = XPIDatabase.activeBundles.includes(currentAddon.path);
 
           // If the add-on wasn't active and it isn't already disabled in some way
           // then it was probably either softDisabled or userDisabled
           if (!isActive && !currentAddon.disabled) {
             // If the add-on is softblocked then assume it is softDisabled
             if (currentAddon.blocklistState == Blocklist.STATE_SOFTBLOCKED)
               currentAddon.softDisabled = true;
             else
@@ -1796,18 +1696,17 @@ this.XPIDatabaseReconcile = {
             XPIProvider.unloadBootstrapScope(previousAddon.id);
           }
 
           // Make sure to flush the cache when an old add-on has gone away
           flushChromeCaches();
 
           if (currentAddon.bootstrap) {
             // Visible bootstrapped add-ons need to have their install method called
-            let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-            file.persistentDescriptor = currentAddon._sourceBundle.persistentDescriptor;
+            let file = currentAddon._sourceBundle.clone();
             XPIProvider.callBootstrapMethod(currentAddon, file,
                                             "install", installReason,
                                             { oldVersion: previousAddon.version });
             if (currentAddon.disabled)
               XPIProvider.unloadBootstrapScope(currentAddon.id);
           }
         }
 
@@ -1816,29 +1715,16 @@ this.XPIDatabaseReconcile = {
                                 : AddonManager.STARTUP_CHANGE_DISABLED;
           AddonManagerPrivate.addStartupChange(change, id);
         }
       }
 
       XPIDatabase.makeAddonVisible(currentAddon);
       currentAddon.active = isActive;
 
-      // Make sure the bootstrap information is up to date for this ID
-      if (currentAddon.bootstrap && currentAddon.active) {
-        XPIProvider.bootstrappedAddons[id] = {
-          version: currentAddon.version,
-          type: currentAddon.type,
-          descriptor: currentAddon._sourceBundle.persistentDescriptor,
-          multiprocessCompatible: currentAddon.multiprocessCompatible,
-          runInSafeMode: canRunInSafeMode(currentAddon),
-          dependencies: currentAddon.dependencies,
-          hasEmbeddedWebExtension: currentAddon.hasEmbeddedWebExtension,
-        };
-      }
-
       if (currentAddon.active && currentAddon.internalName == XPIProvider.selectedSkin)
         sawActiveTheme = true;
     }
 
     // Pass over the set of previously visible add-ons that have now gone away
     // and record the change.
     for (let [id, previousAddon] of previousVisible) {
       if (currentVisible.has(id))
@@ -1849,16 +1735,17 @@ this.XPIDatabaseReconcile = {
       // If the previous add-on was bootstrapped and still exists then call its
       // uninstall method.
       if (previousAddon.bootstrap && exists(previousAddon)) {
         XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
                                         "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         XPIProvider.unloadBootstrapScope(previousAddon.id);
       }
       AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
+      XPIStates.removeAddon(previousAddon.location, id);
 
       // Make sure to flush the cache when an old add-on has gone away
       flushChromeCaches();
     }
 
     // Make sure add-ons from hidden locations are marked invisible and inactive
     let locationAddonMap = currentAddons.get(hideLocation);
     if (locationAddonMap) {
@@ -1879,17 +1766,15 @@ this.XPIDatabaseReconcile = {
     for (let [locationName, locationAddonMap] of currentAddons) {
       for (let [id, addon] of locationAddonMap) {
         let xpiState = XPIStates.getAddon(locationName, id);
         xpiState.syncWithDB(addon);
       }
     }
     XPIStates.save();
 
-    XPIProvider.persistBootstrappedAddons();
-
     // Clear out any cached migration data.
     XPIDatabase.migrateData = null;
     XPIDatabase.saveChanges();
 
     return true;
   },
 }
--- a/toolkit/mozapps/extensions/moz.build
+++ b/toolkit/mozapps/extensions/moz.build
@@ -9,16 +9,17 @@ SPHINX_TREES['addon-manager'] = 'docs'
 if CONFIG['MOZ_BUILD_APP'] == 'mobile/android':
     DEFINES['MOZ_FENNEC'] = True
 
 DIRS += ['internal']
 TEST_DIRS += ['test']
 
 XPIDL_SOURCES += [
     'amIAddonManager.idl',
+    'amIAddonManagerStartup.idl',
     'amIAddonPathService.idl',
     'amIWebInstallPrompt.idl',
 ]
 
 XPIDL_MODULE = 'extensions'
 
 EXTRA_COMPONENTS += [
     'addonManager.js',
@@ -39,22 +40,24 @@ EXTRA_JS_MODULES += [
     'DeferredSave.jsm',
     'LightweightThemeManager.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXPORTS.mozilla += [
     'AddonContentPolicy.h',
+    'AddonManagerStartup.h',
     'AddonManagerWebAPI.h',
     'AddonPathService.h',
 ]
 
 UNIFIED_SOURCES += [
     'AddonContentPolicy.cpp',
+    'AddonManagerStartup.cpp',
     'AddonManagerWebAPI.cpp',
     'AddonPathService.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
 ]
 
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -58,16 +58,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "MockAsyncShutdown",
                                   "resource://testing-common/AddonTestUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
                                   "resource://testing-common/MockRegistrar.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
                                   "resource://testing-common/MockRegistry.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
+                                   "@mozilla.org/addons/addon-manager-startup;1",
+                                   "amIAddonManagerStartup");
+
 const {
   awaitPromise,
   createAppInfo,
   createInstallRDF,
   createTempWebExtensionFile,
   createUpdateRDF,
   getFileForAddon,
   manuallyUninstall,
@@ -100,19 +104,19 @@ AddonTestUtils.init(this);
 AddonTestUtils.overrideCertDB();
 
 Object.defineProperty(this, "gAppInfo", {
   get() {
     return AddonTestUtils.appInfo;
   },
 });
 
-Object.defineProperty(this, "gExtensionsINI", {
+Object.defineProperty(this, "gAddonStartup", {
   get() {
-    return AddonTestUtils.extensionsINI.clone();
+    return AddonTestUtils.addonStartup.clone();
   },
 });
 
 Object.defineProperty(this, "gInternalManager", {
   get() {
     return AddonTestUtils.addonIntegrationService.QueryInterface(AM_Ci.nsITimerCallback);
   },
 });
@@ -198,16 +202,18 @@ this.BootstrapMonitor = {
 
   // Contain the last state of shutdown and uninstall calls for an add-on
   stopped: new Map(),
   uninstalled: new Map(),
 
   startupPromises: [],
   installPromises: [],
 
+  restartfulIds: new Set(),
+
   init() {
     this.inited = true;
     Services.obs.addObserver(this, "bootstrapmonitor-event");
   },
 
   shutdownCheck() {
     if (!this.inited)
       return;
@@ -314,19 +320,23 @@ this.BootstrapMonitor = {
       // consistent.
       if (info.reason == 2 /* APP_SHUTDOWN */)
         Components.manager.removeBootstrappedManifestLocation(installPath);
     } else {
       this.checkAddonNotStarted(id);
     }
 
     if (info.event == "uninstall") {
-      // Chrome should be unregistered at this point
-      let isRegistered = isManifestRegistered(installPath);
-      do_check_false(isRegistered);
+      // We currently support registering, but not unregistering,
+      // restartful add-on manifests during xpcshell AOM "restarts".
+      if (!this.restartfulIds.has(id)) {
+        // Chrome should be unregistered at this point
+        let isRegistered = isManifestRegistered(installPath);
+        do_check_false(isRegistered);
+      }
 
       this.installed.delete(id);
       this.uninstalled.set(id, info)
     } else if (info.event == "startup") {
       this.started.set(id, info);
 
       // Chrome should be registered at this point
       let isRegistered = isManifestRegistered(installPath);
@@ -361,18 +371,17 @@ function do_check_in_crash_annotation(aI
     return;
 
   if (!("Add-ons" in gAppInfo.annotations)) {
     do_check_false(true);
     return;
   }
 
   let addons = gAppInfo.annotations["Add-ons"].split(",");
-  do_check_false(addons.indexOf(encodeURIComponent(aId) + ":" +
-                                encodeURIComponent(aVersion)) < 0);
+  do_check_true(addons.includes(`${encodeURIComponent(aId)}:${encodeURIComponent(aVersion)}`));
 }
 
 /**
  * Tests that an add-on does not appear in the crash report annotations, if
  * crash reporting is enabled. The test will fail if the add-on is in the
  * annotation.
  * @param  aId
  *         The ID of the add-on
@@ -384,18 +393,17 @@ function do_check_not_in_crash_annotatio
     return;
 
   if (!("Add-ons" in gAppInfo.annotations)) {
     do_check_true(true);
     return;
   }
 
   let addons = gAppInfo.annotations["Add-ons"].split(",");
-  do_check_true(addons.indexOf(encodeURIComponent(aId) + ":" +
-                               encodeURIComponent(aVersion)) < 0);
+  do_check_false(addons.includes(`${encodeURIComponent(aId)}:${encodeURIComponent(aVersion)}`));
 }
 
 /**
  * Returns a testcase xpi
  *
  * @param  aName
  *         The name of the testcase (without extension)
  * @return an nsIFile pointing to the testcase xpi
--- a/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
@@ -99,16 +99,22 @@ function checkChange(XS, aPath, aChange)
 }
 
 // Get a reference to the XPIState (loaded by startupManager) so we can unit test it.
 function getXS() {
   let XPI = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
   return XPI.XPIStates;
 }
 
+async function getXSJSON() {
+  await AddonTestUtils.loadAddonsList(true);
+
+  return aomStartup.readStartupData();
+}
+
 add_task(async function detect_touches() {
   startupManager();
   let [/* pe */, pd, /* ue */, ud] = await promiseAddonsByIDs([
          "packed-enabled@tests.mozilla.org",
          "packed-disabled@tests.mozilla.org",
          "unpacked-enabled@tests.mozilla.org",
          "unpacked-disabled@tests.mozilla.org"
          ]);
@@ -164,49 +170,50 @@ add_task(async function detect_touches()
   /*
    * When we enable an unpacked add-on that was modified while it was
    * disabled, we reflect the new timestamp in the add-on DB (otherwise, we'll
    * think it changed on next restart).
    */
   ud.userDisabled = false;
   let xState = XS.getAddon("app-profile", ud.id);
   do_check_true(xState.enabled);
-  do_check_eq(xState.scanTime, ud.updateDate.getTime());
+  do_check_eq(xState.mtime, ud.updateDate.getTime());
 });
 
 /*
  * Uninstalling bootstrap add-ons should immediately remove them from the
  * extensions.xpiState preference.
  */
 add_task(async function uninstall_bootstrap() {
   let [pe, /* pd, ue, ud */] = await promiseAddonsByIDs([
          "packed-enabled@tests.mozilla.org",
          "packed-disabled@tests.mozilla.org",
          "unpacked-enabled@tests.mozilla.org",
          "unpacked-disabled@tests.mozilla.org"
          ]);
   pe.uninstall();
-  let xpiState = Services.prefs.getCharPref("extensions.xpiState");
-  do_check_false(xpiState.includes("\"packed-enabled@tests.mozilla.org\""));
+
+  let xpiState = await getXSJSON();
+  do_check_false("packed-enabled@tests.mozilla.org" in xpiState["app-profile"].addons);
 });
 
 /*
  * Installing a restartless add-on should immediately add it to XPIState
  */
 add_task(async function install_bootstrap() {
   let XS = getXS();
 
   let installer = await promiseInstallFile(
     do_get_addon("test_bootstrap1_1"));
 
   let newAddon = installer.addon;
   let xState = XS.getAddon("app-profile", newAddon.id);
   do_check_true(!!xState);
   do_check_true(xState.enabled);
-  do_check_eq(xState.scanTime, newAddon.updateDate.getTime());
+  do_check_eq(xState.mtime, newAddon.updateDate.getTime());
   newAddon.uninstall();
 });
 
 /*
  * Installing an add-on that requires restart doesn't add to XPIState
  * until after the restart; disable and enable happen immediately so that
  * the next restart won't / will scan as necessary on the next restart,
  * uninstalling it marks XPIState as disabled immediately
@@ -229,17 +236,17 @@ add_task(async function install_restart(
   newAddon = null;
   await promiseRestartManager();
   XS = getXS();
 
   newAddon = await promiseAddonByID(newID);
   xState = XS.getAddon("app-profile", newID);
   do_check_true(xState);
   do_check_true(xState.enabled);
-  do_check_eq(xState.scanTime, newAddon.updateDate.getTime());
+  do_check_eq(xState.mtime, newAddon.updateDate.getTime());
 
   // Check that XPIState enabled flag is updated immediately,
   // and doesn't change over restart.
   newAddon.userDisabled = true;
   do_check_false(xState.enabled);
   XS = null;
   newAddon = null;
   await promiseRestartManager();
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
@@ -76,18 +76,22 @@ function getInstallOldVersion() {
 }
 
 function getUninstallNewVersion() {
   let info = BootstrapMonitor.uninstalled.get(ID1);
   return info ? info.data.newVersion : undefined;
 }
 
 function do_check_bootstrappedPref(aCallback) {
-  let data = Services.prefs.getCharPref("extensions.bootstrappedAddons");
-  data = JSON.parse(data);
+  let XPIScope = AM_Cu.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+
+  let data = {};
+  for (let entry of XPIScope.XPIStates.bootstrappedAddons()) {
+    data[entry.id] = entry;
+  }
 
   AddonManager.getAddonsByTypes(["extension"], function(aAddons) {
     for (let addon of aAddons) {
       if (!addon.id.endsWith("@tests.mozilla.org"))
         continue;
       if (!addon.isActive)
         continue;
       if (addon.operationsRequiringRestart != AddonManager.OP_NEEDS_RESTART_NONE)
@@ -95,33 +99,33 @@ function do_check_bootstrappedPref(aCall
 
       do_check_true(addon.id in data);
       let addonData = data[addon.id];
       delete data[addon.id];
 
       do_check_eq(addonData.version, addon.version);
       do_check_eq(addonData.type, addon.type);
       let file = addon.getResourceURI().QueryInterface(Components.interfaces.nsIFileURL).file;
-      do_check_eq(addonData.descriptor, file.persistentDescriptor);
+      do_check_eq(addonData.path, file.path);
     }
     do_check_eq(Object.keys(data).length, 0);
 
     do_execute_soon(aCallback);
   });
 }
 
 
 function run_test() {
   do_test_pending();
 
   startupManager();
 
   do_check_false(gExtensionsJSON.exists());
 
-  do_check_false(gExtensionsINI.exists());
+  do_check_false(gAddonStartup.exists());
 
   run_test_1();
 }
 
 // Tests that installing doesn't require a restart
 function run_test_1() {
   prepare_test({ }, [
     "onNewInstall"
@@ -165,18 +169,16 @@ function run_test_1() {
       // startup should not have been called yet.
       BootstrapMonitor.checkAddonNotStarted(ID1);
     });
     install.install();
   });
 }
 
 function check_test_1(installSyncGUID) {
-  do_check_false(gExtensionsINI.exists());
-
   AddonManager.getAllInstalls(function(installs) {
     // There should be no active installs now since the install completed and
     // doesn't require a restart.
     do_check_eq(installs.length, 0);
 
     AddonManager.getAddonByID(ID1, function(b1) {
       do_check_neq(b1, null);
       do_check_eq(b1.version, "1.0");
@@ -254,17 +256,17 @@ function run_test_3() {
   do_check_eq(getShutdownNewVersion(), undefined);
   startupManager(false);
   BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
   BootstrapMonitor.checkAddonNotStarted(ID1);
   do_check_eq(getShutdownReason(), ADDON_DISABLE);
   do_check_eq(getShutdownNewVersion(), undefined);
   do_check_not_in_crash_annotation(ID1, "1.0");
 
-  do_check_false(gExtensionsINI.exists());
+  do_check_true(gAddonStartup.exists());
 
   AddonManager.getAddonByID(ID1, function(b1) {
     do_check_neq(b1, null);
     do_check_eq(b1.version, "1.0");
     do_check_false(b1.appDisabled);
     do_check_true(b1.userDisabled);
     do_check_false(b1.isActive);
 
@@ -1199,17 +1201,17 @@ function check_test_23() {
 }
 
 // Tests that we recover from a broken preference
 function run_test_24() {
   do_print("starting 24");
 
   Promise.all([BootstrapMonitor.promiseAddonStartup(ID2),
               promiseInstallAllFiles([do_get_addon("test_bootstrap1_1"), do_get_addon("test_bootstrap2_1")])])
-         .then(function test_24_pref() {
+         .then(async function test_24_pref() {
     do_print("test 24 got prefs");
     BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
     BootstrapMonitor.checkAddonStarted(ID1, "1.0");
     BootstrapMonitor.checkAddonInstalled(ID2, "1.0");
     BootstrapMonitor.checkAddonStarted(ID2, "1.0");
 
     restartManager();
 
@@ -1220,20 +1222,23 @@ function run_test_24() {
 
     shutdownManager();
 
     BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
     BootstrapMonitor.checkAddonNotStarted(ID1);
     BootstrapMonitor.checkAddonInstalled(ID2, "1.0");
     BootstrapMonitor.checkAddonNotStarted(ID2);
 
-    // Break the preference
-    let bootstrappedAddons = JSON.parse(Services.prefs.getCharPref("extensions.bootstrappedAddons"));
-    bootstrappedAddons[ID1].descriptor += "foo";
-    Services.prefs.setCharPref("extensions.bootstrappedAddons", JSON.stringify(bootstrappedAddons));
+    // Break the JSON.
+    let data = aomStartup.readStartupData();
+    data["app-profile"].addons[ID1].path += "foo";
+
+    await OS.File.writeAtomic(gAddonStartup.path,
+                              new TextEncoder().encode(JSON.stringify(data)),
+                              {compression: "lz4"});
 
     startupManager(false);
 
     BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
     BootstrapMonitor.checkAddonStarted(ID1, "1.0");
     BootstrapMonitor.checkAddonInstalled(ID2, "1.0");
     BootstrapMonitor.checkAddonStarted(ID2, "1.0");
 
@@ -1327,16 +1332,18 @@ function run_test_27() {
     do_check_neq(b1, null);
     b1.userDisabled = true;
     do_check_eq(b1.version, "1.0");
     do_check_false(b1.isActive);
     do_check_eq(b1.pendingOperations, AddonManager.PENDING_NONE);
     BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
     BootstrapMonitor.checkAddonNotStarted(ID1);
 
+    BootstrapMonitor.restartfulIds.add(ID1);
+
     installAllFiles([do_get_addon("test_bootstrap1_4")], function() {
       // Updating disabled things happens immediately
       BootstrapMonitor.checkAddonNotInstalled(ID1);
       do_check_eq(getUninstallReason(), ADDON_UPGRADE);
       do_check_eq(getUninstallNewVersion(), 4);
       BootstrapMonitor.checkAddonNotStarted(ID1);
 
       AddonManager.getAddonByID(ID1, callback_soon(function(b1_2) {
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug563256.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug563256.js
@@ -4,17 +4,17 @@
 
 // This verifies that the themes switch as expected
 
 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
-function run_test() {
+async function run_test() {
   do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   writeInstallRDFForExtension({
     id: "default@tests.mozilla.org",
     version: "1.0",
     name: "Default",
     internalName: "classic/1.0",
@@ -33,17 +33,17 @@ function run_test() {
     internalName: "alternate/1.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "2"
     }]
   }, profileDir);
 
-  startupManager();
+  await promiseStartupManager();
 
   do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
 
   AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
                                "alternate@tests.mozilla.org"], function([d, a]) {
     do_check_neq(d, null);
     do_check_false(d.userDisabled);
     do_check_false(d.appDisabled);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
@@ -3,17 +3,17 @@
  */
 
 // Tests that upgrading an incompatible add-on to a compatible one forces an
 // EM restart
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
-function run_test() {
+async function run_test() {
   do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "1.9.2");
 
   var dest = writeInstallRDFForExtension({
     id: "addon1@tests.mozilla.org",
     version: "1.0",
     name: "Test",
     targetApplications: [{
@@ -21,19 +21,19 @@ function run_test() {
       minVersion: "1",
       maxVersion: "1"
     }]
   }, profileDir);
   // Attempt to make this look like it was added some time in the past so
   // the update makes the last modified time change.
   setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
 
-  startupManager();
+  await promiseStartupManager();
 
-  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a) {
+  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(a) {
     do_check_neq(a, null);
     do_check_eq(a.version, "1.0");
     do_check_false(a.userDisabled);
     do_check_true(a.appDisabled);
     do_check_false(a.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a.id));
 
     writeInstallRDFForExtension({
@@ -42,17 +42,17 @@ function run_test() {
       name: "Test",
       targetApplications: [{
         id: "xpcshell@tests.mozilla.org",
         minVersion: "1",
         maxVersion: "2"
       }]
     }, profileDir);
 
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a2) {
       do_check_neq(a2, null);
       do_check_eq(a2.version, "2.0");
       do_check_false(a2.userDisabled);
       do_check_false(a2.appDisabled);
       do_check_true(a2.isActive);
       do_check_true(isExtensionInAddonsList(profileDir, a2.id));
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js
@@ -118,48 +118,48 @@ function run_test_1() {
     });
   });
 }
 
 // Test that a failed uninstall gets rolled back
 function run_test_2() {
   restartManager();
 
-  installAllFiles([do_get_addon("test_bug587088_1")], function() {
-    restartManager();
+  installAllFiles([do_get_addon("test_bug587088_1")], async function() {
+    await promiseRestartManager();
 
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+    AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(a1) {
       check_addon(a1, "1.0");
 
       // Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons.
       let uri = a1.getResourceURI("install.rdf");
       if (uri.schemeIs("jar"))
         uri = a1.getResourceURI();
 
       let fstream = AM_Cc["@mozilla.org/network/file-input-stream;1"].
                     createInstance(AM_Ci.nsIFileInputStream);
       fstream.init(uri.QueryInterface(AM_Ci.nsIFileURL).file, -1, 0, 0);
 
       a1.uninstall();
 
       check_addon_uninstalling(a1);
 
-      restartManager();
+      await promiseRestartManager();
 
-      AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_2) {
+      AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(a1_2) {
         check_addon_uninstalling(a1_2, true);
 
-        restartManager();
+        await promiseRestartManager();
 
-        AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_3) {
+        AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(a1_3) {
           check_addon_uninstalling(a1_3, true);
 
           fstream.close();
 
-          restartManager();
+          await promiseRestartManager();
 
           AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1_4) {
             do_check_eq(a1_4, null);
             var dir = profileDir.clone();
             dir.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
             do_check_false(dir.exists());
             do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug595573.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug595573.js
@@ -11,18 +11,18 @@ function run_test() {
   do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   startupManager();
   run_test_1();
 }
 
 function run_test_1() {
-  installAllFiles([do_get_addon("test_bug595573")], function() {
-    restartManager();
+  installAllFiles([do_get_addon("test_bug595573")], async function() {
+    await promiseRestartManager();
 
     AddonManager.getAddonByID("{2f69dacd-03df-4150-a9f1-e8a7b2748829}", function(a1) {
       do_check_neq(a1, null);
       do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
       do_execute_soon(run_test_2);
     });
   });
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug655254.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug655254.js
@@ -53,59 +53,59 @@ function run_test() {
   do_test_pending();
   run_test_1();
 }
 
 function end_test() {
   testserver.stop(do_test_finished);
 }
 
-function run_test_1() {
+async function run_test_1() {
   var time = Date.now();
   var dir = writeInstallRDFForExtension(addon1, userDir);
   setExtensionModifiedTime(dir, time);
 
   manuallyInstall(do_get_addon("test_bug655254_2"), userDir, "addon2@tests.mozilla.org");
 
-  startupManager();
+  await promiseStartupManager();
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org"], function([a1, a2]) {
     do_check_neq(a1, null);
     do_check_true(a1.appDisabled);
     do_check_false(a1.isActive);
     do_check_false(isExtensionInAddonsList(userDir, a1.id));
 
     do_check_neq(a2, null);
     do_check_false(a2.appDisabled);
     do_check_true(a2.isActive);
     do_check_false(isExtensionInAddonsList(userDir, a2.id));
     do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 1);
 
     a1.findUpdates({
-      onUpdateFinished() {
-        restartManager();
+      async onUpdateFinished() {
+        await promiseRestartManager();
 
-        AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_2) {
+        AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(a1_2) {
           do_check_neq(a1_2, null);
           do_check_false(a1_2.appDisabled);
           do_check_true(a1_2.isActive);
           do_check_true(isExtensionInAddonsList(userDir, a1_2.id));
 
           shutdownManager();
 
           do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 0);
 
           userDir.parent.moveTo(gProfD, "extensions3");
           userDir = gProfD.clone();
           userDir.append("extensions3");
           userDir.append(gAppInfo.ID);
           do_check_true(userDir.exists());
 
-          startupManager(false);
+          await promiseStartupManager(false);
 
           AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                        "addon2@tests.mozilla.org"], function([a1_3, a2_3]) {
             do_check_neq(a1_3, null);
             do_check_false(a1_3.appDisabled);
             do_check_true(a1_3.isActive);
             do_check_true(isExtensionInAddonsList(userDir, a1_3.id));
 
@@ -120,17 +120,17 @@ function run_test_1() {
         }));
       }
     }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
   });
 }
 
 // Set up the profile
 function run_test_2() {
-  AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(a2) {
+  AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(async function(a2) {
    do_check_neq(a2, null);
    do_check_false(a2.appDisabled);
    do_check_true(a2.isActive);
    do_check_false(isExtensionInAddonsList(userDir, a2.id));
    do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 1);
 
    a2.userDisabled = true;
    do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 0);
@@ -138,17 +138,17 @@ function run_test_2() {
    shutdownManager();
 
    userDir.parent.moveTo(gProfD, "extensions4");
    userDir = gProfD.clone();
    userDir.append("extensions4");
    userDir.append(gAppInfo.ID);
    do_check_true(userDir.exists());
 
-   startupManager(false);
+   await promiseStartupManager(false);
 
    AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                 "addon2@tests.mozilla.org"], function([a1_2, a2_2]) {
      do_check_neq(a1_2, null);
      do_check_false(a1_2.appDisabled);
      do_check_true(a1_2.isActive);
      do_check_true(isExtensionInAddonsList(userDir, a1_2.id));
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug659772.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug659772.js
@@ -54,23 +54,23 @@ profileDir.append("extensions");
 function run_test() {
   do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   run_test_1();
 }
 
 // Tests whether a schema migration without app version change works
-function run_test_1() {
+async function run_test_1() {
   writeInstallRDFForExtension(addon1, profileDir);
   writeInstallRDFForExtension(addon2, profileDir);
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir);
 
-  startupManager();
+  await promiseStartupManager();
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                               function([a1, a2, a3, a4]) {
     do_check_neq(a1, null);
     do_check_eq(a1.version, "2.0");
@@ -99,17 +99,17 @@ function run_test_1() {
     do_check_false(a4.userDisabled);
     do_check_false(a4.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, addon4.id));
 
     // Prepare the add-on update, and a bootstrapped addon (bug 693714)
     installAllFiles([
       do_get_addon("test_bug659772"),
       do_get_addon("test_bootstrap1_1")
-    ], function() {
+    ], async function() {
       shutdownManager();
 
       // Make it look like the next time the app is started it has a new DB schema
       changeXPIDBVersion(1);
       Services.prefs.setIntPref("extensions.databaseSchema", 1);
 
       let jsonfile = gProfD.clone();
       jsonfile.append("extensions");
@@ -137,17 +137,17 @@ function run_test_1() {
       converter.init(stream, "UTF-8", 0, 0x0000);
       converter.writeString(JSON.stringify(addonObj));
       converter.close();
       stream.close();
 
       Services.prefs.clearUserPref("bootstraptest.install_reason");
       Services.prefs.clearUserPref("bootstraptest.uninstall_reason");
 
-      startupManager(false);
+      await promiseStartupManager(false);
 
       AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                    "addon2@tests.mozilla.org",
                                    "addon3@tests.mozilla.org",
                                    "addon4@tests.mozilla.org"],
                                   function([a1_2, a2_2, a3_2, a4_2]) {
         do_check_neq(a1_2, null);
         do_check_eq(a1_2.version, "2.0");
@@ -189,27 +189,27 @@ function run_test_1() {
         a4_2.uninstall();
         do_execute_soon(run_test_2);
       });
     });
   });
 }
 
 // Tests whether a schema migration with app version change works
-function run_test_2() {
+async function run_test_2() {
   restartManager();
 
   shutdownManager();
 
   writeInstallRDFForExtension(addon1, profileDir);
   writeInstallRDFForExtension(addon2, profileDir);
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir);
 
-  startupManager();
+  await promiseStartupManager();
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                               function([a1, a2, a3, a4]) {
     do_check_neq(a1, null);
     do_check_eq(a1.version, "2.0");
@@ -240,17 +240,17 @@ function run_test_2() {
     do_check_false(isExtensionInAddonsList(profileDir, addon4.id));
 
     // Prepare the add-on update, and a bootstrapped addon (bug 693714)
     installAllFiles([
       do_get_addon("test_bug659772"),
       do_get_addon("test_bootstrap1_1")
     ], function() { do_execute_soon(prepare_schema_migrate); });
 
-    function prepare_schema_migrate() {
+    async function prepare_schema_migrate() {
       shutdownManager();
 
       // Make it look like the next time the app is started it has a new DB schema
       changeXPIDBVersion(1);
       Services.prefs.setIntPref("extensions.databaseSchema", 1);
 
       let jsonfile = gProfD.clone();
       jsonfile.append("extensions");
@@ -279,17 +279,17 @@ function run_test_2() {
       converter.writeString(JSON.stringify(addonObj));
       converter.close();
       stream.close();
 
       Services.prefs.clearUserPref("bootstraptest.install_reason");
       Services.prefs.clearUserPref("bootstraptest.uninstall_reason");
 
       gAppInfo.version = "2";
-      startupManager(true);
+      await promiseStartupManager(true);
 
       AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                    "addon2@tests.mozilla.org",
                                    "addon3@tests.mozilla.org",
                                    "addon4@tests.mozilla.org"],
                                   callback_soon(function([a1_2, a2_2, a3_2, a4_2]) {
         do_check_neq(a1_2, null);
         do_check_eq(a1_2.version, "2.0");
--- a/toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests the extensions.defaultProviders.enabled pref which turns
 // off the default XPIProvider and LightweightThemeManager.
 
-function run_test() {
+async function run_test() {
   Services.prefs.setBoolPref("extensions.defaultProviders.enabled", false);
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
-  startupManager();
+  await promiseStartupManager();
   do_check_false(AddonManager.isInstallEnabled("application/x-xpinstall"));
   Services.prefs.clearUserPref("extensions.defaultProviders.enabled");
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_disable.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_disable.js
@@ -27,24 +27,24 @@ var gIconURL = null;
 
 // Sets up the profile by installing an add-on.
 function run_test() {
   do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   startupManager();
 
-  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(a1) {
     do_check_eq(a1, null);
     do_check_not_in_crash_annotation(addon1.id, addon1.version);
 
     writeInstallRDFForExtension(addon1, profileDir, addon1.id, "icon.png");
     gIconURL = do_get_addon_root_uri(profileDir.clone(), addon1.id) + "icon.png";
 
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
       do_check_neq(newa1, null);
       do_check_true(newa1.isActive);
       do_check_false(newa1.userDisabled);
       do_check_eq(newa1.aboutURL, "chrome://foo/content/about.xul");
       do_check_eq(newa1.optionsURL, "chrome://foo/content/options.xul");
       do_check_eq(newa1.iconURL, "chrome://foo/content/icon.png");
--- a/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js
@@ -1,17 +1,28 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-var scope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
-const XPIProvider = scope.XPIProvider;
 const ID = "experiment1@tests.mozilla.org";
 
 var gIsNightly = false;
 
+function getXS() {
+  let XPI = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+  return XPI.XPIStates;
+}
+
+function getBootstrappedAddons() {
+  let obj = {}
+  for (let addon of getXS().bootstrappedAddons()) {
+    obj[addon.id] = addon;
+  }
+  return obj;
+}
+
 function run_test() {
   BootstrapMonitor.init();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   startupManager();
 
   gIsNightly = isNightlyChannel();
 
   run_next_test();
@@ -75,32 +86,32 @@ add_task(async function test_userDisable
 
   BootstrapMonitor.checkAddonInstalled(ID, "1.0");
   BootstrapMonitor.checkAddonStarted(ID, "1.0");
 
   Assert.equal(addon2.id, addon.id, "Changed add-on matches expected.");
   Assert.equal(addon2.userDisabled, false, "Add-on is no longer user disabled.");
   Assert.ok(addon2.isActive, "Add-on is active.");
 
-  Assert.ok(ID in XPIProvider.bootstrappedAddons,
+  Assert.ok(ID in getBootstrappedAddons(),
             "Experiment add-on listed in XPIProvider bootstrapped list.");
 
   addon = await promiseAddonByID(ID);
   Assert.ok(addon, "Add-on retrieved.");
   Assert.equal(addon.userDisabled, false, "Add-on is still enabled after API retrieve.");
   Assert.ok(addon.isActive, "Add-on is still active.");
   Assert.ok(!(addon.pendingOperations & AddonManager.PENDING_ENABLE),
             "Should not be pending enable");
   Assert.ok(!(addon.pendingOperations & AddonManager.PENDING_DISABLE),
             "Should not be pending disable");
 
   // Now when we restart the manager the add-on should revert state.
   await promiseRestartManager();
-  let persisted = JSON.parse(Services.prefs.getCharPref("extensions.bootstrappedAddons"));
-  Assert.ok(!(ID in persisted),
+
+  Assert.ok(!(ID in getBootstrappedAddons()),
             "Experiment add-on not persisted to bootstrappedAddons.");
 
   BootstrapMonitor.checkAddonInstalled(ID, "1.0");
   BootstrapMonitor.checkAddonNotStarted(ID);
 
   addon = await promiseAddonByID(ID);
   Assert.ok(addon, "Add-on retrieved.");
   Assert.ok(addon.userDisabled, "Add-on is disabled after restart.");
--- a/toolkit/mozapps/extensions/test/xpcshell/test_hasbinarycomponents.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_hasbinarycomponents.js
@@ -13,26 +13,26 @@ function run_test() {
 
   startupManager();
 
   installAllFiles([do_get_addon("test_chromemanifest_1"),
                    do_get_addon("test_chromemanifest_2"),
                    do_get_addon("test_chromemanifest_3"),
                    do_get_addon("test_chromemanifest_4"),
                    do_get_addon("test_chromemanifest_5")],
-                  function() {
+                  async function() {
 
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                  "addon2@tests.mozilla.org",
                                  "addon3@tests.mozilla.org",
                                  "addon4@tests.mozilla.org",
                                  "addon5@tests.mozilla.org"],
-                                function([a1, a2, a3, a4, a5]) {
+                                async function([a1, a2, a3, a4, a5]) {
       // addon1 has no binary components
       do_check_neq(a1, null);
       do_check_false(a1.userDisabled);
       do_check_false(a1.hasBinaryComponents);
       do_check_true(a1.isCompatible);
       do_check_false(a1.appDisabled);
       do_check_true(a1.isActive);
       do_check_true(isExtensionInAddonsList(profileDir, a1.id));
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -112,17 +112,17 @@ function run_test_1() {
   });
 }
 
 function check_test_1(installSyncGUID) {
   ensure_test_completed();
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(olda1) {
     do_check_eq(olda1, null);
 
-    AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(pendingAddons) {
+    AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(async function(pendingAddons) {
       do_check_eq(pendingAddons.length, 1);
       do_check_eq(pendingAddons[0].id, "addon1@tests.mozilla.org");
       let uri = NetUtil.newURI(pendingAddons[0].iconURL);
       if (uri instanceof AM_Ci.nsIJARURI) {
         let jarURI = uri.QueryInterface(AM_Ci.nsIJARURI);
         let archiveURI = jarURI.JARFile;
         let archiveFile = archiveURI.QueryInterface(AM_Ci.nsIFileURL).file;
         let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
@@ -145,17 +145,17 @@ function check_test_1(installSyncGUID) {
       let extURI = pendingAddons[0].getResourceURI("");
       let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
       setExtensionModifiedTime(ext, updateDate);
 
       // The pending add-on cannot be disabled or enabled.
       do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_ENABLE));
       do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_DISABLE));
 
-      restartManager();
+      await promiseRestartManager();
 
       AddonManager.getAllInstalls(function(activeInstalls) {
         do_check_eq(activeInstalls, 0);
 
         AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
           do_check_neq(a1, null);
           do_check_neq(a1.syncGUID, null);
           do_check_true(a1.syncGUID.length >= 9);
@@ -271,19 +271,19 @@ function run_test_3(install) {
 function check_test_3(aInstall) {
   // Make the pending install have a sensible date
   let updateDate = Date.now();
   let extURI = aInstall.addon.getResourceURI("");
   let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
   setExtensionModifiedTime(ext, updateDate);
 
   ensure_test_completed();
-  AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(olda2) {
+  AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(async function(olda2) {
     do_check_eq(olda2, null);
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAllInstalls(function(installs) {
       do_check_eq(installs, 0);
 
       AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
         do_check_neq(a2, null);
         do_check_neq(a2.syncGUID, null);
         do_check_eq(a2.type, "extension");
@@ -460,19 +460,19 @@ function run_test_7() {
   }, [
     "onInstallStarted",
     "onInstallEnded",
   ], check_test_7);
 }
 
 function check_test_7() {
   ensure_test_completed();
-  AddonManager.getAddonByID("addon3@tests.mozilla.org", callback_soon(function(olda3) {
+  AddonManager.getAddonByID("addon3@tests.mozilla.org", callback_soon(async function(olda3) {
     do_check_eq(olda3, null);
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAllInstalls(function(installs) {
       do_check_eq(installs, 0);
 
       AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
         do_check_neq(a3, null);
         do_check_neq(a3.syncGUID, null);
         do_check_eq(a3.type, "extension");
@@ -509,18 +509,18 @@ function run_test_8() {
     }, [
       "onInstallStarted",
       "onInstallEnded",
     ], callback_soon(check_test_8));
     install.install();
   });
 }
 
-function check_test_8() {
-  restartManager();
+async function check_test_8() {
+  await promiseRestartManager();
 
   AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
     do_check_neq(a3, null);
     do_check_neq(a3.syncGUID, null);
     do_check_eq(a3.type, "extension");
     do_check_eq(a3.version, "1.0");
     do_check_eq(a3.name, "Real Test 4");
     do_check_true(a3.isActive);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
@@ -109,17 +109,17 @@ function run_test_1() {
   });
 }
 
 function check_test_1() {
   ensure_test_completed();
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(olda1) {
     do_check_eq(olda1, null);
 
-    AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(pendingAddons) {
+    AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(async function(pendingAddons) {
       do_check_eq(pendingAddons.length, 1);
       do_check_eq(pendingAddons[0].id, "addon1@tests.mozilla.org");
       let uri = NetUtil.newURI(pendingAddons[0].iconURL);
       if (uri instanceof AM_Ci.nsIJARURI) {
         let jarURI = uri.QueryInterface(AM_Ci.nsIJARURI);
         let archiveURI = jarURI.JARFile;
         let archiveFile = archiveURI.QueryInterface(AM_Ci.nsIFileURL).file;
         let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
@@ -140,17 +140,17 @@ function check_test_1() {
       let extURI = pendingAddons[0].getResourceURI("");
       let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
       setExtensionModifiedTime(ext, updateDate);
 
       // The pending add-on cannot be disabled or enabled.
       do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_ENABLE));
       do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_DISABLE));
 
-      restartManager();
+      await promiseRestartManager();
 
       AddonManager.getAllInstalls(function(activeInstalls) {
         do_check_eq(activeInstalls, 0);
 
         AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
           do_check_neq(a1, null);
           do_check_eq(a1.type, "extension");
           do_check_eq(a1.version, "1.0");
@@ -254,19 +254,19 @@ function run_test_3(install) {
 function check_test_3(aInstall) {
   // Make the pending install have a sensible date
   let updateDate = Date.now();
   let extURI = aInstall.addon.getResourceURI("");
   let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
   setExtensionModifiedTime(ext, updateDate);
 
   ensure_test_completed();
-  AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(olda2) {
+  AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(async function(olda2) {
     do_check_eq(olda2, null);
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAllInstalls(function(installs) {
       do_check_eq(installs, 0);
 
       AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
         do_check_neq(a2, null);
         do_check_eq(a2.type, "extension");
         do_check_eq(a2.version, "2.0");
@@ -354,20 +354,20 @@ function check_test_5(install) {
   ensure_test_completed();
 
   do_check_eq(install.existingAddon.pendingUpgrade.install, install);
 
   AddonManager.getAddonByID("addon2@tests.mozilla.org", function(olda2) {
     do_check_neq(olda2, null);
     do_check_true(hasFlag(olda2.pendingOperations, AddonManager.PENDING_UPGRADE));
 
-    AddonManager.getInstallsByTypes(null, callback_soon(function(installs) {
+    AddonManager.getInstallsByTypes(null, callback_soon(async function(installs) {
       do_check_eq(installs.length, 1);
       do_check_eq(installs[0].addon, olda2.pendingUpgrade);
-      restartManager();
+      await promiseRestartManager();
 
       AddonManager.getInstallsByTypes(null, function(installs2) {
         do_check_eq(installs2.length, 0);
 
         AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
           do_check_neq(a2, null);
           do_check_eq(a2.type, "extension");
           do_check_eq(a2.version, "3.0");
@@ -441,19 +441,19 @@ function run_test_7() {
   }, [
     "onInstallStarted",
     "onInstallEnded",
   ], check_test_7);
 }
 
 function check_test_7() {
   ensure_test_completed();
-  AddonManager.getAddonByID("addon3@tests.mozilla.org", callback_soon(function(olda3) {
+  AddonManager.getAddonByID("addon3@tests.mozilla.org", callback_soon(async function(olda3) {
     do_check_eq(olda3, null);
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAllInstalls(function(installs) {
       do_check_eq(installs, 0);
 
       AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
         do_check_neq(a3, null);
         do_check_eq(a3.type, "extension");
         do_check_eq(a3.version, "1.0");
@@ -489,18 +489,18 @@ function run_test_8() {
     }, [
       "onInstallStarted",
       "onInstallEnded",
     ], callback_soon(check_test_8));
     install.install();
   });
 }
 
-function check_test_8() {
-  restartManager();
+async function check_test_8() {
+  await promiseRestartManager();
 
   AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
     do_check_neq(a3, null);
     do_check_eq(a3.type, "extension");
     do_check_eq(a3.version, "1.0");
     do_check_eq(a3.name, "Real Test 4");
     do_check_true(a3.isActive);
     do_check_false(a3.appDisabled);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked.js
@@ -177,16 +177,17 @@ add_task(async function init() {
     }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
 
   });
 });
 
 
 add_task(async function run_test_1() {
   restartManager();
+
   let [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
     await promiseAddonsByIDs(["addon1@tests.mozilla.org",
                               "addon2@tests.mozilla.org",
                               "addon3@tests.mozilla.org",
                               "addon4@tests.mozilla.org",
                               "addon5@tests.mozilla.org",
                               "addon6@tests.mozilla.org",
                               "addon7@tests.mozilla.org",
@@ -448,18 +449,17 @@ add_task(async function run_test_1() {
    try {
      shutdownManager();
    } catch (e) {
      // We're expecting an error here.
    }
    do_print("Unlocking " + gExtensionsJSON.path);
    await file.close();
    gExtensionsJSON.permissions = filePermissions;
-   startupManager();
-
+   await promiseStartupManager();
 
    // Shouldn't have seen any startup changes
    check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
    [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
      await promiseAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
@@ -159,17 +159,17 @@ add_task(async function() {
     options.unixFlags = OS.Constants.libc.O_EXLOCK;
 
   let file = await OS.File.open(gExtensionsJSON.path, {read: true, write: true, existing: true}, options);
 
   let filePermissions = gExtensionsJSON.permissions;
   if (!OS.Constants.Win) {
     gExtensionsJSON.permissions = 0;
   }
-  startupManager(false);
+  await promiseStartupManager(false);
 
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
 
   [a1, a2, a3, a4, a5, a6] =
     await promiseAddonsByIDs(["addon1@tests.mozilla.org",
                               "addon2@tests.mozilla.org",
                               "addon3@tests.mozilla.org",
@@ -220,17 +220,17 @@ add_task(async function() {
   let shutdownError;
   try {
     shutdownManager();
   } catch (e) {
     shutdownError = e;
   }
   await file.close();
   gExtensionsJSON.permissions = filePermissions;
-  startupManager();
+  await promiseStartupManager();
 
   // On Unix, we can save the DB even when the original file wasn't
   // readable, so our changes were saved. On Windows,
   // these things happened when we had no access to the database so
   // they are seen as external changes when we get the database back
   if (shutdownError) {
     do_print("Previous XPI save failed");
     check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED,
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
@@ -143,17 +143,17 @@ add_task(async function init() {
   writeInstallRDFForExtension(addon4, profileDir);
   writeInstallRDFForExtension(addon5, profileDir);
   writeInstallRDFForExtension(addon6, profileDir);
   writeInstallRDFForExtension(addon7, profileDir);
   writeInstallRDFForExtension(theme1, profileDir);
   writeInstallRDFForExtension(theme2, profileDir);
 
   // Startup the profile and setup the initial state
-  startupManager();
+  await promiseStartupManager();
 
   // New profile so new add-ons are ignored
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
   let [a2, a3, a4, a7, t2] =
     await promiseAddonsByIDs(["addon2@tests.mozilla.org",
                               "addon3@tests.mozilla.org",
                               "addon4@tests.mozilla.org",
@@ -266,17 +266,17 @@ add_task(async function run_test_1() {
     options.unixFlags = OS.Constants.libc.O_EXLOCK;
 
   let file = await OS.File.open(gExtensionsJSON.path, {read: true, write: true, existing: true}, options);
 
   let filePermissions = gExtensionsJSON.permissions;
   if (!OS.Constants.Win) {
     gExtensionsJSON.permissions = 0;
   }
-  startupManager(false);
+  await promiseStartupManager(false);
 
   // Shouldn't have seen any startup changes
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
   // Accessing the add-ons should open and recover the database
   [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
     await promiseAddonsByIDs(["addon1@tests.mozilla.org",
                               "addon2@tests.mozilla.org",
@@ -360,17 +360,17 @@ add_task(async function run_test_1() {
   // Restarting will actually apply changes to extensions.ini which will
   // then be put into the in-memory database when we next fail to load the
   // real thing
   try {
     shutdownManager();
   } catch (e) {
     // We're expecting an error here.
   }
-  startupManager(false);
+  await promiseStartupManager(false);
 
   // Shouldn't have seen any startup changes
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
   [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
     await promiseAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
@@ -448,17 +448,17 @@ add_task(async function run_test_1() {
   try {
     shutdownManager();
   } catch (e) {
     shutdownError = e;
   }
   do_print("Unlocking " + gExtensionsJSON.path);
   await file.close();
   gExtensionsJSON.permissions = filePermissions;
-  startupManager(false);
+  await promiseStartupManager(false);
 
   // Shouldn't have seen any startup changes
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
   [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
     await promiseAddonsByIDs(["addon1@tests.mozilla.org",
                               "addon2@tests.mozilla.org",
                               "addon3@tests.mozilla.org",
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate_state_prefs.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+/* globals Preferences */
+AM_Cu.import("resource://gre/modules/Preferences.jsm");
+
+function getXS() {
+  let XPI = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+  return XPI.XPIStates;
+}
+
+function installExtension(id, data) {
+  return AddonTestUtils.promiseWriteFilesToExtension(
+    AddonTestUtils.profileExtensions.path, id, data);
+}
+
+add_task(async function test_migrate_prefs() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "54");
+
+  ok(!AddonTestUtils.addonStartup.exists(),
+     "addonStartup.json.lz4 should not exist");
+
+  const ID1 = "bootstrapped-enabled@xpcshell.mozilla.org";
+  const ID2 = "bootstrapped-disabled@xpcshell.mozilla.org";
+  const ID3 = "restartful-enabled@xpcshell.mozilla.org";
+  const ID4 = "restartful-disabled@xpcshell.mozilla.org";
+
+  let targetApplications = [{ id: "toolkit@mozilla.org", "minVersion": "0", "maxVersion": "*" }];
+
+  let file1 = await installExtension(ID1, { "install.rdf": { id: ID1, name: ID1, bootstrapped: true, version: "0.1", targetApplications } });
+  let file2 = await installExtension(ID2, { "install.rdf": { id: ID2, name: ID2, bootstrapped: true, version: "0.2", targetApplications } });
+
+  let file3 = await installExtension(ID3, { "install.rdf": { id: ID3, name: ID1, bootstrapped: false, version: "0.3", targetApplications } });
+  let file4 = await installExtension(ID4, { "install.rdf": { id: ID4, name: ID2, bootstrapped: false, version: "0.4", targetApplications } });
+
+  function mt(file) {
+    let f = file.clone();
+    if (TEST_UNPACKED) {
+      f.append("install.rdf");
+    }
+    return f.lastModifiedTime;
+  }
+
+  // Startup and shut down the add-on manager so the add-ons are added
+  // to the DB.
+  await promiseStartupManager();
+  await promiseShutdownManager();
+
+  // Remove the startup state file and add legacy prefs to replace it.
+  AddonTestUtils.addonStartup.remove(false);
+
+  Preferences.set("extensions.xpiState", JSON.stringify({
+    "app-profile": {
+      [ID1]: {e: true, d: file1.persistentDescriptor, v: "0.1", mt: mt(file1)},
+      [ID2]: {e: false, d: file2.persistentDescriptor, v: "0.2", mt: mt(file2)},
+      [ID3]: {e: true, d: file3.persistentDescriptor, v: "0.3", mt: mt(file3)},
+      [ID4]: {e: false, d: file4.persistentDescriptor, v: "0.4", mt: mt(file4)},
+    }
+  }));
+
+  Preferences.set("extensions.bootstrappedAddons", JSON.stringify({
+    [ID1]: {
+      version: "0.1",
+      type: "extension",
+      multiprocessCompatible: false,
+      descriptor: file1.persistentDescriptor,
+      hasEmbeddedWebExtension: true,
+    }
+  }));
+
+  await promiseStartupManager();
+
+  // Check the the state data is updated correctly.
+  let states = getXS();
+
+  let addon1 = states.findAddon(ID1);
+  ok(addon1.enabled, "Addon 1 should be enabled");
+  ok(addon1.bootstrapped, "Addon 1 should be bootstrapped");
+  equal(addon1.version, "0.1", "Addon 1 has the correct version");
+  equal(addon1.mtime, mt(file1), "Addon 1 has the correct timestamp");
+  ok(addon1.enableShims, "Addon 1 has shims enabled");
+  ok(addon1.hasEmbeddedWebExtension, "Addon 1 has an embedded WebExtension");
+
+  let addon2 = states.findAddon(ID2);
+  ok(!addon2.enabled, "Addon 2 should not be enabled");
+  ok(!addon2.bootstrapped, "Addon 2 should be bootstrapped, because that information is not stored in xpiStates");
+  equal(addon2.version, "0.2", "Addon 2 has the correct version");
+  equal(addon2.mtime, mt(file2), "Addon 2 has the correct timestamp");
+  ok(!addon2.enableShims, "Addon 2 does not have shims enabled");
+  ok(!addon2.hasEmbeddedWebExtension, "Addon 2 no embedded WebExtension");
+
+  let addon3 = states.findAddon(ID3);
+  ok(addon3.enabled, "Addon 3 should be enabled");
+  ok(!addon3.bootstrapped, "Addon 3 should not be bootstrapped");
+  equal(addon3.version, "0.3", "Addon 3 has the correct version");
+  equal(addon3.mtime, mt(file3), "Addon 3 has the correct timestamp");
+  ok(!addon3.enableShims, "Addon 3 does not have shims enabled");
+  ok(!addon3.hasEmbeddedWebExtension, "Addon 3 no embedded WebExtension");
+
+  let addon4 = states.findAddon(ID4);
+  ok(!addon4.enabled, "Addon 4 should not be enabled");
+  ok(!addon4.bootstrapped, "Addon 4 should not be bootstrapped");
+  equal(addon4.version, "0.4", "Addon 4 has the correct version");
+  equal(addon4.mtime, mt(file4), "Addon 4 has the correct timestamp");
+  ok(!addon4.enableShims, "Addon 4 does not have shims enabled");
+  ok(!addon4.hasEmbeddedWebExtension, "Addon 4 no embedded WebExtension");
+
+  // Check that legacy prefs and files have been removed.
+  ok(!Preferences.has("extensions.xpiState"), "No xpiState pref left behind");
+  ok(!Preferences.has("extensions.bootstrappedAddons"), "No bootstrappedAddons pref left behind");
+  ok(!Preferences.has("extensions.enabledAddons"), "No enabledAddons pref left behind");
+
+  let file = AddonTestUtils.profileDir.clone();
+  file.append("extensions.ini");
+  ok(!file.exists(), "No extensions.ini file left behind");
+
+  await promiseShutdownManager();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js
@@ -22,33 +22,19 @@ function run_test() {
 function checkPending() {
   try {
     do_check_false(Services.prefs.getBoolPref("extensions.pendingOperations"));
   } catch (e) {
     // OK
   }
 }
 
-function checkString(aPref, aValue) {
-  try {
-    do_check_eq(Services.prefs.getCharPref(aPref), aValue)
-  } catch (e) {
-    // OK
-  }
-}
-
 // Make sure all our extension state is empty/nonexistent
 function check_empty_state() {
-  do_check_false(gExtensionsJSON.exists());
-  do_check_false(gExtensionsINI.exists());
-
   do_check_eq(Services.prefs.getIntPref("extensions.databaseSchema"), DB_SCHEMA);
-
-  checkString("extensions.bootstrappedAddons", "{}");
-  checkString("extensions.installCache", "[]");
   checkPending();
 }
 
 // After first run with no add-ons, we expect:
 // no extensions.json is created
 // no extensions.ini
 // database schema version preference is set
 // bootstrap add-ons preference is not found
--- a/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js
@@ -28,24 +28,24 @@ var gIconURL = null;
 // Sets up the profile by installing an add-on.
 function run_test() {
   do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   gAppInfo.inSafeMode = true;
 
   startupManager();
 
-  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(a1) {
     do_check_eq(a1, null);
     do_check_not_in_crash_annotation(addon1.id, addon1.version);
 
     writeInstallRDFForExtension(addon1, profileDir, addon1.id, "icon.png");
     gIconURL = do_get_addon_root_uri(profileDir.clone(), addon1.id) + "icon.png";
 
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
       do_check_neq(newa1, null);
       do_check_false(newa1.isActive);
       do_check_false(newa1.userDisabled);
       do_check_eq(newa1.aboutURL, null);
       do_check_eq(newa1.optionsURL, null);
       do_check_eq(newa1.iconURL, gIconURL);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
@@ -256,30 +256,30 @@ add_task(async function() {
 add_task(async function() {
   let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID);
 
   // Make it appear to come from the past so when we modify it later it is
   // detected during startup. Obviously malware can bypass this method of
   // detection but the periodic scan will catch that
   await promiseSetExtensionModifiedTime(file.path, Date.now() - 60000);
 
-  startupManager();
+  await promiseStartupManager();
   let addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
   do_check_false(addon.appDisabled);
   do_check_true(addon.isActive);
   do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED);
   do_check_true(isExtensionInAddonsList(profileDir, ID));
 
   await promiseShutdownManager();
 
   clearCache(file);
   breakAddon(file);
 
-  startupManager();
+  await promiseStartupManager();
 
   addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
   do_check_true(addon.appDisabled);
   do_check_false(addon.isActive);
   do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
   do_check_false(isExtensionInAddonsList(profileDir, ID));
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js
@@ -99,17 +99,17 @@ async function test_breaking_migrate(add
 
   // Now replace it with the version to test. Doing this so quickly shouldn't
   // trigger the file modification code to detect the change by itself.
   manuallyUninstall(profileDir, ID);
   manuallyInstall(do_get_file(DATA + addons[test]), profileDir, ID);
 
   // Update the application
   gAppInfo.version = "5";
-  startupManager(true);
+  await promiseStartupManager(true);
 
   let addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
   do_check_true(addon.appDisabled);
   do_check_false(addon.isActive);
   do_check_eq(addon.signedState, expectedSignedState);
 
   // Add-on shouldn't be active
@@ -149,17 +149,17 @@ async function test_working_migrate(addo
 
   // Now replace it with the version to test. Doing this so quickly shouldn't
   // trigger the file modification code to detect the change by itself.
   manuallyUninstall(profileDir, ID);
   manuallyInstall(do_get_file(DATA + addons[test]), profileDir, ID);
 
   // Update the application
   gAppInfo.version = "5";
-  startupManager(true);
+  await promiseStartupManager(true);
 
   let addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
   do_check_false(addon.appDisabled);
   do_check_true(addon.isActive);
   do_check_eq(addon.signedState, expectedSignedState);
 
   if (addons == ADDONS.bootstrap)
--- a/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
@@ -129,17 +129,17 @@ function run_test() {
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
 
   do_check_false(gExtensionsJSON.exists());
 
-  do_check_false(gExtensionsINI.exists());
+  do_check_false(gAddonStartup.exists());
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org",
                                "addon6@tests.mozilla.org",
                                "addon7@tests.mozilla.org"],
@@ -158,42 +158,42 @@ function run_test() {
   });
 }
 
 function end_test() {
   do_test_finished("test_startup main");
 }
 
 // Try to install all the items into the profile
-function run_test_1() {
+async function run_test_1() {
   writeInstallRDFForExtension(addon1, profileDir);
   var dest = writeInstallRDFForExtension(addon2, profileDir);
   // Attempt to make this look like it was added some time in the past so
   // the change in run_test_2 makes the last modified time change.
   setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
 
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir, "addon4@tests.mozilla.org");
   writeInstallRDFForExtension(addon5, profileDir);
   writeInstallRDFForExtension(addon6, profileDir);
   writeInstallRDFForExtension(addon7, profileDir);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, ["addon1@tests.mozilla.org",
                                       "addon2@tests.mozilla.org",
                                       "addon3@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
-  do_print("Checking for " + gExtensionsINI.path);
-  do_check_true(gExtensionsINI.exists());
+  do_print("Checking for " + gAddonStartup.path);
+  do_check_true(gAddonStartup.exists());
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org",
                                "addon6@tests.mozilla.org",
                                "addon7@tests.mozilla.org"],
@@ -276,39 +276,40 @@ function run_test_1() {
 
       do_execute_soon(run_test_2);
     });
   });
 }
 
 // Test that modified items are detected and items in other install locations
 // are ignored
-function run_test_2() {
+async function run_test_2() {
   addon1.version = "1.1";
   writeInstallRDFForExtension(addon1, userDir);
   addon2.version = "2.1";
   writeInstallRDFForExtension(addon2, profileDir);
   addon2.version = "2.2";
   writeInstallRDFForExtension(addon2, globalDir);
   addon2.version = "2.3";
   writeInstallRDFForExtension(addon2, userDir);
   var dest = profileDir.clone();
   dest.append(do_get_expected_addon_name("addon3@tests.mozilla.org"));
   dest.remove(true);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon3@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
-  do_check_true(gExtensionsINI.exists());
+  do_check_true(gAddonStartup.exists());
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org"],
                                function([a1, a2, a3, a4, a5]) {
 
@@ -345,27 +346,28 @@ function run_test_2() {
     do_check_eq(a5, null);
     do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
 
     do_execute_soon(run_test_3);
   });
 }
 
 // Check that removing items from the profile reveals their hidden versions.
-function run_test_3() {
+async function run_test_3() {
   var dest = profileDir.clone();
   dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
   dest.remove(true);
   dest = profileDir.clone();
   dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
   dest.remove(true);
   writeInstallRDFForExtension(addon3, profileDir, "addon4@tests.mozilla.org");
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon1@tests.mozilla.org",
                                     "addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
@@ -410,21 +412,22 @@ function run_test_3() {
     dest.append(do_get_expected_addon_name("addon4@tests.mozilla.org"));
     do_check_false(dest.exists());
 
     do_execute_soon(run_test_4);
   });
 }
 
 // Test that disabling an install location works
-function run_test_4() {
+async function run_test_4() {
   Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_SYSTEM);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon1@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
@@ -449,21 +452,22 @@ function run_test_4() {
     do_check_in_crash_annotation(addon2.id, a2.version);
     do_check_eq(a2.scope, AddonManager.SCOPE_SYSTEM);
 
     do_execute_soon(run_test_5);
   });
 }
 
 // Switching disabled locations works
-function run_test_5() {
+async function run_test_5() {
   Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_USER);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, ["addon1@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
@@ -494,21 +498,22 @@ function run_test_5() {
     do_check_in_crash_annotation(addon2.id, a2.version);
     do_check_eq(a2.scope, AddonManager.SCOPE_USER);
 
     do_execute_soon(run_test_6);
   });
 }
 
 // Resetting the pref makes everything visible again
-function run_test_6() {
+async function run_test_6() {
   Services.prefs.clearUserPref("extensions.enabledScopes");
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
@@ -539,25 +544,26 @@ function run_test_6() {
     do_check_in_crash_annotation(addon2.id, a2.version);
     do_check_eq(a2.scope, AddonManager.SCOPE_USER);
 
     do_execute_soon(run_test_7);
   });
 }
 
 // Check that items in the profile hide the others again.
-function run_test_7() {
+async function run_test_7() {
   addon1.version = "1.2";
   writeInstallRDFForExtension(addon1, profileDir);
   var dest = userDir.clone();
   dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
   dest.remove(true);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon1@tests.mozilla.org",
                                     "addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
@@ -598,21 +604,22 @@ function run_test_7() {
     do_check_eq(a5, null);
     do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
 
     do_execute_soon(run_test_8);
   });
 }
 
 // Disabling all locations still leaves the profile working
-function run_test_8() {
+async function run_test_8() {
   Services.prefs.setIntPref("extensions.enabledScopes", 0);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
@@ -637,30 +644,31 @@ function run_test_8() {
     do_check_false(isExtensionInAddonsList(userDir, "addon2@tests.mozilla.org"));
     do_check_false(isExtensionInAddonsList(globalDir, "addon2@tests.mozilla.org"));
 
     do_execute_soon(run_test_9);
   });
 }
 
 // More hiding and revealing
-function run_test_9() {
+async function run_test_9() {
   Services.prefs.clearUserPref("extensions.enabledScopes");
 
   var dest = userDir.clone();
   dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
   dest.remove(true);
   dest = globalDir.clone();
   dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
   dest.remove(true);
   addon2.version = "2.4";
   writeInstallRDFForExtension(addon2, profileDir);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, ["addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
@@ -699,25 +707,26 @@ function run_test_9() {
     do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
 
     do_execute_soon(run_test_10);
   });
 }
 
 // Checks that a removal from one location and an addition in another location
 // for the same item is handled
-function run_test_10() {
+async function run_test_10() {
   var dest = profileDir.clone();
   dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
   dest.remove(true);
   addon1.version = "1.3";
   writeInstallRDFForExtension(addon1, userDir);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon1@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
@@ -755,26 +764,27 @@ function run_test_10() {
     do_check_eq(a5, null);
     do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
 
     do_execute_soon(run_test_11);
   });
 }
 
 // This should remove any remaining items
-function run_test_11() {
+async function run_test_11() {
   var dest = userDir.clone();
   dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
   dest.remove(true);
   dest = profileDir.clone();
   dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
   dest.remove(true);
 
   gCachePurged = false;
-  restartManager();
+  await promiseRestartManager();
+
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon1@tests.mozilla.org",
                                         "addon2@tests.mozilla.org"]);
   check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
   do_check_true(gCachePurged);
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_targetPlatforms.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_targetPlatforms.js
@@ -84,26 +84,27 @@ var addon5 = {
 };
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
 // Set up the profile
-function run_test() {
+async function run_test() {
   do_test_pending();
 
   writeInstallRDFForExtension(addon1, profileDir);
   writeInstallRDFForExtension(addon2, profileDir);
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir);
   writeInstallRDFForExtension(addon5, profileDir);
 
-  restartManager();
+  await promiseRestartManager();
+
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org"],
                                function([a1, a2, a3, a4, a5]) {
 
     do_check_neq(a1, null);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_theme.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_theme.js
@@ -29,17 +29,17 @@ var LightweightThemeObserver = {
   }
 };
 
 AM_Cc["@mozilla.org/observer-service;1"]
      .getService(Components.interfaces.nsIObserverService)
      .addObserver(LightweightThemeObserver, "lightweight-theme-styling-update");
 
 
-function run_test() {
+async function run_test() {
   do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, "theme1/1.0");
   writeInstallRDFForExtension({
     id: "theme1@tests.mozilla.org",
     version: "1.0",
     name: "Test 1",
@@ -75,17 +75,17 @@ function run_test() {
     internalName: "classic/1.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "2"
     }]
   }, profileDir);
 
-  startupManager();
+  await promiseStartupManager();
   // Make sure we only register once despite multiple calls
   AddonManager.addInstallListener(InstallListener);
   AddonManager.addAddonListener(AddonListener);
   AddonManager.addInstallListener(InstallListener);
   AddonManager.addAddonListener(AddonListener);
   AddonManager.addInstallListener(InstallListener);
 
   AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
@@ -153,18 +153,19 @@ function run_test_1() {
     do_check_true(t1.userDisabled);
     do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
     do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
 
     do_execute_soon(check_test_1);
   });
 }
 
-function check_test_1() {
-  restartManager();
+async function check_test_1() {
+  await promiseRestartManager();
+
   do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme2/1.0");
 
   AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
                                "theme2@tests.mozilla.org"], function([t1, t2]) {
     do_check_neq(t1, null);
     do_check_true(t1.userDisabled);
     do_check_false(t1.appDisabled);
     do_check_false(t1.isActive);
@@ -185,22 +186,23 @@ function check_test_1() {
     do_check_false(gLWThemeChanged);
 
     do_execute_soon(run_test_2);
   });
 }
 
 // Removing the active theme should fall back to the default (not ideal in this
 // case since we don't have the default theme installed)
-function run_test_2() {
+async function run_test_2() {
   var dest = profileDir.clone();
   dest.append(do_get_expected_addon_name("theme2@tests.mozilla.org"));
   dest.remove(true);
 
-  restartManager();
+  await promiseRestartManager();
+
   do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
 
   AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
                                "theme2@tests.mozilla.org"], function([t1, t2]) {
     do_check_neq(t1, null);
     do_check_true(t1.userDisabled);
     do_check_false(t1.appDisabled);
     do_check_false(t1.isActive);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js
@@ -20,22 +20,22 @@ profileDir.append("extensions");
 
 // Sets up the profile by installing an add-on.
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   do_test_pending();
   startupManager();
 
-  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(olda1) {
+  AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(olda1) {
     do_check_eq(olda1, null);
 
     writeInstallRDFForExtension(addon1, profileDir);
 
-    restartManager();
+    await promiseRestartManager();
 
     AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
       do_check_neq(a1, null);
       do_check_true(a1.isActive);
       do_check_false(a1.userDisabled);
       do_check_true(isExtensionInAddonsList(profileDir, a1.id));
       do_check_eq(a1.pendingOperations, 0);
       do_check_in_crash_annotation(addon1.id, addon1.version);
@@ -87,18 +87,18 @@ function check_test_1() {
     dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
     do_check_false(dest.exists());
     writeInstallRDFForExtension(addon1, profileDir);
     do_execute_soon(run_test_2);
   });
 }
 
 // Cancelling the uninstall should send onOperationCancelled
-function run_test_2() {
-  restartManager();
+async function run_test_2() {
+  await promiseRestartManager();
 
   prepare_test({
     "addon1@tests.mozilla.org": [
       "onUninstalling"
     ]
   });
 
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
@@ -121,18 +121,18 @@ function run_test_2() {
     do_check_eq(a1.pendingOperations, 0);
 
     ensure_test_completed();
 
     do_execute_soon(check_test_2);
   });
 }
 
-function check_test_2() {
-  restartManager();
+async function check_test_2() {
+  await promiseRestartManager();
 
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
     do_check_neq(a1, null);
     do_check_true(a1.isActive);
     do_check_false(a1.userDisabled);
     do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
     run_test_3();
--- a/toolkit/mozapps/extensions/test/xpcshell/test_update.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update.js
@@ -216,24 +216,26 @@ for (let test of testParams) {
         });
       }
     }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
   };
 
   check_test_2 = () => {
     ensure_test_completed();
 
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(olda1) {
+    AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(olda1) {
+      await AddonTestUtils.loadAddonsList(true);
+
       do_check_neq(olda1, null);
       do_check_eq(olda1.version, "1.0");
       do_check_true(isExtensionInAddonsList(profileDir, olda1.id));
 
       shutdownManager();
 
-      startupManager();
+      await promiseStartupManager();
 
       do_check_true(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
 
       AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
         do_check_neq(a1, null);
         do_check_eq(a1.version, "2.0");
         do_check_true(isExtensionInAddonsList(profileDir, a1.id));
         do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js
@@ -211,24 +211,26 @@ for (let test of testParams) {
         });
       }
     }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
   };
 
   check_test_2 = () => {
     ensure_test_completed();
 
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(olda1) {
+    AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(async function(olda1) {
+      await AddonTestUtils.loadAddonsList(true);
+
       do_check_neq(olda1, null);
       do_check_eq(olda1.version, "1.0");
       do_check_true(isExtensionInAddonsList(profileDir, olda1.id));
 
       shutdownManager();
 
-      startupManager();
+      await promiseStartupManager();
 
       do_check_true(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
 
       AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
         do_check_neq(a1, null);
         do_check_eq(a1.version, "2.0");
         do_check_true(isExtensionInAddonsList(profileDir, a1.id));
         do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
@@ -91,18 +91,18 @@ function end_test() {
   } else {
     globalDir.append(do_get_expected_addon_name("addon4@tests.mozilla.org"));
     globalDir.remove(true);
   }
   do_execute_soon(do_test_finished);
 }
 
 // Test that the test extensions are all installed
-function run_test_1() {
-  startupManager();
+async function run_test_1() {
+  await promiseStartupManager();
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                                function([a1, a2, a3, a4]) {
 
     do_check_neq(a1, null);
@@ -118,31 +118,31 @@ function run_test_1() {
     do_check_true(isExtensionInAddonsList(globalDir, a4.id));
     do_check_eq(a4.version, "1.0");
 
     do_execute_soon(run_test_2);
   });
 }
 
 // Test that upgrading the application doesn't disable now incompatible add-ons
-function run_test_2() {
+async function run_test_2() {
   // Upgrade the extension
   var dest = writeInstallRDFForExtension({
     id: "addon4@tests.mozilla.org",
     version: "2.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "2",
       maxVersion: "2"
     }],
     name: "Test Addon 4",
   }, globalDir);
   setExtensionModifiedTime(dest, gInstallTime);
 
-  restartManager("2");
+  await promiseRestartManager("2");
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                                function([a1, a2, a3, a4]) {
 
     do_check_neq(a1, null);
     do_check_true(isExtensionInAddonsList(profileDir, a1.id));
@@ -173,17 +173,17 @@ function run_test_3() {
       maxVersion: "3"
     }],
     name: "Test Addon 4",
   }, globalDir);
   setExtensionModifiedTime(dest, gInstallTime);
 
   // Simulates a simple Build ID change, the platform deletes extensions.ini
   // whenever the application is changed.
-  gExtensionsINI.remove(true);
+  gAddonStartup.remove(true);
   restartManager();
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                                function([a1, a2, a3, a4]) {
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_strictcompat.js
@@ -94,18 +94,18 @@ function end_test() {
   }
 
   Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY);
 
   do_execute_soon(do_test_finished);
 }
 
 // Test that the test extensions are all installed
-function run_test_1() {
-  startupManager();
+async function run_test_1() {
+  await promiseStartupManager();
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                                function([a1, a2, a3, a4]) {
 
     do_check_neq(a1, null);
@@ -121,31 +121,32 @@ function run_test_1() {
     do_check_true(isExtensionInAddonsList(globalDir, a4.id));
     do_check_eq(a4.version, "1.0");
 
     do_execute_soon(run_test_2);
   });
 }
 
 // Test that upgrading the application disables now incompatible add-ons
-function run_test_2() {
+async function run_test_2() {
   // Upgrade the extension
   var dest = writeInstallRDFForExtension({
     id: "addon4@tests.mozilla.org",
     version: "2.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "2",
       maxVersion: "2"
     }],
     name: "Test Addon 4",
   }, globalDir);
   setExtensionModifiedTime(dest, gInstallTime);
 
-  restartManager("2");
+  await promiseRestartManager("2");
+
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                                function([a1, a2, a3, a4]) {
 
     do_check_neq(a1, null);
     do_check_false(isExtensionInAddonsList(profileDir, a1.id));
@@ -176,17 +177,17 @@ function run_test_3() {
       maxVersion: "3"
     }],
     name: "Test Addon 4",
   }, globalDir);
   setExtensionModifiedTime(dest, gInstallTime);
 
   // Simulates a simple Build ID change, the platform deletes extensions.ini
   // whenever the application is changed.
-  gExtensionsINI.remove(true);
+  gAddonStartup.remove(true);
   restartManager();
 
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                                function([a1, a2, a3, a4]) {
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
@@ -100,17 +100,18 @@ add_task(async function has_embedded_web
      "Got an webExtension property in the startup bootstrap method params");
   ok(("startup" in startupInfo.data.webExtension),
      "Got the expected 'startup' property in the webExtension object");
 
   // After restarting the manager, the add-on should still have the
   // hasEmbeddedWebExtension property as expected.
   await promiseRestartManager();
 
-  let persisted = JSON.parse(Services.prefs.getCharPref("extensions.bootstrappedAddons"));
+  let persisted = aomStartup.readStartupData()["app-profile"].addons;
+
   ok(ID in persisted, "Hybrid add-on persisted to bootstrappedAddons.");
   equal(persisted[ID].hasEmbeddedWebExtension, true,
         "hasEmbeddedWebExtension flag persisted to bootstrappedAddons.");
 
   // Check that the addon has been installed and started.
   BootstrapMonitor.checkAddonInstalled(ID, "1.0");
   BootstrapMonitor.checkAddonStarted(ID, "1.0");
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
@@ -22,19 +22,20 @@ function promiseAddonStartup() {
 
     Management.on("startup", listener);
   });
 }
 
 async function testSimpleIconsetParsing(manifest) {
   await promiseWriteWebManifestForExtension(manifest, profileDir);
 
-  await promiseRestartManager();
-  if (!manifest.theme)
-    await promiseAddonStartup();
+  await Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   let uri = do_get_addon_root_uri(profileDir, ID);
 
   let addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
 
   function check_icons(addon_copy) {
     deepEqual(addon_copy.icons, {
@@ -55,36 +56,36 @@ async function testSimpleIconsetParsing(
     equal(AddonManager.getPreferredIconURL(addon, 48), uri + "icon48.png");
     equal(AddonManager.getPreferredIconURL(addon, 64), uri + "icon64.png");
     equal(AddonManager.getPreferredIconURL(addon, 128), uri + "icon64.png");
   }
 
   check_icons(addon);
 
   // check if icons are persisted through a restart
-  await promiseRestartManager();
-  if (!manifest.theme)
-    await promiseAddonStartup();
+  await Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
 
   check_icons(addon);
 
   addon.uninstall();
-
-  await promiseRestartManager();
 }
 
 async function testRetinaIconsetParsing(manifest) {
   await promiseWriteWebManifestForExtension(manifest, profileDir);
 
-  await promiseRestartManager();
-  if (!manifest.theme)
-    await promiseAddonStartup();
+  await Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   let addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
 
   let uri = do_get_addon_root_uri(profileDir, ID);
 
   // AddonManager displays larger icons for higher pixel density
   equal(AddonManager.getPreferredIconURL(addon, 32, {
@@ -95,40 +96,37 @@ async function testRetinaIconsetParsing(
     devicePixelRatio: 2
   }), uri + "icon128.png");
 
   equal(AddonManager.getPreferredIconURL(addon, 64, {
     devicePixelRatio: 2
   }), uri + "icon128.png");
 
   addon.uninstall();
-
-  await promiseRestartManager();
 }
 
 async function testNoIconsParsing(manifest) {
   await promiseWriteWebManifestForExtension(manifest, profileDir);
 
-  await promiseRestartManager();
-  if (!manifest.theme)
-    await promiseAddonStartup();
+  await Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   let addon = await promiseAddonByID(ID);
   do_check_neq(addon, null);
 
   deepEqual(addon.icons, {});
 
   equal(addon.iconURL, null);
   equal(addon.icon64URL, null);
 
   equal(AddonManager.getPreferredIconURL(addon, 128), null);
 
   addon.uninstall();
-
-  await promiseRestartManager();
 }
 
 // Test simple icon set parsing
 add_task(async function() {
   await testSimpleIconsetParsing({
     name: "Web Extension Name",
     version: "1.0",
     manifest_version: 2,
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -302,16 +302,17 @@ skip-if = os == "android"
 [test_update_ignorecompat.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_updatecheck.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses hardcoded ports in xpi files.
 [test_json_updatecheck.js]
+[test_migrate_state_prefs.js]
 [test_seen.js]
 [test_seen_newprofile.js]
 [test_updateid.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses hardcoded ports in xpi files.
 [test_update_compatmode.js]
 [test_upgrade.js]
--- a/toolkit/xre/nsXREDirProvider.cpp
+++ b/toolkit/xre/nsXREDirProvider.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsAppRunner.h"
 #include "nsToolkitCompsCID.h"
 #include "nsXREDirProvider.h"
+#include "mozilla/AddonManagerStartup.h"
 
 #include "jsapi.h"
 #include "xpcpublic.h"
 
 #include "nsIAddonInterposition.h"
 #include "nsIAppStartup.h"
 #include "nsIDirectoryEnumerator.h"
 #include "nsIFile.h"
@@ -22,17 +23,16 @@
 #include "nsIXULRuntime.h"
 
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsDirectoryServiceDefs.h"
 #include "nsDirectoryServiceUtils.h"
 #include "nsXULAppAPI.h"
 #include "nsCategoryManagerUtils.h"
 
-#include "nsINIParser.h"
 #include "nsDependentString.h"
 #include "nsCOMArray.h"
 #include "nsArrayEnumerator.h"
 #include "nsEnumeratorUtils.h"
 #include "nsReadableUtils.h"
 
 #include "SpecialSystemDirectory.h"
 
@@ -81,25 +81,16 @@
 #if (defined(XP_WIN) || defined(XP_MACOSX)) && defined(MOZ_CONTENT_SANDBOX)
 static already_AddRefed<nsIFile> GetContentProcessSandboxTempDir();
 static nsresult DeleteDirIfExists(nsIFile *dir);
 static bool IsContentSandboxDisabled();
 static const char* GetContentProcessTempBaseDirKey();
 static already_AddRefed<nsIFile> CreateContentProcessSandboxTempDir();
 #endif
 
-static already_AddRefed<nsIFile>
-CloneAndAppend(nsIFile* aFile, const char* name)
-{
-  nsCOMPtr<nsIFile> file;
-  aFile->Clone(getter_AddRefs(file));
-  file->AppendNative(nsDependentCString(name));
-  return file.forget();
-}
-
 nsXREDirProvider* gDirServiceProvider = nullptr;
 
 nsXREDirProvider::nsXREDirProvider() :
   mProfileNotified(false)
 {
   gDirServiceProvider = this;
 }
 
@@ -593,17 +584,17 @@ LoadDirIntoArray(nsIFile* dir,
 
   bool exists;
   if (NS_SUCCEEDED(subdir->Exists(&exists)) && exists) {
     aDirectories.AppendObject(subdir);
   }
 }
 
 static void
-LoadDirsIntoArray(nsCOMArray<nsIFile>& aSourceDirs,
+LoadDirsIntoArray(const nsCOMArray<nsIFile>& aSourceDirs,
                   const char *const* aAppendList,
                   nsCOMArray<nsIFile>& aDirectories)
 {
   nsCOMPtr<nsIFile> appended;
   bool exists;
 
   for (int32_t i = 0; i < aSourceDirs.Count(); ++i) {
     aSourceDirs[i]->Clone(getter_AddRefs(appended));
@@ -654,83 +645,16 @@ nsXREDirProvider::GetFiles(const char* a
 
   rv = NS_NewUnionEnumerator(aResult, appEnum, xreEnum);
   if (NS_FAILED(rv))
     return rv;
 
   return NS_SUCCESS_AGGREGATE_RESULT;
 }
 
-static void
-RegisterExtensionInterpositions(nsINIParser &parser)
-{
-  if (!mozilla::Preferences::GetBool("extensions.interposition.enabled", false))
-    return;
-
-  nsCOMPtr<nsIAddonInterposition> interposition =
-    do_GetService("@mozilla.org/addons/multiprocess-shims;1");
-
-  nsresult rv;
-  int32_t i = 0;
-  do {
-    nsAutoCString buf("Extension");
-    buf.AppendInt(i++);
-
-    nsAutoCString addonId;
-    rv = parser.GetString("MultiprocessIncompatibleExtensions", buf.get(), addonId);
-    if (NS_FAILED(rv))
-      return;
-
-    if (!xpc::SetAddonInterposition(addonId, interposition))
-      continue;
-
-    if (!xpc::AllowCPOWsInAddon(addonId, true))
-      continue;
-  }
-  while (true);
-}
-
-static void
-LoadExtensionDirectories(nsINIParser &parser,
-                         const char *aSection,
-                         nsCOMArray<nsIFile> &aDirectories,
-                         NSLocationType aType)
-{
-  nsresult rv;
-  int32_t i = 0;
-  do {
-    nsAutoCString buf("Extension");
-    buf.AppendInt(i++);
-
-    nsAutoCString path;
-    rv = parser.GetString(aSection, buf.get(), path);
-    if (NS_FAILED(rv))
-      return;
-
-    nsCOMPtr<nsIFile> dir = do_CreateInstance("@mozilla.org/file/local;1", &rv);
-    if (NS_FAILED(rv))
-      continue;
-
-    rv = dir->SetPersistentDescriptor(path);
-    if (NS_FAILED(rv))
-      continue;
-
-    aDirectories.AppendObject(dir);
-    if (Substring(path, path.Length() - 4).EqualsLiteral(".xpi")) {
-      XRE_AddJarManifestLocation(aType, dir);
-    }
-    else {
-      nsCOMPtr<nsIFile> manifest =
-        CloneAndAppend(dir, "chrome.manifest");
-      XRE_AddManifestLocation(aType, manifest);
-    }
-  }
-  while (true);
-}
-
 #if (defined(XP_WIN) || defined(XP_MACOSX)) && defined(MOZ_CONTENT_SANDBOX)
 
 static const char*
 GetContentProcessTempBaseDirKey()
 {
 #if defined(XP_WIN)
   return NS_WIN_LOW_INTEGRITY_TEMP_BASE;
 #else
@@ -898,69 +822,16 @@ DeleteDirIfExists(nsIFile* dir)
     }
   }
   return NS_OK;
 }
 
 #endif // (defined(XP_WIN) || defined(XP_MACOSX)) &&
   // defined(MOZ_CONTENT_SANDBOX)
 
-void
-nsXREDirProvider::LoadExtensionBundleDirectories()
-{
-  if (!mozilla::Preferences::GetBool("extensions.defaultProviders.enabled", true))
-    return;
-
-  if (mProfileDir) {
-    if (!gSafeMode) {
-      nsCOMPtr<nsIFile> extensionsINI;
-      mProfileDir->Clone(getter_AddRefs(extensionsINI));
-      if (!extensionsINI)
-        return;
-
-      extensionsINI->AppendNative(NS_LITERAL_CSTRING("extensions.ini"));
-
-      nsCOMPtr<nsIFile> extensionsINILF =
-        do_QueryInterface(extensionsINI);
-      if (!extensionsINILF)
-        return;
-
-      nsINIParser parser;
-      nsresult rv = parser.Init(extensionsINILF);
-      if (NS_FAILED(rv))
-        return;
-
-      RegisterExtensionInterpositions(parser);
-      LoadExtensionDirectories(parser, "ExtensionDirs", mExtensionDirectories,
-                               NS_EXTENSION_LOCATION);
-      LoadExtensionDirectories(parser, "ThemeDirs", mThemeDirectories,
-                               NS_SKIN_LOCATION);
-/* non-Firefox applications that use overrides in their default theme should
- * define AC_DEFINE(MOZ_SEPARATE_MANIFEST_FOR_THEME_OVERRIDES) in their
- * configure.in */
-#if defined(MOZ_BUILD_APP_IS_BROWSER) || defined(MOZ_SEPARATE_MANIFEST_FOR_THEME_OVERRIDES)
-    } else {
-      // In safe mode, still load the default theme directory:
-      nsCOMPtr<nsIFile> themeManifest;
-      mXULAppDir->Clone(getter_AddRefs(themeManifest));
-      themeManifest->AppendNative(NS_LITERAL_CSTRING("extensions"));
-      themeManifest->AppendNative(NS_LITERAL_CSTRING("{972ce4c6-7e08-4474-a285-3208198ce6fd}.xpi"));
-      bool exists = false;
-      if (NS_SUCCEEDED(themeManifest->Exists(&exists)) && exists) {
-        XRE_AddJarManifestLocation(NS_SKIN_LOCATION, themeManifest);
-      } else {
-        themeManifest->SetNativeLeafName(NS_LITERAL_CSTRING("{972ce4c6-7e08-4474-a285-3208198ce6fd}"));
-        themeManifest->AppendNative(NS_LITERAL_CSTRING("chrome.manifest"));
-        XRE_AddManifestLocation(NS_SKIN_LOCATION, themeManifest);
-      }
-#endif
-    }
-  }
-}
-
 #ifdef MOZ_B2G
 void
 nsXREDirProvider::LoadAppBundleDirs()
 {
   nsCOMPtr<nsIFile> dir;
   bool persistent = false;
   nsresult rv = GetFile(XRE_APP_DISTRIBUTION_DIR, &persistent, getter_AddRefs(dir));
   if (NS_FAILED(rv))
@@ -1014,34 +885,34 @@ nsXREDirProvider::GetFilesInternal(const
 
   if (!strcmp(aProperty, XRE_EXTENSIONS_DIR_LIST)) {
     nsCOMArray<nsIFile> directories;
 
     static const char *const kAppendNothing[] = { nullptr };
 
     LoadDirsIntoArray(mAppBundleDirectories,
                       kAppendNothing, directories);
-    LoadDirsIntoArray(mExtensionDirectories,
+    LoadDirsIntoArray(AddonManagerStartup::GetSingleton().ExtensionPaths(),
                       kAppendNothing, directories);
 
     rv = NS_NewArrayEnumerator(aResult, directories);
   }
   else if (!strcmp(aProperty, NS_APP_PREFS_DEFAULTS_DIR_LIST)) {
     nsCOMArray<nsIFile> directories;
 
     LoadDirIntoArray(mXULAppDir, kAppendPrefDir, directories);
     LoadDirsIntoArray(mAppBundleDirectories,
                       kAppendPrefDir, directories);
 
     rv = NS_NewArrayEnumerator(aResult, directories);
   }
   else if (!strcmp(aProperty, NS_EXT_PREFS_DEFAULTS_DIR_LIST)) {
     nsCOMArray<nsIFile> directories;
 
-    LoadDirsIntoArray(mExtensionDirectories,
+    LoadDirsIntoArray(AddonManagerStartup::GetSingleton().ExtensionPaths(),
                       kAppendPrefDir, directories);
 
     if (mProfileDir) {
       nsCOMPtr<nsIFile> overrideFile;
       mProfileDir->Clone(getter_AddRefs(overrideFile));
       overrideFile->AppendNative(NS_LITERAL_CSTRING(PREF_OVERRIDE_DIRNAME));
 
       bool exists;
@@ -1058,17 +929,17 @@ nsXREDirProvider::GetFilesInternal(const
     static const char *const kAppendChromeDir[] = { "chrome", nullptr };
     nsCOMArray<nsIFile> directories;
     LoadDirIntoArray(mXULAppDir,
                      kAppendChromeDir,
                      directories);
     LoadDirsIntoArray(mAppBundleDirectories,
                       kAppendChromeDir,
                       directories);
-    LoadDirsIntoArray(mExtensionDirectories,
+    LoadDirsIntoArray(AddonManagerStartup::GetSingleton().ExtensionPaths(),
                       kAppendChromeDir,
                       directories);
 
     rv = NS_NewArrayEnumerator(aResult, directories);
   }
   else if (!strcmp(aProperty, NS_APP_PLUGINS_DIR_LIST)) {
     nsCOMArray<nsIFile> directories;
 
@@ -1083,17 +954,17 @@ nsXREDirProvider::GetFilesInternal(const
 
     static const char *const kAppendPlugins[] = { "plugins", nullptr };
 
     // The root dirserviceprovider does quite a bit for us: we're mainly
     // interested in xulapp and extension-provided plugins.
     LoadDirsIntoArray(mAppBundleDirectories,
                       kAppendPlugins,
                       directories);
-    LoadDirsIntoArray(mExtensionDirectories,
+    LoadDirsIntoArray(AddonManagerStartup::GetSingleton().ExtensionPaths(),
                       kAppendPlugins,
                       directories);
 
     if (mProfileDir) {
       nsCOMArray<nsIFile> profileDir;
       profileDir.AppendObject(mProfileDir);
       LoadDirsIntoArray(profileDir,
                         kAppendPlugins,
@@ -1156,18 +1027,16 @@ nsXREDirProvider::DoStartup()
     // Init the Extension Manager
     nsCOMPtr<nsIObserver> em = do_GetService("@mozilla.org/addons/integration;1");
     if (em) {
       em->Observe(nullptr, "addons-startup", nullptr);
     } else {
       NS_WARNING("Failed to create Addons Manager.");
     }
 
-    LoadExtensionBundleDirectories();
-
     obsSvc->NotifyObservers(nullptr, "load-extension-defaults", nullptr);
     obsSvc->NotifyObservers(nullptr, "profile-after-change", kStartup);
 
     // Any component that has registered for the profile-after-change category
     // should also be created at this time.
     (void)NS_CreateServicesFromCategory("profile-after-change", nullptr,
                                         "profile-after-change");
 
--- a/toolkit/xre/nsXREDirProvider.h
+++ b/toolkit/xre/nsXREDirProvider.h
@@ -121,19 +121,16 @@ protected:
   // delimiters.
   static inline nsresult AppendProfileString(nsIFile* aFile, const char* aPath);
 
 #if (defined(XP_WIN) || defined(XP_MACOSX)) && defined(MOZ_CONTENT_SANDBOX)
   // Load the temp directory for sandboxed content processes
   nsresult LoadContentProcessTempDir();
 #endif
 
-  // Calculate and register extension and theme bundle directories.
-  void LoadExtensionBundleDirectories();
-
 #ifdef MOZ_B2G
   // Calculate and register app-bundled extension directories.
   void LoadAppBundleDirs();
 #endif
 
   void Append(nsIFile* aDirectory);
 
   nsCOMPtr<nsIDirectoryServiceProvider> mAppProvider;
@@ -146,13 +143,11 @@ protected:
   nsCOMPtr<nsIFile>      mProfileDir;
   nsCOMPtr<nsIFile>      mProfileLocalDir;
   bool                   mProfileNotified;
 #if (defined(XP_WIN) || defined(XP_MACOSX)) && defined(MOZ_CONTENT_SANDBOX)
   nsCOMPtr<nsIFile>      mContentTempDir;
   nsCOMPtr<nsIFile>      mContentProcessSandboxTempDir;
 #endif
   nsCOMArray<nsIFile>    mAppBundleDirectories;
-  nsCOMArray<nsIFile>    mExtensionDirectories;
-  nsCOMArray<nsIFile>    mThemeDirectories;
 };
 
 #endif