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

Performance Enhancement in EventEmitter’s emit Method #53056

Open
mertcanaltin opened this issue May 19, 2024 · 12 comments · May be fixed by #56741
Open

Performance Enhancement in EventEmitter’s emit Method #53056

mertcanaltin opened this issue May 19, 2024 · 12 comments · May be fixed by #56741
Labels
events Issues and PRs related to the events subsystem / EventEmitter.

Comments

@mertcanaltin
Copy link
Member

mertcanaltin commented May 19, 2024

What is the problem this feature will solve?

Specific problems addressed by this feature:

1.	Inefficient Error Handling:
 	The existing emit method combines error handling with regular event processing, causing unnecessary overhead even when no errors are involved.
2.	Redundant Checks and Operations:
 	The method contains multiple checks and operations that can be streamlined using modern JavaScript features like optional chaining (?.).
3.	Unnecessary Listener Array Cloning:
 	When multiple listeners are registered for an event, the current implementation clones the listener array, leading to unnecessary memory usage and processing time.
4.	Lack of Early Return Optimization:
 	The method does not leverage early returns effectively, resulting in extra processing steps that could be avoided when conditions are not met.

What is the feature you are proposing to solve the problem?

I propose a series of optimizations to the emit method in the EventEmitter class. These optimizations are designed to improve the performance and efficiency of event handling in Node.js. The proposed changes include:

1.	Separate Error Handling Logic:
	By handling errors early and separately from regular events, the overhead associated with error checking is reduced for non-error events.
2.	Utilize Optional Chaining (?.):
	Implementing optional chaining to simplify and speed up checks for null or undefined values within the event handling process.
3.	Direct Listener Invocation Without Cloning:
	Avoid unnecessary cloning of listener arrays by directly invoking listeners. This reduces memory usage and processing time, especially when multiple listeners are registered for the same event.
4.	Implement Early Return Statements:
	Introduce early returns to exit the function as soon as conditions are met, preventing unnecessary processing steps.

Proposed Changes to emit Method:

  • lib/events.js
EventEmitter.prototype.emit = function emit(type, ...args) {
  if (type === 'error') {
    const events = this._events;
    const handler = events?.error;
    if (!handler) {
      const err = args[0];
      if (err instanceof Error) {
        try {
          const capture = {};
          ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit);
          ObjectDefineProperty(err, kEnhanceStackBeforeInspector, {
            __proto__: null,
            value: FunctionPrototypeBind(enhanceStackTrace, this, err, capture),
            configurable: true,
          });
        } catch {
          // If enhancing the error stack fails, continue without enhancement.
        }
        throw err; // Unhandled 'error' event
      }
      throw new ERR_UNHANDLED_ERROR(inspect(err));
    }
    if (typeof handler === 'function') {
      handler.apply(this, args);
    } else {
      const len = handler.length;
      for (let i = 0; i < len; ++i) {
        handler[i].apply(this, args);
      }
    }
    return true;
  }

  const events = this._events;
  if (!events) return false;

  const handler = events[type];
  if (!handler) return false;

  if (typeof handler === 'function') {
    handler.apply(this, args);
  } else {
    const len = handler.length;
    for (let i = 0; i < len; ++i) {
      handler[i].apply(this, args);
    }
  }
  return true;
};

Benefits of the Proposed Feature:

1.	Improved Performance:
	Reduces overhead for non-error events by handling errors separately and early.
	Decreases memory usage and processing time by eliminating unnecessary cloning of listener arrays.
2.	Cleaner and More Maintainable Code:
	Simplifies the codebase using modern JavaScript features like optional chaining.
	Introduces early returns for more readable and efficient code.
3.	Enhanced Efficiency:
	Optimizes event handling, particularly in applications with a high volume of events or complex event handling requirements.

These changes aim to enhance the overall performance and efficiency of the emit method in the EventEmitter class

What alternatives have you considered?

No response

@mertcanaltin mertcanaltin added the feature request Issues that request new features to be added to Node.js. label May 19, 2024
@mertcanaltin mertcanaltin added events Issues and PRs related to the events subsystem / EventEmitter. and removed feature request Issues that request new features to be added to Node.js. labels May 19, 2024
@bnb
Copy link
Contributor

bnb commented May 20, 2024

If you're confident in these changes, I'd highly recommend submitting a PR - easier and more in-line with our workflow to do code review that way 👍🏻

@Qard
Copy link
Member

Qard commented May 21, 2024

The listener cloning is important as a listener could within itself remove listeners that already triggered, and indeed "once" does this, which would result in array positions changing and still registered listeners could get skipped due to shifting past the position of the index into the array during the loop.

@mertcanaltin
Copy link
Member Author

The listener cloning is important as a listener could within itself remove listeners that already triggered, and indeed "once" does this, which would result in array positions changing and still registered listeners could get skipped due to shifting past the position of the index into the array during the loop.

To address this, I propose a conditional cloning approach. The listener array will only be cloned if a change (such as removal of the listener) is detected during event execution. In this way, can we avoid unnecessary cloning and still handle edge cases correctly I wonder if this approach will be performant

@simonkcleung
Copy link

Will it be still performant if there is only one event listener?

@mertcanaltin
Copy link
Member Author

Will it be still performant if there is only one event listener?

Sorry for the late reply, my aim was to provide conditions and reduce the cost

@simonkcleung
Copy link

simonkcleung commented Jul 14, 2024

Here is my suggestion:

EventEmitter.prototype.emit = function emit(type, ...args) {
  let doError = (type === 'error');

  const events = this._events;

  if (doError && events?.[kErrorMonitor] !== undefined)
      this.emit(kErrorMonitor, ...args);
  
  const handler = events?.[type];

  if (handler !== undefined) {
    if (typeof handler === 'function') {
      const result = handler.apply(this, args);
      // We check if result is undefined first because that
      // is the most common case so we do not pay any perf
      // penalty
      if (result !== undefined && result !== null) {
        addCatch(this, result, type, args);
      }
    } else {
      const len = handler.length;
      handler.isHandling = true;
      for (let i = 0; i < len; ++i) {
        const result = handler[i].apply(this, args);
        // We check if result is undefined first because that
        // is the most common case so we do not pay any perf
        // penalty.
        // This code is duplicated because extracting it away
        // would make it non-inlineable.
        if (result !== undefined && result !== null) {
          addCatch(this, result, type, args);
        }
      }
      handler.isHandling = false;
    }
  } else if (doError) 
    _handleUncaughtError(this, args)
  else 
    return false;
};

function _handleUncaughtError(target, args) {
  let er;
  if (args.length > 0)
    er = args[0];
  if (er instanceof Error) {
    try {
      const capture = {};
      ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit);
      ObjectDefineProperty(er, kEnhanceStackBeforeInspector, {
        __proto__: null,
        value: FunctionPrototypeBind(enhanceStackTrace, target, er, capture),
        configurable: true,
      });
    } catch {
      // Continue regardless of error.
    }

    // Note: The comments on the `throw` lines are intentional, they show
    // up in Node's output if this results in an unhandled exception.
    throw er; // Unhandled 'error' event
  }

  let stringifiedEr;
  try {
    stringifiedEr = inspect(er);
  } catch {
    stringifiedEr = er;
  }

  // At least give some kind of context to the user
  const err = new ERR_UNHANDLED_ERROR(stringifiedEr);
  err.context = er;
  throw err; // Unhandled 'error' event
}

function _addListener(target, type, listener, prepend) {
  let max;
  let events;
  let existing;

  checkListener(listener);

  events = target._events;
  if (events === undefined) {
    events = target._events = { __proto__: null };
    target._eventsCount = 0;
  } else {
    // To avoid recursion in the case that type === "newListener"! Before
    // adding it to the listeners, first emit "newListener".
    if (events.newListener !== undefined) {
      target.emit('newListener', type,
                  listener.listener ?? listener);

      // Re-assign `events` because a newListener handler could have caused the
      // this._events to be assigned to a new object
      events = target._events;
    }
    existing = events[type];
  }

  if (existing === undefined) {
    // Optimize the case of one listener. Don't need the extra array object.
    events[type] = listener;
    ++target._eventsCount;
  } else {
    if (typeof existing === 'function') {
      // Adding the second element, need to change to array.
      existing = events[type] =
        prepend ? [listener, existing] : [existing, listener];
      // If we've already got an array, just append.
    } else {
      if (existing.isHandling === true)
        events[type] = existing = arrayClone(existing);
      prepend ? existing.unshift(listener) : existing.push(listener);
    }

    // Check for listener leak
    max = _getMaxListeners(target);
    if (max > 0 && existing.length > max && !existing.warned) {
      existing.warned = true;
      // No error code for this since it is a Warning
      const w = genericNodeError(
        `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` +
        `added to ${inspect(target, { depth: -1 })}. MaxListeners is ${max}. Use emitter.setMaxListeners() to increase limit`,
        { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length });
      process.emitWarning(w);
    }
  }
  return target;
}

EventEmitter.prototype.removeListener =
    function removeListener(type, listener) {
      checkListener(listener);

      const events = this._events;
      if (events === undefined)
        return this;

      const list = events[type];
      if (list === undefined)
        return this;

      if (list === listener || list.listener === listener) {
        this._eventsCount -= 1;

        if (this[kShapeMode]) {
          events[type] = undefined;
        } else if (this._eventsCount === 0) {
          this._events = { __proto__: null };
        } else {
          delete events[type];
          if (events.removeListener)
            this.emit('removeListener', type, list.listener || listener);
        }
      } else if (typeof list !== 'function') {
        let position = -1;
        if (list.isHandling === true)
          events[type] = list = arrayClone(list);
        for (let i = list.length - 1; i >= 0; i--) {
          if (list[i] === listener || list[i].listener === listener) {
            position = i;
            break;
          }
        }

        if (position < 0)
          return this;

        if (position === 0)
          list.shift();
        else {
          if (spliceOne === undefined)
            spliceOne = require('internal/util').spliceOne;
          spliceOne(list, position);
        }

        if (list.length === 1)
          events[type] = list[0];

        if (events.removeListener !== undefined)
          this.emit('removeListener', type, listener);
      }

      return this;
    };

    
function onceWrapper() {
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    this.wrapFn.listener = null;
    this.wrapFn = null;
    const result = (arguments.length === 0) ?
       this.listener.call(this.target):
       this.listener.apply(this.target, arguments);
    this.target = null;
    this.listener = null;
    return result;
  }
}

My points:

  1. [kErrorMonitor] event should be emitted at the beginning before handling the events, if there is a [kErrorMonitor] handler.
  2. The [result] is useful for debugging / catching error.
  3. Separate the default error handling codes into another function [_handleUncaughtError] for better readability.
  4. Setting [isHandling] flag to handler, in order to prevent unnecessary cloning, but the add/removeEventlistenser methods should also modified, which is a cost and lead to a performance downgrade ; Anyway I don't think the performance may not be significant changed because a single function event handler occurred in most real-life codes.
  5. De-reference wrapFn and listener after the wrap function fired.

@mertcanaltin
Copy link
Member Author

it looks great to me thank you very much for your research would you like to open a mr about it ? @simonkcleung

@simonkcleung
Copy link

Sorry I am not going to open one because I am not familiar with it.

@mertcanaltin
Copy link
Member Author

Okay, I'll be opening a pr that does what you say

@KunalKumar-1
Copy link
Contributor

KunalKumar-1 commented Jan 20, 2025

hey @mertcanaltin ,
recently i have been doing deep dive into events ... i would like to open a pr ,
with your help .

@mertcanaltin
Copy link
Member Author

hey @mertcanaltin , recently i have been doing deep dive into events ... i would like to open a pr , with your help .

hello of course, you can open a pr and let us review it, please feel free

KunalKumar-1 added a commit to KunalKumar-1/node that referenced this issue Jan 24, 2025
Refactored error handling logic for clarity and efficiency.
Streamlined error inspection and stringification process.
Enhanced stack trace handling with minimal performance overhead.
Simplified listener iteration and result handling in emit method.

Fixes: nodejs#53056
Signed-off-by: KUNAL KUMAR <[email protected]>
@KunalKumar-1 KunalKumar-1 linked a pull request Jan 24, 2025 that will close this issue
@KunalKumar-1
Copy link
Contributor

hey @mertcanaltin , recently i have been doing deep dive into events ... i would like to open a pr , with your help .

hello of course, you can open a pr and let us review it, please feel free

hey @mertcanaltin can you review it ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
events Issues and PRs related to the events subsystem / EventEmitter.
Projects
Status: Awaiting Triage
Development

Successfully merging a pull request may close this issue.

5 participants