mikro-orm: High memory profile and very slow

Describe the bug

I’ve integrated Mikro-ORM in a benchmark suite and noticed a memory leak and very slow execution times.

The benchmark basically creates 10k objects, persist() each, and then flushes. After that it reads all 10k from the database. What happens with Mikro ORM is that the first 10k insert takes 7 seconds, the second 10k insert 11 seconds, the third 21 seconds. Memory climbing as well rapidly 166mb, then 278mb, then 417mb.

Raw output:

round 1
0.13634226180139436 ops/s Mikro-ORM insert 7,334.482989996672 ms, 166.2109375 MB memory
7.202537523174704 ops/s Mikro-ORM fetch 138.83995699882507 ms, 175.12109375 MB memory
round 2
0.09214466685562421 ops/s Mikro-ORM insert 10,852.500032007694 ms, 278.3046875 MB memory
9.313704069546839 ops/s Mikro-ORM fetch 107.36866798996925 ms, 286.4140625 MB memory
round 3
0.04624159269974236 ops/s Mikro-ORM insert 21,625.552703022957 ms, 417.4453125 MB memory
10.529599470788153 ops/s Mikro-ORM fetch 94.97037401795387 ms, 419.08984375 MB memory
round 4
0.03031239307193595 ops/s Mikro-ORM insert 32,989.80709397793 ms, 481.515625 MB memory
15.424512427769912 ops/s Mikro-ORM fetch 64.83187100291252 ms, 482.24609375 MB memory
round 5
0.023591314642157822 ops/s Mikro-ORM insert 42,388.48131901026 ms, 548.6171875 MB memory
9.086378374941388 ops/s Mikro-ORM fetch 110.05484899878502 ms, 550.55859375 MB memory

To Reproduce Steps to reproduce the behavior:

Execute the following Typescript code:

import 'reflect-metadata';
import {Entity as MikroEntity, MikroORM, PrimaryKey, Property, ReflectMetadataProvider} from 'mikro-orm';
import {performance} from 'perf_hooks';

export async function bench(times: number, title: string, exec: () => void | Promise<void>) {
    const start = performance.now();
    for (let i = 0; i < times; i++) {
        await exec();
    }
    const took = performance.now() - start;

    process.stdout.write([
        (1000 / took) * times, 'ops/s',
        title,
        took.toLocaleString(undefined, {maximumFractionDigits: 17}), 'ms,',
        process.memoryUsage().rss / 1024 / 1024, 'MB memory'
    ].join(' ') + '\n');
}


@MikroEntity({collection: 'mikro'})
export class MikroModel {
    @PrimaryKey()
    _id!: any;

    @Property() id2!: number;

    @Property() ready?: boolean;

    @Property() tags: string[] = [];

    @Property() priority: number = 0;

    @Property()
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}


(async () => {
    const count = 10_000;

    const orm = await MikroORM.init({
        entities: [MikroModel],
        dbName: 'bench-insert',
        type: 'mongo',
        metadataProvider: ReflectMetadataProvider,
        clientUrl: 'mongodb://localhost:27017'
    });

    for (let j = 1; j <= 15; j++) {
        console.log('round', j);
        await orm.em.remove(MikroModel, {}, true);
        await bench(1, 'Mikro-ORM insert', async () => {
            for (let i = 1; i <= count; i++) {
                const user = new MikroModel('Peter ' + i);
                user.id2 = i;
                user.ready = true;
                user.priority = 5;
                user.tags = ['a', 'b', 'c'];
                await orm.em.persist(user);
            }

            await orm.em.flush();
        });

        await bench(1, 'Mikro-ORM fetch', async () => {
            const items = await orm.em.find(MikroModel, {});
        });
    }
    await orm.close();
})();

Additional Information

The same benchmark with TypeORM has following output.

round 1
0.44799316696261965 ops/s TypeORM insert 2,232.1769030094147 ms, 112.78515625 MB memory
14.745251303605988 ops/s TypeORM fetch 67.81844401359558 ms, 116.30859375 MB memory
round 2
0.5518305396852811 ops/s TypeORM insert 1,812.1505210101604 ms, 157.296875 MB memory
10.61794395223521 ops/s TypeORM fetch 94.18019199371338 ms, 125.12890625 MB memory
round 3
0.5414625441330295 ops/s TypeORM insert 1,846.8498159945011 ms, 139.37890625 MB memory
7.658094507705051 ops/s TypeORM fetch 130.58078598976135 ms, 140.234375 MB memory
round 4
0.5847921986324484 ops/s TypeORM insert 1,710.009132027626 ms, 141.4921875 MB memory
5.850612679085713 ops/s TypeORM fetch 170.92226999998093 ms, 154.796875 MB memory

Note: TypeORM has no UnitOfWork and thus inserting 10k happens via typeorm.manager.save(TypeOrmModel, items), which is naturally faster. Here is a benchmark of a ORM with UnitOfWork for a better reference.

round 1
4.781501792938342 ops/s Marshal insert 209.1393129825592 ms, 70.2734375 MB memory
16.781018394176918 ops/s Marshal fetch 59.591139018535614 ms, 86.30859375 MB memory
round 2
7.130604420772976 ops/s Marshal insert 140.24056601524353 ms, 100.296875 MB memory
32.69504859809784 ops/s Marshal fetch 30.58567100763321 ms, 107.0390625 MB memory
round 3
8.989556662613543 ops/s Marshal insert 111.2401909828186 ms, 113.4609375 MB memory
49.55669555919416 ops/s Marshal fetch 20.178907990455627 ms, 121.6015625 MB memory
round 4
8.519063107519148 ops/s Marshal insert 117.38379999995232 ms, 111.03515625 MB memory
48.701026779872905 ops/s Marshal fetch 20.533447980880737 ms, 112 MB memory
round 5
10.715570703516867 ops/s Marshal insert 93.32214099168777 ms, 114.41015625 MB memory
52.12080869452746 ops/s Marshal fetch 19.186195015907288 ms, 115.3828125 MB memory

Without Identity Map

When I add orm.em.clear(); right before the await bench(1, 'Mikro-ORM fetch, I basically disable Identity Map for the fetch, which I do as well for all other benchmarks (typeorm/marshal). The memory leak is then not that bad, however fetch times drastically increase.

round 1
0.14027045884962996 ops/s Mikro-ORM insert 7,129.08482798934 ms, 167.1484375 MB memory
2.160074054846749 ops/s Mikro-ORM fetch 462.9470909833908 ms, 160.453125 MB memory
round 2
0.0850145411912262 ops/s Mikro-ORM insert 11,762.693604975939 ms, 166.82421875 MB memory
2.45439285240989 ops/s Mikro-ORM fetch 407.43273800611496 ms, 181.80859375 MB memory
round 3
0.08564632189772563 ops/s Mikro-ORM insert 11,675.92463800311 ms, 321.8828125 MB memory
2.701008498703899 ops/s Mikro-ORM fetch 370.2320820093155 ms, 330.484375 MB memory
round 4
0.09211291201040393 ops/s Mikro-ORM insert 10,856.241304010153 ms, 346.94140625 MB memory
2.7796832507085085 ops/s Mikro-ORM fetch 359.7532199919224 ms, 352.234375 MB memory
round 5
0.08652217583452965 ops/s Mikro-ORM insert 11,557.73060899973 ms, 191.89453125 MB memory
2.7081659883433256 ops/s Mikro-ORM fetch 369.253585010767 ms, 183.26171875 MB memory
round 6
0.08637979298259872 ops/s Mikro-ORM insert 11,576.781623005867 ms, 352.48828125 MB memory
2.5409797655486086 ops/s Mikro-ORM fetch 393.5489819943905 ms, 355.92578125 MB memory

Expected behavior

Consistent insert times. Quicker inserts.

Versions

Dependency Version
node 14.5.0
typescript 3.9.6
mikro-orm 3.6.15?
mongodb 3.6.0

Questions

Is there anything I did wrong with the code? It feels weird to me that orm.em.persist is an async function, so maybe its related to that.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 43 (26 by maintainers)

Commits related to this issue

Most upvoted comments

I just want to say something. Both of you guys, you are unbelievable. I’ve been watching this topic from the beginning and I’ve learned a lot. Thank you

Reading this thread 2 years later is still a thrill!

Thank you both so much!

4.1 is out, benchmark updated with numbers for all drivers, here are most up to date ones for sqlite (total avg 290ms -> 215ms, so another ~25% improvement):

using sqlite driver, 10 rounds (+1 warm up), 10000 items
┌─────────┬────────────────────┬────────────────────┬────────────────────┬───────────────────┬────────────────────┐
│ (index) │        all         │        find        │       insert       │      update       │       remove       │
├─────────┼────────────────────┼────────────────────┼────────────────────┼───────────────────┼────────────────────┤
│   min   │ 216.86564300209284 │ 43.300829999148846 │ 60.35792099684477  │ 77.61356300115585 │ 28.919142000377178 │
│   avg   │ 232.87055759802462 │ 46.32837530001998  │ 70.59637359827757  │ 83.71949990019202 │ 32.22630879953503  │
│   max   │ 275.9551530107856  │  54.6859880015254  │ 101.25787800550461 │ 90.96762400120497 │ 39.46483799815178  │
└─────────┴────────────────────┴────────────────────┴────────────────────┴───────────────────┴────────────────────┘

https://github.com/mikro-orm/mikro-orm/releases/tag/v4.1.0 https://github.com/mikro-orm/benchmark

edit: lol, the remove numbers were wrong, i knew it was too good to be true :]

Just chiming in to say that I’m impressed! That’s really nice to see ppl focusing on performances like you do. Congrats to both of you 🙏

There was quite a lot of room for improvement for sure, here are new numbers for upcoming v4.1 :] I am stuck with sqlite3 for now, I don’t see easy way to use better-sqlite with knex, so that is probably also have an effect on your numbers. But this looks good enough already.

$ tsc && node --expose-gc perf sqlite
using sqlite driver
round 1
6.370955983929441 ops/s insert 156,96231499989517 ms, 79.6640625 MB memory
12.060290259357679 ops/s find 82,91674399992917 ms, 88.09765625 MB memory
6.023804847900662 ops/s update 166,0080340000568 ms, 99.59765625 MB memory
42.38281258275932 ops/s remove 23,594470000010915 ms, 100.1953125 MB memory
round 2
8.859347728571624 ops/s insert 112,87512700003572 ms, 109.140625 MB memory
15.467140909833951 ops/s find 64,65318999998271 ms, 111.3671875 MB memory
9.765550041778416 ops/s update 102,40078599995468 ms, 125.57421875 MB memory
149.70145039759103 ops/s remove 6,679961999994703 ms, 127.12109375 MB memory
round 3
11.170997712751092 ops/s insert 89,51751899998635 ms, 137.59765625 MB memory
19.376428175971814 ops/s find 51,609098999993876 ms, 139.96484375 MB memory
10.238220246772185 ops/s update 97,67322599992622 ms, 152.9140625 MB memory
155.58879623701947 ops/s remove 6,427198000019416 ms, 154.45703125 MB memory
round 4
11.148606103160386 ops/s insert 89,69731199997477 ms, 129.015625 MB memory
18.809806485124838 ops/s find 53,16375799989328 ms, 130.15625 MB memory
9.875265813756954 ops/s update 101,2630970000755 ms, 131.64453125 MB memory
148.36687389035913 ops/s remove 6,740049000014551 ms, 132.125 MB memory
round 5
11.893724104019482 ops/s insert 84,0779549999861 ms, 135.87890625 MB memory
19.430157018229387 ops/s find 51,466387999942526 ms, 136.59375 MB memory
10.098293558040666 ops/s update 99,0266319999937 ms, 141.93359375 MB memory
158.8167012282555 ops/s remove 6,2965669999830425 ms, 143.48046875 MB memory
total (avg per round)  290.60988559992984
insert (avg per round) 106.62604559997563
find (avg per round)   60.761835799948315
update (avg per round) 113.27435500000138
remove (avg per round) 9.947649200004525

Here are numbers for postgres:

$ tsc && node --expose-gc perf pg
using pg driver
round 1
3.6945200982605613 ops/s insert 270,6711490000598 ms, 75.625 MB memory
13.198887703353805 ops/s find 75,76395999989472 ms, 81.046875 MB memory
3.6205490517738843 ops/s update 276,20120200002566 ms, 91.8828125 MB memory
39.408957072652754 ops/s remove 25,37494200002402 ms, 92.5078125 MB memory
round 2
4.777701914772789 ops/s insert 209,30564899998717 ms, 102.95703125 MB memory
20.301054899581757 ops/s find 49,25852400017902 ms, 105.53125 MB memory
4.972188931346617 ops/s update 201,11866499995813 ms, 117.6953125 MB memory
145.93929742402167 ops/s remove 6,85216400003992 ms, 119.234375 MB memory
round 3
5.20890750686799 ops/s insert 191,97883599996567 ms, 129.8203125 MB memory
28.840573749755173 ops/s find 34,673374000005424 ms, 131.7578125 MB memory
5.061378578820338 ops/s update 197,57462999993004 ms, 143.69921875 MB memory
153.87129405238394 ops/s remove 6,49893799982965 ms, 145.2421875 MB memory
round 4
5.206020802622566 ops/s insert 192,08528700005263 ms, 124.0625 MB memory
27.747559144303928 ops/s find 36,03920600004494 ms, 125.203125 MB memory
4.853623712312074 ops/s update 206,0316289998591 ms, 126.69921875 MB memory
162.82293282713601 ops/s remove 6,141641000052914 ms, 127.17578125 MB memory
round 5
5.619807027846423 ops/s insert 177,94205300020985 ms, 130.109375 MB memory
27.934196084625924 ops/s find 35,79841700010002 ms, 131.25 MB memory
4.72560250345026 ops/s update 211,61322800000198 ms, 132.89453125 MB memory
149.95316213102706 ops/s remove 6,668748999945819 ms, 133.56640625 MB memory
total (avg per round)  483.5184486000333
insert (avg per round) 208.39659480005503
find (avg per round)   46.306696200044826
update (avg per round) 218.507870799955
remove (avg per round) 10.307286799978465

I guess I can now get back to working on features and bugs, lol.

Will update the benchmark once v4.1 is out.

Can you elaborate on that? How could we leverage it?

V8 has a JIT engine. This engine makes Javascript as fast as we know it. Without it, it is up to 30x slower. This engine also makes certain code almost as fast as native c/c++ code, which is pretty damn fast. However, some programs are far too complex to efficiently optimize. For example when you have loops, dynamic property access/writes, or to many stacks/frames. In case of an ORM this is not easily optimizable. To fully utilize the power of the v8 JIT engine, you need to either write better javascript code, or better: you generate javascript code in a way that the v8 JIT engine can perfectly easy optimize further. For example in change-detection (but that applies to a wide variety of algos, like serialization/deserialization, snapshotting, validation, etc) instead of having a loop over your property and doing something like this:

    function simple(last: any, current: any) {
        const changed: any = {};
        for (const property of schema.getClassProperties().values()) {
            if (last[property.name] !== current[property.name]) changed[property.name] = true;
        }
        return changed;
    }

you generate a function that looks like that:

function jit() {
        const props: string[] = [];

        for (const property of schema.getClassProperties().values()) {
            props.push(`if (last.${property.name} !== current.${property.name}) changed.${property.name} = true;`)
        }

        const code = `
            return function(last, current) {
                const changed = {};
                ${props.join('\n')}
                return changed;
            }
        `

        return new Function(code)();
    }

This can perfectly be optimized by the v8 jit engine and runs way faster. How much faster? It almost capped out the maximum possible (999m ops/s) at 931m ops/s, which is 63 times faster than the simple function. That’s quite a lot.

This JIT way was used heavily in Marshal for stuff like validation, decoding/encoding from JSON to class, or from Mongo types to class, and even a custom BSON parser that is 13x faster than official BSON parser. Having this JIT methods for costly operations makes your stuff incredible fast. That’s why Marshal / Marshal ORM is the by far fastest ORM/serializer/validator.

Got a PoC for the update case solution working, looks like it works fine with all of sqlite/mysql/postgres. Will need to spend some time with supporting optimistic locks and composite keys there, but that should not be problem.

Not sure if something like this can be also achieved in mongo?

Current numbers with the batch update in sqlite:

$ tsc && node --expose-gc perf
round 1
4.0803606133330765 ops/s insert 245,07637798786163 ms, 80.08203125 MB memory
9.038617399983586 ops/s find 110,63639003038406 ms, 94.84765625 MB memory
4.1624846881205855 ops/s update 240,24112397432327 ms, 105.55859375 MB memory
36.48914711639207 ops/s remove 27,40540897846222 ms, 106.14453125 MB memory
round 2
5.24306291735753 ops/s insert 190,72820901870728 ms, 114.5859375 MB memory
12.423610791058719 ops/s find 80,49189698696136 ms, 118.74609375 MB memory
7.019757866074138 ops/s update 142,4550560116768 ms, 142.6796875 MB memory
50.290804172518875 ops/s remove 19,884350955486298 ms, 144.73828125 MB memory
round 3
6.171988190641806 ops/s insert 162,0223450064659 ms, 155.2734375 MB memory
14.848147038252966 ops/s find 67,34847098588943 ms, 161.48046875 MB memory
6.282899460589861 ops/s update 159,16218400001526 ms, 124.49609375 MB memory
53.24452441687174 ops/s remove 18,7812739610672 ms, 124.9765625 MB memory
round 4
6.188236410823383 ops/s insert 161,5969290137291 ms, 127.83984375 MB memory
15.021255371238361 ops/s find 66,57233202457428 ms, 129.01171875 MB memory
7.064212975326958 ops/s update 141,55858600139618 ms, 140.125 MB memory
50.101337512299075 ops/s remove 19,959546983242035 ms, 142.1484375 MB memory
round 5
6.545511688195332 ops/s insert 152,77644401788712 ms, 152.53515625 MB memory
15.190553730774962 ops/s find 65,8303849697113 ms, 155.890625 MB memory
6.749867788259535 ops/s update 148,1510499715805 ms, 126.10546875 MB memory
48.27265934471801 ops/s remove 20,7156600356102 ms, 126.6015625 MB memory
total (avg per round)  448.2788041830063
insert (avg per round) 182.4400610089302
find (avg per round)   78.1758949995041
update (avg per round) 166.3135999917984
remove (avg per round) 21.34924818277359

edit: also implemented batching in deletes to get around the variable limit in sqlite

Thanks again for everything, this looks fantastic already!

Or actually even better, when I refactored things to not need internal entity uuid, we are getting closer to the typeorm numbers.

For fetching this is more than 100% improvement from 4.0.2 (~200ms vs ~80ms).

https://github.com/mikro-orm/benchmark/commit/bcf82ec731b71a63e2bffcbc50f582ee2c2c2ed2#diff-04c6e90faac2675aa89e2176d2eec7d8

round 1
0.9954934895810675 ops/s insert 1 004,5269109904766 ms, 82.46484375 MB memory
7.782948206949013 ops/s find 128,48601499199867 ms, 93.59765625 MB memory
0.8873976852954234 ops/s update 1 126,8904760181904 ms, 109 MB memory
4.317558481815836 ops/s remove 231,6123809814453 ms, 119.2109375 MB memory
round 2
1.1881453604731704 ops/s insert 841,6478599905968 ms, 128.56640625 MB memory
11.294083039597691 ops/s find 88,54193797707558 ms, 136.83984375 MB memory
1.115577268939108 ops/s update 896,3968949913979 ms, 156.21875 MB memory
5.072308805280724 ops/s remove 197,1488800048828 ms, 173.55078125 MB memory
round 3
1.2548891185650644 ops/s insert 796,8831550180912 ms, 127.25390625 MB memory
12.924437558032448 ops/s find 77,37280601263046 ms, 130.140625 MB memory
1.1544925382213758 ops/s update 866,1814320087433 ms, 132.42578125 MB memory
5.1841174831857355 ops/s remove 192,89686301350594 ms, 141.81640625 MB memory
round 4
1.2666150060136725 ops/s insert 789,5058839917183 ms, 132.890625 MB memory
12.947535278755813 ops/s find 77,23477700352669 ms, 134.03515625 MB memory
1.1554680612638535 ops/s update 865,4501439929008 ms, 135.0078125 MB memory
5.252239374030461 ops/s remove 190,3949779868126 ms, 141.48828125 MB memory
round 5
1.2693650508164096 ops/s insert 787,7954410016537 ms, 136.65625 MB memory
13.018666775267711 ops/s find 76,8127810060978 ms, 137.8515625 MB memory
1.158530762554522 ops/s update 863,1622329950333 ms, 139.14453125 MB memory
5.020697045364009 ops/s remove 199,1755309998989 ms, 141.2109375 MB memory
total (avg per round)  2059.6234761953356

New numbers for rc.5, again few minor improvements:

$ tsc && node --expose-gc perf
round 1
0.7575823789522909 ops/s insert 1,319.9884630143642 ms, 90.359375 MB memory
3.079504004690222 ops/s find 324.7276179790497 ms, 98.67578125 MB memory
0.8171382944686062 ops/s update 1,223.7830570042133 ms, 103.46875 MB memory
2.862082107681792 ops/s remove 349.3959859907627 ms, 107.20703125 MB memory
round 2
0.9187852418816814 ops/s insert 1,088.3936249911785 ms, 111.46875 MB memory
4.255327315516708 ops/s find 234.9995490014553 ms, 124.1484375 MB memory
0.9817556431647438 ops/s update 1,018.5833989977837 ms, 126.7421875 MB memory
3.3291832289097254 ops/s remove 300.3739750087261 ms, 128.375 MB memory
round 3
0.9924124441665777 ops/s insert 1,007.6455669999123 ms, 131.21484375 MB memory
3.9653716622665227 ops/s find 252.18317100405693 ms, 143.30859375 MB memory
0.9771941300480337 ops/s update 1,023.3381159901619 ms, 134.94140625 MB memory
3.367285039725782 ops/s remove 296.97515600919724 ms, 136.5546875 MB memory
round 4
1.002180391768816 ops/s insert 997.8243519961834 ms, 139.3671875 MB memory
4.39435386843244 ops/s find 227.56474101543427 ms, 144.203125 MB memory
0.9432723801017461 ops/s update 1,060.139171987772 ms, 138.94921875 MB memory
3.3762921606283323 ops/s remove 296.1828989982605 ms, 140.59765625 MB memory
round 5
0.99630119495989 ops/s insert 1,003.7125369906425 ms, 143.23046875 MB memory
4.4685460937971175 ops/s find 223.786435008049 ms, 146.30078125 MB memory
1.0015612396719114 ops/s update 998.44119399786 ms, 138.52734375 MB memory
3.282159812167071 ops/s remove 304.6774249970913 ms, 140.25390625 MB memory
total (avg per round) 2710.543287396431

This is what I get with the update bench added (measuring only the flush + clear):

$ tsc && node --expose-gc perf
round 1
0.6106483601875827 ops/s insert 1,637.6036770045757 ms, 87.7734375 MB memory
3.0904967383254816 ops/s find 323.5725789964199 ms, 103.65625 MB memory
0.6805107264596321 ops/s update 1,469.4845519959927 ms, 102.79296875 MB memory
round 2
0.7296803779947298 ops/s insert 1,370.4630549997091 ms, 109.27734375 MB memory
4.1020662336413745 ops/s find 243.77958400547504 ms, 112.02734375 MB memory
0.8363761723075782 ops/s update 1,195.6342529952526 ms, 109.22265625 MB memory
round 3
0.7731778140195882 ops/s insert 1,293.3635469973087 ms, 122.59765625 MB memory
4.24439705097696 ops/s find 235.6047250032425 ms, 135.8671875 MB memory
0.8619505461478784 ops/s update 1,160.1593669950962 ms, 115.2109375 MB memory
round 4
0.7826750586737928 ops/s insert 1,277.6694349944592 ms, 124.734375 MB memory
4.145625698748438 ops/s find 241.21811100840569 ms, 135.92578125 MB memory
0.8490885189242044 ops/s update 1,177.7335080057383 ms, 119.796875 MB memory
round 5
0.7595827409041899 ops/s insert 1,316.5122720003128 ms, 125.82421875 MB memory
4.035412536858233 ops/s find 247.80613899230957 ms, 137.68359375 MB memory
0.8281088964438231 ops/s update 1,207.5706519931555 ms, 121.10546875 MB memory

And here without clearing, so faster reads but higher memory usage:

$ tsc && node --expose-gc perf
round 1
0.6116594882708752 ops/s insert 1,634.8965710103512 ms, 87.37109375 MB memory
11.453465108521412 ops/s find 87.30982200801373 ms, 93.96875 MB memory
0.6829324191217391 ops/s update 1,464.2737289965153 ms, 95.86328125 MB memory
round 2
0.6874308972977207 ops/s insert 1,454.6916700005531 ms, 110.8203125 MB memory
15.751985367744137 ops/s find 63.48406100273132 ms, 114.57421875 MB memory
0.7730608552362382 ops/s update 1,293.5592239946127 ms, 113 MB memory
round 3
0.6981477566164542 ops/s insert 1,432.3615459948778 ms, 120.7265625 MB memory
16.73565438303123 ops/s find 59.752667993307114 ms, 122.765625 MB memory
0.8046211808253018 ops/s update 1,242.820875003934 ms, 137.796875 MB memory
round 4
0.686897004284854 ops/s insert 1,455.8223340064287 ms, 162.1171875 MB memory
16.707378930520807 ops/s find 59.85379299521446 ms, 164.29296875 MB memory
0.7457298796796644 ops/s update 1,340.9681269973516 ms, 136.79296875 MB memory
round 5
0.6586297190780649 ops/s insert 1,518.303791999817 ms, 139.89453125 MB memory
15.94404177343894 ops/s find 62.71935398876667 ms, 142.109375 MB memory
0.7389865366928314 ops/s update 1,353.204625993967 ms, 163.3125 MB memory

So the changes definitely helped there as well.

Will probably try to spend a bit more time on this, and publish my version of the benchmark when rc.2 is out, so you can also try.

Nice! We should also add uow/update into the benchmark, right after fetch section.

I tested and got:

        const dbItems = await orm.em.find(MikroModel, {});
        for (const item of dbItems) {
            item.priority++;
        }
        await bench(1, 'update', async () => {
            await orm.em.flush();
        });
round 1
0.23605106821611768 ops/s update 4,236.371424019337 ms, 159.05078125 MB memory
round 2
0.25321180577452007 ops/s update 3,949.262937963009 ms, 306.93359375 MB memory
round 3
0.2274217697826203 ops/s update 4,397.116428017616 ms, 325.12109375 MB memory

Might be worth optimizing as well.

I guess you are right, will implement that too, in the end it is configurable, so one can always disable it if they want different lock mode…

Things can be even better 😉

round 1
0.9433104279324339 ops/s insert 1 060,0964119434357 ms, 87.87890625 MB memory
5.559592496110255 ops/s find 179,8692981004715 ms, 98.53125 MB memory
0.8882095174800321 ops/s update 1 125,8604871034622 ms, 98.75 MB memory
3.8735956949897954 ops/s remove 258,1580729484558 ms, 103.3515625 MB memory
round 2
1.125560748477111 ops/s insert 888,4460490942001 ms, 108.1015625 MB memory
7.841640950058083 ops/s find 127,52432894706726 ms, 119.4453125 MB memory
1.1142908291923046 ops/s update 897,4317779541016 ms, 142.04296875 MB memory
4.189949712080312 ops/s remove 238,66634893417358 ms, 129.26171875 MB memory
round 3
1.2043808418747608 ops/s insert 830,3021479845047 ms, 132.1171875 MB memory
8.285880711531167 ops/s find 120,68723106384277 ms, 135.94140625 MB memory
1.1708094367186892 ops/s update 854,1099590063095 ms, 146.0703125 MB memory
4.338565157136339 ops/s remove 230,49094891548157 ms, 131.9375 MB memory
round 4
1.2257480929563607 ops/s insert 815,8283139467239 ms, 134.90625 MB memory
8.42843786425007 ops/s find 118,64594793319702 ms, 136.2578125 MB memory
1.1917762522942008 ops/s update 839,0836770534515 ms, 146.6171875 MB memory
4.378059691524534 ops/s remove 228,41168701648712 ms, 133.609375 MB memory
round 5
1.1966170209221523 ops/s insert 835,6892660856247 ms, 136.59765625 MB memory
8.443651973008045 ops/s find 118,43216693401337 ms, 139.30859375 MB memory
1.1959961784498272 ops/s update 836,1230729818344 ms, 146.8125 MB memory
4.235373119230692 ops/s remove 236,10670697689056 ms, 136.52734375 MB memory
total (avg per round) 2167.9927801847457

Yes, sqlite, but behind knex, so can’t just swap it with better-sqlite3 easily… But it could be another driver independent of knex.

Btw it’s still WIP (not pushed yet), but I made more improvements in the internal merging and diffing workflow, current numbers are even better (~30% faster reads):

$ tsc && node --expose-gc perf
round 1
0.8733276190780688 ops/s insert 1 145,045660018921 ms, 89.75 MB memory
4.712951023851957 ops/s find 212,18128407001495 ms, 103.140625 MB memory
0.8884324351932215 ops/s update 1 125,5779960155487 ms, 102.65234375 MB memory
3.8158657056223255 ops/s remove 262,0637300014496 ms, 108.50390625 MB memory
round 2
1.0778768642852692 ops/s insert 927,7497580051422 ms, 112.51171875 MB memory
6.185433429359799 ops/s find 161,6701580286026 ms, 123.12890625 MB memory
1.1159938844274606 ops/s update 896,0622580051422 ms, 143.84375 MB memory
4.156501302321598 ops/s remove 240,58695697784424 ms, 129.42578125 MB memory
round 3
1.1300812246216987 ops/s insert 884,8921459913254 ms, 132.06640625 MB memory
6.459667228426267 ops/s find 154,80673611164093 ms, 135.5078125 MB memory
1.1709506182071765 ops/s update 854,0069789886475 ms, 150.515625 MB memory
4.162818885686175 ops/s remove 240,2218370437622 ms, 135.3671875 MB memory
round 4
1.1081626240466105 ops/s insert 902,3946290016174 ms, 138.03125 MB memory
6.544209157000309 ops/s find 152,80685198307037 ms, 140.0078125 MB memory
1.1567482523715666 ops/s update 864,4923369884491 ms, 150.60546875 MB memory
4.234164490139725 ops/s remove 236,17410290241241 ms, 139.12890625 MB memory
round 5
1.130810150573332 ops/s insert 884,3217400312424 ms, 141.7734375 MB memory
6.739778753150154 ops/s find 148,37282299995422 ms, 144.0546875 MB memory
1.174012775895575 ops/s update 851,7794870138168 ms, 150.86328125 MB memory
4.262369380232582 ops/s remove 234,6112949848175 ms, 134.7734375 MB memory
total (avg per round) 2275.9637530326845

Here are the numbers when using a Set for persist/remove/orphanRemove/extraUpdate stacks.

$ tsc && node --expose-gc perf
round 1
0.6159320929802645 ops/s MikroORM insert 1,623.5556019842625 ms, 88.97265625 MB memory
3.4817421149844865 ops/s MikroORM fetch 287.21254101395607 ms, 103.5078125 MB memory
round 2
0.6982235352113625 ops/s MikroORM insert 1,432.2060909867287 ms, 94.78125 MB memory
4.68477259123528 ops/s MikroORM fetch 213.45753300189972 ms, 107.4921875 MB memory
round 3
0.7840485774855951 ops/s MikroORM insert 1,275.4311769902706 ms, 105.984375 MB memory
4.736212007277201 ops/s MikroORM fetch 211.13919699192047 ms, 110.68359375 MB memory
round 4
0.7834369655101185 ops/s MikroORM insert 1,276.4268780052662 ms, 103.09765625 MB memory
4.6585906432358914 ops/s MikroORM fetch 214.65719497203827 ms, 111.25 MB memory
round 5
0.8001526044639456 ops/s MikroORM insert 1,249.7616010010242 ms, 122.51171875 MB memory
4.42888557285627 ops/s MikroORM fetch 225.79043498635292 ms, 107.48046875 MB memory