Skip to content

Commit

Permalink
perf(NODE-5934): replace DataView uses with bit math (#649)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken authored Feb 27, 2024
1 parent 24d035e commit 6d343ab
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 245 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
"no-bigint-usage/no-bigint-literals": "error",
"no-restricted-globals": [
"error",
"BigInt"
"BigInt",
"DataView"
]
},
"overrides": [
Expand Down
7 changes: 2 additions & 5 deletions src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { BSONRegExp } from './regexp';
import { BSONSymbol } from './symbol';
import { Timestamp } from './timestamp';
import { ByteUtils } from './utils/byte_utils';
import { NumberUtils } from './utils/number_utils';
export type { UUIDExtended, BinaryExtended, BinaryExtendedLegacy, BinarySequence } from './binary';
export type { CodeExtended } from './code';
export type { DBRefLike } from './db_ref';
Expand Down Expand Up @@ -232,11 +233,7 @@ export function deserializeStream(
// Loop over all documents
for (let i = 0; i < numberOfDocuments; i++) {
// Find size of the document
const size =
bufferData[index] |
(bufferData[index + 1] << 8) |
(bufferData[index + 2] << 16) |
(bufferData[index + 3] << 24);
const size = NumberUtils.getInt32LE(bufferData, index);
// Update options with index
internalOptions.index = index;
// Parse the document at this point
Expand Down
12 changes: 7 additions & 5 deletions src/objectid.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { BSONValue } from './bson_value';
import { BSONError } from './error';
import { type InspectFn, defaultInspect } from './parser/utils';
import { BSONDataView, ByteUtils } from './utils/byte_utils';
import { ByteUtils } from './utils/byte_utils';
import { NumberUtils } from './utils/number_utils';

// Regular expression that checks for hex value
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$');
Expand Down Expand Up @@ -179,7 +180,7 @@ export class ObjectId extends BSONValue {
const buffer = ByteUtils.allocate(12);

// 4-byte timestamp
BSONDataView.fromUint8Array(buffer).setUint32(0, time, false);
NumberUtils.setInt32BE(buffer, 0, time);

// set PROCESS_UNIQUE if yet not initialized
if (PROCESS_UNIQUE === null) {
Expand Down Expand Up @@ -259,7 +260,7 @@ export class ObjectId extends BSONValue {
/** Returns the generation date (accurate up to the second) that this ID was generated. */
getTimestamp(): Date {
const timestamp = new Date();
const time = BSONDataView.fromUint8Array(this.id).getUint32(0, false);
const time = NumberUtils.getUint32BE(this.buffer, 0);
timestamp.setTime(Math.floor(time) * 1000);
return timestamp;
}
Expand Down Expand Up @@ -292,9 +293,10 @@ export class ObjectId extends BSONValue {
* @param time - an integer number representing a number of seconds.
*/
static createFromTime(time: number): ObjectId {
const buffer = ByteUtils.fromNumberArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
const buffer = ByteUtils.allocate(12);
for (let i = 11; i >= 4; i--) buffer[i] = 0;
// Encode time into first 4 bytes
BSONDataView.fromUint8Array(buffer).setUint32(0, time, false);
NumberUtils.setInt32BE(buffer, 0, time);
// Return the new objectId
return new ObjectId(buffer);
}
Expand Down
191 changes: 59 additions & 132 deletions src/parser/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { ObjectId } from '../objectid';
import { BSONRegExp } from '../regexp';
import { BSONSymbol } from '../symbol';
import { Timestamp } from '../timestamp';
import { BSONDataView, ByteUtils } from '../utils/byte_utils';
import { ByteUtils } from '../utils/byte_utils';
import { NumberUtils } from '../utils/number_utils';
import { validateUtf8 } from '../validate_utf8';

/** @public */
Expand Down Expand Up @@ -91,11 +92,7 @@ export function internalDeserialize(
options = options == null ? {} : options;
const index = options && options.index ? options.index : 0;
// Read the document size
const size =
buffer[index] |
(buffer[index + 1] << 8) |
(buffer[index + 2] << 16) |
(buffer[index + 3] << 24);
const size = NumberUtils.getInt32LE(buffer, index);

if (size < 5) {
throw new BSONError(`bson size must be >= 5, is ${size}`);
Expand Down Expand Up @@ -204,8 +201,8 @@ function deserializeObject(
if (buffer.length < 5) throw new BSONError('corrupt bson message < 5 bytes long');

// Read the document size
const size =
buffer[index++] | (buffer[index++] << 8) | (buffer[index++] << 16) | (buffer[index++] << 24);
const size = NumberUtils.getInt32LE(buffer, index);
index += 4;

// Ensure buffer is valid size
if (size < 5 || size > buffer.length) throw new BSONError('corrupt bson message');
Expand All @@ -218,8 +215,6 @@ function deserializeObject(

let isPossibleDBRef = isArray ? false : null;

let dataView;

// While we have more left data left keep parsing
while (!done) {
// Read the type
Expand Down Expand Up @@ -257,11 +252,8 @@ function deserializeObject(
index = i + 1;

if (elementType === constants.BSON_DATA_STRING) {
const stringSize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const stringSize = NumberUtils.getInt32LE(buffer, index);
index += 4;
if (
stringSize <= 0 ||
stringSize > buffer.length - index ||
Expand All @@ -277,34 +269,19 @@ function deserializeObject(
value = new ObjectId(oid);
index = index + 12;
} else if (elementType === constants.BSON_DATA_INT && promoteValues === false) {
value = new Int32(
buffer[index++] | (buffer[index++] << 8) | (buffer[index++] << 16) | (buffer[index++] << 24)
);
value = new Int32(NumberUtils.getInt32LE(buffer, index));
index += 4;
} else if (elementType === constants.BSON_DATA_INT) {
value =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
} else if (elementType === constants.BSON_DATA_NUMBER && promoteValues === false) {
dataView ??= new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
value = new Double(dataView.getFloat64(index, true));
index = index + 8;
value = NumberUtils.getInt32LE(buffer, index);
index += 4;
} else if (elementType === constants.BSON_DATA_NUMBER) {
dataView ??= new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
value = dataView.getFloat64(index, true);
index = index + 8;
value = NumberUtils.getFloat64LE(buffer, index);
index += 8;
if (promoteValues === false) value = new Double(value);
} else if (elementType === constants.BSON_DATA_DATE) {
const lowBits =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const highBits =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const lowBits = NumberUtils.getInt32LE(buffer, index);
const highBits = NumberUtils.getInt32LE(buffer, index + 4);
index += 8;

value = new Date(new Long(lowBits, highBits).toNumber());
} else if (elementType === constants.BSON_DATA_BOOLEAN) {
Expand All @@ -313,11 +290,8 @@ function deserializeObject(
value = buffer[index++] === 1;
} else if (elementType === constants.BSON_DATA_OBJECT) {
const _index = index;
const objectSize =
buffer[index] |
(buffer[index + 1] << 8) |
(buffer[index + 2] << 16) |
(buffer[index + 3] << 24);
const objectSize = NumberUtils.getInt32LE(buffer, index);

if (objectSize <= 0 || objectSize > buffer.length - index)
throw new BSONError('bad embedded document length in bson');

Expand All @@ -335,11 +309,7 @@ function deserializeObject(
index = index + objectSize;
} else if (elementType === constants.BSON_DATA_ARRAY) {
const _index = index;
const objectSize =
buffer[index] |
(buffer[index + 1] << 8) |
(buffer[index + 2] << 16) |
(buffer[index + 3] << 24);
const objectSize = NumberUtils.getInt32LE(buffer, index);
let arrayOptions: DeserializeOptions = options;

// Stop index
Expand All @@ -363,30 +333,25 @@ function deserializeObject(
} else if (elementType === constants.BSON_DATA_NULL) {
value = null;
} else if (elementType === constants.BSON_DATA_LONG) {
// Unpack the low and high bits
const dataview = BSONDataView.fromUint8Array(buffer.subarray(index, index + 8));

const lowBits =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const highBits =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const long = new Long(lowBits, highBits);
if (useBigInt64) {
value = dataview.getBigInt64(0, true);
} else if (promoteLongs && promoteValues === true) {
// Promote the long if possible
value =
long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG)
? long.toNumber()
: long;
value = NumberUtils.getBigInt64LE(buffer, index);
index += 8;
} else {
value = long;
// Unpack the low and high bits
const lowBits = NumberUtils.getInt32LE(buffer, index);
const highBits = NumberUtils.getInt32LE(buffer, index + 4);
index += 8;

const long = new Long(lowBits, highBits);
// Promote the long if possible
if (promoteLongs && promoteValues === true) {
value =
long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG)
? long.toNumber()
: long;
} else {
value = long;
}
}
} else if (elementType === constants.BSON_DATA_DECIMAL128) {
// Buffer to contain the decimal bytes
Expand All @@ -398,11 +363,8 @@ function deserializeObject(
// Assign the new Decimal128 value
value = new Decimal128(bytes);
} else if (elementType === constants.BSON_DATA_BINARY) {
let binarySize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
let binarySize = NumberUtils.getInt32LE(buffer, index);
index += 4;
const totalBinarySize = binarySize;
const subType = buffer[index++];

Expand All @@ -417,11 +379,8 @@ function deserializeObject(
if (buffer['slice'] != null) {
// If we have subtype 2 skip the 4 bytes for the size
if (subType === Binary.SUBTYPE_BYTE_ARRAY) {
binarySize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
binarySize = NumberUtils.getInt32LE(buffer, index);
index += 4;
if (binarySize < 0)
throw new BSONError('Negative binary type element size found for subtype 0x02');
if (binarySize > totalBinarySize - 4)
Expand All @@ -442,11 +401,8 @@ function deserializeObject(
const _buffer = ByteUtils.allocate(binarySize);
// If we have subtype 2 skip the 4 bytes for the size
if (subType === Binary.SUBTYPE_BYTE_ARRAY) {
binarySize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
binarySize = NumberUtils.getInt32LE(buffer, index);
index += 4;
if (binarySize < 0)
throw new BSONError('Negative binary type element size found for subtype 0x02');
if (binarySize > totalBinarySize - 4)
Expand Down Expand Up @@ -545,11 +501,8 @@ function deserializeObject(
// Set the object
value = new BSONRegExp(source, regExpOptions);
} else if (elementType === constants.BSON_DATA_SYMBOL) {
const stringSize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const stringSize = NumberUtils.getInt32LE(buffer, index);
index += 4;
if (
stringSize <= 0 ||
stringSize > buffer.length - index ||
Expand All @@ -561,31 +514,18 @@ function deserializeObject(
value = promoteValues ? symbol : new BSONSymbol(symbol);
index = index + stringSize;
} else if (elementType === constants.BSON_DATA_TIMESTAMP) {
// We intentionally **do not** use bit shifting here
// Bit shifting in javascript coerces numbers to **signed** int32s
// We need to keep i, and t unsigned
const i =
buffer[index++] +
buffer[index++] * (1 << 8) +
buffer[index++] * (1 << 16) +
buffer[index++] * (1 << 24);
const t =
buffer[index++] +
buffer[index++] * (1 << 8) +
buffer[index++] * (1 << 16) +
buffer[index++] * (1 << 24);

value = new Timestamp({ i, t });
value = new Timestamp({
i: NumberUtils.getUint32LE(buffer, index),
t: NumberUtils.getUint32LE(buffer, index + 4)
});
index += 8;
} else if (elementType === constants.BSON_DATA_MIN_KEY) {
value = new MinKey();
} else if (elementType === constants.BSON_DATA_MAX_KEY) {
value = new MaxKey();
} else if (elementType === constants.BSON_DATA_CODE) {
const stringSize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const stringSize = NumberUtils.getInt32LE(buffer, index);
index += 4;
if (
stringSize <= 0 ||
stringSize > buffer.length - index ||
Expand All @@ -605,23 +545,17 @@ function deserializeObject(
// Update parse index position
index = index + stringSize;
} else if (elementType === constants.BSON_DATA_CODE_W_SCOPE) {
const totalSize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const totalSize = NumberUtils.getInt32LE(buffer, index);
index += 4;

// Element cannot be shorter than totalSize + stringSize + documentSize + terminator
if (totalSize < 4 + 4 + 4 + 1) {
throw new BSONError('code_w_scope total size shorter minimum expected length');
}

// Get the code string size
const stringSize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const stringSize = NumberUtils.getInt32LE(buffer, index);
index += 4;
// Check if we have a valid string
if (
stringSize <= 0 ||
Expand All @@ -643,11 +577,7 @@ function deserializeObject(
// Parse the element
const _index = index;
// Decode the size of the object document
const objectSize =
buffer[index] |
(buffer[index + 1] << 8) |
(buffer[index + 2] << 16) |
(buffer[index + 3] << 24);
const objectSize = NumberUtils.getInt32LE(buffer, index);
// Decode the scope object
const scopeObject = deserializeObject(buffer, _index, options, false);
// Adjust the index
Expand All @@ -666,11 +596,8 @@ function deserializeObject(
value = new Code(functionString, scopeObject);
} else if (elementType === constants.BSON_DATA_DBPOINTER) {
// Get the code string size
const stringSize =
buffer[index++] |
(buffer[index++] << 8) |
(buffer[index++] << 16) |
(buffer[index++] << 24);
const stringSize = NumberUtils.getInt32LE(buffer, index);
index += 4;
// Check if we have a valid string
if (
stringSize <= 0 ||
Expand Down
Loading

0 comments on commit 6d343ab

Please sign in to comment.