Skip to content

Commit

Permalink
perf(NODE-6126): improve Long.fromBigInt performance (mongodb#681)
Browse files Browse the repository at this point in the history
  • Loading branch information
aditi-khare-mongoDB authored May 7, 2024
1 parent 8f3eec5 commit 63eafcb
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 33 deletions.
63 changes: 43 additions & 20 deletions src/long.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,42 +119,57 @@ export class Long extends BSONValue {
/**
* The high 32 bits as a signed value.
*/
high!: number;
high: number;

/**
* The low 32 bits as a signed value.
*/
low!: number;
low: number;

/**
* Whether unsigned or not.
*/
unsigned!: boolean;
unsigned: boolean;

/**
* Constructs a 64 bit two's-complement integer, given its low and high 32 bit values as *signed* integers.
* See the from* functions below for more convenient ways of constructing Longs.
*
* Acceptable signatures are:
* - Long(low, high, unsigned?)
* - Long(bigint, unsigned?)
* - Long(string, unsigned?)
*
* @param low - The low (signed) 32 bits of the long
* @param high - The high (signed) 32 bits of the long
* @param unsigned - Whether unsigned or not, defaults to signed
*/
constructor(low: number | bigint | string = 0, high?: number | boolean, unsigned?: boolean) {
constructor(low: number, high?: number, unsigned?: boolean);
/**
* Constructs a 64 bit two's-complement integer, given a bigint representation.
*
* @param value - BigInt representation of the long value
* @param unsigned - Whether unsigned or not, defaults to signed
*/
constructor(value: bigint, unsigned?: boolean);
/**
* Constructs a 64 bit two's-complement integer, given a string representation.
*
* @param value - String representation of the long value
* @param unsigned - Whether unsigned or not, defaults to signed
*/
constructor(value: string, unsigned?: boolean);
constructor(
lowOrValue: number | bigint | string = 0,
highOrUnsigned?: number | boolean,
unsigned?: boolean
) {
super();
if (typeof low === 'bigint') {
Object.assign(this, Long.fromBigInt(low, !!high));
} else if (typeof low === 'string') {
Object.assign(this, Long.fromString(low, !!high));
} else {
this.low = low | 0;
this.high = (high as number) | 0;
this.unsigned = !!unsigned;
}
const unsignedBool = typeof highOrUnsigned === 'boolean' ? highOrUnsigned : Boolean(unsigned);
const high = typeof highOrUnsigned === 'number' ? highOrUnsigned : 0;
const res =
typeof lowOrValue === 'string'
? Long.fromString(lowOrValue, unsignedBool)
: typeof lowOrValue === 'bigint'
? Long.fromBigInt(lowOrValue, unsignedBool)
: { low: lowOrValue | 0, high: high | 0, unsigned: unsignedBool };
this.low = res.low;
this.high = res.high;
this.unsigned = res.unsigned;
}

static TWO_PWR_24 = Long.fromInt(TWO_PWR_24_DBL);
Expand Down Expand Up @@ -243,7 +258,15 @@ export class Long extends BSONValue {
* @returns The corresponding Long value
*/
static fromBigInt(value: bigint, unsigned?: boolean): Long {
return Long.fromString(value.toString(), unsigned);
// eslint-disable-next-line no-restricted-globals
const FROM_BIGINT_BIT_MASK = BigInt(0xffffffff);
// eslint-disable-next-line no-restricted-globals
const FROM_BIGINT_BIT_SHIFT = BigInt(32);
return new Long(
Number(value & FROM_BIGINT_BIT_MASK),
Number((value >> FROM_BIGINT_BIT_SHIFT) & FROM_BIGINT_BIT_MASK),
unsigned
);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions test/node/bson_type_classes.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'chai';
import { inspect } from 'node:util';
import { __isWeb__ } from '../register-bson';
import { __isWeb__, __noBigInt__ } from '../register-bson';
import {
Binary,
BSONRegExp,
Expand Down Expand Up @@ -44,7 +44,7 @@ const BSONTypeClassCtors = new Map<string, () => BSONValue>([
['Decimal128', () => new Decimal128('1.23')],
['Double', () => new Double(1.23)],
['Int32', () => new Int32(1)],
['Long', () => new Long(1n)],
['Long', () => (__noBigInt__ ? new Long(1) : new Long(1n))],
['MinKey', () => new MinKey()],
['MaxKey', () => new MaxKey()],
['ObjectId', () => new ObjectId('00'.repeat(12))],
Expand Down
62 changes: 55 additions & 7 deletions test/node/long.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from 'chai';
import { Long, BSONError, __noBigInt__ } from '../register-bson';
import { BSON_INT32_MAX, BSON_INT32_MIN } from '../../src/constants';

describe('Long', function () {
it('accepts strings in the constructor', function () {
Expand All @@ -16,14 +17,15 @@ describe('Long', function () {
it('accepts BigInts in Long constructor', function () {
if (__noBigInt__) {
this.currentTest?.skip();
} else {
expect(new Long(0n).toString()).to.equal('0');
expect(new Long(-1n).toString()).to.equal('-1');
expect(new Long(-1n, true).toString()).to.equal('18446744073709551615');
expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789');
expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789');
expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904');
expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712');
}
expect(new Long(0n).toString()).to.equal('0');
expect(new Long(-1n).toString()).to.equal('-1');
expect(new Long(-1n, true).toString()).to.equal('18446744073709551615');
expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789');
expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789');
expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904');
expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712');
});

describe('static fromExtendedJSON()', function () {
Expand Down Expand Up @@ -164,6 +166,52 @@ describe('Long', function () {
});
});

describe('static fromBigInt()', function () {
const inputs: [
name: string,
input: bigint,
unsigned: boolean | undefined,
expectedLong?: Long
][] = [
['0', BigInt('0'), false, Long.ZERO],
['-0 (bigint coerces this to 0)', BigInt('-0'), false, Long.ZERO],
[
'max unsigned input',
BigInt(Long.MAX_UNSIGNED_VALUE.toString(10)),
true,
Long.MAX_UNSIGNED_VALUE
],
['max signed input', BigInt(Long.MAX_VALUE.toString(10)), false, Long.MAX_VALUE],
['min signed input', BigInt(Long.MIN_VALUE.toString(10)), false, Long.MIN_VALUE],
[
'negative greater than 32 bits',
BigInt(-9228915101),
false,
Long.fromBits(0xd9e9ee63, 0xfffffffd)
],
['less than 32 bits', BigInt(245666), false, new Long(245666)],
['unsigned less than 32 bits', BigInt(245666), true, new Long(245666, true)],
['negative less than 32 bits', BigInt(-245666), false, new Long(-245666, -1)],
['max int32', BigInt(BSON_INT32_MAX), false, new Long(BSON_INT32_MAX)],
['max int32 unsigned', BigInt(BSON_INT32_MAX), true, new Long(BSON_INT32_MAX, 0, true)],
['min int32', BigInt(BSON_INT32_MIN), false, new Long(BSON_INT32_MIN, -1)]
];

beforeEach(function () {
if (__noBigInt__) {
this.currentTest?.skip();
}
});

for (const [testName, num, unsigned, expectedLong] of inputs) {
context(`when the input is ${testName}`, () => {
it(`should return a Long representation of the input`, () => {
expect(Long.fromBigInt(num, unsigned)).to.deep.equal(expectedLong);
});
});
}
});

describe('static fromString()', function () {
const successInputs: [
name: string,
Expand Down
18 changes: 14 additions & 4 deletions test/node/timestamp.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'chai';
import * as BSON from '../register-bson';
import { Timestamp } from '../register-bson';
import { Timestamp, __noBigInt__ } from '../register-bson';

describe('Timestamp', () => {
describe('static MAX_VALUE', () => {
Expand All @@ -10,11 +10,14 @@ describe('Timestamp', () => {
});

it('should always be an unsigned value', () => {
let bigIntInputs: Timestamp[] = [];
if (!__noBigInt__) {
bigIntInputs = [new BSON.Timestamp(0xffffffffffn), new BSON.Timestamp(0xffffffffffffffffn)];
}
const table = [
// @ts-expect-error: Not advertized by the types, but constructs a 0 timestamp
new BSON.Timestamp(),
new BSON.Timestamp(0xffffffffffn),
new BSON.Timestamp(0xffffffffffffffffn),
...bigIntInputs,
new BSON.Timestamp(new BSON.Long(0xffff_ffff, 0xffff_ffff, false)),
new BSON.Timestamp(new BSON.Long(0xffff_ffff, 0xffff_ffff, true)),
new BSON.Timestamp({ t: 0xffff_ffff, i: 0xffff_ffff }),
Expand All @@ -29,22 +32,29 @@ describe('Timestamp', () => {
});

context('output formats', () => {
const timestamp = new BSON.Timestamp(0xffffffffffffffffn);
beforeEach(function () {
if (__noBigInt__) {
this.currentTest?.skip();
}
});

context('when converting toString', () => {
it('exports an unsigned number', () => {
const timestamp = new BSON.Timestamp(0xffffffffffffffffn);
expect(timestamp.toString()).to.equal('18446744073709551615');
});
});

context('when converting toJSON', () => {
it('exports an unsigned number', () => {
const timestamp = new BSON.Timestamp(0xffffffffffffffffn);
expect(timestamp.toJSON()).to.deep.equal({ $timestamp: '18446744073709551615' });
});
});

context('when converting toExtendedJSON', () => {
it('exports an unsigned number', () => {
const timestamp = new BSON.Timestamp(0xffffffffffffffffn);
expect(timestamp.toExtendedJSON()).to.deep.equal({
$timestamp: { t: 4294967295, i: 4294967295 }
});
Expand Down

0 comments on commit 63eafcb

Please sign in to comment.