Closed mhdawson closed 3 years ago
@addaleax any chance you can take a look at this?
@addaleax I know you've been busy, but wondering if you might be able to take a look at this.
@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.
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?
@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.
@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.
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?
Pushed a commit to remove _attach as we can always drop that commit if that's not the right thing to do.
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.
@mhdawson Thank you! This is published in 📦 ref-napi@3.0.2
@addaleax and thank you too :)
I am still observing the issue with the fix also https://github.com/ibm-messaging/mq-mqi-nodejs/issues/108#issuecomment-808186080
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