Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.79k stars 3.81k forks source link

Property getting on Model objects extremely slow comparing with Plain objects. #12953

Open wunderb1t opened 1 year ago

wunderb1t commented 1 year ago

Prerequisites

Last performant version

6.9.0

Slowed down in version

6.9.0

Node.js version

13.14.0

🦥 Performance issue

Suppose we have 1k documents in MongoDB collection and we fetched them all. Then we want to work with all document properties inside loop because we have familiar application structure. The problem is that property getting on Model objects extremely slow comparing with Plain objects.

Here is ours tests output result:

ModelObject loop ms: 390 (size: 1010)
PlainObject loop ms: 8 (size: 1010)

ModelObject loop ms: 371 (size: 1010)
PlainObject loop ms: 6 (size: 1010)

ModelObject loop ms: 421 (size: 1010)
PlainObject loop ms: 7 (size: 1010)

ModelObject loop ms: 595 (size: 1010)
PlainObject loop ms: 7 (size: 1010)

ModelObject loop ms: 481 (size: 1010)
PlainObject loop ms: 7 (size: 1010)

Steps to Reproduce

const mongoose = require('mongoose');

// Create connection.
mongoose.connect(mongoConfig.server, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    authSource: 'my_authSource',
    user: 'my_username',
    pass: 'my_password',
});

let db = mongoose.connection;
db.on('error', (err) => {
    global.logger.error(`Mongoose: ${err}`);
});
db.once('open', async () => {
    global.logger.info("Mongoose: Connected to DB.");
});

/**
 * @singleton
 */
class TestModel {
    static _instance;
    _model;

    /**
     * @private
     */
    constructor() {}

    static uniqueString = function(type) {
        return type + '-' + Math.random().toString(36).substr(2, 16);
    };

    /**
     * @return {PairModel}
     */
    static create() {
        if (this._instance) {return this._instance;}
        this._instance = new this();

        let pairSchema = new mongoose.Schema({
            field1: {type: String, default: TestModel.uniqueString('field1')},
            field2: {type: String, default: TestModel.uniqueString('field2')},
            field3: {type: String, default: TestModel.uniqueString('field3')},
            field4: {type: String, default: TestModel.uniqueString('field4')},
            field5: {type: String, default: TestModel.uniqueString('field5')},
            field6: {type: String, default: TestModel.uniqueString('field6')},
            field7: {type: String, default: TestModel.uniqueString('field7')},
            field8: {type: String, default: TestModel.uniqueString('field8')},
        }, { versionKey: false });

        this._instance._model = mongoose.model('test', pairSchema);

        return this._instance;
    }

    async insertTests() {
        const tests = [];

        for (let i = 0; i < 1000; i++) {
            tests.push(new this._model())
        }

        try {
            return this._model.insertMany(tests, {
                ordered: false
            });
        }
        catch (err) {
            throw new Error(`${this.constructor.name} insertMany: ${err}`);
        }
    }

    async all() {
        return await this._model.find({});
    }

}

const testModel = TestModel.create();

async function test() {
    await testModel.insertTests(); // generate 1000 objects
    let tests = await testModel.all();
    let len = tests.length;
    let loopStart = Date.now();

    // run loop with mongoose objects
    for (let k = 0; k < 100; k++) {
        for (let test of tests) {
            test.field1;
            test.field2;
            test.field3;
            test.field4;
            test.field5;
            test.field6;
            test.field7;
            test.field8;
        }
    }

    console.log(`ModelObject loop ms: ${Date.now() - loopStart} (size: ${len})`);
    console.log(`----------------------------------`);

    const plainTests = [];

    for (let test of tests) {
        plainTests.push(test.toObject());
    }

    loopStart = Date.now();

    // run loop with plain objects
    for (let k = 0; k < 100; k++) {
        for (let test of plainTests) {
            test.field1;
            test.field2;
            test.field3;
            test.field4;
            test.field5;
            test.field6;
            test.field7;
            test.field8;
        }
    }

    console.log(`PlainObject loop ms: ${Date.now() - loopStart} (size: ${len})`);
    console.log(`----------------------------------`);
}

test();

Expected Behavior

Iteration time should be similar to Plain objects loop time.

wunderb1t commented 1 year ago

Hello, I see this issue was dropped from the milestone, when to expect it? Thank you!

vkarpov15 commented 1 year ago

@wunderb1t we'll add it back to our next milestone and see if we can repro and fix.

vkarpov15 commented 1 year ago

With #13254, we got a 10x speedup:

Before:

$ node ./benchmarks/get.js 
{
  "Model loop ms": 2011,
  "POJO loop ms": 7
}

After:

$ node ./benchmarks/get.js 
{
  "Model loop ms": 206,
  "POJO loop ms": 8
}

We may be able to improve this more in the future by avoiding the this.$__schema.paths[path] lookup by passing the schematype in directly, and by passing in noDottedPath as a primitive rather than relying on options. However, we are limited in how much we can speed up model property accesses - they will always be significantly slower than a plain old property accesss because we need to support custom getters.