Skip to content
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

events: remove the abort listener on iterator completion #51091

Merged
merged 3 commits into from
Mar 15, 2024

Conversation

nbbeeken
Copy link
Contributor

@nbbeeken nbbeeken commented Dec 7, 2023

events: remove abort listener from signal in on

the abortHandler function is declared within the scope of
the events.on function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the addAbortListener helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: #51010

@nodejs-github-bot nodejs-github-bot added events Issues and PRs related to the events subsystem / EventEmitter. needs-ci PRs that need a full CI run. labels Dec 7, 2023
@benjamingr
Copy link
Member

Where are you actually removing the listener?

@nbbeeken
Copy link
Contributor Author

Hi, thanks for taking a look, I changed the signal abort listener to be added using addEventListener which comes from the listenersController() helper function. This helper function tracks all add listener calls in this scope and gives us a removeAll helper that is invoked inside the closeHandler that the iterator calls both on error or return.

@nbbeeken
Copy link
Contributor Author

Hey, @benjamingr anything else I can help with to move this along? Thank you in advance!

@nbbeeken
Copy link
Contributor Author

Hey @benjamingr sorry for the bump, I appreciate any time you can spend on this with me, thanks in advance. 🙂

I figure it may help to provide another reason for my motivation behind fixing this issue. I attempted working around the leaked listener by recreating a new controller and signal after the iterator was closed. However, it appeared that creating AbortSignals had a surprising performance cost that was preferable to avoid.

Code snippet measuring creating AbortSignals
import perf_hooks from 'node:perf_hooks'
import util from 'node:util'
import os from 'node:os'
import process from 'node:process'

function abortListener() { }
const ITERATIONS = 1000000;

function noop() { }

let shared = null;
function sharedSignal() {
    shared ??= new AbortController().signal;
    shared.addEventListener('abort', abortListener);
    shared.removeEventListener('abort', abortListener);
}

function recreateSignal() {
    const { signal } = new AbortController();
    signal.addEventListener('abort', abortListener);
    signal.removeEventListener('abort', abortListener);
}

const timerFunctions = [noop, sharedSignal, recreateSignal]
    .map(fn => ({ name: fn.name, fn, histogram: perf_hooks.createHistogram() }));

// Warm up
for (const { fn } of timerFunctions) {
    for (let i = 0; i < ITERATIONS; i++) {
        fn();
    }
}

for (const { fn, histogram } of timerFunctions) {
    for (let i = 0; i < ITERATIONS; i++) {
        histogram.recordDelta();
        fn();
        histogram.recordDelta();
    }
}

console.log('ITERATIONS:', ITERATIONS)
console.log('System:', os.platform(), os.arch(), os.cpus().length + ' cores', 'Node.js ' + process.version)
for (const { name, histogram } of timerFunctions) {
    console.log('-'.repeat(20), name, '-'.repeat(20))
    const { mean, stddev } = histogram;
    const median = histogram.percentiles.get(50)
    console.log(util.inspect({ mean, median, stddev }, { breakLength: Infinity, colors: true }))
}

The following numbers are in nanoseconds as measured by histogram.recordDelta(). In ~1ms sharedSignal() can execute ~2,500 times, while in the same time recreateSignal() will execute ~300 times.

And for reference an empty function will execute ~15,800 times.

ITERATIONS: 1000000
System: win32 x64 16 cores Node.js v20.10.0
-------------------- noop --------------------
{ mean: 65.52607426303713, median: 100, stddev: 165.59818354797054 }
-------------------- sharedSignal --------------------
{ mean: 405.4852127426064, median: 500, stddev: 3071.621721322665 }
-------------------- recreateSignal --------------------
{ mean: 3475.6129638064817, median: 5400, stddev: 21840.742869666145 }

@nbbeeken nbbeeken force-pushed the fix-abort-listener-events-on branch from 883b575 to 6748f0c Compare March 1, 2024 02:31
@nbbeeken nbbeeken force-pushed the fix-abort-listener-events-on branch from 6748f0c to e75733b Compare March 13, 2024 14:14
lib/events.js Outdated Show resolved Hide resolved
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: nodejs#51010
@nbbeeken nbbeeken force-pushed the fix-abort-listener-events-on branch from 01aade6 to 719f745 Compare March 14, 2024 14:17
@nbbeeken
Copy link
Contributor Author

I fixed the commit message line length lint in rebase

@atlowChemi atlowChemi added request-ci Add this label to start a Jenkins CI on a PR. commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. labels Mar 15, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 15, 2024
@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@atlowChemi atlowChemi added the commit-queue Add this label to land a pull request using GitHub Actions. label Mar 15, 2024
@nodejs-github-bot nodejs-github-bot removed the commit-queue Add this label to land a pull request using GitHub Actions. label Mar 15, 2024
@nodejs-github-bot nodejs-github-bot merged commit 40ef2da into nodejs:main Mar 15, 2024
58 checks passed
@nodejs-github-bot
Copy link
Collaborator

Landed in 40ef2da

rdw-msft pushed a commit to rdw-msft/node that referenced this pull request Mar 26, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: nodejs#51010
PR-URL: nodejs#51091
Reviewed-By: Chemi Atlow <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
marco-ippolito pushed a commit that referenced this pull request May 2, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: #51010
PR-URL: #51091
Reviewed-By: Chemi Atlow <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
marco-ippolito pushed a commit that referenced this pull request May 3, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: #51010
PR-URL: #51091
Reviewed-By: Chemi Atlow <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
jcbhmr pushed a commit to jcbhmr/node that referenced this pull request May 15, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: nodejs#51010
PR-URL: nodejs#51091
Reviewed-By: Chemi Atlow <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. events Issues and PRs related to the events subsystem / EventEmitter. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

events.on AsyncIterator API does not remove abort listener from signal in closeHandler
4 participants