Bug 1361900: Part 4 - Use a separate script cache for scripts loaded in the child process. r?erahm,gabor
MozReview-Commit-ID: EIdwmuTOl90
--- a/dom/base/nsFrameMessageManager.cpp
+++ b/dom/base/nsFrameMessageManager.cpp
@@ -1604,17 +1604,17 @@ nsMessageManagerScriptExecutor::TryCache
AutoJSAPI jsapi;
if (!jsapi.Init(xpc::CompilationScope())) {
return;
}
JSContext* cx = jsapi.cx();
JS::Rooted<JSScript*> script(cx);
if (XRE_IsParentProcess()) {
- script = ScriptPreloader::GetSingleton().GetCachedScript(cx, url);
+ script = ScriptPreloader::GetChildSingleton().GetCachedScript(cx, url);
}
if (!script) {
nsCOMPtr<nsIChannel> channel;
NS_NewChannel(getter_AddRefs(channel),
uri,
nsContentUtils::GetSystemPrincipal(),
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
@@ -1669,17 +1669,17 @@ nsMessageManagerScriptExecutor::TryCache
MOZ_ASSERT(script);
aScriptp.set(script);
nsAutoCString scheme;
uri->GetScheme(scheme);
// We don't cache data: scripts!
if (aShouldCache && !scheme.EqualsLiteral("data")) {
if (XRE_IsParentProcess()) {
- ScriptPreloader::GetSingleton().NoteScript(url, url, script);
+ ScriptPreloader::GetChildSingleton().NoteScript(url, url, script);
}
// Root the object also for caching.
auto* holder = new nsMessageManagerScriptHolder(cx, script, aRunInGlobalScope);
sCachedScripts->Put(aURL, holder);
}
}
void
--- a/js/xpconnect/loader/ScriptPreloader.cpp
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -77,17 +77,63 @@ ScriptPreloader::CollectReports(nsIHandl
ScriptPreloader&
ScriptPreloader::GetSingleton()
{
static RefPtr<ScriptPreloader> singleton;
if (!singleton) {
+ if (XRE_IsParentProcess()) {
+ singleton = new ScriptPreloader();
+ singleton->mChildCache = &GetChildSingleton();
+ Unused << singleton->InitCache();
+ } else {
+ singleton = &GetChildSingleton();
+ }
+
+ ClearOnShutdown(&singleton);
+ }
+
+ return *singleton;
+}
+
+// The child singleton is available in all processes, including the parent, and
+// is used for scripts which are expected to be loaded into child processes
+// (such as process and frame scripts), or scripts that have already been loaded
+// into a child. The child caches are managed as follows:
+//
+// - Every startup, we open the cache file from the last session, move it to a
+// new location, and begin pre-loading the scripts that are stored in it. There
+// is a separate cache file for parent and content processes, but the parent
+// process opens both the parent and content cache files.
+//
+// - Once startup is complete, we write a new cache file for the next session,
+// containing only the scripts that were used during early startup, so we don't
+// waste pre-loading scripts that may not be needed.
+//
+// - For content processes, opening and writing the cache file is handled in the
+// parent process. The first content process of each type sends back the data
+// for scripts that were loaded in early startup, and the parent merges them and
+// writes them to a cache file.
+//
+// - Currently, content processes only benefit from the cache data written
+// during the *previous* session. Ideally, new content processes should probably
+// use the cache data written during this session if there was no previous cache
+// file, but I'd rather do that as a follow-up.
+ScriptPreloader&
+ScriptPreloader::GetChildSingleton()
+{
+ static RefPtr<ScriptPreloader> singleton;
+
+ if (!singleton) {
singleton = new ScriptPreloader();
+ if (XRE_IsParentProcess()) {
+ Unused << singleton->InitCache(NS_LITERAL_STRING("scriptCache-child"));
+ }
ClearOnShutdown(&singleton);
}
return *singleton;
}
ProcessType
@@ -209,96 +255,98 @@ ScriptPreloader::FlushCache()
FlushScripts(mSavedScripts);
FlushScripts(mRestoredScripts);
// If we've already finished saving the cache at this point, start a new
// delayed save operation. This will write out an empty cache file in place
// of any cache file we've already written out this session, which will
// prevent us from falling back to the current session's cache file on the
// next startup.
- if (mSaveComplete) {
+ if (mSaveComplete && mChildCache) {
mSaveComplete = false;
Unused << NS_NewNamedThread("SaveScripts",
getter_AddRefs(mSaveThread), this);
}
}
nsresult
ScriptPreloader::Observe(nsISupports* subject, const char* topic, const char16_t* data)
{
if (!strcmp(topic, DELAYED_STARTUP_TOPIC)) {
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
obs->RemoveObserver(this, DELAYED_STARTUP_TOPIC);
mStartupFinished = true;
- if (XRE_IsParentProcess()) {
+
+ if (XRE_IsParentProcess() && mChildCache) {
Unused << NS_NewNamedThread("SaveScripts",
getter_AddRefs(mSaveThread), this);
}
} else if (!strcmp(topic, SHUTDOWN_TOPIC)) {
ForceWriteCacheFile();
} else if (!strcmp(topic, CLEANUP_TOPIC)) {
Cleanup();
} else if (!strcmp(topic, CACHE_FLUSH_TOPIC)) {
FlushCache();
}
return NS_OK;
}
Result<nsCOMPtr<nsIFile>, nsresult>
-ScriptPreloader::GetCacheFile(const char* leafName)
+ScriptPreloader::GetCacheFile(const nsAString& suffix)
{
nsCOMPtr<nsIFile> cacheFile;
NS_TRY(mProfD->Clone(getter_AddRefs(cacheFile)));
NS_TRY(cacheFile->AppendNative(NS_LITERAL_CSTRING("startupCache")));
Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777);
- NS_TRY(cacheFile->AppendNative(nsDependentCString(leafName)));
+ NS_TRY(cacheFile->Append(mBaseName + suffix));
return Move(cacheFile);
}
static const uint8_t MAGIC[] = "mozXDRcachev001";
Result<Ok, nsresult>
ScriptPreloader::OpenCache()
{
NS_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
nsCOMPtr<nsIFile> cacheFile;
- MOZ_TRY_VAR(cacheFile, GetCacheFile("scriptCache.bin"));
+ MOZ_TRY_VAR(cacheFile, GetCacheFile(NS_LITERAL_STRING(".bin")));
bool exists;
NS_TRY(cacheFile->Exists(&exists));
if (exists) {
- NS_TRY(cacheFile->MoveTo(nullptr, NS_LITERAL_STRING("scriptCache-current.bin")));
+ NS_TRY(cacheFile->MoveTo(nullptr, mBaseName + NS_LITERAL_STRING("-current.bin")));
} else {
- NS_TRY(cacheFile->SetLeafName(NS_LITERAL_STRING("scriptCache-current.bin")));
+ NS_TRY(cacheFile->SetLeafName(mBaseName + NS_LITERAL_STRING("-current.bin")));
NS_TRY(cacheFile->Exists(&exists));
if (!exists) {
return Err(NS_ERROR_FILE_NOT_FOUND);
}
}
MOZ_TRY(mCacheData.init(cacheFile));
return Ok();
}
// Opens the script cache file for this session, and initializes the script
// cache based on its contents. See WriteCache for details of the cache file.
Result<Ok, nsresult>
-ScriptPreloader::InitCache()
+ScriptPreloader::InitCache(const nsAString& basePath)
{
mCacheInitialized = true;
+ mBaseName = basePath;
RegisterWeakMemoryReporter(this);
if (!XRE_IsParentProcess()) {
return Ok();
}
MOZ_TRY(OpenCache());
@@ -330,17 +378,17 @@ ScriptPreloader::InitCache()
Range<uint8_t> header(data, data + headerSize);
data += headerSize;
InputBuffer buf(header);
size_t offset = 0;
while (!buf.finished()) {
- auto script = MakeUnique<CachedScript>(buf);
+ auto script = MakeUnique<CachedScript>(*this, buf);
auto scriptData = data + script->mOffset;
if (scriptData + script->mSize > end) {
return Err(NS_ERROR_UNEXPECTED);
}
// Make sure offsets match what we'd expect based on script ordering and
// size, as a basic sanity check.
@@ -368,16 +416,17 @@ ScriptPreloader::InitCache()
MOZ_RELEASE_ASSERT(jsapi.Init(xpc::CompilationScope()));
JSContext* cx = jsapi.cx();
auto start = TimeStamp::Now();
LOG(Info, "Off-thread decoding scripts...\n");
JS::CompileOptions options(cx, JSVERSION_LATEST);
for (auto script : mRestoredScripts) {
+ // Only async decode scripts which have been used in this process type.
if (script->mProcessTypes.contains(CurrentProcessType()) &&
script->mSize > MIN_OFFTHREAD_SIZE &&
JS::CanCompileOffThread(cx, options, script->mSize)) {
DecodeScriptOffThread(cx, script);
} else {
script->mReadyToExecute = true;
}
}
@@ -397,16 +446,22 @@ Write(PRFileDesc* fd, const void* data,
return Ok();
}
void
ScriptPreloader::PrepareCacheWrite()
{
MOZ_ASSERT(NS_IsMainThread());
+ auto cleanup = MakeScopeExit([&] () {
+ if (mChildCache) {
+ mChildCache->PrepareCacheWrite();
+ }
+ });
+
if (mDataPrepared) {
return;
}
if (mRestoredScripts.isEmpty()) {
// Check for any new scripts that we need to save. If there aren't
// any, and there aren't any saved scripts that we need to remove,
// don't bother writing out a new cache file.
@@ -426,17 +481,30 @@ ScriptPreloader::PrepareCacheWrite()
AutoSafeJSAPI jsapi;
LinkedList<CachedScript> asyncScripts;
for (CachedScript* next = mSavedScripts.getFirst(); next; ) {
CachedScript* script = next;
next = script->getNext();
- if (!script->mSize && !script->XDREncode(jsapi.cx())) {
+ // Don't write any scripts that are also in the child cache. They'll be
+ // loaded from the child cache in that case, so there's no need to write
+ // them twice.
+ CachedScript* childScript = mChildCache ? mChildCache->mScripts.Get(script->mCachePath) : nullptr;
+ if (childScript) {
+ if (FindScript(mChildCache->mSavedScripts, script->mCachePath)) {
+ childScript->UpdateLoadTime(script->mLoadTime);
+ childScript->mProcessTypes += script->mProcessTypes;
+ } else {
+ childScript = nullptr;
+ }
+ }
+
+ if (childScript || (!script->mSize && !script->XDREncode(jsapi.cx()))) {
script->remove();
delete script;
} else {
script->mSize = script->Range().length();
if (script->mSize > MIN_OFFTHREAD_SIZE) {
script->remove();
asyncScripts.insertBack(script);
@@ -482,17 +550,17 @@ ScriptPreloader::WriteCache()
}
if (mSaveComplete) {
// If we don't have anything we need to save, we're done.
return Ok();
}
nsCOMPtr<nsIFile> cacheFile;
- MOZ_TRY_VAR(cacheFile, GetCacheFile("scriptCache-new.bin"));
+ MOZ_TRY_VAR(cacheFile, GetCacheFile(NS_LITERAL_STRING("-new.bin")));
bool exists;
NS_TRY(cacheFile->Exists(&exists));
if (exists) {
NS_TRY(cacheFile->Remove(false));
}
AutoFDClose fd;
@@ -514,33 +582,37 @@ ScriptPreloader::WriteCache()
MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
for (auto script : mSavedScripts) {
MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));
script->mXDRData.reset();
}
- NS_TRY(cacheFile->MoveTo(nullptr, NS_LITERAL_STRING("scriptCache.bin")));
+ NS_TRY(cacheFile->MoveTo(nullptr, mBaseName + NS_LITERAL_STRING(".bin")));
return Ok();
}
// Runs in the mSaveThread thread, and writes out the cache file for the next
// session after a reasonable delay.
nsresult
ScriptPreloader::Run()
{
MonitorAutoLock mal(mSaveMonitor);
// Ideally wait about 10 seconds before saving, to avoid unnecessary IO
// during early startup.
mal.Wait(10000);
- Unused << WriteCache();
+ auto result = WriteCache();
+ Unused << NS_WARN_IF(result.isErr());
+
+ result = mChildCache->WriteCache();
+ Unused << NS_WARN_IF(result.isErr());
mSaveComplete = true;
NS_ReleaseOnMainThread(mSaveThread.forget());
mal.NotifyAll();
return NS_OK;
}
@@ -585,27 +657,36 @@ ScriptPreloader::NoteScript(const nsCStr
restored->remove();
mSavedScripts.insertBack(restored);
MOZ_ASSERT(script);
restored->mProcesses += CurrentProcessType();
restored->mScript = script;
restored->mReadyToExecute = true;
} else if (!exists) {
- auto cachedScript = new CachedScript(url, cachePath, script);
+ auto cachedScript = new CachedScript(*this, url, cachePath, script);
cachedScript->mProcesses += CurrentProcessType();
mSavedScripts.insertBack(cachedScript);
mScripts.Put(cachePath, cachedScript);
}
}
JSScript*
ScriptPreloader::GetCachedScript(JSContext* cx, const nsCString& path)
{
+ // If a script is used by both the parent and the child, it's stored only
+ // in the child cache.
+ if (mChildCache) {
+ auto script = mChildCache->GetCachedScript(cx, path);
+ if (script) {
+ return script;
+ }
+ }
+
auto script = mScripts.Get(path);
if (script) {
return WaitForCachedScript(cx, script);
}
return nullptr;
}
@@ -657,38 +738,38 @@ ScriptPreloader::CancelOffThreadParse(vo
JS::CancelOffThreadScriptDecoder(jsapi.cx(), token);
}
/* static */ void
ScriptPreloader::OffThreadDecodeCallback(void* token, void* context)
{
auto script = static_cast<CachedScript*>(context);
- MonitorAutoLock mal(GetSingleton().mMonitor);
+ MonitorAutoLock mal(script->mCache.mMonitor);
if (script->mReadyToExecute) {
// We've already executed this script on the main thread, and opted to
// main thread decode it rather waiting for off-thread decoding to
// finish. So just cancel the off-thread parse rather than completing
// it.
NS_DispatchToMainThread(
- NewRunnableMethod<void*>(&GetSingleton(),
+ NewRunnableMethod<void*>(&script->mCache,
&ScriptPreloader::CancelOffThreadParse,
token));
return;
}
script->mToken = token;
script->mReadyToExecute = true;
mal.NotifyAll();
}
-inline
-ScriptPreloader::CachedScript::CachedScript(InputBuffer& buf)
+ScriptPreloader::CachedScript::CachedScript(ScriptPreloader& cache, InputBuffer& buf)
+ : mCache(cache)
{
Code(buf);
}
bool
ScriptPreloader::CachedScript::XDREncode(JSContext* cx)
{
JSAutoCompartment ac(cx, mScript);
@@ -704,17 +785,17 @@ ScriptPreloader::CachedScript::XDREncode
JS_ClearPendingException(cx);
return false;
}
void
ScriptPreloader::CachedScript::Cancel()
{
if (mToken) {
- GetSingleton().mMonitor.AssertCurrentThreadOwns();
+ mCache.mMonitor.AssertCurrentThreadOwns();
AutoSafeJSAPI jsapi;
JS::CancelOffThreadScriptDecoder(jsapi.cx(), mToken);
mReadyToExecute = true;
mToken = nullptr;
}
}
--- a/js/xpconnect/loader/ScriptPreloader.h
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -47,30 +47,31 @@ class ScriptPreloader : public nsIObserv
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIOBSERVER
NS_DECL_NSIMEMORYREPORTER
NS_DECL_NSIRUNNABLE
static ScriptPreloader& GetSingleton();
+ static ScriptPreloader& GetChildSingleton();
static ProcessType GetChildProcessType(const nsAString& remoteType);
// Retrieves the script with the given cache key from the script cache.
// Returns null if the script is not cached.
JSScript* GetCachedScript(JSContext* cx, const nsCString& name);
// Notes the execution of a script with the given URL and cache key.
// Depending on the stage of startup, the script may be serialized and
// stored to the startup script cache.
void NoteScript(const nsCString& url, const nsCString& cachePath, JS::HandleScript script);
// Initializes the script cache from the startup script cache file.
- Result<Ok, nsresult> InitCache();
+ Result<Ok, nsresult> InitCache(const nsAString& = NS_LITERAL_STRING("scriptCache"));
void Trace(JSTracer* trc);
static ProcessType CurrentProcessType()
{
return sProcessType;
}
@@ -102,33 +103,33 @@ private:
// the next session's cache file. If it was compiled in this session, its
// mXDRRange will initially be empty, and its mXDRData buffer will be
// populated just before it is written to the cache file.
class CachedScript : public LinkedListElement<CachedScript>
{
public:
CachedScript(CachedScript&&) = default;
- CachedScript(const nsCString& url, const nsCString& cachePath, JSScript* script)
- : mURL(url)
+ CachedScript(ScriptPreloader& cache, const nsCString& url, const nsCString& cachePath, JSScript* script)
+ : mCache(cache)
+ , mURL(url)
, mCachePath(cachePath)
, mScript(script)
, mReadyToExecute(true)
{}
- explicit inline CachedScript(InputBuffer& buf);
+ inline CachedScript(ScriptPreloader& cache, InputBuffer& buf);
~CachedScript()
{
- auto& cache = GetSingleton();
#ifdef DEBUG
- auto hashValue = cache.mScripts.Get(mCachePath);
+ auto hashValue = mCache->mScripts.Get(mCachePath);
MOZ_ASSERT_IF(hashValue, hashValue == this);
#endif
- cache.mScripts.Remove(mCachePath);
+ mCache->mScripts.Remove(mCachePath);
}
void Cancel();
// Encodes this script into XDR data, and stores the result in mXDRData.
// Returns true on success, false on failure.
bool XDREncode(JSContext* cx);
@@ -167,16 +168,18 @@ private:
if (mXDRData.isSome()) {
size += (mXDRData->sizeOfExcludingThis(mallocSizeOf) +
mURL.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
mCachePath.SizeOfExcludingThisEvenIfShared(mallocSizeOf));
}
return size;
}
+ ScriptPreloader& mCache;
+
// The URL from which this script was initially read and compiled.
nsCString mURL;
// A unique identifier for this script's filesystem location, used as a
// primary cache lookup value.
nsCString mCachePath;
// The offset of this script in the cache file, from the start of the XDR
// data block.
@@ -242,17 +245,17 @@ private:
// Prepares scripts for writing to the cache, serializing new scripts to
// XDR, and calculating their size-based offsets.
void PrepareCacheWrite();
// Returns a file pointer for the cache file with the given name in the
// current profile.
Result<nsCOMPtr<nsIFile>, nsresult>
- GetCacheFile(const char* leafName);
+ GetCacheFile(const nsAString& suffix);
static CachedScript* FindScript(LinkedList<CachedScript>& scripts, const nsCString& cachePath);
// Waits for the given cached script to finish compiling off-thread, or
// decodes it synchronously on the main thread, as appropriate.
JSScript* WaitForCachedScript(JSContext* cx, CachedScript* script);
// Begins decoding the given script in a background thread.
@@ -294,16 +297,20 @@ private:
bool mCacheInitialized = false;
bool mSaveComplete = false;
bool mDataPrepared = false;
// The process type of the current process.
static ProcessType sProcessType;
+ RefPtr<ScriptPreloader> mChildCache;
+
+ nsString mBaseName;
+
nsCOMPtr<nsIFile> mProfD;
nsCOMPtr<nsIThread> mSaveThread;
// The mmapped cache data from this session's cache file.
AutoMemMap mCacheData;
Monitor mMonitor;
Monitor mSaveMonitor;
--- a/xpcom/build/XPCOMInit.cpp
+++ b/xpcom/build/XPCOMInit.cpp
@@ -705,17 +705,17 @@ NS_InitXPCOM2(nsIServiceManager** aResul
// Force layout to spin up so that nsContentUtils is available for cx stack
// munging. Note that layout registers a number of static atoms, and also
// seals the static atom table, so NS_RegisterStaticAtom may not be called
// beyond this point.
nsCOMPtr<nsISupports> componentLoader =
do_GetService("@mozilla.org/moz/jsloader;1");
- Unused << mozilla::ScriptPreloader::GetSingleton().InitCache();
+ mozilla::ScriptPreloader::GetSingleton();
mozilla::scache::StartupCache::GetSingleton();
mozilla::AvailableMemoryTracker::Activate();
// Notify observers of xpcom autoregistration start
NS_CreateServicesFromCategory(NS_XPCOM_STARTUP_CATEGORY,
nullptr,
NS_XPCOM_STARTUP_OBSERVER_ID);
#ifdef XP_WIN