From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- dom/asmjscache/AsmJSCache.cpp | 1935 ++++++++++++++++++++++++++++ dom/asmjscache/AsmJSCache.h | 194 +++ dom/asmjscache/PAsmJSCacheEntry.ipdl | 38 + dom/asmjscache/moz.build | 23 + dom/asmjscache/test/file_slow.js | 73 ++ dom/asmjscache/test/mochitest.ini | 10 + dom/asmjscache/test/test_cachingBasic.html | 70 + dom/asmjscache/test/test_cachingMulti.html | 85 ++ dom/asmjscache/test/test_slow.html | 48 + dom/asmjscache/test/test_workers.html | 74 ++ 10 files changed, 2550 insertions(+) create mode 100644 dom/asmjscache/AsmJSCache.cpp create mode 100644 dom/asmjscache/AsmJSCache.h create mode 100644 dom/asmjscache/PAsmJSCacheEntry.ipdl create mode 100644 dom/asmjscache/moz.build create mode 100644 dom/asmjscache/test/file_slow.js create mode 100644 dom/asmjscache/test/mochitest.ini create mode 100644 dom/asmjscache/test/test_cachingBasic.html create mode 100644 dom/asmjscache/test/test_cachingMulti.html create mode 100644 dom/asmjscache/test/test_slow.html create mode 100644 dom/asmjscache/test/test_workers.html (limited to 'dom/asmjscache') diff --git a/dom/asmjscache/AsmJSCache.cpp b/dom/asmjscache/AsmJSCache.cpp new file mode 100644 index 000000000..4afcc6d6b --- /dev/null +++ b/dom/asmjscache/AsmJSCache.cpp @@ -0,0 +1,1935 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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/Assertions.h" +#include "mozilla/CondVar.h" +#include "mozilla/CycleCollectedJSContext.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/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/Unused.h" +#include "nsAutoPtr.h" +#include "nsIAtom.h" +#include "nsIFile.h" +#include "nsIIPCBackgroundChildCreateCallback.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsIRunnable.h" +#include "nsISimpleEnumerator.h" +#include "nsIThread.h" +#include "nsJSPrincipals.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "prio.h" +#include "private/pprio.h" +#include "mozilla/Services.h" + +#define ASMJSCACHE_METADATA_FILE_NAME "metadata" +#define ASMJSCACHE_ENTRY_FILE_NAME_BASE "module" + +using mozilla::dom::quota::AssertIsOnIOThread; +using mozilla::dom::quota::DirectoryLock; +using mozilla::dom::quota::PersistenceType; +using mozilla::dom::quota::QuotaManager; +using mozilla::dom::quota::QuotaObject; +using mozilla::dom::quota::UsageInfo; +using mozilla::ipc::AssertIsOnBackgroundThread; +using mozilla::ipc::BackgroundChild; +using mozilla::ipc::IsOnBackgroundThread; +using mozilla::ipc::PBackgroundChild; +using mozilla::ipc::PrincipalInfo; +using mozilla::Unused; +using mozilla::HashString; + +namespace mozilla { + +MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, PRFileDesc, PR_Close); + +namespace dom { +namespace asmjscache { + +namespace { + +// Anything smaller should compile fast enough that caching will just add +// overhead. +// static const size_t sMinCachedModuleLength = 10000; + +// The number of characters to hash into the Metadata::Entry::mFastHash. +static const unsigned sNumFastHashChars = 4096; + +nsresult +WriteMetadataFile(nsIFile* aMetadataFile, const Metadata& aMetadata) +{ + int32_t openFlags = PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE; + + JS::BuildIdCharVector buildId; + bool ok = GetBuildId(&buildId); + NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY); + + ScopedPRFileDesc fd; + nsresult rv = aMetadataFile->OpenNSPRFileDesc(openFlags, 0644, &fd.rwget()); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t length = buildId.length(); + int32_t bytesWritten = PR_Write(fd, &length, sizeof(length)); + NS_ENSURE_TRUE(bytesWritten == sizeof(length), NS_ERROR_UNEXPECTED); + + bytesWritten = PR_Write(fd, buildId.begin(), length); + NS_ENSURE_TRUE(bytesWritten == int32_t(length), NS_ERROR_UNEXPECTED); + + bytesWritten = PR_Write(fd, &aMetadata, sizeof(aMetadata)); + NS_ENSURE_TRUE(bytesWritten == sizeof(aMetadata), NS_ERROR_UNEXPECTED); + + return NS_OK; +} + +nsresult +ReadMetadataFile(nsIFile* aMetadataFile, Metadata& aMetadata) +{ + int32_t openFlags = PR_RDONLY; + + ScopedPRFileDesc fd; + nsresult rv = aMetadataFile->OpenNSPRFileDesc(openFlags, 0644, &fd.rwget()); + NS_ENSURE_SUCCESS(rv, rv); + + // Read the buildid and check that it matches the current buildid + + JS::BuildIdCharVector currentBuildId; + bool ok = GetBuildId(¤tBuildId); + NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY); + + uint32_t length; + int32_t bytesRead = PR_Read(fd, &length, sizeof(length)); + NS_ENSURE_TRUE(bytesRead == sizeof(length), NS_ERROR_UNEXPECTED); + + NS_ENSURE_TRUE(currentBuildId.length() == length, NS_ERROR_UNEXPECTED); + + JS::BuildIdCharVector fileBuildId; + ok = fileBuildId.resize(length); + NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY); + + bytesRead = PR_Read(fd, fileBuildId.begin(), length); + NS_ENSURE_TRUE(bytesRead == int32_t(length), NS_ERROR_UNEXPECTED); + + for (uint32_t i = 0; i < length; i++) { + if (currentBuildId[i] != fileBuildId[i]) { + return NS_ERROR_FAILURE; + } + } + + // Read the Metadata struct + + bytesRead = PR_Read(fd, &aMetadata, sizeof(aMetadata)); + NS_ENSURE_TRUE(bytesRead == sizeof(aMetadata), NS_ERROR_UNEXPECTED); + + return NS_OK; +} + +nsresult +GetCacheFile(nsIFile* aDirectory, unsigned aModuleIndex, nsIFile** aCacheFile) +{ + nsCOMPtr cacheFile; + nsresult rv = aDirectory->Clone(getter_AddRefs(cacheFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString cacheFileName = NS_LITERAL_STRING(ASMJSCACHE_ENTRY_FILE_NAME_BASE); + cacheFileName.AppendInt(aModuleIndex); + rv = cacheFile->Append(cacheFileName); + NS_ENSURE_SUCCESS(rv, rv); + + cacheFile.forget(aCacheFile); + return NS_OK; +} + +class AutoDecreaseUsageForOrigin +{ + const nsACString& mGroup; + const nsACString& mOrigin; + +public: + uint64_t mFreed; + + AutoDecreaseUsageForOrigin(const nsACString& aGroup, + const nsACString& aOrigin) + + : mGroup(aGroup), + mOrigin(aOrigin), + mFreed(0) + { } + + ~AutoDecreaseUsageForOrigin() + { + AssertIsOnIOThread(); + + if (!mFreed) { + return; + } + + QuotaManager* qm = QuotaManager::Get(); + MOZ_ASSERT(qm, "We are on the QuotaManager's IO thread"); + + qm->DecreaseUsageForOrigin(quota::PERSISTENCE_TYPE_TEMPORARY, + mGroup, mOrigin, mFreed); + } +}; + +static void +EvictEntries(nsIFile* aDirectory, const nsACString& aGroup, + const nsACString& aOrigin, uint64_t aNumBytes, + Metadata& aMetadata) +{ + AssertIsOnIOThread(); + + AutoDecreaseUsageForOrigin usage(aGroup, aOrigin); + + for (int i = Metadata::kLastEntry; i >= 0 && usage.mFreed < aNumBytes; i--) { + Metadata::Entry& entry = aMetadata.mEntries[i]; + unsigned moduleIndex = entry.mModuleIndex; + + nsCOMPtr file; + nsresult rv = GetCacheFile(aDirectory, moduleIndex, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + bool exists; + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (exists) { + int64_t fileSize; + rv = file->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = file->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + usage.mFreed += fileSize; + } + + entry.clear(); + } +} + +// FileDescriptorHolder owns a file descriptor and its memory mapping. +// FileDescriptorHolder is derived by two runnable classes (that is, +// (Parent|Child)Runnable. +class FileDescriptorHolder : public Runnable +{ +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 the directory lock is released (the lock + // is 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; + } + + RefPtr mQuotaObject; + int64_t mFileSize; + PRFileDesc* mFileDesc; + PRFileMap* mFileMap; + void* mMappedMemory; +}; + +// A runnable that implements a state machine required to open a cache entry. +// It executes in the parent for a cache access originating in the child. +// This runnable gets registered as an IPDL subprotocol actor so that it +// can communicate with the corresponding ChildRunnable. +class ParentRunnable final + : public FileDescriptorHolder + , public quota::OpenDirectoryListener + , public PAsmJSCacheEntryParent +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + + ParentRunnable(const PrincipalInfo& aPrincipalInfo, + OpenMode aOpenMode, + WriteParams aWriteParams) + : mOwningThread(NS_GetCurrentThread()), + mPrincipalInfo(aPrincipalInfo), + mOpenMode(aOpenMode), + mWriteParams(aWriteParams), + mPersistence(quota::PERSISTENCE_TYPE_INVALID), + mState(eInitial), + mResult(JS::AsmJSCache_InternalError), + mIsApp(false), + mEnforcingQuota(true), + mActorDestroyed(false), + mOpened(false) + { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertIsOnOwningThread(); + MOZ_COUNT_CTOR(ParentRunnable); + } + +private: + ~ParentRunnable() + { + MOZ_ASSERT(mState == eFinished); + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(mActorDestroyed); + MOZ_COUNT_DTOR(ParentRunnable); + } + + bool + IsOnOwningThread() const + { + MOZ_ASSERT(mOwningThread); + + bool current; + return NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t)) && current; + } + + void + AssertIsOnOwningThread() const + { + MOZ_ASSERT(IsOnBackgroundThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + void + AssertIsOnNonOwningThread() const + { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!IsOnOwningThread()); + } + + // This method is called on the owning thread when no cache entry was found + // to open. If we just tried a lookup in persistent storage then we might + // still get a hit in temporary storage (for an asm.js module that wasn't + // compiled at install-time). + void + CacheMiss() + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == eFailedToReadMetadata || + mState == eWaitingToOpenCacheFileForRead); + MOZ_ASSERT(mOpenMode == eOpenForRead); + + if (mPersistence == quota::PERSISTENCE_TYPE_TEMPORARY) { + Fail(); + return; + } + + // Try again with a clean slate. InitOnMainThread will see that mPersistence + // is initialized and switch to temporary storage. + MOZ_ASSERT(mPersistence == quota::PERSISTENCE_TYPE_PERSISTENT); + FinishOnOwningThread(); + mState = eInitial; + NS_DispatchToMainThread(this); + } + + // This method is called on the owning thread when the JS engine is finished + // reading/writing the cache entry. + void + Close() + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == eOpened); + + mState = eFinished; + + MOZ_ASSERT(mOpened); + + FinishOnOwningThread(); + } + + // This method is called upon any failure that prevents the eventual opening + // of the cache entry. + void + Fail() + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState != eFinished); + + mState = eFinished; + + MOZ_ASSERT(!mOpened); + + FinishOnOwningThread(); + + if (!mActorDestroyed) { + Unused << Send__delete__(this, mResult); + } + } + + // The same as method above but is intended to be called off the owning + // thread. + void + FailOnNonOwningThread() + { + AssertIsOnNonOwningThread(); + MOZ_ASSERT(mState != eOpened && + mState != eFailing && + mState != eFinished); + + mState = eFailing; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + void + InitPersistenceType(); + + nsresult + InitOnMainThread(); + + void + OpenDirectory(); + + nsresult + ReadMetadata(); + + nsresult + OpenCacheFileForWrite(); + + nsresult + OpenCacheFileForRead(); + + void + FinishOnOwningThread(); + + void + DispatchToIOThread() + { + AssertIsOnOwningThread(); + + // If shutdown just started, the QuotaManager may have been deleted. + QuotaManager* qm = QuotaManager::Get(); + if (!qm) { + FailOnNonOwningThread(); + return; + } + + nsresult rv = qm->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + FailOnNonOwningThread(); + return; + } + } + + // OpenDirectoryListener overrides. + virtual void + DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void + DirectoryLockFailed() override; + + // IPDL methods. + bool + Recv__delete__(const JS::AsmJSCacheResult& aResult) override + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState != eFinished); + + if (mOpened) { + Close(); + } else { + Fail(); + } + + MOZ_ASSERT(mState == eFinished); + + return true; + } + + void + ActorDestroy(ActorDestroyReason why) override + { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + // Assume ActorDestroy can happen at any time, so probe the current state to + // determine what needs to happen. + + if (mState == eFinished) { + return; + } + + if (mOpened) { + Close(); + } else { + Fail(); + } + + MOZ_ASSERT(mState == eFinished); + } + + bool + RecvSelectCacheFileToRead(const uint32_t& aModuleIndex) override + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == eWaitingToOpenCacheFileForRead); + MOZ_ASSERT(mOpenMode == eOpenForRead); + + // A cache entry has been selected to open. + + mModuleIndex = aModuleIndex; + mState = eReadyToOpenCacheFileForRead; + DispatchToIOThread(); + + return true; + } + + bool + RecvCacheMiss() override + { + AssertIsOnOwningThread(); + + CacheMiss(); + + return true; + } + + nsCOMPtr mOwningThread; + const PrincipalInfo mPrincipalInfo; + const OpenMode mOpenMode; + const WriteParams mWriteParams; + + // State initialized during eInitial: + quota::PersistenceType mPersistence; + nsCString mSuffix; + nsCString mGroup; + nsCString mOrigin; + RefPtr mDirectoryLock; + + // State initialized during eReadyToReadMetadata + nsCOMPtr mDirectory; + nsCOMPtr mMetadataFile; + Metadata mMetadata; + + // State initialized during eWaitingToOpenCacheFileForRead + unsigned mModuleIndex; + + enum State { + eInitial, // Just created, waiting to be dispatched to main thread + eWaitingToFinishInit, // Waiting to finish initialization + eWaitingToOpenDirectory, // Waiting to open directory + eWaitingToOpenMetadata, // Waiting to be called back from OpenDirectory + eReadyToReadMetadata, // Waiting to read the metadata file on the IO thread + eFailedToReadMetadata, // Waiting to be dispatched to owning thread after fail + eSendingMetadataForRead, // Waiting to send OnOpenMetadataForRead + eWaitingToOpenCacheFileForRead, // Waiting to hear back from child + eReadyToOpenCacheFileForRead, // Waiting to open cache file for read + eSendingCacheFile, // Waiting to send OnOpenCacheFile on the owning thread + eOpened, // Finished calling OnOpenCacheFile, waiting to be closed + eFailing, // Just failed, waiting to be dispatched to the owning thread + eFinished, // Terminal state + }; + State mState; + JS::AsmJSCacheResult mResult; + + bool mIsApp; + bool mEnforcingQuota; + bool mActorDestroyed; + bool mOpened; +}; + +void +ParentRunnable::InitPersistenceType() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eInitial); + + if (mOpenMode == eOpenForWrite) { + MOZ_ASSERT(mPersistence == quota::PERSISTENCE_TYPE_INVALID); + + // If we are performing install-time caching of an app, we'd like to store + // the cache entry in persistent storage so the entry is never evicted, + // but we need to check that quota is not enforced for the app. + // That justifies us in skipping all quota checks when storing the cache + // entry and avoids all the issues around the persistent quota prompt. + // If quota is enforced for the app, then we can still cache in temporary + // for a likely good first-run experience. + + MOZ_ASSERT_IF(mWriteParams.mInstalled, mIsApp); + + if (mWriteParams.mInstalled && + !QuotaManager::IsQuotaEnforced(quota::PERSISTENCE_TYPE_PERSISTENT, + mOrigin, mIsApp)) { + mPersistence = quota::PERSISTENCE_TYPE_PERSISTENT; + } else { + mPersistence = quota::PERSISTENCE_TYPE_TEMPORARY; + } + + return; + } + + // For the reasons described above, apps may have cache entries in both + // persistent and temporary storage. At lookup time we don't know how and + // where the given script was cached, so start the search in persistent + // storage and, if that fails, search in temporary storage. (Non-apps can + // only be stored in temporary storage.) + + MOZ_ASSERT_IF(mPersistence != quota::PERSISTENCE_TYPE_INVALID, + mIsApp && mPersistence == quota::PERSISTENCE_TYPE_PERSISTENT); + + if (mPersistence == quota::PERSISTENCE_TYPE_INVALID && mIsApp) { + mPersistence = quota::PERSISTENCE_TYPE_PERSISTENT; + } else { + mPersistence = quota::PERSISTENCE_TYPE_TEMPORARY; + } +} + +nsresult +ParentRunnable::InitOnMainThread() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eInitial); + MOZ_ASSERT(mPrincipalInfo.type() != PrincipalInfo::TNullPrincipalInfo); + + nsresult rv; + nsCOMPtr principal = + PrincipalInfoToPrincipal(mPrincipalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = QuotaManager::GetInfoFromPrincipal(principal, &mSuffix, &mGroup, + &mOrigin, &mIsApp); + NS_ENSURE_SUCCESS(rv, rv); + + InitPersistenceType(); + + mEnforcingQuota = + QuotaManager::IsQuotaEnforced(mPersistence, mOrigin, mIsApp); + + return NS_OK; +} + +void +ParentRunnable::OpenDirectory() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == eWaitingToFinishInit || + mState == eWaitingToOpenDirectory); + MOZ_ASSERT(QuotaManager::Get()); + + mState = eWaitingToOpenMetadata; + + // XXX The exclusive lock shouldn't be needed for read operations. + QuotaManager::Get()->OpenDirectory(mPersistence, + mGroup, + mOrigin, + mIsApp, + quota::Client::ASMJS, + /* aExclusive */ true, + this); +} + +nsresult +ParentRunnable::ReadMetadata() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == eReadyToReadMetadata); + + QuotaManager* qm = QuotaManager::Get(); + MOZ_ASSERT(qm, "We are on the QuotaManager's IO thread"); + + nsresult rv = + qm->EnsureOriginIsInitialized(mPersistence, mSuffix, mGroup, mOrigin, + mIsApp, getter_AddRefs(mDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + mResult = JS::AsmJSCache_StorageInitFailure; + return rv; + } + + rv = mDirectory->Append(NS_LITERAL_STRING(ASMJSCACHE_DIRECTORY_NAME)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = mDirectory->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + rv = mDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); + NS_ENSURE_SUCCESS(rv, rv); + } else { + DebugOnly isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(mDirectory->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory, "Should have caught this earlier!"); + } + + rv = mDirectory->Clone(getter_AddRefs(mMetadataFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mMetadataFile->Append(NS_LITERAL_STRING(ASMJSCACHE_METADATA_FILE_NAME)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mMetadataFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists && NS_FAILED(ReadMetadataFile(mMetadataFile, mMetadata))) { + exists = false; + } + + if (!exists) { + // If we are reading, we can't possibly have a cache hit. + if (mOpenMode == eOpenForRead) { + return NS_ERROR_FILE_NOT_FOUND; + } + + // Initialize Metadata with a valid empty state for the LRU cache. + for (unsigned i = 0; i < Metadata::kNumEntries; i++) { + Metadata::Entry& entry = mMetadata.mEntries[i]; + entry.mModuleIndex = i; + entry.clear(); + } + } + + return NS_OK; +} + +nsresult +ParentRunnable::OpenCacheFileForWrite() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == eReadyToReadMetadata); + MOZ_ASSERT(mOpenMode == eOpenForWrite); + + mFileSize = mWriteParams.mSize; + + // Kick out the oldest entry in the LRU queue in the metadata. + mModuleIndex = mMetadata.mEntries[Metadata::kLastEntry].mModuleIndex; + + nsCOMPtr file; + nsresult rv = GetCacheFile(mDirectory, mModuleIndex, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + QuotaManager* qm = QuotaManager::Get(); + MOZ_ASSERT(qm, "We are on the QuotaManager's IO thread"); + + if (mEnforcingQuota) { + // Create the QuotaObject before all file IO and keep it alive until caching + // completes to get maximum assertion coverage in QuotaManager against + // concurrent removal, etc. + mQuotaObject = qm->GetQuotaObject(mPersistence, mGroup, mOrigin, file); + NS_ENSURE_STATE(mQuotaObject); + + if (!mQuotaObject->MaybeUpdateSize(mWriteParams.mSize, + /* aTruncate */ false)) { + // If the request fails, it might be because mOrigin is using too much + // space (MaybeUpdateSize will not evict our own origin since it is + // active). Try to make some space by evicting LRU entries until there is + // enough space. + EvictEntries(mDirectory, mGroup, mOrigin, mWriteParams.mSize, mMetadata); + if (!mQuotaObject->MaybeUpdateSize(mWriteParams.mSize, + /* aTruncate */ false)) { + mResult = JS::AsmJSCache_QuotaExceeded; + return NS_ERROR_FAILURE; + } + } + } + + int32_t openFlags = PR_RDWR | PR_TRUNCATE | PR_CREATE_FILE; + rv = file->OpenNSPRFileDesc(openFlags, 0644, &mFileDesc); + NS_ENSURE_SUCCESS(rv, rv); + + // Move the mModuleIndex's LRU entry to the recent end of the queue. + PodMove(mMetadata.mEntries + 1, mMetadata.mEntries, Metadata::kLastEntry); + Metadata::Entry& entry = mMetadata.mEntries[0]; + entry.mFastHash = mWriteParams.mFastHash; + entry.mNumChars = mWriteParams.mNumChars; + entry.mFullHash = mWriteParams.mFullHash; + entry.mModuleIndex = mModuleIndex; + + rv = WriteMetadataFile(mMetadataFile, mMetadata); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +ParentRunnable::OpenCacheFileForRead() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == eReadyToOpenCacheFileForRead); + MOZ_ASSERT(mOpenMode == eOpenForRead); + + nsCOMPtr file; + nsresult rv = GetCacheFile(mDirectory, mModuleIndex, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + QuotaManager* qm = QuotaManager::Get(); + MOZ_ASSERT(qm, "We are on the QuotaManager's IO thread"); + + if (mEnforcingQuota) { + // Even though it's not strictly necessary, create the QuotaObject before + // all file IO and keep it alive until caching completes to get maximum + // assertion coverage in QuotaManager against concurrent removal, etc. + mQuotaObject = qm->GetQuotaObject(mPersistence, mGroup, mOrigin, file); + NS_ENSURE_STATE(mQuotaObject); + } + + rv = file->GetFileSize(&mFileSize); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t openFlags = PR_RDONLY | nsIFile::OS_READAHEAD; + rv = file->OpenNSPRFileDesc(openFlags, 0644, &mFileDesc); + NS_ENSURE_SUCCESS(rv, rv); + + // Move the mModuleIndex's LRU entry to the recent end of the queue. + unsigned lruIndex = 0; + while (mMetadata.mEntries[lruIndex].mModuleIndex != mModuleIndex) { + if (++lruIndex == Metadata::kNumEntries) { + return NS_ERROR_UNEXPECTED; + } + } + Metadata::Entry entry = mMetadata.mEntries[lruIndex]; + PodMove(mMetadata.mEntries + 1, mMetadata.mEntries, lruIndex); + mMetadata.mEntries[0] = entry; + + rv = WriteMetadataFile(mMetadataFile, mMetadata); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void +ParentRunnable::FinishOnOwningThread() +{ + AssertIsOnOwningThread(); + + // Per FileDescriptorHolder::Finish()'s comment, call before + // releasing the directory lock. + FileDescriptorHolder::Finish(); + + mDirectoryLock = nullptr; +} + +NS_IMETHODIMP +ParentRunnable::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)) { + FailOnNonOwningThread(); + return NS_OK; + } + + mState = eWaitingToFinishInit; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; + } + + case eWaitingToFinishInit: { + AssertIsOnOwningThread(); + + if (QuotaManager::IsShuttingDown()) { + Fail(); + return NS_OK; + } + + if (QuotaManager::Get()) { + OpenDirectory(); + return NS_OK; + } + + mState = eWaitingToOpenDirectory; + QuotaManager::GetOrCreate(this); + + return NS_OK; + } + + case eWaitingToOpenDirectory: { + AssertIsOnOwningThread(); + + if (NS_WARN_IF(!QuotaManager::Get())) { + Fail(); + return NS_OK; + } + + OpenDirectory(); + return NS_OK; + } + + case eReadyToReadMetadata: { + AssertIsOnIOThread(); + + rv = ReadMetadata(); + if (NS_FAILED(rv)) { + mState = eFailedToReadMetadata; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + return NS_OK; + } + + if (mOpenMode == eOpenForRead) { + mState = eSendingMetadataForRead; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; + } + + rv = OpenCacheFileForWrite(); + if (NS_FAILED(rv)) { + FailOnNonOwningThread(); + return NS_OK; + } + + mState = eSendingCacheFile; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + return NS_OK; + } + + case eFailedToReadMetadata: { + AssertIsOnOwningThread(); + + if (mOpenMode == eOpenForRead) { + CacheMiss(); + return NS_OK; + } + + Fail(); + return NS_OK; + } + + case eSendingMetadataForRead: { + AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenMode == eOpenForRead); + + mState = eWaitingToOpenCacheFileForRead; + + // Metadata is now open. + if (!SendOnOpenMetadataForRead(mMetadata)) { + Fail(); + return NS_OK; + } + + return NS_OK; + } + + case eReadyToOpenCacheFileForRead: { + AssertIsOnIOThread(); + MOZ_ASSERT(mOpenMode == eOpenForRead); + + rv = OpenCacheFileForRead(); + if (NS_FAILED(rv)) { + FailOnNonOwningThread(); + return NS_OK; + } + + mState = eSendingCacheFile; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + return NS_OK; + } + + case eSendingCacheFile: { + AssertIsOnOwningThread(); + + mState = eOpened; + + // The entry is now open. + MOZ_ASSERT(!mOpened); + mOpened = true; + + FileDescriptor::PlatformHandleType handle = + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(mFileDesc)); + if (!SendOnOpenCacheFile(mFileSize, FileDescriptor(handle))) { + Fail(); + return NS_OK; + } + + return NS_OK; + } + + case eFailing: { + AssertIsOnOwningThread(); + + Fail(); + + return NS_OK; + } + + case eWaitingToOpenMetadata: + case eWaitingToOpenCacheFileForRead: + case eOpened: + case eFinished: { + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Shouldn't Run() in this state"); + } + } + + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Corrupt state"); + return NS_OK; +} + +void +ParentRunnable::DirectoryLockAcquired(DirectoryLock* aLock) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == eWaitingToOpenMetadata); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + mState = eReadyToReadMetadata; + DispatchToIOThread(); +} + +void +ParentRunnable::DirectoryLockFailed() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == eWaitingToOpenMetadata); + MOZ_ASSERT(!mDirectoryLock); + + Fail(); +} + +NS_IMPL_ISUPPORTS_INHERITED0(ParentRunnable, FileDescriptorHolder) + +bool +FindHashMatch(const Metadata& aMetadata, const ReadParams& aReadParams, + unsigned* aModuleIndex) +{ + // Perform a fast hash of the first sNumFastHashChars chars. Each cache entry + // also stores an mFastHash of its first sNumFastHashChars so this gives us a + // fast way to probabilistically determine whether we have a cache hit. We + // still do a full hash of all the chars before returning the cache file to + // the engine to avoid penalizing the case where there are multiple cached + // asm.js modules where the first sNumFastHashChars are the same. The + // mFullHash of each cache entry can have a different mNumChars so the fast + // hash allows us to avoid performing up to Metadata::kNumEntries separate + // full hashes. + uint32_t numChars = aReadParams.mLimit - aReadParams.mBegin; + MOZ_ASSERT(numChars > sNumFastHashChars); + uint32_t fastHash = HashString(aReadParams.mBegin, sNumFastHashChars); + + for (unsigned i = 0; i < Metadata::kNumEntries ; i++) { + // Compare the "fast hash" first to see whether it is worthwhile to + // hash all the chars. + Metadata::Entry entry = aMetadata.mEntries[i]; + if (entry.mFastHash != fastHash) { + continue; + } + + // Assuming we have enough characters, hash all the chars it would take + // to match this cache entry and compare to the cache entry. If we get a + // hit we'll still do a full source match later (in the JS engine), but + // the full hash match means this is probably the cache entry we want. + if (numChars < entry.mNumChars) { + continue; + } + uint32_t fullHash = HashString(aReadParams.mBegin, entry.mNumChars); + if (entry.mFullHash != fullHash) { + continue; + } + + *aModuleIndex = entry.mModuleIndex; + return true; + } + + return false; +} + +} // unnamed namespace + +PAsmJSCacheEntryParent* +AllocEntryParent(OpenMode aOpenMode, + WriteParams aWriteParams, + const PrincipalInfo& aPrincipalInfo) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(aPrincipalInfo.type() == PrincipalInfo::TNullPrincipalInfo)) { + MOZ_ASSERT(false); + return nullptr; + } + + RefPtr runnable = + new ParentRunnable(aPrincipalInfo, aOpenMode, aWriteParams); + + nsresult rv = NS_DispatchToMainThread(runnable); + NS_ENSURE_SUCCESS(rv, nullptr); + + // Transfer ownership to IPDL. + return runnable.forget().take(); +} + +void +DeallocEntryParent(PAsmJSCacheEntryParent* aActor) +{ + // Transfer ownership back from IPDL. + RefPtr op = + dont_AddRef(static_cast(aActor)); +} + +namespace { + +// A runnable that presents a single interface to the AsmJSCache ops which need +// to wait until the file is open. +class ChildRunnable final + : public FileDescriptorHolder + , public PAsmJSCacheEntryChild + , public nsIIPCBackgroundChildCreateCallback +{ + typedef mozilla::ipc::PBackgroundChild PBackgroundChild; + +public: + class AutoClose + { + ChildRunnable* mChildRunnable; + + public: + explicit AutoClose(ChildRunnable* aChildRunnable = nullptr) + : mChildRunnable(aChildRunnable) + { } + + void + Init(ChildRunnable* aChildRunnable) + { + MOZ_ASSERT(!mChildRunnable); + mChildRunnable = aChildRunnable; + } + + ChildRunnable* + operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN + { + MOZ_ASSERT(mChildRunnable); + return mChildRunnable; + } + + void + Forget(ChildRunnable** aChildRunnable) + { + *aChildRunnable = mChildRunnable; + mChildRunnable = nullptr; + } + + ~AutoClose() + { + if (mChildRunnable) { + mChildRunnable->Close(); + } + } + }; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK + + ChildRunnable(nsIPrincipal* aPrincipal, + OpenMode aOpenMode, + WriteParams aWriteParams, + ReadParams aReadParams) + : mPrincipal(aPrincipal), + mWriteParams(aWriteParams), + mReadParams(aReadParams), + mMutex("ChildRunnable::mMutex"), + mCondVar(mMutex, "ChildRunnable::mCondVar"), + mOpenMode(aOpenMode), + mState(eInitial), + mResult(JS::AsmJSCache_InternalError), + mActorDestroyed(false), + mWaiting(false), + mOpened(false) + { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_COUNT_CTOR(ChildRunnable); + } + + JS::AsmJSCacheResult + 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); + if (NS_WARN_IF(NS_FAILED(rv))) { + return JS::AsmJSCache_InternalError; + } + + { + MutexAutoLock lock(mMutex); + while (mWaiting) { + mCondVar.Wait(); + } + } + + if (!mOpened) { + return mResult; + } + + // Now that we're open, we're guaranteed a Close() call. However, we are + // not guaranteed 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 JS::AsmJSCache_Success; + } + + void Cleanup() + { +#ifdef DEBUG + NoteActorDestroyed(); +#endif + } + +private: + ~ChildRunnable() + { + MOZ_ASSERT(!mWaiting, "Shouldn't be destroyed while thread is waiting"); + MOZ_ASSERT(!mOpened); + MOZ_ASSERT(mState == eFinished); + MOZ_ASSERT(mActorDestroyed); + MOZ_COUNT_DTOR(ChildRunnable); + } + + // IPDL methods. + bool + RecvOnOpenMetadataForRead(const Metadata& aMetadata) override + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eOpening); + + uint32_t moduleIndex; + if (FindHashMatch(aMetadata, mReadParams, &moduleIndex)) { + return SendSelectCacheFileToRead(moduleIndex); + } + + return SendCacheMiss(); + } + + bool + RecvOnOpenCacheFile(const int64_t& aFileSize, + const FileDescriptor& aFileDesc) override + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eOpening); + + mFileSize = aFileSize; + + auto rawFD = aFileDesc.ClonePlatformHandle(); + mFileDesc = PR_ImportFile(PROsfd(rawFD.release())); + if (!mFileDesc) { + return false; + } + + mState = eOpened; + Notify(JS::AsmJSCache_Success); + return true; + } + + bool + Recv__delete__(const JS::AsmJSCacheResult& aResult) override + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eOpening); + + Fail(aResult); + return true; + } + + void + ActorDestroy(ActorDestroyReason why) override + { + MOZ_ASSERT(NS_IsMainThread()); + NoteActorDestroyed(); + } + + void + Close() + { + MOZ_ASSERT(mState == eOpened); + + mState = eClosing; + NS_DispatchToMainThread(this); + } + + void + Fail(JS::AsmJSCacheResult aResult) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eInitial || mState == eOpening); + MOZ_ASSERT(aResult != JS::AsmJSCache_Success); + + mState = eFinished; + + FileDescriptorHolder::Finish(); + Notify(aResult); + } + + void + Notify(JS::AsmJSCacheResult aResult) + { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mWaiting); + + mWaiting = false; + mOpened = aResult == JS::AsmJSCache_Success; + mResult = aResult; + mCondVar.Notify(); + } + + void NoteActorDestroyed() + { + mActorDestroyed = true; + } + + nsIPrincipal* const mPrincipal; + nsAutoPtr mPrincipalInfo; + WriteParams mWriteParams; + ReadParams mReadParams; + Mutex mMutex; + CondVar mCondVar; + + // Couple enums and bools together + const OpenMode mOpenMode; + enum State { + eInitial, // Just created, waiting to be dispatched to the main thread + eBackgroundChildPending, // Waiting for the background child to be created + 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; + JS::AsmJSCacheResult mResult; + + bool mActorDestroyed; + bool mWaiting; + bool mOpened; +}; + +NS_IMETHODIMP +ChildRunnable::Run() +{ + switch (mState) { + case eInitial: { + MOZ_ASSERT(NS_IsMainThread()); + + if (mPrincipal->GetIsNullPrincipal()) { + NS_WARNING("AsmsJSCache not supported on null principal."); + Fail(JS::AsmJSCache_InternalError); + return NS_OK; + } + + nsAutoPtr principalInfo(new PrincipalInfo()); + nsresult rv = PrincipalToPrincipalInfo(mPrincipal, principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(JS::AsmJSCache_InternalError); + return NS_OK; + } + + mPrincipalInfo = Move(principalInfo); + + PBackgroundChild* actor = BackgroundChild::GetForCurrentThread(); + if (actor) { + ActorCreated(actor); + return NS_OK; + } + + if (NS_WARN_IF(!BackgroundChild::GetOrCreateForCurrentThread(this))) { + Fail(JS::AsmJSCache_InternalError); + return NS_OK; + } + + mState = eBackgroundChildPending; + return NS_OK; + } + + case eClosing: { + MOZ_ASSERT(NS_IsMainThread()); + + // Per FileDescriptorHolder::Finish()'s comment, call before + // releasing the directory lock (which happens in the parent upon receipt + // of the Send__delete__ message). + 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(); + + if (!mActorDestroyed) { + Unused << Send__delete__(this, JS::AsmJSCache_Success); + } + + mState = eFinished; + return NS_OK; + } + + case eBackgroundChildPending: + case eOpening: + case eOpened: + case eFinished: { + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Shouldn't Run() in this state"); + } + } + + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Corrupt state"); + return NS_OK; +} + +void +ChildRunnable::ActorCreated(PBackgroundChild* aActor) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!aActor->SendPAsmJSCacheEntryConstructor(this, mOpenMode, mWriteParams, + *mPrincipalInfo)) { + // Unblock the parsing thread with a failure. + + Fail(JS::AsmJSCache_InternalError); + + return; + } + + // AddRef to keep this runnable alive until IPDL deallocates the + // subprotocol (DeallocEntryChild). + AddRef(); + + mState = eOpening; +} + +void +ChildRunnable::ActorFailed() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eBackgroundChildPending); + + Fail(JS::AsmJSCache_InternalError); +} + +NS_IMPL_ISUPPORTS_INHERITED(ChildRunnable, + FileDescriptorHolder, + nsIIPCBackgroundChildCreateCallback) + +} // unnamed namespace + +void +DeallocEntryChild(PAsmJSCacheEntryChild* aActor) +{ + // Match the AddRef before SendPAsmJSCacheEntryConstructor. + static_cast(aActor)->Release(); +} + +/* +namespace { + +JS::AsmJSCacheResult +OpenFile(nsIPrincipal* aPrincipal, + OpenMode aOpenMode, + WriteParams aWriteParams, + ReadParams aReadParams, + ChildRunnable::AutoClose* aChildRunnable) +{ + MOZ_ASSERT_IF(aOpenMode == eOpenForRead, aWriteParams.mSize == 0); + MOZ_ASSERT_IF(aOpenMode == eOpenForWrite, aReadParams.mBegin == nullptr); + + // 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 JS::AsmJSCache_SynchronousScript; + } + + // Check to see whether the principal reflects a private browsing session. + // Since AsmJSCache requires disk access at the moment, caching should be + // disabled in private browsing situations. Failing here will cause later + // read/write requests to also fail. + uint32_t pbId; + if (NS_WARN_IF(NS_FAILED(aPrincipal->GetPrivateBrowsingId(&pbId)))) { + return JS::AsmJSCache_InternalError; + } + + if (pbId > 0) { + return JS::AsmJSCache_Disabled_PrivateBrowsing; + } + + // We need to synchronously call into the parent to open the file and + // interact with the QuotaManager. The child can then map the file into its + // address space to perform I/O. + RefPtr childRunnable = + new ChildRunnable(aPrincipal, aOpenMode, aWriteParams, aReadParams); + + JS::AsmJSCacheResult openResult = + childRunnable->BlockUntilOpen(aChildRunnable); + if (openResult != JS::AsmJSCache_Success) { + childRunnable->Cleanup(); + return openResult; + } + + if (!childRunnable->MapMemory(aOpenMode)) { + return JS::AsmJSCache_InternalError; + } + + return JS::AsmJSCache_Success; +} + +} // namespace +*/ + +typedef uint32_t AsmJSCookieType; +static const uint32_t sAsmJSCookie = 0x600d600d; + +bool +OpenEntryForRead(nsIPrincipal* aPrincipal, + const char16_t* aBegin, + const char16_t* aLimit, + size_t* aSize, + const uint8_t** aMemory, + intptr_t* aHandle) +{ + return false; + +/* + if (size_t(aLimit - aBegin) < sMinCachedModuleLength) { + return false; + } + + ReadParams readParams; + readParams.mBegin = aBegin; + readParams.mLimit = aLimit; + + ChildRunnable::AutoClose childRunnable; + WriteParams notAWrite; + JS::AsmJSCacheResult openResult = + OpenFile(aPrincipal, eOpenForRead, notAWrite, readParams, &childRunnable); + if (openResult != JS::AsmJSCache_Success) { + 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 (childRunnable->FileSize() < sizeof(AsmJSCookieType) || + *(AsmJSCookieType*)childRunnable->MappedMemory() != sAsmJSCookie) { + return false; + } + + *aSize = childRunnable->FileSize() - sizeof(AsmJSCookieType); + *aMemory = (uint8_t*) childRunnable->MappedMemory() + sizeof(AsmJSCookieType); + + // The caller guarnatees a call to CloseEntryForRead (on success or + // failure) at which point the file will be closed. + childRunnable.Forget(reinterpret_cast(aHandle)); + return true; +*/ +} + +void +CloseEntryForRead(size_t aSize, + const uint8_t* aMemory, + intptr_t aHandle) +{ + ChildRunnable::AutoClose childRunnable( + reinterpret_cast(aHandle)); + + MOZ_ASSERT(aSize + sizeof(AsmJSCookieType) == childRunnable->FileSize()); + MOZ_ASSERT(aMemory - sizeof(AsmJSCookieType) == + childRunnable->MappedMemory()); +} + +JS::AsmJSCacheResult +OpenEntryForWrite(nsIPrincipal* aPrincipal, + bool aInstalled, + const char16_t* aBegin, + const char16_t* aEnd, + size_t aSize, + uint8_t** aMemory, + intptr_t* aHandle) +{ + return JS::AsmJSCache_ESR52; + +/* + if (size_t(aEnd - aBegin) < sMinCachedModuleLength) { + return JS::AsmJSCache_ModuleTooSmall; + } + + // Add extra space for the AsmJSCookieType (see OpenEntryForRead). + aSize += sizeof(AsmJSCookieType); + + static_assert(sNumFastHashChars < sMinCachedModuleLength, "HashString safe"); + + WriteParams writeParams; + writeParams.mInstalled = aInstalled; + writeParams.mSize = aSize; + writeParams.mFastHash = HashString(aBegin, sNumFastHashChars); + writeParams.mNumChars = aEnd - aBegin; + writeParams.mFullHash = HashString(aBegin, writeParams.mNumChars); + + ChildRunnable::AutoClose childRunnable; + ReadParams notARead; + JS::AsmJSCacheResult openResult = + OpenFile(aPrincipal, eOpenForWrite, writeParams, notARead, &childRunnable); + if (openResult != JS::AsmJSCache_Success) { + return openResult; + } + + // 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*) childRunnable->MappedMemory() + sizeof(AsmJSCookieType); + + // The caller guarnatees a call to CloseEntryForWrite (on success or + // failure) at which point the file will be closed + childRunnable.Forget(reinterpret_cast(aHandle)); + return JS::AsmJSCache_Success; +*/ +} + +void +CloseEntryForWrite(size_t aSize, + uint8_t* aMemory, + intptr_t aHandle) +{ + ChildRunnable::AutoClose childRunnable( + reinterpret_cast(aHandle)); + + MOZ_ASSERT(aSize + sizeof(AsmJSCookieType) == childRunnable->FileSize()); + MOZ_ASSERT(aMemory - sizeof(AsmJSCookieType) == + childRunnable->MappedMemory()); + + // Flush to disk before writing the cookie (see OpenEntryForRead). + if (PR_SyncMemMap(childRunnable->FileDesc(), + childRunnable->MappedMemory(), + childRunnable->FileSize()) == PR_SUCCESS) { + *(AsmJSCookieType*)childRunnable->MappedMemory() = sAsmJSCookie; + } +} + +class Client : public quota::Client +{ + ~Client() {} + +public: + NS_IMETHOD_(MozExternalRefCountType) + AddRef() override; + + NS_IMETHOD_(MozExternalRefCountType) + Release() override; + + virtual Type + GetType() override + { + return ASMJS; + } + + virtual nsresult + InitOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) override + { + if (!aUsageInfo) { + return NS_OK; + } + return GetUsageForOrigin(aPersistenceType, + aGroup, + aOrigin, + aCanceled, + aUsageInfo); + } + + virtual nsresult + GetUsageForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) 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); + + DebugOnly exists; + MOZ_ASSERT(NS_SUCCEEDED(directory->Exists(&exists)) && exists); + + nsCOMPtr entries; + rv = directory->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && + hasMore && !aCanceled) { + nsCOMPtr entry; + rv = entries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file = do_QueryInterface(entry); + NS_ENSURE_TRUE(file, NS_NOINTERFACE); + + int64_t fileSize; + rv = file->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)); + } + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + virtual void + OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) + override + { } + + virtual void + ReleaseIOThreadObjects() override + { } + + virtual void + AbortOperations(const nsACString& aOrigin) override + { } + + virtual void + AbortOperationsForProcess(ContentParentId aContentParentId) override + { } + + virtual void + StartIdleMaintenance() override + { } + + virtual void + StopIdleMaintenance() override + { } + + virtual void + ShutdownWorkThreads() 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 + +namespace IPC { + +using mozilla::dom::asmjscache::Metadata; +using mozilla::dom::asmjscache::WriteParams; + +void +ParamTraits::Write(Message* aMsg, const paramType& aParam) +{ + for (unsigned i = 0; i < Metadata::kNumEntries; i++) { + const Metadata::Entry& entry = aParam.mEntries[i]; + WriteParam(aMsg, entry.mFastHash); + WriteParam(aMsg, entry.mNumChars); + WriteParam(aMsg, entry.mFullHash); + WriteParam(aMsg, entry.mModuleIndex); + } +} + +bool +ParamTraits::Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) +{ + for (unsigned i = 0; i < Metadata::kNumEntries; i++) { + Metadata::Entry& entry = aResult->mEntries[i]; + if (!ReadParam(aMsg, aIter, &entry.mFastHash) || + !ReadParam(aMsg, aIter, &entry.mNumChars) || + !ReadParam(aMsg, aIter, &entry.mFullHash) || + !ReadParam(aMsg, aIter, &entry.mModuleIndex)) + { + return false; + } + } + return true; +} + +void +ParamTraits::Log(const paramType& aParam, std::wstring* aLog) +{ + for (unsigned i = 0; i < Metadata::kNumEntries; i++) { + const Metadata::Entry& entry = aParam.mEntries[i]; + LogParam(entry.mFastHash, aLog); + LogParam(entry.mNumChars, aLog); + LogParam(entry.mFullHash, aLog); + LogParam(entry.mModuleIndex, aLog); + } +} + +void +ParamTraits::Write(Message* aMsg, const paramType& aParam) +{ + WriteParam(aMsg, aParam.mSize); + WriteParam(aMsg, aParam.mFastHash); + WriteParam(aMsg, aParam.mNumChars); + WriteParam(aMsg, aParam.mFullHash); + WriteParam(aMsg, aParam.mInstalled); +} + +bool +ParamTraits::Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) +{ + return ReadParam(aMsg, aIter, &aResult->mSize) && + ReadParam(aMsg, aIter, &aResult->mFastHash) && + ReadParam(aMsg, aIter, &aResult->mNumChars) && + ReadParam(aMsg, aIter, &aResult->mFullHash) && + ReadParam(aMsg, aIter, &aResult->mInstalled); +} + +void +ParamTraits::Log(const paramType& aParam, std::wstring* aLog) +{ + LogParam(aParam.mSize, aLog); + LogParam(aParam.mFastHash, aLog); + LogParam(aParam.mNumChars, aLog); + LogParam(aParam.mFullHash, aLog); + LogParam(aParam.mInstalled, aLog); +} + +} // namespace IPC diff --git a/dom/asmjscache/AsmJSCache.h b/dom/asmjscache/AsmJSCache.h new file mode 100644 index 000000000..92bb7780a --- /dev/null +++ b/dom/asmjscache/AsmJSCache.h @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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/. */ + +#ifndef mozilla_dom_asmjscache_asmjscache_h +#define mozilla_dom_asmjscache_asmjscache_h + +#include "ipc/IPCMessageUtils.h" +#include "js/TypeDecls.h" +#include "js/Vector.h" +#include "jsapi.h" + +class nsIPrincipal; + +namespace mozilla { + +namespace ipc { + +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +namespace quota { +class Client; +} // namespace quota + +namespace asmjscache { + +class PAsmJSCacheEntryChild; +class PAsmJSCacheEntryParent; + +enum OpenMode +{ + eOpenForRead, + eOpenForWrite, + NUM_OPEN_MODES +}; + +// Each origin stores a fixed size (kNumEntries) LRU cache of compiled asm.js +// modules. Each compiled asm.js module is stored in a separate file with one +// extra metadata file that stores the LRU cache and enough information for a +// client to pick which cached module's file to open. +struct Metadata +{ + static const unsigned kNumEntries = 16; + static const unsigned kLastEntry = kNumEntries - 1; + + struct Entry + { + uint32_t mFastHash; + uint32_t mNumChars; + uint32_t mFullHash; + unsigned mModuleIndex; + + void clear() { + mFastHash = -1; + mNumChars = -1; + mFullHash = -1; + } + }; + + Entry mEntries[kNumEntries]; +}; + +// Parameters specific to opening a cache entry for writing +struct WriteParams +{ + int64_t mSize; + int64_t mFastHash; + int64_t mNumChars; + int64_t mFullHash; + bool mInstalled; + + WriteParams() + : mSize(0), + mFastHash(0), + mNumChars(0), + mFullHash(0), + mInstalled(false) + { } +}; + +// Parameters specific to opening a cache entry for reading +struct ReadParams +{ + const char16_t* mBegin; + const char16_t* mLimit; + + ReadParams() + : mBegin(nullptr), + mLimit(nullptr) + { } +}; + +// Implementation of AsmJSCacheOps, installed for the main JSRuntime by +// nsJSEnvironment.cpp and DOM Worker JSRuntimes in RuntimeService.cpp. +// +// The Open* functions cannot be called directly from AsmJSCacheOps: they take +// an nsIPrincipal as the first argument instead of a Handle. The +// caller must map the object to an nsIPrincipal. +// +// These methods may be called off the main thread and guarantee not to +// access the given aPrincipal except on the main thread. In exchange, the +// caller must ensure the given principal is alive from when OpenEntryForX is +// called to when CloseEntryForX returns. + +bool +OpenEntryForRead(nsIPrincipal* aPrincipal, + const char16_t* aBegin, + const char16_t* aLimit, + size_t* aSize, + const uint8_t** aMemory, + intptr_t *aHandle); +void +CloseEntryForRead(size_t aSize, + const uint8_t* aMemory, + intptr_t aHandle); +JS::AsmJSCacheResult +OpenEntryForWrite(nsIPrincipal* aPrincipal, + bool aInstalled, + const char16_t* aBegin, + const char16_t* aEnd, + size_t aSize, + uint8_t** aMemory, + intptr_t* aHandle); +void +CloseEntryForWrite(size_t aSize, + uint8_t* aMemory, + intptr_t aHandle); + +// Called from QuotaManager.cpp: + +quota::Client* +CreateClient(); + +// Called from ipc/ContentParent.cpp: + +PAsmJSCacheEntryParent* +AllocEntryParent(OpenMode aOpenMode, WriteParams aWriteParams, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo); + +void +DeallocEntryParent(PAsmJSCacheEntryParent* aActor); + +// Called from ipc/ContentChild.cpp: + +void +DeallocEntryChild(PAsmJSCacheEntryChild* aActor); + +} // namespace asmjscache +} // namespace dom +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits : + public ContiguousEnumSerializer +{ }; + +template <> +struct ParamTraits +{ + typedef mozilla::dom::asmjscache::Metadata paramType; + static void Write(Message* aMsg, const paramType& aParam); + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult); + static void Log(const paramType& aParam, std::wstring* aLog); +}; + +template <> +struct ParamTraits +{ + typedef mozilla::dom::asmjscache::WriteParams paramType; + static void Write(Message* aMsg, const paramType& aParam); + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult); + static void Log(const paramType& aParam, std::wstring* aLog); +}; + +template <> +struct ParamTraits : + public ContiguousEnumSerializer +{ }; + +} // namespace IPC + +#endif // mozilla_dom_asmjscache_asmjscache_h diff --git a/dom/asmjscache/PAsmJSCacheEntry.ipdl b/dom/asmjscache/PAsmJSCacheEntry.ipdl new file mode 100644 index 000000000..4980650cc --- /dev/null +++ b/dom/asmjscache/PAsmJSCacheEntry.ipdl @@ -0,0 +1,38 @@ +/* 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 protocol PBackground; + +using mozilla::dom::asmjscache::Metadata from "mozilla/dom/asmjscache/AsmJSCache.h"; +using JS::AsmJSCacheResult from "mozilla/dom/asmjscache/AsmJSCache.h"; + +namespace mozilla { +namespace dom { +namespace asmjscache { + +protocol PAsmJSCacheEntry +{ + manager PBackground; + + // When the cache is opened to read, the parent process sends over the + // origin's Metadata so the child process can select the cache entry to open + // (based on hash) and notify the parent (via SelectCacheFileToRead). +child: + async OnOpenMetadataForRead(Metadata metadata); +parent: + async SelectCacheFileToRead(uint32_t moduleIndex); + async CacheMiss(); + +child: + // Once the cache file has been opened, the child is notified and sent an + // open file descriptor. + async OnOpenCacheFile(int64_t fileSize, FileDescriptor fileDesc); + +both: + async __delete__(AsmJSCacheResult result); +}; + +} // namespace asmjscache +} // namespace dom +} // namespace mozilla diff --git a/dom/asmjscache/moz.build b/dom/asmjscache/moz.build new file mode 100644 index 000000000..abb0b82fb --- /dev/null +++ b/dom/asmjscache/moz.build @@ -0,0 +1,23 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.dom.asmjscache += [ + 'AsmJSCache.h' +] + +SOURCES += [ + 'AsmJSCache.cpp' +] + +IPDL_SOURCES += [ + 'PAsmJSCacheEntry.ipdl' +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] diff --git a/dom/asmjscache/test/file_slow.js b/dom/asmjscache/test/file_slow.js new file mode 100644 index 000000000..963422143 --- /dev/null +++ b/dom/asmjscache/test/file_slow.js @@ -0,0 +1,73 @@ +function f1() { "use asm"; function g() {} return g } +if (this.jsFuns) { + ok(jsFuns.isAsmJSModule(f1), "f1 is an asm.js module"); + ok(jsFuns.isAsmJSFunction(f1()), "f1.g is an asm.js function"); +} + +function f2(stdlib, foreign, buffer) { + "use asm"; + var i32 = new stdlib.Int32Array(buffer); + function main(n) { + n = n|0; + var i = 0, sum = 0; + for (; (i|0) < (n|0); i=(i+1)|0) + sum = (sum + (i32[(i<<2)>>2]|0))|0; + return sum|0; + } + return main; +} +if (this.jsFuns) + ok(jsFuns.isAsmJSModule(f2), "f2 is an asm.js module"); +var i32 = new Int32Array(16384); // Smallest allowed buffer size is 64KBy +for (var i = 0; i < i32.length; i++) + i32[i] = i; +var f2Main = f2(this, null, i32.buffer); +if (this.jsFuns) + ok(jsFuns.isAsmJSFunction(f2Main), "f2.main is an asm.js function"); +if (f2Main(4) !== 6) + throw "f2Main(4)"; +if (f2Main(100) !== 4950) + throw "f2.main(100)"; +var sum = (((i32.length - 1) * i32.length) / 2); +if (f2Main(i32.length) !== sum) + throw "f2.main(" + i32.length + ")"; +if (f2Main(i32.length + 100) !== sum) + throw "f2.main(" + i32.length + ")"; + +function f3(stdlib, foreign, buffer) { + "use asm"; + var done = foreign.done; + var i32 = new stdlib.Int32Array(buffer); + function main() { + var i = 0, sum = 0; + while (1) { + for (i = 0; (i|0) < 1000; i=(i+1)|0) + sum = (sum + i)|0; + if (done(sum|0)|0) + break; + } + return sum|0; + } + return main; +} +var begin; +var lastSum; +function done(sum) { + if (sum !== ((lastSum + 499500)|0)) + throw "bad sum: " + sum + ", " + lastSum + ", " + ((lastSum + 499500)|0); + lastSum = sum; + return (Date.now() - begin) > 3000; +} +var f3Main = f3(this, {done:done}, i32.buffer); +if (this.jsFuns) + ok(jsFuns.isAsmJSFunction(f3Main), "f3.main is an asm.js function"); + +begin = Date.now(); +lastSum = 0; +if (f3Main() !== lastSum) + throw "f3.main()"; + +if (!this.jsFuns) + postMessage("ok"); +else + complete(); diff --git a/dom/asmjscache/test/mochitest.ini b/dom/asmjscache/test/mochitest.ini new file mode 100644 index 000000000..a4f8dba7e --- /dev/null +++ b/dom/asmjscache/test/mochitest.ini @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = + file_slow.js + +[test_cachingBasic.html] +[test_workers.html] +[test_cachingMulti.html] +[test_slow.html] +# bug 929498 +skip-if = os == 'android' diff --git a/dom/asmjscache/test/test_cachingBasic.html b/dom/asmjscache/test/test_cachingBasic.html new file mode 100644 index 000000000..e84fdba8b --- /dev/null +++ b/dom/asmjscache/test/test_cachingBasic.html @@ -0,0 +1,70 @@ + + + + + + asm.js browser tests + + + + + asm.js browser tests +

+ +

+
+  
+
+
+
diff --git a/dom/asmjscache/test/test_cachingMulti.html b/dom/asmjscache/test/test_cachingMulti.html
new file mode 100644
index 000000000..ca092fda0
--- /dev/null
+++ b/dom/asmjscache/test/test_cachingMulti.html
@@ -0,0 +1,85 @@
+
+
+
+
+  
+  asm.js browser tests
+  
+  
+
+
+  asm.js browser tests
+  

+ +

+
+  
+
+
+
+
diff --git a/dom/asmjscache/test/test_slow.html b/dom/asmjscache/test/test_slow.html
new file mode 100644
index 000000000..2f19041b3
--- /dev/null
+++ b/dom/asmjscache/test/test_slow.html
@@ -0,0 +1,48 @@
+
+
+
+
+  
+  asm.js browser tests
+  
+  
+
+
+  asm.js browser tests
+  

+ +

+
+  
+
+  
+
+
+
diff --git a/dom/asmjscache/test/test_workers.html b/dom/asmjscache/test/test_workers.html
new file mode 100644
index 000000000..992ed785b
--- /dev/null
+++ b/dom/asmjscache/test/test_workers.html
@@ -0,0 +1,74 @@
+
+
+
+
+  
+  asm.js browser tests
+  
+  
+
+
+  asm.js browser tests
+  

+ +

+
+  
+
+
+
-- 
cgit v1.2.3