Skip to content

perf(NODE-5934): replace DataView uses with bit math #649

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 5 commits into from
Feb 27, 2024
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
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