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
|