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:
|
gcPersistentSignals.add(this); |
- calling the
aborted function add the listener but as weak listener
- when the listener is garbage collected we call
remove on the listener:
|
(listener) => listener.remove(), |
- the
remove does not call the removeEventListener which decreases the size:
- because the size is not decreased it will never reach 0 so the abort signal won't get GCed
|
if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) { |
|
gcPersistentSignals.delete(this); |
|
} |
This is also the reason why calling aborted on the same signal and garbage collecting still emit the max listener warning
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-gcHow often does it reproduce? Is there a required condition?
always.
required conditions are to add the
abortedafter the regular listener and remove the regular listener after the abortedWhat 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:
abortedfunction add weak listener:node/lib/internal/abort_controller.js
Line 250 in ccdfb37
abortedfunction add the listener but as weak listenerremoveon the listener:node/lib/internal/event_target.js
Line 409 in 38dee8a
removedoes not call the removeEventListener which decreases thesize: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