Search code examples
node.jsmemory-managementv8jit

v8 memory spike (rss) when defining more than 1000 function (does not reproduce when using --jitless)


I have a simple node app with 1 function that defines 1000+ functions inside it (without running them). When I call this function (the wrapper) around 200 times the RSS memory of the process spikes from 100MB to 1000MB and immediately goes down. (The memory spike only happens after around 200~ calls, before that all the calls do not cause a memory spike, and all the calls after do not cause a memory spike)

This issue is happening to us in our node server in production, and I was able to reproduce it in a simple node app here:

https://github.com/gileck/node-v8-memory-issue

When I use --jitless pr --no-opt the issue does not happen (no spikes). but obviously we do not want to remove all the v8 optimizations in production.

This issue must be some kind of a specific v8 optimization, I tried a few other v8 flags but non of them fix the issue (only --jitless and --no-opt fix it)

Anyone knows which v8 optimization could cause this?

Update:

We found that --no-concurrent-recompilation fix this issue (No memory spikes at all). but still, we can't explain it. We are not sure why it happens and which code changes might fix it (without the flag).

As one of the answers suggests, moving all the 1000+ function definitions out of the main function will solve it, but then those functions will not be able to access the context of the main function which is why they are defined inside it.

Imagine that you have a server and you want to handle a request. Obviously, The request handler is going to run many times as the server gets a lot of requests from the client. Would you define functions inside the request handler (so you can access the request context in those functions) or define them outside of the request handler and pass the request context as a parameter to all of them? We chose the first option... what do you think?


Solution

  • anyone knows which v8 optimization could cause this?

    Load Elimination.

    I guess it's fair to say that any optimization could cause lots of memory consumption in pathological cases (such as: a nearly 14 MB monster of a function as input, wow!), but Load Elimination is what causes it in this particular case.
    You can see for yourself when your run with --turbo-stats (and optionally --turbo-filter=foo to zoom in on just that function).

    You can disable Load Elimination if you feel that you must. A preferable approach would probably be to reorganize your code somewhat: defining 2,000 functions is totally fine, but the function defining all these other functions probably doesn't need to be run in a loop long enough until it gets optimized? You'll avoid not only this particular issue, but get better efficiency in general, if you define functions only once each.

    There may or may not be room for improving Load Elimination in Turbofan to be more efficient for huge inputs; that's a longer investigation and I'm not sure it's worth it (compared to working on other things that likely show up more frequently in practice).

    I do want to emphasize for any future readers of this that disabling optimization(s) is not generally a good rule of thumb for improving performance (or anything else), on the contrary; nor are any other "secret" flags needed to unlock "secret" performance: the default configuration is very carefully optimized to give you what's (usually) best. It's a very rare special case that a particular optimization pass interacts badly with a particular code pattern in an input function.