LCOV - code coverage report
Current view: top level - js/xpconnect/loader - ScriptPreloader.cpp (source / functions) Hit Total Coverage
Test: output.info Lines: 285 447 63.8 %
Date: 2017-07-14 16:53:18 Functions: 30 40 75.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
       2             : /* vim: set ts=8 sts=4 et sw=4 tw=99: */
       3             : /* This Source Code Form is subject to the terms of the Mozilla Public
       4             :  * License, v. 2.0. If a copy of the MPL was not distributed with this
       5             :  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
       6             : 
       7             : #include "mozilla/ScriptPreloader.h"
       8             : #include "ScriptPreloader-inl.h"
       9             : #include "mozilla/loader/ScriptCacheActors.h"
      10             : 
      11             : #include "mozilla/ArrayUtils.h"
      12             : #include "mozilla/ClearOnShutdown.h"
      13             : #include "mozilla/FileUtils.h"
      14             : #include "mozilla/Logging.h"
      15             : #include "mozilla/ScopeExit.h"
      16             : #include "mozilla/Services.h"
      17             : #include "mozilla/Unused.h"
      18             : #include "mozilla/dom/ContentChild.h"
      19             : #include "mozilla/dom/ContentParent.h"
      20             : 
      21             : #include "MainThreadUtils.h"
      22             : #include "nsDebug.h"
      23             : #include "nsDirectoryServiceUtils.h"
      24             : #include "nsIFile.h"
      25             : #include "nsIObserverService.h"
      26             : #include "nsJSUtils.h"
      27             : #include "nsProxyRelease.h"
      28             : #include "nsThreadUtils.h"
      29             : #include "nsXULAppAPI.h"
      30             : #include "xpcpublic.h"
      31             : 
      32             : #define DELAYED_STARTUP_TOPIC "browser-delayed-startup-finished"
      33             : #define DOC_ELEM_INSERTED_TOPIC "document-element-inserted"
      34             : #define CLEANUP_TOPIC "xpcom-shutdown"
      35             : #define SHUTDOWN_TOPIC "quit-application-granted"
      36             : #define CACHE_INVALIDATE_TOPIC "startupcache-invalidate"
      37             : 
      38             : namespace mozilla {
      39             : namespace {
      40             : static LazyLogModule gLog("ScriptPreloader");
      41             : 
      42             : #define LOG(level, ...) MOZ_LOG(gLog, LogLevel::level, (__VA_ARGS__))
      43             : }
      44             : 
      45             : using mozilla::dom::AutoJSAPI;
      46             : using mozilla::dom::ContentChild;
      47             : using mozilla::dom::ContentParent;
      48             : using namespace mozilla::loader;
      49             : 
      50             : ProcessType ScriptPreloader::sProcessType;
      51             : 
      52             : 
      53             : nsresult
      54           0 : ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport,
      55             :                                 nsISupports* aData, bool aAnonymize)
      56             : {
      57           0 :     MOZ_COLLECT_REPORT(
      58             :         "explicit/script-preloader/heap/saved-scripts", KIND_HEAP, UNITS_BYTES,
      59             :         SizeOfHashEntries<ScriptStatus::Saved>(mScripts, MallocSizeOf),
      60             :         "Memory used to hold the scripts which have been executed in this "
      61           0 :         "session, and will be written to the startup script cache file.");
      62             : 
      63           0 :     MOZ_COLLECT_REPORT(
      64             :         "explicit/script-preloader/heap/restored-scripts", KIND_HEAP, UNITS_BYTES,
      65             :         SizeOfHashEntries<ScriptStatus::Restored>(mScripts, MallocSizeOf),
      66             :         "Memory used to hold the scripts which have been restored from the "
      67           0 :         "startup script cache file, but have not been executed in this session.");
      68             : 
      69           0 :     MOZ_COLLECT_REPORT(
      70             :         "explicit/script-preloader/heap/other", KIND_HEAP, UNITS_BYTES,
      71             :         ShallowHeapSizeOfIncludingThis(MallocSizeOf),
      72           0 :         "Memory used by the script cache service itself.");
      73             : 
      74           0 :     MOZ_COLLECT_REPORT(
      75             :         "explicit/script-preloader/non-heap/memmapped-cache", KIND_NONHEAP, UNITS_BYTES,
      76             :         mCacheData.nonHeapSizeOfExcludingThis(),
      77           0 :         "The memory-mapped startup script cache file.");
      78             : 
      79           0 :     return NS_OK;
      80             : }
      81             : 
      82             : 
      83             : ScriptPreloader&
      84         579 : ScriptPreloader::GetSingleton()
      85             : {
      86         579 :     static RefPtr<ScriptPreloader> singleton;
      87             : 
      88         579 :     if (!singleton) {
      89           3 :         if (XRE_IsParentProcess()) {
      90           1 :             singleton = new ScriptPreloader();
      91           1 :             singleton->mChildCache = &GetChildSingleton();
      92           1 :             Unused << singleton->InitCache();
      93             :         } else {
      94           2 :             singleton = &GetChildSingleton();
      95             :         }
      96             : 
      97           3 :         ClearOnShutdown(&singleton);
      98             :     }
      99             : 
     100         579 :     return *singleton;
     101             : }
     102             : 
     103             : // The child singleton is available in all processes, including the parent, and
     104             : // is used for scripts which are expected to be loaded into child processes
     105             : // (such as process and frame scripts), or scripts that have already been loaded
     106             : // into a child. The child caches are managed as follows:
     107             : //
     108             : // - Every startup, we open the cache file from the last session, move it to a
     109             : //  new location, and begin pre-loading the scripts that are stored in it. There
     110             : //  is a separate cache file for parent and content processes, but the parent
     111             : //  process opens both the parent and content cache files.
     112             : //
     113             : // - Once startup is complete, we write a new cache file for the next session,
     114             : //   containing only the scripts that were used during early startup, so we don't
     115             : //   waste pre-loading scripts that may not be needed.
     116             : //
     117             : // - For content processes, opening and writing the cache file is handled in the
     118             : //  parent process. The first content process of each type sends back the data
     119             : //  for scripts that were loaded in early startup, and the parent merges them and
     120             : //  writes them to a cache file.
     121             : //
     122             : // - Currently, content processes only benefit from the cache data written
     123             : //  during the *previous* session. Ideally, new content processes should probably
     124             : //  use the cache data written during this session if there was no previous cache
     125             : //  file, but I'd rather do that as a follow-up.
     126             : ScriptPreloader&
     127          94 : ScriptPreloader::GetChildSingleton()
     128             : {
     129          94 :     static RefPtr<ScriptPreloader> singleton;
     130             : 
     131          94 :     if (!singleton) {
     132           3 :         singleton = new ScriptPreloader();
     133           3 :         if (XRE_IsParentProcess()) {
     134           1 :             Unused << singleton->InitCache(NS_LITERAL_STRING("scriptCache-child"));
     135             :         }
     136           3 :         ClearOnShutdown(&singleton);
     137             :     }
     138             : 
     139          94 :     return *singleton;
     140             : }
     141             : 
     142             : void
     143           2 : ScriptPreloader::InitContentChild(ContentParent& parent)
     144             : {
     145           2 :     auto& cache = GetChildSingleton();
     146             : 
     147             :     // We want startup script data from the first process of a given type.
     148             :     // That process sends back its script data before it executes any
     149             :     // untrusted code, and then we never accept further script data for that
     150             :     // type of process for the rest of the session.
     151             :     //
     152             :     // The script data from each process type is merged with the data from the
     153             :     // parent process's frame and process scripts, and shared between all
     154             :     // content process types in the next session.
     155             :     //
     156             :     // Note that if the first process of a given type crashes or shuts down
     157             :     // before sending us its script data, we silently ignore it, and data for
     158             :     // that process type is not included in the next session's cache. This
     159             :     // should be a sufficiently rare occurrence that it's not worth trying to
     160             :     // handle specially.
     161           2 :     auto processType = GetChildProcessType(parent.GetRemoteType());
     162           2 :     bool wantScriptData = !cache.mInitializedProcesses.contains(processType);
     163           2 :     cache.mInitializedProcesses += processType;
     164             : 
     165           4 :     auto fd = cache.mCacheData.cloneFileDescriptor();
     166             :     // Don't send original cache data to new processes if the cache has been
     167             :     // invalidated.
     168           2 :     if (fd.IsValid() && !cache.mCacheInvalidated) {
     169           2 :         Unused << parent.SendPScriptCacheConstructor(fd, wantScriptData);
     170             :     } else {
     171           0 :         Unused << parent.SendPScriptCacheConstructor(NS_ERROR_FILE_NOT_FOUND, wantScriptData);
     172             :     }
     173           2 : }
     174             : 
     175             : ProcessType
     176           5 : ScriptPreloader::GetChildProcessType(const nsAString& remoteType)
     177             : {
     178           5 :     if (remoteType.EqualsLiteral(EXTENSION_REMOTE_TYPE)) {
     179           0 :         return ProcessType::Extension;
     180             :     }
     181           5 :     return ProcessType::Web;
     182             : }
     183             : 
     184             : 
     185             : namespace {
     186             : 
     187             : static void
     188           2 : TraceOp(JSTracer* trc, void* data)
     189             : {
     190           2 :     auto preloader = static_cast<ScriptPreloader*>(data);
     191             : 
     192           2 :     preloader->Trace(trc);
     193           2 : }
     194             : 
     195             : } // anonymous namespace
     196             : 
     197             : void
     198           2 : ScriptPreloader::Trace(JSTracer* trc)
     199             : {
     200         247 :     for (auto& script : IterHash(mScripts)) {
     201         245 :         JS::TraceEdge(trc, &script->mScript, "ScriptPreloader::CachedScript.mScript");
     202             :     }
     203           2 : }
     204             : 
     205             : 
     206           4 : ScriptPreloader::ScriptPreloader()
     207             :   : mMonitor("[ScriptPreloader.mMonitor]")
     208           4 :   , mSaveMonitor("[ScriptPreloader.mSaveMonitor]")
     209             : {
     210           4 :     if (XRE_IsParentProcess()) {
     211           2 :         sProcessType = ProcessType::Parent;
     212             :     } else {
     213           2 :         sProcessType = GetChildProcessType(dom::ContentChild::GetSingleton()->GetRemoteType());
     214             :     }
     215             : 
     216           8 :     nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
     217           4 :     MOZ_RELEASE_ASSERT(obs);
     218             : 
     219           4 :     if (XRE_IsParentProcess()) {
     220             :         // In the parent process, we want to freeze the script cache as soon
     221             :         // as delayed startup for the first browser window has completed.
     222           2 :         obs->AddObserver(this, DELAYED_STARTUP_TOPIC, false);
     223             :     } else {
     224             :         // In the child process, we need to freeze the script cache before any
     225             :         // untrusted code has been executed. The insertion of the first DOM
     226             :         // document element may sometimes be earlier than is ideal, but at
     227             :         // least it should always be safe.
     228           2 :         obs->AddObserver(this, DOC_ELEM_INSERTED_TOPIC, false);
     229             :     }
     230           4 :     obs->AddObserver(this, SHUTDOWN_TOPIC, false);
     231           4 :     obs->AddObserver(this, CLEANUP_TOPIC, false);
     232           4 :     obs->AddObserver(this, CACHE_INVALIDATE_TOPIC, false);
     233             : 
     234           8 :     AutoSafeJSAPI jsapi;
     235           4 :     JS_AddExtraGCRootsTracer(jsapi.cx(), TraceOp, this);
     236           4 : }
     237             : 
     238             : void
     239           0 : ScriptPreloader::ForceWriteCacheFile()
     240             : {
     241           0 :     if (mSaveThread) {
     242           0 :         MonitorAutoLock mal(mSaveMonitor);
     243             : 
     244             :         // Make sure we've prepared scripts, so we don't risk deadlocking while
     245             :         // dispatching the prepare task during shutdown.
     246           0 :         PrepareCacheWrite();
     247             : 
     248             :         // Unblock the save thread, so it can start saving before we get to
     249             :         // XPCOM shutdown.
     250           0 :         mal.Notify();
     251             :     }
     252           0 : }
     253             : 
     254             : void
     255           0 : ScriptPreloader::Cleanup()
     256             : {
     257           0 :     if (mSaveThread) {
     258           0 :         MonitorAutoLock mal(mSaveMonitor);
     259             : 
     260             :         // Make sure the save thread is not blocked dispatching a sync task to
     261             :         // the main thread, or we will deadlock.
     262           0 :         MOZ_RELEASE_ASSERT(!mBlockedOnSyncDispatch);
     263             : 
     264           0 :         while (!mSaveComplete && mSaveThread) {
     265           0 :             mal.Wait();
     266             :         }
     267             :     }
     268             : 
     269           0 :     mScripts.Clear();
     270             : 
     271           0 :     AutoSafeJSAPI jsapi;
     272           0 :     JS_RemoveExtraGCRootsTracer(jsapi.cx(), TraceOp, this);
     273             : 
     274           0 :     UnregisterWeakMemoryReporter(this);
     275           0 : }
     276             : 
     277             : void
     278           0 : ScriptPreloader::InvalidateCache()
     279             : {
     280           0 :     mMonitor.AssertNotCurrentThreadOwns();
     281           0 :     MonitorAutoLock mal(mMonitor);
     282             : 
     283           0 :     mCacheInvalidated = true;
     284             : 
     285           0 :     mParsingScripts.clearAndFree();
     286           0 :     while (auto script = mPendingScripts.getFirst())
     287           0 :         script->remove();
     288           0 :     for (auto& script : IterHash(mScripts))
     289           0 :         script.Remove();
     290             : 
     291             :     // If we've already finished saving the cache at this point, start a new
     292             :     // delayed save operation. This will write out an empty cache file in place
     293             :     // of any cache file we've already written out this session, which will
     294             :     // prevent us from falling back to the current session's cache file on the
     295             :     // next startup.
     296           0 :     if (mSaveComplete && mChildCache) {
     297           0 :         mSaveComplete = false;
     298             : 
     299             :         // Make sure scripts are prepared to avoid deadlock when invalidating
     300             :         // the cache during shutdown.
     301           0 :         PrepareCacheWriteInternal();
     302             : 
     303           0 :         Unused << NS_NewNamedThread("SaveScripts",
     304           0 :                                     getter_AddRefs(mSaveThread), this);
     305             :     }
     306           0 : }
     307             : 
     308             : nsresult
     309           3 : ScriptPreloader::Observe(nsISupports* subject, const char* topic, const char16_t* data)
     310             : {
     311           6 :     nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
     312           3 :     if (!strcmp(topic, DELAYED_STARTUP_TOPIC)) {
     313           2 :         obs->RemoveObserver(this, DELAYED_STARTUP_TOPIC);
     314             : 
     315           2 :         MOZ_ASSERT(XRE_IsParentProcess());
     316             : 
     317           2 :         mStartupFinished = true;
     318             : 
     319           2 :         if (mChildCache) {
     320           2 :             Unused << NS_NewNamedThread("SaveScripts",
     321           2 :                                         getter_AddRefs(mSaveThread), this);
     322             :         }
     323           1 :     } else if (!strcmp(topic, DOC_ELEM_INSERTED_TOPIC)) {
     324           1 :         obs->RemoveObserver(this, DOC_ELEM_INSERTED_TOPIC);
     325             : 
     326           1 :         MOZ_ASSERT(XRE_IsContentProcess());
     327             : 
     328           1 :         mStartupFinished = true;
     329             : 
     330           1 :         if (mChildActor) {
     331           1 :             mChildActor->SendScriptsAndFinalize(mScripts);
     332             :         }
     333           0 :     } else if (!strcmp(topic, SHUTDOWN_TOPIC)) {
     334           0 :         ForceWriteCacheFile();
     335           0 :     } else if (!strcmp(topic, CLEANUP_TOPIC)) {
     336           0 :         Cleanup();
     337           0 :     } else if (!strcmp(topic, CACHE_INVALIDATE_TOPIC)) {
     338           0 :         InvalidateCache();
     339             :     }
     340             : 
     341           6 :     return NS_OK;
     342             : }
     343             : 
     344             : 
     345             : Result<nsCOMPtr<nsIFile>, nsresult>
     346           2 : ScriptPreloader::GetCacheFile(const nsAString& suffix)
     347             : {
     348           4 :     nsCOMPtr<nsIFile> cacheFile;
     349           2 :     NS_TRY(mProfD->Clone(getter_AddRefs(cacheFile)));
     350             : 
     351           2 :     NS_TRY(cacheFile->AppendNative(NS_LITERAL_CSTRING("startupCache")));
     352           2 :     Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777);
     353             : 
     354           2 :     NS_TRY(cacheFile->Append(mBaseName + suffix));
     355             : 
     356           2 :     return Move(cacheFile);
     357             : }
     358             : 
     359             : static const uint8_t MAGIC[] = "mozXDRcachev001";
     360             : 
     361             : Result<Ok, nsresult>
     362           2 : ScriptPreloader::OpenCache()
     363             : {
     364           2 :     NS_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
     365             : 
     366           4 :     nsCOMPtr<nsIFile> cacheFile;
     367           2 :     MOZ_TRY_VAR(cacheFile, GetCacheFile(NS_LITERAL_STRING(".bin")));
     368             : 
     369             :     bool exists;
     370           2 :     NS_TRY(cacheFile->Exists(&exists));
     371           2 :     if (exists) {
     372           2 :         NS_TRY(cacheFile->MoveTo(nullptr, mBaseName + NS_LITERAL_STRING("-current.bin")));
     373             :     } else {
     374           0 :         NS_TRY(cacheFile->SetLeafName(mBaseName + NS_LITERAL_STRING("-current.bin")));
     375           0 :         NS_TRY(cacheFile->Exists(&exists));
     376           0 :         if (!exists) {
     377           0 :             return Err(NS_ERROR_FILE_NOT_FOUND);
     378             :         }
     379             :     }
     380             : 
     381           2 :     MOZ_TRY(mCacheData.init(cacheFile));
     382             : 
     383           2 :     return Ok();
     384             : }
     385             : 
     386             : // Opens the script cache file for this session, and initializes the script
     387             : // cache based on its contents. See WriteCache for details of the cache file.
     388             : Result<Ok, nsresult>
     389           2 : ScriptPreloader::InitCache(const nsAString& basePath)
     390             : {
     391           2 :     mCacheInitialized = true;
     392           2 :     mBaseName = basePath;
     393             : 
     394           2 :     RegisterWeakMemoryReporter(this);
     395             : 
     396           2 :     if (!XRE_IsParentProcess()) {
     397           0 :         return Ok();
     398             :     }
     399             : 
     400           2 :     MOZ_TRY(OpenCache());
     401             : 
     402           2 :     return InitCacheInternal();
     403             : }
     404             : 
     405             : Result<Ok, nsresult>
     406           2 : ScriptPreloader::InitCache(const Maybe<ipc::FileDescriptor>& cacheFile, ScriptCacheChild* cacheChild)
     407             : {
     408           2 :     MOZ_ASSERT(XRE_IsContentProcess());
     409             : 
     410           2 :     mCacheInitialized = true;
     411           2 :     mChildActor = cacheChild;
     412             : 
     413           2 :     RegisterWeakMemoryReporter(this);
     414             : 
     415           2 :     if (cacheFile.isNothing()){
     416           0 :         return Ok();
     417             :     }
     418             : 
     419           2 :     MOZ_TRY(mCacheData.init(cacheFile.ref()));
     420             : 
     421           2 :     return InitCacheInternal();
     422             : }
     423             : 
     424             : Result<Ok, nsresult>
     425           4 : ScriptPreloader::InitCacheInternal()
     426             : {
     427           4 :     auto size = mCacheData.size();
     428             : 
     429             :     uint32_t headerSize;
     430           4 :     if (size < sizeof(MAGIC) + sizeof(headerSize)) {
     431           0 :         return Err(NS_ERROR_UNEXPECTED);
     432             :     }
     433             : 
     434           4 :     auto data = mCacheData.get<uint8_t>();
     435           4 :     auto end = data + size;
     436             : 
     437           4 :     if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) {
     438           0 :         return Err(NS_ERROR_UNEXPECTED);
     439             :     }
     440           4 :     data += sizeof(MAGIC);
     441             : 
     442           4 :     headerSize = LittleEndian::readUint32(data.get());
     443           4 :     data += sizeof(headerSize);
     444             : 
     445           4 :     if (data + headerSize > end) {
     446           0 :         return Err(NS_ERROR_UNEXPECTED);
     447             :     }
     448             : 
     449             :     {
     450           0 :         auto cleanup = MakeScopeExit([&] () {
     451           0 :             mScripts.Clear();
     452           8 :         });
     453             : 
     454           8 :         LinkedList<CachedScript> scripts;
     455             : 
     456           4 :         Range<uint8_t> header(data, data + headerSize);
     457           4 :         data += headerSize;
     458             : 
     459           4 :         InputBuffer buf(header);
     460             : 
     461           4 :         size_t offset = 0;
     462         412 :         while (!buf.finished()) {
     463         408 :             auto script = MakeUnique<CachedScript>(*this, buf);
     464         204 :             MOZ_RELEASE_ASSERT(script);
     465             : 
     466         204 :             auto scriptData = data + script->mOffset;
     467         204 :             if (scriptData + script->mSize > end) {
     468           0 :                 return Err(NS_ERROR_UNEXPECTED);
     469             :             }
     470             : 
     471             :             // Make sure offsets match what we'd expect based on script ordering and
     472             :             // size, as a basic sanity check.
     473         204 :             if (script->mOffset != offset) {
     474           0 :                 return Err(NS_ERROR_UNEXPECTED);
     475             :             }
     476         204 :             offset += script->mSize;
     477             : 
     478         204 :             script->mXDRRange.emplace(scriptData, scriptData + script->mSize);
     479             : 
     480             :             // Don't pre-decode the script unless it was used in this process type during the
     481             :             // previous session.
     482         204 :             if (script->mOriginalProcessTypes.contains(CurrentProcessType())) {
     483         189 :                 scripts.insertBack(script.get());
     484             :             } else {
     485          15 :                 script->mReadyToExecute = true;
     486             :             }
     487             : 
     488         204 :             mScripts.Put(script->mCachePath, script.get());
     489         204 :             Unused << script.release();
     490             :         }
     491             : 
     492           4 :         if (buf.error()) {
     493           0 :             return Err(NS_ERROR_UNEXPECTED);
     494             :         }
     495             : 
     496           4 :         mPendingScripts = Move(scripts);
     497           4 :         cleanup.release();
     498             :     }
     499             : 
     500           4 :     DecodeNextBatch(OFF_THREAD_FIRST_CHUNK_SIZE);
     501           4 :     return Ok();
     502             : }
     503             : 
     504             : static inline Result<Ok, nsresult>
     505           0 : Write(PRFileDesc* fd, const void* data, int32_t len)
     506             : {
     507           0 :     if (PR_Write(fd, data, len) != len) {
     508           0 :         return Err(NS_ERROR_FAILURE);
     509             :     }
     510           0 :     return Ok();
     511             : }
     512             : 
     513             : void
     514           0 : ScriptPreloader::PrepareCacheWriteInternal()
     515             : {
     516           0 :     MOZ_ASSERT(NS_IsMainThread());
     517             : 
     518           0 :     mMonitor.AssertCurrentThreadOwns();
     519             : 
     520           0 :     auto cleanup = MakeScopeExit([&] () {
     521           0 :         if (mChildCache) {
     522           0 :             mChildCache->PrepareCacheWrite();
     523             :         }
     524           0 :     });
     525             : 
     526           0 :     if (mDataPrepared) {
     527           0 :         return;
     528             :     }
     529             : 
     530           0 :     AutoSafeJSAPI jsapi;
     531           0 :     bool found = false;
     532           0 :     for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) {
     533             :         // Don't write any scripts that are also in the child cache. They'll be
     534             :         // loaded from the child cache in that case, so there's no need to write
     535             :         // them twice.
     536           0 :         CachedScript* childScript = mChildCache ? mChildCache->mScripts.Get(script->mCachePath) : nullptr;
     537           0 :         if (childScript && !childScript->mProcessTypes.isEmpty()) {
     538           0 :             childScript->UpdateLoadTime(script->mLoadTime);
     539           0 :             childScript->mProcessTypes += script->mProcessTypes;
     540           0 :             script.Remove();
     541           0 :             continue;
     542             :         }
     543             : 
     544           0 :         if (!(script->mProcessTypes == script->mOriginalProcessTypes)) {
     545             :             // Note: EnumSet doesn't support operator!=, hence the weird form above.
     546           0 :             found = true;
     547             :         }
     548             : 
     549           0 :         if (!script->mSize && !script->XDREncode(jsapi.cx())) {
     550           0 :             script.Remove();
     551             :         } else {
     552           0 :             script->mSize = script->Range().length();
     553             :         }
     554             :     }
     555             : 
     556           0 :     if (!found) {
     557           0 :         mSaveComplete = true;
     558           0 :         return;
     559             :     }
     560             : 
     561           0 :     mDataPrepared = true;
     562             : }
     563             : 
     564             : void
     565           0 : ScriptPreloader::PrepareCacheWrite()
     566             : {
     567           0 :     MonitorAutoLock mal(mMonitor);
     568             : 
     569           0 :     PrepareCacheWriteInternal();
     570           0 : }
     571             : 
     572             : // Writes out a script cache file for the scripts accessed during early
     573             : // startup in this session. The cache file is a little-endian binary file with
     574             : // the following format:
     575             : //
     576             : // - A uint32 containing the size of the header block.
     577             : //
     578             : // - A header entry for each file stored in the cache containing:
     579             : //   - The URL that the script was originally read from.
     580             : //   - Its cache key.
     581             : //   - The offset of its XDR data within the XDR data block.
     582             : //   - The size of its XDR data in the XDR data block.
     583             : //   - A bit field describing which process types the script is used in.
     584             : //
     585             : // - A block of XDR data for the encoded scripts, with each script's data at
     586             : //   an offset from the start of the block, as specified above.
     587             : Result<Ok, nsresult>
     588           0 : ScriptPreloader::WriteCache()
     589             : {
     590           0 :     MOZ_ASSERT(!NS_IsMainThread());
     591             : 
     592           0 :     if (!mDataPrepared && !mSaveComplete) {
     593           0 :         MOZ_ASSERT(!mBlockedOnSyncDispatch);
     594           0 :         mBlockedOnSyncDispatch = true;
     595             : 
     596           0 :         MonitorAutoUnlock mau(mSaveMonitor);
     597             : 
     598           0 :         NS_DispatchToMainThread(
     599           0 :           NewRunnableMethod("ScriptPreloader::PrepareCacheWrite",
     600             :                             this,
     601             :                             &ScriptPreloader::PrepareCacheWrite),
     602           0 :           NS_DISPATCH_SYNC);
     603             :     }
     604             : 
     605           0 :     mBlockedOnSyncDispatch = false;
     606             : 
     607           0 :     if (mSaveComplete) {
     608             :         // If we don't have anything we need to save, we're done.
     609           0 :         return Ok();
     610             :     }
     611             : 
     612           0 :     nsCOMPtr<nsIFile> cacheFile;
     613           0 :     MOZ_TRY_VAR(cacheFile, GetCacheFile(NS_LITERAL_STRING("-new.bin")));
     614             : 
     615             :     bool exists;
     616           0 :     NS_TRY(cacheFile->Exists(&exists));
     617           0 :     if (exists) {
     618           0 :         NS_TRY(cacheFile->Remove(false));
     619             :     }
     620             : 
     621             :     {
     622           0 :         AutoFDClose fd;
     623           0 :         NS_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, &fd.rwget()));
     624             : 
     625           0 :         nsTArray<CachedScript*> scripts;
     626           0 :         for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) {
     627           0 :             scripts.AppendElement(script);
     628             :         }
     629             : 
     630             :         // Sort scripts by load time, with async loaded scripts before sync scripts.
     631             :         // Since async scripts are always loaded immediately at startup, it helps to
     632             :         // have them stored contiguously.
     633           0 :         scripts.Sort(CachedScript::Comparator());
     634             : 
     635           0 :         OutputBuffer buf;
     636           0 :         size_t offset = 0;
     637           0 :         for (auto script : scripts) {
     638           0 :             script->mOffset = offset;
     639           0 :             script->Code(buf);
     640             : 
     641           0 :             offset += script->mSize;
     642             :         }
     643             : 
     644             :         uint8_t headerSize[4];
     645           0 :         LittleEndian::writeUint32(headerSize, buf.cursor());
     646             : 
     647           0 :         MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
     648           0 :         MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
     649           0 :         MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
     650           0 :         for (auto script : scripts) {
     651           0 :             MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));
     652             : 
     653           0 :             if (script->mScript) {
     654           0 :                 script->FreeData();
     655             :             }
     656             :         }
     657             :     }
     658             : 
     659           0 :     NS_TRY(cacheFile->MoveTo(nullptr, mBaseName + NS_LITERAL_STRING(".bin")));
     660             : 
     661           0 :     return Ok();
     662             : }
     663             : 
     664             : // Runs in the mSaveThread thread, and writes out the cache file for the next
     665             : // session after a reasonable delay.
     666             : nsresult
     667           1 : ScriptPreloader::Run()
     668             : {
     669           1 :     MonitorAutoLock mal(mSaveMonitor);
     670             : 
     671             :     // Ideally wait about 10 seconds before saving, to avoid unnecessary IO
     672             :     // during early startup. But only if the cache hasn't been invalidated,
     673             :     // since that can trigger a new write during shutdown, and we don't want to
     674             :     // cause shutdown hangs.
     675           1 :     if (!mCacheInvalidated) {
     676           1 :         mal.Wait(10000);
     677             :     }
     678             : 
     679           0 :     auto result = WriteCache();
     680           0 :     Unused << NS_WARN_IF(result.isErr());
     681             : 
     682           0 :     result = mChildCache->WriteCache();
     683           0 :     Unused << NS_WARN_IF(result.isErr());
     684             : 
     685           0 :     mSaveComplete = true;
     686           0 :     NS_ReleaseOnMainThread("ScriptPreloader::mSaveThread", mSaveThread.forget());
     687             : 
     688           0 :     mal.NotifyAll();
     689           0 :     return NS_OK;
     690             : }
     691             : 
     692             : void
     693         327 : ScriptPreloader::NoteScript(const nsCString& url, const nsCString& cachePath,
     694             :                             JS::HandleScript jsscript)
     695             : {
     696             :     // Don't bother trying to cache any URLs with cache-busting query
     697             :     // parameters.
     698         327 :     if (mStartupFinished || !mCacheInitialized || cachePath.FindChar('?') >= 0) {
     699         138 :         return;
     700             :     }
     701             : 
     702             :     // Don't bother caching files that belong to the mochitest harness.
     703         258 :     NS_NAMED_LITERAL_CSTRING(mochikitPrefix, "chrome://mochikit/");
     704         258 :     if (StringHead(url, mochikitPrefix.Length()) == mochikitPrefix) {
     705           0 :         return;
     706             :     }
     707             : 
     708         258 :     auto script = mScripts.LookupOrAdd(cachePath, *this, url, cachePath, jsscript);
     709             : 
     710         258 :     if (!script->mScript) {
     711          14 :         MOZ_ASSERT(jsscript);
     712          14 :         script->mScript = jsscript;
     713          14 :         script->mReadyToExecute = true;
     714             :     }
     715             : 
     716         258 :     script->UpdateLoadTime(TimeStamp::Now());
     717         258 :     script->mProcessTypes += CurrentProcessType();
     718             : }
     719             : 
     720             : void
     721          42 : ScriptPreloader::NoteScript(const nsCString& url, const nsCString& cachePath,
     722             :                             ProcessType processType, nsTArray<uint8_t>&& xdrData,
     723             :                             TimeStamp loadTime)
     724             : {
     725          42 :     auto script = mScripts.LookupOrAdd(cachePath, *this, url, cachePath, nullptr);
     726             : 
     727          42 :     if (!script->HasRange()) {
     728          18 :         MOZ_ASSERT(!script->HasArray());
     729             : 
     730          18 :         script->mSize = xdrData.Length();
     731          18 :         script->mXDRData.construct<nsTArray<uint8_t>>(Forward<nsTArray<uint8_t>>(xdrData));
     732             : 
     733          18 :         auto& data = script->Array();
     734          18 :         script->mXDRRange.emplace(data.Elements(), data.Length());
     735             :     }
     736             : 
     737          42 :     if (!script->mSize && !script->mScript) {
     738             :         // If the content process is sending us a script entry for a script
     739             :         // which was in the cache at startup, it expects us to already have this
     740             :         // script data, so it doesn't send it.
     741             :         //
     742             :         // However, the cache may have been invalidated at this point (usually
     743             :         // due to the add-on manager installing or uninstalling a legacy
     744             :         // extension during very early startup), which means we may no longer
     745             :         // have an entry for this script. Since that means we have no data to
     746             :         // write to the new cache, and no JSScript to generate it from, we need
     747             :         // to discard this entry.
     748           0 :         mScripts.Remove(cachePath);
     749           0 :         return;
     750             :     }
     751             : 
     752          42 :     script->UpdateLoadTime(loadTime);
     753          42 :     script->mProcessTypes += processType;
     754             : }
     755             : 
     756             : JSScript*
     757         551 : ScriptPreloader::GetCachedScript(JSContext* cx, const nsCString& path)
     758             : {
     759             :     // If a script is used by both the parent and the child, it's stored only
     760             :     // in the child cache.
     761         551 :     if (mChildCache) {
     762         218 :         auto script = mChildCache->GetCachedScript(cx, path);
     763         218 :         if (script) {
     764           6 :             return script;
     765             :         }
     766             :     }
     767             : 
     768         545 :     auto script = mScripts.Get(path);
     769         545 :     if (script) {
     770         183 :         return WaitForCachedScript(cx, script);
     771             :     }
     772             : 
     773         362 :     return nullptr;
     774             : }
     775             : 
     776             : JSScript*
     777         183 : ScriptPreloader::WaitForCachedScript(JSContext* cx, CachedScript* script)
     778             : {
     779             :     // Check for finished operations before locking so that we can move onto
     780             :     // decoding the next batch as soon as possible after the pending batch is
     781             :     // ready. If we wait until we hit an unfinished script, we wind up having at
     782             :     // most one batch of buffered scripts, and occasionally under-running that
     783             :     // buffer.
     784         183 :     FinishOffThreadDecode();
     785             : 
     786         183 :     if (!script->mReadyToExecute) {
     787           7 :         LOG(Info, "Must wait for async script load: %s\n", script->mURL.get());
     788           7 :         auto start = TimeStamp::Now();
     789             : 
     790           7 :         mMonitor.AssertNotCurrentThreadOwns();
     791          14 :         MonitorAutoLock mal(mMonitor);
     792             : 
     793             :         // Check for finished operations again *after* locking, or we may race
     794             :         // against mToken being set between our last check and the time we
     795             :         // entered the mutex.
     796           7 :         FinishOffThreadDecode();
     797             : 
     798           7 :         if (!script->mReadyToExecute && script->mSize < MAX_MAINTHREAD_DECODE_SIZE) {
     799           6 :             LOG(Info, "Script is small enough to recompile on main thread\n");
     800             : 
     801           6 :             script->mReadyToExecute = true;
     802             :         } else {
     803           3 :             while (!script->mReadyToExecute) {
     804           1 :                 mal.Wait();
     805             : 
     806           2 :                 MonitorAutoUnlock mau(mMonitor);
     807           1 :                 FinishOffThreadDecode();
     808             :             }
     809             :         }
     810             : 
     811           7 :         LOG(Debug, "Waited %fms\n", (TimeStamp::Now() - start).ToMilliseconds());
     812             :     }
     813             : 
     814         183 :     return script->GetJSScript(cx);
     815             : }
     816             : 
     817             : 
     818             : 
     819             : /* static */ void
     820          16 : ScriptPreloader::OffThreadDecodeCallback(void* token, void* context)
     821             : {
     822          16 :     auto cache = static_cast<ScriptPreloader*>(context);
     823             : 
     824          16 :     cache->mMonitor.AssertNotCurrentThreadOwns();
     825          32 :     MonitorAutoLock mal(cache->mMonitor);
     826             : 
     827             :     // First notify any tasks that are already waiting on scripts, since they'll
     828             :     // be blocking the main thread, and prevent any runnables from executing.
     829          16 :     cache->mToken = token;
     830          16 :     mal.NotifyAll();
     831             : 
     832             :     // If nothing processed the token, and we don't already have a pending
     833             :     // runnable, then dispatch a new one to finish the processing on the main
     834             :     // thread as soon as possible.
     835          16 :     if (cache->mToken && !cache->mFinishDecodeRunnablePending) {
     836           4 :         cache->mFinishDecodeRunnablePending = true;
     837           8 :         NS_DispatchToMainThread(
     838           8 :           NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode",
     839             :                             cache,
     840           4 :                             &ScriptPreloader::DoFinishOffThreadDecode));
     841             :     }
     842          16 : }
     843             : 
     844             : void
     845           3 : ScriptPreloader::DoFinishOffThreadDecode()
     846             : {
     847           3 :     mFinishDecodeRunnablePending = false;
     848           3 :     FinishOffThreadDecode();
     849           3 : }
     850             : 
     851             : void
     852         194 : ScriptPreloader::FinishOffThreadDecode()
     853             : {
     854         194 :     if (!mToken) {
     855         178 :         return;
     856             :     }
     857             : 
     858          16 :     auto cleanup = MakeScopeExit([&] () {
     859          48 :         mToken = nullptr;
     860          16 :         mParsingSources.clear();
     861          16 :         mParsingScripts.clear();
     862             : 
     863          16 :         DecodeNextBatch(OFF_THREAD_CHUNK_SIZE);
     864          48 :     });
     865             : 
     866          32 :     AutoJSAPI jsapi;
     867          16 :     MOZ_RELEASE_ASSERT(jsapi.Init(xpc::CompilationScope()));
     868             : 
     869          16 :     JSContext* cx = jsapi.cx();
     870          32 :     JS::Rooted<JS::ScriptVector> jsScripts(cx, JS::ScriptVector(cx));
     871             : 
     872             :     // If this fails, we still need to mark the scripts as finished. Any that
     873             :     // weren't successfully compiled in this operation (which should never
     874             :     // happen under ordinary circumstances) will be re-decoded on the main
     875             :     // thread, and raise the appropriate errors when they're executed.
     876             :     //
     877             :     // The exception from the off-thread decode operation will be reported when
     878             :     // we pop the AutoJSAPI off the stack.
     879          16 :     Unused << JS::FinishMultiOffThreadScriptsDecoder(cx, mToken, &jsScripts);
     880             : 
     881          16 :     unsigned i = 0;
     882         203 :     for (auto script : mParsingScripts) {
     883         187 :         LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get());
     884         187 :         if (i < jsScripts.length())
     885         187 :             script->mScript = jsScripts[i++];
     886         187 :         script->mReadyToExecute = true;
     887             :     }
     888             : }
     889             : 
     890             : void
     891          20 : ScriptPreloader::DecodeNextBatch(size_t chunkSize)
     892             : {
     893          20 :     MOZ_ASSERT(mParsingSources.length() == 0);
     894          20 :     MOZ_ASSERT(mParsingScripts.length() == 0);
     895             : 
     896           4 :     auto cleanup = MakeScopeExit([&] () {
     897           4 :         mParsingScripts.clearAndFree();
     898           4 :         mParsingSources.clearAndFree();
     899          40 :     });
     900             : 
     901          20 :     auto start = TimeStamp::Now();
     902          20 :     LOG(Debug, "Off-thread decoding scripts...\n");
     903             : 
     904          20 :     size_t size = 0;
     905         209 :     for (CachedScript* next = mPendingScripts.getFirst(); next;) {
     906         201 :         auto script = next;
     907         201 :         next = script->getNext();
     908             : 
     909             :         // Skip any scripts that we decoded on the main thread rather than
     910             :         // waiting for an off-thread operation to complete.
     911         201 :         if (script->mReadyToExecute) {
     912           2 :             script->remove();
     913           2 :             continue;
     914             :         }
     915             :         // If we have enough data for one chunk and this script would put us
     916             :         // over our chunk size limit, we're done.
     917         340 :         if (size > SMALL_SCRIPT_CHUNK_THRESHOLD &&
     918         141 :             size + script->mSize > chunkSize) {
     919          24 :             break;
     920             :         }
     921         935 :         if (!mParsingScripts.append(script) ||
     922         935 :             !mParsingSources.emplaceBack(script->Range(), script->mURL.get(), 0)) {
     923           0 :             break;
     924             :         }
     925             : 
     926         187 :         LOG(Debug, "Beginning off-thread decode of script %s (%u bytes)\n",
     927             :             script->mURL.get(), script->mSize);
     928             : 
     929         187 :         script->remove();
     930         187 :         size += script->mSize;
     931             :     }
     932             : 
     933          20 :     if (size == 0 && mPendingScripts.isEmpty()) {
     934           4 :         return;
     935             :     }
     936             : 
     937          32 :     AutoJSAPI jsapi;
     938          16 :     MOZ_RELEASE_ASSERT(jsapi.Init(xpc::CompilationScope()));
     939          16 :     JSContext* cx = jsapi.cx();
     940             : 
     941          32 :     JS::CompileOptions options(cx, JSVERSION_LATEST);
     942          16 :     options.setNoScriptRval(true);
     943             : 
     944          32 :     if (!JS::CanCompileOffThread(cx, options, size) ||
     945          16 :         !JS::DecodeMultiOffThreadScripts(cx, options, mParsingSources,
     946             :                                          OffThreadDecodeCallback,
     947             :                                          static_cast<void*>(this))) {
     948             :         // If we fail here, we don't move on to process the next batch, so make
     949             :         // sure we don't have any other scripts left to process.
     950           0 :         MOZ_ASSERT(mPendingScripts.isEmpty());
     951           0 :         for (auto script : mPendingScripts) {
     952           0 :             script->mReadyToExecute = true;
     953             :         }
     954             : 
     955           0 :         LOG(Info, "Can't decode %lu bytes of scripts off-thread", (unsigned long)size);
     956           0 :         for (auto script : mParsingScripts) {
     957           0 :             script->mReadyToExecute = true;
     958             :         }
     959           0 :         return;
     960             :     }
     961             : 
     962          16 :     cleanup.release();
     963             : 
     964          16 :     LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n",
     965             :         (unsigned)mParsingSources.length(), (unsigned)size, (TimeStamp::Now() - start).ToMilliseconds());
     966             : }
     967             : 
     968             : 
     969         204 : ScriptPreloader::CachedScript::CachedScript(ScriptPreloader& cache, InputBuffer& buf)
     970         204 :     : mCache(cache)
     971             : {
     972         204 :     Code(buf);
     973             : 
     974             :     // Swap the mProcessTypes and mOriginalProcessTypes values, since we want to
     975             :     // start with an empty set of processes loaded into for this session, and
     976             :     // compare against last session's values later.
     977         204 :     mOriginalProcessTypes = mProcessTypes;
     978         204 :     mProcessTypes = {};
     979         204 : }
     980             : 
     981             : bool
     982          18 : ScriptPreloader::CachedScript::XDREncode(JSContext* cx)
     983             : {
     984          36 :     JSAutoCompartment ac(cx, mScript);
     985          36 :     JS::RootedScript jsscript(cx, mScript);
     986             : 
     987          18 :     mXDRData.construct<JS::TranscodeBuffer>();
     988             : 
     989          18 :     JS::TranscodeResult code = JS::EncodeScript(cx, Buffer(), jsscript);
     990          18 :     if (code == JS::TranscodeResult_Ok) {
     991          18 :         mXDRRange.emplace(Buffer().begin(), Buffer().length());
     992          18 :         return true;
     993             :     }
     994           0 :     JS_ClearPendingException(cx);
     995           0 :     return false;
     996             : }
     997             : 
     998             : JSScript*
     999         183 : ScriptPreloader::CachedScript::GetJSScript(JSContext* cx)
    1000             : {
    1001         183 :     MOZ_ASSERT(mReadyToExecute);
    1002         183 :     if (mScript) {
    1003         163 :         return mScript;
    1004             :     }
    1005             : 
    1006             :     // If we have no script at this point, the script was too small to decode
    1007             :     // off-thread, or it was needed before the off-thread compilation was
    1008             :     // finished, and is small enough to decode on the main thread rather than
    1009             :     // wait for the off-thread decoding to finish. In either case, we decode
    1010             :     // it synchronously the first time it's needed.
    1011          20 :     MOZ_ASSERT(HasRange());
    1012             : 
    1013          20 :     auto start = TimeStamp::Now();
    1014          20 :     LOG(Info, "Decoding script %s on main thread...\n", mURL.get());
    1015             : 
    1016          40 :     JS::RootedScript script(cx);
    1017          20 :     if (JS::DecodeScript(cx, Range(), &script)) {
    1018           0 :         mScript = script;
    1019             : 
    1020           0 :         if (mCache.mSaveComplete) {
    1021           0 :             FreeData();
    1022             :         }
    1023             :     }
    1024             : 
    1025          20 :     LOG(Debug, "Finished decoding in %fms", (TimeStamp::Now() - start).ToMilliseconds());
    1026             : 
    1027          20 :     return mScript;
    1028             : }
    1029             : 
    1030          80 : NS_IMPL_ISUPPORTS(ScriptPreloader, nsIObserver, nsIRunnable, nsIMemoryReporter)
    1031             : 
    1032             : #undef LOG
    1033             : 
    1034             : } // namespace mozilla

Generated by: LCOV version 1.13