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