node-ffi-napi / ref-napi

Turn Buffer instances into "pointers"
MIT License
123 stars 67 forks source link

src: keep reference buffer alive longer #55

Closed mhdawson closed 3 years ago

mhdawson commented 3 years ago

Keep a buffer written by WritePointer alive until the finalizers for the buffer to which the pointer has been run have been executed:

Refs: https://github.com/node-ffi-napi/ref-napi/issues/54

Signed-off-by: Michael Dawson mdawson@devrus.com

coveralls commented 3 years ago

Coverage Status

Coverage decreased (-0.05%) to 74.359% when pulling 63cd2698a50f2b37120524a393af60f079cb7ba0 on mhdawson:write-pointer into b0809c25e5d9e4efa82ee6c323e1f961044b65e0 on node-ffi-napi:latest.

mhdawson commented 3 years ago

@addaleax any chance you can take a look at this?

mhdawson commented 3 years ago

@addaleax I know you've been busy, but wondering if you might be able to take a look at this.

mhdawson commented 3 years ago

@addaleax thanks for taking a look.

I saw the following code:

exports.writePointer = function writePointer (buf, offset, ptr) {
  debug('writing pointer to buffer', buf, offset, ptr);
  exports._writePointer(buf, offset, ptr);
  exports._attach(buf, ptr);
};

Which I thought was doing a similar association at the JS level. It looks to me like _attach adds a reference to ptr in buf, such that ptr won't be collected until after buf is collected. The comment in _attach says:

 * This function "attaches" _object_ to _buffer_ to prevent it from being garbage
 * collected.

So based on that my understanding is that only once buf is collected by the gc, will the ptr buffer be able to be collected. This PR was trying to do the same thing but ensure the order in which finalizers would be run. When buf is collected, the finalizer will run which will delete the references and then allow the ptr to be collected.

I understand the point about what happens if you write another pointer at the same offset but I can't find any code which removes the reference set in _attach so I was thinking that would already be the behavior but maybe I'm just missing something as I'm not familiar with the code.

mhdawson commented 3 years ago

I can see that maybe the problem is that its changed for _writePointer as well as writePointer. If that is the issue then we can look at refactoring so the addition only applies to writePointer versus _writePointer as well. Does that make sense?

addaleax commented 3 years ago

@mhdawson In that case, wouldn’t it make sense to remove the JS _attach code? It’s still buggy but you’re right, it’s not making anything worse per se, I guess.

mhdawson commented 3 years ago

@addaleax I pushed another commit to limit the change so that it only affects writePointer and not _writePointer.

From what I can see writePointer was already associating the object referenced by the pointer with the buffer, preventing it from being freed until the buffer was collected.

The change in this PR should now just mean that the object is not eligible to be collected until after the finalizers for the buffer have been run which is tied to when the buffer is freed just like before. ie, no change to what is kept alive.

To confirm this I wrote this simple test:

var ref = require('ref-napi');
var st = require('ref-struct-di')(ref);
var at = require('ref-array-di')(ref);

var idx = 0
const mainBuffer = Buffer.alloc(10)
setInterval(() => {
  const ptr = Buffer.alloc(100000);
  ref.writePointer(mainBuffer, 0, ptr);
  if (++idx % 1000 === 0) console.log(`${idx}th iteration`)
},1)

Running that test without the changes from this PR results in the heap space as reported by --trace-gc continually increasing.

If I remove the existing line exports._attach(buf, ptr); in writePointer then I see that the memory stays constant.

This confirms that the existing implementation of writePointer already keeps all buffers that are written to the buffer alive until the buffer they are written is eligible for collection even if they have been overwritten.

I also validated that with the changes from this PR, using the example above with _writePointer reports constant memory usage versus growing with --trace-gc. This is expected because it does not call _attach, and also is not affected by the updated PR.

I think this confirms that this PR only affects the timing of when buffers written with _writePointer will be eligible to be collected (such that this is only the case after the finalizer for the buffer they were written to has executed), versus adding a potential memory leak (as that potential was already present in the existing implementation).

I had also already run the original test in #54 PR under valgrind and --trace-gc with the PR and for the test case in #54 no memory is leaked.

mhdawson commented 3 years ago

I terms of removing the '_attach' code I'm happy to do that, was just not sure at the time if it was doing anything else. @addaleax should I push another commit to remove the _attach call?

mhdawson commented 3 years ago

Pushed a commit to remove _attach as we can always drop that commit if that's not the right thing to do.

mhdawson commented 3 years ago

I don't think the coverage failure should block this as its a result of removing code that was covered, versus adding code that was not covered.

addaleax commented 3 years ago

@mhdawson Thank you! This is published in 📦 ref-napi@3.0.2

mhdawson commented 3 years ago

@addaleax and thank you too :)

nidibm commented 3 years ago

I am still observing the issue with the fix also https://github.com/ibm-messaging/mq-mqi-nodejs/issues/108#issuecomment-808186080