Skip to content

Commit be5fe3e

Browse files
authored
perf(NODE-4194): improve objectId constructor performance (#498)
1 parent b86cabf commit be5fe3e

File tree

6 files changed

+183
-71
lines changed

6 files changed

+183
-71
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ coverage/
2424
*.d.ts
2525
*.tgz
2626
docs/public
27+
28+
*.0x

etc/benchmarks/main.mjs

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
import { performance } from 'perf_hooks';
3+
import { readFile } from 'fs/promises';
4+
import { cpus, totalmem } from 'os';
5+
6+
const hw = cpus();
7+
const ram = totalmem() / 1024 ** 3;
8+
const platform = { name: hw[0].model, cores: hw.length, ram: `${ram}GB` };
9+
const ITERATIONS = 1_000_000;
10+
11+
const systemInfo = [
12+
`\n- cpu: ${platform.name}`,
13+
`- cores: ${platform.cores}`,
14+
`- os: ${process.platform}`,
15+
`- ram: ${platform.ram}`,
16+
`- iterations: ${ITERATIONS.toLocaleString()}`
17+
].join('\n');
18+
19+
const readJSONFile = async path =>
20+
JSON.parse(await readFile(new URL(path, import.meta.url), { encoding: 'utf8' }));
21+
22+
function average(array) {
23+
let sum = 0;
24+
for (const value of array) sum += value;
25+
return sum / array.length;
26+
}
27+
28+
function testPerformance(fn, iterations = ITERATIONS) {
29+
let measurements = [];
30+
for (let i = 0; i < iterations; i++) {
31+
const start = performance.now();
32+
fn(i);
33+
const end = performance.now();
34+
measurements.push(end - start);
35+
}
36+
return average(measurements).toFixed(8);
37+
}
38+
39+
async function main() {
40+
const [currentBSON, currentReleaseBSON, legacyBSONLib] = await Promise.all([
41+
(async () => ({
42+
lib: await import('../../lib/bson.js'),
43+
version: 'current local'
44+
}))(),
45+
(async () => ({
46+
lib: await import('../../node_modules/bson_latest/lib/bson.js'),
47+
version: (await readJSONFile('../../node_modules/bson_latest/package.json')).version
48+
}))(),
49+
(async () => {
50+
const legacyBSON = (await import('../../node_modules/bson_legacy/index.js')).default;
51+
return {
52+
lib: { ...legacyBSON, ...legacyBSON.prototype },
53+
version: (await readJSONFile('../../node_modules/bson_legacy/package.json')).version
54+
};
55+
})()
56+
]).catch(error => {
57+
console.error(error);
58+
console.error(
59+
`Please run:\n${[
60+
'npm run build',
61+
'npm install --no-save bson_legacy@npm:bson@1 bson_latest@npm:bson@latest'
62+
].join('\n')}`
63+
);
64+
process.exit(1);
65+
});
66+
67+
const documents = Array.from({ length: ITERATIONS }, () =>
68+
currentReleaseBSON.lib.serialize({
69+
_id: new currentReleaseBSON.lib.ObjectId(),
70+
field1: 'value1'
71+
})
72+
);
73+
74+
console.log(systemInfo);
75+
76+
for (const bson of [currentBSON, currentReleaseBSON, legacyBSONLib]) {
77+
console.log(`\nBSON@${bson.version}`);
78+
console.log(
79+
`deserialize({ oid, string }, { validation: { utf8: false } }) takes ${testPerformance(i =>
80+
bson.lib.deserialize(documents[i], { validation: { utf8: false } })
81+
)}ms on average`
82+
);
83+
84+
const oidBuffer = Buffer.from('00'.repeat(12), 'hex');
85+
console.log(
86+
`new Oid(buf) take ${testPerformance(() => new bson.lib.ObjectId(oidBuffer))}ms on average`
87+
);
88+
}
89+
90+
console.log();
91+
}
92+
93+
main()
94+
.then(() => null)
95+
.catch(error => console.error(error));

package-lock.json

+70-70
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/objectid.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ export class ObjectId {
7272
// Generate a new id
7373
this[kId] = ObjectId.generate(typeof workingId === 'number' ? workingId : undefined);
7474
} else if (ArrayBuffer.isView(workingId) && workingId.byteLength === 12) {
75-
this[kId] = ensureBuffer(workingId);
75+
// If intstanceof matches we can escape calling ensure buffer in Node.js environments
76+
this[kId] = workingId instanceof Buffer ? workingId : ensureBuffer(workingId);
7677
} else if (typeof workingId === 'string') {
7778
if (workingId.length === 12) {
7879
const bytes = Buffer.from(workingId);

test/node/object_id_tests.js

+13
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,17 @@ describe('ObjectId', function () {
383383
expect(propAccessRecord).to.deep.equal([oidKId, oidKId]);
384384
});
385385
});
386+
387+
it('should return the same instance if a buffer is passed in', function () {
388+
const inBuffer = Buffer.from('00'.repeat(12), 'hex');
389+
390+
const outBuffer = new ObjectId(inBuffer);
391+
392+
// instance equality
393+
expect(inBuffer).to.equal(outBuffer.id);
394+
// deep equality
395+
expect(inBuffer).to.deep.equal(outBuffer.id);
396+
// class method equality
397+
expect(Buffer.prototype.equals.call(inBuffer, outBuffer.id)).to.be.true;
398+
});
386399
});

test/node/tools/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* globals window */
12
'use strict';
23

34
exports.assertArrayEqual = function (array1, array2) {

0 commit comments

Comments
 (0)