Skip to content

Commit

Permalink
perf(NODE-5934): replace DataView uses with bit math
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken committed Feb 15, 2024
1 parent 9a150e1 commit 6e70471
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 72 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
27 changes: 22 additions & 5 deletions src/objectid.ts
Original file line number Diff line number Diff line change
@@ -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}$');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -275,9 +285,16 @@ 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);
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);
}
Expand Down
81 changes: 49 additions & 32 deletions src/parser/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -128,6 +128,9 @@ export function internalDeserialize(

const allowedDBRefKeys = /^\$ref$|^\$id$|^\$db$/;

const FLOAT_READ = new Float64Array(1);
const FLOAT_WRITE_BYTES = new Uint8Array(FLOAT_READ.buffer, 0, 8);

function deserializeObject(
buffer: Uint8Array,
index: number,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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_WRITE_BYTES[0] = buffer[index++];
FLOAT_WRITE_BYTES[1] = buffer[index++];
FLOAT_WRITE_BYTES[2] = buffer[index++];
FLOAT_WRITE_BYTES[3] = buffer[index++];
FLOAT_WRITE_BYTES[4] = buffer[index++];
FLOAT_WRITE_BYTES[5] = buffer[index++];
FLOAT_WRITE_BYTES[6] = buffer[index++];
FLOAT_WRITE_BYTES[7] = buffer[index++];
value = FLOAT_READ[0];
if (promoteValues === false) value = new Double(value);
} else if (elementType === constants.BSON_DATA_DATE) {
const lowBits =
buffer[index++] |
Expand Down Expand Up @@ -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
Expand Down
76 changes: 54 additions & 22 deletions src/parser/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_WRITE = new Float64Array(1);
const FLOAT_READ_BYTES = new Uint8Array(FLOAT_WRITE.buffer, 0, 8);

function serializeNumber(buffer: Uint8Array, key: string, value: number, index: number) {
const isNegativeZero = Object.is(value, -0);
Expand All @@ -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_WRITE[0] = value;
buffer[index++] = FLOAT_READ_BYTES[0];
buffer[index++] = FLOAT_READ_BYTES[1];
buffer[index++] = FLOAT_READ_BYTES[2];
buffer[index++] = FLOAT_READ_BYTES[3];
buffer[index++] = FLOAT_READ_BYTES[4];
buffer[index++] = FLOAT_READ_BYTES[5];
buffer[index++] = FLOAT_READ_BYTES[6];
buffer[index++] = FLOAT_READ_BYTES[7];
}

return index;
}
Expand All @@ -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(0xffff_ffff);

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;
}

Expand Down Expand Up @@ -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_WRITE[0] = value.value;
buffer[index++] = FLOAT_READ_BYTES[0];
buffer[index++] = FLOAT_READ_BYTES[1];
buffer[index++] = FLOAT_READ_BYTES[2];
buffer[index++] = FLOAT_READ_BYTES[3];
buffer[index++] = FLOAT_READ_BYTES[4];
buffer[index++] = FLOAT_READ_BYTES[5];
buffer[index++] = FLOAT_READ_BYTES[6];
buffer[index++] = FLOAT_READ_BYTES[7];

// Adjust index
index = index + 8;
return index;
}

Expand Down
6 changes: 0 additions & 6 deletions src/utils/byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 2 additions & 2 deletions src/utils/web_byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},

Expand Down
19 changes: 15 additions & 4 deletions test/node/bigint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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');
Expand Down

0 comments on commit 6e70471

Please sign in to comment.