Merge mozilla-central to autoland. on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Fri, 19 Apr 2019 06:56:17 +0300
changeset 470144 cb9a0420ac32c5e52f52d1b4f3c18a8065935b68
parent 470143 bcd124c140a275c26c764c1793f4b036a37c639f (current diff)
parent 470056 f7a15eb24f3de849d7b42dca8b827ade5354df0e (diff)
child 470145 cebb49d20b00f9c076ca49862653dae263074d11
push id35888
push useraiakab@mozilla.com
push dateFri, 19 Apr 2019 09:47:45 +0000
treeherdermozilla-central@0160424142d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. on a CLOSED TREE
devtools/client/themes/rules.css
modules/libpref/init/all.js
--- a/devtools/client/inspector/index.xhtml
+++ b/devtools/client/inspector/index.xhtml
@@ -94,19 +94,19 @@
               <input id="ruleview-searchbox"
                      class="devtools-filterinput devtools-rule-searchbox"
                      type="search"
                      data-localization="placeholder=inspector.filterStyles.placeholder"/>
               <button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></button>
             </div>
             <div class="devtools-separator"></div>
             <div id="ruleview-command-toolbar">
-              <button id="ruleview-add-rule-button" data-localization="title=inspector.addRule.tooltip" class="devtools-button"></button>
               <button id="pseudo-class-panel-toggle" data-localization="title=inspector.togglePseudo.tooltip" class="devtools-button"></button>
               <button id="class-panel-toggle" data-localization="title=inspector.classPanel.toggleClass.tooltip" class="devtools-button"></button>
+              <button id="ruleview-add-rule-button" data-localization="title=inspector.addRule.tooltip" class="devtools-button"></button>
               <button id="print-simulation-toggle" data-localization="title=inspector.printSimulation.tooltip" class="devtools-button" hidden="true"></button>
             </div>
           </div>
           <div id="pseudo-class-panel" class="ruleview-reveal-panel" hidden="true">
             <label><input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</label>
             <label><input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</label>
             <label><input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</label>
             <label><input id="pseudo-focus-within-toggle" type="checkbox" value=":focus-within" tabindex="-1" />:focus-within</label>
--- a/devtools/client/inspector/rules/actions/index.js
+++ b/devtools/client/inspector/rules/actions/index.js
@@ -26,16 +26,19 @@ createEnum([
   "UPDATE_CLASSES",
 
   // Updates whether or not the class list panel is expanded.
   "UPDATE_CLASS_PANEL_EXPANDED",
 
   // Updates the highlighted selector.
   "UPDATE_HIGHLIGHTED_SELECTOR",
 
+  // Updates whether or not the print simulation button is hidden.
+  "UPDATE_PRINT_SIMULATION_HIDDEN",
+
   // Updates the rules state with the new list of CSS rules for the selected element.
   "UPDATE_RULES",
 
   // Updates whether or not the source links are enabled.
   "UPDATE_SOURCE_LINK_ENABLED",
 
   // Updates the source link information for a given rule.
   "UPDATE_SOURCE_LINK",
--- a/devtools/client/inspector/rules/actions/rules.js
+++ b/devtools/client/inspector/rules/actions/rules.js
@@ -2,16 +2,17 @@
  * 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/. */
 
 "use strict";
 
 const {
   UPDATE_ADD_RULE_ENABLED,
   UPDATE_HIGHLIGHTED_SELECTOR,
+  UPDATE_PRINT_SIMULATION_HIDDEN,
   UPDATE_RULES,
   UPDATE_SOURCE_LINK_ENABLED,
   UPDATE_SOURCE_LINK,
 } = require("./index");
 
 module.exports = {
 
   /**
@@ -36,16 +37,29 @@ module.exports = {
   updateHighlightedSelector(highlightedSelector) {
     return {
       type: UPDATE_HIGHLIGHTED_SELECTOR,
       highlightedSelector,
     };
   },
 
   /**
+   * Updates whether or not the print simulation button is hidden.
+   *
+   * @param  {Boolean} hidden
+   *         Whether or not the print simulation button is hidden.
+   */
+  updatePrintSimulationHidden(hidden) {
+    return {
+      type: UPDATE_PRINT_SIMULATION_HIDDEN,
+      hidden,
+    };
+  },
+
+  /**
    * Updates the rules state with the new list of CSS rules for the selected element.
    *
    * @param  {Array} rules
    *         Array of Rule objects containing the selected element's CSS rules.
    */
   updateRules(rules) {
     return {
       type: UPDATE_RULES,
--- a/devtools/client/inspector/rules/components/RulesApp.js
+++ b/devtools/client/inspector/rules/components/RulesApp.js
@@ -29,16 +29,17 @@ class RulesApp extends PureComponent {
   static get propTypes() {
     return {
       onAddClass: PropTypes.func.isRequired,
       onAddRule: PropTypes.func.isRequired,
       onOpenSourceLink: PropTypes.func.isRequired,
       onSetClassState: PropTypes.func.isRequired,
       onToggleClassPanelExpanded: PropTypes.func.isRequired,
       onToggleDeclaration: PropTypes.func.isRequired,
+      onTogglePrintSimulation: PropTypes.func.isRequired,
       onTogglePseudoClass: PropTypes.func.isRequired,
       onToggleSelectorHighlighter: PropTypes.func.isRequired,
       rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
       showDeclarationNameEditor: PropTypes.func.isRequired,
       showDeclarationValueEditor: PropTypes.func.isRequired,
       showNewDeclarationEditor: PropTypes.func.isRequired,
       showSelectorEditor: PropTypes.func.isRequired,
     };
@@ -178,16 +179,17 @@ class RulesApp extends PureComponent {
           id: "sidebar-panel-ruleview",
           className: "theme-sidebar inspector-tabpanel",
         },
         Toolbar({
           onAddClass: this.props.onAddClass,
           onAddRule: this.props.onAddRule,
           onSetClassState: this.props.onSetClassState,
           onToggleClassPanelExpanded: this.props.onToggleClassPanelExpanded,
+          onTogglePrintSimulation: this.props.onTogglePrintSimulation,
           onTogglePseudoClass: this.props.onTogglePseudoClass,
         }),
         dom.div(
           {
             id: "ruleview-container",
             className: "ruleview",
           },
           dom.div(
--- a/devtools/client/inspector/rules/components/Toolbar.js
+++ b/devtools/client/inspector/rules/components/Toolbar.js
@@ -20,34 +20,39 @@ loader.lazyGetter(this, "PseudoClassPane
 
 const { getStr } = require("../utils/l10n");
 
 class Toolbar extends PureComponent {
   static get propTypes() {
     return {
       isAddRuleEnabled: PropTypes.bool.isRequired,
       isClassPanelExpanded: PropTypes.bool.isRequired,
+      isPrintSimulationHidden: PropTypes.bool.isRequired,
       onAddClass: PropTypes.func.isRequired,
       onAddRule: PropTypes.func.isRequired,
       onSetClassState: PropTypes.func.isRequired,
       onToggleClassPanelExpanded: PropTypes.func.isRequired,
+      onTogglePrintSimulation: PropTypes.func.isRequired,
       onTogglePseudoClass: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
+      // Whether or not the print simulation button is enabled.
+      isPrintSimulationEnabled: false,
       // Whether or not the pseudo class panel is expanded.
       isPseudoClassPanelExpanded: false,
     };
 
     this.onAddRuleClick = this.onAddRuleClick.bind(this);
     this.onClassPanelToggle = this.onClassPanelToggle.bind(this);
+    this.onPrintSimulationToggle = this.onPrintSimulationToggle.bind(this);
     this.onPseudoClassPanelToggle = this.onPseudoClassPanelToggle.bind(this);
   }
 
   onAddRuleClick(event) {
     event.stopPropagation();
     this.props.onAddRule();
   }
 
@@ -60,63 +65,88 @@ class Toolbar extends PureComponent {
       return {
         isPseudoClassPanelExpanded: isClassPanelExpanded ?
                                     false :
                                     prevState.isPseudoClassPanelExpanded,
       };
     });
   }
 
+  onPrintSimulationToggle(event) {
+    event.stopPropagation();
+    this.props.onTogglePrintSimulation();
+    this.setState(prevState => ({
+      isPrintSimulationEnabled: !prevState.isPrintSimulationEnabled,
+    }));
+  }
+
   onPseudoClassPanelToggle(event) {
     event.stopPropagation();
 
     const isPseudoClassPanelExpanded = !this.state.isPseudoClassPanelExpanded;
 
     if (isPseudoClassPanelExpanded) {
       this.props.onToggleClassPanelExpanded(false);
     }
 
     this.setState({ isPseudoClassPanelExpanded });
   }
 
   render() {
-    const { isAddRuleEnabled, isClassPanelExpanded } = this.props;
-    const { isPseudoClassPanelExpanded } = this.state;
+    const {
+      isAddRuleEnabled,
+      isClassPanelExpanded,
+      isPrintSimulationHidden,
+    } = this.props;
+    const {
+      isPrintSimulationEnabled,
+      isPseudoClassPanelExpanded,
+    } = this.state;
 
     return (
       dom.div(
         {
           id: "ruleview-toolbar-container",
           className: "devtools-toolbar",
         },
         dom.div({ id: "ruleview-toolbar" },
           SearchBox({}),
           dom.div({ className: "devtools-separator" }),
           dom.div({ id: "ruleview-command-toolbar" },
             dom.button({
+              id: "pseudo-class-panel-toggle",
+              className: "devtools-button" +
+                         (isPseudoClassPanelExpanded ? " checked" : ""),
+              onClick: this.onPseudoClassPanelToggle,
+              title: getStr("rule.togglePseudo.tooltip"),
+            }),
+            dom.button({
+              id: "class-panel-toggle",
+              className: "devtools-button" +
+                         (isClassPanelExpanded ? " checked" : ""),
+              onClick: this.onClassPanelToggle,
+              title: getStr("rule.classPanel.toggleClass.tooltip"),
+            }),
+            dom.button({
               id: "ruleview-add-rule-button",
               className: "devtools-button",
               disabled: !isAddRuleEnabled,
               onClick: this.onAddRuleClick,
               title: getStr("rule.addRule.tooltip"),
             }),
-            dom.button({
-              id: "pseudo-class-panel-toggle",
-              className: "devtools-button" +
-                          (isPseudoClassPanelExpanded ? " checked" : ""),
-              onClick: this.onPseudoClassPanelToggle,
-              title: getStr("rule.togglePseudo.tooltip"),
-            }),
-            dom.button({
-              id: "class-panel-toggle",
-              className: "devtools-button" +
-                          (isClassPanelExpanded ? " checked" : ""),
-              onClick: this.onClassPanelToggle,
-              title: getStr("rule.classPanel.toggleClass.tooltip"),
-            })
+            !isPrintSimulationHidden ?
+              dom.button({
+                id: "print-simulation-toggle",
+                className: "devtools-button" +
+                           (isPrintSimulationEnabled ? " checked" : ""),
+                onClick: this.onPrintSimulationToggle,
+                title: getStr("rule.printSimulation.tooltip"),
+              })
+              :
+              null
           )
         ),
         isClassPanelExpanded ?
           ClassListPanel({
             onAddClass: this.props.onAddClass,
             onSetClassState: this.props.onSetClassState,
           })
           :
@@ -131,12 +161,13 @@ class Toolbar extends PureComponent {
     );
   }
 }
 
 const mapStateToProps = state => {
   return {
     isAddRuleEnabled: state.rules.isAddRuleEnabled,
     isClassPanelExpanded: state.classList.isClassPanelExpanded,
+    isPrintSimulationHidden: state.rules.isPrintSimulationHidden,
   };
 };
 
 module.exports = connect(mapStateToProps)(Toolbar);
--- a/devtools/client/inspector/rules/new-rules.js
+++ b/devtools/client/inspector/rules/new-rules.js
@@ -17,16 +17,17 @@ const {
 const {
   disableAllPseudoClasses,
   setPseudoClassLocks,
   togglePseudoClass,
 } = require("./actions/pseudo-classes");
 const {
   updateAddRuleEnabled,
   updateHighlightedSelector,
+  updatePrintSimulationHidden,
   updateRules,
   updateSourceLinkEnabled,
 } = require("./actions/rules");
 
 const RulesApp = createFactory(require("./components/RulesApp"));
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
@@ -57,16 +58,17 @@ class RulesView {
 
     this.onAddClass = this.onAddClass.bind(this);
     this.onAddRule = this.onAddRule.bind(this);
     this.onOpenSourceLink = this.onOpenSourceLink.bind(this);
     this.onSelection = this.onSelection.bind(this);
     this.onSetClassState = this.onSetClassState.bind(this);
     this.onToggleClassPanelExpanded = this.onToggleClassPanelExpanded.bind(this);
     this.onToggleDeclaration = this.onToggleDeclaration.bind(this);
+    this.onTogglePrintSimulation = this.onTogglePrintSimulation.bind(this);
     this.onTogglePseudoClass = this.onTogglePseudoClass.bind(this);
     this.onToolChanged = this.onToolChanged.bind(this);
     this.onToggleSelectorHighlighter = this.onToggleSelectorHighlighter.bind(this);
     this.showDeclarationNameEditor = this.showDeclarationNameEditor.bind(this);
     this.showDeclarationValueEditor = this.showDeclarationValueEditor.bind(this);
     this.showNewDeclarationEditor = this.showNewDeclarationEditor.bind(this);
     this.showSelectorEditor = this.showSelectorEditor.bind(this);
     this.updateClassList = this.updateClassList.bind(this);
@@ -90,35 +92,57 @@ class RulesView {
 
     const rulesApp = RulesApp({
       onAddClass: this.onAddClass,
       onAddRule: this.onAddRule,
       onOpenSourceLink: this.onOpenSourceLink,
       onSetClassState: this.onSetClassState,
       onToggleClassPanelExpanded: this.onToggleClassPanelExpanded,
       onToggleDeclaration: this.onToggleDeclaration,
+      onTogglePrintSimulation: this.onTogglePrintSimulation,
       onTogglePseudoClass: this.onTogglePseudoClass,
       onToggleSelectorHighlighter: this.onToggleSelectorHighlighter,
       showDeclarationNameEditor: this.showDeclarationNameEditor,
       showDeclarationValueEditor: this.showDeclarationValueEditor,
       showNewDeclarationEditor: this.showNewDeclarationEditor,
       showSelectorEditor: this.showSelectorEditor,
     });
 
+    this.initPrintSimulation();
+
     const provider = createElement(Provider, {
       id: "ruleview",
       key: "ruleview",
       store: this.store,
       title: INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
     }, rulesApp);
 
     // Exposes the provider to let inspector.js use it in setupSidebar.
     this.provider = provider;
   }
 
+  async initPrintSimulation() {
+    const target = this.inspector.target;
+
+    // In order to query if the emulation actor's print simulation methods are supported,
+    // we have to call the emulation front so that the actor is lazily loaded. This allows
+    // us to use `actorHasMethod`. Please see `getActorDescription` for more information.
+    this.emulationFront = await target.getFront("emulation");
+
+    // Show the toggle button if:
+    // - Print simulation is supported for the current target.
+    // - Not debugging content document.
+    if (await target.actorHasMethod("emulation", "getIsPrintSimulationEnabled") &&
+        !target.chrome) {
+      this.store.dispatch(updatePrintSimulationHidden(false));
+    } else {
+      this.store.dispatch(updatePrintSimulationHidden(true));
+    }
+  }
+
   destroy() {
     this.inspector.sidebar.off("select", this.onSelection);
     this.selection.off("detached-front", this.onSelection);
     this.selection.off("new-node-front", this.onSelection);
     this.toolbox.off("tool-registered", this.onToolChanged);
     this.toolbox.off("tool-unregistered", this.onToolChanged);
 
     if (this._autocompletePopup) {
@@ -134,22 +158,27 @@ class RulesView {
 
     if (this._selectHighlighter) {
       this._selectorHighlighter.finalize();
       this._selectorHighlighter = null;
     }
 
     if (this.elementStyle) {
       this.elementStyle.destroy();
+      this.elementStyle = null;
+    }
+
+    if (this.emulationFront) {
+      this.emulationFront.destroy();
+      this.emulationFront = null;
     }
 
     this._dummyElement = null;
     this.cssProperties = null;
     this.doc = null;
-    this.elementStyle = null;
     this.inspector = null;
     this.pageStyle = null;
     this.selection = null;
     this.showUserAgentStyles = null;
     this.store = null;
     this.telemetry = null;
     this.toolbox = null;
   }
@@ -374,16 +403,31 @@ class RulesView {
   onToggleDeclaration(ruleId, declarationId) {
     this.elementStyle.toggleDeclaration(ruleId, declarationId);
     this.telemetry.recordEvent("edit_rule", "ruleview", null, {
       "session_id": this.toolbox.sessionId,
     });
   }
 
   /**
+   * Handler for toggling print media simulation.
+   */
+  async onTogglePrintSimulation() {
+    const enabled = await this.emulationFront.getIsPrintSimulationEnabled();
+
+    if (!enabled) {
+      await this.emulationFront.startPrintMediaSimulation();
+    } else {
+      await this.emulationFront.stopPrintMediaSimulation(false);
+    }
+
+    await this.updateElementStyle();
+  }
+
+  /**
    * Handler for toggling a pseudo class in the pseudo class panel. Toggles on and off
    * a given pseudo class value.
    *
    * @param  {String} value
    *         The pseudo class to toggle on or off.
    */
   onTogglePseudoClass(value) {
     this.store.dispatch(togglePseudoClass(value));
@@ -606,34 +650,44 @@ class RulesView {
       this.store.dispatch(updateClasses([]));
       this.store.dispatch(updateRules([]));
       return;
     }
 
     this.elementStyle = new ElementStyle(element, this, {}, this.pageStyle,
       this.showUserAgentStyles);
     this.elementStyle.onChanged = this.updateRules;
+
+    await this.updateElementStyle();
+  }
+
+  /**
+   * Updates the class list panel with the current list of CSS classes.
+   */
+  updateClassList() {
+    this.store.dispatch(updateClasses(this.classList.currentClasses));
+  }
+
+  /**
+   * Updates the list of rules for the selected element. This should be called after
+   * ElementStyle is initialized or if the list of rules for the selected element needs
+   * to be refresh (e.g. when print media simulation is toggled).
+   */
+  async updateElementStyle() {
     await this.elementStyle.populate();
 
     const isAddRuleEnabled = this.selection.isElementNode() &&
                              !this.selection.isAnonymousNode();
     this.store.dispatch(updateAddRuleEnabled(isAddRuleEnabled));
     this.store.dispatch(setPseudoClassLocks(this.elementStyle.element.pseudoClassLocks));
     this.updateClassList();
     this.updateRules();
   }
 
   /**
-   * Updates the class list panel with the current list of CSS classes.
-   */
-  updateClassList() {
-    this.store.dispatch(updateClasses(this.classList.currentClasses));
-  }
-
-  /**
    * Updates the rules view by dispatching the current rules state. This is called from
    * the update() function, and from the ElementStyle's onChange() handler.
    */
   updateRules() {
     this.store.dispatch(updateRules(this.elementStyle.rules));
   }
 }
 
--- a/devtools/client/inspector/rules/reducers/rules.js
+++ b/devtools/client/inspector/rules/reducers/rules.js
@@ -4,26 +4,29 @@
 
 "use strict";
 
 const Services = require("Services");
 
 const {
   UPDATE_ADD_RULE_ENABLED,
   UPDATE_HIGHLIGHTED_SELECTOR,
+  UPDATE_PRINT_SIMULATION_HIDDEN,
   UPDATE_RULES,
   UPDATE_SOURCE_LINK_ENABLED,
   UPDATE_SOURCE_LINK,
 } = require("../actions/index");
 
 const INITIAL_RULES = {
   // The selector of the node that is highlighted by the selector highlighter.
   highlightedSelector: "",
   // Whether or not the add new rule button should be enabled.
   isAddRuleEnabled: false,
+  // Whether or not the print simulation button is hidden.
+  isPrintSimulationHidden: false,
   // Whether or not the source links are enabled. This is determined by
   // whether or not the style editor is registered.
   isSourceLinkEnabled: Services.prefs.getBoolPref("devtools.styleeditor.enabled"),
   // Array of CSS rules.
   rules: [],
 };
 
 /**
@@ -113,20 +116,28 @@ const reducers = {
 
   [UPDATE_HIGHLIGHTED_SELECTOR](rules, { highlightedSelector }) {
     return {
       ...rules,
       highlightedSelector,
     };
   },
 
+  [UPDATE_PRINT_SIMULATION_HIDDEN](rules, { hidden }) {
+    return {
+      ...rules,
+      isPrintSimulationHidden: hidden,
+    };
+  },
+
   [UPDATE_RULES](rules, { rules: newRules }) {
     return {
       highlightedSelector: rules.highlightedSelector,
       isAddRuleEnabled: rules.isAddRuleEnabled,
+      isPrintSimulationHidden: rules.isPrintSimulationHidden,
       isSourceLinkEnabled: rules.isSourceLinkEnabled,
       rules: newRules.map(rule => getRuleState(rule)),
     };
   },
 
   [UPDATE_SOURCE_LINK_ENABLED](rules, { enabled }) {
     return {
       ...rules,
--- a/devtools/client/themes/images/rules-view-print-simulation.svg
+++ b/devtools/client/themes/images/rules-view-print-simulation.svg
@@ -1,6 +1,7 @@
 <!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
-  <path fill="context-fill" d="M12 0H4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3zm1 13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1z"/><path fill="context-fill" d="M10.5 5h-5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1zm0 2h-5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1zm0 2h-5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1zm-3 2h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1z"/>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill">
+  <path d="M2 4a3 3 0 0 1 3-3h4.17a3 3 0 0 1 2.12.88l1.83 1.83A3 3 0 0 1 14 5.83V12a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V4zm3-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V5.83a1 1 0 0 0-.3-.7L9.89 3.28a1 1 0 0 0-.7-.29H5z"/>
+  <path d="M6 10.5c0-.28.22-.5.5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zM6 8.5c0-.28.22-.5.5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zM6 6.5c0-.28.22-.5.5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5zM9 4.5V3h1v1.5c0 .28.22.5.5.5H12v1h-1.5A1.5 1.5 0 0 1 9 4.5z"/>
 </svg>
--- a/devtools/client/themes/inspector.css
+++ b/devtools/client/themes/inspector.css
@@ -103,29 +103,32 @@ window {
 
 #inspector-searchlabel {
   padding: 0 3px;
 }
 
 /* Add element toolbar button */
 #inspector-element-add-button::before {
   background-image: url("chrome://devtools/skin/images/add.svg");
-  list-style-image: url("chrome://devtools/skin/images/add.svg");
+  background-position: center;
+  background-size: 14px;
   -moz-user-focus: normal;
 }
 
 /* Eyedropper toolbar button */
 
 #inspector-eyedropper-toggle {
   /* Required to display tooltip when eyedropper is disabled in non-HTML documents */
   pointer-events: auto;
 }
 
 #inspector-eyedropper-toggle::before {
   background-image: url(images/command-eyedropper.svg);
+  background-position: center;
+  background-size: 14px;
 }
 
 #inspector-breadcrumbs-toolbar {
   padding: 0px;
   border-bottom-width: 0px;
   border-top-width: 1px;
   border-top-color: var(--theme-splitter-color);
   /* Bug 1262668 - Use the same background as the body so the breadcrumbs toolbar doesn't
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -642,28 +642,40 @@
 
 .ruleview-selectorhighlighter:active,
 .ruleview-selectorhighlighter.highlighted {
   fill: var(--theme-icon-checked-color);
 }
 
 #ruleview-add-rule-button::before {
   background-image: url("chrome://devtools/skin/images/add.svg");
+  background-position: center;
+  background-size: 14px;
 }
 
 #pseudo-class-panel-toggle::before {
-  background-image: url("chrome://devtools/skin/images/pseudo-class.svg");
+  content: ":hov";
 }
 
 #class-panel-toggle::before {
   content: ".cls";
 }
 
+#pseudo-class-panel-toggle::before,
+#class-panel-toggle::before {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: 600;
+}
+
 #print-simulation-toggle::before {
   background-image: url("chrome://devtools/skin/images/rules-view-print-simulation.svg");
+  background-position: center;
+  background-size: 14px;
 }
 
 .flash-out {
   animation: flash-out 1s ease-out;
 }
 
 @keyframes flash-out {
   from {
--- a/devtools/shared/locales/en-US/styleinspector.properties
+++ b/devtools/shared/locales/en-US/styleinspector.properties
@@ -133,16 +133,21 @@ rule.classPanel.toggleClass.tooltip=Togg
 # LOCALIZATION NOTE (rule.classPanel.newClass.placeholder): This is the placeholder
 # shown inside the text field used to add a new class in the rule-view.
 rule.classPanel.newClass.placeholder=Add new class
 
 # LOCALIZATION NOTE (rule.classPanel.noClasses): This is the text displayed in the
 # class panel when the current element has no classes applied.
 rule.classPanel.noClasses=No classes on this element
 
+# LOCALIZATION NOTE (rule.printSimulation.tooltip):
+# This is the tooltip of the print simulation button in the Rule View toolbar
+# that toggles print simulation.
+rule.printSimulation.tooltip=Toggle print media simulation for the page
+
 # LOCALIZATION NOTE (styleinspector.contextmenu.copyColor): Text displayed in the rule
 # and computed view context menu when a color value was clicked.
 styleinspector.contextmenu.copyColor=Copy Color
 
 # LOCALIZATION NOTE (styleinspector.contextmenu.copyColor.accessKey): Access key for
 # the rule and computed view context menu "Copy Color" entry.
 styleinspector.contextmenu.copyColor.accessKey=L
 
--- a/image/LookupResult.h
+++ b/image/LookupResult.h
@@ -57,16 +57,23 @@ class MOZ_STACK_CLASS LookupResult {
     MOZ_ASSERT(!mSurface || !(mMatchType == MatchType::NOT_FOUND ||
                               mMatchType == MatchType::PENDING),
                "Only NOT_FOUND or PENDING make sense with no surface");
     MOZ_ASSERT(mSurface || mMatchType == MatchType::NOT_FOUND ||
                    mMatchType == MatchType::PENDING,
                "NOT_FOUND or PENDING do not make sense with a surface");
   }
 
+  LookupResult(MatchType aMatchType, const gfx::IntSize& aSuggestedSize)
+      : mMatchType(aMatchType), mSuggestedSize(aSuggestedSize) {
+    MOZ_ASSERT(
+        mMatchType == MatchType::NOT_FOUND || mMatchType == MatchType::PENDING,
+        "Only NOT_FOUND or PENDING make sense with no surface");
+  }
+
   LookupResult(DrawableSurface&& aSurface, MatchType aMatchType,
                const gfx::IntSize& aSuggestedSize)
       : mSurface(std::move(aSurface)),
         mMatchType(aMatchType),
         mSuggestedSize(aSuggestedSize) {
     MOZ_ASSERT(!mSurface || !(mMatchType == MatchType::NOT_FOUND ||
                               mMatchType == MatchType::PENDING),
                "Only NOT_FOUND or PENDING make sense with no surface");
--- a/image/SurfaceCache.cpp
+++ b/image/SurfaceCache.cpp
@@ -506,36 +506,18 @@ class ImageSurfaceCache {
     // if we discarded surfaces due to the volatile buffers getting released,
     // it is possible.
     AfterMaybeRemove();
   }
 
   IntSize SuggestedSize(const IntSize& aSize) const {
     IntSize suggestedSize = SuggestedSizeInternal(aSize);
     if (mIsVectorImage) {
-      // Whether or not we are in factor of 2 mode, vector image rasterization
-      // is clamped at a configured maximum if the caller is willing to accept
-      // substitutes.
-      MOZ_ASSERT(SurfaceCache::IsLegalSize(suggestedSize));
-
-      // If we exceed the maximum, we need to scale the size downwards to fit.
-      // It shouldn't get here if it is significantly larger because
-      // VectorImage::UseSurfaceCacheForSize should prevent us from requesting
-      // a rasterized version of a surface greater than 4x the maximum.
-      int32_t maxSizeKB = gfxPrefs::ImageCacheMaxRasterizedSVGThresholdKB();
-      int32_t proposedKB = suggestedSize.width * suggestedSize.height / 256;
-      if (maxSizeKB >= proposedKB) {
-        return suggestedSize;
-      }
-
-      double scale = sqrt(double(maxSizeKB) / proposedKB);
-      suggestedSize.width = int32_t(scale * suggestedSize.width);
-      suggestedSize.height = int32_t(scale * suggestedSize.height);
+      suggestedSize = SurfaceCache::ClampVectorSize(suggestedSize);
     }
-
     return suggestedSize;
   }
 
   IntSize SuggestedSizeInternal(const IntSize& aSize) const {
     // When not in factor of 2 mode, we can always decode at the given size.
     if (!mFactor2Mode) {
       return aSize;
     }
@@ -959,17 +941,19 @@ class SurfaceCacheImpl final : public ns
 
   LookupResult LookupBestMatch(const ImageKey aImageKey,
                                const SurfaceKey& aSurfaceKey,
                                const StaticMutexAutoLock& aAutoLock,
                                bool aMarkUsed) {
     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
     if (!cache) {
       // No cached surfaces for this image.
-      return LookupResult(MatchType::NOT_FOUND);
+      return LookupResult(
+          MatchType::NOT_FOUND,
+          SurfaceCache::ClampSize(aImageKey, aSurfaceKey.Size()));
     }
 
     // Repeatedly look up the best match, trying again if the resulting surface
     // has been freed by the operating system, until we can either lock a
     // surface for drawing or there are no matching surfaces left.
     // XXX(seth): This is O(N^2), but N is expected to be very small. If we
     // encounter a performance problem here we can revisit this.
 
@@ -978,17 +962,17 @@ class SurfaceCacheImpl final : public ns
     MatchType matchType = MatchType::NOT_FOUND;
     IntSize suggestedSize;
     while (true) {
       Tie(surface, matchType, suggestedSize) =
           cache->LookupBestMatch(aSurfaceKey);
 
       if (!surface) {
         return LookupResult(
-            matchType);  // Lookup in the per-image cache missed.
+            matchType, suggestedSize);  // Lookup in the per-image cache missed.
       }
 
       drawableSurface = surface->GetDrawableSurface();
       if (drawableSurface) {
         break;
       }
 
       // The surface was released by the operating system. Remove the cache
@@ -1642,10 +1626,37 @@ bool SurfaceCache::IsLegalSize(const Int
       CheckedInt32(aSize.width) * CheckedInt32(aSize.height) * 4;
   if (MOZ_UNLIKELY(!requiredBytes.isValid())) {
     NS_WARNING("width or height too large");
     return false;
   }
   return true;
 }
 
+IntSize SurfaceCache::ClampVectorSize(const IntSize& aSize) {
+  // If we exceed the maximum, we need to scale the size downwards to fit.
+  // It shouldn't get here if it is significantly larger because
+  // VectorImage::UseSurfaceCacheForSize should prevent us from requesting
+  // a rasterized version of a surface greater than 4x the maximum.
+  int32_t maxSizeKB = gfxPrefs::ImageCacheMaxRasterizedSVGThresholdKB();
+  if (maxSizeKB <= 0) {
+    return aSize;
+  }
+
+  int32_t proposedKB = int32_t(int64_t(aSize.width) * aSize.height / 256);
+  if (maxSizeKB >= proposedKB) {
+    return aSize;
+  }
+
+  double scale = sqrt(double(maxSizeKB) / proposedKB);
+  return IntSize(int32_t(scale * aSize.width), int32_t(scale * aSize.height));
+}
+
+IntSize SurfaceCache::ClampSize(ImageKey aImageKey, const IntSize& aSize) {
+  if (aImageKey->GetType() != imgIContainer::TYPE_VECTOR) {
+    return aSize;
+  }
+
+  return ClampVectorSize(aSize);
+}
+
 }  // namespace image
 }  // namespace mozilla
--- a/image/SurfaceCache.h
+++ b/image/SurfaceCache.h
@@ -429,16 +429,26 @@ struct SurfaceCache {
    */
   static size_t MaximumCapacity();
 
   /**
    * @return true if the given size is valid.
    */
   static bool IsLegalSize(const IntSize& aSize);
 
+  /**
+   * @return clamped size for the given vector image size to rasterize at.
+   */
+  static IntSize ClampVectorSize(const IntSize& aSize);
+
+  /**
+   * @return clamped size for the given image and size to rasterize at.
+   */
+  static IntSize ClampSize(const ImageKey aImageKey, const IntSize& aSize);
+
  private:
   virtual ~SurfaceCache() = 0;  // Forbid instantiation.
 };
 
 }  // namespace image
 }  // namespace mozilla
 
 #endif  // mozilla_image_SurfaceCache_h
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -12,16 +12,17 @@
 #include "gfxUtils.h"
 #include "imgFrame.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/SVGSVGElement.h"
 #include "mozilla/dom/SVGDocument.h"
 #include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/gfxVars.h"
 #include "mozilla/PendingAnimationTracker.h"
 #include "mozilla/PresShell.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Tuple.h"
 #include "nsIStreamListener.h"
 #include "nsMimeTypes.h"
 #include "nsPresContext.h"
 #include "nsRect.h"
@@ -750,20 +751,16 @@ VectorImage::GetFrameInternal(const IntS
   if (mError) {
     return MakeTuple(ImgDrawResult::BAD_IMAGE, aSize, RefPtr<SourceSurface>());
   }
 
   if (!mIsFullyLoaded) {
     return MakeTuple(ImgDrawResult::NOT_READY, aSize, RefPtr<SourceSurface>());
   }
 
-  // We don't allow large surfaces to be rasterized on the Draw and
-  // GetImageContainerAtSize paths, because those have alternatives. If we get
-  // here however, then we know it came from GetFrame(AtSize) and that path does
-  // not have any fallback method, so we don't check UseSurfaceCacheForSize.
   RefPtr<SourceSurface> sourceSurface;
   IntSize decodeSize;
   Tie(sourceSurface, decodeSize) =
       LookupCachedSurface(aSize, aSVGContext, aFlags);
   if (sourceSurface) {
     return MakeTuple(ImgDrawResult::SUCCESS, decodeSize,
                      std::move(sourceSurface));
   }
@@ -848,31 +845,26 @@ VectorImage::GetImageContainer(LayerMana
 
 //******************************************************************************
 NS_IMETHODIMP_(bool)
 VectorImage::IsImageContainerAvailableAtSize(LayerManager* aManager,
                                              const IntSize& aSize,
                                              uint32_t aFlags) {
   // Since we only support image containers with WebRender, and it can handle
   // textures larger than the hw max texture size, we don't need to check aSize.
-  return !aSize.IsEmpty() && UseSurfaceCacheForSize(aSize) &&
-         IsImageContainerAvailable(aManager, aFlags);
+  return !aSize.IsEmpty() && IsImageContainerAvailable(aManager, aFlags);
 }
 
 //******************************************************************************
 NS_IMETHODIMP_(ImgDrawResult)
 VectorImage::GetImageContainerAtSize(layers::LayerManager* aManager,
                                      const gfx::IntSize& aSize,
                                      const Maybe<SVGImageContext>& aSVGContext,
                                      uint32_t aFlags,
                                      layers::ImageContainer** aOutContainer) {
-  if (!UseSurfaceCacheForSize(aSize)) {
-    return ImgDrawResult::NOT_SUPPORTED;
-  }
-
   Maybe<SVGImageContext> newSVGContext;
   MaybeRestrictSVGContext(newSVGContext, aSVGContext, aFlags);
 
   // The aspect ratio flag was already handled as part of the SVG context
   // restriction above.
   uint32_t flags = aFlags & ~(FLAG_FORCE_PRESERVEASPECTRATIO_NONE);
   return GetImageContainerImpl(aManager, aSize,
                                newSVGContext ? newSVGContext : aSVGContext,
@@ -945,19 +937,23 @@ VectorImage::Draw(gfxContext* aContext, 
   if (mAnimationConsumers == 0) {
     SendOnUnlockedDraw(aFlags);
   }
 
   // We should bypass the cache when:
   // - We are using a DrawTargetRecording because we prefer the drawing commands
   //   in general to the rasterized surface. This allows blob images to avoid
   //   rasterized SVGs with WebRender.
-  // - The size exceeds what we are will to cache as a rasterized surface.
+  // - The size exceeds what we are willing to cache as a rasterized surface.
+  //   We don't do this for WebRender because the performance of the fallback
+  //   path is quite bad and upscaling the SVG from the clamped size is better
+  //   than bringing the browser to a crawl.
   if (aContext->GetDrawTarget()->GetBackendType() == BackendType::RECORDING ||
-      !UseSurfaceCacheForSize(aSize)) {
+      (!gfxVars::UseWebRender() &&
+       aSize != SurfaceCache::ClampVectorSize(aSize))) {
     aFlags |= FLAG_BYPASS_SURFACE_CACHE;
   }
 
   MOZ_ASSERT(!(aFlags & FLAG_FORCE_PRESERVEASPECTRATIO_NONE) ||
                  (aSVGContext && aSVGContext->GetViewportSize()),
              "Viewport size is required when using "
              "FLAG_FORCE_PRESERVEASPECTRATIO_NONE");
 
@@ -1015,34 +1011,16 @@ already_AddRefed<gfxDrawable> VectorImag
     const SVGDrawingParameters& aParams) {
   RefPtr<gfxDrawingCallback> cb = new SVGDrawingCallback(
       mSVGDocumentWrapper, aParams.viewportSize, aParams.size, aParams.flags);
 
   RefPtr<gfxDrawable> svgDrawable = new gfxCallbackDrawable(cb, aParams.size);
   return svgDrawable.forget();
 }
 
-bool VectorImage::UseSurfaceCacheForSize(const IntSize& aSize) const {
-  int32_t maxSizeKB = gfxPrefs::ImageCacheMaxRasterizedSVGThresholdKB();
-  if (maxSizeKB <= 0) {
-    return true;
-  }
-
-  if (!SurfaceCache::IsLegalSize(aSize)) {
-    return false;
-  }
-
-  // With factor of 2 mode, we should be willing to use a surface which is up
-  // to twice the width, and twice the height, of the maximum sized surface
-  // before switching to drawing to the target directly. That means the size in
-  // KB works out to be:
-  //   width * height * 4 [bytes/pixel] * / 1024 [bytes/KB] <= 2 * 2 * maxSizeKB
-  return aSize.width * aSize.height / 1024 <= maxSizeKB;
-}
-
 Tuple<RefPtr<SourceSurface>, IntSize> VectorImage::LookupCachedSurface(
     const IntSize& aSize, const Maybe<SVGImageContext>& aSVGContext,
     uint32_t aFlags) {
   // If we're not allowed to use a cached surface, don't attempt a lookup.
   if (aFlags & FLAG_BYPASS_SURFACE_CACHE) {
     return MakeTuple(RefPtr<SourceSurface>(), aSize);
   }
 
--- a/image/VectorImage.h
+++ b/image/VectorImage.h
@@ -98,19 +98,16 @@ class VectorImage final : public ImageRe
   bool MaybeRestrictSVGContext(Maybe<SVGImageContext>& aNewSVGContext,
                                const Maybe<SVGImageContext>& aSVGContext,
                                uint32_t aFlags);
 
   /// Create a gfxDrawable which callbacks into the SVG document.
   already_AddRefed<gfxDrawable> CreateSVGDrawable(
       const SVGDrawingParameters& aParams);
 
-  /// Returns true if we use the surface cache to store rasterized copies.
-  bool UseSurfaceCacheForSize(const IntSize& aSize) const;
-
   /// Rasterize the SVG into a surface. aWillCache will be set to whether or
   /// not the new surface was put into the cache.
   already_AddRefed<SourceSurface> CreateSurface(
       const SVGDrawingParameters& aParams, gfxDrawable* aSVGDrawable,
       bool& aWillCache);
 
   /// Send a frame complete notification if appropriate. Must be called only
   /// after all drawing has been completed.
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -235,8 +235,11 @@ XPC_MSG_DEF(NS_ERROR_DOM_NOT_ALLOWED_ERR
 XPC_MSG_DEF(NS_ERROR_MALWARE_URI                      , "The URI is malware")
 XPC_MSG_DEF(NS_ERROR_PHISHING_URI                     , "The URI is phishing")
 XPC_MSG_DEF(NS_ERROR_TRACKING_URI                     , "The URI is tracking")
 XPC_MSG_DEF(NS_ERROR_UNWANTED_URI                     , "The URI is unwanted")
 XPC_MSG_DEF(NS_ERROR_BLOCKED_URI                      , "The URI is blocked")
 XPC_MSG_DEF(NS_ERROR_HARMFUL_URI                      , "The URI is harmful")
 XPC_MSG_DEF(NS_ERROR_FINGERPRINTING_URI               , "The URI is fingerprinting")
 XPC_MSG_DEF(NS_ERROR_CRYPTOMINING_URI                 , "The URI is cryptomining")
+
+/* Profile manager error codes */
+XPC_MSG_DEF(NS_ERROR_DATABASE_CHANGED                 , "Flushing the profiles to disk would have overwritten changes made elsewhere.")
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4815,19 +4815,19 @@ pref("image.animated.resume-from-last-di
 
 // Maximum number of surfaces for an image before entering "factor of 2" mode.
 // This in addition to the number of "native" sizes of an image. A native size
 // is a size for which we can decode a frame without up or downscaling. Most
 // images only have 1, but some (i.e. ICOs) may have multiple frames for the
 // same data at different sizes.
 pref("image.cache.factor2.threshold-surfaces", 4);
 
-// Maximum number of pixels in either dimension that we are willing to upscale
-// an SVG to when we are in "factor of 2" mode.
-pref("image.cache.max-rasterized-svg-threshold-kb", 92160);
+// Maximum size of a surface in KB we are willing to produce when rasterizing
+// an SVG.
+pref("image.cache.max-rasterized-svg-threshold-kb", 204800);
 
 // The maximum size, in bytes, of the decoded images we cache
 pref("image.cache.size", 5242880);
 
 // A weight, from 0-1000, to place on time when comparing to size.
 // Size is given a weight of 1000 - timeweight.
 pref("image.cache.timeweight", 500);
 
--- a/toolkit/content/aboutProfiles.js
+++ b/toolkit/content/aboutProfiles.js
@@ -9,16 +9,40 @@ const {XPCOMUtils} = ChromeUtils.import(
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "ProfileService",
   "@mozilla.org/toolkit/profile-service;1",
   "nsIToolkitProfileService"
 );
 
+async function flush() {
+  try {
+    ProfileService.flush();
+    refreshUI();
+  } catch (e) {
+    let [title, msg, button] = await document.l10n.formatValues([
+      { id: "profiles-flush-fail-title" },
+      { id: e.result == Cr.NS_ERROR_DATABASE_CHANGED ?
+                        "profiles-flush-conflict" :
+                        "profiles-flush-failed" },
+      { id: "profiles-flush-restart-button" },
+    ]);
+
+    const PS = Ci.nsIPromptService;
+    let result = Services.prompt.confirmEx(window, title, msg,
+                                          (PS.BUTTON_POS_0 * PS.BUTTON_TITLE_CANCEL) +
+                                          (PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING),
+                                          null, button, null, null, {});
+    if (result == 1) {
+      restart(false);
+    }
+  }
+}
+
 function refreshUI() {
   let parent = document.getElementById("profiles");
   while (parent.firstChild) {
     parent.firstChild.remove();
   }
 
   let defaultProfile;
   try {
@@ -205,18 +229,17 @@ async function renameProfile(profile) {
         { id: "profiles-invalid-profile-name-title" },
         { id: "profiles-invalid-profile-name", args: { name: newName } },
       ]);
 
       Services.prompt.alert(window, title, msg);
       return;
     }
 
-    ProfileService.flush();
-    refreshUI();
+    flush();
   }
 }
 
 async function removeProfile(profile) {
   let deleteFiles = false;
 
   if (profile.rootDir.exists()) {
     let [title, msg, dontDeleteStr, deleteStr] = await document.l10n.formatValues([
@@ -275,34 +298,32 @@ async function removeProfile(profile) {
         { id: "profiles-delete-profile-failed-title" },
         { id: "profiles-delete-profile-failed-message" },
     ]);
 
     Services.prompt.alert(window, title, msg);
     return;
   }
 
-  ProfileService.flush();
-  refreshUI();
+  flush();
 }
 
 async function defaultProfile(profile) {
   try {
     ProfileService.defaultProfile = profile;
-    ProfileService.flush();
+    flush();
   } catch (e) {
     // This can happen on dev-edition.
     let [title, msg] = await document.l10n.formatValues([
         { id: "profiles-cannot-set-as-default-title" },
         { id: "profiles-cannot-set-as-default-message" },
     ]);
 
     Services.prompt.alert(window, title, msg);
   }
-  refreshUI();
 }
 
 function openProfile(profile) {
   let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
                      .createInstance(Ci.nsISupportsPRBool);
   Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
 
   if (cancelQuit.data) {
@@ -326,10 +347,15 @@ function restart(safeMode) {
   if (safeMode) {
     Services.startup.restartInSafeMode(flags);
   } else {
     Services.startup.quit(flags);
   }
 }
 
 window.addEventListener("DOMContentLoaded", function() {
-  refreshUI();
+  if (ProfileService.isListOutdated) {
+    document.getElementById("owned").hidden = true;
+  } else {
+    document.getElementById("conflict").hidden = true;
+    refreshUI();
+  }
 }, {once: true});
--- a/toolkit/content/aboutProfiles.xhtml
+++ b/toolkit/content/aboutProfiles.xhtml
@@ -18,17 +18,23 @@
   <body id="body" class="wide-container">
     <div id="action-box" class="notice-box">
       <h3 data-l10n-id="profiles-restart-title"></h3>
       <button id="restart-in-safe-mode-button" data-l10n-id="profiles-restart-in-safe-mode"></button>
       <button id="restart-button" data-l10n-id="profiles-restart-normal"></button>
     </div>
 
     <h1 data-l10n-id="profiles-title"></h1>
-    <div data-l10n-id="profiles-subtitle"></div>
+
+    <div id="conflict">
+      <p data-l10n-id="profiles-conflict" />
+    </div>
+    <div id="owned">
+      <div data-l10n-id="profiles-subtitle"></div>
 
-    <div>
-      <button id="create-button" data-l10n-id="profiles-create"></button>
+      <div>
+        <button id="create-button" data-l10n-id="profiles-create"></button>
+      </div>
+
+      <div id="profiles" class="tab"></div>
     </div>
-
-    <div id="profiles" class="tab"></div>
   </body>
 </html>
--- a/toolkit/locales/en-US/chrome/mozapps/profile/profileSelection.properties
+++ b/toolkit/locales/en-US/chrome/mozapps/profile/profileSelection.properties
@@ -46,8 +46,16 @@ profileFinishTextMac=Click Done to creat
 profileMissing=Your %S profile cannot be loaded. It may be missing or inaccessible.
 profileMissingTitle=Profile Missing
 profileDeletionFailed=Profile couldn’t be deleted as it may be in use.
 profileDeletionFailedTitle=Deletion Failed
 
 # Profile reset
 # LOCALIZATION NOTE (resetBackupDirectory): Directory name for the profile directory backup created during reset. This directory is placed in a location users will see it (ie. their desktop). %S is the application name.
 resetBackupDirectory=Old %S Data
+
+flushFailTitle=Changes not saved
+# LOCALIZATION NOTE (conflictMessage): %1$S is brandProductName, %2$S is brandShortName.
+conflictMessage=Another copy of %1$S has made changes to profiles. You must restart %2$S before making more changes.
+flushFailMessage=An unexpected error has prevented your changes from being saved.
+# LOCALIZATION NOTE (flushFailRestartButton): $S is brandShortName.
+flushFailRestartButton=Restart %S
+flushFailExitButton=Exit
--- a/toolkit/locales/en-US/toolkit/about/aboutProfiles.ftl
+++ b/toolkit/locales/en-US/toolkit/about/aboutProfiles.ftl
@@ -4,16 +4,21 @@
 
 
 profiles-title = About Profiles
 profiles-subtitle = This page helps you to manage your profiles. Each profile is a separate world which contains separate history, bookmarks, settings and add-ons.
 profiles-create = Create a New Profile
 profiles-restart-title = Restart
 profiles-restart-in-safe-mode = Restart with Add-ons Disabled…
 profiles-restart-normal = Restart normally…
+profiles-conflict = Another copy of { -brand-product-name } has made changes to profiles. You must restart { -brand-short-name } before making more changes.
+profiles-flush-fail-title = Changes not saved
+profiles-flush-conflict = { profiles-conflict }
+profiles-flush-failed = An unexpected error has prevented your changes from being saved.
+profiles-flush-restart-button = Restart { -brand-short-name }
 
 # Variables:
 #   $name (String) - Name of the profile
 profiles-name = Profile: { $name }
 profiles-is-default = Default Profile
 profiles-rootdir = Root Directory
 
 # localDir is used to show the directory corresponding to
--- a/toolkit/profile/content/profileSelection.js
+++ b/toolkit/profile/content/profileSelection.js
@@ -11,16 +11,17 @@ const C = Cc;
 const I = Ci;
 
 const ToolkitProfileService = "@mozilla.org/toolkit/profile-service;1";
 
 var gDialogParams;
 var gProfileManagerBundle;
 var gBrandBundle;
 var gProfileService;
+var gNeedsFlush = false;
 
 function startup() {
   try {
     gDialogParams = window.arguments[0].
       QueryInterface(I.nsIDialogParamBlock);
 
     gProfileService = C[ToolkitProfileService].getService(I.nsIToolkitProfileService);
 
@@ -54,16 +55,58 @@ function startup() {
   } catch (e) {
     window.close();
     throw (e);
   }
   document.addEventListener("dialogaccept", acceptDialog);
   document.addEventListener("dialogcancel", exitDialog);
 }
 
+function flush(cancelled) {
+  updateStartupPrefs();
+
+  gDialogParams.SetInt(1, document.getElementById("offlineState").checked ? 1 : 0);
+
+  if (gNeedsFlush) {
+    try {
+      gProfileService.flush();
+    } catch (e) {
+      let productName = gBrandBundle.getString("brandProductName");
+      let appName = gBrandBundle.getString("brandShortName");
+
+      let title = gProfileManagerBundle.getString("flushFailTitle");
+      let restartButton = gProfileManagerBundle.getFormattedString("flushFailRestartButton",
+                                                                  [appName]);
+      let exitButton = gProfileManagerBundle.getString("flushFailExitButton");
+
+      let message;
+      if (e.result == undefined) {
+        message = gProfileManagerBundle.getFormattedString("conflictMessage",
+                                                          [productName, appName]);
+      } else {
+        message = gProfileManagerBundle.getString("flushFailMessage");
+      }
+
+      const PS = Ci.nsIPromptService;
+      let result = Services.prompt.confirmEx(window, title, message,
+                                            (PS.BUTTON_POS_0 * PS.BUTTON_TITLE_IS_STRING) +
+                                            (PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING),
+                                            restartButton, exitButton, null, null, {});
+
+      gDialogParams.SetInt(0, result == 0 ? Ci.nsIToolkitProfileService.restart
+                                          : Ci.nsIToolkitProfileService.exit);
+      return;
+    }
+    gNeedsFlush = false;
+  }
+
+  gDialogParams.SetInt(0, cancelled ? Ci.nsIToolkitProfileService.exit
+                                    : Ci.nsIToolkitProfileService.launchWithProfile);
+}
+
 function acceptDialog(event) {
   var appName = gBrandBundle.getString("brandShortName");
 
   var profilesElement = document.getElementById("profiles");
   var selectedProfile = profilesElement.selectedItem;
   if (!selectedProfile) {
     var pleaseSelectTitle = gProfileManagerBundle.getString("pleaseSelectTitle");
     var pleaseSelect =
@@ -71,36 +114,38 @@ function acceptDialog(event) {
     Services.prompt.alert(window, pleaseSelectTitle, pleaseSelect);
     event.preventDefault();
     return;
   }
 
   gDialogParams.objects.insertElementAt(selectedProfile.profile.rootDir, 0);
   gDialogParams.objects.insertElementAt(selectedProfile.profile.localDir, 1);
 
-  try {
-    gProfileService.defaultProfile = selectedProfile.profile;
-  } catch (e) {
-    // This can happen on dev-edition. We'll still restart with the selected
-    // profile based on the lock's directories.
+  if (gProfileService.defaultProfile != selectedProfile.profile) {
+    try {
+      gProfileService.defaultProfile = selectedProfile.profile;
+      gNeedsFlush = true;
+    } catch (e) {
+      // This can happen on dev-edition. We'll still restart with the selected
+      // profile based on the lock's directories.
+    }
   }
-  updateStartupPrefs();
-
-  gDialogParams.SetInt(0, 1);
-  /* Bug 257777 */
-  gDialogParams.SetInt(1, document.getElementById("offlineState").checked ? 1 : 0);
+  flush(false);
 }
 
 function exitDialog() {
-  updateStartupPrefs();
+  flush(true);
 }
 
 function updateStartupPrefs() {
   var autoSelectLastProfile = document.getElementById("autoSelectLastProfile");
-  gProfileService.startWithLastProfile = autoSelectLastProfile.checked;
+  if (gProfileService.startWithLastProfile != autoSelectLastProfile.checked) {
+    gProfileService.startWithLastProfile = autoSelectLastProfile.checked;
+    gNeedsFlush = true;
+  }
 }
 
 // handle key event on listboxes
 function onProfilesKey(aEvent) {
   switch ( aEvent.keyCode ) {
   case KeyEvent.DOM_VK_BACK_SPACE:
     if (AppConstants.platform != "macosx")
       break;
@@ -134,16 +179,18 @@ function CreateProfile(aProfile) {
 
   var tooltiptext =
     gProfileManagerBundle.getFormattedString("profileTooltip", [aProfile.name, aProfile.rootDir.path]);
   listitem.setAttribute("tooltiptext", tooltiptext);
   listitem.profile = aProfile;
 
   profilesElement.ensureElementIsVisible(listitem);
   profilesElement.selectItem(listitem);
+
+  gNeedsFlush = true;
 }
 
 // rename the selected profile
 function RenameProfile() {
   var profilesElement = document.getElementById("profiles");
   var selectedItem = profilesElement.selectedItem;
   if (!selectedItem) {
     return false;
@@ -162,16 +209,17 @@ function RenameProfile() {
     newName = newName.value;
 
     // User hasn't changed the profile name. Treat as if cancel was pressed.
     if (newName == oldName)
       return false;
 
     try {
       selectedProfile.name = newName;
+      gNeedsFlush = true;
     } catch (e) {
       var alTitle = gProfileManagerBundle.getString("profileNameInvalidTitle");
       var alMsg = gProfileManagerBundle.getFormattedString("profileNameInvalid", [newName]);
       Services.prompt.alert(window, alTitle, alMsg);
       return false;
     }
 
     selectedItem.firstChild.setAttribute("value", newName);
@@ -215,16 +263,17 @@ function ConfirmDelete() {
       return false;
 
     if (buttonPressed == 2)
       deleteFiles = true;
   }
 
   try {
     selectedProfile.remove(deleteFiles);
+    gNeedsFlush = true;
   } catch (e) {
     let title = gProfileManagerBundle.getString("profileDeletionFailedTitle");
     let msg = gProfileManagerBundle.getString("profileDeletionFailed");
     Services.prompt.alert(window, title, msg);
 
     return true;
   }
 
--- a/toolkit/profile/nsIToolkitProfileService.idl
+++ b/toolkit/profile/nsIToolkitProfileService.idl
@@ -9,32 +9,44 @@ interface nsISimpleEnumerator;
 interface nsIFile;
 interface nsIToolkitProfile;
 interface nsIProfileLock;
 
 [scriptable, builtinclass, uuid(1947899b-f369-48fa-89da-f7c37bb1e6bc)]
 interface nsIToolkitProfileService : nsISupports
 {
     /**
+     * Tests whether the profile lists on disk have changed since they were
+     * loaded. When this is true attempts to flush changes to disk will fail.
+     */
+    [infallible] readonly attribute boolean isListOutdated;
+
+    /**
      * When a downgrade is detected UI is presented to the user to ask how to
      * proceed. These flags are used to pass some information to the UI.
      */
     cenum downgradeUIFlags: 8 {
         hasSync = 1,
     };
 
     /**
      * When a downgrade is detected UI is presented to the user to ask how to
      * proceed. These are the possible options the user can choose.
      */
     cenum downgradeUIChoice: 8 {
         quit = 0,
         createNewProfile = 1,
     };
 
+    cenum profileManagerResult: 8 {
+        exit = 0,
+        launchWithProfile = 1,
+        restart = 2,
+    };
+
     attribute boolean startWithLastProfile;
 
     readonly attribute nsISimpleEnumerator /*nsIToolkitProfile*/ profiles;
 
     /**
      * The profile currently in use if it is a named profile. This will return
      * null if the current profile path doesn't match a profile in the database.
      */
@@ -123,16 +135,18 @@ interface nsIToolkitProfileService : nsI
 
     /**
      * Returns the number of profiles.
      * @return the number of profiles.
      */
     readonly attribute unsigned long profileCount;
 
     /**
-     * Flush the profiles list file.
+     * Flush the profiles list file. This will fail with
+     * NS_ERROR_DATABASE_CHANGED if the files on disk have changed since the
+     * profiles were loaded.
      */
     void flush();
 };
 
 %{C++
 #define NS_PROFILESERVICE_CONTRACTID "@mozilla.org/toolkit/profile-service;1"
 %}
--- a/toolkit/profile/nsToolkitProfileService.cpp
+++ b/toolkit/profile/nsToolkitProfileService.cpp
@@ -41,16 +41,17 @@
 #include "nsReadableUtils.h"
 #include "nsNativeCharsetUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Sprintf.h"
 #include "nsPrintfCString.h"
 #include "mozilla/UniquePtr.h"
 #include "nsIToolkitShellService.h"
 #include "mozilla/Telemetry.h"
+#include "nsProxyRelease.h"
 
 using namespace mozilla;
 
 #define DEV_EDITION_NAME "dev-edition-default"
 #define DEFAULT_NAME "default"
 #define COMPAT_FILE NS_LITERAL_STRING("compatibility.ini")
 #define PROFILE_DB_VERSION "2"
 #define INSTALL_PREFIX "Install"
@@ -77,16 +78,62 @@ static bool GetStrings(const char* aStri
  */
 nsTArray<UniquePtr<KeyValue>> GetSectionStrings(nsINIParser* aParser,
                                                 const char* aSection) {
   nsTArray<UniquePtr<KeyValue>> result;
   aParser->GetStrings(aSection, &GetStrings, &result);
   return result;
 }
 
+void RemoveProfileFiles(nsIToolkitProfile* aProfile, bool aInBackground) {
+  nsCOMPtr<nsIFile> rootDir;
+  aProfile->GetRootDir(getter_AddRefs(rootDir));
+  nsCOMPtr<nsIFile> localDir;
+  aProfile->GetLocalDir(getter_AddRefs(localDir));
+
+  // Just lock the directories, don't mark the profile as locked or the lock
+  // will attempt to release its reference to the profile on the background
+  // thread which will assert.
+  nsCOMPtr<nsIProfileLock> lock;
+  nsresult rv =
+      NS_LockProfilePath(rootDir, localDir, nullptr, getter_AddRefs(lock));
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+      "nsToolkitProfile::RemoveProfileFiles",
+      [rootDir, localDir, lock]() mutable {
+        bool equals;
+        nsresult rv = rootDir->Equals(localDir, &equals);
+        // The root dir might contain the temp dir, so remove
+        // the temp dir first.
+        if (NS_SUCCEEDED(rv) && !equals) {
+          localDir->Remove(true);
+        }
+
+        // Ideally we'd unlock after deleting but since the lock is a file
+        // in the profile we must unlock before removing.
+        lock->Unlock();
+        // nsIProfileLock is not threadsafe so release our reference to it on
+        // the main thread.
+        NS_ReleaseOnMainThreadSystemGroup(
+            "nsToolkitProfile::RemoveProfileFiles::Unlock", lock.forget());
+
+        rv = rootDir->Remove(true);
+        NS_ENSURE_SUCCESS_VOID(rv);
+      });
+
+  if (aInBackground) {
+    nsCOMPtr<nsIEventTarget> target =
+        do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+    target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+  } else {
+    runnable->Run();
+  }
+}
+
 nsToolkitProfile::nsToolkitProfile(const nsACString& aName, nsIFile* aRootDir,
                                    nsIFile* aLocalDir, bool aFromDB)
     : mName(aName),
       mRootDir(aRootDir),
       mLocalDir(aLocalDir),
       mLock(nullptr),
       mIndex(0),
       mSection("Profile") {
@@ -173,48 +220,17 @@ nsresult nsToolkitProfile::RemoveInterna
 
   if (mLock) return NS_ERROR_FILE_IS_LOCKED;
 
   if (!isInList()) {
     return NS_ERROR_NOT_INITIALIZED;
   }
 
   if (aRemoveFiles) {
-    // Check if another instance is using this profile.
-    nsCOMPtr<nsIProfileLock> lock;
-    nsresult rv = Lock(nullptr, getter_AddRefs(lock));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    nsCOMPtr<nsIFile> rootDir(mRootDir);
-    nsCOMPtr<nsIFile> localDir(mLocalDir);
-
-    nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
-        "nsToolkitProfile::RemoveInternal", [rootDir, localDir, lock]() {
-          bool equals;
-          nsresult rv = rootDir->Equals(localDir, &equals);
-          // The root dir might contain the temp dir, so remove
-          // the temp dir first.
-          if (NS_SUCCEEDED(rv) && !equals) {
-            localDir->Remove(true);
-          }
-
-          // Ideally we'd unlock after deleting but since the lock is a file
-          // in the profile we must unlock before removing.
-          lock->Unlock();
-
-          rootDir->Remove(true);
-        });
-
-    if (aInBackground) {
-      nsCOMPtr<nsIEventTarget> target =
-          do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
-      target->Dispatch(runnable, NS_DISPATCH_NORMAL);
-    } else {
-      runnable->Run();
-    }
+    RemoveProfileFiles(this, aInBackground);
   }
 
   nsINIParser* db = &nsToolkitProfileService::gService->mProfileDB;
   db->DeleteSection(mSection.get());
 
   // We make some assumptions that the profile's index in the database is based
   // on its position in the linked list. Removing a profile means we have to fix
   // the index of later profiles in the list. The easiest way to do that is just
@@ -368,17 +384,23 @@ nsToolkitProfileService::nsToolkitProfil
 #ifdef MOZ_DEDICATED_PROFILES
       mUseDedicatedProfile(!IsSnapEnvironment()),
 #else
       mUseDedicatedProfile(false),
 #endif
       mCreatedAlternateProfile(false),
       mStartupReason(NS_LITERAL_STRING("unknown")),
       mMaybeLockProfile(false),
-      mUpdateChannel(NS_STRINGIFY(MOZ_UPDATE_CHANNEL)) {
+      mUpdateChannel(NS_STRINGIFY(MOZ_UPDATE_CHANNEL)),
+      mProfileDBExists(false),
+      mProfileDBFileSize(0),
+      mProfileDBModifiedTime(0),
+      mInstallDBExists(false),
+      mInstallDBFileSize(0),
+      mInstallDBModifiedTime(0) {
 #ifdef MOZ_DEV_EDITION
   mUseDevEditionProfile = true;
 #endif
   gService = this;
 }
 
 nsToolkitProfileService::~nsToolkitProfileService() {
   gService = nullptr;
@@ -401,17 +423,23 @@ void nsToolkitProfileService::CompleteSt
     }
 
     bool isDefaultApp;
     nsresult rv = shell->IsDefaultApplication(&isDefaultApp);
     NS_ENSURE_SUCCESS_VOID(rv);
 
     if (isDefaultApp) {
       mProfileDB.SetString(mInstallSection.get(), "Locked", "1");
-      Flush();
+
+      // There is a very small chance that this could fail if something else
+      // overwrote the profiles database since we started up, probably less than
+      // a second ago. There isn't really a sane response here, all the other
+      // profile changes are already flushed so whether we fail to flush here or
+      // force quit the app makes no difference.
+      NS_ENSURE_SUCCESS_VOID(Flush());
     }
   }
 }
 
 // Tests whether the passed profile was last used by this install.
 bool nsToolkitProfileService::IsProfileForCurrentInstall(
     nsIToolkitProfile* aProfile) {
   nsCOMPtr<nsIFile> profileDir;
@@ -479,30 +507,32 @@ bool nsToolkitProfileService::IsProfileF
  *
  * We won't attempt to use the profile if it was last used by a different
  * install.
  *
  * If the profile is currently in use by an install that was either the OS
  * default install or the profile has been explicitely chosen by some other
  * means then we won't use it.
  *
- * Returns true if we chose to make the profile the new dedicated default.
+ * aResult will be set to true if we chose to make the profile the new dedicated
+ * default.
  */
-bool nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
-    nsIToolkitProfile* aProfile) {
+nsresult nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
+    nsIToolkitProfile* aProfile, bool* aResult) {
   nsresult rv;
+  *aResult = false;
 
   // If the profile was last used by a different install then we won't use it.
   if (!IsProfileForCurrentInstall(aProfile)) {
-    return false;
+    return NS_OK;
   }
 
   nsCString descriptor;
   rv = GetProfileDescriptor(aProfile, descriptor, nullptr);
-  NS_ENSURE_SUCCESS(rv, false);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   // Get a list of all the installs.
   nsTArray<nsCString> installs = GetKnownInstalls();
 
   // Cache the installs that use the profile.
   nsTArray<nsCString> inUseInstalls;
 
   // See if the profile is already in use by an install that hasn't locked it.
@@ -519,17 +549,17 @@ bool nsToolkitProfileService::MaybeMakeD
     if (!descriptor.Equals(path)) {
       continue;
     }
 
     // Is this profile locked to this other install?
     nsCString isLocked;
     rv = mProfileDB.GetString(install.get(), "Locked", isLocked);
     if (NS_SUCCEEDED(rv) && isLocked.Equals("1")) {
-      return false;
+      return NS_OK;
     }
 
     inUseInstalls.AppendElement(install);
   }
 
   // At this point we've decided to take the profile. Strip it from other
   // installs.
   for (uint32_t i = 0; i < inUseInstalls.Length(); i++) {
@@ -542,23 +572,102 @@ bool nsToolkitProfileService::MaybeMakeD
   SetDefaultProfile(aProfile);
 
   // SetDefaultProfile will have locked this profile to this install so no
   // other installs will steal it, but this was auto-selected so we want to
   // unlock it so that other installs can potentially take it.
   mProfileDB.DeleteString(mInstallSection.get(), "Locked");
 
   // Persist the changes.
-  Flush();
+  rv = Flush();
+  NS_ENSURE_SUCCESS(rv, rv);
 
   // Once XPCOM is available check if this is the default application and if so
   // lock the profile again.
   mMaybeLockProfile = true;
+  *aResult = true;
 
-  return true;
+  return NS_OK;
+}
+
+bool
+IsFileOutdated(nsIFile* aFile, bool aExists, PRTime aLastModified,
+               int64_t aLastSize) {
+  nsCOMPtr<nsIFile> file;
+  nsresult rv = aFile->Clone(getter_AddRefs(file));
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  bool exists;
+  rv = aFile->Exists(&exists);
+  if (NS_FAILED(rv) || exists != aExists) {
+    return true;
+  }
+
+  if (!exists) {
+    return false;
+  }
+
+  int64_t size;
+  rv = aFile->GetFileSize(&size);
+  if (NS_FAILED(rv) || size != aLastSize) {
+    return true;
+  }
+
+  PRTime time;
+  rv = aFile->GetLastModifiedTime(&time);
+  if (NS_FAILED(rv) || time != aLastModified) {
+    return true;
+  }
+
+  return false;
+}
+
+nsresult
+UpdateFileStats(nsIFile* aFile, bool* aExists, PRTime* aLastModified,
+                int64_t* aLastSize) {
+  nsCOMPtr<nsIFile> file;
+  nsresult rv = aFile->Clone(getter_AddRefs(file));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = file->Exists(aExists);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!(*aExists)) {
+    *aLastModified = 0;
+    *aLastSize = 0;
+    return NS_OK;
+  }
+
+  rv = file->GetFileSize(aLastSize);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = file->GetLastModifiedTime(aLastModified);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetIsListOutdated(bool *aResult) {
+  if (IsFileOutdated(mProfileDBFile, mProfileDBExists, mProfileDBModifiedTime,
+                     mProfileDBFileSize)) {
+    *aResult = true;
+    return NS_OK;
+  }
+
+  if (IsFileOutdated(mInstallDBFile, mInstallDBExists, mInstallDBModifiedTime,
+                     mInstallDBFileSize)) {
+    *aResult = true;
+    return NS_OK;
+  }
+
+  *aResult = false;
+  return NS_OK;
 }
 
 struct ImportInstallsClosure {
   nsINIParser* backupData;
   nsINIParser* profileDB;
 };
 
 static bool ImportInstalls(const char* aSection, void* aClosure) {
@@ -600,21 +709,28 @@ nsresult nsToolkitProfileService::Init()
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mAppData->Clone(getter_AddRefs(mInstallDBFile));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mInstallDBFile->AppendNative(NS_LITERAL_CSTRING("installs.ini"));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  rv = UpdateFileStats(mInstallDBFile, &mInstallDBExists,
+                       &mInstallDBModifiedTime, &mInstallDBFileSize);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   nsAutoCString buffer;
 
-  bool exists;
-  rv = mProfileDBFile->IsFile(&exists);
-  if (NS_SUCCEEDED(rv) && exists) {
+  rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists,
+                       &mProfileDBModifiedTime, &mProfileDBFileSize);
+  if (NS_SUCCEEDED(rv) && mProfileDBExists) {
+    mProfileDBFile->GetFileSize(&mProfileDBFileSize);
+    mProfileDBFile->GetLastModifiedTime(&mProfileDBModifiedTime);
+
     rv = mProfileDB.Init(mProfileDBFile);
     // Init does not fail on parsing errors, only on OOM/really unexpected
     // conditions.
     if (NS_FAILED(rv)) {
       return rv;
     }
 
     rv = mProfileDB.GetString("General", "StartWithLastProfile", buffer);
@@ -623,19 +739,17 @@ nsresult nsToolkitProfileService::Init()
     }
 
     rv = mProfileDB.GetString("General", "Version", buffer);
     if (NS_FAILED(rv)) {
       // This is a profiles.ini written by an older version. We must restore
       // any install data from the backup.
       nsINIParser installDB;
 
-      rv = mInstallDBFile->IsFile(&exists);
-      if (NS_SUCCEEDED(rv) && exists &&
-          NS_SUCCEEDED(installDB.Init(mInstallDBFile))) {
+      if (mInstallDBExists && NS_SUCCEEDED(installDB.Init(mInstallDBFile))) {
         // There is install data to import.
         ImportInstallsClosure closure = {&installDB, &mProfileDB};
         installDB.GetSections(&ImportInstalls, &closure);
       }
 
       rv = mProfileDB.SetString("General", "Version", PROFILE_DB_VERSION);
       NS_ENSURE_SUCCESS(rv, rv);
     }
@@ -1054,17 +1168,20 @@ nsresult nsToolkitProfileService::Select
 
     if (profile && mIsFirstRun && mUseDedicatedProfile) {
       if (profile ==
           (mUseDevEditionProfile ? mDevEditionDefault : mNormalDefault)) {
         // This is the first run of a dedicated profile build where the selected
         // profile is the previous default so we should either make it the
         // default profile for this install or push the user to a new profile.
 
-        if (MaybeMakeDefaultDedicatedProfile(profile)) {
+        bool result;
+        rv = MaybeMakeDefaultDedicatedProfile(profile, &result);
+        NS_ENSURE_SUCCESS(rv, rv);
+        if (result) {
           mStartupReason = NS_LITERAL_STRING("restart-claimed-default");
 
           mCurrent = profile;
         } else {
           if (aIsResetting) {
             // We don't want to create a fresh profile when we're attempting a
             // profile reset so just bail out here, the calling code will handle
             // it.
@@ -1073,17 +1190,18 @@ nsresult nsToolkitProfileService::Select
           }
 
           rv = CreateDefaultProfile(getter_AddRefs(mCurrent));
           if (NS_FAILED(rv)) {
             *aProfile = nullptr;
             return rv;
           }
 
-          Flush();
+          rv = Flush();
+          NS_ENSURE_SUCCESS(rv, rv);
 
           mStartupReason = NS_LITERAL_STRING("restart-skipped-default");
           *aDidCreate = true;
           mCreatedAlternateProfile = true;
         }
 
         NS_IF_ADDREF(*aProfile = mCurrent);
         mCurrent->GetRootDir(aRootDir);
@@ -1177,24 +1295,20 @@ nsresult nsToolkitProfileService::Select
       // main profile directory.
       rv = CreateProfile(lf, nsDependentCSubstring(arg, delim),
                          getter_AddRefs(profile));
     } else {
       rv = CreateProfile(nullptr, nsDependentCString(arg),
                          getter_AddRefs(profile));
     }
     // Some pathological arguments can make it this far
-    if (NS_FAILED(rv)) {
+    if (NS_FAILED(rv) || NS_FAILED(Flush())) {
       PR_fprintf(PR_STDERR, "Error creating profile.\n");
-      return rv;
     }
-    rv = NS_ERROR_ABORT;
-    Flush();
-
-    return rv;
+    return NS_ERROR_ABORT;
   }
 
   // Check the -p command line argument. It either accepts a profile name and
   // uses that named profile or without a name it opens the profile manager.
   ar = CheckArg(*aArgc, aArgv, "p", &arg);
   if (ar == ARG_BAD) {
     ar = CheckArg(*aArgc, aArgv, "osint");
     if (ar == ARG_FOUND) {
@@ -1278,17 +1392,20 @@ nsresult nsToolkitProfileService::Select
         bool exists;
         rv = compat->Exists(&exists);
         NS_ENSURE_SUCCESS(rv, rv);
 
         // If the file is missing then either this is an empty profile (likely
         // generated by bug 1518591) or it is from an ancient version. We'll opt
         // to leave it for older versions in this case.
         if (exists) {
-          if (MaybeMakeDefaultDedicatedProfile(profile)) {
+          bool result;
+          rv = MaybeMakeDefaultDedicatedProfile(profile, &result);
+          NS_ENSURE_SUCCESS(rv, rv);
+          if (result) {
             mStartupReason = NS_LITERAL_STRING("firstrun-claimed-default");
 
             mCurrent = profile;
             rootDir.forget(aRootDir);
             profile->GetLocalDir(aLocalDir);
             profile.forget(aProfile);
             return NS_OK;
           }
@@ -1311,17 +1428,18 @@ nsresult nsToolkitProfileService::Select
       if ((mUseDedicatedProfile || mUseDevEditionProfile) &&
           mProfiles.getFirst() == mProfiles.getLast()) {
         nsCOMPtr<nsIToolkitProfile> newProfile;
         CreateProfile(nullptr, NS_LITERAL_CSTRING(DEFAULT_NAME),
                       getter_AddRefs(newProfile));
         SetNormalDefault(newProfile);
       }
 
-      Flush();
+      rv = Flush();
+      NS_ENSURE_SUCCESS(rv, rv);
 
       if (mCreatedAlternateProfile) {
         mStartupReason = NS_LITERAL_STRING("firstrun-skipped-default");
       } else {
         mStartupReason = NS_LITERAL_STRING("firstrun-created-default");
       }
 
       // Use the new profile.
@@ -1372,22 +1490,21 @@ nsresult nsToolkitProfileService::Create
   } else {
     newProfileName.AssignLiteral("default-");
   }
   newProfileName.AppendPrintf("%" PRId64, PR_Now() / 1000);
   nsresult rv = CreateProfile(nullptr,  // choose a default dir for us
                               newProfileName, getter_AddRefs(newProfile));
   if (NS_FAILED(rv)) return rv;
 
-  rv = Flush();
-  if (NS_FAILED(rv)) return rv;
-
   mCurrent = newProfile;
   newProfile.forget(aNewProfile);
 
+  // Don't flush the changes yet. That will happen once the migration
+  // successfully completes.
   return NS_OK;
 }
 
 /**
  * This is responsible for deleting the old profile, copying its name to the
  * current profile and if the old profile was default making the new profile
  * default as well.
  */
@@ -1414,25 +1531,35 @@ nsresult nsToolkitProfileService::ApplyR
       mProfileDB.DeleteString(mInstallSection.get(), "Locked");
     }
   }
 
   nsCString name;
   nsresult rv = aOldProfile->GetName(name);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // Don't remove the old profile's files until after we've successfully flushed
+  // the profile changes to disk.
   rv = aOldProfile->Remove(false);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Switching the name will make this the default for dev-edition if
   // appropriate.
   rv = mCurrent->SetName(name);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  return Flush();
+  rv = Flush();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Now that the profile changes are flushed, try to remove the old profile's
+  // files. If we fail the worst that will happen is that an orphan directory is
+  // left. Let this run in the background while we start up.
+  RemoveProfileFiles(aOldProfile, true);
+
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::GetProfileByName(const nsACString& aName,
                                           nsIToolkitProfile** aResult) {
   for (RefPtr<nsToolkitProfile> profile : mProfiles) {
     if (profile->mName.Equals(aName)) {
       NS_ADDREF(*aResult = profile);
@@ -1691,16 +1818,20 @@ nsToolkitProfileService::GetProfileCount
     (*aResult)++;
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::Flush() {
+  if (GetIsListOutdated()) {
+    return NS_ERROR_DATABASE_CHANGED;
+  }
+
   nsresult rv;
 
   // If we aren't using dedicated profiles then nothing about the list of
   // installs can have changed, so no need to update the backup.
   if (mUseDedicatedProfile) {
     // Export the installs to the backup.
     nsTArray<nsCString> installs = GetKnownInstalls();
 
@@ -1734,28 +1865,37 @@ nsToolkitProfileService::Flush() {
 
       uint32_t length = data.Length();
       if (fwrite(data.get(), sizeof(char), length, writeFile) != length) {
         fclose(writeFile);
         return NS_ERROR_UNEXPECTED;
       }
 
       fclose(writeFile);
+
+      rv = UpdateFileStats(mInstallDBFile, &mInstallDBExists,
+                          &mInstallDBModifiedTime, &mInstallDBFileSize);
+      NS_ENSURE_SUCCESS(rv, rv);
     } else {
       rv = mInstallDBFile->Remove(false);
       if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
           rv != NS_ERROR_FILE_NOT_FOUND) {
         return rv;
       }
+      mInstallDBExists = false;
     }
   }
 
   rv = mProfileDB.WriteToFile(mProfileDBFile);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists,
+                       &mProfileDBModifiedTime, &mProfileDBFileSize);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   return NS_OK;
 }
 
 NS_IMPL_ISUPPORTS(nsToolkitProfileFactory, nsIFactory)
 
 NS_IMETHODIMP
 nsToolkitProfileFactory::CreateInstance(nsISupports* aOuter, const nsID& aIID,
                                         void** aResult) {
--- a/toolkit/profile/nsToolkitProfileService.h
+++ b/toolkit/profile/nsToolkitProfileService.h
@@ -97,17 +97,18 @@ class nsToolkitProfileService final : pu
   nsresult CreateTimesInternal(nsIFile* profileDir);
   void GetProfileByDir(nsIFile* aRootDir, nsIFile* aLocalDir,
                        nsIToolkitProfile** aResult);
 
   nsresult GetProfileDescriptor(nsIToolkitProfile* aProfile,
                                 nsACString& aDescriptor, bool* aIsRelative);
   bool IsProfileForCurrentInstall(nsIToolkitProfile* aProfile);
   void ClearProfileFromOtherInstalls(nsIToolkitProfile* aProfile);
-  bool MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile);
+  nsresult MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile,
+                                            bool* aResult);
   bool IsSnapEnvironment();
   nsresult CreateDefaultProfile(nsIToolkitProfile** aResult);
   void SetNormalDefault(nsIToolkitProfile* aProfile);
 
   // Returns the known install hashes from the installs database. Modifying the
   // installs database is safe while iterating the returned array.
   nsTArray<nsCString> GetKnownInstalls();
 
@@ -144,20 +145,27 @@ class nsToolkitProfileService final : pu
   bool mUseDevEditionProfile;
   // True if this install should use a dedicated default profile.
   const bool mUseDedicatedProfile;
   // True if during startup no dedicated profile was already selected, an old
   // default profile existed but was rejected so a new profile was created.
   bool mCreatedAlternateProfile;
   nsString mStartupReason;
   bool mMaybeLockProfile;
-
   // Holds the current application update channel. This is only really held
   // so it can be overriden in tests.
   nsCString mUpdateChannel;
+  // Keep track of some attributes of the databases so we can tell if another
+  // process has changed them.
+  bool mProfileDBExists;
+  int64_t mProfileDBFileSize;
+  PRTime mProfileDBModifiedTime;
+  bool mInstallDBExists;
+  int64_t mInstallDBFileSize;
+  PRTime mInstallDBModifiedTime;
 
   static nsToolkitProfileService* gService;
 
   class ProfileEnumerator final : public nsSimpleEnumerator {
    public:
     NS_DECL_NSISIMPLEENUMERATOR
 
     const nsID& DefaultInterface() override {
--- a/toolkit/profile/xpcshell/head.js
+++ b/toolkit/profile/xpcshell/head.js
@@ -1,11 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
+   http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 const { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
 const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
 const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 const { TelemetryTestUtils } = ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm");
 
@@ -98,17 +97,17 @@ function selectStartupProfile(args = [],
   let profile = {};
   let didCreate = service.selectStartupProfile(["xpcshell", ...args], isResetting,
                                                UPDATE_CHANNEL, rootDir, localDir,
                                                profile);
 
   if (profile.value) {
     Assert.ok(rootDir.value.equals(profile.value.rootDir), "Should have matched the root dir.");
     Assert.ok(localDir.value.equals(profile.value.localDir), "Should have matched the local dir.");
-    Assert.equal(service.currentProfile, profile.value, "Should have marked the profile as the current profile.");
+    Assert.ok(service.currentProfile === profile.value, "Should have marked the profile as the current profile.");
   } else {
     Assert.ok(!service.currentProfile, "Should be no current profile.");
   }
 
   return {
     rootDir: rootDir.value,
     localDir: localDir.value,
     profile: profile.value,
@@ -328,17 +327,16 @@ function readInstallsIni() {
   let target = gDataHome.clone();
   target.append("installs.ini");
 
   let installData = {
     installs: {},
   };
 
   if (!target.exists()) {
-    dump("Missing installs.ini\n");
     return installData;
   }
 
   let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
                 getService(Ci.nsIINIParserFactory);
   let ini = factory.createINIParser(target);
 
   let sections = ini.getSections();
--- a/toolkit/profile/xpcshell/test_check_backup.js
+++ b/toolkit/profile/xpcshell/test_check_backup.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that when the profiles DB is missing the install data we reload it.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
 
   let profileData = {
--- a/toolkit/profile/xpcshell/test_claim_locked.js
+++ b/toolkit/profile/xpcshell/test_claim_locked.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that an old-style default profile already locked to a different install
  * isn't claimed by this install.
  */
 
 add_task(async () => {
   let defaultProfile = makeRandomProfileDir("default");
 
--- a/toolkit/profile/xpcshell/test_clean.js
+++ b/toolkit/profile/xpcshell/test_clean.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests from a clean state.
  * Then does some testing that creating new profiles and marking them as
  * selected works.
  */
 
 add_task(async () => {
   let service = getProfileService();
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_conflict_installs.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the profile service refuses to flush when the install.ini file
+ * has been modified.
+ */
+
+function check_unchanged(service) {
+  Assert.ok(!service.isListOutdated, "Should not have detected a modification.");
+  try {
+    service.flush();
+    Assert.ok(true, "Should have flushed.");
+  } catch (e) {
+    Assert.ok(false, "Should have succeeded flushing.");
+  }
+}
+
+function check_outdated(service) {
+  Assert.ok(service.isListOutdated, "Should have detected a modification.");
+  try {
+    service.flush();
+    Assert.ok(false, "Should have failed to flush.");
+  } catch (e) {
+    Assert.equal(e.result, Cr.NS_ERROR_DATABASE_CHANGED, "Should have refused to flush.");
+  }
+}
+
+add_task(async () => {
+  let service = getProfileService();
+
+  Assert.ok(!service.isListOutdated, "Should not be modified yet.");
+
+  let installsini = gDataHome.clone();
+  installsini.append("installs.ini");
+
+  Assert.ok(!installsini.exists(), "File should not exist yet.");
+  installsini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+  check_outdated(service);
+
+  installsini.remove(false);
+  // We have to do profile selection to actually have any install data.
+  selectStartupProfile();
+  check_unchanged(service);
+
+  let oldTime = installsini.lastModifiedTime;
+  installsini.lastModifiedTime = oldTime - 10000;
+  check_outdated(service);
+
+  // We can't reset the modification time back to exactly what it was, so I
+  // guess we can't do much more here :(
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_conflict_profiles.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the profile service refuses to flush when the profiles.ini file
+ * has been modified.
+ */
+
+function check_unchanged(service) {
+  Assert.ok(!service.isListOutdated, "Should not have detected a modification.");
+  try {
+    service.flush();
+    Assert.ok(true, "Should have flushed.");
+  } catch (e) {
+    Assert.ok(false, "Should have succeeded flushing.");
+  }
+}
+
+function check_outdated(service) {
+  Assert.ok(service.isListOutdated, "Should have detected a modification.");
+  try {
+    service.flush();
+    Assert.ok(false, "Should have failed to flush.");
+  } catch (e) {
+    Assert.equal(e.result, Cr.NS_ERROR_DATABASE_CHANGED, "Should have refused to flush.");
+  }
+}
+
+add_task(async () => {
+  let service = getProfileService();
+
+  Assert.ok(!service.isListOutdated, "Should not be modified yet.");
+
+  let profilesini = gDataHome.clone();
+  profilesini.append("profiles.ini");
+
+  Assert.ok(!profilesini.exists(), "File should not exist yet.");
+  profilesini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+  check_outdated(service);
+
+  profilesini.remove(false);
+  check_unchanged(service);
+
+  let oldTime = profilesini.lastModifiedTime;
+  profilesini.lastModifiedTime = oldTime - 10000;
+  check_outdated(service);
+
+  // We can't reset the modification time back to exactly what it was, so I
+  // guess we can't do much more here :(
+});
--- a/toolkit/profile/xpcshell/test_create_default.js
+++ b/toolkit/profile/xpcshell/test_create_default.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that from an empty database a default profile is created.
  */
 
 add_task(async () => {
   let service = getProfileService();
   let { profile, didCreate } = selectStartupProfile();
 
--- a/toolkit/profile/xpcshell/test_lock.js
+++ b/toolkit/profile/xpcshell/test_lock.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that when the default application claims the old-style default profile
  * it locks it to itself.
  */
 
 add_task(async () => {
   gIsDefaultApp = true;
   let defaultProfile = makeRandomProfileDir("default");
--- a/toolkit/profile/xpcshell/test_missing_profilesini.js
+++ b/toolkit/profile/xpcshell/test_missing_profilesini.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /**
  * When profiles.ini is missing there isn't any point in restoring from any
  * installs.ini, the profiles it refers to are gone anyway.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
 
--- a/toolkit/profile/xpcshell/test_new_default.js
+++ b/toolkit/profile/xpcshell/test_new_default.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that an old-style default profile previously used by this build gets
  * updated to a dedicated profile for this build.
  */
 
 add_task(async () => {
   let mydefaultProfile = makeRandomProfileDir("mydefault");
   let defaultProfile = makeRandomProfileDir("default");
--- a/toolkit/profile/xpcshell/test_previous_dedicated.js
+++ b/toolkit/profile/xpcshell/test_previous_dedicated.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /**
  * If install.ini lists a default profile for this build but that profile no
  * longer exists don't try to steal the old-style default even if it was used
  * by this build. It means this install has previously used dedicated profiles.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
--- a/toolkit/profile/xpcshell/test_profile_reset.js
+++ b/toolkit/profile/xpcshell/test_profile_reset.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that from an empty database profile reset doesn't create a new profile.
  */
 
 add_task(async () => {
   let service = getProfileService();
 
   let { profile, didCreate } = selectStartupProfile([], true);
--- a/toolkit/profile/xpcshell/test_remove.js
+++ b/toolkit/profile/xpcshell/test_remove.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests adding and removing functions correctly.
  */
 
 function compareLists(service, knownProfiles) {
   Assert.equal(service.profileCount, knownProfiles.length, "profileCount should be correct.");
   let serviceProfiles = Array.from(service.profiles);
   Assert.equal(serviceProfiles.length, knownProfiles.length, "Enumerator length should be correct.");
--- a/toolkit/profile/xpcshell/test_remove_default.js
+++ b/toolkit/profile/xpcshell/test_remove_default.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /**
  * Tests that calling nsIToolkitProfile.remove on the default profile correctly
  * removes the profile.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
   let defaultProfile = makeRandomProfileDir("default");
--- a/toolkit/profile/xpcshell/test_select_default.js
+++ b/toolkit/profile/xpcshell/test_select_default.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that from a database of profiles the default profile is selected.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
 
   let profileData = {
--- a/toolkit/profile/xpcshell/test_select_environment.js
+++ b/toolkit/profile/xpcshell/test_select_environment.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that the environment variables are used to select a profile.
  */
 
 add_task(async () => {
   let dir = makeRandomProfileDir("foo");
 
   let profileData = {
--- a/toolkit/profile/xpcshell/test_select_environment_named.js
+++ b/toolkit/profile/xpcshell/test_select_environment_named.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that the environment variables are used to select a profile.
  */
 
 add_task(async () => {
   let root = makeRandomProfileDir("foo");
   let local = gDataHomeLocal.clone();
   local.append("foo");
--- a/toolkit/profile/xpcshell/test_select_missing.js
+++ b/toolkit/profile/xpcshell/test_select_missing.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that when choosing an unknown profile the profile manager is shown.
  */
 
 add_task(async () => {
   let profileData = {
     options: {
       startWithLastProfile: true,
--- a/toolkit/profile/xpcshell/test_select_named.js
+++ b/toolkit/profile/xpcshell/test_select_named.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that from a database of profiles the correct profile is selected.
  */
 
 add_task(async () => {
   let profileData = {
     options: {
       startWithLastProfile: true,
--- a/toolkit/profile/xpcshell/test_select_noname.js
+++ b/toolkit/profile/xpcshell/test_select_noname.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that when passing the -P command line argument and not passing a
  * profile name the profile manager is opened.
  */
 
 add_task(async () => {
   let profileData = {
     options: {
--- a/toolkit/profile/xpcshell/test_select_profilemanager.js
+++ b/toolkit/profile/xpcshell/test_select_profilemanager.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that when requested the profile manager is shown.
  */
 
 add_task(async () => {
   let profileData = {
     options: {
       startWithLastProfile: true,
--- a/toolkit/profile/xpcshell/test_single_profile_selected.js
+++ b/toolkit/profile/xpcshell/test_single_profile_selected.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Previous versions of Firefox automatically used a single profile even if it
  * wasn't marked as the default. So we should try to upgrade that one if it was
  * last used by this build. This test checks the case where it was.
  */
 
 add_task(async () => {
   let defaultProfile = makeRandomProfileDir("default");
--- a/toolkit/profile/xpcshell/test_single_profile_unselected.js
+++ b/toolkit/profile/xpcshell/test_single_profile_unselected.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Previous versions of Firefox automatically used a single profile even if it
  * wasn't marked as the default. So we should try to upgrade that one if it was
  * last used by this build. This test checks the case where it wasn't.
  */
 
 add_task(async () => {
   let defaultProfile = makeRandomProfileDir("default");
--- a/toolkit/profile/xpcshell/test_skip_locked_environment.js
+++ b/toolkit/profile/xpcshell/test_skip_locked_environment.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that the environment variables are used to select a profile and that
  * on the first run of a dedicated profile build we don't snatch it if it is
  * locked by another install.
  */
 
 add_task(async () => {
   let root = makeRandomProfileDir("foo");
--- a/toolkit/profile/xpcshell/test_snap.js
+++ b/toolkit/profile/xpcshell/test_snap.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that an old-style default profile not previously used by this build gets
  * used in a snap environment.
  */
 
 add_task(async () => {
   let defaultProfile = makeRandomProfileDir("default");
 
--- a/toolkit/profile/xpcshell/test_snap_empty.js
+++ b/toolkit/profile/xpcshell/test_snap_empty.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that from a clean slate snap builds create an appropriate profile.
  */
 
 add_task(async () => {
   simulateSnapEnvironment();
 
   let service = getProfileService();
--- a/toolkit/profile/xpcshell/test_snatch_environment.js
+++ b/toolkit/profile/xpcshell/test_snatch_environment.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that the environment variables are used to select a profile and that
  * on the first run of a dedicated profile build we snatch it if it was the
  * default profile.
  */
 
 add_task(async () => {
   let root = makeRandomProfileDir("foo");
--- a/toolkit/profile/xpcshell/test_snatch_environment_default.js
+++ b/toolkit/profile/xpcshell/test_snatch_environment_default.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that the environment variables are used to select a profile and that
  * on the first run of a dedicated profile build we snatch it if it was the
  * default profile and lock it when we're the default app.
  */
 
 add_task(async () => {
   gIsDefaultApp = true;
@@ -45,22 +48,22 @@ add_task(async () => {
   env.set("XRE_PROFILE_LOCAL_PATH", local.path);
 
   let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
   checkStartupReason("restart-claimed-default");
 
   Assert.ok(!didCreate, "Should not have created a new profile.");
   Assert.ok(rootDir.equals(root), "Should have selected the right root dir.");
   Assert.ok(localDir.equals(local), "Should have selected the right local dir.");
-  Assert.ok(profile, "A named profile matches this.");
+  Assert.ok(!!profile, "A named profile matches this.");
   Assert.equal(profile.name, PROFILE_DEFAULT, "The right profile was matched.");
 
   let service = getProfileService();
-  Assert.equal(service.defaultProfile, profile, "Should be the default profile.");
-  Assert.equal(service.currentProfile, profile, "Should be the current profile.");
+  Assert.ok(service.defaultProfile === profile, "Should be the default profile.");
+  Assert.ok(service.currentProfile === profile, "Should be the current profile.");
 
   profileData = readProfilesIni();
   Assert.equal(profileData.profiles[0].name, PROFILE_DEFAULT, "Should be the right profile.");
   Assert.ok(profileData.profiles[0].default, "Should still be the old default profile.");
 
   let hash = xreDirProvider.getInstallHash();
   // The info about the other install will have been removed so it goes through first run on next startup.
   Assert.equal(Object.keys(profileData.installs).length, 1, "Should be one known install.");
--- a/toolkit/profile/xpcshell/test_startswithlast.js
+++ b/toolkit/profile/xpcshell/test_startswithlast.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that if profiles.ini is set to not start with the last profile then
  * we show the profile manager in preference to assigning the old default.
  */
 
 add_task(async () => {
   let defaultProfile = makeRandomProfileDir("default");
 
--- a/toolkit/profile/xpcshell/test_steal_inuse.js
+++ b/toolkit/profile/xpcshell/test_steal_inuse.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that an old-style default profile previously used by this build but
  * that has already been claimed by a different build gets stolen by this build.
  */
 
 add_task(async () => {
   let defaultProfile = makeRandomProfileDir("default");
 
--- a/toolkit/profile/xpcshell/test_update_selected_dedicated.js
+++ b/toolkit/profile/xpcshell/test_update_selected_dedicated.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that an old-style default profile previously used by this build gets
  * updated to a dedicated profile for this build.
  */
 
 add_task(async () => {
   let defaultProfile = makeRandomProfileDir("default");
 
--- a/toolkit/profile/xpcshell/test_update_unknown_dedicated.js
+++ b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that an old-style default profile not previously used by any build
  * doesn't get updated to a dedicated profile for this build and we don't set
  * the flag to show the user info about dedicated profiles.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
--- a/toolkit/profile/xpcshell/test_update_unselected_dedicated.js
+++ b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /*
  * Tests that an old-style default profile not previously used by this build gets
  * ignored.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
   let defaultProfile = makeRandomProfileDir("default");
--- a/toolkit/profile/xpcshell/test_use_dedicated.js
+++ b/toolkit/profile/xpcshell/test_use_dedicated.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 /**
  * Tests that if installs.ini lists a profile we use it as the default.
  */
 
 add_task(async () => {
   let hash = xreDirProvider.getInstallHash();
   let defaultProfile = makeRandomProfileDir("default");
   let dedicatedProfile = makeRandomProfileDir("dedicated");
--- a/toolkit/profile/xpcshell/xpcshell.ini
+++ b/toolkit/profile/xpcshell/xpcshell.ini
@@ -30,8 +30,10 @@ skip-if = devedition
 [test_lock.js]
 [test_startswithlast.js]
 [test_snatch_environment.js]
 [test_skip_locked_environment.js]
 [test_snatch_environment_default.js]
 [test_check_backup.js]
 [test_missing_profilesini.js]
 [test_remove.js]
+[test_conflict_profiles.js]
+[test_conflict_installs.js]
--- a/toolkit/xre/ProfileReset.h
+++ b/toolkit/xre/ProfileReset.h
@@ -42,33 +42,36 @@ class ProfileResetCleanupAsyncTask : pub
         mTargetDir(aTargetDir),
         mLeafName(aLeafName) {}
 
   /**
    * Copy a root profile to a backup folder before deleting it.  Then delete the
    * local profile dir.
    */
   NS_IMETHOD Run() override {
-    // Copy to the destination then delete the profile. A move doesn't follow
-    // links.
+    // Copy profile's files to the destination. The profile folder will be
+    // removed after the changes to the known profiles have been flushed to disk
+    // in nsToolkitProfileService::ApplyResetProfile which isn't called until
+    // after this thread finishes copying the files.
     nsresult rv = mProfileDir->CopyToFollowingLinks(mTargetDir, mLeafName);
-    if (NS_SUCCEEDED(rv)) rv = mProfileDir->Remove(true);
+    // I guess we just warn if we fail to make the backup?
     if (NS_WARN_IF(NS_FAILED(rv))) {
       NS_WARNING("Could not backup the root profile directory");
     }
 
     // If we have a separate local cache profile directory, just delete it.
     // Don't return an error if this fails so that reset can proceed if it can't
     // be deleted.
     bool sameDir;
     nsresult rvLocal = mProfileDir->Equals(mProfileLocalDir, &sameDir);
     if (NS_SUCCEEDED(rvLocal) && !sameDir) {
       rvLocal = mProfileLocalDir->Remove(true);
-      if (NS_FAILED(rvLocal))
+      if (NS_FAILED(rvLocal)) {
         NS_WARNING("Could not remove the old local profile directory (cache)");
+      }
     }
     gProfileResetCleanupCompleted = true;
 
     nsCOMPtr<nsIRunnable> resultRunnable = new ProfileResetCleanupResultTask();
     NS_DispatchToMainThread(resultRunnable);
     return NS_OK;
   }
 
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -1833,16 +1833,17 @@ static const char kProfileManagerURL[] =
     "chrome://mozapps/content/profile/profileSelection.xul";
 
 static ReturnAbortOnError ShowProfileManager(
     nsIToolkitProfileService* aProfileSvc, nsINativeAppSupport* aNative) {
   nsresult rv;
 
   nsCOMPtr<nsIFile> profD, profLD;
   bool offline = false;
+  int32_t dialogReturn;
 
   {
     ScopedXPCOMStartup xpcom;
     rv = xpcom.Initialize();
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Initialize the graphics prefs, some of the paths need them before
     // any other graphics is initialized (e.g., showing the profile chooser.)
@@ -1874,43 +1875,50 @@ static ReturnAbortOnError ShowProfileMan
 
       nsCOMPtr<mozIDOMWindowProxy> newWindow;
       rv = windowWatcher->OpenWindow(nullptr, kProfileManagerURL, "_blank",
                                      "centerscreen,chrome,modal,titlebar",
                                      ioParamBlock, getter_AddRefs(newWindow));
 
       NS_ENSURE_SUCCESS_LOG(rv, rv);
 
-      aProfileSvc->Flush();
-
-      int32_t dialogConfirmed;
-      rv = ioParamBlock->GetInt(0, &dialogConfirmed);
-      if (NS_FAILED(rv) || dialogConfirmed == 0) return NS_ERROR_ABORT;
+      rv = ioParamBlock->GetInt(0, &dialogReturn);
+      if (NS_FAILED(rv) || dialogReturn == nsIToolkitProfileService::exit) {
+        return NS_ERROR_ABORT;
+      }
 
       int32_t startOffline;
       rv = ioParamBlock->GetInt(1, &startOffline);
       offline = NS_SUCCEEDED(rv) && startOffline == 1;
 
       rv = dlgArray->QueryElementAt(0, NS_GET_IID(nsIFile),
                                     getter_AddRefs(profD));
       NS_ENSURE_SUCCESS_LOG(rv, rv);
 
       rv = dlgArray->QueryElementAt(1, NS_GET_IID(nsIFile),
                                     getter_AddRefs(profLD));
       NS_ENSURE_SUCCESS_LOG(rv, rv);
     }
   }
 
-  SaveFileToEnv("XRE_PROFILE_PATH", profD);
-  SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", profLD);
-  SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1");
-
   if (offline) {
     SaveToEnv("XRE_START_OFFLINE=1");
   }
+
+  // User requested that we restart back into the profile manager.
+  if (dialogReturn == nsIToolkitProfileService::restart) {
+    SaveToEnv("XRE_RESTART_TO_PROFILE_MANAGER=1");
+    SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1");
+  } else {
+    MOZ_ASSERT(dialogReturn == nsIToolkitProfileService::launchWithProfile);
+    SaveFileToEnv("XRE_PROFILE_PATH", profD);
+    SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", profLD);
+    SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1");
+  }
+
   if (gRestartedByOS) {
     // Re-add this argument when actually starting the application.
     char** newArgv =
         (char**)realloc(gRestartArgv, sizeof(char*) * (gRestartArgc + 2));
     NS_ENSURE_TRUE(newArgv, NS_ERROR_OUT_OF_MEMORY);
     gRestartArgv = newArgv;
     gRestartArgv[gRestartArgc++] = const_cast<char*>("-os-restarted");
     gRestartArgv[gRestartArgc] = nullptr;
@@ -2006,16 +2014,20 @@ static nsresult SelectProfile(nsToolkitP
                "Error: argument --migration is invalid when argument --osint "
                "is specified\n");
     return NS_ERROR_FAILURE;
   }
   if (ar == ARG_FOUND) {
     gDoMigration = true;
   }
 
+  if (EnvHasValue("XRE_RESTART_TO_PROFILE_MANAGER")) {
+    return ShowProfileManager(aProfileSvc, aNative);
+  }
+
   // Ask the profile manager to select the profile directories to use.
   bool didCreate = false;
   rv = aProfileSvc->SelectStartupProfile(&gArgc, gArgv, gDoProfileReset,
                                          aRootDir, aLocalDir, aProfile,
                                          &didCreate, aWasDefaultSelection);
 
   if (rv == NS_ERROR_SHOW_PROFILE_MANAGER) {
     return ShowProfileManager(aProfileSvc, aNative);
@@ -4356,18 +4368,19 @@ nsresult XREMain::XRE_mainRun() {
         }
         pm->Migrate(&mDirProvider, aKey, aName);
       }
     }
 
     if (gDoProfileReset) {
       nsresult backupCreated =
           ProfileResetCleanup(mProfileSvc, gResetOldProfile);
-      if (NS_FAILED(backupCreated))
+      if (NS_FAILED(backupCreated)) {
         NS_WARNING("Could not cleanup the profile that was reset");
+      }
     }
   }
 
 #ifndef XP_WIN
   nsCOMPtr<nsIFile> profileDir;
   nsAutoCString path;
   rv = mDirProvider.GetProfileStartupDir(getter_AddRefs(profileDir));
   if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(profileDir->GetNativePath(path)) &&
--- a/xpcom/base/ErrorList.py
+++ b/xpcom/base/ErrorList.py
@@ -751,16 +751,17 @@ with modules["XPCONNECT"]:
 
 
 # =======================================================================
 # 19: NS_ERROR_MODULE_PROFILE
 # =======================================================================
 with modules["PROFILE"]:
     errors["NS_ERROR_LAUNCHED_CHILD_PROCESS"] = FAILURE(200)
     errors["NS_ERROR_SHOW_PROFILE_MANAGER"] = FAILURE(201)
+    errors["NS_ERROR_DATABASE_CHANGED"] = FAILURE(202)
 
 
 # =======================================================================
 # 21: NS_ERROR_MODULE_SECURITY
 # =======================================================================
 with modules["SECURITY"]:
     # Error code for CSP
     errors["NS_ERROR_CSP_FORM_ACTION_VIOLATION"] = FAILURE(98)