Skip to content

Optimize ObjectID sort and serdes #1582

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 20 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 50 additions & 128 deletions bson/src/main/org/bson/types/ObjectId.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@

package org.bson.types;

import static org.bson.assertions.Assertions.isTrueArgument;
import static org.bson.assertions.Assertions.notNull;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.SecureRandom;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

import static org.bson.assertions.Assertions.isTrueArgument;
import static org.bson.assertions.Assertions.notNull;

/**
* <p>A globally unique identifier for objects.</p>
*
Expand All @@ -53,9 +54,8 @@ public final class ObjectId implements Comparable<ObjectId>, Serializable {
private static final int OBJECT_ID_LENGTH = 12;
private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff;

// Use primitives to represent the 5-byte random value.
private static final int RANDOM_VALUE1;
private static final short RANDOM_VALUE2;
// Use upper bytes of a long to represent the 5-byte random value.
private static final long RANDOM_VALUE;

private static final AtomicInteger NEXT_COUNTER;

Expand All @@ -67,18 +67,12 @@ public final class ObjectId implements Comparable<ObjectId>, Serializable {
* The timestamp
*/
private final int timestamp;

/**
* The counter.
*/
private final int counter;
/**
* the first four bits of randomness.
*/
private final int randomValue1;
/**
* The last two bits of randomness.
* The final 8 bytes of the ObjectID are 5 bytes probabilistically unique to the machine and
* process, followed by a 3 byte incrementing counter initialized to a random value.
*/
private final short randomValue2;
private final long nonce;

/**
* Gets a new object id.
Expand All @@ -101,7 +95,7 @@ public static ObjectId get() {
* @since 4.1
*/
public static ObjectId getSmallestWithDate(final Date date) {
return new ObjectId(dateToTimestampSeconds(date), 0, (short) 0, 0, false);
return new ObjectId(dateToTimestampSeconds(date), 0L);
}

/**
Expand Down Expand Up @@ -152,7 +146,7 @@ public ObjectId() {
* @param date the date
*/
public ObjectId(final Date date) {
this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false);
this(dateToTimestampSeconds(date), RANDOM_VALUE | (NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES));
}

/**
Expand All @@ -163,7 +157,7 @@ public ObjectId(final Date date) {
* @throws IllegalArgumentException if the high order byte of counter is not zero
*/
public ObjectId(final Date date, final int counter) {
this(dateToTimestampSeconds(date), counter, true);
this(dateToTimestampSeconds(date), getNonceFromUntrustedCounter(counter));
}

/**
Expand All @@ -174,25 +168,19 @@ public ObjectId(final Date date, final int counter) {
* @throws IllegalArgumentException if the high order byte of counter is not zero
*/
public ObjectId(final int timestamp, final int counter) {
this(timestamp, counter, true);
this(timestamp, getNonceFromUntrustedCounter(counter));
}

private ObjectId(final int timestamp, final int counter, final boolean checkCounter) {
this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter);
private ObjectId(final int timestamp, final long nonce) {
this.timestamp = timestamp;
this.nonce = nonce;
}

private ObjectId(final int timestamp, final int randomValue1, final short randomValue2, final int counter,
final boolean checkCounter) {
if ((randomValue1 & 0xff000000) != 0) {
throw new IllegalArgumentException("The random value must be between 0 and 16777215 (it must fit in three bytes).");
}
if (checkCounter && ((counter & 0xff000000) != 0)) {
private static long getNonceFromUntrustedCounter(final int counter) {
if ((counter & 0xff000000) != 0) {
throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes).");
}
this.timestamp = timestamp;
this.counter = counter & LOW_ORDER_THREE_BYTES;
this.randomValue1 = randomValue1;
this.randomValue2 = randomValue2;
return RANDOM_VALUE | counter;
}

/**
Expand Down Expand Up @@ -226,12 +214,14 @@ public ObjectId(final ByteBuffer buffer) {
notNull("buffer", buffer);
isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH);

// Note: Cannot use ByteBuffer.getInt because it depends on tbe buffer's byte order
// and ObjectId's are always in big-endian order.
timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get());
randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());
randomValue2 = makeShort(buffer.get(), buffer.get());
counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());
ByteOrder originalOrder = buffer.order();
try {
buffer.order(ByteOrder.BIG_ENDIAN);
this.timestamp = buffer.getInt();
this.nonce = buffer.getLong();
} finally {
buffer.order(originalOrder);
}
}

/**
Expand All @@ -240,9 +230,11 @@ public ObjectId(final ByteBuffer buffer) {
* @return the byte array
*/
public byte[] toByteArray() {
ByteBuffer buffer = ByteBuffer.allocate(OBJECT_ID_LENGTH);
putToByteBuffer(buffer);
return buffer.array(); // using .allocate ensures there is a backing array that can be returned
// using .allocate ensures there is a backing array that can be returned
return ByteBuffer.allocate(OBJECT_ID_LENGTH)
.putInt(this.timestamp)
.putLong(this.nonce)
.array();
}

/**
Expand All @@ -257,18 +249,14 @@ public void putToByteBuffer(final ByteBuffer buffer) {
notNull("buffer", buffer);
isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH);

buffer.put(int3(timestamp));
buffer.put(int2(timestamp));
buffer.put(int1(timestamp));
buffer.put(int0(timestamp));
buffer.put(int2(randomValue1));
buffer.put(int1(randomValue1));
buffer.put(int0(randomValue1));
buffer.put(short1(randomValue2));
buffer.put(short0(randomValue2));
buffer.put(int2(counter));
buffer.put(int1(counter));
buffer.put(int0(counter));
ByteOrder originalOrder = buffer.order();
try {
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.putInt(this.timestamp);
buffer.putLong(this.nonce);
} finally {
buffer.order(originalOrder);
}
}

/**
Expand Down Expand Up @@ -313,49 +301,26 @@ public boolean equals(final Object o) {
return false;
}

ObjectId objectId = (ObjectId) o;

if (counter != objectId.counter) {
return false;
}
if (timestamp != objectId.timestamp) {
return false;
}

if (randomValue1 != objectId.randomValue1) {
ObjectId other = (ObjectId) o;
if (timestamp != other.timestamp) {
return false;
}

if (randomValue2 != objectId.randomValue2) {
return false;
}

return true;
return nonce == other.nonce;
}

@Override
public int hashCode() {
int result = timestamp;
result = 31 * result + counter;
result = 31 * result + randomValue1;
result = 31 * result + randomValue2;
return result;
return 31 * timestamp + Long.hashCode(nonce);
}

@Override
public int compareTo(final ObjectId other) {
if (other == null) {
throw new NullPointerException();
int cmp = Integer.compareUnsigned(this.timestamp, other.timestamp);
if (cmp != 0) {
return cmp;
}

byte[] byteArray = toByteArray();
byte[] otherByteArray = other.toByteArray();
for (int i = 0; i < OBJECT_ID_LENGTH; i++) {
if (byteArray[i] != otherByteArray[i]) {
return ((byteArray[i] & 0xff) < (otherByteArray[i] & 0xff)) ? -1 : 1;
}
}
return 0;
return Long.compareUnsigned(nonce, other.nonce);
}

@Override
Expand Down Expand Up @@ -407,8 +372,7 @@ private Object readResolve() {
static {
try {
SecureRandom secureRandom = new SecureRandom();
RANDOM_VALUE1 = secureRandom.nextInt(0x01000000);
RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000);
RANDOM_VALUE = secureRandom.nextLong() & ~LOW_ORDER_THREE_BYTES;
NEXT_COUNTER = new AtomicInteger(secureRandom.nextInt());
} catch (Exception e) {
throw new RuntimeException(e);
Expand Down Expand Up @@ -443,46 +407,4 @@ private static int hexCharToInt(final char c) {
private static int dateToTimestampSeconds(final Date time) {
return (int) (time.getTime() / 1000);
}

// Big-Endian helpers, in this class because all other BSON numbers are little-endian

private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) {
// CHECKSTYLE:OFF
return (((b3) << 24) |
((b2 & 0xff) << 16) |
((b1 & 0xff) << 8) |
((b0 & 0xff)));
// CHECKSTYLE:ON
}

private static short makeShort(final byte b1, final byte b0) {
// CHECKSTYLE:OFF
return (short) (((b1 & 0xff) << 8) | ((b0 & 0xff)));
// CHECKSTYLE:ON
}

private static byte int3(final int x) {
return (byte) (x >> 24);
}

private static byte int2(final int x) {
return (byte) (x >> 16);
}

private static byte int1(final int x) {
return (byte) (x >> 8);
}

private static byte int0(final int x) {
return (byte) (x);
}

private static byte short1(final short x) {
return (byte) (x >> 8);
}

private static byte short0(final short x) {
return (byte) (x);
}

}
Loading