Source code

Revision control

Other Tools

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