Skip to content

Commit 63eafcb

Browse files
perf(NODE-6126): improve Long.fromBigInt performance (#681)
1 parent 8f3eec5 commit 63eafcb

File tree

4 files changed

+114
-33
lines changed

4 files changed

+114
-33
lines changed

src/long.ts

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -119,42 +119,57 @@ export class Long extends BSONValue {
119119
/**
120120
* The high 32 bits as a signed value.
121121
*/
122-
high!: number;
122+
high: number;
123123

124124
/**
125125
* The low 32 bits as a signed value.
126126
*/
127-
low!: number;
127+
low: number;
128128

129129
/**
130130
* Whether unsigned or not.
131131
*/
132-
unsigned!: boolean;
132+
unsigned: boolean;
133133

134134
/**
135135
* Constructs a 64 bit two's-complement integer, given its low and high 32 bit values as *signed* integers.
136-
* See the from* functions below for more convenient ways of constructing Longs.
137-
*
138-
* Acceptable signatures are:
139-
* - Long(low, high, unsigned?)
140-
* - Long(bigint, unsigned?)
141-
* - Long(string, unsigned?)
142136
*
143137
* @param low - The low (signed) 32 bits of the long
144138
* @param high - The high (signed) 32 bits of the long
145139
* @param unsigned - Whether unsigned or not, defaults to signed
146140
*/
147-
constructor(low: number | bigint | string = 0, high?: number | boolean, unsigned?: boolean) {
141+
constructor(low: number, high?: number, unsigned?: boolean);
142+
/**
143+
* Constructs a 64 bit two's-complement integer, given a bigint representation.
144+
*
145+
* @param value - BigInt representation of the long value
146+
* @param unsigned - Whether unsigned or not, defaults to signed
147+
*/
148+
constructor(value: bigint, unsigned?: boolean);
149+
/**
150+
* Constructs a 64 bit two's-complement integer, given a string representation.
151+
*
152+
* @param value - String representation of the long value
153+
* @param unsigned - Whether unsigned or not, defaults to signed
154+
*/
155+
constructor(value: string, unsigned?: boolean);
156+
constructor(
157+
lowOrValue: number | bigint | string = 0,
158+
highOrUnsigned?: number | boolean,
159+
unsigned?: boolean
160+
) {
148161
super();
149-
if (typeof low === 'bigint') {
150-
Object.assign(this, Long.fromBigInt(low, !!high));
151-
} else if (typeof low === 'string') {
152-
Object.assign(this, Long.fromString(low, !!high));
153-
} else {
154-
this.low = low | 0;
155-
this.high = (high as number) | 0;
156-
this.unsigned = !!unsigned;
157-
}
162+
const unsignedBool = typeof highOrUnsigned === 'boolean' ? highOrUnsigned : Boolean(unsigned);
163+
const high = typeof highOrUnsigned === 'number' ? highOrUnsigned : 0;
164+
const res =
165+
typeof lowOrValue === 'string'
166+
? Long.fromString(lowOrValue, unsignedBool)
167+
: typeof lowOrValue === 'bigint'
168+
? Long.fromBigInt(lowOrValue, unsignedBool)
169+
: { low: lowOrValue | 0, high: high | 0, unsigned: unsignedBool };
170+
this.low = res.low;
171+
this.high = res.high;
172+
this.unsigned = res.unsigned;
158173
}
159174

160175
static TWO_PWR_24 = Long.fromInt(TWO_PWR_24_DBL);
@@ -243,7 +258,15 @@ export class Long extends BSONValue {
243258
* @returns The corresponding Long value
244259
*/
245260
static fromBigInt(value: bigint, unsigned?: boolean): Long {
246-
return Long.fromString(value.toString(), unsigned);
261+
// eslint-disable-next-line no-restricted-globals
262+
const FROM_BIGINT_BIT_MASK = BigInt(0xffffffff);
263+
// eslint-disable-next-line no-restricted-globals
264+
const FROM_BIGINT_BIT_SHIFT = BigInt(32);
265+
return new Long(
266+
Number(value & FROM_BIGINT_BIT_MASK),
267+
Number((value >> FROM_BIGINT_BIT_SHIFT) & FROM_BIGINT_BIT_MASK),
268+
unsigned
269+
);
247270
}
248271

249272
/**

test/node/bson_type_classes.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'chai';
22
import { inspect } from 'node:util';
3-
import { __isWeb__ } from '../register-bson';
3+
import { __isWeb__, __noBigInt__ } from '../register-bson';
44
import {
55
Binary,
66
BSONRegExp,
@@ -44,7 +44,7 @@ const BSONTypeClassCtors = new Map<string, () => BSONValue>([
4444
['Decimal128', () => new Decimal128('1.23')],
4545
['Double', () => new Double(1.23)],
4646
['Int32', () => new Int32(1)],
47-
['Long', () => new Long(1n)],
47+
['Long', () => (__noBigInt__ ? new Long(1) : new Long(1n))],
4848
['MinKey', () => new MinKey()],
4949
['MaxKey', () => new MaxKey()],
5050
['ObjectId', () => new ObjectId('00'.repeat(12))],

test/node/long.test.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect } from 'chai';
22
import { Long, BSONError, __noBigInt__ } from '../register-bson';
3+
import { BSON_INT32_MAX, BSON_INT32_MIN } from '../../src/constants';
34

45
describe('Long', function () {
56
it('accepts strings in the constructor', function () {
@@ -16,14 +17,15 @@ describe('Long', function () {
1617
it('accepts BigInts in Long constructor', function () {
1718
if (__noBigInt__) {
1819
this.currentTest?.skip();
20+
} else {
21+
expect(new Long(0n).toString()).to.equal('0');
22+
expect(new Long(-1n).toString()).to.equal('-1');
23+
expect(new Long(-1n, true).toString()).to.equal('18446744073709551615');
24+
expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789');
25+
expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789');
26+
expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904');
27+
expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712');
1928
}
20-
expect(new Long(0n).toString()).to.equal('0');
21-
expect(new Long(-1n).toString()).to.equal('-1');
22-
expect(new Long(-1n, true).toString()).to.equal('18446744073709551615');
23-
expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789');
24-
expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789');
25-
expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904');
26-
expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712');
2729
});
2830

2931
describe('static fromExtendedJSON()', function () {
@@ -164,6 +166,52 @@ describe('Long', function () {
164166
});
165167
});
166168

169+
describe('static fromBigInt()', function () {
170+
const inputs: [
171+
name: string,
172+
input: bigint,
173+
unsigned: boolean | undefined,
174+
expectedLong?: Long
175+
][] = [
176+
['0', BigInt('0'), false, Long.ZERO],
177+
['-0 (bigint coerces this to 0)', BigInt('-0'), false, Long.ZERO],
178+
[
179+
'max unsigned input',
180+
BigInt(Long.MAX_UNSIGNED_VALUE.toString(10)),
181+
true,
182+
Long.MAX_UNSIGNED_VALUE
183+
],
184+
['max signed input', BigInt(Long.MAX_VALUE.toString(10)), false, Long.MAX_VALUE],
185+
['min signed input', BigInt(Long.MIN_VALUE.toString(10)), false, Long.MIN_VALUE],
186+
[
187+
'negative greater than 32 bits',
188+
BigInt(-9228915101),
189+
false,
190+
Long.fromBits(0xd9e9ee63, 0xfffffffd)
191+
],
192+
['less than 32 bits', BigInt(245666), false, new Long(245666)],
193+
['unsigned less than 32 bits', BigInt(245666), true, new Long(245666, true)],
194+
['negative less than 32 bits', BigInt(-245666), false, new Long(-245666, -1)],
195+
['max int32', BigInt(BSON_INT32_MAX), false, new Long(BSON_INT32_MAX)],
196+
['max int32 unsigned', BigInt(BSON_INT32_MAX), true, new Long(BSON_INT32_MAX, 0, true)],
197+
['min int32', BigInt(BSON_INT32_MIN), false, new Long(BSON_INT32_MIN, -1)]
198+
];
199+
200+
beforeEach(function () {
201+
if (__noBigInt__) {
202+
this.currentTest?.skip();
203+
}
204+
});
205+
206+
for (const [testName, num, unsigned, expectedLong] of inputs) {
207+
context(`when the input is ${testName}`, () => {
208+
it(`should return a Long representation of the input`, () => {
209+
expect(Long.fromBigInt(num, unsigned)).to.deep.equal(expectedLong);
210+
});
211+
});
212+
}
213+
});
214+
167215
describe('static fromString()', function () {
168216
const successInputs: [
169217
name: string,

test/node/timestamp.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'chai';
22
import * as BSON from '../register-bson';
3-
import { Timestamp } from '../register-bson';
3+
import { Timestamp, __noBigInt__ } from '../register-bson';
44

55
describe('Timestamp', () => {
66
describe('static MAX_VALUE', () => {
@@ -10,11 +10,14 @@ describe('Timestamp', () => {
1010
});
1111

1212
it('should always be an unsigned value', () => {
13+
let bigIntInputs: Timestamp[] = [];
14+
if (!__noBigInt__) {
15+
bigIntInputs = [new BSON.Timestamp(0xffffffffffn), new BSON.Timestamp(0xffffffffffffffffn)];
16+
}
1317
const table = [
1418
// @ts-expect-error: Not advertized by the types, but constructs a 0 timestamp
1519
new BSON.Timestamp(),
16-
new BSON.Timestamp(0xffffffffffn),
17-
new BSON.Timestamp(0xffffffffffffffffn),
20+
...bigIntInputs,
1821
new BSON.Timestamp(new BSON.Long(0xffff_ffff, 0xffff_ffff, false)),
1922
new BSON.Timestamp(new BSON.Long(0xffff_ffff, 0xffff_ffff, true)),
2023
new BSON.Timestamp({ t: 0xffff_ffff, i: 0xffff_ffff }),
@@ -29,22 +32,29 @@ describe('Timestamp', () => {
2932
});
3033

3134
context('output formats', () => {
32-
const timestamp = new BSON.Timestamp(0xffffffffffffffffn);
35+
beforeEach(function () {
36+
if (__noBigInt__) {
37+
this.currentTest?.skip();
38+
}
39+
});
3340

3441
context('when converting toString', () => {
3542
it('exports an unsigned number', () => {
43+
const timestamp = new BSON.Timestamp(0xffffffffffffffffn);
3644
expect(timestamp.toString()).to.equal('18446744073709551615');
3745
});
3846
});
3947

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

4655
context('when converting toExtendedJSON', () => {
4756
it('exports an unsigned number', () => {
57+
const timestamp = new BSON.Timestamp(0xffffffffffffffffn);
4858
expect(timestamp.toExtendedJSON()).to.deep.equal({
4959
$timestamp: { t: 4294967295, i: 4294967295 }
5060
});

0 commit comments

Comments
 (0)