Bug 1334716 - Make process selection a service and implementable in JS. r=krizsa draft
authorBlake Kaplan <mrbkap@gmail.com>
Tue, 01 Nov 2016 16:02:43 -0700
changeset 488898 4e5e36b93dc0a0a447dbdc499986291513f349b2
parent 486451 d11c29c1db3a1bc96ad5792ebf8a89b2fbadcf85
child 546869 f949b6cd3911779e434e62e6de20b36d2307ac09
push id46677
push userbmo:mrbkap@mozilla.com
push dateThu, 23 Feb 2017 23:20:35 +0000
reviewerskrizsa
bugs1334716
milestone54.0a1
Bug 1334716 - Make process selection a service and implementable in JS. r=krizsa MozReview-Commit-ID: CViRvZB8nKe
browser/installer/package-manifest.in
dom/base/ProcessSelector.js
dom/base/ProcessSelector.manifest
dom/base/moz.build
dom/interfaces/base/moz.build
dom/interfaces/base/nsIContentProcess.idl
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -515,16 +515,19 @@
 @RESPATH@/components/NotificationStorage.manifest
 @RESPATH@/components/Push.js
 @RESPATH@/components/Push.manifest
 @RESPATH@/components/PushComponents.js
 
 @RESPATH@/components/remotebrowserutils.manifest
 @RESPATH@/components/RemoteWebNavigation.js
 
+@RESPATH@/components/ProcessSelector.js
+@RESPATH@/components/ProcessSelector.manifest
+
 @RESPATH@/components/SlowScriptDebug.manifest
 @RESPATH@/components/SlowScriptDebug.js
 
 #ifdef MOZ_WEBRTC
 @RESPATH@/components/PeerConnection.js
 @RESPATH@/components/PeerConnection.manifest
 #endif
 
new file mode 100644
--- /dev/null
+++ b/dom/base/ProcessSelector.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+
+const BASE_PREF = "dom.ipc.processCount"
+const PREF_BRANCH = BASE_PREF + ".";
+
+// Utilities:
+function getMaxContentParents(processType) {
+  let maxContentParents = -1;
+  try {
+    maxContentParents = Services.prefs.getIntPref(PREF_BRANCH + aType);
+  } catch (e) {
+    // Pref probably didn't exist, get the default number of processes.
+    try {
+      maxContentParents = Services.prefs.getIntPref(BASE_PREF);
+    } catch (e) {
+      // No prefs? That's odd, use only one process.
+      maxContentParents = 1;
+    }
+  }
+
+  return maxContentParents;
+}
+
+// Fills up aProcesses until max and then selects randomly from the available
+// ones.
+function RandomSelector() {
+}
+
+RandomSelector.prototype = {
+  classID:          Components.ID("{c616fcfd-9737-41f1-aa74-cee72a38f91b}"),
+  QueryInterface:   XPCOMUtils.generateQI([Ci.nsIContentProcessProvider]),
+
+  provideProcess(aType, aOpener, aProcesses, aCount) {
+    let maxContentParents = getMaxContentParents();
+    if (aCount < maxContentParents) {
+      return Ci.nsIContentProcessProvider.NEW_PROCESS;
+    }
+
+    let startIdx = Math.floor(Math.random() * maxContentParents);
+    let curIdx = startIdx;
+
+    do {
+      if (aProcesses[curIdx].opener === aOpener) {
+        return curIdx;
+      }
+
+      curIdx = (curIdx + 1) % maxContentParents;
+    } while (curIdx !== startIdx);
+
+    return Ci.nsIContentProcessProvider.NEW_PROCESS;
+  },
+};
+
+var components = [RandomSelector];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
new file mode 100644
--- /dev/null
+++ b/dom/base/ProcessSelector.manifest
@@ -0,0 +1,2 @@
+component {c616fcfd-9737-41f1-aa74-cee72a38f91b} ProcessSelector.js
+contract @mozilla.org/ipc/processselector;1 {c616fcfd-9737-41f1-aa74-cee72a38f91b}
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -386,16 +386,18 @@ if CONFIG['INTEL_ARCHITECTURE']:
     SOURCES += ['nsTextFragmentSSE2.cpp']
     SOURCES['nsTextFragmentSSE2.cpp'].flags += CONFIG['SSE2_FLAGS']
 
 EXTRA_COMPONENTS += [
     'contentAreaDropListener.js',
     'contentAreaDropListener.manifest',
     'messageWakeupService.js',
     'messageWakeupService.manifest',
+    'ProcessSelector.js',
+    'ProcessSelector.manifest',
     'SlowScriptDebug.js',
     'SlowScriptDebug.manifest',
 ]
 
 # Firefox for Android provides an alternate version of this component
 if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
     EXTRA_COMPONENTS += [
         'SiteSpecificUserAgent.js',
--- a/dom/interfaces/base/moz.build
+++ b/dom/interfaces/base/moz.build
@@ -6,16 +6,17 @@
 
 XPIDL_SOURCES += [
     'domstubs.idl',
     'nsIBrowser.idl',
     'nsIBrowserDOMWindow.idl',
     'nsIContentPermissionPrompt.idl',
     'nsIContentPrefService.idl',
     'nsIContentPrefService2.idl',
+    'nsIContentProcess.idl',
     'nsIContentURIGrouper.idl',
     'nsIDOMChromeWindow.idl',
     'nsIDOMClientRect.idl',
     'nsIDOMClientRectList.idl',
     'nsIDOMConstructor.idl',
     'nsIDOMCrypto.idl',
     'nsIDOMGlobalPropertyInitializer.idl',
     'nsIDOMHistory.idl',
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/base/nsIContentProcess.idl
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMElement;
+interface nsIMessageSender;
+interface nsIURI;
+
+[scriptable, builtinclass, uuid(456f58be-29dd-4973-885b-95aece1c9a8a)]
+interface nsIContentProcessInfo : nsISupports
+{
+  /**
+   * Is this content process alive?
+   */
+  readonly attribute boolean isAlive;
+
+  /**
+   * The content process's PID.
+   * Throws if the process is not alive.
+   */
+  readonly attribute int32_t processId;
+
+  /**
+   * This content process's opener.
+   */
+  readonly attribute nsIContentProcessInfo opener;
+
+  /**
+   * The process manager for this ContentParent (so a process message manager
+   * as opposed to a frame message manager.
+   */
+  readonly attribute nsIMessageSender messageManager;
+};
+
+[scriptable, uuid(83ffb063-5f65-4c45-ae07-3f553e0809bb)]
+interface nsIContentProcessProvider : nsISupports
+{
+  /**
+   * Return this from provideProcess to create a new process.
+   */
+  const int32_t NEW_PROCESS = -1;
+
+  /**
+   * Given aAliveProcesses (with an opener aOpener), choose which process of
+   * aType to use. Return nsIContentProcessProvider.NEW_PROCESS to ask the
+   * caller to create a new content process.
+   */
+  int32_t provideProcess(in AString aType, in nsIContentProcessInfo aOpener,
+                         [array, size_is(aCount)] in nsIContentProcessInfo aAliveProcesses,
+                         in uint32_t aCount);
+};
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -108,16 +108,17 @@
 #include "nsContentUtils.h"
 #include "nsDebugImpl.h"
 #include "nsFrameLoader.h"
 #include "nsFrameMessageManager.h"
 #include "nsHashPropertyBag.h"
 #include "nsIAlertsService.h"
 #include "nsIClipboard.h"
 #include "nsContentPermissionHelper.h"
+#include "nsIContentProcess.h"
 #include "nsICycleCollectorListener.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsIDocument.h"
 #include "nsIDOMGeoGeolocation.h"
 #include "nsIDOMGeoPositionError.h"
 #include "nsIDragService.h"
 #include "mozilla/dom/WakeLock.h"
 #include "nsIDOMWindow.h"
@@ -434,16 +435,100 @@ ContentParentsMemoryReporter::CollectRep
                             KIND_OTHER, UNITS_COUNT,
                             numQueuedMessages, desc, aData);
   }
 
   return NS_OK;
 }
 
 nsClassHashtable<nsStringHashKey, nsTArray<ContentParent*>>* ContentParent::sBrowserContentParents;
+
+namespace {
+
+class ScriptableCPInfo final : public nsIContentProcessInfo
+{
+public:
+  explicit ScriptableCPInfo(ContentParent* aParent)
+    : mContentParent(aParent)
+  {
+    MOZ_ASSERT(mContentParent);
+  }
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSICONTENTPROCESSINFO
+
+  void ProcessDied()
+  {
+    mContentParent = nullptr;
+  }
+
+private:
+  ~ScriptableCPInfo()
+  {
+    MOZ_ASSERT(!mContentParent, "must call ProcessDied");
+  }
+
+  ContentParent* mContentParent;
+};
+
+NS_IMPL_ISUPPORTS(ScriptableCPInfo, nsIContentProcessInfo)
+
+NS_IMETHODIMP
+ScriptableCPInfo::GetIsAlive(bool* aIsAlive)
+{
+  *aIsAlive = mContentParent != nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableCPInfo::GetProcessId(int32_t* aPID)
+{
+  if (!mContentParent) {
+    *aPID = -1;
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  *aPID = mContentParent->Pid();
+  if (*aPID == -1) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableCPInfo::GetOpener(nsIContentProcessInfo** aInfo)
+{
+  *aInfo = nullptr;
+  if (!mContentParent) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  if (ContentParent* opener = mContentParent->Opener()) {
+    nsCOMPtr<nsIContentProcessInfo> info = opener->ScriptableHelper();
+    info.forget(aInfo);
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableCPInfo::GetMessageManager(nsIMessageSender** aMessenger)
+{
+  *aMessenger = nullptr;
+  if (!mContentParent) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  nsCOMPtr<nsIMessageSender> manager = mContentParent->GetMessageManager();
+  manager.forget(aMessenger);
+  return NS_OK;
+}
+
+} // anonymous namespace
+
 nsTArray<ContentParent*>* ContentParent::sPrivateContent;
 StaticAutoPtr<LinkedList<ContentParent> > ContentParent::sContentParents;
 #if defined(XP_LINUX) && defined(MOZ_CONTENT_SANDBOX)
 UniquePtr<SandboxBrokerPolicyFactory> ContentParent::sSandboxBrokerPolicyFactory;
 #endif
 
 // This is true when subprocess launching is enabled.  This is the
 // case between StartUp() and ShutDown() or JoinAllSubprocesses().
@@ -670,47 +755,74 @@ ContentParent::RandomSelect(const nsTArr
 }
 
 /*static*/ already_AddRefed<ContentParent>
 ContentParent::GetNewOrUsedBrowserProcess(const nsAString& aRemoteType,
                                           ProcessPriority aPriority,
                                           ContentParent* aOpener)
 {
   nsTArray<ContentParent*>& contentParents = GetOrCreatePool(aRemoteType);
-
   uint32_t maxContentParents = GetMaxProcessCount(aRemoteType);
 
-  RefPtr<ContentParent> p;
-  if (contentParents.Length() >= maxContentParents) {
+  if (aRemoteType.EqualsLiteral(LARGE_ALLOCATION_REMOTE_TYPE)) {
     // We never want to re-use Large-Allocation processes.
-    if (aRemoteType.EqualsLiteral(LARGE_ALLOCATION_REMOTE_TYPE)) {
+    if (contentParents.Length() >= maxContentParents) {
       return GetNewOrUsedBrowserProcess(NS_LITERAL_STRING(DEFAULT_REMOTE_TYPE),
                                         aPriority,
                                         aOpener);
     }
-
-    if ((p = RandomSelect(contentParents, aOpener, maxContentParents))) {
+  } else {
+    nsTArray<nsIContentProcessInfo*> infos(contentParents.Length());
+    for (auto* cp : contentParents) {
+      infos.AppendElement(cp->mScriptableHelper);
+    }
+
+    nsCOMPtr<nsIContentProcessProvider> cpp =
+      do_GetService("@mozilla.org/ipc/processselector;1");
+    nsIContentProcessInfo* openerInfo = aOpener ? aOpener->mScriptableHelper.get() : nullptr;
+    int32_t index;
+    if (cpp &&
+        NS_SUCCEEDED(cpp->ProvideProcess(aRemoteType, openerInfo,
+                                         infos.Elements(), infos.Length(),
+                                         &index))) {
+      // If the provider returned an existing ContentParent, use that one.
+      if (0 <= index && static_cast<uint32_t>(index) <= maxContentParents) {
+        RefPtr<ContentParent> retval = contentParents[index];
+        return retval.forget();
+      }
+    } else {
+      // If there was a problem with the JS chooser, fall back to a random
+      // selection.
+      NS_WARNING("nsIContentProcessProvider failed to return a process");
+      RefPtr<ContentParent> random;
+      if (contentParents.Length() >= maxContentParents &&
+          (random = RandomSelect(contentParents, aOpener, maxContentParents))) {
+        return random.forget();
+      }
+    }
+
+    // Try to take the preallocated process only for the default process type.
+    RefPtr<ContentParent> p;
+    if (aRemoteType.EqualsLiteral(DEFAULT_REMOTE_TYPE) &&
+        (p = PreallocatedProcessManager::Take())) {
+      // For pre-allocated process we have not set the opener yet.
+      p->mOpener = aOpener;
+      contentParents.AppendElement(p);
       return p.forget();
     }
   }
 
-  // Try to take the preallocated process only for the default process type.
-  if (aRemoteType.Equals(NS_LITERAL_STRING(DEFAULT_REMOTE_TYPE)) &&
-      (p = PreallocatedProcessManager::Take())) {
-    // For pre-allocated process we have not set the opener yet.
-    p->mOpener = aOpener;
-  } else {
-    p = new ContentParent(aOpener, aRemoteType);
-
-    if (!p->LaunchSubprocess(aPriority)) {
-      return nullptr;
-    }
-
-    p->Init();
-  }
+  // Create a new process from scratch.
+  RefPtr<ContentParent> p = new ContentParent(aOpener, aRemoteType);
+
+  if (!p->LaunchSubprocess(aPriority)) {
+    return nullptr;
+  }
+
+  p->Init();
 
   contentParents.AppendElement(p);
   return p.forget();
 }
 
 /*static*/ ProcessPriority
 ContentParent::GetInitialProcessPriority(Element* aFrameElement)
 {
@@ -1180,16 +1292,18 @@ ContentParent::Init()
     mGatherer = static_cast<ProfileGatherer*>(gatherer.get());
 
     StartProfiler(currentProfilerParams);
   }
 #endif
 
   RefPtr<GeckoMediaPluginServiceParent> gmps(GeckoMediaPluginServiceParent::GetSingleton());
   gmps->UpdateContentProcessGMPCapabilities();
+
+  mScriptableHelper = new ScriptableCPInfo(this);
 }
 
 namespace {
 
 class RemoteWindowContext final : public nsIRemoteWindowContext
                                 , public nsIInterfaceRequestor
 {
 public:
@@ -1250,16 +1364,21 @@ ContentParent::SetPriorityAndCheckIsAliv
 #endif
 
   return true;
 }
 
 void
 ContentParent::ShutDownProcess(ShutDownMethod aMethod)
 {
+  if (mScriptableHelper) {
+    static_cast<ScriptableCPInfo*>(mScriptableHelper.get())->ProcessDied();
+    mScriptableHelper = nullptr;
+  }
+
   // Shutting down by sending a shutdown message works differently than the
   // other methods. We first call Shutdown() in the child. After the child is
   // ready, it calls FinishShutdown() on us. Then we close the channel.
   if (aMethod == SEND_SHUTDOWN_MESSAGE) {
     if (mIPCOpen && !mShutdownPending && SendShutdown()) {
       mShutdownPending = true;
       // Start the force-kill timer if we haven't already.
       StartForceKillTimer();
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -40,16 +40,17 @@
 #define DEFAULT_REMOTE_TYPE "web"
 #define FILE_REMOTE_TYPE "file"
 #define EXTENSION_REMOTE_TYPE "extension"
 
 // This must start with the DEFAULT_REMOTE_TYPE above.
 #define LARGE_ALLOCATION_REMOTE_TYPE "webLargeAllocation"
 
 class nsConsoleService;
+class nsIContentProcessInfo;
 class nsICycleCollectorLogSink;
 class nsIDumpGCAndCCLogsCallback;
 class nsITabParent;
 class nsITimer;
 class ParentIdleListener;
 class nsIWidget;
 
 namespace mozilla {
@@ -367,16 +368,20 @@ public:
   {
     return mSubprocess;
   }
 
   ContentParent* Opener() const
   {
     return mOpener;
   }
+  nsIContentProcessInfo* ScriptableHelper() const
+  {
+    return mScriptableHelper;
+  }
 
   bool NeedsPermissionsUpdate() const
   {
     return mSendPermissionUpdates;
   }
 
   /**
    * Kill our subprocess and make sure it dies.  Should only be used
@@ -1161,16 +1166,17 @@ private:
   bool mCalledClose;
   bool mCalledKillHard;
   bool mCreatedPairedMinidumps;
   bool mShutdownPending;
   bool mIPCOpen;
 
   RefPtr<nsConsoleService>  mConsoleService;
   nsConsoleService* GetConsoleService();
+  nsCOMPtr<nsIContentProcessInfo> mScriptableHelper;
 
   nsTArray<nsCOMPtr<nsIObserver>> mIdleListeners;
 
 #ifdef MOZ_X11
   // Dup of child's X socket, used to scope its resources to this
   // object instead of the child process's lifetime.
   ScopedClose mChildXSocketFdDup;
 #endif