Skip to content

Commit fac9610

Browse files
authored
fix(NODE-3309): remove redundant iteration of bulk write result (#2815)
Add caching for getters that build the result object. Add benchmarking script. Add unit test for getter caching.
1 parent 58c4e69 commit fac9610

File tree

4 files changed

+157
-26
lines changed

4 files changed

+157
-26
lines changed

lib/bulk/common.js

+28-9
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class Batch {
5757
}
5858
}
5959

60+
const kUpsertedIds = Symbol('upsertedIds');
61+
const kInsertedIds = Symbol('insertedIds');
62+
6063
/**
6164
* @classdesc
6265
* The result of a bulk write.
@@ -69,6 +72,8 @@ class BulkWriteResult {
6972
*/
7073
constructor(bulkResult) {
7174
this.result = bulkResult;
75+
this[kUpsertedIds] = undefined;
76+
this[kInsertedIds] = undefined;
7277
}
7378

7479
/** Number of documents inserted. */
@@ -94,20 +99,33 @@ class BulkWriteResult {
9499

95100
/** Upserted document generated Id's, hash key is the index of the originating operation */
96101
get upsertedIds() {
97-
const upserted = {};
98-
for (const doc of !this.result.upserted ? [] : this.result.upserted) {
99-
upserted[doc.index] = doc._id;
102+
if (this[kUpsertedIds]) {
103+
return this[kUpsertedIds];
104+
}
105+
106+
this[kUpsertedIds] = {};
107+
for (const doc of this.result.upserted || []) {
108+
this[kUpsertedIds][doc.index] = doc._id;
100109
}
101-
return upserted;
110+
return this[kUpsertedIds];
102111
}
103112

104113
/** Inserted document generated Id's, hash key is the index of the originating operation */
105114
get insertedIds() {
106-
const inserted = {};
107-
for (const doc of !this.result.insertedIds ? [] : this.result.insertedIds) {
108-
inserted[doc.index] = doc._id;
115+
if (this[kInsertedIds]) {
116+
return this[kInsertedIds];
117+
}
118+
119+
this[kInsertedIds] = {};
120+
for (const doc of this.result.insertedIds || []) {
121+
this[kInsertedIds][doc.index] = doc._id;
109122
}
110-
return inserted;
123+
return this[kInsertedIds];
124+
}
125+
126+
/** The number of inserted documents @type {number} */
127+
get n() {
128+
return this.result.insertedCount;
111129
}
112130

113131
/**
@@ -1370,5 +1388,6 @@ module.exports = {
13701388
INSERT: INSERT,
13711389
UPDATE: UPDATE,
13721390
REMOVE: REMOVE,
1373-
BulkWriteError
1391+
BulkWriteError,
1392+
BulkWriteResult
13741393
};

lib/operations/bulk_write.js

-17
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,6 @@ class BulkWriteOperation extends OperationBase {
7070
return callback(err, null);
7171
}
7272

73-
// Update the n
74-
r.n = r.insertedCount;
75-
76-
// Inserted documents
77-
const inserted = r.getInsertedIds();
78-
// Map inserted ids
79-
for (let i = 0; i < inserted.length; i++) {
80-
r.insertedIds[inserted[i].index] = inserted[i]._id;
81-
}
82-
83-
// Upserted documents
84-
const upserted = r.getUpsertedIds();
85-
// Map upserted ids
86-
for (let i = 0; i < upserted.length; i++) {
87-
r.upsertedIds[upserted[i].index] = upserted[i]._id;
88-
}
89-
9073
// Return the results
9174
callback(null, r);
9275
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const performance = require('perf_hooks').performance;
4+
const PerformanceObserver = require('perf_hooks').PerformanceObserver;
5+
const MongoClient = require('../../../index').MongoClient;
6+
7+
/**
8+
* # BulkWriteResult class Benchmark
9+
* This script can be used to reproduce a performance regression between 3.6.6...3.6.8
10+
* - [Changes to bulk/common.js](https://github.com/mongodb/node-mongodb-native/compare/v3.6.6...v3.6.8#diff-ab41c37a93c7b6e74f6d2dd30dec67a140f0a84562b4bd28d0ffc3b150c43600)
11+
* - [Changes to operations/bulk_write.js](https://github.com/mongodb/node-mongodb-native/compare/v3.6.6...v3.6.8#diff-93e45847ed36e2aead01a003826fd4057104d76cdeba4807adc1f76b573a87d8)
12+
*
13+
* ## Solution
14+
* A nested loop was introduced through the use of getters.
15+
* - Results running this script (modify the mongodb import) against v3.6.6
16+
* - bulkWrite took `217.664902ms` to insert 10000 documents
17+
* - Results with performance regression:
18+
* - bulkWrite took `1713.479087ms` to insert 10000 documents
19+
* - Results with nested loop removal and getter caching:
20+
* - bulkWrite took `190.523483ms` to insert 10000 documents
21+
*/
22+
23+
const client = new MongoClient(process.env.MONGODB_URI || 'mongodb://localhost', {
24+
useUnifiedTopology: true
25+
});
26+
27+
const DOC_COUNT = 10000;
28+
29+
const MANY_DOCS = new Array(DOC_COUNT).fill(null).map((_, index) => ({
30+
_id: `id is ${index}`
31+
}));
32+
33+
const obs = new PerformanceObserver(items => {
34+
items.getEntries().forEach(entry => {
35+
console.log(`${entry.name} took ${entry.duration}ms to insert ${MANY_DOCS.length} documents`);
36+
});
37+
});
38+
39+
obs.observe({ entryTypes: ['measure'], buffer: true });
40+
41+
async function main() {
42+
await client.connect();
43+
const collection = client.db('test').collection('test');
44+
45+
try {
46+
await collection.drop();
47+
} catch (_) {
48+
// resetting collection if exists
49+
}
50+
51+
performance.mark('bulkWrite-start');
52+
await collection.insertMany(MANY_DOCS);
53+
performance.mark('bulkWrite-end');
54+
55+
performance.measure('bulkWrite', 'bulkWrite-start', 'bulkWrite-end');
56+
}
57+
58+
main(process.argv)
59+
.catch(console.error)
60+
.finally(() => client.close());

test/unit/bulk_write.test.js

+69
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const expect = require('chai').expect;
44
const mock = require('mongodb-mock-server');
5+
const BulkWriteResult = require('../../lib/bulk/common').BulkWriteResult;
56

67
describe('Bulk Writes', function() {
78
const test = {};
@@ -62,4 +63,72 @@ describe('Bulk Writes', function() {
6263
});
6364
});
6465
});
66+
67+
it('should cache the upsertedIds in result', function() {
68+
const result = new BulkWriteResult({
69+
upserted: [
70+
{ index: 0, _id: 1 },
71+
{ index: 1, _id: 2 },
72+
{ index: 2, _id: 3 }
73+
]
74+
});
75+
76+
const bulkWriteResultSymbols = Object.getOwnPropertySymbols(result);
77+
78+
expect(bulkWriteResultSymbols.length).to.be.equal(2);
79+
80+
const kUpsertedIds = bulkWriteResultSymbols.filter(
81+
s => s.toString() === 'Symbol(upsertedIds)'
82+
)[0];
83+
84+
expect(kUpsertedIds).to.be.a('symbol');
85+
86+
expect(result[kUpsertedIds]).to.equal(undefined);
87+
88+
const upsertedIds = result.upsertedIds; // calls getter
89+
90+
expect(upsertedIds).to.be.a('object');
91+
92+
expect(result[kUpsertedIds]).to.equal(upsertedIds);
93+
94+
Object.freeze(result); // If the getters try to write to `this`
95+
Object.freeze(result[kUpsertedIds]);
96+
// or either cached object then they will throw in these expects:
97+
98+
expect(() => result.upsertedIds).to.not.throw();
99+
});
100+
101+
it('should cache the insertedIds in result', function() {
102+
const result = new BulkWriteResult({
103+
insertedIds: [
104+
{ index: 0, _id: 4 },
105+
{ index: 1, _id: 5 },
106+
{ index: 2, _id: 6 }
107+
]
108+
});
109+
110+
const bulkWriteResultSymbols = Object.getOwnPropertySymbols(result);
111+
112+
expect(bulkWriteResultSymbols.length).to.be.equal(2);
113+
114+
const kInsertedIds = bulkWriteResultSymbols.filter(
115+
s => s.toString() === 'Symbol(insertedIds)'
116+
)[0];
117+
118+
expect(kInsertedIds).to.be.a('symbol');
119+
120+
expect(result[kInsertedIds]).to.equal(undefined);
121+
122+
const insertedIds = result.insertedIds; // calls getter
123+
124+
expect(insertedIds).to.be.a('object');
125+
126+
expect(result[kInsertedIds]).to.equal(insertedIds);
127+
128+
Object.freeze(result); // If the getters try to write to `this`
129+
Object.freeze(result[kInsertedIds]);
130+
// or either cached object then they will throw in these expects:
131+
132+
expect(() => result.insertedIds).to.not.throw();
133+
});
65134
});

0 commit comments

Comments
 (0)