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