Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | #include "prio.h" |
michael@0 | 8 | #include "pldhash.h" |
michael@0 | 9 | #include "nsXPCOMStrings.h" |
michael@0 | 10 | #include "mozilla/IOInterposer.h" |
michael@0 | 11 | #include "mozilla/MemoryReporting.h" |
michael@0 | 12 | #include "mozilla/scache/StartupCache.h" |
michael@0 | 13 | |
michael@0 | 14 | #include "nsAutoPtr.h" |
michael@0 | 15 | #include "nsClassHashtable.h" |
michael@0 | 16 | #include "nsComponentManagerUtils.h" |
michael@0 | 17 | #include "nsDirectoryServiceUtils.h" |
michael@0 | 18 | #include "nsIClassInfo.h" |
michael@0 | 19 | #include "nsIFile.h" |
michael@0 | 20 | #include "nsIObserver.h" |
michael@0 | 21 | #include "nsIObserverService.h" |
michael@0 | 22 | #include "nsIOutputStream.h" |
michael@0 | 23 | #include "nsIStartupCache.h" |
michael@0 | 24 | #include "nsIStorageStream.h" |
michael@0 | 25 | #include "nsIStreamBufferAccess.h" |
michael@0 | 26 | #include "nsIStringStream.h" |
michael@0 | 27 | #include "nsISupports.h" |
michael@0 | 28 | #include "nsITimer.h" |
michael@0 | 29 | #include "nsIZipWriter.h" |
michael@0 | 30 | #include "nsIZipReader.h" |
michael@0 | 31 | #include "nsWeakReference.h" |
michael@0 | 32 | #include "nsZipArchive.h" |
michael@0 | 33 | #include "mozilla/Omnijar.h" |
michael@0 | 34 | #include "prenv.h" |
michael@0 | 35 | #include "mozilla/Telemetry.h" |
michael@0 | 36 | #include "nsThreadUtils.h" |
michael@0 | 37 | #include "nsXULAppAPI.h" |
michael@0 | 38 | #include "nsIProtocolHandler.h" |
michael@0 | 39 | |
michael@0 | 40 | #ifdef IS_BIG_ENDIAN |
michael@0 | 41 | #define SC_ENDIAN "big" |
michael@0 | 42 | #else |
michael@0 | 43 | #define SC_ENDIAN "little" |
michael@0 | 44 | #endif |
michael@0 | 45 | |
michael@0 | 46 | #if PR_BYTES_PER_WORD == 4 |
michael@0 | 47 | #define SC_WORDSIZE "4" |
michael@0 | 48 | #else |
michael@0 | 49 | #define SC_WORDSIZE "8" |
michael@0 | 50 | #endif |
michael@0 | 51 | |
michael@0 | 52 | namespace mozilla { |
michael@0 | 53 | namespace scache { |
michael@0 | 54 | |
michael@0 | 55 | MOZ_DEFINE_MALLOC_SIZE_OF(StartupCacheMallocSizeOf) |
michael@0 | 56 | |
michael@0 | 57 | NS_IMETHODIMP |
michael@0 | 58 | StartupCache::CollectReports(nsIHandleReportCallback* aHandleReport, |
michael@0 | 59 | nsISupports* aData) |
michael@0 | 60 | { |
michael@0 | 61 | #define REPORT(_path, _kind, _amount, _desc) \ |
michael@0 | 62 | do { \ |
michael@0 | 63 | nsresult rv = \ |
michael@0 | 64 | aHandleReport->Callback(EmptyCString(), \ |
michael@0 | 65 | NS_LITERAL_CSTRING(_path), \ |
michael@0 | 66 | _kind, UNITS_BYTES, _amount, \ |
michael@0 | 67 | NS_LITERAL_CSTRING(_desc), aData); \ |
michael@0 | 68 | NS_ENSURE_SUCCESS(rv, rv); \ |
michael@0 | 69 | } while (0) |
michael@0 | 70 | |
michael@0 | 71 | REPORT("explicit/startup-cache/mapping", KIND_NONHEAP, |
michael@0 | 72 | SizeOfMapping(), |
michael@0 | 73 | "Memory used to hold the mapping of the startup cache from file. " |
michael@0 | 74 | "This memory is likely to be swapped out shortly after start-up."); |
michael@0 | 75 | |
michael@0 | 76 | REPORT("explicit/startup-cache/data", KIND_HEAP, |
michael@0 | 77 | HeapSizeOfIncludingThis(StartupCacheMallocSizeOf), |
michael@0 | 78 | "Memory used by the startup cache for things other than the file " |
michael@0 | 79 | "mapping."); |
michael@0 | 80 | |
michael@0 | 81 | return NS_OK; |
michael@0 | 82 | } |
michael@0 | 83 | |
michael@0 | 84 | static const char sStartupCacheName[] = "startupCache." SC_WORDSIZE "." SC_ENDIAN; |
michael@0 | 85 | #if defined(XP_WIN) && defined(MOZ_METRO) |
michael@0 | 86 | static const char sMetroStartupCacheName[] = "metroStartupCache." SC_WORDSIZE "." SC_ENDIAN; |
michael@0 | 87 | #endif |
michael@0 | 88 | |
michael@0 | 89 | StartupCache* |
michael@0 | 90 | StartupCache::GetSingleton() |
michael@0 | 91 | { |
michael@0 | 92 | if (!gStartupCache) { |
michael@0 | 93 | if (XRE_GetProcessType() != GeckoProcessType_Default) { |
michael@0 | 94 | return nullptr; |
michael@0 | 95 | } |
michael@0 | 96 | #ifdef MOZ_B2G |
michael@0 | 97 | return nullptr; |
michael@0 | 98 | #endif |
michael@0 | 99 | |
michael@0 | 100 | StartupCache::InitSingleton(); |
michael@0 | 101 | } |
michael@0 | 102 | |
michael@0 | 103 | return StartupCache::gStartupCache; |
michael@0 | 104 | } |
michael@0 | 105 | |
michael@0 | 106 | void |
michael@0 | 107 | StartupCache::DeleteSingleton() |
michael@0 | 108 | { |
michael@0 | 109 | StartupCache::gStartupCache = nullptr; |
michael@0 | 110 | } |
michael@0 | 111 | |
michael@0 | 112 | nsresult |
michael@0 | 113 | StartupCache::InitSingleton() |
michael@0 | 114 | { |
michael@0 | 115 | nsresult rv; |
michael@0 | 116 | StartupCache::gStartupCache = new StartupCache(); |
michael@0 | 117 | |
michael@0 | 118 | rv = StartupCache::gStartupCache->Init(); |
michael@0 | 119 | if (NS_FAILED(rv)) { |
michael@0 | 120 | StartupCache::gStartupCache = nullptr; |
michael@0 | 121 | } |
michael@0 | 122 | return rv; |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | StaticRefPtr<StartupCache> StartupCache::gStartupCache; |
michael@0 | 126 | bool StartupCache::gShutdownInitiated; |
michael@0 | 127 | bool StartupCache::gIgnoreDiskCache; |
michael@0 | 128 | enum StartupCache::TelemetrifyAge StartupCache::gPostFlushAgeAction = StartupCache::IGNORE_AGE; |
michael@0 | 129 | |
michael@0 | 130 | NS_IMPL_ISUPPORTS(StartupCache, nsIMemoryReporter) |
michael@0 | 131 | |
michael@0 | 132 | StartupCache::StartupCache() |
michael@0 | 133 | : mArchive(nullptr), mStartupWriteInitiated(false), mWriteThread(nullptr) |
michael@0 | 134 | { } |
michael@0 | 135 | |
michael@0 | 136 | StartupCache::~StartupCache() |
michael@0 | 137 | { |
michael@0 | 138 | if (mTimer) { |
michael@0 | 139 | mTimer->Cancel(); |
michael@0 | 140 | } |
michael@0 | 141 | |
michael@0 | 142 | // Generally, the in-memory table should be empty here, |
michael@0 | 143 | // but an early shutdown means either mTimer didn't run |
michael@0 | 144 | // or the write thread is still running. |
michael@0 | 145 | WaitOnWriteThread(); |
michael@0 | 146 | |
michael@0 | 147 | // If we shutdown quickly timer wont have fired. Instead of writing |
michael@0 | 148 | // it on the main thread and block the shutdown we simply wont update |
michael@0 | 149 | // the startup cache. Always do this if the file doesn't exist since |
michael@0 | 150 | // we use it part of the package step. |
michael@0 | 151 | if (!mArchive) { |
michael@0 | 152 | WriteToDisk(); |
michael@0 | 153 | } |
michael@0 | 154 | |
michael@0 | 155 | UnregisterWeakMemoryReporter(this); |
michael@0 | 156 | } |
michael@0 | 157 | |
michael@0 | 158 | nsresult |
michael@0 | 159 | StartupCache::Init() |
michael@0 | 160 | { |
michael@0 | 161 | // workaround for bug 653936 |
michael@0 | 162 | nsCOMPtr<nsIProtocolHandler> jarInitializer(do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "jar")); |
michael@0 | 163 | |
michael@0 | 164 | nsresult rv; |
michael@0 | 165 | |
michael@0 | 166 | // This allows to override the startup cache filename |
michael@0 | 167 | // which is useful from xpcshell, when there is no ProfLDS directory to keep cache in. |
michael@0 | 168 | char *env = PR_GetEnv("MOZ_STARTUP_CACHE"); |
michael@0 | 169 | if (env) { |
michael@0 | 170 | rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(env), false, getter_AddRefs(mFile)); |
michael@0 | 171 | } else { |
michael@0 | 172 | nsCOMPtr<nsIFile> file; |
michael@0 | 173 | rv = NS_GetSpecialDirectory("ProfLDS", |
michael@0 | 174 | getter_AddRefs(file)); |
michael@0 | 175 | if (NS_FAILED(rv)) { |
michael@0 | 176 | // return silently, this will fail in mochitests's xpcshell process. |
michael@0 | 177 | return rv; |
michael@0 | 178 | } |
michael@0 | 179 | |
michael@0 | 180 | nsCOMPtr<nsIFile> profDir; |
michael@0 | 181 | NS_GetSpecialDirectory("ProfDS", getter_AddRefs(profDir)); |
michael@0 | 182 | if (profDir) { |
michael@0 | 183 | bool same; |
michael@0 | 184 | if (NS_SUCCEEDED(profDir->Equals(file, &same)) && !same) { |
michael@0 | 185 | // We no longer store the startup cache in the main profile |
michael@0 | 186 | // directory, so we should cleanup the old one. |
michael@0 | 187 | if (NS_SUCCEEDED( |
michael@0 | 188 | profDir->AppendNative(NS_LITERAL_CSTRING("startupCache")))) { |
michael@0 | 189 | profDir->Remove(true); |
michael@0 | 190 | } |
michael@0 | 191 | } |
michael@0 | 192 | } |
michael@0 | 193 | |
michael@0 | 194 | rv = file->AppendNative(NS_LITERAL_CSTRING("startupCache")); |
michael@0 | 195 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 196 | |
michael@0 | 197 | // Try to create the directory if it's not there yet |
michael@0 | 198 | rv = file->Create(nsIFile::DIRECTORY_TYPE, 0777); |
michael@0 | 199 | if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) |
michael@0 | 200 | return rv; |
michael@0 | 201 | |
michael@0 | 202 | #if defined(XP_WIN) && defined(MOZ_METRO) |
michael@0 | 203 | if (XRE_GetWindowsEnvironment() == WindowsEnvironmentType_Metro) { |
michael@0 | 204 | rv = file->AppendNative(NS_LITERAL_CSTRING(sMetroStartupCacheName)); |
michael@0 | 205 | } else |
michael@0 | 206 | #endif |
michael@0 | 207 | { |
michael@0 | 208 | rv = file->AppendNative(NS_LITERAL_CSTRING(sStartupCacheName)); |
michael@0 | 209 | } |
michael@0 | 210 | |
michael@0 | 211 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 212 | |
michael@0 | 213 | mFile = do_QueryInterface(file); |
michael@0 | 214 | } |
michael@0 | 215 | |
michael@0 | 216 | NS_ENSURE_TRUE(mFile, NS_ERROR_UNEXPECTED); |
michael@0 | 217 | |
michael@0 | 218 | mObserverService = do_GetService("@mozilla.org/observer-service;1"); |
michael@0 | 219 | |
michael@0 | 220 | if (!mObserverService) { |
michael@0 | 221 | NS_WARNING("Could not get observerService."); |
michael@0 | 222 | return NS_ERROR_UNEXPECTED; |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | mListener = new StartupCacheListener(); |
michael@0 | 226 | rv = mObserverService->AddObserver(mListener, NS_XPCOM_SHUTDOWN_OBSERVER_ID, |
michael@0 | 227 | false); |
michael@0 | 228 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 229 | rv = mObserverService->AddObserver(mListener, "startupcache-invalidate", |
michael@0 | 230 | false); |
michael@0 | 231 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 232 | |
michael@0 | 233 | rv = LoadArchive(RECORD_AGE); |
michael@0 | 234 | |
michael@0 | 235 | // Sometimes we don't have a cache yet, that's ok. |
michael@0 | 236 | // If it's corrupted, just remove it and start over. |
michael@0 | 237 | if (gIgnoreDiskCache || (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND)) { |
michael@0 | 238 | NS_WARNING("Failed to load startupcache file correctly, removing!"); |
michael@0 | 239 | InvalidateCache(); |
michael@0 | 240 | } |
michael@0 | 241 | |
michael@0 | 242 | RegisterWeakMemoryReporter(this); |
michael@0 | 243 | |
michael@0 | 244 | return NS_OK; |
michael@0 | 245 | } |
michael@0 | 246 | |
michael@0 | 247 | /** |
michael@0 | 248 | * LoadArchive can be called from the main thread or while reloading cache on write thread. |
michael@0 | 249 | */ |
michael@0 | 250 | nsresult |
michael@0 | 251 | StartupCache::LoadArchive(enum TelemetrifyAge flag) |
michael@0 | 252 | { |
michael@0 | 253 | if (gIgnoreDiskCache) |
michael@0 | 254 | return NS_ERROR_FAILURE; |
michael@0 | 255 | |
michael@0 | 256 | bool exists; |
michael@0 | 257 | mArchive = nullptr; |
michael@0 | 258 | nsresult rv = mFile->Exists(&exists); |
michael@0 | 259 | if (NS_FAILED(rv) || !exists) |
michael@0 | 260 | return NS_ERROR_FILE_NOT_FOUND; |
michael@0 | 261 | |
michael@0 | 262 | mArchive = new nsZipArchive(); |
michael@0 | 263 | rv = mArchive->OpenArchive(mFile); |
michael@0 | 264 | if (NS_FAILED(rv) || flag == IGNORE_AGE) |
michael@0 | 265 | return rv; |
michael@0 | 266 | |
michael@0 | 267 | nsCString comment; |
michael@0 | 268 | if (!mArchive->GetComment(comment)) { |
michael@0 | 269 | return rv; |
michael@0 | 270 | } |
michael@0 | 271 | |
michael@0 | 272 | const char *data; |
michael@0 | 273 | size_t len = NS_CStringGetData(comment, &data); |
michael@0 | 274 | PRTime creationStamp; |
michael@0 | 275 | // We might not have a comment if the startup cache file was created |
michael@0 | 276 | // before we started recording creation times in the comment. |
michael@0 | 277 | if (len == sizeof(creationStamp)) { |
michael@0 | 278 | memcpy(&creationStamp, data, len); |
michael@0 | 279 | PRTime current = PR_Now(); |
michael@0 | 280 | int64_t diff = current - creationStamp; |
michael@0 | 281 | |
michael@0 | 282 | // We can't use AccumulateTimeDelta here because we have no way of |
michael@0 | 283 | // reifying a TimeStamp from creationStamp. |
michael@0 | 284 | int64_t usec_per_hour = PR_USEC_PER_SEC * int64_t(3600); |
michael@0 | 285 | int64_t hour_diff = (diff + usec_per_hour - 1) / usec_per_hour; |
michael@0 | 286 | mozilla::Telemetry::Accumulate(Telemetry::STARTUP_CACHE_AGE_HOURS, |
michael@0 | 287 | hour_diff); |
michael@0 | 288 | } |
michael@0 | 289 | |
michael@0 | 290 | return rv; |
michael@0 | 291 | } |
michael@0 | 292 | |
michael@0 | 293 | namespace { |
michael@0 | 294 | |
michael@0 | 295 | nsresult |
michael@0 | 296 | GetBufferFromZipArchive(nsZipArchive *zip, bool doCRC, const char* id, |
michael@0 | 297 | char** outbuf, uint32_t* length) |
michael@0 | 298 | { |
michael@0 | 299 | if (!zip) |
michael@0 | 300 | return NS_ERROR_NOT_AVAILABLE; |
michael@0 | 301 | |
michael@0 | 302 | nsZipItemPtr<char> zipItem(zip, id, doCRC); |
michael@0 | 303 | if (!zipItem) |
michael@0 | 304 | return NS_ERROR_NOT_AVAILABLE; |
michael@0 | 305 | |
michael@0 | 306 | *outbuf = zipItem.Forget(); |
michael@0 | 307 | *length = zipItem.Length(); |
michael@0 | 308 | return NS_OK; |
michael@0 | 309 | } |
michael@0 | 310 | |
michael@0 | 311 | } /* anonymous namespace */ |
michael@0 | 312 | |
michael@0 | 313 | // NOTE: this will not find a new entry until it has been written to disk! |
michael@0 | 314 | // Consumer should take ownership of the resulting buffer. |
michael@0 | 315 | nsresult |
michael@0 | 316 | StartupCache::GetBuffer(const char* id, char** outbuf, uint32_t* length) |
michael@0 | 317 | { |
michael@0 | 318 | NS_ASSERTION(NS_IsMainThread(), "Startup cache only available on main thread"); |
michael@0 | 319 | WaitOnWriteThread(); |
michael@0 | 320 | if (!mStartupWriteInitiated) { |
michael@0 | 321 | CacheEntry* entry; |
michael@0 | 322 | nsDependentCString idStr(id); |
michael@0 | 323 | mTable.Get(idStr, &entry); |
michael@0 | 324 | if (entry) { |
michael@0 | 325 | *outbuf = new char[entry->size]; |
michael@0 | 326 | memcpy(*outbuf, entry->data, entry->size); |
michael@0 | 327 | *length = entry->size; |
michael@0 | 328 | return NS_OK; |
michael@0 | 329 | } |
michael@0 | 330 | } |
michael@0 | 331 | |
michael@0 | 332 | nsresult rv = GetBufferFromZipArchive(mArchive, true, id, outbuf, length); |
michael@0 | 333 | if (NS_SUCCEEDED(rv)) |
michael@0 | 334 | return rv; |
michael@0 | 335 | |
michael@0 | 336 | nsRefPtr<nsZipArchive> omnijar = mozilla::Omnijar::GetReader(mozilla::Omnijar::APP); |
michael@0 | 337 | // no need to checksum omnijarred entries |
michael@0 | 338 | rv = GetBufferFromZipArchive(omnijar, false, id, outbuf, length); |
michael@0 | 339 | if (NS_SUCCEEDED(rv)) |
michael@0 | 340 | return rv; |
michael@0 | 341 | |
michael@0 | 342 | omnijar = mozilla::Omnijar::GetReader(mozilla::Omnijar::GRE); |
michael@0 | 343 | // no need to checksum omnijarred entries |
michael@0 | 344 | return GetBufferFromZipArchive(omnijar, false, id, outbuf, length); |
michael@0 | 345 | } |
michael@0 | 346 | |
michael@0 | 347 | // Makes a copy of the buffer, client retains ownership of inbuf. |
michael@0 | 348 | nsresult |
michael@0 | 349 | StartupCache::PutBuffer(const char* id, const char* inbuf, uint32_t len) |
michael@0 | 350 | { |
michael@0 | 351 | NS_ASSERTION(NS_IsMainThread(), "Startup cache only available on main thread"); |
michael@0 | 352 | WaitOnWriteThread(); |
michael@0 | 353 | if (StartupCache::gShutdownInitiated) { |
michael@0 | 354 | return NS_ERROR_NOT_AVAILABLE; |
michael@0 | 355 | } |
michael@0 | 356 | |
michael@0 | 357 | nsAutoArrayPtr<char> data(new char[len]); |
michael@0 | 358 | memcpy(data, inbuf, len); |
michael@0 | 359 | |
michael@0 | 360 | nsDependentCString idStr(id); |
michael@0 | 361 | // Cache it for now, we'll write all together later. |
michael@0 | 362 | CacheEntry* entry; |
michael@0 | 363 | |
michael@0 | 364 | #ifdef DEBUG |
michael@0 | 365 | mTable.Get(idStr, &entry); |
michael@0 | 366 | NS_ASSERTION(entry == nullptr, "Existing entry in StartupCache."); |
michael@0 | 367 | |
michael@0 | 368 | if (mArchive) { |
michael@0 | 369 | nsZipItem* zipItem = mArchive->GetItem(id); |
michael@0 | 370 | NS_ASSERTION(zipItem == nullptr, "Existing entry in disk StartupCache."); |
michael@0 | 371 | } |
michael@0 | 372 | #endif |
michael@0 | 373 | |
michael@0 | 374 | entry = new CacheEntry(data.forget(), len); |
michael@0 | 375 | mTable.Put(idStr, entry); |
michael@0 | 376 | return ResetStartupWriteTimer(); |
michael@0 | 377 | } |
michael@0 | 378 | |
michael@0 | 379 | size_t |
michael@0 | 380 | StartupCache::SizeOfMapping() |
michael@0 | 381 | { |
michael@0 | 382 | return mArchive ? mArchive->SizeOfMapping() : 0; |
michael@0 | 383 | } |
michael@0 | 384 | |
michael@0 | 385 | size_t |
michael@0 | 386 | StartupCache::HeapSizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) |
michael@0 | 387 | { |
michael@0 | 388 | // This function could measure more members, but they haven't been found by |
michael@0 | 389 | // DMD to be significant. They can be added later if necessary. |
michael@0 | 390 | return aMallocSizeOf(this) + |
michael@0 | 391 | mTable.SizeOfExcludingThis(SizeOfEntryExcludingThis, aMallocSizeOf); |
michael@0 | 392 | } |
michael@0 | 393 | |
michael@0 | 394 | /* static */ size_t |
michael@0 | 395 | StartupCache::SizeOfEntryExcludingThis(const nsACString& key, const nsAutoPtr<CacheEntry>& data, |
michael@0 | 396 | mozilla::MallocSizeOf mallocSizeOf, void *) |
michael@0 | 397 | { |
michael@0 | 398 | return data->SizeOfExcludingThis(mallocSizeOf); |
michael@0 | 399 | } |
michael@0 | 400 | |
michael@0 | 401 | struct CacheWriteHolder |
michael@0 | 402 | { |
michael@0 | 403 | nsCOMPtr<nsIZipWriter> writer; |
michael@0 | 404 | nsCOMPtr<nsIStringInputStream> stream; |
michael@0 | 405 | PRTime time; |
michael@0 | 406 | }; |
michael@0 | 407 | |
michael@0 | 408 | PLDHashOperator |
michael@0 | 409 | CacheCloseHelper(const nsACString& key, nsAutoPtr<CacheEntry>& data, |
michael@0 | 410 | void* closure) |
michael@0 | 411 | { |
michael@0 | 412 | nsresult rv; |
michael@0 | 413 | |
michael@0 | 414 | CacheWriteHolder* holder = (CacheWriteHolder*) closure; |
michael@0 | 415 | nsIStringInputStream* stream = holder->stream; |
michael@0 | 416 | nsIZipWriter* writer = holder->writer; |
michael@0 | 417 | |
michael@0 | 418 | stream->ShareData(data->data, data->size); |
michael@0 | 419 | |
michael@0 | 420 | #ifdef DEBUG |
michael@0 | 421 | bool hasEntry; |
michael@0 | 422 | rv = writer->HasEntry(key, &hasEntry); |
michael@0 | 423 | NS_ASSERTION(NS_SUCCEEDED(rv) && hasEntry == false, |
michael@0 | 424 | "Existing entry in disk StartupCache."); |
michael@0 | 425 | #endif |
michael@0 | 426 | rv = writer->AddEntryStream(key, holder->time, true, stream, false); |
michael@0 | 427 | |
michael@0 | 428 | if (NS_FAILED(rv)) { |
michael@0 | 429 | NS_WARNING("cache entry deleted but not written to disk."); |
michael@0 | 430 | } |
michael@0 | 431 | return PL_DHASH_REMOVE; |
michael@0 | 432 | } |
michael@0 | 433 | |
michael@0 | 434 | |
michael@0 | 435 | /** |
michael@0 | 436 | * WriteToDisk writes the cache out to disk. Callers of WriteToDisk need to call WaitOnWriteThread |
michael@0 | 437 | * to make sure there isn't a write happening on another thread |
michael@0 | 438 | */ |
michael@0 | 439 | void |
michael@0 | 440 | StartupCache::WriteToDisk() |
michael@0 | 441 | { |
michael@0 | 442 | nsresult rv; |
michael@0 | 443 | mStartupWriteInitiated = true; |
michael@0 | 444 | |
michael@0 | 445 | if (mTable.Count() == 0) |
michael@0 | 446 | return; |
michael@0 | 447 | |
michael@0 | 448 | nsCOMPtr<nsIZipWriter> zipW = do_CreateInstance("@mozilla.org/zipwriter;1"); |
michael@0 | 449 | if (!zipW) |
michael@0 | 450 | return; |
michael@0 | 451 | |
michael@0 | 452 | rv = zipW->Open(mFile, PR_RDWR | PR_CREATE_FILE); |
michael@0 | 453 | if (NS_FAILED(rv)) { |
michael@0 | 454 | NS_WARNING("could not open zipfile for write"); |
michael@0 | 455 | return; |
michael@0 | 456 | } |
michael@0 | 457 | |
michael@0 | 458 | // If we didn't have an mArchive member, that means that we failed to |
michael@0 | 459 | // open the startup cache for reading. Therefore, we need to record |
michael@0 | 460 | // the time of creation in a zipfile comment; this will be useful for |
michael@0 | 461 | // Telemetry statistics. |
michael@0 | 462 | PRTime now = PR_Now(); |
michael@0 | 463 | if (!mArchive) { |
michael@0 | 464 | nsCString comment; |
michael@0 | 465 | comment.Assign((char *)&now, sizeof(now)); |
michael@0 | 466 | zipW->SetComment(comment); |
michael@0 | 467 | } |
michael@0 | 468 | |
michael@0 | 469 | nsCOMPtr<nsIStringInputStream> stream |
michael@0 | 470 | = do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); |
michael@0 | 471 | if (NS_FAILED(rv)) { |
michael@0 | 472 | NS_WARNING("Couldn't create string input stream."); |
michael@0 | 473 | return; |
michael@0 | 474 | } |
michael@0 | 475 | |
michael@0 | 476 | CacheWriteHolder holder; |
michael@0 | 477 | holder.stream = stream; |
michael@0 | 478 | holder.writer = zipW; |
michael@0 | 479 | holder.time = now; |
michael@0 | 480 | |
michael@0 | 481 | mTable.Enumerate(CacheCloseHelper, &holder); |
michael@0 | 482 | |
michael@0 | 483 | // Close the archive so Windows doesn't choke. |
michael@0 | 484 | mArchive = nullptr; |
michael@0 | 485 | zipW->Close(); |
michael@0 | 486 | |
michael@0 | 487 | // We succesfully wrote the archive to disk; mark the disk file as trusted |
michael@0 | 488 | gIgnoreDiskCache = false; |
michael@0 | 489 | |
michael@0 | 490 | // Our reader's view of the archive is outdated now, reload it. |
michael@0 | 491 | LoadArchive(gPostFlushAgeAction); |
michael@0 | 492 | |
michael@0 | 493 | return; |
michael@0 | 494 | } |
michael@0 | 495 | |
michael@0 | 496 | void |
michael@0 | 497 | StartupCache::InvalidateCache() |
michael@0 | 498 | { |
michael@0 | 499 | WaitOnWriteThread(); |
michael@0 | 500 | mTable.Clear(); |
michael@0 | 501 | mArchive = nullptr; |
michael@0 | 502 | nsresult rv = mFile->Remove(false); |
michael@0 | 503 | if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && |
michael@0 | 504 | rv != NS_ERROR_FILE_NOT_FOUND) { |
michael@0 | 505 | gIgnoreDiskCache = true; |
michael@0 | 506 | mozilla::Telemetry::Accumulate(Telemetry::STARTUP_CACHE_INVALID, true); |
michael@0 | 507 | return; |
michael@0 | 508 | } |
michael@0 | 509 | gIgnoreDiskCache = false; |
michael@0 | 510 | LoadArchive(gPostFlushAgeAction); |
michael@0 | 511 | } |
michael@0 | 512 | |
michael@0 | 513 | void |
michael@0 | 514 | StartupCache::IgnoreDiskCache() |
michael@0 | 515 | { |
michael@0 | 516 | gIgnoreDiskCache = true; |
michael@0 | 517 | if (gStartupCache) |
michael@0 | 518 | gStartupCache->InvalidateCache(); |
michael@0 | 519 | } |
michael@0 | 520 | |
michael@0 | 521 | /* |
michael@0 | 522 | * WaitOnWriteThread() is called from a main thread to wait for the worker |
michael@0 | 523 | * thread to finish. However since the same code is used in the worker thread and |
michael@0 | 524 | * main thread, the worker thread can also call WaitOnWriteThread() which is a no-op. |
michael@0 | 525 | */ |
michael@0 | 526 | void |
michael@0 | 527 | StartupCache::WaitOnWriteThread() |
michael@0 | 528 | { |
michael@0 | 529 | NS_ASSERTION(NS_IsMainThread(), "Startup cache should only wait for io thread on main thread"); |
michael@0 | 530 | if (!mWriteThread || mWriteThread == PR_GetCurrentThread()) |
michael@0 | 531 | return; |
michael@0 | 532 | |
michael@0 | 533 | PR_JoinThread(mWriteThread); |
michael@0 | 534 | mWriteThread = nullptr; |
michael@0 | 535 | } |
michael@0 | 536 | |
michael@0 | 537 | void |
michael@0 | 538 | StartupCache::ThreadedWrite(void *aClosure) |
michael@0 | 539 | { |
michael@0 | 540 | PR_SetCurrentThreadName("StartupCache"); |
michael@0 | 541 | mozilla::IOInterposer::RegisterCurrentThread(); |
michael@0 | 542 | /* |
michael@0 | 543 | * It is safe to use the pointer passed in aClosure to reference the |
michael@0 | 544 | * StartupCache object because the thread's lifetime is tightly coupled to |
michael@0 | 545 | * the lifetime of the StartupCache object; this thread is joined in the |
michael@0 | 546 | * StartupCache destructor, guaranteeing that this function runs if and only |
michael@0 | 547 | * if the StartupCache object is valid. |
michael@0 | 548 | */ |
michael@0 | 549 | StartupCache* startupCacheObj = static_cast<StartupCache*>(aClosure); |
michael@0 | 550 | startupCacheObj->WriteToDisk(); |
michael@0 | 551 | mozilla::IOInterposer::UnregisterCurrentThread(); |
michael@0 | 552 | } |
michael@0 | 553 | |
michael@0 | 554 | /* |
michael@0 | 555 | * The write-thread is spawned on a timeout(which is reset with every write). This |
michael@0 | 556 | * can avoid a slow shutdown. After writing out the cache, the zipreader is |
michael@0 | 557 | * reloaded on the worker thread. |
michael@0 | 558 | */ |
michael@0 | 559 | void |
michael@0 | 560 | StartupCache::WriteTimeout(nsITimer *aTimer, void *aClosure) |
michael@0 | 561 | { |
michael@0 | 562 | /* |
michael@0 | 563 | * It is safe to use the pointer passed in aClosure to reference the |
michael@0 | 564 | * StartupCache object because the timer's lifetime is tightly coupled to |
michael@0 | 565 | * the lifetime of the StartupCache object; this timer is canceled in the |
michael@0 | 566 | * StartupCache destructor, guaranteeing that this function runs if and only |
michael@0 | 567 | * if the StartupCache object is valid. |
michael@0 | 568 | */ |
michael@0 | 569 | StartupCache* startupCacheObj = static_cast<StartupCache*>(aClosure); |
michael@0 | 570 | startupCacheObj->mWriteThread = PR_CreateThread(PR_USER_THREAD, |
michael@0 | 571 | StartupCache::ThreadedWrite, |
michael@0 | 572 | startupCacheObj, |
michael@0 | 573 | PR_PRIORITY_NORMAL, |
michael@0 | 574 | PR_GLOBAL_THREAD, |
michael@0 | 575 | PR_JOINABLE_THREAD, |
michael@0 | 576 | 0); |
michael@0 | 577 | } |
michael@0 | 578 | |
michael@0 | 579 | // We don't want to refcount StartupCache, so we'll just |
michael@0 | 580 | // hold a ref to this and pass it to observerService instead. |
michael@0 | 581 | NS_IMPL_ISUPPORTS(StartupCacheListener, nsIObserver) |
michael@0 | 582 | |
michael@0 | 583 | nsresult |
michael@0 | 584 | StartupCacheListener::Observe(nsISupports *subject, const char* topic, const char16_t* data) |
michael@0 | 585 | { |
michael@0 | 586 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 587 | if (!sc) |
michael@0 | 588 | return NS_OK; |
michael@0 | 589 | |
michael@0 | 590 | if (strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { |
michael@0 | 591 | // Do not leave the thread running past xpcom shutdown |
michael@0 | 592 | sc->WaitOnWriteThread(); |
michael@0 | 593 | StartupCache::gShutdownInitiated = true; |
michael@0 | 594 | } else if (strcmp(topic, "startupcache-invalidate") == 0) { |
michael@0 | 595 | sc->InvalidateCache(); |
michael@0 | 596 | } |
michael@0 | 597 | return NS_OK; |
michael@0 | 598 | } |
michael@0 | 599 | |
michael@0 | 600 | nsresult |
michael@0 | 601 | StartupCache::GetDebugObjectOutputStream(nsIObjectOutputStream* aStream, |
michael@0 | 602 | nsIObjectOutputStream** aOutStream) |
michael@0 | 603 | { |
michael@0 | 604 | NS_ENSURE_ARG_POINTER(aStream); |
michael@0 | 605 | #ifdef DEBUG |
michael@0 | 606 | StartupCacheDebugOutputStream* stream |
michael@0 | 607 | = new StartupCacheDebugOutputStream(aStream, &mWriteObjectMap); |
michael@0 | 608 | NS_ADDREF(*aOutStream = stream); |
michael@0 | 609 | #else |
michael@0 | 610 | NS_ADDREF(*aOutStream = aStream); |
michael@0 | 611 | #endif |
michael@0 | 612 | |
michael@0 | 613 | return NS_OK; |
michael@0 | 614 | } |
michael@0 | 615 | |
michael@0 | 616 | nsresult |
michael@0 | 617 | StartupCache::ResetStartupWriteTimer() |
michael@0 | 618 | { |
michael@0 | 619 | mStartupWriteInitiated = false; |
michael@0 | 620 | nsresult rv; |
michael@0 | 621 | if (!mTimer) |
michael@0 | 622 | mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); |
michael@0 | 623 | else |
michael@0 | 624 | rv = mTimer->Cancel(); |
michael@0 | 625 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 626 | // Wait for 10 seconds, then write out the cache. |
michael@0 | 627 | mTimer->InitWithFuncCallback(StartupCache::WriteTimeout, this, 60000, |
michael@0 | 628 | nsITimer::TYPE_ONE_SHOT); |
michael@0 | 629 | return NS_OK; |
michael@0 | 630 | } |
michael@0 | 631 | |
michael@0 | 632 | nsresult |
michael@0 | 633 | StartupCache::RecordAgesAlways() |
michael@0 | 634 | { |
michael@0 | 635 | gPostFlushAgeAction = RECORD_AGE; |
michael@0 | 636 | return NS_OK; |
michael@0 | 637 | } |
michael@0 | 638 | |
michael@0 | 639 | // StartupCacheDebugOutputStream implementation |
michael@0 | 640 | #ifdef DEBUG |
michael@0 | 641 | NS_IMPL_ISUPPORTS(StartupCacheDebugOutputStream, nsIObjectOutputStream, |
michael@0 | 642 | nsIBinaryOutputStream, nsIOutputStream) |
michael@0 | 643 | |
michael@0 | 644 | bool |
michael@0 | 645 | StartupCacheDebugOutputStream::CheckReferences(nsISupports* aObject) |
michael@0 | 646 | { |
michael@0 | 647 | nsresult rv; |
michael@0 | 648 | |
michael@0 | 649 | nsCOMPtr<nsIClassInfo> classInfo = do_QueryInterface(aObject); |
michael@0 | 650 | if (!classInfo) { |
michael@0 | 651 | NS_ERROR("aObject must implement nsIClassInfo"); |
michael@0 | 652 | return false; |
michael@0 | 653 | } |
michael@0 | 654 | |
michael@0 | 655 | uint32_t flags; |
michael@0 | 656 | rv = classInfo->GetFlags(&flags); |
michael@0 | 657 | NS_ENSURE_SUCCESS(rv, false); |
michael@0 | 658 | if (flags & nsIClassInfo::SINGLETON) |
michael@0 | 659 | return true; |
michael@0 | 660 | |
michael@0 | 661 | nsISupportsHashKey* key = mObjectMap->GetEntry(aObject); |
michael@0 | 662 | if (key) { |
michael@0 | 663 | NS_ERROR("non-singleton aObject is referenced multiple times in this" |
michael@0 | 664 | "serialization, we don't support that."); |
michael@0 | 665 | return false; |
michael@0 | 666 | } |
michael@0 | 667 | |
michael@0 | 668 | mObjectMap->PutEntry(aObject); |
michael@0 | 669 | return true; |
michael@0 | 670 | } |
michael@0 | 671 | |
michael@0 | 672 | // nsIObjectOutputStream implementation |
michael@0 | 673 | nsresult |
michael@0 | 674 | StartupCacheDebugOutputStream::WriteObject(nsISupports* aObject, bool aIsStrongRef) |
michael@0 | 675 | { |
michael@0 | 676 | nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject)); |
michael@0 | 677 | |
michael@0 | 678 | NS_ASSERTION(rootObject.get() == aObject, |
michael@0 | 679 | "bad call to WriteObject -- call WriteCompoundObject!"); |
michael@0 | 680 | bool check = CheckReferences(aObject); |
michael@0 | 681 | NS_ENSURE_TRUE(check, NS_ERROR_FAILURE); |
michael@0 | 682 | return mBinaryStream->WriteObject(aObject, aIsStrongRef); |
michael@0 | 683 | } |
michael@0 | 684 | |
michael@0 | 685 | nsresult |
michael@0 | 686 | StartupCacheDebugOutputStream::WriteSingleRefObject(nsISupports* aObject) |
michael@0 | 687 | { |
michael@0 | 688 | nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject)); |
michael@0 | 689 | |
michael@0 | 690 | NS_ASSERTION(rootObject.get() == aObject, |
michael@0 | 691 | "bad call to WriteSingleRefObject -- call WriteCompoundObject!"); |
michael@0 | 692 | bool check = CheckReferences(aObject); |
michael@0 | 693 | NS_ENSURE_TRUE(check, NS_ERROR_FAILURE); |
michael@0 | 694 | return mBinaryStream->WriteSingleRefObject(aObject); |
michael@0 | 695 | } |
michael@0 | 696 | |
michael@0 | 697 | nsresult |
michael@0 | 698 | StartupCacheDebugOutputStream::WriteCompoundObject(nsISupports* aObject, |
michael@0 | 699 | const nsIID& aIID, |
michael@0 | 700 | bool aIsStrongRef) |
michael@0 | 701 | { |
michael@0 | 702 | nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject)); |
michael@0 | 703 | |
michael@0 | 704 | nsCOMPtr<nsISupports> roundtrip; |
michael@0 | 705 | rootObject->QueryInterface(aIID, getter_AddRefs(roundtrip)); |
michael@0 | 706 | NS_ASSERTION(roundtrip.get() == aObject, |
michael@0 | 707 | "bad aggregation or multiple inheritance detected by call to " |
michael@0 | 708 | "WriteCompoundObject!"); |
michael@0 | 709 | |
michael@0 | 710 | bool check = CheckReferences(aObject); |
michael@0 | 711 | NS_ENSURE_TRUE(check, NS_ERROR_FAILURE); |
michael@0 | 712 | return mBinaryStream->WriteCompoundObject(aObject, aIID, aIsStrongRef); |
michael@0 | 713 | } |
michael@0 | 714 | |
michael@0 | 715 | nsresult |
michael@0 | 716 | StartupCacheDebugOutputStream::WriteID(nsID const& aID) |
michael@0 | 717 | { |
michael@0 | 718 | return mBinaryStream->WriteID(aID); |
michael@0 | 719 | } |
michael@0 | 720 | |
michael@0 | 721 | char* |
michael@0 | 722 | StartupCacheDebugOutputStream::GetBuffer(uint32_t aLength, uint32_t aAlignMask) |
michael@0 | 723 | { |
michael@0 | 724 | return mBinaryStream->GetBuffer(aLength, aAlignMask); |
michael@0 | 725 | } |
michael@0 | 726 | |
michael@0 | 727 | void |
michael@0 | 728 | StartupCacheDebugOutputStream::PutBuffer(char* aBuffer, uint32_t aLength) |
michael@0 | 729 | { |
michael@0 | 730 | mBinaryStream->PutBuffer(aBuffer, aLength); |
michael@0 | 731 | } |
michael@0 | 732 | #endif //DEBUG |
michael@0 | 733 | |
michael@0 | 734 | StartupCacheWrapper* StartupCacheWrapper::gStartupCacheWrapper = nullptr; |
michael@0 | 735 | |
michael@0 | 736 | NS_IMPL_ISUPPORTS(StartupCacheWrapper, nsIStartupCache) |
michael@0 | 737 | |
michael@0 | 738 | StartupCacheWrapper* StartupCacheWrapper::GetSingleton() |
michael@0 | 739 | { |
michael@0 | 740 | if (!gStartupCacheWrapper) |
michael@0 | 741 | gStartupCacheWrapper = new StartupCacheWrapper(); |
michael@0 | 742 | |
michael@0 | 743 | NS_ADDREF(gStartupCacheWrapper); |
michael@0 | 744 | return gStartupCacheWrapper; |
michael@0 | 745 | } |
michael@0 | 746 | |
michael@0 | 747 | nsresult |
michael@0 | 748 | StartupCacheWrapper::GetBuffer(const char* id, char** outbuf, uint32_t* length) |
michael@0 | 749 | { |
michael@0 | 750 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 751 | if (!sc) { |
michael@0 | 752 | return NS_ERROR_NOT_INITIALIZED; |
michael@0 | 753 | } |
michael@0 | 754 | return sc->GetBuffer(id, outbuf, length); |
michael@0 | 755 | } |
michael@0 | 756 | |
michael@0 | 757 | nsresult |
michael@0 | 758 | StartupCacheWrapper::PutBuffer(const char* id, const char* inbuf, uint32_t length) |
michael@0 | 759 | { |
michael@0 | 760 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 761 | if (!sc) { |
michael@0 | 762 | return NS_ERROR_NOT_INITIALIZED; |
michael@0 | 763 | } |
michael@0 | 764 | return sc->PutBuffer(id, inbuf, length); |
michael@0 | 765 | } |
michael@0 | 766 | |
michael@0 | 767 | nsresult |
michael@0 | 768 | StartupCacheWrapper::InvalidateCache() |
michael@0 | 769 | { |
michael@0 | 770 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 771 | if (!sc) { |
michael@0 | 772 | return NS_ERROR_NOT_INITIALIZED; |
michael@0 | 773 | } |
michael@0 | 774 | sc->InvalidateCache(); |
michael@0 | 775 | return NS_OK; |
michael@0 | 776 | } |
michael@0 | 777 | |
michael@0 | 778 | nsresult |
michael@0 | 779 | StartupCacheWrapper::IgnoreDiskCache() |
michael@0 | 780 | { |
michael@0 | 781 | StartupCache::IgnoreDiskCache(); |
michael@0 | 782 | return NS_OK; |
michael@0 | 783 | } |
michael@0 | 784 | |
michael@0 | 785 | nsresult |
michael@0 | 786 | StartupCacheWrapper::GetDebugObjectOutputStream(nsIObjectOutputStream* stream, |
michael@0 | 787 | nsIObjectOutputStream** outStream) |
michael@0 | 788 | { |
michael@0 | 789 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 790 | if (!sc) { |
michael@0 | 791 | return NS_ERROR_NOT_INITIALIZED; |
michael@0 | 792 | } |
michael@0 | 793 | return sc->GetDebugObjectOutputStream(stream, outStream); |
michael@0 | 794 | } |
michael@0 | 795 | |
michael@0 | 796 | nsresult |
michael@0 | 797 | StartupCacheWrapper::StartupWriteComplete(bool *complete) |
michael@0 | 798 | { |
michael@0 | 799 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 800 | if (!sc) { |
michael@0 | 801 | return NS_ERROR_NOT_INITIALIZED; |
michael@0 | 802 | } |
michael@0 | 803 | sc->WaitOnWriteThread(); |
michael@0 | 804 | *complete = sc->mStartupWriteInitiated && sc->mTable.Count() == 0; |
michael@0 | 805 | return NS_OK; |
michael@0 | 806 | } |
michael@0 | 807 | |
michael@0 | 808 | nsresult |
michael@0 | 809 | StartupCacheWrapper::ResetStartupWriteTimer() |
michael@0 | 810 | { |
michael@0 | 811 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 812 | return sc ? sc->ResetStartupWriteTimer() : NS_ERROR_NOT_INITIALIZED; |
michael@0 | 813 | } |
michael@0 | 814 | |
michael@0 | 815 | nsresult |
michael@0 | 816 | StartupCacheWrapper::GetObserver(nsIObserver** obv) { |
michael@0 | 817 | StartupCache* sc = StartupCache::GetSingleton(); |
michael@0 | 818 | if (!sc) { |
michael@0 | 819 | return NS_ERROR_NOT_INITIALIZED; |
michael@0 | 820 | } |
michael@0 | 821 | NS_ADDREF(*obv = sc->mListener); |
michael@0 | 822 | return NS_OK; |
michael@0 | 823 | } |
michael@0 | 824 | |
michael@0 | 825 | nsresult |
michael@0 | 826 | StartupCacheWrapper::RecordAgesAlways() { |
michael@0 | 827 | StartupCache *sc = StartupCache::GetSingleton(); |
michael@0 | 828 | return sc ? sc->RecordAgesAlways() : NS_ERROR_NOT_INITIALIZED; |
michael@0 | 829 | } |
michael@0 | 830 | |
michael@0 | 831 | } // namespace scache |
michael@0 | 832 | } // namespace mozilla |