Skip to content

Commit 6d343ab

Browse files
authored
perf(NODE-5934): replace DataView uses with bit math (#649)
1 parent 24d035e commit 6d343ab

11 files changed

+382
-245
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
"no-bigint-usage/no-bigint-literals": "error",
7676
"no-restricted-globals": [
7777
"error",
78-
"BigInt"
78+
"BigInt",
79+
"DataView"
7980
]
8081
},
8182
"overrides": [

src/bson.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { BSONRegExp } from './regexp';
1616
import { BSONSymbol } from './symbol';
1717
import { Timestamp } from './timestamp';
1818
import { ByteUtils } from './utils/byte_utils';
19+
import { NumberUtils } from './utils/number_utils';
1920
export type { UUIDExtended, BinaryExtended, BinaryExtendedLegacy, BinarySequence } from './binary';
2021
export type { CodeExtended } from './code';
2122
export type { DBRefLike } from './db_ref';
@@ -232,11 +233,7 @@ export function deserializeStream(
232233
// Loop over all documents
233234
for (let i = 0; i < numberOfDocuments; i++) {
234235
// Find size of the document
235-
const size =
236-
bufferData[index] |
237-
(bufferData[index + 1] << 8) |
238-
(bufferData[index + 2] << 16) |
239-
(bufferData[index + 3] << 24);
236+
const size = NumberUtils.getInt32LE(bufferData, index);
240237
// Update options with index
241238
internalOptions.index = index;
242239
// Parse the document at this point

src/objectid.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { BSONValue } from './bson_value';
22
import { BSONError } from './error';
33
import { type InspectFn, defaultInspect } from './parser/utils';
4-
import { BSONDataView, ByteUtils } from './utils/byte_utils';
4+
import { ByteUtils } from './utils/byte_utils';
5+
import { NumberUtils } from './utils/number_utils';
56

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

181182
// 4-byte timestamp
182-
BSONDataView.fromUint8Array(buffer).setUint32(0, time, false);
183+
NumberUtils.setInt32BE(buffer, 0, time);
183184

184185
// set PROCESS_UNIQUE if yet not initialized
185186
if (PROCESS_UNIQUE === null) {
@@ -259,7 +260,7 @@ export class ObjectId extends BSONValue {
259260
/** Returns the generation date (accurate up to the second) that this ID was generated. */
260261
getTimestamp(): Date {
261262
const timestamp = new Date();
262-
const time = BSONDataView.fromUint8Array(this.id).getUint32(0, false);
263+
const time = NumberUtils.getUint32BE(this.buffer, 0);
263264
timestamp.setTime(Math.floor(time) * 1000);
264265
return timestamp;
265266
}
@@ -292,9 +293,10 @@ export class ObjectId extends BSONValue {
292293
* @param time - an integer number representing a number of seconds.
293294
*/
294295
static createFromTime(time: number): ObjectId {
295-
const buffer = ByteUtils.fromNumberArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
296+
const buffer = ByteUtils.allocate(12);
297+
for (let i = 11; i >= 4; i--) buffer[i] = 0;
296298
// Encode time into first 4 bytes
297-
BSONDataView.fromUint8Array(buffer).setUint32(0, time, false);
299+
NumberUtils.setInt32BE(buffer, 0, time);
298300
// Return the new objectId
299301
return new ObjectId(buffer);
300302
}

src/parser/deserializer.ts

+59-132
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { ObjectId } from '../objectid';
1414
import { BSONRegExp } from '../regexp';
1515
import { BSONSymbol } from '../symbol';
1616
import { Timestamp } from '../timestamp';
17-
import { BSONDataView, ByteUtils } from '../utils/byte_utils';
17+
import { ByteUtils } from '../utils/byte_utils';
18+
import { NumberUtils } from '../utils/number_utils';
1819
import { validateUtf8 } from '../validate_utf8';
1920

2021
/** @public */
@@ -91,11 +92,7 @@ export function internalDeserialize(
9192
options = options == null ? {} : options;
9293
const index = options && options.index ? options.index : 0;
9394
// Read the document size
94-
const size =
95-
buffer[index] |
96-
(buffer[index + 1] << 8) |
97-
(buffer[index + 2] << 16) |
98-
(buffer[index + 3] << 24);
95+
const size = NumberUtils.getInt32LE(buffer, index);
9996

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

206203
// Read the document size
207-
const size =
208-
buffer[index++] | (buffer[index++] << 8) | (buffer[index++] << 16) | (buffer[index++] << 24);
204+
const size = NumberUtils.getInt32LE(buffer, index);
205+
index += 4;
209206

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

219216
let isPossibleDBRef = isArray ? false : null;
220217

221-
let dataView;
222-
223218
// While we have more left data left keep parsing
224219
while (!done) {
225220
// Read the type
@@ -257,11 +252,8 @@ function deserializeObject(
257252
index = i + 1;
258253

259254
if (elementType === constants.BSON_DATA_STRING) {
260-
const stringSize =
261-
buffer[index++] |
262-
(buffer[index++] << 8) |
263-
(buffer[index++] << 16) |
264-
(buffer[index++] << 24);
255+
const stringSize = NumberUtils.getInt32LE(buffer, index);
256+
index += 4;
265257
if (
266258
stringSize <= 0 ||
267259
stringSize > buffer.length - index ||
@@ -277,34 +269,19 @@ function deserializeObject(
277269
value = new ObjectId(oid);
278270
index = index + 12;
279271
} else if (elementType === constants.BSON_DATA_INT && promoteValues === false) {
280-
value = new Int32(
281-
buffer[index++] | (buffer[index++] << 8) | (buffer[index++] << 16) | (buffer[index++] << 24)
282-
);
272+
value = new Int32(NumberUtils.getInt32LE(buffer, index));
273+
index += 4;
283274
} else if (elementType === constants.BSON_DATA_INT) {
284-
value =
285-
buffer[index++] |
286-
(buffer[index++] << 8) |
287-
(buffer[index++] << 16) |
288-
(buffer[index++] << 24);
289-
} else if (elementType === constants.BSON_DATA_NUMBER && promoteValues === false) {
290-
dataView ??= new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
291-
value = new Double(dataView.getFloat64(index, true));
292-
index = index + 8;
275+
value = NumberUtils.getInt32LE(buffer, index);
276+
index += 4;
293277
} else if (elementType === constants.BSON_DATA_NUMBER) {
294-
dataView ??= new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
295-
value = dataView.getFloat64(index, true);
296-
index = index + 8;
278+
value = NumberUtils.getFloat64LE(buffer, index);
279+
index += 8;
280+
if (promoteValues === false) value = new Double(value);
297281
} else if (elementType === constants.BSON_DATA_DATE) {
298-
const lowBits =
299-
buffer[index++] |
300-
(buffer[index++] << 8) |
301-
(buffer[index++] << 16) |
302-
(buffer[index++] << 24);
303-
const highBits =
304-
buffer[index++] |
305-
(buffer[index++] << 8) |
306-
(buffer[index++] << 16) |
307-
(buffer[index++] << 24);
282+
const lowBits = NumberUtils.getInt32LE(buffer, index);
283+
const highBits = NumberUtils.getInt32LE(buffer, index + 4);
284+
index += 8;
308285

309286
value = new Date(new Long(lowBits, highBits).toNumber());
310287
} else if (elementType === constants.BSON_DATA_BOOLEAN) {
@@ -313,11 +290,8 @@ function deserializeObject(
313290
value = buffer[index++] === 1;
314291
} else if (elementType === constants.BSON_DATA_OBJECT) {
315292
const _index = index;
316-
const objectSize =
317-
buffer[index] |
318-
(buffer[index + 1] << 8) |
319-
(buffer[index + 2] << 16) |
320-
(buffer[index + 3] << 24);
293+
const objectSize = NumberUtils.getInt32LE(buffer, index);
294+
321295
if (objectSize <= 0 || objectSize > buffer.length - index)
322296
throw new BSONError('bad embedded document length in bson');
323297

@@ -335,11 +309,7 @@ function deserializeObject(
335309
index = index + objectSize;
336310
} else if (elementType === constants.BSON_DATA_ARRAY) {
337311
const _index = index;
338-
const objectSize =
339-
buffer[index] |
340-
(buffer[index + 1] << 8) |
341-
(buffer[index + 2] << 16) |
342-
(buffer[index + 3] << 24);
312+
const objectSize = NumberUtils.getInt32LE(buffer, index);
343313
let arrayOptions: DeserializeOptions = options;
344314

345315
// Stop index
@@ -363,30 +333,25 @@ function deserializeObject(
363333
} else if (elementType === constants.BSON_DATA_NULL) {
364334
value = null;
365335
} else if (elementType === constants.BSON_DATA_LONG) {
366-
// Unpack the low and high bits
367-
const dataview = BSONDataView.fromUint8Array(buffer.subarray(index, index + 8));
368-
369-
const lowBits =
370-
buffer[index++] |
371-
(buffer[index++] << 8) |
372-
(buffer[index++] << 16) |
373-
(buffer[index++] << 24);
374-
const highBits =
375-
buffer[index++] |
376-
(buffer[index++] << 8) |
377-
(buffer[index++] << 16) |
378-
(buffer[index++] << 24);
379-
const long = new Long(lowBits, highBits);
380336
if (useBigInt64) {
381-
value = dataview.getBigInt64(0, true);
382-
} else if (promoteLongs && promoteValues === true) {
383-
// Promote the long if possible
384-
value =
385-
long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG)
386-
? long.toNumber()
387-
: long;
337+
value = NumberUtils.getBigInt64LE(buffer, index);
338+
index += 8;
388339
} else {
389-
value = long;
340+
// Unpack the low and high bits
341+
const lowBits = NumberUtils.getInt32LE(buffer, index);
342+
const highBits = NumberUtils.getInt32LE(buffer, index + 4);
343+
index += 8;
344+
345+
const long = new Long(lowBits, highBits);
346+
// Promote the long if possible
347+
if (promoteLongs && promoteValues === true) {
348+
value =
349+
long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG)
350+
? long.toNumber()
351+
: long;
352+
} else {
353+
value = long;
354+
}
390355
}
391356
} else if (elementType === constants.BSON_DATA_DECIMAL128) {
392357
// Buffer to contain the decimal bytes
@@ -398,11 +363,8 @@ function deserializeObject(
398363
// Assign the new Decimal128 value
399364
value = new Decimal128(bytes);
400365
} else if (elementType === constants.BSON_DATA_BINARY) {
401-
let binarySize =
402-
buffer[index++] |
403-
(buffer[index++] << 8) |
404-
(buffer[index++] << 16) |
405-
(buffer[index++] << 24);
366+
let binarySize = NumberUtils.getInt32LE(buffer, index);
367+
index += 4;
406368
const totalBinarySize = binarySize;
407369
const subType = buffer[index++];
408370

@@ -417,11 +379,8 @@ function deserializeObject(
417379
if (buffer['slice'] != null) {
418380
// If we have subtype 2 skip the 4 bytes for the size
419381
if (subType === Binary.SUBTYPE_BYTE_ARRAY) {
420-
binarySize =
421-
buffer[index++] |
422-
(buffer[index++] << 8) |
423-
(buffer[index++] << 16) |
424-
(buffer[index++] << 24);
382+
binarySize = NumberUtils.getInt32LE(buffer, index);
383+
index += 4;
425384
if (binarySize < 0)
426385
throw new BSONError('Negative binary type element size found for subtype 0x02');
427386
if (binarySize > totalBinarySize - 4)
@@ -442,11 +401,8 @@ function deserializeObject(
442401
const _buffer = ByteUtils.allocate(binarySize);
443402
// If we have subtype 2 skip the 4 bytes for the size
444403
if (subType === Binary.SUBTYPE_BYTE_ARRAY) {
445-
binarySize =
446-
buffer[index++] |
447-
(buffer[index++] << 8) |
448-
(buffer[index++] << 16) |
449-
(buffer[index++] << 24);
404+
binarySize = NumberUtils.getInt32LE(buffer, index);
405+
index += 4;
450406
if (binarySize < 0)
451407
throw new BSONError('Negative binary type element size found for subtype 0x02');
452408
if (binarySize > totalBinarySize - 4)
@@ -545,11 +501,8 @@ function deserializeObject(
545501
// Set the object
546502
value = new BSONRegExp(source, regExpOptions);
547503
} else if (elementType === constants.BSON_DATA_SYMBOL) {
548-
const stringSize =
549-
buffer[index++] |
550-
(buffer[index++] << 8) |
551-
(buffer[index++] << 16) |
552-
(buffer[index++] << 24);
504+
const stringSize = NumberUtils.getInt32LE(buffer, index);
505+
index += 4;
553506
if (
554507
stringSize <= 0 ||
555508
stringSize > buffer.length - index ||
@@ -561,31 +514,18 @@ function deserializeObject(
561514
value = promoteValues ? symbol : new BSONSymbol(symbol);
562515
index = index + stringSize;
563516
} else if (elementType === constants.BSON_DATA_TIMESTAMP) {
564-
// We intentionally **do not** use bit shifting here
565-
// Bit shifting in javascript coerces numbers to **signed** int32s
566-
// We need to keep i, and t unsigned
567-
const i =
568-
buffer[index++] +
569-
buffer[index++] * (1 << 8) +
570-
buffer[index++] * (1 << 16) +
571-
buffer[index++] * (1 << 24);
572-
const t =
573-
buffer[index++] +
574-
buffer[index++] * (1 << 8) +
575-
buffer[index++] * (1 << 16) +
576-
buffer[index++] * (1 << 24);
577-
578-
value = new Timestamp({ i, t });
517+
value = new Timestamp({
518+
i: NumberUtils.getUint32LE(buffer, index),
519+
t: NumberUtils.getUint32LE(buffer, index + 4)
520+
});
521+
index += 8;
579522
} else if (elementType === constants.BSON_DATA_MIN_KEY) {
580523
value = new MinKey();
581524
} else if (elementType === constants.BSON_DATA_MAX_KEY) {
582525
value = new MaxKey();
583526
} else if (elementType === constants.BSON_DATA_CODE) {
584-
const stringSize =
585-
buffer[index++] |
586-
(buffer[index++] << 8) |
587-
(buffer[index++] << 16) |
588-
(buffer[index++] << 24);
527+
const stringSize = NumberUtils.getInt32LE(buffer, index);
528+
index += 4;
589529
if (
590530
stringSize <= 0 ||
591531
stringSize > buffer.length - index ||
@@ -605,23 +545,17 @@ function deserializeObject(
605545
// Update parse index position
606546
index = index + stringSize;
607547
} else if (elementType === constants.BSON_DATA_CODE_W_SCOPE) {
608-
const totalSize =
609-
buffer[index++] |
610-
(buffer[index++] << 8) |
611-
(buffer[index++] << 16) |
612-
(buffer[index++] << 24);
548+
const totalSize = NumberUtils.getInt32LE(buffer, index);
549+
index += 4;
613550

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

619556
// Get the code string size
620-
const stringSize =
621-
buffer[index++] |
622-
(buffer[index++] << 8) |
623-
(buffer[index++] << 16) |
624-
(buffer[index++] << 24);
557+
const stringSize = NumberUtils.getInt32LE(buffer, index);
558+
index += 4;
625559
// Check if we have a valid string
626560
if (
627561
stringSize <= 0 ||
@@ -643,11 +577,7 @@ function deserializeObject(
643577
// Parse the element
644578
const _index = index;
645579
// Decode the size of the object document
646-
const objectSize =
647-
buffer[index] |
648-
(buffer[index + 1] << 8) |
649-
(buffer[index + 2] << 16) |
650-
(buffer[index + 3] << 24);
580+
const objectSize = NumberUtils.getInt32LE(buffer, index);
651581
// Decode the scope object
652582
const scopeObject = deserializeObject(buffer, _index, options, false);
653583
// Adjust the index
@@ -666,11 +596,8 @@ function deserializeObject(
666596
value = new Code(functionString, scopeObject);
667597
} else if (elementType === constants.BSON_DATA_DBPOINTER) {
668598
// Get the code string size
669-
const stringSize =
670-
buffer[index++] |
671-
(buffer[index++] << 8) |
672-
(buffer[index++] << 16) |
673-
(buffer[index++] << 24);
599+
const stringSize = NumberUtils.getInt32LE(buffer, index);
600+
index += 4;
674601
// Check if we have a valid string
675602
if (
676603
stringSize <= 0 ||

0 commit comments

Comments
 (0)