-
Notifications
You must be signed in to change notification settings - Fork 31k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Strange slowness about crypto.webcrypto.subtle.deriveBits, when called with identical inputs a second time #56635
Comments
I tried on below platform I tried overriding node garbage collector and logged the time involved in garbage collection but it was too less in comparison to decryption time GC was called total 9 times and this was log produced
so it can be concluded that only about 2.5ms of time is used for garbage collection between encryption end and decryption start step. |
After digging further found that it is not decrypt operation which is taking this much timing, I did benchmarking of derivebits in two mode:
below is the code from which I tested these: import webcrypto from "tiny-webcrypto";
import encryptor from "tiny-encryptor";
const _deriveBits = webcrypto.subtle.deriveBits.bind(webcrypto.subtle);
webcrypto.subtle.deriveBits = async function (...args) {
console.time("deriveBits_work");
const result = await _deriveBits(...args);
console.timeEnd("deriveBits_work");
return result;
};
async function runTest() {
const SECRET = "P@ssword!";
const SAMPLE_1GB = new Uint8Array(1024 * 1024 * 1024);
console.log("\n=== Starting encryption ===");
console.time("encryption");
const enc = await encryptor.encrypt(SAMPLE_1GB, SECRET);
console.timeEnd("encryption");
console.log("\n=== Starting decryption ===");
// Log event loop state
console.log("Event loop state after encryption:");
console.log(process.memoryUsage());
// Test both with and without delay
for (const withDelay of [false, true]) {
if (withDelay) {
console.log("\n--- Testing with delay ---");
await new Promise((resolve) => setTimeout(resolve, 100));
} else {
console.log("\n--- Testing without delay ---");
}
// Log event loop state before decryption
console.log("Event loop state before decryption:");
console.log(process.memoryUsage());
console.time("decryption");
const dec = await encryptor.decrypt(enc, SECRET);
console.timeEnd("decryption");
}
}
await runTest().catch(console.error); and following are the logs produced:
so important observation is that:
so basically it is overhead of cleanup fronm encryption job which is causing actual delay. If we look the process as whole on bun vs node overall decryption time is smaller in nodejs in comparison to bun js. Also I would like to add that the sync version of deriveBits is completed much faster than that of async job for above operation. I would love to contribute further as I got good understanding of codebase during digging up the details. |
I think that may be its own problem 🤔 what are we even cleaning up here? Are we cloning the input buffer just to process it asynchronously super safely or something? |
I will check and let you know |
@fabiospampinato after further debugging, found the root cause: Also if we take overall execution time of the above js programme, nodejs is completed much faster than bun.js I tried moving concat operation to worker and here is the result for completion time of derivebits:
You can use this code to reproduce: import { parentPort } from 'node:worker_threads';
parentPort.on('message', (data) => {
const [version, salt, rounds, iv, encrypted] = data;
const result = new Uint8Array(version.length + salt.length + rounds.length + iv.length + encrypted.length);
let offset = 0;
result.set(version, offset);
offset += version.length;
result.set(salt, offset);
offset += salt.length;
result.set(rounds, offset);
offset += rounds.length;
result.set(iv, offset);
offset += iv.length;
result.set(encrypted, offset);
parentPort.postMessage(result);
}); and inside your encrypt operation replace below code with const worker = new Worker('./node_modules/tiny-encryptor/dist/concatWorker.js');
const archive = await new Promise((resolve, reject) => {
worker.on('message', (result) => {
// console.timeEnd("encryt worker");
worker.terminate();
resolve(result);
});
worker.on('error', (error) => {
worker.terminate();
reject(error);
});
// console.time("encryt worker");
worker.postMessage([version, salt, Int32.encode(rounds), iv, encryptedUint8]);
}); But this also add one more question: |
Version
22.2.0
Platform
Subsystem
No response
What steps will reproduce the bug?
Execute this:
I see the following output:
I also tried bundling this little repro for the browser and the problem doesn't manifest there either.
Basically through the course of executing that code we end up calling
webcrypto.subtle.deriveBits
twice, with identical arguments, reported below (but you can log these yourself by uncommenting the console.log in the repro), also asking Node to do very little work to begin with (just one iteration of the derivation function, not a million), and crucially as far as I can see there should be nothing else running concurrently that is blocking the main thread, but still the second execution in this specific scenario is always way slower than the first one, which seems symptomatic of some underlying issue in Node.I think this is worth a look just because of the wild difference in performance between the two calls, but also since we are dealing with crypto stuff it's probably worth making sure we aren't messing up something important internally.
How often does it reproduce? Is there a required condition?
Always, just execute the code.
What is the expected behavior? Why is that the expected behavior?
The expected behavior is that calling the same function twice takes about the same amount of time basically.
It could be that the GC is triggered during the second call for some reason? But it seems unlikely that if that's the problem it would reproduce pretty much exactly every time, and also ~70ms spent on a GC for what? There are relatively few objects getting allocated here in the first place, at least in userland as far as I can see.
What do you see instead?
I see the second call always taking much longer, which shouldn't be happening.
Additional information
No response
The text was updated successfully, but these errors were encountered: