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