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

Zombie process #1566

Closed
Ty3uK opened this issue Aug 31, 2021 · 8 comments
Closed

Zombie process #1566

Ty3uK opened this issue Aug 31, 2021 · 8 comments

Comments

@Ty3uK
Copy link

Ty3uK commented Aug 31, 2021

Hi!

In my project I'm using esbuild inside worker_threads. In watch mode worker starts on external file change (I'm using chokidar).

On macOS 11.5.2 I noticed that esbuild process becomes a zombie after successfull build.

CleanShot 2021-09-01 at 00 49 45@2x

Also I found some similar issue - #643

@evanw
Copy link
Owner

evanw commented Sep 1, 2021

From the graph, it looks like the parent process is still running? To me, a zombie process means one where the child continues without exiting even after the parent exits. Using the JS esbuild API starts a long-lived child process for the rest of the session that is reused for future JS API calls. So what you are showing appears to be expected behavior.

@Ty3uK
Copy link
Author

Ty3uK commented Sep 1, 2021

Yep, parent process is running, but inside worker I called process.exit().

Calling child.kill() from esbuild/lib/main.js has no effect too.

If this is expected behavior - how I can stop esbuild process? In my project I want to re-run esbuild on file change, but without bundle option. So I need to refresh entriesList on some changes (unlink, add). How can I refresh entries without restarting child process?

@Ty3uK
Copy link
Author

Ty3uK commented Sep 1, 2021

To me, a zombie process means one where the child continues without exiting even after the parent exits

I got into a situation where I had 6 esbuild processes and htop marks all of them as Z

UPD: like this
изображение

@evanw
Copy link
Owner

evanw commented Sep 1, 2021

That does sounds like a zombie process then. Can you provide instructions for how to replicate your situation (or provide some code to demonstrate the issue)?

@Ty3uK
Copy link
Author

Ty3uK commented Sep 1, 2021

Yep, I will make a repro code for you

@Ty3uK
Copy link
Author

Ty3uK commented Sep 1, 2021

@evanw
Copy link
Owner

evanw commented Sep 1, 2021

After some investigation the problem appears to be that in node, ending a worker thread means any child processes created by that thread turn into a zombie process when they die. I believe this means that node isn't reading the exit status of the process so the OS is keeping the process table entry around until it does. I think this is maybe a bug in node? At least there doesn't seem to be anything I can do about this on my end. Things I tried:

  • I thought doing this inside esbuild's library might work:

    process.on('exit', () => {
      child.kill()
    })

    But this still makes zombie processes.

  • I also tried having esbuild's library do this:

    let exit = process.exit
    process.exit = function() {
      child.kill()
      return exit.apply(this, arguments)
    }

    But that didn't work either.

  • For completeness, I tried this too:

    let exit = process.exit
    process.exit = function() {
      child.kill()
      setTimeout(() => exit.apply(this, arguments))
    }

    That did work, but monkey-patching process.exit like this changes its semantics so that's not something I can ship. Also this doesn't handle the case where you don't call process.exit() and the thread just exits naturally.

I think this may mean that node requires at least one full turn of the event loop in between the call to child.kill() and the thread actually exiting so I won't be able to do anything to avoid creating a zombie process at the point where process.exit is called. It's too late by then. This problem seems like something that needs to be fixed within node itself.

However, there's really no reason you need to be using worker threads like this. It's likely both slower and less efficient than just calling esbuild's API directly. The underlying implementation uses Go and already spreads the work out over all available CPU cores. Using worker threads to call esbuild's API like this is very inefficient because it means every build involves spawning a new JS VM, a new child process (which can be quite slow on some OSes), and creating two new additional GC threads (since JS and Go both have multi-threaded GCs). All of that extra CPU and memory usage is avoided by just calling esbuild's API directly.

So I recommend avoiding this issue by just removing the worker entirely.

@Ty3uK
Copy link
Author

Ty3uK commented Sep 1, 2021

Thank you for so detailed answer!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants