Bug 1463587: Part 3 - Add bindings for SharedMap, and expose it via process message managers. r=erahm,baku r?bz
This is the first basic implementation of a shared-memory key-value store for
JS message managers. It has one read-write endpoint in the parent process, and
separate read-only endpoints for each child-process message manager.
Changes to the parent endpoint are broadcast to the children as snapshots.
Each snapshot triggers a "change" event with a list of changed keys.
It currently has the following limitations:
- It only supports basic structured clone data. There's no support for blobs,
input streams, message ports... Blob support will be added in a follow-up
patch.
- Changes are currently only broadcast to child endpoints when flush() is
explicitly called in the parent, or when new child processes are launched.
In a follow-up, this will be changed to automatically flush after changes
when the event loop is idle.
- All set operations clone their inputs synchronously, which means that
there's no trivial way for callers to batch multiple changes to a single key
without some additional effort. It might be useful to add a
delayed-serialization option to the .set() call in a follow-up, for callers
who are sure they know what they're doing.
MozReview-Commit-ID: IM8a3UgejXU
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -564,16 +564,29 @@ DOMInterfaces = {
'nativeType': 'mozilla::dom::HTMLCanvasPrintState',
},
'MozChannel': {
'nativeType': 'nsIChannel',
'notflattened': True
},
+'MozSharedMap': {
+ 'nativeType': 'mozilla::dom::ipc::SharedMap',
+},
+
+'MozWritableSharedMap': {
+ 'headerFile': 'mozilla/dom/ipc/SharedMap.h',
+ 'nativeType': 'mozilla::dom::ipc::WritableSharedMap',
+},
+
+'MozSharedMapChangeEvent': {
+ 'nativeType': 'mozilla::dom::ipc::SharedMapChangeEvent',
+},
+
'MozStorageAsyncStatementParams': {
'headerFile': 'mozilla/storage/mozStorageAsyncStatementParams.h',
'nativeType': 'mozilla::storage::AsyncStatementParams',
},
'MozStorageStatementParams': {
'headerFile': 'mozilla/storage/mozStorageStatementParams.h',
'nativeType': 'mozilla::storage::StatementParams',
--- a/dom/chrome-webidl/MessageManager.webidl
+++ b/dom/chrome-webidl/MessageManager.webidl
@@ -452,16 +452,18 @@ interface GlobalProcessScriptLoader : Pr
* attribute of its childprocessmessagemanager.
*
* This value will always be a JS object if it's not null or undefined. Different
* users are expected to set properties on this object. The property name should be
* unique enough that other Gecko consumers won't accidentally choose it.
*/
[Throws]
readonly attribute any initialProcessData;
+
+ readonly attribute MozWritableSharedMap sharedData;
};
[ChromeOnly, Global, NeedResolve]
interface ContentFrameMessageManager : EventTarget
{
/**
* The current top level window in the frame or null.
*/
@@ -489,16 +491,18 @@ ContentFrameMessageManager implements Me
interface ContentProcessMessageManager
{
/**
* Read out a copy of the object that was initialized in the parent
* process via ProcessScriptLoader.initialProcessData.
*/
[Throws]
readonly attribute any initialProcessData;
+
+ readonly attribute MozSharedMap sharedData;
};
// MessageManagerGlobal inherits from SyncMessageSender, which is a real interface, not a
// mixin. This will need to change when we implement mixins according to the current
// WebIDL spec.
ContentProcessMessageManager implements MessageManagerGlobal;
/**
* Message "broadcasters" don't have a single "other side" that they send messages to, but
new file mode 100644
--- /dev/null
+++ b/dom/chrome-webidl/MozSharedMap.webidl
@@ -0,0 +1,54 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+typedef any StructuredClonable;
+
+[ChromeOnly]
+interface MozSharedMapChangeEvent : Event {
+ [Cached, Constant]
+ readonly attribute sequence<DOMString> changedKeys;
+};
+
+dictionary MozSharedMapChangeEventInit : EventInit {
+ required sequence<DOMString> changedKeys;
+};
+
+[ChromeOnly]
+interface MozSharedMap : EventTarget {
+ boolean has(DOMString name);
+
+ [Throws]
+ StructuredClonable get(DOMString name);
+
+ iterable<DOMString, StructuredClonable>;
+};
+
+[ChromeOnly]
+interface MozWritableSharedMap : MozSharedMap {
+ /**
+ * Sets the given key to the given structured-clonable value. The value is
+ * synchronously structured cloned, and the serialized value is saved in the
+ * map.
+ *
+ * Unless flush() is called, the new value will be broadcast to content
+ * processes after a short delay.
+ */
+ [Throws]
+ void set(DOMString name, StructuredClonable value);
+
+ /**
+ * Removes the given key from the map.
+ *
+ * Unless flush() is called, the removal will be broadcast to content
+ * processes after a short delay.
+ */
+ void delete(DOMString name);
+
+ /**
+ * Broadcasts any pending changes to all content processes.
+ */
+ void flush();
+};
--- a/dom/chrome-webidl/moz.build
+++ b/dom/chrome-webidl/moz.build
@@ -32,16 +32,17 @@ PREPROCESSED_WEBIDL_FILES = [
WEBIDL_FILES = [
'ChannelWrapper.webidl',
'DominatorTree.webidl',
'HeapSnapshot.webidl',
'InspectorUtils.webidl',
'MatchGlob.webidl',
'MatchPattern.webidl',
'MessageManager.webidl',
+ 'MozSharedMap.webidl',
'MozStorageAsyncStatementParams.webidl',
'MozStorageStatementParams.webidl',
'MozStorageStatementRow.webidl',
'PrecompiledScript.webidl',
'PromiseDebugging.webidl',
'StructuredCloneHolder.webidl',
'WebExtensionContentScript.webidl',
'WebExtensionPolicy.webidl',
--- a/dom/ipc/SharedMap.cpp
+++ b/dom/ipc/SharedMap.cpp
@@ -1,21 +1,23 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* vim: set ts=8 sts=4 et sw=4 tw=99: */
/* 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/ContentParent.h"
#include "mozilla/dom/ProcessGlobal.h"
+#include "mozilla/dom/ScriptSettings.h"
using namespace mozilla::loader;
namespace mozilla {
using namespace ipc;
namespace dom {
@@ -31,20 +33,22 @@ 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)
+ : DOMEventTargetHelper(aGlobal)
{
mMapFile.reset(new FileDescriptor(aMapFile));
mMapSize = aMapSize;
}
bool
SharedMap::Has(const nsACString& aName)
@@ -116,16 +120,81 @@ SharedMap::Update(const FileDescriptor&
mMap.reset();
if (mMapFile) {
*mMapFile = aMapFile;
} else {
mMapFile.reset(new FileDescriptor(aMapFile));
}
mMapSize = aMapSize;
mEntries.Clear();
+ mEntryArray.reset();
+
+
+ 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, NS_LITERAL_STRING("change"), 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 : IterHash(mEntries)) {
+ array.AppendElement(entry);
+ }
+ }
+
+ return mEntryArray.ref();
+}
+
+const nsString
+SharedMap::GetKeyAtIndex(uint32_t aIndex) const
+{
+ return NS_ConvertUTF8toUTF16(EntryArray()[aIndex]->Name());
+}
+
+JS::Value
+SharedMap::GetValueAtIndex(uint32_t aIndex) const
+{
+ JSObject* wrapper = GetWrapper();
+ MOZ_ASSERT(wrapper,
+ "Should never see GetValueAtIndex on a SharedMap without a live "
+ "wrapper");
+ if (!wrapper) {
+ return JS::NullValue();
+ }
+
+ AutoJSContext cx;
+
+ JSAutoRealm ar(cx, wrapper);
+
+ JS::RootedValue val(cx);
+ EntryArray()[aIndex]->Read(cx, &val, IgnoreErrors());
+
+ return val;
}
void
SharedMap::Entry::TakeData(StructuredCloneData&& aHolder)
{
mData = AsVariant(std::move(aHolder));
mSize = Holder().Data().Size();
@@ -354,15 +423,44 @@ WritableSharedMap::Flush()
}
void
WritableSharedMap::KeyChanged(const nsACString& aName)
{
if (!mChangedKeys.ContainsSorted(aName)) {
mChangedKeys.InsertElementSorted(aName);
}
+ mEntryArray.reset();
+}
+
+
+JSObject*
+SharedMap::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+ return MozSharedMap_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+JSObject*
+WritableSharedMap::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+ return MozWritableSharedMap_Binding::Wrap(aCx, this, aGivenProto);
}
-NS_IMPL_ISUPPORTS0(SharedMap)
+/* 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();
+}
} // ipc
} // dom
} // mozilla
--- a/dom/ipc/SharedMap.h
+++ b/dom/ipc/SharedMap.h
@@ -2,18 +2,21 @@
/* vim: set ts=8 sts=4 et sw=4 tw=99: */
/* 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 dom_ipc_SharedMap_h
#define dom_ipc_SharedMap_h
+#include "mozilla/dom/MozSharedMapBinding.h"
+
#include "mozilla/AutoMemMap.h"
#include "mozilla/dom/ipc/StructuredCloneData.h"
+#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/Maybe.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Variant.h"
#include "nsClassHashtable.h"
#include "nsTArray.h"
class nsIGlobalObject;
@@ -40,36 +43,69 @@ namespace ipc {
* future, it will happen automatically in idle tasks.
*
*
* Whenever a read-only SharedMap is updated, it dispatches a "change" event.
* The event contains a "changedKeys" property with a list of all keys which
* were changed in the last update batch. Change events are never dispatched to
* WritableSharedMap instances.
*/
-class SharedMap : public nsISupports
+class SharedMap : public DOMEventTargetHelper
{
using FileDescriptor = mozilla::ipc::FileDescriptor;
public:
- NS_DECL_ISUPPORTS
SharedMap();
SharedMap(nsIGlobalObject* aGlobal, const FileDescriptor&, size_t);
// Returns true if the map contains the given (UTF-8) key.
bool Has(const nsACString& name);
// If the map contains the given (UTF-8) key, decodes and returns a new copy
// of its value. Otherwise returns null.
void Get(JSContext* cx, const nsACString& name, JS::MutableHandleValue aRetVal,
ErrorResult& aRv);
+ // Conversion helpers for WebIDL callers
+ bool Has(const nsAString& aName)
+ {
+ return Has(NS_ConvertUTF16toUTF8(aName));
+ }
+
+ void Get(JSContext* aCx, const nsAString& aName, JS::MutableHandleValue aRetVal,
+ ErrorResult& aRv)
+ {
+ return Get(aCx, NS_ConvertUTF16toUTF8(aName), aRetVal, aRv);
+ }
+
+
+ /**
+ * WebIDL iterator glue.
+ */
+ uint32_t GetIterableLength() const
+ {
+ return EntryArray().Length();
+ }
+
+ /**
+ * These functions return the key or value, respectively, at the given index.
+ * The index *must* be less than the value returned by GetIterableLength(), or
+ * the program will crash.
+ */
+ const nsString GetKeyAtIndex(uint32_t aIndex) const;
+ // Note: This function should only be called if the instance has a live,
+ // cached wrapper. If it does not, this function will return null, and assert
+ // in debug builds.
+ // The returned value will always be in the same Realm as that wrapper.
+ JS::Value GetValueAtIndex(uint32_t aIndex) const;
+
+
/**
* Returns a copy of the read-only file descriptor which backs the shared
* memory region for this map. The file descriptor may be passed between
* processes, and used to update corresponding instances in child processes.
*/
FileDescriptor CloneMapFile();
/**
@@ -84,18 +120,20 @@ public:
* Updates this instance to reflect the contents of the shared memory region
* in the given map file, and broadcasts a change event for the given set of
* changed (UTF-8-encoded) keys.
*/
void Update(const FileDescriptor& aMapFile, size_t aMapSize,
nsTArray<nsCString>&& aChangedKeys);
+ JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
protected:
- virtual ~SharedMap() = default;
+ ~SharedMap() override = default;
class Entry
{
public:
Entry(Entry&&) = delete;
explicit Entry(SharedMap& aMap, const nsACString& aName = EmptyCString())
: mMap(aMap)
@@ -216,32 +254,35 @@ protected:
* replaced with a buffer offset, as described above.
*/
Variant<uint32_t, StructuredCloneData> mData;
// The size, in bytes, of the entry's structured clone data.
uint32_t mSize = 0;
};
+ const nsTArray<Entry*>& EntryArray() const;
+
// Rebuilds the entry hashtable mEntries from the values serialized in the
// current snapshot, if necessary. The hashtable is rebuilt lazily after
// construction and after every Update() call, so this function must be called
// before any attempt to access mEntries.
Result<Ok, nsresult> MaybeRebuild();
void MaybeRebuild() const;
// Note: This header is included by WebIDL binding headers, and therefore
// can't include "windows.h". Since FileDescriptor.h does include "windows.h"
// on Windows, we can only forward declare FileDescriptor, and can't include
// it as an inline member.
UniquePtr<FileDescriptor> mMapFile;
// The size of the memory-mapped region backed by mMapFile, in bytes.
size_t mMapSize = 0;
mutable nsClassHashtable<nsCStringHashKey, Entry> mEntries;
+ mutable Maybe<nsTArray<Entry*>> mEntryArray;
// Manages the memory mapping of the current snapshot. This is initialized
// lazily after each SharedMap construction or updated, based on the values in
// mMapFile and mMapSize.
loader::AutoMemMap mMap;
bool mWritable = false;
@@ -249,36 +290,53 @@ protected:
// offsets are relative to this pointer, and Entry objects access their
// structured clone data by indexing this pointer.
char* Data() { return mMap.get<char>().get(); }
};
class WritableSharedMap final : public SharedMap
{
public:
+
WritableSharedMap();
// Sets the value of the given (UTF-8 encoded) key to a structured clone
// snapshot of the given value.
void Set(JSContext* cx, const nsACString& name, JS::HandleValue value, ErrorResult& aRv);
// Deletes the given (UTF-8 encoded) key from the map.
void Delete(const nsACString& name);
+
+ // Conversion helpers for WebIDL callers
+ void Set(JSContext* aCx, const nsAString& aName, JS::HandleValue aValue, ErrorResult& aRv)
+ {
+ return Set(aCx, NS_ConvertUTF16toUTF8(aName), aValue, aRv);
+ }
+
+ void Delete(const nsAString& aName)
+ {
+ return Delete(NS_ConvertUTF16toUTF8(aName));
+ }
+
+
// Flushes any queued changes to a new snapshot, and broadcasts it to all
// child SharedMap instances.
void Flush();
/**
* Returns the read-only SharedMap instance corresponding to this
* WritableSharedMap for use in the parent process.
*/
SharedMap* GetReadOnly();
+
+ JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
protected:
~WritableSharedMap() override = default;
private:
// The set of (UTF-8 encoded) keys which have changed, or been deleted, since
// the last snapshot.
nsTArray<nsCString> mChangedKeys;
new file mode 100644
--- /dev/null
+++ b/dom/ipc/SharedMapChangeEvent.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=4 et sw=4 tw=99: */
+/* 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 dom_ipc_SharedMapChangeEvent_h
+#define dom_ipc_SharedMapChangeEvent_h
+
+#include "mozilla/dom/MozSharedMapBinding.h"
+
+#include "mozilla/dom/Event.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+namespace ipc {
+
+class SharedMapChangeEvent final : public Event
+{
+public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(SharedMapChangeEvent, Event)
+
+ JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+ {
+ return MozSharedMapChangeEvent_Binding::Wrap(aCx, this, aGivenProto);
+ }
+
+ static already_AddRefed<SharedMapChangeEvent>
+ Constructor(EventTarget* aEventTarget,
+ const nsAString& aType,
+ const MozSharedMapChangeEventInit& aInit);
+
+ void GetChangedKeys(nsTArray<nsString>& aChangedKeys) const
+ {
+ aChangedKeys.AppendElements(mChangedKeys);
+ }
+
+protected:
+ ~SharedMapChangeEvent() override = default;
+
+private:
+ explicit SharedMapChangeEvent(EventTarget* aEventTarget)
+ : Event(aEventTarget, nullptr, nullptr)
+ {}
+
+ nsTArray<nsString> mChangedKeys;
+};
+
+} // ipc
+} // dom
+} // mozilla
+
+#endif // dom_ipc_SharedMapChangeEvent_h
--- a/dom/ipc/moz.build
+++ b/dom/ipc/moz.build
@@ -11,16 +11,17 @@ XPIDL_SOURCES += [
'nsIHangReport.idl',
]
XPIDL_MODULE = 'dom'
EXPORTS.mozilla.dom.ipc += [
'IdType.h',
'SharedMap.h',
+ 'SharedMapChangeEvent.h',
'StructuredCloneData.h',
]
EXPORTS.mozilla.dom += [
'CoalescedInputData.h',
'CoalescedMouseData.h',
'CoalescedWheelData.h',
'ContentBridgeChild.h',