Bug 1358846: Part 4 - Merge various startup information stores into a single JSON file. r=rhelmer r?jonco draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 29 Apr 2017 16:18:54 -0700
changeset 570715 cb5e655044ff0445af50c95d9123c56ac5dbbbc5
parent 570712 8bf7ca9a5741fb8128742dfedd3025cfafd11e1a
child 570716 db0bce4c4685a8e5614a53e846746f87a9514e0e
push id56556
push usermaglione.k@gmail.com
push dateSat, 29 Apr 2017 23:19:55 +0000
reviewersrhelmer, jonco
bugs1358846
milestone55.0a1
Bug 1358846: Part 4 - Merge various startup information stores into a single JSON file. r=rhelmer r?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_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_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)
 
@@ -119,16 +120,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_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)
 
 #if defined(MOZ_HAS_TERMINATOR)
 NS_DEFINE_NAMED_CID(NS_TOOLKIT_TERMINATOR_CID);
@@ -152,16 +154,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);
 
 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
 #if defined(MOZ_HAS_PERFSTATS)
@@ -186,16 +189,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 },
   { nullptr }
 };
 
 static const Module::ContractIDEntry kToolkitContracts[] = {
   { NS_APPSTARTUP_CONTRACTID, &kNS_TOOLKIT_APPSTARTUP_CID },
 #if defined(MOZ_HAS_TERMINATOR)
   { NS_TOOLKIT_TERMINATOR_CONTRACTID, &kNS_TOOLKIT_TERMINATOR_CID },
@@ -222,16 +226,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 },
   { 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;
+
+  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,561 @@
+/* -*- 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.
+  nsDependentCString 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, 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, 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 CheckStartupModifications() { 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)
+  {}
+
+  nsString Id() { return mId; }
+
+  nsString Path() { return GetString("path"); }
+
+  bool Bootstrapped() { return GetBool("bootstrapped"); }
+
+  bool Enabled() { return GetBool("enabled"); }
+
+  bool EnableShims() { 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, 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(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 (InstallLocation loc : PropertyIter(cx, locs)) {
+    if (!loc.CheckStartupModifications()) {
+      continue;
+    }
+
+    for (Addon addon : loc.Addons()) {
+      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 (InstallLocation loc : PropertyIter(cx, locs)) {
+    for (Addon addon : loc.Addons()) {
+      if (!addon.Bootstrapped()) {
+        AddInstallLocation(addon);
+      }
+
+      if (enableInterpositions && addon.EnableShims()) {
+        nsString addonId = addon.Id();
+        EnableShims(addonId);
+      }
+    }
+  }
+
+  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
@@ -25,28 +25,33 @@ Cu.import("resource://gre/modules/Task.j
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
 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");
@@ -124,69 +129,65 @@ 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();
+            file.appendRelativePath(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;
     });
   }
@@ -203,31 +204,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.
@@ -546,37 +547,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()
@@ -596,16 +593,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) {
@@ -613,18 +616,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
@@ -53,16 +53,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,
@@ -76,16 +78,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", () => {
@@ -94,33 +99,84 @@ XPCOMUtils.defineLazyGetter(this, "IconD
 });
 
 
 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;
+  }
+}
+
+/**
+ * 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.
+ * @returns {string}
+ *        The file's path.
+ */
+function descriptorToPath(descriptor) {
+  try {
+    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    file.persistentDescriptor = descriptor;
+    return file.path;
+  } 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";
@@ -145,26 +201,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";
@@ -176,16 +240,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";
 
@@ -333,34 +402,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;
@@ -1663,17 +1729,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);
@@ -2070,17 +2136,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();
     }
   }
 }
 
@@ -2095,93 +2163,183 @@ 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;
+    }
+  }
+
+  /**
+   * 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),
+      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);
+
+      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) {
+      return this.file.getRelativePath(this.location.dir);
+    }
+    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.
@@ -2191,48 +2349,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(),
 
@@ -2242,134 +2502,188 @@ 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));
+        continue;
+      }
+
+      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});
+
+          changed = true;
+          xpiState = loc.addFile(id, file);
+          if (!location.isSystem) {
+            this.sideLoadedAddons.set(id, xpiState);
           }
-
-          this.db.set(location.name, foundAddons);
-          delete oldState[location.name];
-        }
-        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)) {
-          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);
         } 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.
@@ -2378,58 +2692,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) {
@@ -2440,18 +2794,16 @@ this.XPIStates = {
       if (location.size == 0) {
         this.db.delete(aLocation);
       }
       this.save();
     }
   },
 };
 
-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
@@ -2465,27 +2817,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,
@@ -2496,41 +2844,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);
 
@@ -2804,17 +3150,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.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS);
@@ -2829,16 +3174,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);
 
@@ -2856,18 +3202,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",
@@ -2876,25 +3220,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);
@@ -2908,27 +3251,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
@@ -2969,49 +3310,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;
@@ -3117,19 +3453,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
@@ -3295,52 +3629,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 => `${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);
@@ -3434,16 +3746,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;
           }
         }
@@ -3502,46 +3815,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();
 
@@ -3663,16 +3978,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))
@@ -3726,24 +4042,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) {
@@ -3761,25 +4072,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);
@@ -3792,34 +4094,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);
@@ -3853,43 +4137,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>>}
@@ -4802,36 +5080,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);
@@ -4905,18 +5174,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 });
     }
   },
@@ -5154,33 +5421,16 @@ 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();
@@ -5301,19 +5551,19 @@ 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);
@@ -5359,16 +5609,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",
@@ -8038,16 +8291,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) {
@@ -8495,16 +8752,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.
  */
@@ -8687,17 +8946,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");
@@ -33,49 +33,48 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
                                    "@mozilla.org/extensions/blocklist;1",
                                    Ci.nsIBlocklistService);
 
 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
@@ -184,33 +183,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) &&
@@ -487,21 +493,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();
@@ -627,72 +632,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) {
@@ -925,29 +895,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;
   },
@@ -955,61 +925,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
@@ -1021,21 +1002,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.
@@ -1085,16 +1068,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;
@@ -1133,104 +1125,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) {
@@ -1313,18 +1217,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) {
@@ -1369,17 +1272,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
@@ -1410,18 +1313,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;
@@ -1442,37 +1344,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.
    *
@@ -1497,27 +1399,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"];
@@ -1564,17 +1464,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;
       }
@@ -1620,29 +1520,31 @@ this.XPIDatabaseReconcile = {
               if (xpiState.mtime < oldAddon.updateDate) {
                 XPIProvider.setTelemetry(oldAddon.id, "olderFile", {
                   mtime: xpiState.mtime,
                   oldtime: oldAddon.updateDate
                 });
               }
             }
 
+            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 {
@@ -1701,17 +1603,16 @@ 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)
@@ -1727,17 +1628,17 @@ this.XPIDatabaseReconcile = {
 
         // 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
@@ -1779,18 +1680,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);
           }
         }
 
@@ -1799,29 +1699,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))
@@ -1832,16 +1719,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) {
@@ -1862,17 +1750,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);
--- 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(function* detect_touches() {
   startupManager();
   let [/* pe */, pd, /* ue */, ud] = yield 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(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(function* uninstall_bootstrap() {
   let [pe, /* pd, ue, ud */] = yield 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 = yield 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(function* install_bootstrap() {
   let XS = getXS();
 
   let installer = yield 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(function* install_restart() {
   newAddon = null;
   yield promiseRestartManager();
   XS = getXS();
 
   newAddon = yield 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;
   yield 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_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(function* test_userDisabledNotP
 
   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 = yield 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.
   yield 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 = yield 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(function* init() {
   }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
 
   yield deferredUpdateFinished.promise;
 });
 
 
 add_task(function* run_test_1() {
   restartManager();
+
   let [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
     yield 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(function* run_test_1() {
    try {
      shutdownManager();
    } catch (e) {
      // We're expecting an error here.
    }
    do_print("Unlocking " + gExtensionsJSON.path);
    yield file.close();
    gExtensionsJSON.permissions = filePermissions;
-   startupManager();
-
+   yield promiseStartupManager();
 
    // Shouldn't have seen any startup changes
    check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
    [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
      yield 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(function*() {
     options.unixFlags = OS.Constants.libc.O_EXLOCK;
 
   let file = yield 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);
+  yield promiseStartupManager(false);
 
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
 
   [a1, a2, a3, a4, a5, a6] =
     yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
                               "addon2@tests.mozilla.org",
                               "addon3@tests.mozilla.org",
@@ -220,17 +220,17 @@ add_task(function*() {
   let shutdownError;
   try {
     shutdownManager();
   } catch (e) {
     shutdownError = e;
   }
   yield file.close();
   gExtensionsJSON.permissions = filePermissions;
-  startupManager();
+  yield 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,
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate_state_prefs.js
@@ -0,0 +1,112 @@
+/* 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 } });
+
+  // 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: file1.lastModifiedTime},
+      [ID2]: {e: false, d: file2.persistentDescriptor, v: "0.2", mt: file2.lastModifiedTime},
+      [ID3]: {e: true, d: file3.persistentDescriptor, v: "0.3", mt: file3.lastModifiedTime},
+      [ID4]: {e: false, d: file4.persistentDescriptor, v: "0.4", mt: file4.lastModifiedTime},
+    }
+  }));
+
+  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, file1.lastModifiedTime, "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, file2.lastModifiedTime, "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, file3.lastModifiedTime, "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, file4.lastModifiedTime, "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(function*() {
 add_task(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
   yield promiseSetExtensionModifiedTime(file.path, Date.now() - 60000);
 
-  startupManager();
+  yield promiseStartupManager();
   let addon = yield 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));
 
   yield promiseShutdownManager();
 
   clearCache(file);
   breakAddon(file);
 
-  startupManager();
+  yield promiseStartupManager();
 
   addon = yield 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 @@ function* test_breaking_migrate(addons, 
 
   // 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);
+  yield promiseStartupManager(true);
 
   let addon = yield 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 @@ function* test_working_migrate(addons, t
 
   // 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);
+  yield promiseStartupManager(true);
 
   let addon = yield 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(function* has_embedded_webexten
      "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.
   yield 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);
   });
 }
 
 function* testSimpleIconsetParsing(manifest) {
   yield promiseWriteWebManifestForExtension(manifest, profileDir);
 
-  yield promiseRestartManager();
-  if (!manifest.theme)
-    yield promiseAddonStartup();
+  yield Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   let uri = do_get_addon_root_uri(profileDir, ID);
 
   let addon = yield promiseAddonByID(ID);
   do_check_neq(addon, null);
 
   function check_icons(addon_copy) {
     deepEqual(addon_copy.icons, {
@@ -55,36 +56,36 @@ function* testSimpleIconsetParsing(manif
     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
-  yield promiseRestartManager();
-  if (!manifest.theme)
-    yield promiseAddonStartup();
+  yield Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   addon = yield promiseAddonByID(ID);
   do_check_neq(addon, null);
 
   check_icons(addon);
 
   addon.uninstall();
-
-  yield promiseRestartManager();
 }
 
 function* testRetinaIconsetParsing(manifest) {
   yield promiseWriteWebManifestForExtension(manifest, profileDir);
 
-  yield promiseRestartManager();
-  if (!manifest.theme)
-    yield promiseAddonStartup();
+  yield Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   let addon = yield 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 @@ function* testRetinaIconsetParsing(manif
     devicePixelRatio: 2
   }), uri + "icon128.png");
 
   equal(AddonManager.getPreferredIconURL(addon, 64, {
     devicePixelRatio: 2
   }), uri + "icon128.png");
 
   addon.uninstall();
-
-  yield promiseRestartManager();
 }
 
 function* testNoIconsParsing(manifest) {
   yield promiseWriteWebManifestForExtension(manifest, profileDir);
 
-  yield promiseRestartManager();
-  if (!manifest.theme)
-    yield promiseAddonStartup();
+  yield Promise.all([
+    promiseRestartManager(),
+    manifest.theme || promiseAddonStartup(),
+  ]);
 
   let addon = yield 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();
-
-  yield promiseRestartManager();
 }
 
 // Test simple icon set parsing
 add_task(function*() {
   yield* 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