Description
Version
20.5.0,18.16.0
Platform
Darwin razluvaXFX99QJK 22.5.0 Darwin Kernel Version 22.5.0: Thu Jun 8 22:22:20 PDT 2023; root:xnu-8796.121.3~7/RELEASE_ARM64_T6000 arm64
Subsystem
events
What steps will reproduce the bug?
Run this with the flag --expose-gc
// Flags: --expose-gc
const {setImmediate} = require('timers/promises');
const {aborted} = require('util');
const formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`;
function logMemory() {
const memoryData = process.memoryUsage();
const memoryUsage = {
rss: `${formatMemoryUsage(memoryData.rss)} -> Resident Set Size - total memory allocated for the process execution`,
heapTotal: `${formatMemoryUsage(memoryData.heapTotal)} -> total size of the allocated heap`,
heapUsed: `${formatMemoryUsage(memoryData.heapUsed)} -> actual memory used during the execution`,
external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`,
};
console.log(memoryUsage);
}
(async () => {
while (true) {
for (let i = 0; i < 10000; i++) {
function lis() {
}
const timeoutSignal = AbortSignal.timeout(1_000_000_000);
timeoutSignal.addEventListener('abort', lis);
aborted(timeoutSignal, {});
timeoutSignal.removeEventListener('abort', lis);
}
await setImmediate();
global.gc();
}
})().catch(console.error)
setInterval(() => {
logMemory();
}, 1000);
How often does it reproduce? Is there a required condition?
always.
required conditions are to add the aborted
after the regular listener and remove the regular listener after the aborted
What is the expected behavior? Why is that the expected behavior?
no memory leak
What do you see instead?
memory leak
Additional information
this is happening because:
- the regular listener goes to here which add to the map the
aborted
function add weak listener:
node/lib/internal/abort_controller.js
Line 250 in ccdfb37
- calling the
aborted
function add the listener but as weak listener - when the listener is garbage collected we call
remove
on the listener:
node/lib/internal/event_target.js
Line 409 in 38dee8a
- the
remove
does not call the removeEventListener which decreases thesize
: - because the size is not decreased it will never reach 0 so the abort signal won't get GCed
node/lib/internal/abort_controller.js
Lines 257 to 259 in ccdfb37
This is also the reason why calling aborted on the same signal and garbage collecting still emit the max listener warning