feathersjs / feathers

The API and real-time application framework
https://feathersjs.com
MIT License
14.97k stars 742 forks source link

Feature: sync resolvers #3504

Open DaddyWarbucks opened 1 week ago

DaddyWarbucks commented 1 week ago

See: https://github.com/feathersjs/feathers/issues/3492

This PR is a WIP and a proof of concept. I have added a new _resolve method and added a test to compare the speed of this method compared to the current resolve method. The new _resolve method does not use Promise.all to resolve properties. Instead, it uses a basic for loop and done callback. This ensures resolvers that are not actually promises are not scheduled on the event loop.

When running the tests for the package you will see some console.time results. I purposefully left the test configure in a manner that favored syncResolvers. This should accurately illustrate the problem with assuming all resolvers are promises. But you can configure these settings however you would like.

const count = 10000
const asyncResolvers = 1
const syncResolvers = 10
const runs = 10

sync0: 86.879ms
sync1: 72.509ms
sync2: 171.684ms
sync3: 69.843ms
sync4: 67.955ms
sync5: 77.137ms
sync6: 75.034ms
sync7: 79.12ms
sync8: 77.653ms
sync9: 78.587ms

async0: 180.141ms
async1: 173.855ms
async2: 169.611ms
async3: 168.678ms
async4: 162.778ms
async5: 177.389ms
async6: 193.163ms
async7: 316.745ms
async8: 157.414ms
async9: 163.333ms

Interestingly, even when using all asyncResolvers the _resolve method is faster. I have noticed this when writing similar updates for yupjs and feathers-fletching. I speculate its because the callback pattern can pop promises off the loop as it goes and Promise.all has to keep them all in the loop until they are done? Pure speculation.

const count = 10000
const asyncResolvers = 10
const syncResolvers = 0
const runs = 10

sync0: 258.305ms
sync1: 124.654ms
sync2: 123.168ms
sync3: 128.291ms
sync4: 126.634ms
sync5: 126.913ms
sync6: 131.84ms
sync7: 134.369ms
sync8: 125.799ms
sync9: 128.844ms

async0: 159.942ms
async1: 167.286ms
async2: 156.082ms
async3: 156.891ms
async4: 321.241ms
async5: 177.017ms
async6: 160.917ms
async7: 155.687ms
async8: 149.171ms
async9: 146.085ms

The _resolve method passes all tests but one. You can rename it to resolve (comment/rename real resolve method). It fails one test around virtuals. I played with it and couldn't get it, but I am confident its solvable.

I am also curious if feathersjs/hooks could benefit from a similar treatment. I am not sure how it would work with the next-able hooks, but that may even make it easier. This would allow entire Feathers applications to get a performance boost if hooks like disallow, etc aren't consuming the event loop.