You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Our module that has been migrated to N-API is experiencing a memory leak in version 10.15.3 but not in version 10.15.2. I verified that reverting the changes made in this pull request eliminates the memory leak. Clearly that isn't the right solution but hopefully it will point in the right direction!
I have a reproduction that works with our node-oracledb driver -- but if you don't have an Oracle Database handy that won't help much. If you do I can share the code that causes the issue; otherwise, it will take some time to develop a test case that demonstrates the issue!
The code for the driver can be found here. The code that is probably the most likely to be the source of the issue can be found here. Specifically, it acquires the value of a reference (for the constructor), creates an instance and then calls napi_wrap() to associate a structure with the newly created object.
I attach a zip file containing a simple module that simply creates an instance and wraps it: issue_26667.zip. The pattern suggests that there is a race condition involved and that only occasionally it leaks -- but if you do it often enough it becomes obvious. Hopefully this is sufficient to track down the issue. After building the module, you can run the script as follows:
NODE_PATH=build/Release node --expose-gc demo.js
This will create a file called stats.txt which contains a dump of the number of iterations processed as well as the RSS and heap size.
The pattern suggests that there is a race condition involved and that only occasionally it leaks -- but if you do it often enough it becomes obvious. Hopefully this is sufficient to track down the issue.
@anthony-tuininga Are you saying that only occasionally there is a leak visible when running the program, or that you need many iterations within a single process in order to see the leak?
Because I’m having a hard time seeing an actual leak here – at the very least, the finalizer callback always seems to be called for exactly 25000 objects each iteration (so, the expected amount), at least locally…
What I meant was that the amount of memory lost is not sufficient to indicate that memory is being lost each iteration. Instead memory must be lost on some iterations only. Unfortunately, I ran both 10.15.2 and 10.15.3 with my simple module and discovered that both leak memory, albeit very slowly. I ran each of the loops 4 billion times and heap memory increased about 4 MB in both cases and RSS about 10 MB. So my example doesn't demonstrate the problem. :-(
But there is a marked difference between the two versions when using the node-oracledb driver test case and the difference goes away if I revert the pull request I mentioned earlier. I'll see if I can find a better test case next week.
Ok. I created a cut-down version of the node-oracledb module which has all of the Oracle bits ripped out. It demonstrates the problem. With Node.js 10.15.2 it stabilises around 350,000 iterations but with Node.js 10.15.3 it increases at around 100 bytes/iteration.
This is what I see running your example on master:
``
Num Iters,RSS,Heap Total,Heap Used,Ext
25000,84611072,43147264,2840488,9146
50000,82440192,39477248,2716024,9146
75000,84344832,38952960,2722368,9146
100000,84377600,39477248,2724104,9146
125000,86728704,39477248,2724104,9146
150000,89051136,39477248,2724104,9146
175000,91439104,39477248,2724240,9146
200000,94646272,38428672,2725080,9146
225000,96698368,38952960,2725080,9146
250000,99041280,39477248,2713416,9146
275000,101924864,38428672,2725080,9146
300000,104325120,38428672,2725088,9146
325000,106901504,39477248,2725088,9146
350000,109899776,38428672,2725088,9146
375000,111890432,38428672,2725088,9146
400000,114360320,38952960,2725088,9146
425000,116314112,39477248,2725088,9146
450000,119160832,39477248,2725088,9146
475000,121499648,38952960,2713424,9146
500000,124059648,39477248,2725088,9146
525000,126201856,38952960,2725088,9146
550000,128806912,38952960,2713424,9146
575000,131485696,38952960,2725088,9146
600000,134086656,38952960,2726368,9146
625000,136589312,39477248,2714704,9146
650000,139042816,38428672,2726368,9146
675000,141357056,38952960,2726368,9146
It that similar to what you were seeing on 10.15.3?
@hashseed one thing I thought of is that I think the check in #24494 assumes that the Finalizer called by the gc will run on the main thread as opposed to some other thread. Can you confirm that is a valid assumption?
I am running into this issue or something very closely related. I am using buffers with external memory. The fix does not seem to help – I am testing this on Node.js v12.10.0 on macOS.
I have modified the test case to demonstrate this behaviour with napi_create_external_buffer(). The finalizer is never run at all!
However the finalizers are run if we occasionally allow the event loop to continue with setImmediate(). See the modified demo.js: demo_modified.js.zip. This is quite clear from the log, memory usage stabilises pretty quickly:
@bnoordhuis Thanks for the quick response. Is there any way to release memory earlier, or is this just the way things are expected to work with external buffers? Seems like it should be possible to release the memory sooner... :/
@rolftimmermans It's by design. Finalizers can call into JS land. If n-api didn't defer the callback, it'd be pretty easy to create a cb→js→cb→js→etc. stack overflow
Seems the behaviour I saw only applies to napi_create_external_buffer() and not to napi_create_external_arraybuffer(). The latter works fine and leaks no memory in similar circumstances.
I think this can be closed out as the issue was addressed, and 10.x is also now out of service. Please let me know if that was not the right thing to do.
Activity
addaleax commentedon Mar 14, 2019
Is there any chance you could share a reproduction?
anthony-tuininga commentedon Mar 14, 2019
I have a reproduction that works with our node-oracledb driver -- but if you don't have an Oracle Database handy that won't help much. If you do I can share the code that causes the issue; otherwise, it will take some time to develop a test case that demonstrates the issue!
addaleax commentedon Mar 14, 2019
Depending on the complexity, looking at the code might help, because we know we’re looking for code that uses
napi_create_reference()et al?/cc @mhdawson as the PR author
anthony-tuininga commentedon Mar 14, 2019
The code for the driver can be found here. The code that is probably the most likely to be the source of the issue can be found here. Specifically, it acquires the value of a reference (for the constructor), creates an instance and then calls napi_wrap() to associate a structure with the newly created object.
anthony-tuininga commentedon Mar 14, 2019
I attach a zip file containing a simple module that simply creates an instance and wraps it:
issue_26667.zip. The pattern suggests that there is a race condition involved and that only occasionally it leaks -- but if you do it often enough it becomes obvious. Hopefully this is sufficient to track down the issue. After building the module, you can run the script as follows:
This will create a file called stats.txt which contains a dump of the number of iterations processed as well as the RSS and heap size.
addaleax commentedon Mar 16, 2019
@anthony-tuininga Are you saying that only occasionally there is a leak visible when running the program, or that you need many iterations within a single process in order to see the leak?
Because I’m having a hard time seeing an actual leak here – at the very least, the finalizer callback always seems to be called for exactly 25000 objects each iteration (so, the expected amount), at least locally…
anthony-tuininga commentedon Mar 16, 2019
What I meant was that the amount of memory lost is not sufficient to indicate that memory is being lost each iteration. Instead memory must be lost on some iterations only. Unfortunately, I ran both 10.15.2 and 10.15.3 with my simple module and discovered that both leak memory, albeit very slowly. I ran each of the loops 4 billion times and heap memory increased about 4 MB in both cases and RSS about 10 MB. So my example doesn't demonstrate the problem. :-(
But there is a marked difference between the two versions when using the node-oracledb driver test case and the difference goes away if I revert the pull request I mentioned earlier. I'll see if I can find a better test case next week.
anthony-tuininga commentedon Mar 18, 2019
Ok. I created a cut-down version of the node-oracledb module which has all of the Oracle bits ripped out. It demonstrates the problem. With Node.js 10.15.2 it stabilises around 350,000 iterations but with Node.js 10.15.3 it increases at around 100 bytes/iteration.
issue_26667.zip
Once unzipped and built, run with the following command:
anthony-tuininga commentedon Mar 20, 2019
@addaleax, with the new test case are you able to replicate the issue?
mhdawson commentedon Mar 20, 2019
This is what I see running your example on master:
``
Num Iters,RSS,Heap Total,Heap Used,Ext
25000,84611072,43147264,2840488,9146
50000,82440192,39477248,2716024,9146
75000,84344832,38952960,2722368,9146
100000,84377600,39477248,2724104,9146
125000,86728704,39477248,2724104,9146
150000,89051136,39477248,2724104,9146
175000,91439104,39477248,2724240,9146
200000,94646272,38428672,2725080,9146
225000,96698368,38952960,2725080,9146
250000,99041280,39477248,2713416,9146
275000,101924864,38428672,2725080,9146
300000,104325120,38428672,2725088,9146
325000,106901504,39477248,2725088,9146
350000,109899776,38428672,2725088,9146
375000,111890432,38428672,2725088,9146
400000,114360320,38952960,2725088,9146
425000,116314112,39477248,2725088,9146
450000,119160832,39477248,2725088,9146
475000,121499648,38952960,2713424,9146
500000,124059648,39477248,2725088,9146
525000,126201856,38952960,2725088,9146
550000,128806912,38952960,2713424,9146
575000,131485696,38952960,2725088,9146
600000,134086656,38952960,2726368,9146
625000,136589312,39477248,2714704,9146
650000,139042816,38428672,2726368,9146
675000,141357056,38952960,2726368,9146
anthony-tuininga commentedon Mar 20, 2019
Yes. If you run with Node.js 10.15.2 you will see that the RSS remains relatively stable.
mhdawson commentedon Mar 20, 2019
I'll run with valgrind to see if anything obvious shows up.
mhdawson commentedon Mar 20, 2019
@hashseed one thing I thought of is that I think the check in #24494 assumes that the Finalizer called by the gc will run on the main thread as opposed to some other thread. Can you confirm that is a valid assumption?
24 remaining items
anthony-tuininga commentedon Apr 5, 2019
I have verified that a copy of 10.15.3, patched with the PR for optimization #27085 resolves the memory leak.
mhdawson commentedon Apr 8, 2019
@anthony-tuininga, plan to land #27085 in master tomorrow.
anthony-tuininga commentedon Apr 8, 2019
Great. Thanks!
n-api: reduce gc finalization stress
orgads commentedon Aug 15, 2019
Was this fixed in v10, or only in v12?
mhdawson commentedon Aug 15, 2019
I believe it landed in master before 12.x was cut so it has #27085 as well.
orgads commentedon Aug 15, 2019
But this is a regression in 10, right? What's the backporting policy?
rolftimmermans commentedon Oct 8, 2019
I am running into this issue or something very closely related. I am using buffers with external memory. The fix does not seem to help – I am testing this on Node.js v12.10.0 on macOS.
I have modified the test case to demonstrate this behaviour with
napi_create_external_buffer(). The finalizer is never run at all!issue_26667_buffer.zip
These are my results:
However the finalizers are run if we occasionally allow the event loop to continue with
setImmediate(). See the modified demo.js: demo_modified.js.zip. This is quite clear from the log, memory usage stabilises pretty quickly:I hope this helps!
bnoordhuis commentedon Oct 8, 2019
@rolftimmermans That's working as expected. Your code has a finalizer and those are always deferred to the next event loop tick:
node/src/node_api.cc
Lines 38 to 59 in 064e111
(Note that
SetImmediate()call.)rolftimmermans commentedon Oct 8, 2019
@bnoordhuis Thanks for the quick response. Is there any way to release memory earlier, or is this just the way things are expected to work with external buffers? Seems like it should be possible to release the memory sooner... :/
bnoordhuis commentedon Oct 8, 2019
@rolftimmermans It's by design. Finalizers can call into JS land. If n-api didn't defer the callback, it'd be pretty easy to create a cb→js→cb→js→etc. stack overflow
rolftimmermans commentedon Oct 8, 2019
Seems the behaviour I saw only applies to
napi_create_external_buffer()and not tonapi_create_external_arraybuffer(). The latter works fine and leaks no memory in similar circumstances.I'll open a new issue to address this.
bnoordhuis commentedon Oct 8, 2019
That's because it doesn't defer the callback:
node/src/js_native_api_v8.cc
Lines 305 to 325 in 064e111
I'd say that's an oversight, possibly due to a misunderstanding of how the two GC passes work.
mhdawson commentedon May 27, 2021
I think this can be closed out as the issue was addressed, and 10.x is also now out of service. Please let me know if that was not the right thing to do.