/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AsmJSCache.h" #include #include "js/RootingAPI.h" #include "jsfriendapi.h" #include "mozilla/CondVar.h" #include "mozilla/dom/asmjscache/PAsmJSCacheEntryChild.h" #include "mozilla/dom/asmjscache/PAsmJSCacheEntryParent.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/PermissionMessageUtils.h" #include "mozilla/dom/quota/Client.h" #include "mozilla/dom/quota/OriginOrPatternString.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/dom/quota/QuotaObject.h" #include "mozilla/dom/quota/UsageInfo.h" #include "mozilla/unused.h" #include "nsIAtom.h" #include "nsIFile.h" #include "nsIPrincipal.h" #include "nsIRunnable.h" #include "nsIThread.h" #include "nsIXULAppInfo.h" #include "nsJSPrincipals.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" #include "prio.h" #include "private/pprio.h" #define ASMJSCACHE_FILE_NAME "module" using mozilla::dom::quota::AssertIsOnIOThread; using mozilla::dom::quota::OriginOrPatternString; using mozilla::dom::quota::PersistenceType; using mozilla::dom::quota::QuotaManager; using mozilla::dom::quota::QuotaObject; using mozilla::dom::quota::UsageInfo; using mozilla::unused; namespace mozilla { namespace dom { namespace asmjscache { namespace { bool IsMainProcess() { return XRE_GetProcessType() == GeckoProcessType_Default; } // FileDescriptorHolder owns a file descriptor and its memory mapping. // FileDescriptorHolder is derived by all three runnable classes (that is, // (Single|Parent|Child)ProcessRunnable. To avoid awkward workarouds, // FileDescriptorHolder is derived virtually by File and MainProcessRunnable for // the benefit of SingleProcessRunnable, which derives both. Since File and // MainProcessRunnable both need to be runnables, FileDescriptorHolder also // derives nsRunnable. class FileDescriptorHolder : public nsRunnable { public: FileDescriptorHolder() : mQuotaObject(nullptr), mFileSize(INT64_MIN), mFileDesc(nullptr), mFileMap(nullptr), mMappedMemory(nullptr) { } ~FileDescriptorHolder() { // These resources should have already been released by Finish(). MOZ_ASSERT(!mQuotaObject); MOZ_ASSERT(!mMappedMemory); MOZ_ASSERT(!mFileMap); MOZ_ASSERT(!mFileDesc); } size_t FileSize() const { MOZ_ASSERT(mFileSize >= 0, "Accessing FileSize of unopened file"); return mFileSize; } PRFileDesc* FileDesc() const { MOZ_ASSERT(mFileDesc, "Accessing FileDesc of unopened file"); return mFileDesc; } bool MapMemory(OpenMode aOpenMode) { MOZ_ASSERT(!mFileMap, "Cannot call MapMemory twice"); PRFileMapProtect mapFlags = aOpenMode == eOpenForRead ? PR_PROT_READONLY : PR_PROT_READWRITE; mFileMap = PR_CreateFileMap(mFileDesc, mFileSize, mapFlags); NS_ENSURE_TRUE(mFileMap, false); mMappedMemory = PR_MemMap(mFileMap, 0, mFileSize); NS_ENSURE_TRUE(mMappedMemory, false); return true; } void* MappedMemory() const { MOZ_ASSERT(mMappedMemory, "Accessing MappedMemory of un-mapped file"); return mMappedMemory; } protected: // This method must be called before AllowNextSynchronizedOp (which releases // the lock protecting these resources). It is idempotent, so it is ok to call // multiple times (or before the file has been fully opened). void Finish() { if (mMappedMemory) { PR_MemUnmap(mMappedMemory, mFileSize); mMappedMemory = nullptr; } if (mFileMap) { PR_CloseFileMap(mFileMap); mFileMap = nullptr; } if (mFileDesc) { PR_Close(mFileDesc); mFileDesc = nullptr; } // Holding the QuotaObject alive until all the cache files are closed enables // assertions in QuotaManager that the cache entry isn't cleared while we // are working on it. mQuotaObject = nullptr; } nsRefPtr mQuotaObject; int64_t mFileSize; PRFileDesc* mFileDesc; PRFileMap* mFileMap; void* mMappedMemory; }; // File is a base class shared by (Single|Client)ProcessEntryRunnable that // presents a single interface to the AsmJSCache ops which need to wait until // the file is open, regardless of whether we are executing in the main process // or not. class File : public virtual FileDescriptorHolder { public: class AutoClose { File* mFile; public: explicit AutoClose(File* aFile = nullptr) : mFile(aFile) { } void Init(File* aFile) { MOZ_ASSERT(!mFile); mFile = aFile; } File* operator->() const { MOZ_ASSERT(mFile); return mFile; } void Forget(File** aFile) { *aFile = mFile; mFile = nullptr; } ~AutoClose() { if (mFile) { mFile->Close(); } } }; bool BlockUntilOpen(AutoClose *aCloser) { MOZ_ASSERT(!mWaiting, "Can only call BlockUntilOpen once"); MOZ_ASSERT(!mOpened, "Can only call BlockUntilOpen once"); mWaiting = true; nsresult rv = NS_DispatchToMainThread(this); NS_ENSURE_SUCCESS(rv, false); { MutexAutoLock lock(mMutex); while (mWaiting) { mCondVar.Wait(); } } if (!mOpened) { return false; } // Now that we're open, we're guarnateed a Close() call. However, we are // not guarnateed someone is holding an outstanding reference until the File // is closed, so we do that ourselves and Release() in OnClose(). aCloser->Init(this); AddRef(); return true; } // This method must be called if BlockUntilOpen returns 'true'. AutoClose // mostly takes care of this. A derived class that implements Close() must // guarnatee that OnClose() is called (eventually). virtual void Close() = 0; protected: File() : mMutex("File::mMutex"), mCondVar(mMutex, "File::mCondVar"), mWaiting(false), mOpened(false) { } ~File() { MOZ_ASSERT(!mWaiting, "Shouldn't be destroyed while thread is waiting"); MOZ_ASSERT(!mOpened, "OnClose() should have been called"); } void OnOpen() { Notify(true); } void OnFailure() { FileDescriptorHolder::Finish(); Notify(false); } void OnClose() { FileDescriptorHolder::Finish(); MOZ_ASSERT(mOpened); mOpened = false; // Match the AddRef in BlockUntilOpen(). The main thread event loop still // holds an outstanding ref which will keep 'this' alive until returning to // the event loop. Release(); } private: void Notify(bool aSuccess) { MOZ_ASSERT(NS_IsMainThread()); MutexAutoLock lock(mMutex); MOZ_ASSERT(mWaiting); mWaiting = false; mOpened = aSuccess; mCondVar.Notify(); } Mutex mMutex; CondVar mCondVar; bool mWaiting; bool mOpened; }; // MainProcessRunnable is a base class shared by (Single|Parent)ProcessRunnable // that factors out the runnable state machine required to open a cache entry // that runs in the main process. class MainProcessRunnable : public virtual FileDescriptorHolder { public: NS_DECL_NSIRUNNABLE // MainProcessRunnable runnable assumes that the derived class ensures // (through ref-counting or preconditions) that aPrincipal is kept alive for // the lifetime of the MainProcessRunnable. MainProcessRunnable(nsIPrincipal* aPrincipal, OpenMode aOpenMode, size_t aSizeToWrite) : mPrincipal(aPrincipal), mOpenMode(aOpenMode), mSizeToWrite(aSizeToWrite), mNeedAllowNextSynchronizedOp(false), mState(eInitial) { MOZ_ASSERT(IsMainProcess()); } virtual ~MainProcessRunnable() { MOZ_ASSERT(mState == eFinished); MOZ_ASSERT(!mNeedAllowNextSynchronizedOp); } protected: // This method is be called by the derived class (either on the JS // compilation thread or the main thread) when JS engine is finished // reading/writing the cache entry. void Close() { MOZ_ASSERT(mState == eOpened); mState = eClosing; NS_DispatchToMainThread(this); } // This method is called both internally and by derived classes upon any // failure that prevents the eventual opening of the cache entry. void Fail() { MOZ_ASSERT(mState == eInitial || mState == eWaitingToOpen || mState == eReadyToOpen || mState == eNotifying); mState = eFailing; NS_DispatchToMainThread(this); } // Called by MainProcessRunnable on the main thread after the entry is open: virtual void OnOpen() = 0; // This method may be overridden, but it must be called from the overrider. // Called by MainProcessRunnable on the main thread after a call to Fail(): virtual void OnFailure() { FinishOnMainThread(); } // This method may be overridden, but it must be called from the overrider. // Called by MainProcessRunnable on the main thread after a call to Close(): virtual void OnClose() { FinishOnMainThread(); } private: nsresult InitOnMainThread(); nsresult OpenFileOnIOThread(); void FinishOnMainThread(); nsIPrincipal* const mPrincipal; const OpenMode mOpenMode; const size_t mSizeToWrite; // State initialized during eInitial: bool mNeedAllowNextSynchronizedOp; nsCString mGroup; nsCString mOrigin; nsCString mStorageId; enum State { eInitial, // Just created, waiting to be dispatched to main thread eWaitingToOpen, // Waiting to be called back from WaitForOpenAllowed eReadyToOpen, // Waiting to be dispatched to the IO thread eNotifying, // Waiting to be dispatched to main thread to notify of success eOpened, // Finished calling OnOpen from main thread, waiting to be closed eClosing, // Waiting to be dispatched to main thread again eFailing, // Just failed, waiting to be dispatched to the main thread eFinished, // Terminal state }; State mState; }; nsresult MainProcessRunnable::InitOnMainThread() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == eInitial); QuotaManager* qm = QuotaManager::GetOrCreate(); NS_ENSURE_STATE(qm); nsresult rv = QuotaManager::GetInfoFromPrincipal(mPrincipal, &mGroup, &mOrigin, nullptr, nullptr); NS_ENSURE_SUCCESS(rv, rv); QuotaManager::GetStorageId(quota::PERSISTENCE_TYPE_TEMPORARY, mOrigin, quota::Client::ASMJS, NS_LITERAL_STRING(ASMJSCACHE_FILE_NAME), mStorageId); return NS_OK; } nsresult MainProcessRunnable::OpenFileOnIOThread() { AssertIsOnIOThread(); MOZ_ASSERT(mState == eReadyToOpen); QuotaManager* qm = QuotaManager::Get(); MOZ_ASSERT(qm, "We are on the QuotaManager's IO thread"); nsCOMPtr path; nsresult rv = qm->EnsureOriginIsInitialized(quota::PERSISTENCE_TYPE_TEMPORARY, mGroup, mOrigin, true, getter_AddRefs(path)); NS_ENSURE_SUCCESS(rv, rv); rv = path->Append(NS_LITERAL_STRING(ASMJSCACHE_DIRECTORY_NAME)); NS_ENSURE_SUCCESS(rv, rv); bool exists; rv = path->Exists(&exists); NS_ENSURE_SUCCESS(rv, rv); if (!exists) { rv = path->Create(nsIFile::DIRECTORY_TYPE, 0755); NS_ENSURE_SUCCESS(rv, rv); } else { DebugOnly isDirectory; MOZ_ASSERT(NS_SUCCEEDED(path->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory, "Should have caught this earlier!"); } rv = path->Append(NS_LITERAL_STRING(ASMJSCACHE_FILE_NAME)); NS_ENSURE_SUCCESS(rv, rv); mQuotaObject = qm->GetQuotaObject(quota::PERSISTENCE_TYPE_TEMPORARY, mGroup, mOrigin, path); NS_ENSURE_STATE(mQuotaObject); PRIntn openFlags; if (mOpenMode == eOpenForRead) { rv = path->GetFileSize(&mFileSize); if (NS_FAILED(rv)) { return rv; } openFlags = PR_RDONLY | nsIFile::OS_READAHEAD; } else { if (!mQuotaObject->MaybeAllocateMoreSpace(0, mSizeToWrite)) { return NS_ERROR_FAILURE; } mFileSize = mSizeToWrite; MOZ_ASSERT(mOpenMode == eOpenForWrite); openFlags = PR_RDWR | PR_TRUNCATE | PR_CREATE_FILE; } rv = path->OpenNSPRFileDesc(openFlags, 0644, &mFileDesc); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } void MainProcessRunnable::FinishOnMainThread() { MOZ_ASSERT(NS_IsMainThread()); // Per FileDescriptorHolder::Finish()'s comment, call before // AllowNextSynchronizedOp. FileDescriptorHolder::Finish(); if (mNeedAllowNextSynchronizedOp) { mNeedAllowNextSynchronizedOp = false; QuotaManager* qm = QuotaManager::Get(); if (qm) { qm->AllowNextSynchronizedOp(OriginOrPatternString::FromOrigin(mOrigin), Nullable(), mStorageId); } } } NS_IMETHODIMP MainProcessRunnable::Run() { nsresult rv; // All success/failure paths must eventually call Finish() to avoid leaving // the parser hanging. switch (mState) { case eInitial: { MOZ_ASSERT(NS_IsMainThread()); rv = InitOnMainThread(); if (NS_FAILED(rv)) { Fail(); return NS_OK; } mState = eWaitingToOpen; rv = QuotaManager::Get()->WaitForOpenAllowed( OriginOrPatternString::FromOrigin(mOrigin), Nullable(), mStorageId, this); if (NS_FAILED(rv)) { Fail(); return NS_OK; } mNeedAllowNextSynchronizedOp = true; return NS_OK; } case eWaitingToOpen: { MOZ_ASSERT(NS_IsMainThread()); mState = eReadyToOpen; QuotaManager* qm = QuotaManager::Get(); if (!qm) { Fail(); return NS_OK; } rv = qm->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_FAILED(rv)) { Fail(); return NS_OK; } return NS_OK; } case eReadyToOpen: { AssertIsOnIOThread(); rv = OpenFileOnIOThread(); if (NS_FAILED(rv)) { Fail(); return NS_OK; } mState = eNotifying; NS_DispatchToMainThread(this); return NS_OK; } case eNotifying: { MOZ_ASSERT(NS_IsMainThread()); mState = eOpened; OnOpen(); return NS_OK; } case eFailing: { MOZ_ASSERT(NS_IsMainThread()); mState = eFinished; OnFailure(); return NS_OK; } case eClosing: { MOZ_ASSERT(NS_IsMainThread()); mState = eFinished; OnClose(); return NS_OK; } case eOpened: case eFinished: { MOZ_ASSUME_UNREACHABLE("Shouldn't Run() in this state"); } } MOZ_ASSUME_UNREACHABLE("Corrupt state"); return NS_OK; } // A runnable that executes for a cache access originating in the main process. class SingleProcessRunnable MOZ_FINAL : public File, private MainProcessRunnable { public: // In the single-process case, the calling JS compilation thread holds the // nsIPrincipal alive indirectly (via the global object -> compartment -> // principal) so we don't have to ref-count it here. This is fortunate since // we are off the main thread and nsIPrincipals can only be ref-counted on // the main thread. SingleProcessRunnable(nsIPrincipal* aPrincipal, OpenMode aOpenMode, size_t aSizeToWrite) : MainProcessRunnable(aPrincipal, aOpenMode, aSizeToWrite) { MOZ_ASSERT(IsMainProcess()); MOZ_ASSERT(!NS_IsMainThread()); MOZ_COUNT_CTOR(SingleProcessRunnable); } ~SingleProcessRunnable() { MOZ_COUNT_DTOR(SingleProcessRunnable); } private: void Close() MOZ_OVERRIDE MOZ_FINAL { MainProcessRunnable::Close(); } void OnOpen() MOZ_OVERRIDE { File::OnOpen(); } void OnFailure() MOZ_OVERRIDE { MainProcessRunnable::OnFailure(); File::OnFailure(); } void OnClose() MOZ_OVERRIDE MOZ_FINAL { MainProcessRunnable::OnClose(); File::OnClose(); } // Avoid MSVC 'dominance' warning by having clear Run() override. NS_IMETHODIMP Run() MOZ_OVERRIDE { return MainProcessRunnable::Run(); } }; // A runnable that executes in a parent process for a cache access originating // in the content process. This runnable gets registered as an IPDL subprotocol // actor so that it can communicate with the corresponding ChildProcessRunnable. class ParentProcessRunnable MOZ_FINAL : public PAsmJSCacheEntryParent, public MainProcessRunnable { public: // The given principal comes from an IPC::Principal which will be dec-refed // at the end of the message, so we must ref-count it here. Fortunately, we // are on the main thread (where PContent messages are delivered). ParentProcessRunnable(nsIPrincipal* aPrincipal, OpenMode aOpenMode, size_t aSizeToWrite) : MainProcessRunnable(aPrincipal, aOpenMode, aSizeToWrite), mPrincipalHolder(aPrincipal), mActorDestroyed(false), mOpened(false), mFinished(false) { MOZ_ASSERT(IsMainProcess()); MOZ_ASSERT(NS_IsMainThread()); MOZ_COUNT_CTOR(ParentProcessRunnable); } private: ~ParentProcessRunnable() { MOZ_ASSERT(!mPrincipalHolder, "Should have already been released"); MOZ_ASSERT(mActorDestroyed); MOZ_ASSERT(mFinished); MOZ_COUNT_DTOR(ParentProcessRunnable); } bool Recv__delete__() MOZ_OVERRIDE { MOZ_ASSERT(mOpened); MOZ_ASSERT(!mFinished); mFinished = true; MainProcessRunnable::Close(); return true; } void ActorDestroy(ActorDestroyReason why) MOZ_OVERRIDE { MOZ_ASSERT(!mActorDestroyed); mActorDestroyed = true; // Assume ActorDestroy can happen at any time, so probe the current state to // determine what needs to happen. if (mFinished) { return; } mFinished = true; if (mOpened) { MainProcessRunnable::Close(); } else { MainProcessRunnable::Fail(); } } void OnOpen() MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!mOpened); mOpened = true; FileDescriptor::PlatformHandleType handle = FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(mFileDesc)); if (!SendOnOpen(mFileSize, handle)) { unused << Send__delete__(this); } } void OnClose() MOZ_OVERRIDE MOZ_FINAL { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mOpened); mFinished = true; MainProcessRunnable::OnClose(); MOZ_ASSERT(mActorDestroyed); mPrincipalHolder = nullptr; } void OnFailure() MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!mOpened); mFinished = true; MainProcessRunnable::OnFailure(); if (!mActorDestroyed) { unused << Send__delete__(this); } mPrincipalHolder = nullptr; } nsCOMPtr mPrincipalHolder; bool mActorDestroyed; bool mOpened; bool mFinished; }; } // unnamed namespace PAsmJSCacheEntryParent* AllocEntryParent(OpenMode aOpenMode, uint32_t aSizeToWrite, nsIPrincipal* aPrincipal) { ParentProcessRunnable* runnable = new ParentProcessRunnable(aPrincipal, aOpenMode, aSizeToWrite); // AddRef to keep the runnable alive until DeallocEntryParent. runnable->AddRef(); nsresult rv = NS_DispatchToMainThread(runnable); NS_ENSURE_SUCCESS(rv, nullptr); return runnable; } void DeallocEntryParent(PAsmJSCacheEntryParent* aActor) { // Match the AddRef in AllocEntryParent. static_cast(aActor)->Release(); } namespace { class ChildProcessRunnable MOZ_FINAL : public File, public PAsmJSCacheEntryChild { public: NS_DECL_NSIRUNNABLE // In the single-process case, the calling JS compilation thread holds the // nsIPrincipal alive indirectly (via the global object -> compartment -> // principal) so we don't have to ref-count it here. This is fortunate since // we are off the main thread and nsIPrincipals can only be ref-counted on // the main thread. ChildProcessRunnable(nsIPrincipal* aPrincipal, OpenMode aOpenMode, size_t aSizeToWrite) : mPrincipal(aPrincipal), mOpenMode(aOpenMode), mSizeToWrite(aSizeToWrite), mActorDestroyed(false), mState(eInitial) { MOZ_ASSERT(!IsMainProcess()); MOZ_ASSERT(!NS_IsMainThread()); MOZ_COUNT_CTOR(ChildProcessRunnable); } ~ChildProcessRunnable() { MOZ_ASSERT(mState == eFinished); MOZ_ASSERT(mActorDestroyed); MOZ_COUNT_DTOR(ChildProcessRunnable); } private: bool RecvOnOpen(const int64_t& aFileSize, const FileDescriptor& aFileDesc) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == eOpening); mFileSize = aFileSize; mFileDesc = PR_ImportFile(PROsfd(aFileDesc.PlatformHandle())); if (!mFileDesc) { return false; } mState = eOpened; File::OnOpen(); return true; } bool Recv__delete__() MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == eOpening); mState = eFinished; File::OnFailure(); return true; } void ActorDestroy(ActorDestroyReason why) MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); mActorDestroyed = true; } void Close() MOZ_OVERRIDE MOZ_FINAL { MOZ_ASSERT(mState == eOpened); mState = eClosing; NS_DispatchToMainThread(this); } private: nsIPrincipal* const mPrincipal; const OpenMode mOpenMode; size_t mSizeToWrite; bool mActorDestroyed; enum State { eInitial, // Just created, waiting to dispatched to the main thread eOpening, // Waiting for the parent process to respond eOpened, // Parent process opened the entry and sent it back eClosing, // Waiting to be dispatched to the main thread to Send__delete__ eFinished // Terminal state }; State mState; }; NS_IMETHODIMP ChildProcessRunnable::Run() { switch (mState) { case eInitial: { MOZ_ASSERT(NS_IsMainThread()); // AddRef to keep this runnable alive until IPDL deallocates the // subprotocol (DeallocEntryChild). AddRef(); if (!ContentChild::GetSingleton()->SendPAsmJSCacheEntryConstructor( this, mOpenMode, mSizeToWrite, IPC::Principal(mPrincipal))) { // On failure, undo the AddRef (since DeallocEntryChild will not be // called) and unblock the parsing thread with a failure. The main // thread event loop still holds an outstanding ref which will keep // 'this' alive until returning to the event loop. Release(); mState = eFinished; File::OnFailure(); return NS_OK; } mState = eOpening; return NS_OK; } case eClosing: { MOZ_ASSERT(NS_IsMainThread()); // Per FileDescriptorHolder::Finish()'s comment, call before // AllowNextSynchronizedOp (which happens in the parent upon receipt of // the Send__delete__ message). File::OnClose(); if (!mActorDestroyed) { unused << Send__delete__(this); } mState = eFinished; return NS_OK; } case eOpening: case eOpened: case eFinished: { MOZ_ASSUME_UNREACHABLE("Shouldn't Run() in this state"); } } MOZ_ASSUME_UNREACHABLE("Corrupt state"); return NS_OK; } } // unnamed namespace void DeallocEntryChild(PAsmJSCacheEntryChild* aActor) { // Match the AddRef before SendPAsmJSCacheEntryConstructor. static_cast(aActor)->Release(); } namespace { bool OpenFile(nsIPrincipal* aPrincipal, OpenMode aOpenMode, size_t aSizeToWrite, File::AutoClose* aFile) { MOZ_ASSERT_IF(aOpenMode == eOpenForRead, aSizeToWrite == 0); // There are three reasons we don't attempt caching from the main thread: // 1. In the parent process: QuotaManager::WaitForOpenAllowed prevents // synchronous waiting on the main thread requiring a runnable to be // dispatched to the main thread. // 2. In the child process: the IPDL PContent messages we need to // synchronously wait on are dispatched to the main thread. // 3. While a cache lookup *should* be much faster than compilation, IO // operations can be unpredictably slow and we'd like to avoid the // occasional janks on the main thread. // We could use a nested event loop to address 1 and 2, but we're potentially // in the middle of running JS (eval()) and nested event loops can be // semantically observable. if (NS_IsMainThread()) { return false; } // If we are in a child process, we need to synchronously call into the // parent process to open the file and interact with the QuotaManager. The // child can then map the file into its address space to perform I/O. nsRefPtr file; if (IsMainProcess()) { file = new SingleProcessRunnable(aPrincipal, aOpenMode, aSizeToWrite); } else { file = new ChildProcessRunnable(aPrincipal, aOpenMode, aSizeToWrite); } if (!file->BlockUntilOpen(aFile)) { return false; } return file->MapMemory(aOpenMode); } } // anonymous namespace typedef uint32_t AsmJSCookieType; static const uint32_t sAsmJSCookie = 0x600d600d; // Anything smaller should compile fast enough that caching will just add // overhead. static const size_t sMinCachedModuleLength = 10000; bool OpenEntryForRead(nsIPrincipal* aPrincipal, const jschar* aBegin, const jschar* aLimit, size_t* aSize, const uint8_t** aMemory, intptr_t* aFile) { if (size_t(aLimit - aBegin) < sMinCachedModuleLength) { return false; } File::AutoClose file; if (!OpenFile(aPrincipal, eOpenForRead, 0, &file)) { return false; } // Although we trust that the stored cache files have not been arbitrarily // corrupted, it is possible that a previous execution aborted in the middle // of writing a cache file (crash, OOM-killer, etc). To protect against // partially-written cache files, we use the following scheme: // - Allocate an extra word at the beginning of every cache file which // starts out 0 (OpenFile opens with PR_TRUNCATE). // - After the asm.js serialization is complete, PR_SyncMemMap to write // everything to disk and then store a non-zero value (sAsmJSCookie) // in the first word. // - When attempting to read a cache file, check whether the first word is // sAsmJSCookie. if (file->FileSize() < sizeof(AsmJSCookieType) || *(AsmJSCookieType*)file->MappedMemory() != sAsmJSCookie) { return false; } *aSize = file->FileSize() - sizeof(AsmJSCookieType); *aMemory = (uint8_t*) file->MappedMemory() + sizeof(AsmJSCookieType); // The caller guarnatees a call to CloseEntryForRead (on success or // failure) at which point the file will be closed. file.Forget(reinterpret_cast(aFile)); return true; } void CloseEntryForRead(JS::Handle global, size_t aSize, const uint8_t* aMemory, intptr_t aFile) { File::AutoClose file(reinterpret_cast(aFile)); MOZ_ASSERT(aSize + sizeof(AsmJSCookieType) == file->FileSize()); MOZ_ASSERT(aMemory - sizeof(AsmJSCookieType) == file->MappedMemory()); } bool OpenEntryForWrite(nsIPrincipal* aPrincipal, const jschar* aBegin, const jschar* aEnd, size_t aSize, uint8_t** aMemory, intptr_t* aFile) { if (size_t(aEnd - aBegin) < sMinCachedModuleLength) { return false; } // Add extra space for the AsmJSCookieType (see OpenEntryForRead). aSize += sizeof(AsmJSCookieType); File::AutoClose file; if (!OpenFile(aPrincipal, eOpenForWrite, aSize, &file)) { return false; } // Strip off the AsmJSCookieType from the buffer returned to the caller, // which expects a buffer of aSize, not a buffer of sizeWithCookie starting // with a cookie. *aMemory = (uint8_t*) file->MappedMemory() + sizeof(AsmJSCookieType); // The caller guarnatees a call to CloseEntryForWrite (on success or // failure) at which point the file will be closed file.Forget(reinterpret_cast(aFile)); return true; } void CloseEntryForWrite(JS::Handle global, size_t aSize, uint8_t* aMemory, intptr_t aFile) { File::AutoClose file(reinterpret_cast(aFile)); MOZ_ASSERT(aSize + sizeof(AsmJSCookieType) == file->FileSize()); MOZ_ASSERT(aMemory - sizeof(AsmJSCookieType) == file->MappedMemory()); // Flush to disk before writing the cookie (see OpenEntryForRead). if (PR_SyncMemMap(file->FileDesc(), file->MappedMemory(), file->FileSize()) == PR_SUCCESS) { *(AsmJSCookieType*)file->MappedMemory() = sAsmJSCookie; } } bool GetBuildId(js::Vector* aBuildID) { nsCOMPtr info = do_GetService("@mozilla.org/xre/app-info;1"); if (!info) { return false; } nsCString buildID; nsresult rv = info->GetPlatformBuildID(buildID); NS_ENSURE_SUCCESS(rv, false); if (!aBuildID->resize(buildID.Length())) { return false; } for (size_t i = 0; i < buildID.Length(); i++) { (*aBuildID)[i] = buildID[i]; } return true; } class Client : public quota::Client { public: NS_IMETHOD_(nsrefcnt) AddRef() MOZ_OVERRIDE; NS_IMETHOD_(nsrefcnt) Release() MOZ_OVERRIDE; virtual Type GetType() MOZ_OVERRIDE { return ASMJS; } virtual nsresult InitOrigin(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, UsageInfo* aUsageInfo) MOZ_OVERRIDE { return GetUsageForOrigin(aPersistenceType, aGroup, aOrigin, aUsageInfo); } virtual nsresult GetUsageForOrigin(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, UsageInfo* aUsageInfo) MOZ_OVERRIDE { QuotaManager* qm = QuotaManager::Get(); MOZ_ASSERT(qm, "We were being called by the QuotaManager"); nsCOMPtr directory; nsresult rv = qm->GetDirectoryForOrigin(aPersistenceType, aOrigin, getter_AddRefs(directory)); NS_ENSURE_SUCCESS(rv, rv); MOZ_ASSERT(directory, "We're here because the origin directory exists"); rv = directory->Append(NS_LITERAL_STRING(ASMJSCACHE_DIRECTORY_NAME)); NS_ENSURE_SUCCESS(rv, rv); bool exists; MOZ_ASSERT(NS_SUCCEEDED(directory->Exists(&exists)) && exists); nsIFile* path = directory; rv = path->Append(NS_LITERAL_STRING(ASMJSCACHE_FILE_NAME)); NS_ENSURE_SUCCESS(rv, rv); rv = path->Exists(&exists); NS_ENSURE_SUCCESS(rv, rv); if (exists) { int64_t fileSize; rv = path->GetFileSize(&fileSize); NS_ENSURE_SUCCESS(rv, rv); MOZ_ASSERT(fileSize >= 0, "Negative size?!"); // Since the client is not explicitly storing files, append to database // usage which represents implicit storage allocation. aUsageInfo->AppendToDatabaseUsage(uint64_t(fileSize)); } return NS_OK; } virtual void OnOriginClearCompleted(PersistenceType aPersistenceType, const OriginOrPatternString& aOriginOrPattern) MOZ_OVERRIDE { } virtual void ReleaseIOThreadObjects() MOZ_OVERRIDE { } virtual bool IsFileServiceUtilized() MOZ_OVERRIDE { return false; } virtual bool IsTransactionServiceActivated() MOZ_OVERRIDE { return false; } virtual void WaitForStoragesToComplete(nsTArray& aStorages, nsIRunnable* aCallback) MOZ_OVERRIDE { MOZ_ASSUME_UNREACHABLE("There are no storages"); } virtual void AbortTransactionsForStorage(nsIOfflineStorage* aStorage) MOZ_OVERRIDE { MOZ_ASSUME_UNREACHABLE("There are no storages"); } virtual bool HasTransactionsForStorage(nsIOfflineStorage* aStorage) MOZ_OVERRIDE { return false; } virtual void ShutdownTransactionService() MOZ_OVERRIDE { } private: nsAutoRefCnt mRefCnt; NS_DECL_OWNINGTHREAD }; NS_IMPL_ADDREF(asmjscache::Client) NS_IMPL_RELEASE(asmjscache::Client) quota::Client* CreateClient() { return new Client(); } } // namespace asmjscache } // namespace dom } // namespace mozilla