Bug 1463587: Part 3 - Add bindings for SharedMap, and expose it via process message managers. r=erahm,baku r?bz draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 29 Jun 2018 14:55:27 -0700
changeset 816387 468a65f46075e9d23333427ec643e6058f42ce7d
parent 816386 dc80a950cf8df699ce688002bc0c45e3988df039
child 816388 0a2f2757853e5b557f7435ff394dbbd9b0c19e30
push id115826
push usermaglione.k@gmail.com
push dateWed, 11 Jul 2018 05:19:48 +0000
reviewerserahm, baku, bz
bugs1463587
milestone63.0a1
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
dom/bindings/Bindings.conf
dom/chrome-webidl/MessageManager.webidl
dom/chrome-webidl/MozSharedMap.webidl
dom/chrome-webidl/moz.build
dom/ipc/SharedMap.cpp
dom/ipc/SharedMap.h
dom/ipc/SharedMapChangeEvent.h
dom/ipc/moz.build
--- 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',