diff options
Diffstat (limited to '')
-rw-r--r-- | dom/ipc/SharedMap.cpp | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/dom/ipc/SharedMap.cpp b/dom/ipc/SharedMap.cpp new file mode 100644 index 0000000000..70e12784e9 --- /dev/null +++ b/dom/ipc/SharedMap.cpp @@ -0,0 +1,466 @@ +/* -*- 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 "SharedMap.h" +#include "SharedMapChangeEvent.h" + +#include "MemMapSnapshot.h" +#include "ScriptPreloader-inl.h" + +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ContentProcessMessageManager.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/IOBuffers.h" +#include "mozilla/ScriptPreloader.h" + +using namespace mozilla::loader; + +namespace mozilla { + +using namespace ipc; + +namespace dom::ipc { + +// Align to size of uintptr_t here, to be safe. It's probably not strictly +// necessary, though. +constexpr size_t kStructuredCloneAlign = sizeof(uintptr_t); + +static inline void AlignTo(size_t* aOffset, size_t aAlign) { + if (auto mod = *aOffset % aAlign) { + *aOffset += aAlign - mod; + } +} + +SharedMap::SharedMap() : DOMEventTargetHelper() {} + +SharedMap::SharedMap(nsIGlobalObject* aGlobal, const FileDescriptor& aMapFile, + size_t aMapSize, nsTArray<RefPtr<BlobImpl>>&& aBlobs) + : DOMEventTargetHelper(aGlobal), mBlobImpls(std::move(aBlobs)) { + mMapFile.reset(new FileDescriptor(aMapFile)); + mMapSize = aMapSize; +} + +bool SharedMap::Has(const nsACString& aName) { + Unused << MaybeRebuild(); + return mEntries.Contains(aName); +} + +void SharedMap::Get(JSContext* aCx, const nsACString& aName, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) { + auto res = MaybeRebuild(); + if (res.isErr()) { + aRv.Throw(res.unwrapErr()); + return; + } + + Entry* entry = mEntries.Get(aName); + if (!entry) { + aRetVal.setNull(); + return; + } + + entry->Read(aCx, aRetVal, aRv); +} + +void SharedMap::Entry::Read(JSContext* aCx, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) { + if (mData.is<StructuredCloneData>()) { + // We have a temporary buffer for a key that was changed after the last + // snapshot. Just decode it directly. + auto& holder = mData.as<StructuredCloneData>(); + holder.Read(aCx, aRetVal, aRv); + return; + } + + // We have a pointer to a shared memory region containing our structured + // clone data. Create a temporary buffer to decode that data, and then + // discard it so that we don't keep a separate process-local copy around any + // longer than necessary. + StructuredCloneData holder; + if (!holder.CopyExternalData(Data(), Size())) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + if (mBlobCount) { + holder.BlobImpls().AppendElements(Blobs()); + } + holder.Read(aCx, aRetVal, aRv); +} + +FileDescriptor SharedMap::CloneMapFile() const { + if (mMap.initialized()) { + return mMap.cloneHandle(); + } + return *mMapFile; +} + +void SharedMap::Update(const FileDescriptor& aMapFile, size_t aMapSize, + nsTArray<RefPtr<BlobImpl>>&& aBlobs, + nsTArray<nsCString>&& aChangedKeys) { + MOZ_DIAGNOSTIC_ASSERT(!mWritable); + + mMap.reset(); + if (mMapFile) { + *mMapFile = aMapFile; + } else { + mMapFile.reset(new FileDescriptor(aMapFile)); + } + mMapSize = aMapSize; + mEntries.Clear(); + mEntryArray.reset(); + + mBlobImpls = std::move(aBlobs); + + AutoEntryScript aes(GetParentObject(), "SharedMap change event"); + JSContext* cx = aes.cx(); + + RootedDictionary<MozSharedMapChangeEventInit> init(cx); + if (!init.mChangedKeys.SetCapacity(aChangedKeys.Length(), fallible)) { + NS_WARNING("Failed to dispatch SharedMap change event"); + return; + } + for (auto& key : aChangedKeys) { + Unused << init.mChangedKeys.AppendElement(NS_ConvertUTF8toUTF16(key), + fallible); + } + + RefPtr<SharedMapChangeEvent> event = + SharedMapChangeEvent::Constructor(this, u"change"_ns, init); + event->SetTrusted(true); + + DispatchEvent(*event); +} + +const nsTArray<SharedMap::Entry*>& SharedMap::EntryArray() const { + if (mEntryArray.isNothing()) { + MaybeRebuild(); + + mEntryArray.emplace(mEntries.Count()); + auto& array = mEntryArray.ref(); + for (auto& entry : mEntries) { + array.AppendElement(entry.GetWeak()); + } + } + + return mEntryArray.ref(); +} + +const nsString SharedMap::GetKeyAtIndex(uint32_t aIndex) const { + return NS_ConvertUTF8toUTF16(EntryArray()[aIndex]->Name()); +} + +bool SharedMap::GetValueAtIndex(JSContext* aCx, uint32_t aIndex, + JS::MutableHandle<JS::Value> aResult) const { + ErrorResult rv; + EntryArray()[aIndex]->Read(aCx, aResult, rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + return true; +} + +void SharedMap::Entry::TakeData(StructuredCloneData&& aHolder) { + mData = AsVariant(std::move(aHolder)); + + mSize = Holder().Data().Size(); + mBlobCount = Holder().BlobImpls().Length(); +} + +void SharedMap::Entry::ExtractData(char* aDestPtr, uint32_t aNewOffset, + uint16_t aNewBlobOffset) { + if (mData.is<StructuredCloneData>()) { + char* ptr = aDestPtr; + Holder().Data().ForEachDataChunk([&](const char* aData, size_t aSize) { + memcpy(ptr, aData, aSize); + ptr += aSize; + return true; + }); + MOZ_ASSERT(uint32_t(ptr - aDestPtr) == mSize); + } else { + memcpy(aDestPtr, Data(), mSize); + } + + mData = AsVariant(aNewOffset); + mBlobOffset = aNewBlobOffset; +} + +Result<Ok, nsresult> SharedMap::MaybeRebuild() { + if (!mMapFile) { + return Ok(); + } + + // This function maps a shared memory region created by Serialize() and reads + // its header block to build a new mEntries hashtable of its contents. + // + // The entries created by this function contain a pointer to this SharedMap + // instance, and the offsets and sizes of their structured clone data within + // its shared memory region. When needed, that structured clone data is + // retrieved directly as indexes into the SharedMap's shared memory region. + + MOZ_TRY(mMap.initWithHandle(*mMapFile, mMapSize)); + mMapFile.reset(); + + // We should be able to pass this range as an initializer list or an immediate + // param, but gcc currently chokes on that if optimization is enabled, and + // initializes everything to 0. + Range<uint8_t> range(&mMap.get<uint8_t>()[0], mMap.size()); + InputBuffer buffer(range); + + uint32_t count; + buffer.codeUint32(count); + + for (uint32_t i = 0; i < count; i++) { + auto entry = MakeUnique<Entry>(*this); + entry->Code(buffer); + + // This buffer was created at runtime, during this session, so any errors + // indicate memory corruption, and are fatal. + MOZ_RELEASE_ASSERT(!buffer.error()); + + // Note: While the order of evaluation of the arguments to Put doesn't + // matter for this (the actual move will only happen within Put), to be + // clear about this, we call entry->Name() before calling Put. + const auto& name = entry->Name(); + mEntries.InsertOrUpdate(name, std::move(entry)); + } + + return Ok(); +} + +void SharedMap::MaybeRebuild() const { + Unused << const_cast<SharedMap*>(this)->MaybeRebuild(); +} + +WritableSharedMap::WritableSharedMap() : SharedMap() { + mWritable = true; + // Serialize the initial empty contents of the map immediately so that we + // always have a file descriptor to send to callers of CloneMapFile(). + Unused << Serialize(); + MOZ_RELEASE_ASSERT(mMap.initialized()); +} + +SharedMap* WritableSharedMap::GetReadOnly() { + if (!mReadOnly) { + nsTArray<RefPtr<BlobImpl>> blobs(mBlobImpls.Clone()); + mReadOnly = + new SharedMap(ContentProcessMessageManager::Get()->GetParentObject(), + CloneMapFile(), MapSize(), std::move(blobs)); + } + return mReadOnly; +} + +Result<Ok, nsresult> WritableSharedMap::Serialize() { + // Serializes a new snapshot of the map, initializes a new read-only shared + // memory region with its contents, and updates all entries to point to that + // new snapshot. + // + // The layout of the snapshot is as follows: + // + // - A header containing a uint32 count field containing the number of + // entries in the map, followed by that number of serialized entry headers, + // as produced by Entry::Code. + // + // - A data block containing structured clone data for each of the entries' + // values. This data is referenced by absolute byte offsets from the start + // of the shared memory region, encoded in each of the entry header values. + // Each entry's data is aligned to kStructuredCloneAlign, and therefore may + // have alignment padding before it. + // + // This serialization format is decoded by the MaybeRebuild() method of + // read-only SharedMap() instances, and used to populate their mEntries + // hashtables. + // + // Writable instances never read the header blocks, but instead directly + // update their Entry instances to point to the appropriate offsets in the + // shared memory region created by this function. + + uint32_t count = mEntries.Count(); + + size_t dataSize = 0; + size_t headerSize = sizeof(count); + size_t blobCount = 0; + + for (const auto& entry : mEntries.Values()) { + headerSize += entry->HeaderSize(); + blobCount += entry->BlobCount(); + + dataSize += entry->Size(); + AlignTo(&dataSize, kStructuredCloneAlign); + } + + size_t offset = headerSize; + AlignTo(&offset, kStructuredCloneAlign); + + OutputBuffer header; + header.codeUint32(count); + + MemMapSnapshot mem; + MOZ_TRY(mem.Init(offset + dataSize)); + + auto ptr = mem.Get<char>(); + + // We need to build the new array of blobs before we overwrite the existing + // one, since previously-serialized entries will store their blob references + // as indexes into our blobs array. + nsTArray<RefPtr<BlobImpl>> blobImpls(blobCount); + + for (const auto& entry : mEntries.Values()) { + AlignTo(&offset, kStructuredCloneAlign); + + size_t blobOffset = blobImpls.Length(); + if (entry->BlobCount()) { + blobImpls.AppendElements(entry->Blobs()); + } + + entry->ExtractData(&ptr[offset], offset, blobOffset); + entry->Code(header); + + offset += entry->Size(); + } + + mBlobImpls = std::move(blobImpls); + + // FIXME: We should create a separate OutputBuffer class which can encode to + // a static memory region rather than dynamically allocating and then + // copying. + MOZ_ASSERT(header.cursor() == headerSize); + memcpy(ptr.get(), header.Get(), header.cursor()); + + // We've already updated offsets at this point. We need this to succeed. + mMap.reset(); + MOZ_RELEASE_ASSERT(mem.Finalize(mMap).isOk()); + + return Ok(); +} + +void WritableSharedMap::SendTo(ContentParent* aParent) const { + nsTArray<IPCBlob> blobs(mBlobImpls.Length()); + + for (auto& blobImpl : mBlobImpls) { + nsresult rv = IPCBlobUtils::Serialize(blobImpl, *blobs.AppendElement()); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + } + + Unused << aParent->SendUpdateSharedData(CloneMapFile(), mMap.size(), blobs, + mChangedKeys); +} + +void WritableSharedMap::BroadcastChanges() { + if (mChangedKeys.IsEmpty()) { + return; + } + + if (!Serialize().isOk()) { + return; + } + + nsTArray<ContentParent*> parents; + ContentParent::GetAll(parents); + for (auto& parent : parents) { + SendTo(parent); + } + + if (mReadOnly) { + nsTArray<RefPtr<BlobImpl>> blobImpls(mBlobImpls.Clone()); + mReadOnly->Update(CloneMapFile(), mMap.size(), std::move(blobImpls), + std::move(mChangedKeys)); + } + + mChangedKeys.Clear(); +} + +void WritableSharedMap::Delete(const nsACString& aName) { + if (mEntries.Remove(aName)) { + KeyChanged(aName); + } +} + +void WritableSharedMap::Set(JSContext* aCx, const nsACString& aName, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + StructuredCloneData holder; + + holder.Write(aCx, aValue, aRv); + if (aRv.Failed()) { + return; + } + + if (!holder.InputStreams().IsEmpty()) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Entry* entry = mEntries.GetOrInsertNew(aName, *this, aName); + entry->TakeData(std::move(holder)); + + KeyChanged(aName); +} + +void WritableSharedMap::Flush() { BroadcastChanges(); } + +void WritableSharedMap::IdleFlush() { + mPendingFlush = false; + Flush(); +} + +nsresult WritableSharedMap::KeyChanged(const nsACString& aName) { + if (!mChangedKeys.ContainsSorted(aName)) { + mChangedKeys.InsertElementSorted(aName); + } + mEntryArray.reset(); + + if (!mPendingFlush) { + MOZ_TRY(NS_DispatchToCurrentThreadQueue( + NewRunnableMethod("WritableSharedMap::IdleFlush", this, + &WritableSharedMap::IdleFlush), + EventQueuePriority::Idle)); + mPendingFlush = true; + } + return NS_OK; +} + +JSObject* SharedMap::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MozSharedMap_Binding::Wrap(aCx, this, aGivenProto); +} + +JSObject* WritableSharedMap::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MozWritableSharedMap_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed<SharedMapChangeEvent> SharedMapChangeEvent::Constructor( + EventTarget* aEventTarget, const nsAString& aType, + const MozSharedMapChangeEventInit& aInit) { + RefPtr<SharedMapChangeEvent> event = new SharedMapChangeEvent(aEventTarget); + + bool trusted = event->Init(aEventTarget); + event->InitEvent(aType, aInit.mBubbles, aInit.mCancelable); + event->SetTrusted(trusted); + event->SetComposed(aInit.mComposed); + + event->mChangedKeys = aInit.mChangedKeys; + + return event.forget(); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableSharedMap, SharedMap, mReadOnly) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WritableSharedMap) +NS_INTERFACE_MAP_END_INHERITING(SharedMap) + +NS_IMPL_ADDREF_INHERITED(WritableSharedMap, SharedMap) +NS_IMPL_RELEASE_INHERITED(WritableSharedMap, SharedMap) + +} // namespace dom::ipc +} // namespace mozilla |