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 "nsXULPrototypeCache.h"
8
9 #include "plstr.h"
10 #include "nsXULPrototypeDocument.h"
11 #include "nsIURI.h"
12 #include "nsNetUtil.h"
13
14 #include "nsIFile.h"
15 #include "nsIMemoryReporter.h"
16 #include "nsIObjectInputStream.h"
17 #include "nsIObjectOutputStream.h"
18 #include "nsIObserverService.h"
19 #include "nsIStorageStream.h"
20
21 #include "nsAppDirectoryServiceDefs.h"
22
23 #include "js/TracingAPI.h"
24
25 #include "mozilla/StyleSheetInlines.h"
26 #include "mozilla/Preferences.h"
27 #include "mozilla/scache/StartupCache.h"
28 #include "mozilla/scache/StartupCacheUtils.h"
29 #include "mozilla/Telemetry.h"
30 #include "mozilla/intl/LocaleService.h"
31
32 using namespace mozilla;
33 using namespace mozilla::scache;
34 using mozilla::intl::LocaleService;
35
36 #define XUL_CACHE_DISABLED_DEFAULT false
37
38 static bool gDisableXULCache =
39 XUL_CACHE_DISABLED_DEFAULT; // enabled by default
40 static const char kDisableXULCachePref[] = "nglayout.debug.disable_xul_cache";
41 static const char kXULCacheInfoKey[] = "nsXULPrototypeCache.startupCache";
42 static const char kXULCachePrefix[] = "xulcache";
43
44 //----------------------------------------------------------------------
45
UpdategDisableXULCache()46 static void UpdategDisableXULCache() {
47 // Get the value of "nglayout.debug.disable_xul_cache" preference
48 gDisableXULCache =
49 Preferences::GetBool(kDisableXULCachePref, XUL_CACHE_DISABLED_DEFAULT);
50
51 // Sets the flag if the XUL cache is disabled
52 if (gDisableXULCache) {
53 Telemetry::Accumulate(Telemetry::XUL_CACHE_DISABLED, true);
54 }
55 }
56
DisableXULCacheChangedCallback(const char * aPref,void * aClosure)57 static void DisableXULCacheChangedCallback(const char* aPref, void* aClosure) {
58 bool wasEnabled = !gDisableXULCache;
59 UpdategDisableXULCache();
60
61 if (wasEnabled && gDisableXULCache) {
62 nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance();
63 if (cache) {
64 // AbortCaching() calls Flush() for us.
65 cache->AbortCaching();
66 }
67 }
68 }
69
70 //----------------------------------------------------------------------
71
72 nsXULPrototypeCache* nsXULPrototypeCache::sInstance = nullptr;
73
74 nsXULPrototypeCache::nsXULPrototypeCache() = default;
75
~nsXULPrototypeCache()76 nsXULPrototypeCache::~nsXULPrototypeCache() { FlushScripts(); }
77
NS_IMPL_ISUPPORTS(nsXULPrototypeCache,nsIObserver)78 NS_IMPL_ISUPPORTS(nsXULPrototypeCache, nsIObserver)
79
80 /* static */
81 nsXULPrototypeCache* nsXULPrototypeCache::GetInstance() {
82 if (!sInstance) {
83 NS_ADDREF(sInstance = new nsXULPrototypeCache());
84
85 UpdategDisableXULCache();
86
87 Preferences::RegisterCallback(DisableXULCacheChangedCallback,
88 kDisableXULCachePref);
89
90 nsCOMPtr<nsIObserverService> obsSvc =
91 mozilla::services::GetObserverService();
92 if (obsSvc) {
93 nsXULPrototypeCache* p = sInstance;
94 obsSvc->AddObserver(p, "chrome-flush-caches", false);
95 obsSvc->AddObserver(p, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
96 obsSvc->AddObserver(p, "startupcache-invalidate", false);
97 }
98 }
99 return sInstance;
100 }
101
102 //----------------------------------------------------------------------
103
104 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)105 nsXULPrototypeCache::Observe(nsISupports* aSubject, const char* aTopic,
106 const char16_t* aData) {
107 if (!strcmp(aTopic, "chrome-flush-caches") ||
108 !strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
109 Flush();
110 } else if (!strcmp(aTopic, "startupcache-invalidate")) {
111 AbortCaching();
112 } else {
113 NS_WARNING("Unexpected observer topic.");
114 }
115 return NS_OK;
116 }
117
GetPrototype(nsIURI * aURI)118 nsXULPrototypeDocument* nsXULPrototypeCache::GetPrototype(nsIURI* aURI) {
119 if (!aURI) return nullptr;
120
121 nsCOMPtr<nsIURI> uriWithoutRef;
122 NS_GetURIWithoutRef(aURI, getter_AddRefs(uriWithoutRef));
123
124 nsXULPrototypeDocument* protoDoc = mPrototypeTable.GetWeak(uriWithoutRef);
125 if (protoDoc) {
126 return protoDoc;
127 }
128
129 nsresult rv = BeginCaching(aURI);
130 if (NS_FAILED(rv)) return nullptr;
131
132 // No prototype in XUL memory cache. Spin up the cache Service.
133 nsCOMPtr<nsIObjectInputStream> ois;
134 rv = GetInputStream(aURI, getter_AddRefs(ois));
135 if (NS_FAILED(rv)) {
136 return nullptr;
137 }
138
139 RefPtr<nsXULPrototypeDocument> newProto;
140 rv = NS_NewXULPrototypeDocument(getter_AddRefs(newProto));
141 if (NS_FAILED(rv)) return nullptr;
142
143 rv = newProto->Read(ois);
144 if (NS_SUCCEEDED(rv)) {
145 rv = PutPrototype(newProto);
146 } else {
147 newProto = nullptr;
148 }
149
150 mInputStreamTable.Remove(aURI);
151 return newProto;
152 }
153
PutPrototype(nsXULPrototypeDocument * aDocument)154 nsresult nsXULPrototypeCache::PutPrototype(nsXULPrototypeDocument* aDocument) {
155 if (!aDocument->GetURI()) {
156 return NS_ERROR_FAILURE;
157 }
158
159 nsCOMPtr<nsIURI> uri;
160 NS_GetURIWithoutRef(aDocument->GetURI(), getter_AddRefs(uri));
161
162 // Put() releases any old value
163 mPrototypeTable.InsertOrUpdate(uri, RefPtr{aDocument});
164
165 return NS_OK;
166 }
167
GetStyleSheet(nsIURI * aURI)168 mozilla::StyleSheet* nsXULPrototypeCache::GetStyleSheet(nsIURI* aURI) {
169 return mStyleSheetTable.GetWeak(aURI);
170 }
171
PutStyleSheet(RefPtr<StyleSheet> && aStyleSheet)172 nsresult nsXULPrototypeCache::PutStyleSheet(RefPtr<StyleSheet>&& aStyleSheet) {
173 nsIURI* uri = aStyleSheet->GetSheetURI();
174 mStyleSheetTable.InsertOrUpdate(uri, std::move(aStyleSheet));
175 return NS_OK;
176 }
177
GetScript(nsIURI * aURI)178 JSScript* nsXULPrototypeCache::GetScript(nsIURI* aURI) {
179 if (auto* entry = mScriptTable.GetEntry(aURI)) {
180 return entry->mScript.get();
181 }
182 return nullptr;
183 }
184
PutScript(nsIURI * aURI,JS::Handle<JSScript * > aScriptObject)185 nsresult nsXULPrototypeCache::PutScript(nsIURI* aURI,
186 JS::Handle<JSScript*> aScriptObject) {
187 MOZ_ASSERT(aScriptObject, "Need a non-NULL script");
188
189 #ifdef DEBUG_BUG_392650
190 if (mScriptTable.Get(aURI)) {
191 nsAutoCString scriptName;
192 aURI->GetSpec(scriptName);
193 nsAutoCString message("Loaded script ");
194 message += scriptName;
195 message += " twice (bug 392650)";
196 NS_WARNING(message.get());
197 }
198 #endif
199
200 mScriptTable.PutEntry(aURI)->mScript.set(aScriptObject);
201
202 return NS_OK;
203 }
204
FlushScripts()205 void nsXULPrototypeCache::FlushScripts() { mScriptTable.Clear(); }
206
Flush()207 void nsXULPrototypeCache::Flush() {
208 mPrototypeTable.Clear();
209 mScriptTable.Clear();
210 mStyleSheetTable.Clear();
211 }
212
IsEnabled()213 bool nsXULPrototypeCache::IsEnabled() { return !gDisableXULCache; }
214
AbortCaching()215 void nsXULPrototypeCache::AbortCaching() {
216 #ifdef DEBUG_brendan
217 NS_BREAK();
218 #endif
219
220 // Flush the XUL cache for good measure, in case we cached a bogus/downrev
221 // script, somehow.
222 Flush();
223
224 // Clear the cache set
225 mStartupCacheURITable.Clear();
226 }
227
WritePrototype(nsXULPrototypeDocument * aPrototypeDocument)228 nsresult nsXULPrototypeCache::WritePrototype(
229 nsXULPrototypeDocument* aPrototypeDocument) {
230 nsresult rv = NS_OK, rv2 = NS_OK;
231
232 if (!StartupCache::GetSingleton()) return NS_OK;
233
234 nsCOMPtr<nsIURI> protoURI = aPrototypeDocument->GetURI();
235
236 nsCOMPtr<nsIObjectOutputStream> oos;
237 rv = GetOutputStream(protoURI, getter_AddRefs(oos));
238 NS_ENSURE_SUCCESS(rv, rv);
239
240 rv = aPrototypeDocument->Write(oos);
241 NS_ENSURE_SUCCESS(rv, rv);
242 FinishOutputStream(protoURI);
243 return NS_FAILED(rv) ? rv : rv2;
244 }
245
GetInputStream(nsIURI * uri,nsIObjectInputStream ** stream)246 nsresult nsXULPrototypeCache::GetInputStream(nsIURI* uri,
247 nsIObjectInputStream** stream) {
248 nsAutoCString spec(kXULCachePrefix);
249 nsresult rv = PathifyURI(uri, spec);
250 if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
251
252 const char* buf;
253 uint32_t len;
254 nsCOMPtr<nsIObjectInputStream> ois;
255 StartupCache* sc = StartupCache::GetSingleton();
256 if (!sc) return NS_ERROR_NOT_AVAILABLE;
257
258 rv = sc->GetBuffer(spec.get(), &buf, &len);
259 if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
260
261 rv = NewObjectInputStreamFromBuffer(buf, len, getter_AddRefs(ois));
262 NS_ENSURE_SUCCESS(rv, rv);
263
264 mInputStreamTable.InsertOrUpdate(uri, ois);
265
266 ois.forget(stream);
267 return NS_OK;
268 }
269
FinishInputStream(nsIURI * uri)270 nsresult nsXULPrototypeCache::FinishInputStream(nsIURI* uri) {
271 mInputStreamTable.Remove(uri);
272 return NS_OK;
273 }
274
GetOutputStream(nsIURI * uri,nsIObjectOutputStream ** stream)275 nsresult nsXULPrototypeCache::GetOutputStream(nsIURI* uri,
276 nsIObjectOutputStream** stream) {
277 nsresult rv;
278 nsCOMPtr<nsIObjectOutputStream> objectOutput;
279 nsCOMPtr<nsIStorageStream> storageStream;
280 bool found = mOutputStreamTable.Get(uri, getter_AddRefs(storageStream));
281 if (found) {
282 // Setting an output stream here causes crashes on Windows. The previous
283 // version of this code always returned NS_ERROR_OUT_OF_MEMORY here,
284 // because it used a mistyped contract ID to create its object stream.
285 return NS_ERROR_NOT_IMPLEMENTED;
286 #if 0
287 nsCOMPtr<nsIOutputStream> outputStream
288 = do_QueryInterface(storageStream);
289 objectOutput = NS_NewObjectOutputStream(outputStream);
290 #endif
291 } else {
292 rv = NewObjectOutputWrappedStorageStream(
293 getter_AddRefs(objectOutput), getter_AddRefs(storageStream), false);
294 NS_ENSURE_SUCCESS(rv, rv);
295 mOutputStreamTable.InsertOrUpdate(uri, storageStream);
296 }
297 objectOutput.forget(stream);
298 return NS_OK;
299 }
300
FinishOutputStream(nsIURI * uri)301 nsresult nsXULPrototypeCache::FinishOutputStream(nsIURI* uri) {
302 nsresult rv;
303 StartupCache* sc = StartupCache::GetSingleton();
304 if (!sc) return NS_ERROR_NOT_AVAILABLE;
305
306 nsCOMPtr<nsIStorageStream> storageStream;
307 bool found = mOutputStreamTable.Get(uri, getter_AddRefs(storageStream));
308 if (!found) return NS_ERROR_UNEXPECTED;
309 nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(storageStream);
310 outputStream->Close();
311
312 UniquePtr<char[]> buf;
313 uint32_t len;
314 rv = NewBufferFromStorageStream(storageStream, &buf, &len);
315 NS_ENSURE_SUCCESS(rv, rv);
316
317 if (!mStartupCacheURITable.GetEntry(uri)) {
318 nsAutoCString spec(kXULCachePrefix);
319 rv = PathifyURI(uri, spec);
320 if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
321 rv = sc->PutBuffer(spec.get(), std::move(buf), len);
322 if (NS_SUCCEEDED(rv)) {
323 mOutputStreamTable.Remove(uri);
324 mStartupCacheURITable.PutEntry(uri);
325 }
326 }
327
328 return rv;
329 }
330
331 // We have data if we're in the middle of writing it or we already
332 // have it in the cache.
HasData(nsIURI * uri,bool * exists)333 nsresult nsXULPrototypeCache::HasData(nsIURI* uri, bool* exists) {
334 if (mOutputStreamTable.Get(uri, nullptr)) {
335 *exists = true;
336 return NS_OK;
337 }
338 nsAutoCString spec(kXULCachePrefix);
339 nsresult rv = PathifyURI(uri, spec);
340 if (NS_FAILED(rv)) {
341 *exists = false;
342 return NS_OK;
343 }
344 UniquePtr<char[]> buf;
345 StartupCache* sc = StartupCache::GetSingleton();
346 if (sc) {
347 *exists = sc->HasEntry(spec.get());
348 } else {
349 *exists = false;
350 }
351 return NS_OK;
352 }
353
BeginCaching(nsIURI * aURI)354 nsresult nsXULPrototypeCache::BeginCaching(nsIURI* aURI) {
355 nsresult rv, tmp;
356
357 nsAutoCString path;
358 aURI->GetPathQueryRef(path);
359 if (!(StringEndsWith(path, ".xul"_ns) || StringEndsWith(path, ".xhtml"_ns))) {
360 return NS_ERROR_NOT_AVAILABLE;
361 }
362
363 StartupCache* startupCache = StartupCache::GetSingleton();
364 if (!startupCache) return NS_ERROR_FAILURE;
365
366 if (gDisableXULCache) return NS_ERROR_NOT_AVAILABLE;
367
368 // Get the chrome directory to validate against the one stored in the
369 // cache file, or to store there if we're generating a new file.
370 nsCOMPtr<nsIFile> chromeDir;
371 rv = NS_GetSpecialDirectory(NS_APP_CHROME_DIR, getter_AddRefs(chromeDir));
372 if (NS_FAILED(rv)) return rv;
373 nsAutoCString chromePath;
374 rv = chromeDir->GetPersistentDescriptor(chromePath);
375 if (NS_FAILED(rv)) return rv;
376
377 // XXXbe we assume the first package's locale is the same as the locale of
378 // all subsequent packages of cached chrome URIs....
379 nsAutoCString package;
380 rv = aURI->GetHost(package);
381 if (NS_FAILED(rv)) return rv;
382 nsAutoCString locale;
383 LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale);
384
385 nsAutoCString fileChromePath, fileLocale;
386
387 const char* buf = nullptr;
388 uint32_t len, amtRead;
389 nsCOMPtr<nsIObjectInputStream> objectInput;
390
391 rv = startupCache->GetBuffer(kXULCacheInfoKey, &buf, &len);
392 if (NS_SUCCEEDED(rv))
393 rv = NewObjectInputStreamFromBuffer(buf, len, getter_AddRefs(objectInput));
394
395 if (NS_SUCCEEDED(rv)) {
396 rv = objectInput->ReadCString(fileLocale);
397 tmp = objectInput->ReadCString(fileChromePath);
398 if (NS_FAILED(tmp)) {
399 rv = tmp;
400 }
401 if (NS_FAILED(rv) ||
402 (!fileChromePath.Equals(chromePath) || !fileLocale.Equals(locale))) {
403 // Our cache won't be valid in this case, we'll need to rewrite.
404 // XXX This blows away work that other consumers (like
405 // mozJSComponentLoader) have done, need more fine-grained control.
406 startupCache->InvalidateCache();
407 mStartupCacheURITable.Clear();
408 rv = NS_ERROR_UNEXPECTED;
409 }
410 } else if (rv != NS_ERROR_NOT_AVAILABLE)
411 // NS_ERROR_NOT_AVAILABLE is normal, usually if there's no cachefile.
412 return rv;
413
414 if (NS_FAILED(rv)) {
415 // Either the cache entry was invalid or it didn't exist, so write it now.
416 nsCOMPtr<nsIObjectOutputStream> objectOutput;
417 nsCOMPtr<nsIInputStream> inputStream;
418 nsCOMPtr<nsIStorageStream> storageStream;
419 rv = NewObjectOutputWrappedStorageStream(
420 getter_AddRefs(objectOutput), getter_AddRefs(storageStream), false);
421 if (NS_SUCCEEDED(rv)) {
422 rv = objectOutput->WriteStringZ(locale.get());
423 tmp = objectOutput->WriteStringZ(chromePath.get());
424 if (NS_FAILED(tmp)) {
425 rv = tmp;
426 }
427 tmp = objectOutput->Close();
428 if (NS_FAILED(tmp)) {
429 rv = tmp;
430 }
431 tmp = storageStream->NewInputStream(0, getter_AddRefs(inputStream));
432 if (NS_FAILED(tmp)) {
433 rv = tmp;
434 }
435 }
436
437 if (NS_SUCCEEDED(rv)) {
438 uint64_t len64;
439 rv = inputStream->Available(&len64);
440 if (NS_SUCCEEDED(rv)) {
441 if (len64 <= UINT32_MAX)
442 len = (uint32_t)len64;
443 else
444 rv = NS_ERROR_FILE_TOO_BIG;
445 }
446 }
447
448 if (NS_SUCCEEDED(rv)) {
449 auto putBuf = MakeUnique<char[]>(len);
450 rv = inputStream->Read(putBuf.get(), len, &amtRead);
451 if (NS_SUCCEEDED(rv) && len == amtRead)
452 rv = startupCache->PutBuffer(kXULCacheInfoKey, std::move(putBuf), len);
453 else {
454 rv = NS_ERROR_UNEXPECTED;
455 }
456 }
457
458 // Failed again, just bail.
459 if (NS_FAILED(rv)) {
460 startupCache->InvalidateCache();
461 mStartupCacheURITable.Clear();
462 return NS_ERROR_FAILURE;
463 }
464 }
465
466 return NS_OK;
467 }
468
MarkInCCGeneration(uint32_t aGeneration)469 void nsXULPrototypeCache::MarkInCCGeneration(uint32_t aGeneration) {
470 for (const auto& prototype : mPrototypeTable.Values()) {
471 prototype->MarkInCCGeneration(aGeneration);
472 }
473 }
474
MarkInGC(JSTracer * aTrc)475 void nsXULPrototypeCache::MarkInGC(JSTracer* aTrc) {
476 for (auto& entry : mScriptTable) {
477 JS::TraceEdge(aTrc, &entry.mScript, "nsXULPrototypeCache script");
478 }
479 }
480
MOZ_DEFINE_MALLOC_SIZE_OF(CacheMallocSizeOf)481 MOZ_DEFINE_MALLOC_SIZE_OF(CacheMallocSizeOf)
482
483 static void ReportSize(const nsCString& aPath, size_t aAmount,
484 const nsCString& aDescription,
485 nsIHandleReportCallback* aHandleReport,
486 nsISupports* aData) {
487 nsAutoCString path("explicit/xul-prototype-cache/");
488 path += aPath;
489 aHandleReport->Callback(""_ns, path, nsIMemoryReporter::KIND_HEAP,
490 nsIMemoryReporter::UNITS_BYTES, aAmount, aDescription,
491 aData);
492 }
493
494 /* static */
CollectMemoryReports(nsIHandleReportCallback * aHandleReport,nsISupports * aData)495 void nsXULPrototypeCache::CollectMemoryReports(
496 nsIHandleReportCallback* aHandleReport, nsISupports* aData) {
497 if (!sInstance) {
498 return;
499 }
500
501 MallocSizeOf mallocSizeOf = CacheMallocSizeOf;
502 size_t other = mallocSizeOf(sInstance);
503
504 #define REPORT_SIZE(_path, _amount, _desc) \
505 ReportSize(_path, _amount, nsLiteralCString(_desc), aHandleReport, aData)
506
507 other += sInstance->mPrototypeTable.ShallowSizeOfExcludingThis(mallocSizeOf);
508 // TODO Report content in mPrototypeTable?
509
510 other += sInstance->mStyleSheetTable.ShallowSizeOfExcludingThis(mallocSizeOf);
511 for (const auto& stylesheet : sInstance->mStyleSheetTable.Values()) {
512 // NOTE: If Loader::DoSheetComplete() is ever modified to stop clongin
513 // sheets before inserting into this cache, we will need to stop using
514 // SizeOfIncludingThis()
515 other += stylesheet->SizeOfIncludingThis(mallocSizeOf);
516 }
517
518 other += sInstance->mScriptTable.ShallowSizeOfExcludingThis(mallocSizeOf);
519 // TODO Report content inside mScriptTable?
520
521 other +=
522 sInstance->mStartupCacheURITable.ShallowSizeOfExcludingThis(mallocSizeOf);
523
524 other +=
525 sInstance->mOutputStreamTable.ShallowSizeOfExcludingThis(mallocSizeOf);
526 other +=
527 sInstance->mInputStreamTable.ShallowSizeOfExcludingThis(mallocSizeOf);
528
529 REPORT_SIZE("other"_ns, other,
530 "Memory used by "
531 "the instance and tables of the XUL prototype cache.");
532
533 #undef REPORT_SIZE
534 }
535