gecko-dev/dom/ipc/SharedMap.cpp

467 lines
14 KiB
C++

/* -*- 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