Simonas Urbelis
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)
});
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!
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:
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.