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