Skip to content
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

perf(NODE-6126): improve Long.fromBigInt performance #681

Merged
merged 9 commits into from
May 7, 2024
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