329 reads
10 comments
Interesting benchmarking work, and surprising results! I think I found why it looks so skewed against async/await.
Looking at the docs, it looks like the callbacks test is missing a call to the second argument of the test callback function. This is what test runners use to know that async work has finished. Otherwise, the test is firing 10000 iterations without waiting for the previous ones to finish, unlike what's happening in the async/await test. I imagine the same must be happening with promises.
So, for a fairer comparison, I believe either the callback and promises tests should call the second argument of the test callback to block like the async/await, or the async/await test should drop the await so that it fires all iterations at once like the others.
In any way, I agree that always blocking on any async work is slow mode and no better than other languages. It's when we can parallelise multiple async work that node truly shines!
Oooooh. Really good point, Ricardo. Let me address that and re-run those tests. Yes. I told you I was a really bad benchmarker! ๐ test('callback passing test', (t, done) should help.
Yep. That changes things a lot! ๐. I'll do an edit!
And thank you for taking the time to read ๐
Gemma Black even that may not be enough. If I'm reading it right, the callbacks test is only blocking once, and it's still running 10000 reads in parallel, while the async test is blocking 10000 times to run reads sequentially. You may need to launch the next readFile only after the previous one completed (ie inside it's callback).
๐๐๐ True Ricardo Lopes. I should compare blocking to blocking.
One thing that came to me was also resource usage. I hope you won't mind me mentioning you in the article as a helpful reviewer too?
Gemma Black of course, glad I could help :)
Looks like node:test
side effect. In my tests, promises have almost the same speed.
My tests:
with node:test
(node v21.4.0):
โ reading file 10,000 times with callback parallel (401.3785ms)
โ reading file 10,000 times with callback blocking (711.3979ms)
โ reading file 10,000 times with async parallel (512.4789ms)
โ reading file 10,000 times with async blocking (1080.5988ms)
โ reading file 10,000 times with promise parallel (474.045ms)
โ reading file 10,000 times with promises blocking (755.0493ms)
without node:test
(node v21.4.0):
reading file 10,000 times with callback parallel: 402.4896ms
reading file 10,000 times with callback blocking: 755.3545ms
reading file 10,000 times with async parallel: 413.0469ms
reading file 10,000 times with async blocking: 825.5176ms
reading file 10,000 times with promise parallel: 408.7469ms
reading file 10,000 times with promises blocking: 775.5422ms
without node:test
(bun 1.0.26):
reading file 10,000 times with callback parallel: 580.7146ms
reading file 10,000 times with callback blocking: 437.1434ms
reading file 10,000 times with async parallel: 589.4524ms
reading file 10,000 times with async blocking: 397.4406ms
reading file 10,000 times with promise parallel: 584.5042ms
reading file 10,000 times with promises blocking: 415.0018ms
My test code:
// const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const tests = [];
const test = async (name, callback) => {
tests.push([name, callback]);
};
setTimeout(async () => {
for await (let item of tests) {
const [name, callback] = item;
const startTime = performance.now();
let done;
const promise = new Promise((resolve) => {
done = () => {
resolve();
console.log(`${name}: ${(performance.now() - startTime).toFixed(4)}ms`);
};
});
const res = callback(null, done);
if (res instanceof Promise) {
await res;
done();
}
await promise;
}
}, 0);
test('reading file 10,000 times with callback parallel', (t, done) => {
let count = 0;
for (let i = 0; i < 10000; i++) {
fs.readFile("./text.txt", { encoding: 'utf-8'}, (err, data) => {
assert.strictEqual(data, "Hello, world");
count++
if (count === 10000) {
done()
}
})
}
});
let read = (i, callback) => {
fs.readFile("./text.txt", { encoding: 'utf-8'}, (err, data) => {
assert.strictEqual(data, "Hello, world");
i += 1
if (i === 10000) {
return callback()
}
read(i, callback)
})
}
test('reading file 10,000 times with callback blocking', (t, done) => {
read(0, done)
});
test('reading file 10,000 times with async parallel', async (t) => {
let allFiles = []
for (let i = 0; i < 10000; i++) {
allFiles.push(fs.promises.readFile("./text.txt", { encoding: 'utf-8'}))
}
return await Promise.all(allFiles)
.then(allFiles => {
return allFiles.forEach((data) => {
assert.strictEqual(data, "Hello, world");
})
})
});
test('reading file 10,000 times with async blocking', async (t) => {
for (let i = 0; i < 10000; i++) {
let data = await fs.promises.readFile("./text.txt", { encoding: 'utf-8'})
assert.strictEqual(data, "Hello, world");
}
});
test('reading file 10,000 times with promise parallel', (t, done) => {
let allFiles = []
for (let i = 0; i < 10000; i++) {
allFiles.push(fs.promises.readFile("./text.txt", { encoding: 'utf-8'}))
}
Promise.all(allFiles)
.then(allFiles => {
for (let i = 0; i < 10000; i++) {
assert.strictEqual(allFiles[i], "Hello, world");
}
done()
})
});
let read2 = (i, callback) => {
let data = fs.promises.readFile("./text.txt", { encoding: 'utf-8'})
.then(data => {
assert.strictEqual(data, "Hello, world")
i += 1
if (i === 10000) {
return callback()
}
read(i, callback)
})
}
test('reading file 10,000 times with promises blocking', (t, done) => {
read2(0, done)
});
Thank you, Sergei. That's a very important point. I noticed when I ran all the tests together, it increased all the times. So you make a good point that node:test may have a greater effect than I expected. Hopefully the results still work out relatively but still, it's definitely worth comparing.
With the promises test I tried to avoid using await, so I'm not sure if that is causing the increase in the times you saw.
But I'd love to take your example and tweak it without node:test and without the await/async in the promises test to see the results.
And thank you for taking the time to read through the benchmarks. I really appreciate that too. And helping pointing out any flaws in my benchmarks. It's so helpful.
I've modified the tests a little to be more like-for-like, added some alternative async/await implementations, and put them up on my github (can't link because I'm newly registered)
My main takeaways are:
- The blocking promise version is now ~30% slower than the parallel one.
- Parallel promises and async/await appear to be about 3.4-3.7x slower than the parallel callback version on my machine. Take the results with a massive pinch of salt since runtime variance is about 200ms for some reason.
This is a much older machine running Windows and doing 20% less iterations (due to open file limits on Windows) so the numbers are not quite directly comparable, but hopefully relative scaling remains mostly intact.
Nice! Thanks Simonas Urbelis. Thanks for checking through it too. That's a really interesting find.
I would love to be able to run the same code across different machines with different specs. My hunch was that the number of cores on a machine had a big effect on parallel performance too.
It's a shame you can't share your GitHub implementation. That would be lovely to see.
My aim, unless someone does it before me is to create a GitHub repo and be able to take advantage of their machines to run the tests and benchmark them too. That'll make the tests more fair. True.
Also, I have to decide whether I want to test just blocking or truly sequential. So as someone mentioned the node:test
overhead, I may have to remove node:test
completely so there are no promises or async awaits being used in the callback tests at all.
Thank you again for your comment. I truly appreciate it.