Bug 1168606 - Support preloading also for index cursors. r=ttung,asuth
authorSimon Giesecke <sgiesecke@mozilla.com>
Tue, 05 Nov 2019 14:40:35 +0000
changeset 500618 c009bbdbbfd5451756cb1f3dc421b82df50911d1
parent 500617 89817a19f3b38187bca7eecd50cbf39867c4dad8
child 500619 b525ad6838f091c2ab7cfc52cda593eca7941fa8
push id36768
push usershindli@mozilla.com
push dateTue, 05 Nov 2019 22:07:34 +0000
treeherdermozilla-central@e96c1ca93d25 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersttung, asuth
bugs1168606
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1168606 - Support preloading also for index cursors. r=ttung,asuth Depends on D43252 Differential Revision: https://phabricator.services.mozilla.com/D43461
dom/indexedDB/ActorsChild.cpp
dom/indexedDB/ActorsChild.h
dom/indexedDB/ActorsParent.cpp
dom/indexedDB/IDBCursor.cpp
dom/indexedDB/IDBCursor.h
dom/indexedDB/PBackgroundIDBCursor.ipdl
--- a/dom/indexedDB/ActorsChild.cpp
+++ b/dom/indexedDB/ActorsChild.cpp
@@ -3210,16 +3210,28 @@ BackgroundRequestChild::PreprocessHelper
 
   return NS_OK;
 }
 
 /*******************************************************************************
  * BackgroundCursorChild
  ******************************************************************************/
 
+BackgroundCursorChild::CachedResponse::CachedResponse(
+    Key aKey, StructuredCloneReadInfo&& aCloneInfo)
+    : mKey{std::move(aKey)}, mCloneInfo{std::move(aCloneInfo)} {}
+
+BackgroundCursorChild::CachedResponse::CachedResponse(
+    Key aKey, Key aLocaleAwareKey, Key aObjectStoreKey,
+    StructuredCloneReadInfo&& aCloneInfo)
+    : mKey{std::move(aKey)},
+      mLocaleAwareKey{std::move(aLocaleAwareKey)},
+      mObjectStoreKey{std::move(aObjectStoreKey)},
+      mCloneInfo{std::move(aCloneInfo)} {}
+
 // Does not need to be threadsafe since this only runs on one thread, but
 // inheriting from CancelableRunnable is easy.
 class BackgroundCursorChild::DelayedActionRunnable final
     : public CancelableRunnable {
   using ActionFunc = void (BackgroundCursorChild::*)();
 
   BackgroundCursorChild* mActor;
   RefPtr<IDBRequest> mRequest;
@@ -3280,17 +3292,18 @@ BackgroundCursorChild::BackgroundCursorC
   MOZ_COUNT_CTOR(indexedDB::BackgroundCursorChild);
 }
 
 BackgroundCursorChild::~BackgroundCursorChild() {
   MOZ_COUNT_DTOR(indexedDB::BackgroundCursorChild);
 }
 
 void BackgroundCursorChild::SendContinueInternal(
-    const CursorRequestParams& aParams, const Key& aCurrentKey) {
+    const CursorRequestParams& aParams, const Key& aCurrentKey,
+    const Key& aCurrentObjectStoreKey) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mRequest);
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(mCursor);
   MOZ_ASSERT(!mStrongRequest);
   MOZ_ASSERT(!mStrongCursor);
 
   // Make sure all our DOM objects stay alive.
@@ -3298,41 +3311,52 @@ void BackgroundCursorChild::SendContinue
 
   MOZ_ASSERT(mRequest->ReadyState() == IDBRequestReadyState::Done);
   mRequest->Reset();
 
   mTransaction->OnNewRequest();
 
   CursorRequestParams params = aParams;
   Key currentKey = aCurrentKey;
+  Key currentObjectStoreKey = aCurrentObjectStoreKey;
 
   switch (params.type()) {
     case CursorRequestParams::TContinueParams: {
       const auto& key = params.get_ContinueParams().key();
       if (key.IsUnset()) {
         break;
       }
 
       // Invalidate cache entries.
       size_t discardedCount = 0;
       while (!mCachedResponses.empty()) {
+        // This duplicates the logic from the parent. We could avoid this
+        // duplication if we invalidated the cached records always for any
+        // continue-with-key operation, but would lose the benefits of
+        // preloading then.
+        const auto& cachedSortKey =
+            mCursor->IsLocaleAware() ? mCachedResponses.front().mLocaleAwareKey
+                                     : mCachedResponses.front().mKey;
         const auto& keyOperator = GetKeyOperator(mDirection);
-        if ((mCachedResponses.front().mKey.*keyOperator)(key)) {
+        if ((cachedSortKey.*keyOperator)(key)) {
           IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
-              "PRELOAD: Continue to key %s, keeping cached key %s and further",
+              "PRELOAD: Continue to key %s, keeping cached key %s/%s and "
+              "further",
               "Continue/keep", mTransaction->LoggingSerialNumber(),
               mRequest->LoggingSerialNumber(), key.GetBuffer().get(),
-              mCachedResponses.front().mKey.GetBuffer().get());
+              mCachedResponses.front().mKey.GetBuffer().get(),
+              mCachedResponses.front().mObjectStoreKey.GetBuffer().get());
           break;
         }
         IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
-            "PRELOAD: Continue to key %s, discarding cached key %s",
+            "PRELOAD: Continue to key %s, discarding cached key %s/%s",
             "Continue/discard", mTransaction->LoggingSerialNumber(),
             mRequest->LoggingSerialNumber(), key.GetBuffer().get(),
-            mCachedResponses.front().mKey.GetBuffer().get());
+            mCachedResponses.front().mKey.GetBuffer().get(),
+            mCachedResponses.front().mObjectStoreKey.GetBuffer().get());
         mCachedResponses.pop_front();
         ++discardedCount;
       }
       IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
           "PRELOAD: Discarded %zu cached responses before requested "
           "continue key, %zu remaining",
           "Discarding", mTransaction->LoggingSerialNumber(),
           mRequest->LoggingSerialNumber(), discardedCount,
@@ -3355,16 +3379,17 @@ void BackgroundCursorChild::SendContinue
       // Invalidate cache entries.
       size_t discardedCount = 0;
       while (!mCachedResponses.empty() && advanceCount > 1) {
         --advanceCount;
 
         // TODO: We only need to update currentKey on the last entry, the others
         // are overwritten in the next iteration anyway.
         currentKey = mCachedResponses.front().mKey;
+        currentObjectStoreKey = mCachedResponses.front().mObjectStoreKey;
         mCachedResponses.pop_front();
         ++discardedCount;
       }
       IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
           "PRELOAD: Discarded %zu cached responses that are advanced over, "
           "%zu remaining",
           "Discarded", mTransaction->LoggingSerialNumber(),
           mRequest->LoggingSerialNumber(), discardedCount,
@@ -3392,33 +3417,48 @@ void BackgroundCursorChild::SendContinue
         this, &BackgroundCursorChild::CompleteContinueRequestFromCache);
     MOZ_ALWAYS_TRUE(
         NS_SUCCEEDED(NS_DispatchToCurrentThread(continueRunnable.forget())));
 
     // TODO: Could we preload further entries in the background when the size of
     // mCachedResponses falls under some threshold? Or does the response
     // handling model disallow this?
   } else {
-    MOZ_ALWAYS_TRUE(
-        PBackgroundIDBCursorChild::SendContinue(params, currentKey));
+    MOZ_ALWAYS_TRUE(PBackgroundIDBCursorChild::SendContinue(
+        params, currentKey, currentObjectStoreKey));
   }
 }
 
 void BackgroundCursorChild::CompleteContinueRequestFromCache() {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(mCursor);
   MOZ_ASSERT(mStrongCursor);
   MOZ_ASSERT(!mDelayedResponses.empty());
+  // TODO: Also support the other types.
+  MOZ_ASSERT(mCursor->GetType() == IDBCursor::Type_ObjectStore ||
+             mCursor->GetType() == IDBCursor::Type_Index);
 
   RefPtr<IDBCursor> cursor;
   mStrongCursor.swap(cursor);
 
   auto& item = mDelayedResponses.front();
-  mCursor->Reset(std::move(item.mKey), std::move(item.mCloneInfo));
+  switch (mCursor->GetType()) {
+    case IDBCursor::Type_ObjectStore:
+      mCursor->Reset(std::move(item.mKey), std::move(item.mCloneInfo));
+      break;
+    case IDBCursor::Type_Index:
+      mCursor->Reset(std::move(item.mKey), std::move(item.mLocaleAwareKey),
+                     std::move(item.mObjectStoreKey),
+                     std::move(item.mCloneInfo));
+      break;
+    default:
+      // TODO: Also support the other types.
+      MOZ_CRASH("Should never get here.");
+  }
   mDelayedResponses.pop_front();
 
   IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
       "PRELOAD: Consumed 1 cached response, %zu cached responses remaining",
       "Consumed cached response", mTransaction->LoggingSerialNumber(),
       mRequest->LoggingSerialNumber(),
       mDelayedResponses.size() + mCachedResponses.size());
 
@@ -3494,71 +3534,105 @@ void BackgroundCursorChild::HandleRespon
   if (!mCursor) {
     nsCOMPtr<nsIRunnable> deleteRunnable = new DelayedActionRunnable(
         this, &BackgroundCursorChild::SendDeleteMeInternal);
     MOZ_ALWAYS_SUCCEEDS(this->GetActorEventTarget()->Dispatch(
         deleteRunnable.forget(), NS_DISPATCH_NORMAL));
   }
 }
 
-void BackgroundCursorChild::HandleResponse(
-    const nsTArray<ObjectStoreCursorResponse>& aResponses) {
+template <typename T, typename Func>
+void BackgroundCursorChild::HandleMultipleCursorResponses(
+    const nsTArray<T>& aResponses, const Func& aHandleRecord) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mRequest);
   MOZ_ASSERT(mTransaction);
-  MOZ_ASSERT(mObjectStore);
   MOZ_ASSERT(!mStrongRequest);
   MOZ_ASSERT(!mStrongCursor);
+  MOZ_ASSERT(aResponses.Length() > 0);
 
   IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
       "PRELOAD: Received %zu cursor responses", "Received",
       mTransaction->LoggingSerialNumber(), mRequest->LoggingSerialNumber(),
       aResponses.Length());
   MOZ_ASSERT_IF(aResponses.Length() > 1, mCachedResponses.empty());
 
   // XXX Fix this somehow...
-  auto& responses =
-      const_cast<nsTArray<ObjectStoreCursorResponse>&>(aResponses);
-
-  for (ObjectStoreCursorResponse& response : responses) {
-    StructuredCloneReadInfo cloneReadInfo(std::move(response.cloneInfo()));
-    cloneReadInfo.mDatabase = mTransaction->Database();
-
-    // TODO: This uses response.cloneInfo() after it was apparently moved above,
-    // which would be invalid. However, it was not really moved, since
-    // StructuredCloneReadInfo::StructuredCloneReadInfo(SerializedStructuredCloneReadInfo&&)
-    // does not touch 'files' at all. This is, however, confusing.
-    // Can't this be done in the constructor of StructuredCloneReadInfo as well?
-    // Note: this will be fixed in a subsequent patch for bug 1168606.
-    DeserializeStructuredCloneFiles(
-        mTransaction->Database(), response.cloneInfo().files(),
-        /* aForPreprocess */ false, cloneReadInfo.mFiles);
-
-    RefPtr<IDBCursor> newCursor;
-
-    if (mCursor) {
-      if (mCursor->IsContinueCalled()) {
-        mCursor->Reset(std::move(response.key()), std::move(cloneReadInfo));
-      } else {
-        CachedResponse cachedResponse;
-        cachedResponse.mKey = std::move(response.key());
-        cachedResponse.mCloneInfo = std::move(cloneReadInfo);
-        mCachedResponses.emplace_back(std::move(cachedResponse));
-      }
-    } else {
-      newCursor = IDBCursor::Create(this, std::move(response.key()),
-                                    std::move(cloneReadInfo));
-      mCursor = newCursor;
-    }
+  auto& responses = const_cast<nsTArray<T>&>(aResponses);
+
+  for (auto& response : responses) {
+    IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
+        "PRELOAD: Processing response for key %s", "Processing",
+        mTransaction->LoggingSerialNumber(), mRequest->LoggingSerialNumber(),
+        response.key().GetBuffer().get());
+
+    aHandleRecord(response);
   }
 
   ResultHelper helper(mRequest, mTransaction, mCursor);
   DispatchSuccessEvent(&helper);
 }
 
+// Note: the parameter type is an rvalue reference, since passing it by value
+// yields a 'Type must not be used as parameter' error
+StructuredCloneReadInfo BackgroundCursorChild::PrepareCloneReadInfo(
+    SerializedStructuredCloneReadInfo&& aCloneInfo) const {
+  StructuredCloneReadInfo cloneReadInfo(
+      std::forward<SerializedStructuredCloneReadInfo>(aCloneInfo));
+  cloneReadInfo.mDatabase = mTransaction->Database();
+
+  // TODO: This uses response.cloneInfo() after it was apparently moved
+  // above, which would be invalid. However, it was not really moved,
+  // since
+  // StructuredCloneReadInfo::StructuredCloneReadInfo(SerializedStructuredCloneReadInfo&&)
+  // does not touch 'files' at all. This is, however, confusing.
+  // Can't this be done in the constructor of StructuredCloneReadInfo as
+  // well?
+  // Note: this will be fixed in a subsequent patch for bug 1168606.
+  DeserializeStructuredCloneFiles(mTransaction->Database(), aCloneInfo.files(),
+                                  /* aForPreprocess */ false,
+                                  cloneReadInfo.mFiles);
+
+  return cloneReadInfo;
+}
+
+void BackgroundCursorChild::HandleResponse(
+    const nsTArray<ObjectStoreCursorResponse>& aResponses) {
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mObjectStore);
+
+  HandleMultipleCursorResponses(
+      aResponses, [this](ObjectStoreCursorResponse& response) {
+        // TODO: Maybe move the deserialization of the clone-read-info into the
+        // cursor, so that it is only done for records actually accessed, which
+        // might not be the case for all cached records.
+        auto cloneReadInfo =
+            PrepareCloneReadInfo(std::move(response.cloneInfo()));
+
+        // TODO: the structure of the rest of this function is the same for all
+        // cursor responses, and it can be unified with a template function,
+        // only the arguments to IDBCursor::Reset, IDBCursor::Create and the
+        // fields for the cached response differ.
+        RefPtr<IDBCursor> newCursor;
+
+        if (mCursor) {
+          if (mCursor->IsContinueCalled()) {
+            mCursor->Reset(std::move(response.key()), std::move(cloneReadInfo));
+          } else {
+            mCachedResponses.emplace_back(std::move(response.key()),
+                                          std::move(cloneReadInfo));
+          }
+        } else {
+          newCursor = IDBCursor::Create(this, std::move(response.key()),
+                                        std::move(cloneReadInfo));
+          mCursor = newCursor;
+        }
+      });
+}
+
 void BackgroundCursorChild::HandleResponse(
     const ObjectStoreKeyCursorResponse& aResponse) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mRequest);
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(mObjectStore);
   MOZ_ASSERT(!mStrongRequest);
   MOZ_ASSERT(!mStrongCursor);
@@ -3575,52 +3649,47 @@ void BackgroundCursorChild::HandleRespon
     mCursor = newCursor;
   }
 
   ResultHelper helper(mRequest, mTransaction, mCursor);
   DispatchSuccessEvent(&helper);
 }
 
 void BackgroundCursorChild::HandleResponse(
-    const IndexCursorResponse& aResponse) {
+    const nsTArray<IndexCursorResponse>& aResponses) {
   AssertIsOnOwningThread();
-  MOZ_ASSERT(mRequest);
-  MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(mIndex);
-  MOZ_ASSERT(!mStrongRequest);
-  MOZ_ASSERT(!mStrongCursor);
-
-  // XXX Fix this somehow...
-  auto& response = const_cast<IndexCursorResponse&>(aResponse);
-
-  StructuredCloneReadInfo cloneReadInfo(std::move(response.cloneInfo()));
-  cloneReadInfo.mDatabase = mTransaction->Database();
-
-  DeserializeStructuredCloneFiles(
-      mTransaction->Database(), aResponse.cloneInfo().files(),
-      /* aForPreprocess */ false, cloneReadInfo.mFiles);
-
-  RefPtr<IDBCursor> newCursor;
-
-  if (mCursor) {
-    mCursor->Reset(std::move(response.key()), std::move(response.sortKey()),
-                   std::move(response.objectKey()), std::move(cloneReadInfo));
-  } else {
-    // TODO: This looks particularly dangerous to me. Why do we need to have an
-    // extra newCursor of type RefPtr? Why can't we directly assign to mCursor?
-    // Why is mCursor not a RefPtr? (A similar construct exists in the other
-    // HandleResponse overloads).
-    newCursor = IDBCursor::Create(
-        this, std::move(response.key()), std::move(response.sortKey()),
-        std::move(response.objectKey()), std::move(cloneReadInfo));
-    mCursor = newCursor;
-  }
-
-  ResultHelper helper(mRequest, mTransaction, mCursor);
-  DispatchSuccessEvent(&helper);
+
+  HandleMultipleCursorResponses(aResponses, [this](
+                                                IndexCursorResponse& response) {
+    auto cloneReadInfo = PrepareCloneReadInfo(std::move(response.cloneInfo()));
+
+    RefPtr<IDBCursor> newCursor;
+
+    if (mCursor) {
+      if (mCursor->IsContinueCalled()) {
+        mCursor->Reset(std::move(response.key()), std::move(response.sortKey()),
+                       std::move(response.objectKey()),
+                       std::move(cloneReadInfo));
+      } else {
+        mCachedResponses.emplace_back(
+            std::move(response.key()), std::move(response.sortKey()),
+            std::move(response.objectKey()), std::move(cloneReadInfo));
+      }
+    } else {
+      // TODO: This looks particularly dangerous to me. Why do we need to
+      // have an extra newCursor of type RefPtr? Why can't we directly
+      // assign to mCursor? Why is mCursor not a RefPtr? (A similar
+      // construct exists in the other HandleResponse overloads).
+      newCursor = IDBCursor::Create(
+          this, std::move(response.key()), std::move(response.sortKey()),
+          std::move(response.objectKey()), std::move(cloneReadInfo));
+      mCursor = newCursor;
+    }
+  });
 }
 
 void BackgroundCursorChild::HandleResponse(
     const IndexKeyCursorResponse& aResponse) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mRequest);
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(mIndex);
@@ -3704,18 +3773,18 @@ mozilla::ipc::IPCResult BackgroundCursor
     case CursorResponse::TArrayOfObjectStoreCursorResponse:
       HandleResponse(aResponse.get_ArrayOfObjectStoreCursorResponse());
       break;
 
     case CursorResponse::TObjectStoreKeyCursorResponse:
       HandleResponse(aResponse.get_ObjectStoreKeyCursorResponse());
       break;
 
-    case CursorResponse::TIndexCursorResponse:
-      HandleResponse(aResponse.get_IndexCursorResponse());
+    case CursorResponse::TArrayOfIndexCursorResponse:
+      HandleResponse(aResponse.get_ArrayOfIndexCursorResponse());
       break;
 
     case CursorResponse::TIndexKeyCursorResponse:
       HandleResponse(aResponse.get_IndexKeyCursorResponse());
       break;
 
     default:
       MOZ_CRASH("Should never get here!");
--- a/dom/indexedDB/ActorsChild.h
+++ b/dom/indexedDB/ActorsChild.h
@@ -613,32 +613,40 @@ class BackgroundRequestChild final : pub
 
   virtual mozilla::ipc::IPCResult Recv__delete__(
       const RequestResponse& aResponse) override;
 
   virtual mozilla::ipc::IPCResult RecvPreprocess(
       const PreprocessParams& aParams) override;
 };
 
+// TODO: Consider defining different subclasses for the different cursor types,
+// possibly using the CRTP, which would remove the need for various case
+// distinctions.
 class BackgroundCursorChild final : public PBackgroundIDBCursorChild {
   friend class BackgroundTransactionChild;
   friend class BackgroundVersionChangeTransactionChild;
 
   class DelayedActionRunnable;
 
   struct CachedResponse {
-    CachedResponse() = default;
+    CachedResponse() = delete;
+
+    CachedResponse(Key aKey, StructuredCloneReadInfo&& aCloneInfo);
+    CachedResponse(Key aKey, Key aLocaleAwareKey, Key aObjectStoreKey,
+                   StructuredCloneReadInfo&& aCloneInfo);
 
     CachedResponse(CachedResponse&& aOther) = default;
     CachedResponse& operator=(CachedResponse&& aOther) = default;
     CachedResponse(const CachedResponse& aOther) = delete;
     CachedResponse& operator=(const CachedResponse& aOther) = delete;
 
     Key mKey;
-    Key mObjectKey;  // TODO: This is not used anywhere right now
+    Key mLocaleAwareKey;
+    Key mObjectStoreKey;
     StructuredCloneReadInfo mCloneInfo;
   };
 
   IDBRequest* mRequest;
   IDBTransaction* mTransaction;
   IDBObjectStore* mObjectStore;
   IDBIndex* mIndex;
   IDBCursor* mCursor;
@@ -660,17 +668,18 @@ class BackgroundCursorChild final : publ
   BackgroundCursorChild(IDBRequest* aRequest, IDBIndex* aIndex,
                         Direction aDirection);
 
   void AssertIsOnOwningThread() const {
     NS_ASSERT_OWNINGTHREAD(BackgroundCursorChild);
   }
 
   void SendContinueInternal(const CursorRequestParams& aParams,
-                            const Key& aCurrentKey);
+                            const Key& aCurrentKey,
+                            const Key& aCurrentObjectStoreKey);
 
   void SendDeleteMeInternal();
 
   void InvalidateCachedResponses();
 
   IDBRequest* GetRequest() const {
     AssertIsOnOwningThread();
 
@@ -701,34 +710,41 @@ class BackgroundCursorChild final : publ
   ~BackgroundCursorChild();
 
   void CompleteContinueRequestFromCache();
 
   void HandleResponse(nsresult aResponse);
 
   void HandleResponse(const void_t& aResponse);
 
-  void HandleResponse(const nsTArray<ObjectStoreCursorResponse>& aResponse);
+  void HandleResponse(const nsTArray<ObjectStoreCursorResponse>& aResponses);
 
   void HandleResponse(const ObjectStoreKeyCursorResponse& aResponse);
 
-  void HandleResponse(const IndexCursorResponse& aResponse);
+  void HandleResponse(const nsTArray<IndexCursorResponse>& aResponses);
+
+  StructuredCloneReadInfo PrepareCloneReadInfo(
+      SerializedStructuredCloneReadInfo&& aCloneInfo) const;
+
+  template <typename T, typename Func>
+  void HandleMultipleCursorResponses(const nsTArray<T>& aResponses,
+                                     const Func& aHandleRecord);
 
   void HandleResponse(const IndexKeyCursorResponse& aResponse);
 
   // IPDL methods are only called by IPDL.
   virtual void ActorDestroy(ActorDestroyReason aWhy) override;
 
   virtual mozilla::ipc::IPCResult RecvResponse(
       const CursorResponse& aResponse) override;
 
  public:
   // Force callers to use SendContinueInternal.
-  bool SendContinue(const CursorRequestParams& aParams,
-                    const Key& aCurrentKey) = delete;
+  bool SendContinue(const CursorRequestParams& aParams, const Key& aCurrentKey,
+                    const Key& aCurrentObjectStoreKey) = delete;
 
   bool SendDeleteMe() = delete;
 };
 
 class BackgroundFileHandleChild : public PBackgroundFileHandleChild {
   friend class BackgroundMutableFileChild;
   friend IDBMutableFile;
 
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -7606,17 +7606,19 @@ class Cursor final : public PBackgroundI
   RefPtr<FullIndexMetadata> mIndexMetadata;
 
   const int64_t mObjectStoreId;
   const int64_t mIndexId;
 
   nsCString mContinueQuery;
   nsCString mContinueToQuery;
   nsCString mContinuePrimaryKeyQuery;
-  const nsCString mLocale;
+  const nsCString
+      mLocale;  ///< The locale if the cursor is locale-aware, otherwise empty.
+                ///< Note that only index-based cursors can be locale-aware.
 
   // TODO: Probably, it is still necessary to change more related identifiers
   // (e.g. local variables) and literals, to be in line with the new member
   // names below.
 
   Key mPosition;  ///< The current key, i.e. the key representing the cursor's
                   ///< position
                   ///< (https://w3c.github.io/IndexedDB/#cursor-position).
@@ -7669,18 +7671,19 @@ class Cursor final : public PBackgroundI
   // Must call SendResponseInternal!
   bool SendResponse(const CursorResponse& aResponse) = delete;
 
   // IPDL methods.
   void ActorDestroy(ActorDestroyReason aWhy) override;
 
   mozilla::ipc::IPCResult RecvDeleteMe() override;
 
-  mozilla::ipc::IPCResult RecvContinue(const CursorRequestParams& aParams,
-                                       const Key& key) override;
+  mozilla::ipc::IPCResult RecvContinue(
+      const CursorRequestParams& aParams, const Key& aCurrentKey,
+      const Key& aCurrentObjectStoreKey) override;
 
   bool IsLocaleAware() const { return !mLocale.IsEmpty(); }
 
   void SetOptionalKeyRange(const Maybe<SerializedKeyRange>& aOptionalKeyRange,
                            bool* aOpen);
 };
 
 class Cursor::CursorOpBase : public TransactionDatabaseOperationBase {
@@ -15218,17 +15221,18 @@ void Cursor::SendResponseInternal(
   MOZ_ASSERT(!mActorDestroyed);
   MOZ_ASSERT(mCurrentlyRunningOp);
 
   for (size_t i = 0; i < aFiles.Length(); ++i) {
     const auto& files = aFiles[i];
     if (!files.IsEmpty()) {
       MOZ_ASSERT(aResponse.type() ==
                      CursorResponse::TArrayOfObjectStoreCursorResponse ||
-                 aResponse.type() == CursorResponse::TIndexCursorResponse);
+                 aResponse.type() ==
+                     CursorResponse::TArrayOfIndexCursorResponse);
       MOZ_ASSERT(mDatabase);
       MOZ_ASSERT(mBackgroundParent);
 
       FallibleTArray<SerializedStructuredCloneFile> serializedFiles;
       nsresult rv = SerializeStructuredCloneFiles(
           mBackgroundParent, mDatabase, files,
           /* aForPreprocess */ false, serializedFiles);
       if (NS_WARN_IF(NS_FAILED(rv))) {
@@ -15240,20 +15244,22 @@ void Cursor::SendResponseInternal(
       switch (aResponse.type()) {
         case CursorResponse::TArrayOfObjectStoreCursorResponse: {
           auto& responses = aResponse.get_ArrayOfObjectStoreCursorResponse();
           MOZ_ASSERT(i < responses.Length());
           serializedInfo = &responses[i].cloneInfo();
           break;
         }
 
-        case CursorResponse::TIndexCursorResponse:
-          MOZ_ASSERT(i == 0);
-          serializedInfo = &aResponse.get_IndexCursorResponse().cloneInfo();
+        case CursorResponse::TArrayOfIndexCursorResponse: {
+          auto& responses = aResponse.get_ArrayOfIndexCursorResponse();
+          MOZ_ASSERT(i < responses.Length());
+          serializedInfo = &responses[i].cloneInfo();
           break;
+        }
 
         default:
           MOZ_CRASH("Should never get here!");
       }
 
       MOZ_ASSERT(serializedInfo);
       MOZ_ASSERT(serializedInfo->files().IsEmpty());
 
@@ -15297,18 +15303,19 @@ mozilla::ipc::IPCResult Cursor::RecvDele
 
   IProtocol* mgr = Manager();
   if (!PBackgroundIDBCursorParent::Send__delete__(this)) {
     return IPC_FAIL_NO_REASON(mgr);
   }
   return IPC_OK();
 }
 
-mozilla::ipc::IPCResult Cursor::RecvContinue(const CursorRequestParams& aParams,
-                                             const Key& aCurrentKey) {
+mozilla::ipc::IPCResult Cursor::RecvContinue(
+    const CursorRequestParams& aParams, const Key& aCurrentKey,
+    const Key& aCurrentObjectStoreKey) {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None);
   MOZ_ASSERT(!mActorDestroyed);
   MOZ_ASSERT(mObjectStoreMetadata);
   MOZ_ASSERT_IF(mType == OpenCursorParams::TIndexOpenCursorParams ||
                     mType == OpenCursorParams::TIndexOpenKeyCursorParams,
                 mIndexMetadata);
 
@@ -15316,17 +15323,18 @@ mozilla::ipc::IPCResult Cursor::RecvCont
 #ifdef DEBUG
       // Always verify parameters in DEBUG builds!
       false
 #else
       mIsSameProcessActor
 #endif
       ;
 
-  // At the time of writing, the child always passes its current position.
+  // At the time of writing, the child always passes its current position and
+  // object store position.
   //
   // TODO: Probably, aCurrentKey is always set, unless ActorsChild is changed to
   // pass it only when necessary, e.g. only pass if some cached responses were
   // invalidated, and the cursor in the parent actually needs to go back to a
   // previous position.
   if (!aCurrentKey.IsUnset()) {
     if (IsLocaleAware()) {
       const nsresult rv =
@@ -15334,16 +15342,23 @@ mozilla::ipc::IPCResult Cursor::RecvCont
       if (NS_WARN_IF(NS_FAILED(rv))) {
         ASSERT_UNLESS_FUZZING();
         return IPC_FAIL_NO_REASON(this);
       }
     }
     mPosition = aCurrentKey;
   }
 
+  if (!aCurrentObjectStoreKey.IsUnset()) {
+    MOZ_ASSERT(mType == OpenCursorParams::TIndexOpenCursorParams ||
+               mType == OpenCursorParams::TIndexOpenKeyCursorParams);
+
+    mObjectStorePosition = aCurrentObjectStoreKey;
+  }
+
   if (!trustParams && !VerifyRequestParams(aParams)) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
   }
 
   if (NS_WARN_IF(mCurrentlyRunningOp)) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
@@ -25771,20 +25786,25 @@ nsresult Cursor::CursorOpBase::PopulateR
         return rv;
       }
 
       if (cloneInfo.mHasPreprocessInfo) {
         IDB_WARNING("Preprocessing for cursors not yet implemented!");
         return NS_ERROR_NOT_IMPLEMENTED;
       }
 
-      MOZ_ASSERT(aInitializeResponse);
-      mResponse = IndexCursorResponse();
-
-      auto& response = mResponse.get_IndexCursorResponse();
+      if (aInitializeResponse) {
+        mResponse = nsTArray<IndexCursorResponse>();
+      } else {
+        MOZ_ASSERT(mResponse.type() ==
+                   CursorResponse::TArrayOfIndexCursorResponse);
+      }
+
+      auto& responses = mResponse.get_ArrayOfIndexCursorResponse();
+      auto& response = *responses.AppendElement();
       response.cloneInfo().data().data = std::move(cloneInfo.mData);
       response.key() = mCursor->mPosition;
       response.sortKey() = mCursor->mLocaleAwarePosition;
       response.objectKey() = mCursor->mObjectStorePosition;
 
       mFiles.AppendElement(std::move(cloneInfo.mFiles));
       break;
     }
@@ -25814,17 +25834,18 @@ nsresult Cursor::CursorOpBase::PopulateR
   return NS_OK;
 }
 
 nsresult Cursor::CursorOpBase::PopulateExtraResponses(
     mozIStorageStatement* const aStmt, const uint32_t aMaxExtraCount,
     const nsCString& aOperation) {
   AssertIsOnConnectionThread();
 
-  if (mCursor->mType != OpenCursorParams::TObjectStoreOpenCursorParams) {
+  if (mCursor->mType != OpenCursorParams::TObjectStoreOpenCursorParams &&
+      mCursor->mType != OpenCursorParams::TIndexOpenCursorParams) {
     IDB_WARNING(
         "PRELOAD: Not yet implemented. Extra results were queried, but are "
         "discarded for now.");
     return NS_OK;
   }
 
   // For unique cursors, we need to skip records with the same key. The SQL
   // queries currently do not filter these out.
@@ -26238,17 +26259,18 @@ nsresult Cursor::OpenOp::DoIndexDatabase
 
     default:
       MOZ_CRASH("Should never get here!");
   }
 
   // Note: Changing the number or order of SELECT columns in the query will
   // require changes to CursorOpBase::PopulateResponseFromStatement.
   const nsCString firstQuery = queryStart + keyRangeClause + directionClause +
-                               kOpenLimit + NS_LITERAL_CSTRING("1");
+                               kOpenLimit +
+                               ToAutoCString(1 + mCursor->mMaxExtraCount);
 
   DatabaseConnection::CachedStatement stmt;
   nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = stmt->BindInt64ByName(kStmtParamNameId, mCursor->mIndexId);
@@ -26284,17 +26306,22 @@ nsresult Cursor::OpenOp::DoIndexDatabase
     return rv;
   }
 
   // Now we need to make the query to get the next match.
   PrepareIndexKeyConditionClause(sortColumn, directionClause,
                                  NS_LITERAL_CSTRING("index_table."),
                                  std::move(queryStart));
 
-  return NS_OK;
+  // The degree to which extra responses on OpenOp can actually be used depends
+  // on the parameters of subsequent ContinueOp operations, see also comment in
+  // ContinueOp::DoDatabaseWork.
+
+  return PopulateExtraResponses(stmt, mCursor->mMaxExtraCount,
+                                NS_LITERAL_CSTRING("OpenOp"));
 }
 
 nsresult Cursor::OpenOp::DoIndexKeyDatabaseWork(
     DatabaseConnection* aConnection) {
   MOZ_ASSERT(aConnection);
   aConnection->AssertIsOnConnectionThread();
   MOZ_ASSERT(mCursor);
   MOZ_ASSERT(mCursor->mType == OpenCursorParams::TIndexOpenKeyCursorParams);
--- a/dom/indexedDB/IDBCursor.cpp
+++ b/dom/indexedDB/IDBCursor.cpp
@@ -253,16 +253,18 @@ IDBCursorDirection IDBCursor::GetDirecti
     case PREV_UNIQUE:
       return IDBCursorDirection::Prevunique;
 
     default:
       MOZ_CRASH("Bad direction!");
   }
 }
 
+IDBCursor::Type IDBCursor::GetType() const { return mType; }
+
 void IDBCursor::GetSource(OwningIDBObjectStoreOrIDBIndex& aSource) const {
   AssertIsOnOwningThread();
 
   switch (mType) {
     case Type_ObjectStore:
     case Type_ObjectStoreKey:
       MOZ_ASSERT(mSourceObjectStore);
       aSource.SetAsIDBObjectStore() = mSourceObjectStore;
@@ -448,17 +450,18 @@ void IDBCursor::Continue(JSContext* aCx,
         "IDBCursor.continue()", mTransaction->LoggingSerialNumber(),
         requestSerialNumber, IDB_LOG_STRINGIFY(mTransaction->Database()),
         IDB_LOG_STRINGIFY(mTransaction),
         IDB_LOG_STRINGIFY(mSourceIndex->ObjectStore()),
         IDB_LOG_STRINGIFY(mSourceIndex), IDB_LOG_STRINGIFY(mDirection),
         IDB_LOG_STRINGIFY(key));
   }
 
-  mBackgroundActor->SendContinueInternal(ContinueParams(key), mKey);
+  mBackgroundActor->SendContinueInternal(ContinueParams(key), mKey,
+                                         mPrimaryKey);
 
   mContinueCalled = true;
 }
 
 void IDBCursor::ContinuePrimaryKey(JSContext* aCx, JS::Handle<JS::Value> aKey,
                                    JS::Handle<JS::Value> aPrimaryKey,
                                    ErrorResult& aRv) {
   AssertIsOnOwningThread();
@@ -554,17 +557,17 @@ void IDBCursor::ContinuePrimaryKey(JSCon
       "IDBCursor.continuePrimaryKey()", mTransaction->LoggingSerialNumber(),
       requestSerialNumber, IDB_LOG_STRINGIFY(mTransaction->Database()),
       IDB_LOG_STRINGIFY(mTransaction),
       IDB_LOG_STRINGIFY(mSourceIndex->ObjectStore()),
       IDB_LOG_STRINGIFY(mSourceIndex), IDB_LOG_STRINGIFY(mDirection),
       IDB_LOG_STRINGIFY(key), IDB_LOG_STRINGIFY(primaryKey));
 
   mBackgroundActor->SendContinueInternal(
-      ContinuePrimaryKeyParams(key, primaryKey), mKey);
+      ContinuePrimaryKeyParams(key, primaryKey), mKey, mPrimaryKey);
 
   mContinueCalled = true;
 }
 
 void IDBCursor::Advance(uint32_t aCount, ErrorResult& aRv) {
   AssertIsOnOwningThread();
 
   if (!aCount) {
@@ -599,17 +602,18 @@ void IDBCursor::Advance(uint32_t aCount,
         "index(%s).cursor(%s).advance(%ld)",
         "IDBCursor.advance()", mTransaction->LoggingSerialNumber(),
         requestSerialNumber, IDB_LOG_STRINGIFY(mTransaction->Database()),
         IDB_LOG_STRINGIFY(mTransaction),
         IDB_LOG_STRINGIFY(mSourceIndex->ObjectStore()),
         IDB_LOG_STRINGIFY(mSourceIndex), IDB_LOG_STRINGIFY(mDirection), aCount);
   }
 
-  mBackgroundActor->SendContinueInternal(AdvanceParams(aCount), mKey);
+  mBackgroundActor->SendContinueInternal(AdvanceParams(aCount), mKey,
+                                         mPrimaryKey);
 
   mContinueCalled = true;
 }
 
 already_AddRefed<IDBRequest> IDBCursor::Update(JSContext* aCx,
                                                JS::Handle<JS::Value> aValue,
                                                ErrorResult& aRv) {
   AssertIsOnOwningThread();
--- a/dom/indexedDB/IDBCursor.h
+++ b/dom/indexedDB/IDBCursor.h
@@ -28,39 +28,42 @@ class IDBObjectStore;
 class IDBRequest;
 class IDBTransaction;
 class OwningIDBObjectStoreOrIDBIndex;
 
 namespace indexedDB {
 class BackgroundCursorChild;
 }
 
+// TODO: Consider defining different subclasses for the different cursor types,
+// possibly using the CRTP, which would remove the need for various case
+// distinctions.
 class IDBCursor final : public nsISupports, public nsWrapperCache {
  public:
   typedef indexedDB::Key Key;
   typedef indexedDB::StructuredCloneReadInfo StructuredCloneReadInfo;
 
   enum Direction {
     NEXT = 0,
     NEXT_UNIQUE,
     PREV,
     PREV_UNIQUE,
 
     // Only needed for IPC serialization helper, should never be used in code.
     DIRECTION_INVALID
   };
 
- private:
   enum Type {
     Type_ObjectStore,
     Type_ObjectStoreKey,
     Type_Index,
     Type_IndexKey,
   };
 
+ private:
   indexedDB::BackgroundCursorChild* mBackgroundActor;
 
   // TODO: mRequest, mSourceObjectStore and mSourceIndex could be made const if
   // Bug 1575173 is resolved. They are initialized in the constructor and never
   // modified/cleared.
   RefPtr<IDBRequest> mRequest;
   RefPtr<IDBObjectStore> mSourceObjectStore;
   RefPtr<IDBIndex> mSourceIndex;
@@ -69,18 +72,18 @@ class IDBCursor final : public nsISuppor
   IDBTransaction* const mTransaction;
 
   // These are cycle-collected!
   JS::Heap<JS::Value> mCachedKey;
   JS::Heap<JS::Value> mCachedPrimaryKey;
   JS::Heap<JS::Value> mCachedValue;
 
   Key mKey;
-  Key mSortKey;
-  Key mPrimaryKey;
+  Key mSortKey;     ///< AKA locale aware key/position elsewhere
+  Key mPrimaryKey;  ///< AKA object store key/position elsewhere
   StructuredCloneReadInfo mCloneInfo;
 
   const Type mType;
   const Direction mDirection;
 
   bool mHaveCachedKey : 1;
   bool mHaveCachedPrimaryKey : 1;
   bool mHaveCachedValue : 1;
@@ -115,16 +118,18 @@ class IDBCursor final : public nsISuppor
 #endif
 
   nsIGlobalObject* GetParentObject() const;
 
   void GetSource(OwningIDBObjectStoreOrIDBIndex& aSource) const;
 
   IDBCursorDirection GetDirection() const;
 
+  Type GetType() const;
+
   bool IsContinueCalled() const { return mContinueCalled; }
 
   void GetKey(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
               ErrorResult& aRv);
 
   void GetPrimaryKey(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
                      ErrorResult& aRv);
 
@@ -158,32 +163,32 @@ class IDBCursor final : public nsISuppor
   void ClearBackgroundActor() {
     AssertIsOnOwningThread();
 
     mBackgroundActor = nullptr;
   }
 
   void InvalidateCachedResponses();
 
+  // Checks if this is a locale aware cursor (ie. the index's sortKey is unset)
+  bool IsLocaleAware() const;
+
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBCursor)
 
   // nsWrapperCache
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
  private:
   IDBCursor(Type aType, indexedDB::BackgroundCursorChild* aBackgroundActor,
             Key aKey);
 
   ~IDBCursor();
 
-  // Checks if this is a locale aware cursor (ie. the index's sortKey is unset)
-  bool IsLocaleAware() const;
-
   void DropJSObjects();
 
   bool IsSourceDeleted() const;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
--- a/dom/indexedDB/PBackgroundIDBCursor.ipdl
+++ b/dom/indexedDB/PBackgroundIDBCursor.ipdl
@@ -75,28 +75,29 @@ struct IndexKeyCursorResponse
 
 // TODO: All cursor responses must be arrays!
 union CursorResponse
 {
   void_t;
   nsresult;
   ObjectStoreCursorResponse[];
   ObjectStoreKeyCursorResponse;
-  IndexCursorResponse;
+  IndexCursorResponse[];
   IndexKeyCursorResponse;
 };
 
 protocol PBackgroundIDBCursor
 {
   manager PBackgroundIDBTransaction or PBackgroundIDBVersionChangeTransaction;
 
 parent:
   async DeleteMe();
 
-  async Continue(CursorRequestParams params, Key currentKey);
+  async Continue(CursorRequestParams params, Key currentKey,
+                 Key currentObjectStoreKey);
 
 child:
   async __delete__();
 
   async Response(CursorResponse response);
 };
 
 } // namespace indexedDB