From 05b9decee2bc95922816e2bb6865d6b872fd7729 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 15 Feb 2024 14:09:11 -0500 Subject: [PATCH] perf(NODE-5934): replace DataView uses with bit math --- .eslintrc.json | 3 +- src/objectid.ts | 34 +++++++++++++--- src/parser/deserializer.ts | 81 ++++++++++++++++++++++--------------- src/parser/serializer.ts | 76 ++++++++++++++++++++++++---------- src/utils/byte_utils.ts | 6 --- src/utils/web_byte_utils.ts | 4 +- test/node/bigint.test.ts | 19 +++++++-- 7 files changed, 151 insertions(+), 72 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 615cd9630..3e4825518 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -75,7 +75,8 @@ "no-bigint-usage/no-bigint-literals": "error", "no-restricted-globals": [ "error", - "BigInt" + "BigInt", + "DataView" ] }, "overrides": [ diff --git a/src/objectid.ts b/src/objectid.ts index b448f61b2..34b112f5b 100644 --- a/src/objectid.ts +++ b/src/objectid.ts @@ -1,7 +1,7 @@ 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'; // Regular expression that checks for hex value const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$'); @@ -179,7 +179,13 @@ export class ObjectId extends BSONValue { const buffer = ByteUtils.allocate(12); // 4-byte timestamp - BSONDataView.fromUint8Array(buffer).setUint32(0, time, false); + buffer[3] = time; + time = time >>> 8; + buffer[2] = time; + time = time >>> 8; + buffer[1] = time; + time = time >>> 8; + buffer[0] = time; // set PROCESS_UNIQUE if yet not initialized if (PROCESS_UNIQUE === null) { @@ -259,7 +265,11 @@ 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 = + this.buffer[3] + + this.buffer[2] * (1 << 8) + + this.buffer[1] * (1 << 16) + + this.buffer[0] * (1 << 24); timestamp.setTime(Math.floor(time) * 1000); return timestamp; } @@ -275,9 +285,23 @@ 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); + buffer[11] = 0; + buffer[10] = 0; + buffer[9] = 0; + buffer[8] = 0; + buffer[7] = 0; + buffer[6] = 0; + buffer[5] = 0; + buffer[4] = 0; // Encode time into first 4 bytes - BSONDataView.fromUint8Array(buffer).setUint32(0, time, false); + buffer[3] = time; + time = time >>> 8; + buffer[2] = time; + time = time >>> 8; + buffer[1] = time; + time = time >>> 8; + buffer[0] = time; // Return the new objectId return new ObjectId(buffer); } diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index c23b36586..fe559f3db 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -14,7 +14,7 @@ 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 { validateUtf8 } from '../validate_utf8'; /** @public */ @@ -128,6 +128,9 @@ export function internalDeserialize( const allowedDBRefKeys = /^\$ref$|^\$id$|^\$db$/; +const FLOAT = new Float64Array(1); +const FLOAT_BYTES = new Uint8Array(FLOAT.buffer, 0, 8); + function deserializeObject( buffer: Uint8Array, index: number, @@ -218,8 +221,6 @@ function deserializeObject( let isPossibleDBRef = isArray ? false : null; - let dataView; - // While we have more left data left keep parsing while (!done) { // Read the type @@ -286,14 +287,17 @@ function deserializeObject( (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; } else if (elementType === constants.BSON_DATA_NUMBER) { - dataView ??= new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); - value = dataView.getFloat64(index, true); - index = index + 8; + FLOAT_BYTES[0] = buffer[index++]; + FLOAT_BYTES[1] = buffer[index++]; + FLOAT_BYTES[2] = buffer[index++]; + FLOAT_BYTES[3] = buffer[index++]; + FLOAT_BYTES[4] = buffer[index++]; + FLOAT_BYTES[5] = buffer[index++]; + FLOAT_BYTES[6] = buffer[index++]; + FLOAT_BYTES[7] = buffer[index++]; + value = FLOAT[0]; + if (promoteValues === false) value = new Double(value); } else if (elementType === constants.BSON_DATA_DATE) { const lowBits = buffer[index++] | @@ -363,30 +367,43 @@ 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; + const lo = + buffer[index] + + buffer[index + 1] * 2 ** 8 + + buffer[index + 2] * 2 ** 16 + + buffer[index + 3] * 2 ** 24; + const hi = + buffer[index + 4] + + buffer[index + 5] * 2 ** 8 + + buffer[index + 6] * 2 ** 16 + + (buffer[index + 7] << 24); // Overflow + + /* eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true */ + value = (BigInt(hi) << BigInt(32)) + BigInt(lo); + index += 8; } else { - value = long; + // Unpack the low and high bits + 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); + // 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 diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index f5f562266..1d950b67b 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -72,9 +72,8 @@ function serializeString(buffer: Uint8Array, key: string, value: string, index: return index; } -const NUMBER_SPACE = new DataView(new ArrayBuffer(8), 0, 8); -const FOUR_BYTE_VIEW_ON_NUMBER = new Uint8Array(NUMBER_SPACE.buffer, 0, 4); -const EIGHT_BYTE_VIEW_ON_NUMBER = new Uint8Array(NUMBER_SPACE.buffer, 0, 8); +const FLOAT = new Float64Array(1); +const FLOAT_BYTES = new Uint8Array(FLOAT.buffer, 0, 8); function serializeNumber(buffer: Uint8Array, key: string, value: number, index: number) { const isNegativeZero = Object.is(value, -0); @@ -87,23 +86,32 @@ function serializeNumber(buffer: Uint8Array, key: string, value: number, index: ? constants.BSON_DATA_INT : constants.BSON_DATA_NUMBER; - if (type === constants.BSON_DATA_INT) { - NUMBER_SPACE.setInt32(0, value, true); - } else { - NUMBER_SPACE.setFloat64(0, value, true); - } - - const bytes = - type === constants.BSON_DATA_INT ? FOUR_BYTE_VIEW_ON_NUMBER : EIGHT_BYTE_VIEW_ON_NUMBER; - buffer[index++] = type; const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index); index = index + numberOfWrittenBytes; buffer[index++] = 0x00; - buffer.set(bytes, index); - index += bytes.byteLength; + if (type === constants.BSON_DATA_INT) { + let int32 = value; + buffer[index++] = int32; + int32 = int32 >>> 8; + buffer[index++] = int32; + int32 = int32 >>> 8; + buffer[index++] = int32; + int32 = int32 >>> 8; + buffer[index++] = int32; + } else { + FLOAT[0] = value; + buffer[index++] = FLOAT_BYTES[0]; + buffer[index++] = FLOAT_BYTES[1]; + buffer[index++] = FLOAT_BYTES[2]; + buffer[index++] = FLOAT_BYTES[3]; + buffer[index++] = FLOAT_BYTES[4]; + buffer[index++] = FLOAT_BYTES[5]; + buffer[index++] = FLOAT_BYTES[6]; + buffer[index++] = FLOAT_BYTES[7]; + } return index; } @@ -115,10 +123,29 @@ function serializeBigInt(buffer: Uint8Array, key: string, value: bigint, index: // Encode the name index += numberOfWrittenBytes; buffer[index++] = 0; - NUMBER_SPACE.setBigInt64(0, value, true); - // Write BigInt value - buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index); - index += EIGHT_BYTE_VIEW_ON_NUMBER.byteLength; + + /* eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true */ + const mask32bits = BigInt(0xffffffff); + + let lo = Number(value & mask32bits); + buffer[index++] = lo; + lo = lo >> 8; + buffer[index++] = lo; + lo = lo >> 8; + buffer[index++] = lo; + lo = lo >> 8; + buffer[index++] = lo; + + /* eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true */ + let hi = Number((value >> BigInt(32)) & mask32bits); + buffer[index++] = hi; + hi = hi >> 8; + buffer[index++] = hi; + hi = hi >> 8; + buffer[index++] = hi; + hi = hi >> 8; + buffer[index++] = hi; + return index; } @@ -401,11 +428,16 @@ function serializeDouble(buffer: Uint8Array, key: string, value: Double, index: buffer[index++] = 0; // Write float - NUMBER_SPACE.setFloat64(0, value.value, true); - buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index); + FLOAT[0] = value.value; + buffer[index++] = FLOAT_BYTES[0]; + buffer[index++] = FLOAT_BYTES[1]; + buffer[index++] = FLOAT_BYTES[2]; + buffer[index++] = FLOAT_BYTES[3]; + buffer[index++] = FLOAT_BYTES[4]; + buffer[index++] = FLOAT_BYTES[5]; + buffer[index++] = FLOAT_BYTES[6]; + buffer[index++] = FLOAT_BYTES[7]; - // Adjust index - index = index + 8; return index; } diff --git a/src/utils/byte_utils.ts b/src/utils/byte_utils.ts index 8cc7e56d5..e4c51dcf4 100644 --- a/src/utils/byte_utils.ts +++ b/src/utils/byte_utils.ts @@ -51,9 +51,3 @@ const hasGlobalBuffer = typeof Buffer === 'function' && Buffer.prototype?._isBuf * @internal */ export const ByteUtils: ByteUtils = hasGlobalBuffer ? nodeJsByteUtils : webByteUtils; - -export class BSONDataView extends DataView { - static fromUint8Array(input: Uint8Array) { - return new DataView(input.buffer, input.byteOffset, input.byteLength); - } -} diff --git a/src/utils/web_byte_utils.ts b/src/utils/web_byte_utils.ts index 374e3835a..62ae589ef 100644 --- a/src/utils/web_byte_utils.ts +++ b/src/utils/web_byte_utils.ts @@ -189,9 +189,9 @@ export const webByteUtils = { return new TextEncoder().encode(input).byteLength; }, - encodeUTF8Into(buffer: Uint8Array, source: string, byteOffset: number): number { + encodeUTF8Into(uint8array: Uint8Array, source: string, byteOffset: number): number { const bytes = new TextEncoder().encode(source); - buffer.set(bytes, byteOffset); + uint8array.set(bytes, byteOffset); return bytes.byteLength; }, diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 7c4d5c089..2f1240ec2 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -2,7 +2,6 @@ import { BSON, BSONError, EJSON, __noBigInt__ } from '../register-bson'; import { bufferFromHexArray } from './tools/utils'; import { expect } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; -import { BSONDataView } from '../../src/utils/byte_utils'; describe('BSON BigInt support', function () { beforeEach(function () { @@ -126,7 +125,11 @@ describe('BSON BigInt support', function () { const DATA_TYPE_OFFSET = 4; const KEY_OFFSET = 5; - const dataView = BSONDataView.fromUint8Array(serializedDoc); + const dataView = new DataView( + serializedDoc.buffer, + serializedDoc.byteOffset, + serializedDoc.byteLength + ); const keySlice = serializedDoc.slice(KEY_OFFSET); let keyLength = 0; @@ -407,7 +410,11 @@ describe('BSON BigInt support', function () { const serialized = BSON.serialize(number); const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serialized); + const dataView = new DataView( + serialized.buffer, + serialized.byteOffset, + serialized.byteLength + ); const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); const parsed = JSON.parse(stringified); @@ -431,7 +438,11 @@ describe('BSON BigInt support', function () { const serializedDoc = BSON.serialize(number); const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serializedDoc); + const dataView = new DataView( + serializedDoc.buffer, + serializedDoc.byteOffset, + serializedDoc.byteLength + ); const parsed = JSON.parse(stringified); expect(parsed).to.have.property('a');