Bug 1168606 - Send extra records with every ObjectStoreCursorResponse if enabled by pref. r=ttung,asuth
authorSimon Giesecke <sgiesecke@mozilla.com>
Tue, 05 Nov 2019 14:40:37 +0000
changeset 500655 89817a19f3b38187bca7eecd50cbf39867c4dad8
parent 500654 2f0447b228c762d88198398974e2818c0d3b5dc5
child 500656 c009bbdbbfd5451756cb1f3dc421b82df50911d1
push id114166
push userapavel@mozilla.com
push dateThu, 07 Nov 2019 10:04:01 +0000
treeherdermozilla-inbound@d271c572a9bc [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 - Send extra records with every ObjectStoreCursorResponse if enabled by pref. r=ttung,asuth Differential Revision: https://phabricator.services.mozilla.com/D43252
dom/indexedDB/ActorsChild.cpp
dom/indexedDB/ActorsChild.h
dom/indexedDB/ActorsParent.cpp
dom/indexedDB/IDBCursor.cpp
dom/indexedDB/IDBCursor.h
dom/indexedDB/IDBIndex.cpp
dom/indexedDB/IDBObjectStore.cpp
dom/indexedDB/IDBTransaction.cpp
dom/indexedDB/IDBTransaction.h
dom/indexedDB/PBackgroundIDBCursor.ipdl
--- a/dom/indexedDB/ActorsChild.cpp
+++ b/dom/indexedDB/ActorsChild.cpp
@@ -1269,16 +1269,29 @@ void DispatchFileHandleSuccessEvent(File
 
   MOZ_ASSERT(fileHandle->IsOpen());
 
   fileRequest->SetResultCallback(aResultHelper);
 
   MOZ_ASSERT(fileHandle->IsOpen() || fileHandle->IsAborted());
 }
 
+auto GetKeyOperator(const IDBCursor::Direction aDirection) {
+  switch (aDirection) {
+    case IDBCursor::NEXT:
+    case IDBCursor::NEXT_UNIQUE:
+      return &Key::operator>=;
+    case IDBCursor::PREV:
+    case IDBCursor::PREV_UNIQUE:
+      return &Key::operator<=;
+    default:
+      MOZ_CRASH("Should never get here.");
+  }
+}
+
 }  // namespace
 
 /*******************************************************************************
  * Actor class declarations
  ******************************************************************************/
 
 // CancelableRunnable is used to make workers happy.
 class BackgroundRequestChild::PreprocessHelper final
@@ -2827,17 +2840,17 @@ void BackgroundRequestChild::ActorDestro
 
       preprocessHelper = nullptr;
     }
   }
 
   if (mTransaction) {
     mTransaction->AssertIsOnOwningThread();
 
-    mTransaction->OnRequestFinished(/* aActorDestroyedNormally */
+    mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */
                                     aWhy == Deletion);
 #ifdef DEBUG
     mTransaction = nullptr;
 #endif
   }
 }
 
 mozilla::ipc::IPCResult BackgroundRequestChild::Recv__delete__(
@@ -2914,17 +2927,17 @@ mozilla::ipc::IPCResult BackgroundReques
         HandleResponse(aResponse.get_IndexCountResponse().count());
         break;
 
       default:
         MOZ_CRASH("Unknown response type!");
     }
   }
 
-  mTransaction->OnRequestFinished(/* aActorDestroyedNormally */ true);
+  mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ true);
 
   // Null this out so that we don't try to call OnRequestFinished() again in
   // ActorDestroy.
   mTransaction = nullptr;
 
   return IPC_OK();
 }
 
@@ -3283,18 +3296,141 @@ void BackgroundCursorChild::SendContinue
   // Make sure all our DOM objects stay alive.
   mStrongCursor = mCursor;
 
   MOZ_ASSERT(mRequest->ReadyState() == IDBRequestReadyState::Done);
   mRequest->Reset();
 
   mTransaction->OnNewRequest();
 
-  MOZ_ALWAYS_TRUE(
-      PBackgroundIDBCursorChild::SendContinue(aParams, aCurrentKey));
+  CursorRequestParams params = aParams;
+  Key currentKey = aCurrentKey;
+
+  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()) {
+        const auto& keyOperator = GetKeyOperator(mDirection);
+        if ((mCachedResponses.front().mKey.*keyOperator)(key)) {
+          IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
+              "PRELOAD: Continue to key %s, keeping cached key %s and further",
+              "Continue/keep", mTransaction->LoggingSerialNumber(),
+              mRequest->LoggingSerialNumber(), key.GetBuffer().get(),
+              mCachedResponses.front().mKey.GetBuffer().get());
+          break;
+        }
+        IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
+            "PRELOAD: Continue to key %s, discarding cached key %s",
+            "Continue/discard", mTransaction->LoggingSerialNumber(),
+            mRequest->LoggingSerialNumber(), key.GetBuffer().get(),
+            mCachedResponses.front().mKey.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,
+          mCachedResponses.size());
+      break;
+    }
+
+    case CursorRequestParams::TContinuePrimaryKeyParams:
+      // TODO: Implement preloading for this case
+      InvalidateCachedResponses();
+      break;
+
+    case CursorRequestParams::TAdvanceParams: {
+      uint32_t& advanceCount = params.get_AdvanceParams().count();
+      IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
+          "PRELOAD: Advancing %" PRIu32 " records", "Advancing",
+          mTransaction->LoggingSerialNumber(), mRequest->LoggingSerialNumber(),
+          advanceCount);
+
+      // 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;
+        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,
+          mCachedResponses.size());
+      break;
+    }
+
+    default:
+      MOZ_CRASH("Should never get here!");
+  }
+
+  if (!mCachedResponses.empty()) {
+    // We need to remove the response here from mCachedResponses, since when
+    // requests are interleaved, other events may be processed before
+    // CompleteContinueRequestFromCache, which may modify mCachedResponses.
+    mDelayedResponses.emplace_back(std::move(mCachedResponses.front()));
+    mCachedResponses.pop_front();
+
+    // We cannot send the response right away, as we must preserve the request
+    // order. Dispatching a DelayedActionRunnable only partially addresses this.
+    // This is accompanied by invalidating cached entries at proper locations to
+    // make it correct. To avoid this, further changes are necessary, see Bug
+    // 1580499.
+    nsCOMPtr<nsIRunnable> continueRunnable = new DelayedActionRunnable(
+        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));
+  }
+}
+
+void BackgroundCursorChild::CompleteContinueRequestFromCache() {
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mTransaction);
+  MOZ_ASSERT(mCursor);
+  MOZ_ASSERT(mStrongCursor);
+  MOZ_ASSERT(!mDelayedResponses.empty());
+
+  RefPtr<IDBCursor> cursor;
+  mStrongCursor.swap(cursor);
+
+  auto& item = mDelayedResponses.front();
+  mCursor->Reset(std::move(item.mKey), std::move(item.mCloneInfo));
+  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());
+
+  ResultHelper helper(mRequest, mTransaction, mCursor);
+  DispatchSuccessEvent(&helper);
+
+  mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ true);
 }
 
 void BackgroundCursorChild::SendDeleteMeInternal() {
   AssertIsOnOwningThread();
   MOZ_ASSERT(!mStrongRequest);
   MOZ_ASSERT(!mStrongCursor);
 
   mRequest = nullptr;
@@ -3305,16 +3441,35 @@ void BackgroundCursorChild::SendDeleteMe
   if (mCursor) {
     mCursor->ClearBackgroundActor();
     mCursor = nullptr;
 
     MOZ_ALWAYS_TRUE(PBackgroundIDBCursorChild::SendDeleteMe());
   }
 }
 
+void BackgroundCursorChild::InvalidateCachedResponses() {
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mTransaction);
+  MOZ_ASSERT(mRequest);
+
+  // TODO: With more information on the reason for the invalidation, we might
+  // only selectively invalidate cached responses. If the reason is an updated
+  // value, we do not need to care for key-only cursors. If the key of the
+  // changed entry is not in the remaining range of the cursor, we also do not
+  // need to care, etc.
+
+  IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
+      "PRELOAD: Invalidating all %zu cached responses", "Invalidating",
+      mTransaction->LoggingSerialNumber(), mRequest->LoggingSerialNumber(),
+      mCachedResponses.size());
+
+  mCachedResponses.clear();
+}
+
 void BackgroundCursorChild::HandleResponse(nsresult aResponse) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(NS_FAILED(aResponse));
   MOZ_ASSERT(NS_ERROR_GET_MODULE(aResponse) == NS_ERROR_MODULE_DOM_INDEXEDDB);
   MOZ_ASSERT(mRequest);
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(!mStrongRequest);
   MOZ_ASSERT(!mStrongCursor);
@@ -3348,34 +3503,51 @@ void BackgroundCursorChild::HandleRespon
     const nsTArray<ObjectStoreCursorResponse>& aResponses) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mRequest);
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(mObjectStore);
   MOZ_ASSERT(!mStrongRequest);
   MOZ_ASSERT(!mStrongCursor);
 
-  MOZ_ASSERT(aResponses.Length() == 1);
+  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) {
-      mCursor->Reset(std::move(response.key()), std::move(cloneReadInfo));
+      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;
     }
   }
 
   ResultHelper helper(mRequest, mTransaction, mCursor);
@@ -3427,16 +3599,20 @@ void BackgroundCursorChild::HandleRespon
       /* 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);
@@ -3473,17 +3649,17 @@ void BackgroundCursorChild::HandleRespon
 void BackgroundCursorChild::ActorDestroy(ActorDestroyReason aWhy) {
   AssertIsOnOwningThread();
   MOZ_ASSERT_IF(aWhy == Deletion, !mStrongRequest);
   MOZ_ASSERT_IF(aWhy == Deletion, !mStrongCursor);
 
   MaybeCollectGarbageOnIPCMessage();
 
   if (mStrongRequest && !mStrongCursor && mTransaction) {
-    mTransaction->OnRequestFinished(/* aActorDestroyedNormally */
+    mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */
                                     aWhy == Deletion);
   }
 
   if (mCursor) {
     mCursor->ClearBackgroundActor();
 #ifdef DEBUG
     mCursor = nullptr;
 #endif
@@ -3540,17 +3716,17 @@ mozilla::ipc::IPCResult BackgroundCursor
     case CursorResponse::TIndexKeyCursorResponse:
       HandleResponse(aResponse.get_IndexKeyCursorResponse());
       break;
 
     default:
       MOZ_CRASH("Should never get here!");
   }
 
-  transaction->OnRequestFinished(/* aActorDestroyedNormally */ true);
+  transaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ true);
 
   return IPC_OK();
 }
 
 NS_IMETHODIMP
 BackgroundCursorChild::DelayedActionRunnable::Run() {
   MOZ_ASSERT(mActor);
   mActor->AssertIsOnOwningThread();
--- a/dom/indexedDB/ActorsChild.h
+++ b/dom/indexedDB/ActorsChild.h
@@ -619,46 +619,63 @@ class BackgroundRequestChild final : pub
 };
 
 class BackgroundCursorChild final : public PBackgroundIDBCursorChild {
   friend class BackgroundTransactionChild;
   friend class BackgroundVersionChangeTransactionChild;
 
   class DelayedActionRunnable;
 
+  struct CachedResponse {
+    CachedResponse() = default;
+
+    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
+    StructuredCloneReadInfo mCloneInfo;
+  };
+
   IDBRequest* mRequest;
   IDBTransaction* mTransaction;
   IDBObjectStore* mObjectStore;
   IDBIndex* mIndex;
   IDBCursor* mCursor;
 
   // These are only set while a request is in progress.
   RefPtr<IDBRequest> mStrongRequest;
   RefPtr<IDBCursor> mStrongCursor;
 
   Direction mDirection;
 
   NS_DECL_OWNINGTHREAD
 
+  std::deque<CachedResponse> mCachedResponses, mDelayedResponses;
+
  public:
   BackgroundCursorChild(IDBRequest* aRequest, IDBObjectStore* aObjectStore,
                         Direction aDirection);
 
   BackgroundCursorChild(IDBRequest* aRequest, IDBIndex* aIndex,
                         Direction aDirection);
 
   void AssertIsOnOwningThread() const {
     NS_ASSERT_OWNINGTHREAD(BackgroundCursorChild);
   }
 
   void SendContinueInternal(const CursorRequestParams& aParams,
                             const Key& aCurrentKey);
 
   void SendDeleteMeInternal();
 
+  void InvalidateCachedResponses();
+
   IDBRequest* GetRequest() const {
     AssertIsOnOwningThread();
 
     return mRequest;
   }
 
   IDBObjectStore* GetObjectStore() const {
     AssertIsOnOwningThread();
@@ -678,16 +695,18 @@ class BackgroundCursorChild final : publ
     return mDirection;
   }
 
  private:
   // Only destroyed by BackgroundTransactionChild or
   // BackgroundVersionChangeTransactionChild.
   ~BackgroundCursorChild();
 
+  void CompleteContinueRequestFromCache();
+
   void HandleResponse(nsresult aResponse);
 
   void HandleResponse(const void_t& aResponse);
 
   void HandleResponse(const nsTArray<ObjectStoreCursorResponse>& aResponse);
 
   void HandleResponse(const ObjectStoreKeyCursorResponse& aResponse);
 
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -5562,21 +5562,23 @@ class TransactionDatabaseOperationBase :
     DatabaseWork,
     SendingPreprocess,
     WaitingForContinue,
     SendingResults,
     Completed
   };
 
   RefPtr<TransactionBase> mTransaction;
-  const int64_t mTransactionLoggingSerialNumber;
   InternalState mInternalState;
   bool mWaitingForContinue;
   const bool mTransactionIsAborted;
 
+ protected:
+  const int64_t mTransactionLoggingSerialNumber;
+
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
  protected:
   // A check only enables when the diagnostic assert turns on. It assumes the
   // mUpdateRefcountFunction is a nullptr because the previous
   // StartTransactionOp failed on the connection thread and the next write
   // operation (e.g. ObjectstoreAddOrPutRequestOp) doesn't have enough time to
   // catch up the failure information.
   bool mAssumingPreviousOperationFail;
@@ -7630,16 +7632,18 @@ class Cursor final : public PBackgroundI
   Key mLocaleAwarePosition;  ///< If mLocale is set, this is mPosition converted
                              ///< to mLocale. Otherwise, it is unset.
 
   CursorOpBase* mCurrentlyRunningOp;
 
   const Type mType;
   const Direction mDirection;
 
+  const int32_t mMaxExtraCount;
+
   const bool mUniqueIndex;
   const bool mIsSameProcessActor;
   bool mActorDestroyed;
 
   struct ConstructFromTransactionBase {};
 
  public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Cursor)
@@ -7704,18 +7708,22 @@ class Cursor::CursorOpBase : public Tran
   }
 
   ~CursorOpBase() override = default;
 
   bool SendFailureResult(nsresult aResultCode) override;
 
   void Cleanup() override;
 
-  nsresult PopulateResponseFromStatement(
-      DatabaseConnection::CachedStatement& aStmt, bool aInitializeResponse);
+  nsresult PopulateResponseFromStatement(mozIStorageStatement* aStmt,
+                                         bool aInitializeResponse);
+
+  nsresult PopulateExtraResponses(mozIStorageStatement* aStmt,
+                                  uint32_t aMaxExtraCount,
+                                  const nsCString& aOperation);
 };
 
 class Cursor::OpenOp final : public Cursor::CursorOpBase {
   friend class Cursor;
 
   const Maybe<SerializedKeyRange> mOptionalKeyRange;
 
  private:
@@ -7751,19 +7759,19 @@ class Cursor::OpenOp final : public Curs
 
 class Cursor::ContinueOp final : public Cursor::CursorOpBase {
   friend class Cursor;
 
   const CursorRequestParams mParams;
 
  private:
   // Only created by Cursor.
-  ContinueOp(Cursor* aCursor, const CursorRequestParams& aParams)
-      : CursorOpBase(aCursor), mParams(aParams) {
-    MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None);
+  ContinueOp(Cursor* aCursor, CursorRequestParams aParams)
+      : CursorOpBase(aCursor), mParams(std::move(aParams)) {
+    MOZ_ASSERT(mParams.type() != CursorRequestParams::T__None);
   }
 
   // Reference counted.
   ~ContinueOp() override = default;
 
   nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
 
   nsresult SendSuccessResult() override;
@@ -9302,16 +9310,25 @@ nsAutoCString MakeColumnPairSelectionLis
 constexpr bool IsIncreasingOrder(const IDBCursor::Direction aDirection) {
   MOZ_ASSERT(
       aDirection == IDBCursor::NEXT || aDirection == IDBCursor::NEXT_UNIQUE ||
       aDirection == IDBCursor::PREV || aDirection == IDBCursor::PREV_UNIQUE);
 
   return aDirection == IDBCursor::NEXT || aDirection == IDBCursor::NEXT_UNIQUE;
 }
 
+constexpr bool IsUnique(const IDBCursor::Direction aDirection) {
+  MOZ_ASSERT(
+      aDirection == IDBCursor::NEXT || aDirection == IDBCursor::NEXT_UNIQUE ||
+      aDirection == IDBCursor::PREV || aDirection == IDBCursor::PREV_UNIQUE);
+
+  return aDirection == IDBCursor::NEXT_UNIQUE ||
+         aDirection == IDBCursor::PREV_UNIQUE;
+}
+
 constexpr bool IsKeyCursor(const Cursor::Type aType) {
   return aType == OpenCursorParams::TObjectStoreOpenKeyCursorParams ||
          aType == OpenCursorParams::TIndexOpenKeyCursorParams;
 }
 
 // TODO: In principle, this could be constexpr, if operator+(nsLiteralCString,
 // nsLiteralCString) were constexpr and returned a literal type.
 nsAutoCString MakeDirectionClause(const IDBCursor::Direction aDirection) {
@@ -15007,16 +15024,17 @@ Cursor::Cursor(RefPtr<TransactionBase> a
       mIndexMetadata(std::move(aIndexMetadata)),
       mObjectStoreId(mObjectStoreMetadata->mCommonMetadata.id()),
       mIndexId(mIndexMetadata ? mIndexMetadata->mCommonMetadata.id() : 0),
       mLocale(mIndexMetadata ? mIndexMetadata->mCommonMetadata.locale()
                              : EmptyCString()),
       mCurrentlyRunningOp(nullptr),
       mType(aType),
       mDirection(aDirection),
+      mMaxExtraCount(IndexedDatabaseManager::MaxPreloadExtraRecords()),
       mUniqueIndex(mIndexMetadata ? mIndexMetadata->mCommonMetadata.unique()
                                   : false),
       mIsSameProcessActor(!BackgroundParent::IsOtherProcessActor(
           mTransaction->GetBackgroundParent())),
       mActorDestroyed(false) {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(mType != OpenCursorParams::T__None);
@@ -15298,19 +15316,33 @@ mozilla::ipc::IPCResult Cursor::RecvCont
 #ifdef DEBUG
       // Always verify parameters in DEBUG builds!
       false
 #else
       mIsSameProcessActor
 #endif
       ;
 
-  // TODO: For now, the child always passes an empty key. This will be changed
-  // in a subsequent patch for Bug 1168606.
-  MOZ_ASSERT(aCurrentKey.IsUnset());
+  // At the time of writing, the child always passes its current 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 =
+          LocalizeKey(aCurrentKey, mLocale, &mLocaleAwarePosition);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        ASSERT_UNLESS_FUZZING();
+        return IPC_FAIL_NO_REASON(this);
+      }
+    }
+    mPosition = aCurrentKey;
+  }
 
   if (!trustParams && !VerifyRequestParams(aParams)) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
   }
 
   if (NS_WARN_IF(mCurrentlyRunningOp)) {
     ASSERT_UNLESS_FUZZING();
@@ -21835,42 +21867,42 @@ nsresult DeleteDatabaseOp::VersionChange
   return NS_OK;
 }
 
 TransactionDatabaseOperationBase::TransactionDatabaseOperationBase(
     TransactionBase* aTransaction)
     : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(),
                             aTransaction->GetLoggingInfo()->NextRequestSN()),
       mTransaction(aTransaction),
-      mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber()),
       mInternalState(InternalState::Initial),
       mWaitingForContinue(false),
+      mTransactionIsAborted(aTransaction->IsAborted()),
+      mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber())
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
-      mTransactionIsAborted(aTransaction->IsAborted()),
-      mAssumingPreviousOperationFail(false) {
-#else
-      mTransactionIsAborted(aTransaction->IsAborted()) {
-#endif
+      ,
+      mAssumingPreviousOperationFail(false)
+#endif
+{
   MOZ_ASSERT(aTransaction);
   MOZ_ASSERT(LoggingSerialNumber());
 }
 
 TransactionDatabaseOperationBase::TransactionDatabaseOperationBase(
     TransactionBase* aTransaction, uint64_t aLoggingSerialNumber)
     : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(),
                             aLoggingSerialNumber),
       mTransaction(aTransaction),
-      mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber()),
       mInternalState(InternalState::Initial),
-#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
       mTransactionIsAborted(aTransaction->IsAborted()),
-      mAssumingPreviousOperationFail(false) {
-#else
-      mTransactionIsAborted(aTransaction->IsAborted()) {
-#endif
+      mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber())
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+      ,
+      mAssumingPreviousOperationFail(false)
+#endif
+{
   MOZ_ASSERT(aTransaction);
 }
 
 TransactionDatabaseOperationBase::~TransactionDatabaseOperationBase() {
   MOZ_ASSERT(mInternalState == InternalState::Completed);
   MOZ_ASSERT(!mTransaction,
              "TransactionDatabaseOperationBase::Cleanup() was not called by a "
              "subclass!");
@@ -25661,27 +25693,33 @@ void Cursor::CursorOpBase::Cleanup() {
   // assertions happy.
   NoteActorDestroyed();
 #endif
 
   TransactionDatabaseOperationBase::Cleanup();
 }
 
 nsresult Cursor::CursorOpBase::PopulateResponseFromStatement(
-    DatabaseConnection::CachedStatement& aStmt,
-    const bool aInitializeResponse) {
+    mozIStorageStatement* const aStmt, const bool aInitializeResponse) {
   Transaction()->AssertIsOnConnectionThread();
-  MOZ_ASSERT(mResponse.type() == CursorResponse::T__None);
+  MOZ_ASSERT(aInitializeResponse ==
+             (mResponse.type() == CursorResponse::T__None));
   MOZ_ASSERT_IF(mFiles.IsEmpty(), aInitializeResponse);
 
   nsresult rv = mCursor->mPosition.SetFromStatement(aStmt, 0);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
+  IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST(
+      "PRELOAD: Populating response with key %s", "Populating",
+      IDB_LOG_ID_STRING(mBackgroundChildLoggingId),
+      mTransactionLoggingSerialNumber, mLoggingSerialNumber,
+      mCursor->mPosition.GetBuffer().get());
+
   switch (mCursor->mType) {
     case OpenCursorParams::TObjectStoreOpenCursorParams: {
       StructuredCloneReadInfo cloneInfo(
           JS::StructuredCloneScope::DifferentProcess);
       rv = GetStructuredCloneReadInfoFromStatement(
           aStmt, 2, 1, mCursor->mFileManager, &cloneInfo);
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
@@ -25771,16 +25809,87 @@ nsresult Cursor::CursorOpBase::PopulateR
 
     default:
       MOZ_CRASH("Should never get here!");
   }
 
   return NS_OK;
 }
 
+nsresult Cursor::CursorOpBase::PopulateExtraResponses(
+    mozIStorageStatement* const aStmt, const uint32_t aMaxExtraCount,
+    const nsCString& aOperation) {
+  AssertIsOnConnectionThread();
+
+  if (mCursor->mType != OpenCursorParams::TObjectStoreOpenCursorParams) {
+    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.
+  Key previousKey =
+      IsUnique(mCursor->mDirection)
+          ? (mCursor->IsLocaleAware() ? mCursor->mLocaleAwarePosition
+                                      : mCursor->mPosition)
+          : Key{};
+
+  uint32_t extraCount = 0;
+  do {
+    bool hasResult;
+    nsresult rv = aStmt->ExecuteStep(&hasResult);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      // In case of a failure on one step, do not attempt to execute further
+      // steps, but use the results already populated.
+
+      break;
+    }
+
+    if (!hasResult) {
+      // TODO: For the first result, in case the cursor reaches the end
+      // prematurely, mCursor's key members are unset. Should we do this here as
+      // well?
+
+      break;
+    }
+
+    if (IsUnique(mCursor->mDirection)) {
+      const auto& currentKey = mCursor->IsLocaleAware()
+                                   ? mCursor->mLocaleAwarePosition
+                                   : mCursor->mPosition;
+      const bool sameKey = previousKey == currentKey;
+      previousKey = currentKey;
+      if (sameKey) {
+        continue;
+      }
+    }
+
+    // TODO: Similar to the call to ExecuteStep above, it would be better not to
+    // fail here. However, this would require the equivalent of the strong
+    // exception guarantee of PopulateResponseFromStatement, which it does not
+    // provide right now.
+    rv = PopulateResponseFromStatement(aStmt, false);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    ++extraCount;
+  } while (true);
+
+  IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST(
+      "PRELOAD: %s: Number of extra results populated: %" PRIu32 "/%" PRIu32,
+      "Populated", IDB_LOG_ID_STRING(mBackgroundChildLoggingId),
+      mTransactionLoggingSerialNumber, mLoggingSerialNumber, aOperation.get(),
+      extraCount, aMaxExtraCount);
+
+  return NS_OK;
+}
+
 void Cursor::SetOptionalKeyRange(
     const Maybe<SerializedKeyRange>& aOptionalKeyRange, bool* const aOpen) {
   MOZ_ASSERT(mLocaleAwareRangeBound.IsUnset());
   MOZ_ASSERT(aOpen);
 
   if (aOptionalKeyRange.isSome()) {
     const SerializedKeyRange& range = aOptionalKeyRange.ref();
 
@@ -25934,17 +26043,18 @@ nsresult Cursor::OpenOp::DoObjectStoreDa
   const auto keyRangeClause =
       MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameKey);
 
   const auto& directionClause = MakeDirectionClause(mCursor->mDirection);
 
   // 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->mObjectStoreId);
@@ -25970,20 +26080,31 @@ nsresult Cursor::OpenOp::DoObjectStoreDa
     return NS_OK;
   }
 
   rv = PopulateResponseFromStatement(stmt, true);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  // Now we need to make the query to get the next match.
+  // Now we need to make the query for ContinueOp.
   PrepareKeyConditionClauses(kStmtParamNameKey, directionClause, 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.
+  //
+  // TODO: We should somehow evaluate the effects of this. Maybe use a smaller
+  // extra count that for ContinueOp?
+  //
+  // TODO: If this is done here, do this in the other Do*DatabaseWork functions
+  // as well (or move this to DoDatabaseWork).
+
+  return PopulateExtraResponses(stmt, mCursor->mMaxExtraCount,
+                                NS_LITERAL_CSTRING("OpenOp"));
 }
 
 nsresult Cursor::OpenOp::DoObjectStoreKeyDatabaseWork(
     DatabaseConnection* aConnection) {
   MOZ_ASSERT(aConnection);
   aConnection->AssertIsOnConnectionThread();
   MOZ_ASSERT(mCursor);
   MOZ_ASSERT(mCursor->mType ==
@@ -26366,77 +26487,115 @@ nsresult Cursor::ContinueOp::DoDatabaseW
                             mCursor->mDirection == IDBCursor::PREV),
                 !mCursor->mContinuePrimaryKeyQuery.IsEmpty());
   MOZ_ASSERT_IF(isIndex, mCursor->mIndexId);
   MOZ_ASSERT_IF(isIndex, !mCursor->mObjectStorePosition.IsUnset());
 
   AUTO_PROFILER_LABEL("Cursor::ContinueOp::DoDatabaseWork", DOM);
 
   // We need to pick a query based on whether or not a key was passed to the
-  // continue function. If not we'll grab the the next item in the database that
+  // continue function. If not we'll grab the next item in the database that
   // is greater than (or less than, if we're running a PREV cursor) the current
   // key. If a key was passed we'll grab the next item in the database that is
   // greater than (or less than, if we're running a PREV cursor) or equal to the
   // key that was specified.
-
+  //
+  // TODO: The description above is not complete, it does not take account of
+  // ContinuePrimaryKey nor Advance.
+  //
   // Note: Changing the number or order of SELECT columns in the query will
   // require changes to CursorOpBase::PopulateResponseFromStatement.
+
+  const uint32_t advanceCount =
+      mParams.type() == CursorRequestParams::TAdvanceParams
+          ? mParams.get_AdvanceParams().count()
+          : 1;
+  MOZ_ASSERT(advanceCount > 0);
+
   bool hasContinueKey = false;
   bool hasContinuePrimaryKey = false;
-  uint32_t advanceCount = 1;
+  // TODO: the name 'currentKey' for this variable is confusing, as this is only
+  // the current key if !hasContinueKey. It is however, always a bound for the
+  // key/position in the operation's result. Maybe rename to
+  // targetKey/targetPosition (which is also not exact, as it might imply that
+  // the result always has this key).
   Key& currentKey = mCursor->IsLocaleAware() ? mCursor->mLocaleAwarePosition
                                              : mCursor->mPosition;
 
+  IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST(
+      "PRELOAD: ContinueOp: currentKey == %s", "currentKey",
+      IDB_LOG_ID_STRING(mBackgroundChildLoggingId),
+      mTransactionLoggingSerialNumber, mLoggingSerialNumber,
+      currentKey.GetBuffer().get());
+
   switch (mParams.type()) {
     case CursorRequestParams::TContinueParams:
       if (!mParams.get_ContinueParams().key().IsUnset()) {
         hasContinueKey = true;
+        // TODO: This writes either to mLocaleAwarePosition or to mPosition, is
+        // that correct? Probably it's ok, because
+        // PopulateResponseFromStatement, contrary to its name, also updates the
+        // fields of mCursor.
         currentKey = mParams.get_ContinueParams().key();
       }
       break;
     case CursorRequestParams::TContinuePrimaryKeyParams:
       MOZ_ASSERT(!mParams.get_ContinuePrimaryKeyParams().key().IsUnset());
       MOZ_ASSERT(
           !mParams.get_ContinuePrimaryKeyParams().primaryKey().IsUnset());
       MOZ_ASSERT(mCursor->mDirection == IDBCursor::NEXT ||
                  mCursor->mDirection == IDBCursor::PREV);
       hasContinueKey = true;
       hasContinuePrimaryKey = true;
       currentKey = mParams.get_ContinuePrimaryKeyParams().key();
       break;
     case CursorRequestParams::TAdvanceParams:
-      advanceCount = mParams.get_AdvanceParams().count();
       break;
     default:
       MOZ_CRASH("Should never get here!");
   }
 
   const nsCString& continueQuery =
       hasContinuePrimaryKey ? mCursor->mContinuePrimaryKeyQuery
                             : hasContinueKey ? mCursor->mContinueToQuery
                                              : mCursor->mContinueQuery;
 
-  MOZ_ASSERT(advanceCount > 0);
-  nsAutoCString countString;
-  countString.AppendInt(advanceCount);
+  // TODO: Whether it makes sense to preload depends on the kind of the
+  // subsequent operations, not of the current operation. We could assume that
+  // the subsequent operations are:
+  // - the same as the current operation (with the same parameter values)
+  // - as above, except for Advance, where we assume the count will be 1 on the
+  // next call
+  // - basic operations (Advance with count 1 or Continue-without-key)
+  //
+  // For now, we implement the second option for now (which correspond to
+  // !hasContinueKey).
+  //
+  // Based on that, we could in both cases either preload for any assumed
+  // subsequent operations, or only for the basic operations. For now, we
+  // preload only for an assumed basic operation. Other operations would require
+  // more work on the client side for invalidation, and may not make any sense
+  // at all.
+  const uint32_t maxExtraCount = hasContinueKey ? 0 : mCursor->mMaxExtraCount;
 
   DatabaseConnection::CachedStatement stmt;
   nsresult rv = aConnection->GetCachedStatement(continueQuery, &stmt);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   // Bind limit.
-  rv = stmt->BindUTF8StringByName(kStmtParamNameLimit,
-                                  ToAutoCString(advanceCount));
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-
-  int64_t id = isIndex ? mCursor->mIndexId : mCursor->mObjectStoreId;
+  rv = stmt->BindUTF8StringByName(
+      kStmtParamNameLimit,
+      ToAutoCString(advanceCount + mCursor->mMaxExtraCount));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  const int64_t id = isIndex ? mCursor->mIndexId : mCursor->mObjectStoreId;
 
   rv = stmt->BindInt64ByName(kStmtParamNameId, id);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   // Bind current key.
   rv = currentKey.BindToStatement(stmt, kStmtParamNameCurrentKey);
@@ -26458,29 +26617,28 @@ nsresult Cursor::ContinueOp::DoDatabaseW
   if (isIndex && !hasContinueKey &&
       (mCursor->mDirection == IDBCursor::NEXT ||
        mCursor->mDirection == IDBCursor::PREV)) {
     rv = mCursor->mObjectStorePosition.BindToStatement(
         stmt, kStmtParamNameObjectStorePosition);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
-  }
-
-  // Bind object store position if primaryKey is specified.
-  if (hasContinuePrimaryKey) {
+  } else if (hasContinuePrimaryKey) {
     rv = mParams.get_ContinuePrimaryKeyParams().primaryKey().BindToStatement(
         stmt, kStmtParamNameObjectStorePosition);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
-  bool hasResult;
+  // TODO: Why do we query the records we don't need and skip them here, rather
+  // than using a OFFSET clause in the query?
   for (uint32_t index = 0; index < advanceCount; index++) {
+    bool hasResult;
     rv = stmt->ExecuteStep(&hasResult);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     if (!hasResult) {
       mCursor->mPosition.Unset();
       mCursor->mLocaleAwarePosition.Unset();
@@ -26491,17 +26649,18 @@ nsresult Cursor::ContinueOp::DoDatabaseW
     }
   }
 
   rv = PopulateResponseFromStatement(stmt, true);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  return NS_OK;
+  return PopulateExtraResponses(stmt, maxExtraCount,
+                                NS_LITERAL_CSTRING("ContinueOp"));
 }
 
 nsresult Cursor::ContinueOp::SendSuccessResult() {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mCursor);
   MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this);
   MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t,
                 mCursor->mPosition.IsUnset());
--- a/dom/indexedDB/IDBCursor.cpp
+++ b/dom/indexedDB/IDBCursor.cpp
@@ -49,25 +49,29 @@ IDBCursor::IDBCursor(Type aType, Backgro
   MOZ_ASSERT(aBackgroundActor);
   aBackgroundActor->AssertIsOnOwningThread();
   MOZ_ASSERT(mRequest);
   MOZ_ASSERT_IF(aType == Type_ObjectStore || aType == Type_ObjectStoreKey,
                 mSourceObjectStore);
   MOZ_ASSERT_IF(aType == Type_Index || aType == Type_IndexKey, mSourceIndex);
   MOZ_ASSERT(mTransaction);
   MOZ_ASSERT(!aKey.IsUnset());
+
+  mTransaction->RegisterCursor(this);
 }
 
 bool IDBCursor::IsLocaleAware() const {
   return mSourceIndex && !mSourceIndex->Locale().IsEmpty();
 }
 
 IDBCursor::~IDBCursor() {
   AssertIsOnOwningThread();
 
+  mTransaction->UnregisterCursor(this);
+
   DropJSObjects();
 
   if (mBackgroundActor) {
     mBackgroundActor->SendDeleteMeInternal();
     MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!");
   }
 }
 
@@ -444,17 +448,17 @@ 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), Key());
+  mBackgroundActor->SendContinueInternal(ContinueParams(key), mKey);
 
   mContinueCalled = true;
 }
 
 void IDBCursor::ContinuePrimaryKey(JSContext* aCx, JS::Handle<JS::Value> aKey,
                                    JS::Handle<JS::Value> aPrimaryKey,
                                    ErrorResult& aRv) {
   AssertIsOnOwningThread();
@@ -550,17 +554,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), Key());
+      ContinuePrimaryKeyParams(key, primaryKey), mKey);
 
   mContinueCalled = true;
 }
 
 void IDBCursor::Advance(uint32_t aCount, ErrorResult& aRv) {
   AssertIsOnOwningThread();
 
   if (!aCount) {
@@ -595,17 +599,17 @@ 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), Key());
+  mBackgroundActor->SendContinueInternal(AdvanceParams(aCount), mKey);
 
   mContinueCalled = true;
 }
 
 already_AddRefed<IDBRequest> IDBCursor::Update(JSContext* aCx,
                                                JS::Handle<JS::Value> aValue,
                                                ErrorResult& aRv) {
   AssertIsOnOwningThread();
@@ -626,16 +630,18 @@ already_AddRefed<IDBRequest> IDBCursor::
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR);
     return nullptr;
   }
 
   MOZ_ASSERT(mType == Type_ObjectStore || mType == Type_Index);
   MOZ_ASSERT(!mKey.IsUnset());
   MOZ_ASSERT_IF(mType == Type_Index, !mPrimaryKey.IsUnset());
 
+  mTransaction->InvalidateCursorCaches();
+
   IDBObjectStore* objectStore;
   if (mType == Type_ObjectStore) {
     objectStore = mSourceObjectStore;
   } else {
     objectStore = mSourceIndex->ObjectStore();
   }
 
   MOZ_ASSERT(objectStore);
@@ -734,16 +740,18 @@ already_AddRefed<IDBRequest> IDBCursor::
       mType == Type_IndexKey || mContinueCalled) {
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR);
     return nullptr;
   }
 
   MOZ_ASSERT(mType == Type_ObjectStore || mType == Type_Index);
   MOZ_ASSERT(!mKey.IsUnset());
 
+  mTransaction->InvalidateCursorCaches();
+
   IDBObjectStore* const objectStore = mType == Type_ObjectStore
                                           ? mSourceObjectStore.get()
                                           : mSourceIndex->ObjectStore();
 
   MOZ_ASSERT(objectStore);
 
   const Key& primaryKey = (mType == Type_ObjectStore) ? mKey : mPrimaryKey;
 
@@ -832,16 +840,25 @@ void IDBCursor::Reset(Key&& aKey, Key&& 
 
   mKey = std::move(aKey);
   mSortKey = std::move(aSortKey);
   mPrimaryKey = std::move(aPrimaryKey);
 
   mHaveValue = !mKey.IsUnset();
 }
 
+void IDBCursor::InvalidateCachedResponses() {
+  AssertIsOnOwningThread();
+
+  // TODO: In what case would mBackgroundActor be nullptr?
+  if (mBackgroundActor) {
+    mBackgroundActor->InvalidateCachedResponses();
+  }
+}
+
 NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBCursor)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBCursor)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBCursor)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
--- a/dom/indexedDB/IDBCursor.h
+++ b/dom/indexedDB/IDBCursor.h
@@ -115,16 +115,18 @@ class IDBCursor final : public nsISuppor
 #endif
 
   nsIGlobalObject* GetParentObject() const;
 
   void GetSource(OwningIDBObjectStoreOrIDBIndex& aSource) const;
 
   IDBCursorDirection GetDirection() 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);
 
   void GetValue(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
                 ErrorResult& aRv);
@@ -154,16 +156,18 @@ class IDBCursor final : public nsISuppor
   void Reset(Key&& aKey, Key&& aSortKey, Key&& aPrimaryKey);
 
   void ClearBackgroundActor() {
     AssertIsOnOwningThread();
 
     mBackgroundActor = nullptr;
   }
 
+  void InvalidateCachedResponses();
+
   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:
--- a/dom/indexedDB/IDBIndex.cpp
+++ b/dom/indexedDB/IDBIndex.cpp
@@ -328,16 +328,21 @@ already_AddRefed<IDBRequest> IDBIndex::G
         "get(%s)",
         "IDBIndex.get()", transaction->LoggingSerialNumber(),
         request->LoggingSerialNumber(),
         IDB_LOG_STRINGIFY(transaction->Database()),
         IDB_LOG_STRINGIFY(transaction), IDB_LOG_STRINGIFY(mObjectStore),
         IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange));
   }
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  transaction->InvalidateCursorCaches();
+
   transaction->StartRequest(request, params);
 
   return request.forget();
 }
 
 already_AddRefed<IDBRequest> IDBIndex::GetAllInternal(
     bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> aKey,
     const Optional<uint32_t>& aLimit, ErrorResult& aRv) {
@@ -398,16 +403,21 @@ already_AddRefed<IDBRequest> IDBIndex::G
         "IDBIndex.getAll()", transaction->LoggingSerialNumber(),
         request->LoggingSerialNumber(),
         IDB_LOG_STRINGIFY(transaction->Database()),
         IDB_LOG_STRINGIFY(transaction), IDB_LOG_STRINGIFY(mObjectStore),
         IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange),
         IDB_LOG_STRINGIFY(aLimit));
   }
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  transaction->InvalidateCursorCaches();
+
   transaction->StartRequest(request, params);
 
   return request.forget();
 }
 
 already_AddRefed<IDBRequest> IDBIndex::OpenCursorInternal(
     bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> aRange,
     IDBCursorDirection aDirection, ErrorResult& aRv) {
@@ -475,16 +485,21 @@ already_AddRefed<IDBRequest> IDBIndex::O
         IDB_LOG_STRINGIFY(transaction), IDB_LOG_STRINGIFY(mObjectStore),
         IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange),
         IDB_LOG_STRINGIFY(direction));
   }
 
   BackgroundCursorChild* const actor =
       new BackgroundCursorChild(request, this, direction);
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  transaction->InvalidateCursorCaches();
+
   mObjectStore->Transaction()->OpenCursor(actor, params);
 
   return request.forget();
 }
 
 already_AddRefed<IDBRequest> IDBIndex::Count(JSContext* aCx,
                                              JS::Handle<JS::Value> aKey,
                                              ErrorResult& aRv) {
@@ -524,16 +539,21 @@ already_AddRefed<IDBRequest> IDBIndex::C
       "database(%s).transaction(%s).objectStore(%s).index(%s)."
       "count(%s)",
       "IDBObjectStore.count()", transaction->LoggingSerialNumber(),
       request->LoggingSerialNumber(),
       IDB_LOG_STRINGIFY(transaction->Database()),
       IDB_LOG_STRINGIFY(transaction), IDB_LOG_STRINGIFY(mObjectStore),
       IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange));
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  transaction->InvalidateCursorCaches();
+
   transaction->StartRequest(request, params);
 
   return request.forget();
 }
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBIndex)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBIndex)
 
--- a/dom/indexedDB/IDBObjectStore.cpp
+++ b/dom/indexedDB/IDBObjectStore.cpp
@@ -1670,16 +1670,18 @@ already_AddRefed<IDBRequest> IDBObjectSt
           IDB_LOG_STRINGIFY(mTransaction->Database()),
           IDB_LOG_STRINGIFY(mTransaction), IDB_LOG_STRINGIFY(this),
           IDB_LOG_STRINGIFY(key));
     }
   }
 
   mTransaction->StartRequest(request, params);
 
+  mTransaction->InvalidateCursorCaches();
+
   return request.forget();
 }
 
 already_AddRefed<IDBRequest> IDBObjectStore::GetAllInternal(
     bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> aKey,
     const Optional<uint32_t>& aLimit, ErrorResult& aRv) {
   AssertIsOnOwningThread();
 
@@ -1735,16 +1737,21 @@ already_AddRefed<IDBRequest> IDBObjectSt
         "getAll(%s, %s)",
         "IDBObjectStore.getAll()", mTransaction->LoggingSerialNumber(),
         request->LoggingSerialNumber(),
         IDB_LOG_STRINGIFY(mTransaction->Database()),
         IDB_LOG_STRINGIFY(mTransaction), IDB_LOG_STRINGIFY(this),
         IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(aLimit));
   }
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  mTransaction->InvalidateCursorCaches();
+
   mTransaction->StartRequest(request, params);
 
   return request.forget();
 }
 
 already_AddRefed<IDBRequest> IDBObjectStore::Clear(JSContext* aCx,
                                                    ErrorResult& aRv) {
   AssertIsOnOwningThread();
@@ -1771,16 +1778,18 @@ already_AddRefed<IDBRequest> IDBObjectSt
 
   IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
       "database(%s).transaction(%s).objectStore(%s).clear()",
       "IDBObjectStore.clear()", mTransaction->LoggingSerialNumber(),
       request->LoggingSerialNumber(),
       IDB_LOG_STRINGIFY(mTransaction->Database()),
       IDB_LOG_STRINGIFY(mTransaction), IDB_LOG_STRINGIFY(this));
 
+  mTransaction->InvalidateCursorCaches();
+
   mTransaction->StartRequest(request, params);
 
   return request.forget();
 }
 
 already_AddRefed<IDBIndex> IDBObjectStore::Index(const nsAString& aName,
                                                  ErrorResult& aRv) {
   AssertIsOnOwningThread();
@@ -1960,16 +1969,21 @@ already_AddRefed<IDBRequest> IDBObjectSt
   IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
       "database(%s).transaction(%s).objectStore(%s).get(%s)",
       "IDBObjectStore.get()", mTransaction->LoggingSerialNumber(),
       request->LoggingSerialNumber(),
       IDB_LOG_STRINGIFY(mTransaction->Database()),
       IDB_LOG_STRINGIFY(mTransaction), IDB_LOG_STRINGIFY(this),
       IDB_LOG_STRINGIFY(keyRange));
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  mTransaction->InvalidateCursorCaches();
+
   mTransaction->StartRequest(request, params);
 
   return request.forget();
 }
 
 already_AddRefed<IDBRequest> IDBObjectStore::DeleteInternal(
     JSContext* aCx, JS::Handle<JS::Value> aKey, bool aFromCursor,
     ErrorResult& aRv) {
@@ -2016,16 +2030,18 @@ already_AddRefed<IDBRequest> IDBObjectSt
         request->LoggingSerialNumber(),
         IDB_LOG_STRINGIFY(mTransaction->Database()),
         IDB_LOG_STRINGIFY(mTransaction), IDB_LOG_STRINGIFY(this),
         IDB_LOG_STRINGIFY(keyRange));
   }
 
   mTransaction->StartRequest(request, params);
 
+  mTransaction->InvalidateCursorCaches();
+
   return request.forget();
 }
 
 already_AddRefed<IDBIndex> IDBObjectStore::CreateIndex(
     const nsAString& aName, const StringOrStringSequence& aKeyPath,
     const IDBIndexParameters& aOptionalParameters, ErrorResult& aRv) {
   AssertIsOnOwningThread();
 
@@ -2232,16 +2248,21 @@ already_AddRefed<IDBRequest> IDBObjectSt
   IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
       "database(%s).transaction(%s).objectStore(%s).count(%s)",
       "IDBObjectStore.count()", mTransaction->LoggingSerialNumber(),
       request->LoggingSerialNumber(),
       IDB_LOG_STRINGIFY(mTransaction->Database()),
       IDB_LOG_STRINGIFY(mTransaction), IDB_LOG_STRINGIFY(this),
       IDB_LOG_STRINGIFY(keyRange));
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  mTransaction->InvalidateCursorCaches();
+
   mTransaction->StartRequest(request, params);
 
   return request.forget();
 }
 
 already_AddRefed<IDBRequest> IDBObjectStore::OpenCursorInternal(
     bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> aRange,
     IDBCursorDirection aDirection, ErrorResult& aRv) {
@@ -2308,16 +2329,21 @@ already_AddRefed<IDBRequest> IDBObjectSt
         IDB_LOG_STRINGIFY(mTransaction->Database()),
         IDB_LOG_STRINGIFY(mTransaction), IDB_LOG_STRINGIFY(this),
         IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(direction));
   }
 
   BackgroundCursorChild* const actor =
       new BackgroundCursorChild(request, this, direction);
 
+  // TODO: This is necessary to preserve request ordering only. Proper
+  // sequencing of requests should be done in a more sophisticated manner that
+  // doesn't require invalidating cursor caches (Bug 1580499).
+  mTransaction->InvalidateCursorCaches();
+
   mTransaction->OpenCursor(actor, params);
 
   return request.forget();
 }
 
 void IDBObjectStore::RefreshSpec(bool aMayDelete) {
   AssertIsOnOwningThread();
   MOZ_ASSERT_IF(mDeletedSpec, mSpec == mDeletedSpec);
--- a/dom/indexedDB/IDBTransaction.cpp
+++ b/dom/indexedDB/IDBTransaction.cpp
@@ -333,26 +333,26 @@ void IDBTransaction::OnNewRequest() {
   if (!mPendingRequestCount) {
     MOZ_ASSERT(INITIAL == mReadyState);
     mReadyState = LOADING;
   }
 
   ++mPendingRequestCount;
 }
 
-void IDBTransaction::OnRequestFinished(bool aActorDestroyedNormally) {
+void IDBTransaction::OnRequestFinished(bool aRequestCompletedSuccessfully) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mPendingRequestCount);
 
   --mPendingRequestCount;
 
   if (!mPendingRequestCount) {
     mReadyState = COMMITTING;
 
-    if (aActorDestroyedNormally) {
+    if (aRequestCompletedSuccessfully) {
       if (NS_SUCCEEDED(mAbortCode)) {
         SendCommit();
       } else {
         SendAbort(mAbortCode);
       }
     } else {
       // Don't try to send any more messages to the parent if the request actor
       // was killed.
@@ -792,16 +792,37 @@ int64_t IDBTransaction::NextObjectStoreI
 
 int64_t IDBTransaction::NextIndexId() {
   AssertIsOnOwningThread();
   MOZ_ASSERT(VERSION_CHANGE == mMode);
 
   return mNextIndexId++;
 }
 
+void IDBTransaction::InvalidateCursorCaches() {
+  AssertIsOnOwningThread();
+
+  for (auto* const cursor : mCursors) {
+    cursor->InvalidateCachedResponses();
+  }
+}
+
+void IDBTransaction::RegisterCursor(IDBCursor* const aCursor) {
+  AssertIsOnOwningThread();
+
+  mCursors.AppendElement(aCursor);
+}
+
+void IDBTransaction::UnregisterCursor(IDBCursor* const aCursor) {
+  AssertIsOnOwningThread();
+
+  DebugOnly<bool> removed = mCursors.RemoveElement(aCursor);
+  MOZ_ASSERT(removed);
+}
+
 nsIGlobalObject* IDBTransaction::GetParentObject() const {
   AssertIsOnOwningThread();
 
   return mDatabase->GetParentObject();
 }
 
 IDBTransactionMode IDBTransaction::GetMode(ErrorResult& aRv) const {
   AssertIsOnOwningThread();
--- a/dom/indexedDB/IDBTransaction.h
+++ b/dom/indexedDB/IDBTransaction.h
@@ -20,16 +20,17 @@ namespace mozilla {
 
 class ErrorResult;
 class EventChainPreVisitor;
 
 namespace dom {
 
 class DOMException;
 class DOMStringList;
+class IDBCursor;
 class IDBDatabase;
 class IDBObjectStore;
 class IDBOpenDBRequest;
 class IDBRequest;
 class StrongWorkerRef;
 
 namespace indexedDB {
 class BackgroundCursorChild;
@@ -62,16 +63,17 @@ class IDBTransaction final : public DOME
 
  private:
   RefPtr<IDBDatabase> mDatabase;
   RefPtr<DOMException> mError;
   nsTArray<nsString> mObjectStoreNames;
   nsTArray<RefPtr<IDBObjectStore>> mObjectStores;
   nsTArray<RefPtr<IDBObjectStore>> mDeletedObjectStores;
   RefPtr<StrongWorkerRef> mWorkerRef;
+  nsTArray<IDBCursor*> mCursors;
 
   // Tagged with mMode. If mMode is VERSION_CHANGE then mBackgroundActor will be
   // a BackgroundVersionChangeTransactionChild. Otherwise it will be a
   // BackgroundTransactionChild.
   union {
     indexedDB::BackgroundTransactionChild* mNormalBackgroundActor;
     indexedDB::BackgroundVersionChangeTransactionChild*
         mVersionChangeBackgroundActor;
@@ -245,16 +247,20 @@ class IDBTransaction final : public DOME
   void FireCompleteOrAbortEvents(nsresult aResult);
 
   // Only for VERSION_CHANGE transactions.
   int64_t NextObjectStoreId();
 
   // Only for VERSION_CHANGE transactions.
   int64_t NextIndexId();
 
+  void InvalidateCursorCaches();
+  void RegisterCursor(IDBCursor* aCursor);
+  void UnregisterCursor(IDBCursor* aCursor);
+
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIRUNNABLE
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBTransaction, DOMEventTargetHelper)
 
   // nsWrapperCache
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
@@ -274,15 +280,15 @@ class IDBTransaction final : public DOME
   void SendAbort(nsresult aResultCode);
 
   void NoteActiveTransaction();
 
   void MaybeNoteInactiveTransaction();
 
   void OnNewRequest();
 
-  void OnRequestFinished(bool aActorDestroyedNormally);
+  void OnRequestFinished(bool aRequestCompletedSuccessfully);
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_idbtransaction_h__
--- a/dom/indexedDB/PBackgroundIDBCursor.ipdl
+++ b/dom/indexedDB/PBackgroundIDBCursor.ipdl
@@ -68,16 +68,17 @@ struct IndexCursorResponse
 
 struct IndexKeyCursorResponse
 {
   Key key;
   Key sortKey;
   Key objectKey;
 };
 
+// TODO: All cursor responses must be arrays!
 union CursorResponse
 {
   void_t;
   nsresult;
   ObjectStoreCursorResponse[];
   ObjectStoreKeyCursorResponse;
   IndexCursorResponse;
   IndexKeyCursorResponse;