Skip to content

perf: use promises rather than setImmediate so that I/O is executed within the current task stack#1100

Merged
G-Rath merged 1 commit intostreamich:masterfrom
VisualSJ:master
May 15, 2025
Merged

perf: use promises rather than setImmediate so that I/O is executed within the current task stack#1100
G-Rath merged 1 commit intostreamich:masterfrom
VisualSJ:master

Conversation

@VisualSJ
Copy link
Copy Markdown
Contributor

@VisualSJ VisualSJ commented May 2, 2025

If setImmediate is used, when there are too many setTimeout, IO will be delayed in execution, and this execution will cause the IO operation interval to become extremely long.
Referring to the NodeJS source code, I/O operations should use microtasks

@VisualSJ VisualSJ changed the title [Bug] The IO interface should be executed within the current task stack The IO interface should be executed within the current task stack May 2, 2025
@G-Rath
Copy link
Copy Markdown
Collaborator

G-Rath commented May 3, 2025

Is there any kind of test or samples you can provide on this? I don't have a big problem with this as is since it gets rid of a file, but I'd like to understand a bit more about the background and (scale of) impact if possible

@VisualSJ
Copy link
Copy Markdown
Contributor Author

VisualSJ commented May 9, 2025

Inside wrapAsync, it is implemented using setImmediate. In this case, it will create a new macro task at the end of the JavaScript task queue. The actual I/O operation will only commence when the macro task is executed.

That is to say, when the user initiates a readFile request, it will be inserted at the end of the JavaScript task queue. The actual I/O operation will only start after the current JavaScript task queue has been completely executed.

When using microtasks (Promise), it is equivalent to inserting a high-priority task into the task queue. It will be executed immediately at the end of the current macro task instead of being placed at the end of all macro tasks.

When there is a for loop that executes readFile within the loop and there are a large number of setTimeout calls in the program, the execution speed will become very slow.

@VisualSJ
Copy link
Copy Markdown
Contributor Author

VisualSJ commented May 9, 2025

test.a.js

function wrap(callback) {
  setImmediate(() => {
    callback();
  });
}
function readFile(file, callback) {
  wrap(() => {
    // nothing
    callback();
  });
}

let timer = null;
function wait() {
  timer = setTimeout(() => {
    const date = Date.now();
    while (Date.now() - date < 100) {
      // nothing
    }
    wait();
  });
}

(async () => {
  wait();
  console.time('A');
  for (let i = 0; i < 1000; i++) {
    await new Promise(resolve => {
      readFile('a.txt', resolve);
    });
  }
  console.timeEnd('A');

  clearTimeout(timer);
})();

test.b.js

function wrap(callback) {
  Promise.resolve().then(() => {
    callback();
  });
}
function readFile(file, callback) {
  wrap(() => {
    // nothing
    callback();
  });
}

let timer = null;
function wait() {
  timer = setTimeout(() => {
    const date = Date.now();
    while (Date.now() - date < 100) {
      // nothing
    }
    wait();
  });
}

(async () => {
  wait();
  console.time('B');
  for (let i = 0; i < 1000; i++) {
    await new Promise(resolve => {
      readFile('a.txt', resolve);
    });
  }
  console.timeEnd('B');

  clearTimeout(timer);
})();

You can try to execute the above two test scripts:

node test.a.js && node test.b.js

Note that do not execute two tests in one file, as some mechanisms in js make the one that is executed first slower

image

@VisualSJ VisualSJ changed the title The IO interface should be executed within the current task stack fix(wrapAsync): The IO interface should be executed within the current task stack May 9, 2025
@VisualSJ VisualSJ changed the title fix(wrapAsync): The IO interface should be executed within the current task stack fix: The IO interface should be executed within the current task stack May 9, 2025
@VisualSJ
Copy link
Copy Markdown
Contributor Author

@G-Rath
Do I need to add any more information?
I changed the PR name according to the rules, but it seems that CI didn't recheck? Could you help trigger this manually and give it a try? Or could you tell me how to change the PR name? 😅

@G-Rath G-Rath changed the title fix: The IO interface should be executed within the current task stack perf: use promises rather than setImmediate so that I/O is executed within the current task stack May 15, 2025
Copy link
Copy Markdown
Collaborator

@G-Rath G-Rath left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're all good, I just hadn't gotten back around to this - I have updated the title to reflect this is a performance change and describe better what the actual change was, but that's just a nit thing :)

@G-Rath G-Rath merged commit 786072f into streamich:master May 15, 2025
9 of 10 checks passed
github-actions Bot pushed a commit that referenced this pull request May 15, 2025
## [4.17.2](v4.17.1...v4.17.2) (2025-05-15)

### Performance Improvements

* use promises rather than `setImmediate` so that I/O is executed within the current task stack ([#1100](#1100)) ([786072f](786072f))
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 4.17.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants