From a4aef929a146aaf4c8b6aaa9170f0a2df88f8102 Mon Sep 17 00:00:00 2001 From: "Nicolas B. Pierron" Date: Wed, 12 May 2021 13:57:55 +0000 Subject: [PATCH] Bug 1698045 part 1 - Add xpc::SelfHostedShmem to hold shared memory for JS initialization. r=smaug,tcampbell,ipc-reviewers,jld This change adds the ground work to share content provided by the JS engine of the Parent process to initialize the JS engine of other threads and Content processes. The singleton class xpc::SelfHostedShmem is used to wrap the logic behind holding the memory. The memory is initialized with `InitFromParent` or `InitFromChild`. The memory is accessible using either the `Content` or `Handle`. The shared memory is transfered through the command line using `mozilla::ipc::ExportSharedJSInit` and read using `mozilla::ipc::ImportSharedJSInit` functions. The command line is used, as we need the shared memory to be avilable for the JS engine initialization. The command line is composed of a single command named `-jsInit` which is followed by the handle (on Windows) and the length of the shared content. The memory associated with the shared memory is cleared in `ShutdownXPCOM` after closing all threads, and shuting down the JS engine. This is necessary as we expect the JS engine to borrow content from the shared memory. Differential Revision: https://phabricator.services.mozilla.com/D110576 --- dom/ipc/ContentParent.cpp | 6 ++ dom/ipc/ContentProcess.cpp | 19 ++++ ipc/glue/ProcessUtils.h | 9 ++ ipc/glue/ProcessUtils_common.cpp | 108 ++++++++++++++++++++++ js/xpconnect/src/XPCSelfHostedShmem.cpp | 115 ++++++++++++++++++++++++ js/xpconnect/src/XPCSelfHostedShmem.h | 89 ++++++++++++++++++ js/xpconnect/src/moz.build | 2 + xpcom/build/XPCOMInit.cpp | 5 ++ 8 files changed, 353 insertions(+) create mode 100644 js/xpconnect/src/XPCSelfHostedShmem.cpp create mode 100644 js/xpconnect/src/XPCSelfHostedShmem.h diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index c145a54e3db7..78d699105cff 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -2512,6 +2512,12 @@ bool ContentParent::BeginSubprocessLaunch(ProcessPriority aPriority) { } mPrefSerializer->AddSharedPrefCmdLineArgs(*mSubprocess, extraArgs); + // The JS engine does some computation during the initialization which can be + // shared across processes. We add command line arguments to pass a file + // handle and its content length, to minimize the startup time of content + // processes. + ::mozilla::ipc::ExportSharedJSInit(*mSubprocess, extraArgs); + // Register ContentParent as an observer for changes to any pref // whose prefix matches the empty string, i.e. all of them. The // observation starts here in order to capture pref updates that diff --git a/dom/ipc/ContentProcess.cpp b/dom/ipc/ContentProcess.cpp index 49fff36bca88..ee8bf1c05cce 100644 --- a/dom/ipc/ContentProcess.cpp +++ b/dom/ipc/ContentProcess.cpp @@ -86,6 +86,8 @@ bool ContentProcess::Init(int aArgc, char* aArgv[]) { char* prefMapHandle = nullptr; char* prefsLen = nullptr; char* prefMapSize = nullptr; + char* jsInitHandle = nullptr; + char* jsInitLen = nullptr; #if defined(XP_MACOSX) && defined(MOZ_SANDBOX) nsCOMPtr profileDir; #endif @@ -141,6 +143,19 @@ bool ContentProcess::Init(int aArgc, char* aArgv[]) { return false; } prefMapSize = aArgv[i]; + + } else if (strcmp(aArgv[i], "-jsInit") == 0) { + // command line: -jsInit [handle] length +#ifdef XP_WIN + if (++i == aArgc) { + return false; + } + jsInitHandle = aArgv[i]; +#endif + if (++i == aArgc) { + return false; + } + jsInitLen = aArgv[i]; } else if (strcmp(aArgv[i], "-safeMode") == 0) { gSafeMode = true; @@ -177,6 +192,10 @@ bool ContentProcess::Init(int aArgc, char* aArgv[]) { return false; } + if (!::mozilla::ipc::ImportSharedJSInit(jsInitHandle, jsInitLen)) { + return false; + } + mContent.Init(IOThreadChild::message_loop(), ParentPid(), *parentBuildID, IOThreadChild::TakeChannel(), *childID, *isForBrowser); diff --git a/ipc/glue/ProcessUtils.h b/ipc/glue/ProcessUtils.h index 870f8d9689e3..efe6a40381ca 100644 --- a/ipc/glue/ProcessUtils.h +++ b/ipc/glue/ProcessUtils.h @@ -76,6 +76,15 @@ void SetPrefsFd(int aFd); void SetPrefMapFd(int aFd); #endif +// Generate command line argument to spawn a child process. If the shared memory +// is not properly initialized, this would be a no-op. +void ExportSharedJSInit(GeckoChildProcessHost& procHost, + std::vector& aExtraOpts); + +// Initialize the content used by the JS engine during the initialization of a +// JS::Runtime. +bool ImportSharedJSInit(char* aJsInitHandleStr, char* aJsInitLenStr); + } // namespace ipc } // namespace mozilla diff --git a/ipc/glue/ProcessUtils_common.cpp b/ipc/glue/ProcessUtils_common.cpp index 9fd9693269a4..07d5f0f1e7f1 100644 --- a/ipc/glue/ProcessUtils_common.cpp +++ b/ipc/glue/ProcessUtils_common.cpp @@ -11,6 +11,8 @@ #include "mozilla/UniquePtrExtensions.h" #include "nsPrintfCString.h" +#include "XPCSelfHostedShmem.h" + namespace mozilla { namespace ipc { @@ -207,5 +209,111 @@ const FileDescriptor& SharedPreferenceDeserializer::GetPrefMapHandle() const { return mPrefMapHandle.ref(); } +#ifdef XP_UNIX +// On Unix, file descriptors are per-process. This value is used when mapping +// a parent process handle to a content process handle. +static const int kJSInitFileDescriptor = 11; +#endif + +void ExportSharedJSInit(mozilla::ipc::GeckoChildProcessHost& procHost, + std::vector& aExtraOpts) { +#ifdef ANDROID + // The code to support Android is added in a follow-up patch. + return; +#else + // Formats a pointer or pointer-sized-integer as a string suitable for passing + // in an arguments list. + auto formatPtrArg = [](auto arg) { + return nsPrintfCString("%zu", uintptr_t(arg)); + }; + + auto& shmem = xpc::SelfHostedShmem::GetSingleton(); + const mozilla::UniqueFileHandle& uniqHandle = shmem.Handle(); + size_t len = shmem.Content().Length(); + + // If the file is not found or the content is empty, then we would start the + // content process without this optimization. + if (!uniqHandle || !len) { + return; + } + + mozilla::detail::FileHandleType handle = uniqHandle.get(); + + // command line: -jsInit [handle] length + aExtraOpts.push_back("-jsInit"); + +# if defined(XP_WIN) + // Record the handle as to-be-shared, and pass it via a command flag. + procHost.AddHandleToShare(HANDLE(handle)); + aExtraOpts.push_back(formatPtrArg(HANDLE(handle)).get()); +# else + // In contrast, Unix fds are per-process. So remap the fd to a fixed one that + // will be used in the child. + // XXX: bug 1440207 is about improving how fixed fds are used. + // + // Note: on Android, AddFdToRemap() sets up the fd to be passed via a Parcel, + // and the fixed fd isn't used. However, we still need to mark it for + // remapping so it doesn't get closed in the child. + procHost.AddFdToRemap(handle, kJSInitFileDescriptor); +# endif + + // Pass the lengths via command line flags. + aExtraOpts.push_back(formatPtrArg(len).get()); +#endif +} + +bool ImportSharedJSInit(char* aJsInitHandleStr, char* aJsInitLenStr) { + // This is an optimization, and as such we can safely recover if the command + // line argument are not provided. + if (!aJsInitLenStr) { + return true; + } + +#ifdef XP_WIN + if (!aJsInitHandleStr) { + return true; + } +#endif + + // Parses an arg containing a pointer-sized-integer. + auto parseUIntPtrArg = [](char*& aArg) { + // ContentParent uses %zu to print a word-sized unsigned integer. So even + // though strtoull() returns an unsigned long long int, it will fit in a + // uintptr_t. + return uintptr_t(strtoull(aArg, &aArg, 10)); + }; + +#ifdef XP_WIN + auto parseHandleArg = [&](char*& aArg) { + return HANDLE(parseUIntPtrArg(aArg)); + }; + + base::SharedMemoryHandle handle(parseHandleArg(aJsInitHandleStr)); + if (aJsInitHandleStr[0] != '\0') { + return false; + } +#endif + + size_t len = parseUIntPtrArg(aJsInitLenStr); + if (aJsInitLenStr[0] != '\0') { + return false; + } + +#ifdef XP_UNIX + auto handle = base::FileDescriptor(kJSInitFileDescriptor, + /* auto_close */ true); +#endif + + // Initialize the shared memory with the file handle and size of the content + // of the self-hosted Xdr. + auto& shmem = xpc::SelfHostedShmem::GetSingleton(); + if (!shmem.InitFromChild(handle, len)) { + NS_ERROR("failed to open shared memory in the child"); + return false; + } + + return true; +} + } // namespace ipc } // namespace mozilla diff --git a/js/xpconnect/src/XPCSelfHostedShmem.cpp b/js/xpconnect/src/XPCSelfHostedShmem.cpp new file mode 100644 index 000000000000..3eb0a95d343a --- /dev/null +++ b/js/xpconnect/src/XPCSelfHostedShmem.cpp @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "XPCSelfHostedShmem.h" + +// static +mozilla::StaticRefPtr + xpc::SelfHostedShmem::sSelfHostedXdr; + +NS_IMPL_ISUPPORTS(xpc::SelfHostedShmem, nsIMemoryReporter) + +// static +xpc::SelfHostedShmem& xpc::SelfHostedShmem::GetSingleton() { + MOZ_ASSERT_IF(!sSelfHostedXdr, NS_IsMainThread()); + + if (!sSelfHostedXdr) { + sSelfHostedXdr = new SelfHostedShmem; + } + + return *sSelfHostedXdr; +} + +void xpc::SelfHostedShmem::InitMemoryReporter() { + mozilla::RegisterWeakMemoryReporter(this); +} + +// static +void xpc::SelfHostedShmem::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + // NOTE: We cannot call UnregisterWeakMemoryReporter(sSelfHostedXdr) as the + // service is shutdown at the time this call is made. In any cases, this would + // already be done when the memory reporter got destroyed. + sSelfHostedXdr = nullptr; +} + +void xpc::SelfHostedShmem::InitFromParent(ContentType aXdr) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mLen, "Shouldn't call this more than once"); + + size_t len = aXdr.Length(); + auto shm = mozilla::MakeUnique(); + if (NS_WARN_IF(!shm->CreateFreezeable(len))) { + return; + } + + if (NS_WARN_IF(!shm->Map(len))) { + return; + } + + void* address = shm->memory(); + memcpy(address, aXdr.Elements(), aXdr.LengthBytes()); + + base::SharedMemory roCopy; + if (NS_WARN_IF(!shm->ReadOnlyCopy(&roCopy))) { + return; + } + + mMem = std::move(shm); + mHandle = roCopy.TakeHandle(); + mLen = len; +} + +bool xpc::SelfHostedShmem::InitFromChild(::base::SharedMemoryHandle aHandle, + size_t aLen) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mLen, "Shouldn't call this more than once"); + + auto shm = mozilla::MakeUnique(); + if (NS_WARN_IF(!shm->SetHandle(aHandle, /* read_only */ true))) { + return false; + } + + if (NS_WARN_IF(!shm->Map(aLen))) { + return false; + } + + // Note: mHandle remains empty, as content processes are not spawning more + // content processes. + mMem = std::move(shm); + mLen = aLen; + return true; +} + +xpc::SelfHostedShmem::ContentType xpc::SelfHostedShmem::Content() const { + if (!mMem) { + MOZ_ASSERT(mLen == 0); + return ContentType(); + } + return ContentType(reinterpret_cast(mMem->memory()), mLen); +} + +const mozilla::UniqueFileHandle& xpc::SelfHostedShmem::Handle() const { + return mHandle; +} + +NS_IMETHODIMP +xpc::SelfHostedShmem::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + // If this is the parent process, then we have a handle and this instance owns + // the data and shares it with other processes, otherwise this is shared data. + if (XRE_IsParentProcess()) { + // This does not exactly report the amount of data mapped by the system, + // but the space requested when creating the handle. + MOZ_COLLECT_REPORT("explicit/js-non-window/shared-memory/self-hosted-xdr", + KIND_NONHEAP, UNITS_BYTES, mLen, + "Memory used to initialize the JS engine with the " + "self-hosted code encoded by the parent process."); + } + return NS_OK; +} diff --git a/js/xpconnect/src/XPCSelfHostedShmem.h b/js/xpconnect/src/XPCSelfHostedShmem.h new file mode 100644 index 000000000000..589beab8a2a5 --- /dev/null +++ b/js/xpconnect/src/XPCSelfHostedShmem.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef xpcselfhostedshmem_h___ +#define xpcselfhostedshmem_h___ + +#include "base/shared_memory.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/Span.h" +#include "mozilla/StaticPtr.h" +#include "nsIMemoryReporter.h" +#include "nsIObserver.h" +#include "nsIThread.h" + +namespace xpc { + +// This class is a singleton which holds a file-mapping of the Self Hosted +// Stencil XDR, to be shared by the parent process with all the child processes. +// +// It is either initialized by the parent process by monitoring when the Self +// hosted stencil is produced, or by the content process by reading a shared +// memory-mapped file. +class SelfHostedShmem final : public nsIMemoryReporter { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMEMORYREPORTER + + // NOTE: This type is identical to JS::SelfHostedCache, but we repeat it to + // avoid including JS engine API in ipc code. + using ContentType = mozilla::Span; + + static SelfHostedShmem& GetSingleton(); + + // Initialize this singleton with the content of the Self-hosted Stencil XDR. + // This will be used to initialize the shared memory which would hold a copy. + // + // This function is not thread-safe and should be call at most once and from + // the main thread. + void InitFromParent(ContentType aXdr); + + // Initialize this singleton with the content coming from the parent process, + // using a file handle which maps to the memory pages of the parent process. + // + // This function is not thread-safe and should be call at most once and from + // the main thread. + [[nodiscard]] bool InitFromChild(base::SharedMemoryHandle aHandle, + size_t aLen); + + // Return a span over the read-only XDR content of the self-hosted stencil. + ContentType Content() const; + + // Return the file handle which is under which the content is mapped. + const mozilla::UniqueFileHandle& Handle() const; + + // Register this class to the memory reporter service. + void InitMemoryReporter(); + + // Unregister this class from the memory reporter service, and delete the + // memory associated with the shared memory. As the memory is borrowed by the + // JS engine, this function should be called after JS_Shutdown. + // + // NOTE: This is not using the Observer service which would call Shutdown + // while JS code might still be running during shutdown process. + static void Shutdown(); + + private: + SelfHostedShmem() = default; + ~SelfHostedShmem() = default; + + static mozilla::StaticRefPtr sSelfHostedXdr; + + // read-only file Handle used to transfer from the parent process to content + // processes. + mozilla::UniqueFileHandle mHandle; + + // Shared memory used by JS runtime initialization. + mozilla::UniquePtr mMem; + + // Length of the content within the shared memory. + size_t mLen = 0; +}; + +} // namespace xpc + +#endif // !xpcselfhostedshmem_h___ diff --git a/js/xpconnect/src/moz.build b/js/xpconnect/src/moz.build index ab6f0efa2ebc..4c2c5cf88d97 100644 --- a/js/xpconnect/src/moz.build +++ b/js/xpconnect/src/moz.build @@ -9,6 +9,7 @@ EXPORTS += [ "XPCJSMemoryReporter.h", "xpcObjectHelper.h", "xpcpublic.h", + "XPCSelfHostedShmem.h", ] UNIFIED_SOURCES += [ @@ -30,6 +31,7 @@ UNIFIED_SOURCES += [ "XPCMaps.cpp", "XPCModule.cpp", "XPCRuntimeService.cpp", + "XPCSelfHostedShmem.cpp", "XPCShellImpl.cpp", "XPCString.cpp", "XPCThrower.cpp", diff --git a/xpcom/build/XPCOMInit.cpp b/xpcom/build/XPCOMInit.cpp index 792c8d04bed5..17567c9055f5 100644 --- a/xpcom/build/XPCOMInit.cpp +++ b/xpcom/build/XPCOMInit.cpp @@ -99,6 +99,7 @@ #include "jsapi.h" #include "js/Initialization.h" +#include "XPCSelfHostedShmem.h" #include "gfxPlatform.h" @@ -473,6 +474,7 @@ NS_InitXPCOM(nsIServiceManager** aResult, nsIFile* aBinDirectory, // The memory reporter manager is up and running -- register our reporters. RegisterStrongMemoryReporter(new ICUReporter()); RegisterStrongMemoryReporter(new OggReporter()); + xpc::SelfHostedShmem::GetSingleton().InitMemoryReporter(); mozilla::Telemetry::Init(); @@ -731,6 +733,9 @@ nsresult ShutdownXPCOM(nsIServiceManager* aServMgr) { sInitializedJS = false; } + // Release shared memory which might be borrowed by the JS engine. + xpc::SelfHostedShmem::Shutdown(); + // After all threads have been joined and the component manager has been shut // down, any remaining objects that could be holding NSS resources (should) // have been released, so we can safely shut down NSS.