-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
weak handles and promises in progress stuff #5292
Conversation
Here's a reduced version of your testcase. process.on('unhandledRejection', function(reason, promise) {
console.log(`got unhandled rejection at ${promise} with reason ${reason}`);
});
Promise.reject(new Error('foo')); GC doesn't give you a guarantee on when the weak callback will be called. Here's the comment about weak callbacks (c.f. v8.h): /**
* Install a finalization callback on this object.
* NOTE: There is no guarantee as to *when* or even *if* the callback is
* invoked. The invocation is performed solely on a best effort basis.
* As always, GC-based finalization should *not* be relied upon for any
* critical form of resource management!
*/ I think what is happening in your case is that the event loop drains and node terminates before GC has had a chance to run and call your weak callbacks. To verify this theory, I tried the following test-case: process.on('unhandledRejection', function(reason, promise) {
console.log(`got unhandled rejection at ${promise} with reason ${reason}`);
});
Promise.reject(new Error('foo'));
setTimeout(function() {
gc(); gc(); gc();
}, 1000); There are some bugs in your patch, but I think this works better. |
Hmmm, that is perhaps too unreliable for the user event (I was thinking of wrapping it in an immediate so that it was not directly fired off of GC), but could still be used for our own warning / error purposes. @ofrobots I realize this is probably not implemented in V8, but is it possible for a JS vm to detect when an object has gone out of scope/has no references rather than waiting for when GC is needed? Or is that expensive enough that it is wrapped into GC? |
I think it should be possible to implement a vm with a reference counting GC that could give you immediate finalization. This would be slower however. Modern GC algorithms tend to optimize for peak performance rather than immediacy of object finalization. |
@@ -0,0 +1,72 @@ | |||
#include "env.h" | |||
#include "env-inl.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not following why "track-promise.h"
isn't included here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trevnorris Actually, it seemed to function without anything being included. I really don't know why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems you have a copy of the TrackPromise
class duplicated in this file below – that's why it works.
If nothing else, other than this files, needs to know about the TrackPromise
class, you don't need track-promise.h
.
@ofrobots Was it explained that if the Promise was made Persistent immediately then allowed to leave scope, it would eventually be GC'd. But as soon as it was rejected the WeakCallback would no longer fire. Even though the rejected Promise was in fact being cleaned up (verified this by only making rejected Promises en mass, and not seeing memory usage increase despite the weak callbacks never firing). |
@trevnorris Do you already have a test-case for the 'making rejected Promises en mass' scenario that I can play with? |
@ofrobots I'm pretty (90%) sure he was just doing this: (Or at least that is what he posted to me in slack like 2? weeks ago hahaha) 'use strict';
(function runner(n) {
if (--n <= 0)
return;
new Promise(function(res, rej) { rej({}) });
process.nextTick(runner, n);
}(1e5)); |
FWIW it works using node-weak: var weak = require('weak')
process.on('unhandledRejection', function(reason, promise) {
weak(promise, () => console.log('gc'));
});
Promise.reject(new Error('foo'));
setTimeout(() => gc(), 1000); |
@Fishrock123 @trevnorris In that test-case if I change |
@ofrobots / @trevnorris I've updated the code to only try to a default action on GC, but I'm getting a fatal error: (My terminal is
Using this test case: try {
console.log('hello')
new Promise(function(res, rej) {
console.log('this is a promise!')
consol.log('oops')
});
} catch (err) {
console.error('caught error:', err)
}
// do some stuff, spend some time, idk
var i = 5
var timeout = setInterval(() => {
gc()
var what = { i }
if (i-- === 0) {
clearTimeout(timeout)
}
console.log(what.i)
}, 200) |
if (!fn.IsEmpty()) { | ||
HandleScope scope(isolate); | ||
Local<Value> promise = object.As<Value>(); | ||
fn->Call(env->context(), Null(env->isolate()), 1, &promise).ToLocalChecked(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks that error is from here, am I doing something incorrectly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can drop the .ToLocalChecked()
. I don't think you need it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that gives me this warning:
[38/39] CXX obj/src/node.track-promise.o
../../src/track-promise.cc:67:5: warning: ignoring return value of function declared with warn_unused_result attribute [-Wunused-result]
fn->Call(env->context(), Null(env->isolate()), 1, &promise);
^~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you can ignore for now, but eventually you will need to decide whether you need to do anything with the value that the fn
returns. The reason ToLocalChecked
doesn't work is because that function asserts that the Local
be non-empty. Maybe look at using FromMaybe
instead.
Alternatively you can ensure that fn
always returns non-empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value is an empty handle when the JS function throws an exception so .ToLocalChecked()
is almost always wrong for function calls.
Aside, I'm not sure if it's allowed to call into the VM from inside a weak callback. The VM state is EXTERNAL at that point so it's probably okay, but...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. I think the original intent of fn
is to conditionally abort the process here. It should be do-able in C++.
Also note that with the new style weakness (which becomes the only non-deprecated option with V8 4.9), the promise object is already be GC'd by the time the weak callback is called and you will not have a handle to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I'm trying to move it to C++ but I'm not sure how I can iterate over a Local<Array>
in C++, so I'm going to try doing that in JS for now...
Here's as slightly lengthy native module for testing: #include <v8.h>
#include <node.h>
using namespace v8;
class CallbackInfo {
public:
static inline CallbackInfo* New(Isolate* isolate, Local<Promise> promise);
private:
static void WeakCallback(
const WeakCallbackData<Promise, CallbackInfo>&);
inline void WeakCallback(Isolate* isolate, Local<Promise> promise);
inline CallbackInfo(Isolate* isolate, Local<Promise> promise);
~CallbackInfo();
Persistent<Promise> persistent_;
};
CallbackInfo* CallbackInfo::New(Isolate* isolate, Local<Promise> promise) {
return new CallbackInfo(isolate, promise);
}
CallbackInfo::CallbackInfo(Isolate* isolate, Local<Promise> promise)
: persistent_(isolate, promise) {
persistent_.SetWeak(this, WeakCallback);
persistent_.MarkIndependent();
}
CallbackInfo::~CallbackInfo() {
persistent_.Reset();
}
void CallbackInfo::WeakCallback(
const WeakCallbackData<Promise, CallbackInfo>& data) {
/* debug:start */
fprintf(stderr, "CallbackInfo::WeakCallback\n");
/* debug:stop */
data.GetParameter()->WeakCallback(data.GetIsolate(), data.GetValue());
}
void CallbackInfo::WeakCallback(Isolate* isolate, Local<Promise> promise) {
delete this;
}
void MakeWeak(const FunctionCallbackInfo<Value>& args) {
assert(args[0]->IsPromise());
CallbackInfo::New(args.GetIsolate(), args[0].As<Promise>());
}
void Init(Handle<Object> target) {
NODE_SET_METHOD(target, "makeWeak", MakeWeak);
}
NODE_MODULE(addon, Init) and the JS: 'use strict';
const makeWeak = require('./build/Release/addon').makeWeak;
(function() {
for (var i = 0; i < 1e3; i++)
makeWeak(Promise.reject(1));
}());
gc(); I've run this against master and also the v8 4.9 branch. if you change /cc @ofrobots @bnoordhuis |
This makes sense, we have strong references to rejected promises as a part of |
@vkurchatkin we get rid of those after we fire/default on |
@Fishrock123 yep, but in @trevnorris's example |
Oh hmmm, even your example uses |
ac75659
to
3af1162
Compare
I've updated this. Contains some debug code, I haven't quite gotten the "Natural GC" Test case: var p = new Promise(function(res, rej) {
console.log('this is a promise!')
consol.log('oops')
});
// do some stuff, spend some time, idk
var i = 200
var timeout = setInterval(() => {
var j = 50;
var string;
while (j--) {
var buf = new Buffer(50000);
string = buf.toString('utf8')
}
var what = { i }
if (i === 0) {
clearTimeout(timeout)
}
i--
}, 20) |
Currently testing the fast-exist case: new Promise(function(res, rej) {
console.log('this is a promise!')
consol.log('oops')
}); Current output is:
... I'm not sure why |
3af1162
to
52d1396
Compare
Looks like it's working. CI: https://ci.nodejs.org/job/node-test-pull-request/2376/ |
|
||
TrackPromise::New(env->isolate(), promise); | ||
|
||
HandleScope scope(env->isolate()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the HandleScope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure I needed it to make new Local
s?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JS -> C++ callbacks have an implicit HandleScope, you don't need to create one yourself.
Making a new PR. |
Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 PR-URL: nodejs#6375
Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 PR-URL: nodejs#6375
Refs: nodejs#12010 Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 PR-URL: nodejs#6375
src: use std::map for the promise reject map Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 Refs: nodejs#6375
Important: Please do not discuss the merits of this here. I will make the case for it once it works. This will be put into a new PR once ready. For now, I am only interested in figuring out why this does not work.
I'm not sure where to ask this with the full details, so I guess here will work.
This attempts to make a GC handler for unhandled promise rejections, but currently the weak handle appears to make the promise un-GC-able. I worked with @trevnorris on this and his conclusion was also that it is probably a V8 bug, but I'd like more review on the method.
This problem persists when applied onto the
vee-eight-4.9
branch.Here's the test failure showing it with this patch:
cc @nodejs/v8