Skip to content

Fix SortedMap's reverse iterator (without start key). #6020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 24, 2022

Conversation

ehsannas
Copy link
Contributor

I happen to be the first person to use getReverseIterator() which has a bug.

@changeset-bot
Copy link

changeset-bot bot commented Feb 19, 2022

⚠️ No Changeset found

Latest commit: 552f3e5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Contributor

Changeset File Check ⚠️

  • Changeset formatting error in following file:
    Some packages have been changed but no changesets were found. Run `changeset add` to resolve this error.
    If this change doesn't need a release, run `changeset add --empty`.
    

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Feb 19, 2022

Size Report 1

Affected Products

  • @firebase/firestore

    TypeBase (9fb82cb)Merge (7b0bd28)Diff
    browser244 kB251 kB+6.59 kB (+2.7%)
    esm5304 kB311 kB+7.50 kB (+2.5%)
    main487 kB500 kB+13.5 kB (+2.8%)
    module244 kB251 kB+6.59 kB (+2.7%)
    react-native244 kB251 kB+6.59 kB (+2.7%)
  • @firebase/firestore-lite

    TypeBase (9fb82cb)Merge (7b0bd28)Diff
    browser73.0 kB73.1 kB+137 B (+0.2%)
    esm586.4 kB86.5 kB+137 B (+0.2%)
    main125 kB126 kB+116 B (+0.1%)
    module73.0 kB73.1 kB+137 B (+0.2%)
    react-native73.2 kB73.3 kB+137 B (+0.2%)
  • bundle

    12 size changes

    TypeBase (9fb82cb)Merge (7b0bd28)Diff
    firestore (Persistence)245 kB252 kB+6.72 kB (+2.7%)
    firestore (Query Cursors)192 kB192 kB+143 B (+0.1%)
    firestore (Query)193 kB193 kB+143 B (+0.1%)
    firestore (Read data once)181 kB181 kB+143 B (+0.1%)
    firestore (Realtime updates)183 kB184 kB+143 B (+0.1%)
    firestore (Transaction)166 kB166 kB+143 B (+0.1%)
    firestore (Write data)165 kB165 kB+143 B (+0.1%)
    firestore-lite (Query Cursors)56.8 kB56.9 kB+115 B (+0.2%)
    firestore-lite (Query)59.9 kB60.0 kB+137 B (+0.2%)
    firestore-lite (Read data once)44.4 kB44.5 kB+115 B (+0.3%)
    firestore-lite (Transaction)61.7 kB61.8 kB+115 B (+0.2%)
    firestore-lite (Write data)47.2 kB47.3 kB+115 B (+0.2%)

  • firebase

    TypeBase (9fb82cb)Merge (7b0bd28)Diff
    firebase-compat.js769 kB775 kB+6.57 kB (+0.9%)
    firebase-firestore-compat.js296 kB302 kB+6.57 kB (+2.2%)
    firebase-firestore-lite.js250 kB250 kB+457 B (+0.2%)
    firebase-firestore.js812 kB835 kB+23.2 kB (+2.9%)

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/ppdNt8CYzD.html

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Feb 19, 2022

Size Analysis Report 1

This report is too large (652,019 characters) to be displayed here in a GitHub comment. Please use the below link to see the full report on Google Cloud Storage.

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/ry1v8LEl5V.html

): void {
const countUp = from <= to;
let expected = from;
while (iterator.hasNext()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW From the method contract, it seems like this method should iterate until it reaches to and validate that hasNext remains true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will rename to validateIteratorCoversRange

@@ -197,7 +197,7 @@ export class SortedMapIterator<K, V> {
while (!node.isEmpty()) {
cmp = startKey ? comparator(node.key, startKey) : 1;
// flip the comparison if we're going in reverse
if (isReverse) {
if (startKey && isReverse) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't get my head around this - why do we not need to do this when we iterate without start key? I can debug this to understand more, but I wonder if you have a succinct explanation why this is the correct fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. It boils down to the iteration logic below (line 204-227).

First, description of the bug:

If this constructor is called with isReverse = true and startKey = null, then cmp will always be -1 in all iterations of the while loop. Notice that this.nodeStack.push is never called if cmp < 0. So, reverse traversal with no startKey will always result in iterating zero nodes.

More explanation

The purpose of this while loop is to initialize a stack that will be used for traversal. The while loop always starts at the root node of a red-black tree.

In the absence of a startKey,

  • For forward iteration (iterating from smallest node to largest node): the while loop should start from the root and keep going to the left child repeatedly until we reach the left-most leaf node (pushing these nodes onto the stack along the way).
  • For reverse iteration (iterating from largest node to smallest node): the while loop should start from the root and keep going to the right child repeatedly until we reach the right-most leaf node (pushing these nodes onto the stack along the way).

In the presence of a startKey,

We should start at the root of the tree, and first search for the startKey. We should not push anything onto the stack until we find the startKey (or find a node that's larger than startKey for forward iteration, or find a node that's smaller than startKey for reverse iteration). At that point, we should start pushing nodes onto the stack.

Copy link
Contributor Author

@ehsannas ehsannas Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, the code can also be written as follows:

if(startKey) {
  while (!node.isEmpty()) {
      cmp = comparator(node.key, startKey);
      // flip the comparison if we're going in reverse
      if (isReverse) {
        cmp *= -1;
      }
      if (cmp < 0) {
        // This node is less than our start key. ignore it
        if (this.isReverse) {
          node = node.left;
        } else {
          node = node.right;
        }
      } else if (cmp === 0) {
        // This node is exactly equal to our start key. Push it on the stack,
        // but stop iterating;
        this.nodeStack.push(node);
        break;
      } else {
        // This node is greater than our start key, add it to the stack and move
        // to the next one
        this.nodeStack.push(node);
        if (this.isReverse) {
          node = node.right;
        } else {
          node = node.left;
        }
      }
  }
} else {
  // There is no startKey, go all the way.
  while (!node.isEmpty()) {
        this.nodeStack.push(node);
        if (this.isReverse) {
          node = node.right;
        } else {
          node = node.left;
        }
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thanks for the explanation. I didn't quite catch that this same code was doing two completely different things (finding the start key and then building the stack). I think we could probably fix this to run two iterations, but that's a P3 and this PR is good to ship.

@ehsannas ehsannas merged commit 8ac8fb0 into master Feb 24, 2022
@ehsannas ehsannas deleted the ehsann/fix-sorted-map branch February 24, 2022 03:46
@firebase firebase locked and limited conversation to collaborators Mar 27, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants