-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
src: render N-API weak callbacks as cleanup hooks
Since worker threads are complete Node.js environments, including the ability to load native addons, and since those native addons can allocate resources to be freed when objects go out of scope, and since, upon worker thread exit, the engine does not invoke the weak callbacks responsible for freeing resources which still have references, this modification introduces tracking for weak references such that a list of outstanding weak references is maintained. This list is traversed during environment teardown. The callbacks for the remaining weak references are called. This change is also relevant for Node.js embedder scenarios, because in those cases the process also outlives the `node::Environment` and therefore weak callbacks should also be rendered as environment cleanup hooks to ensure proper cleanup after native addons. This changes introduces the means by which this can be accomplished. A benchmark is included which measures the time it takes to execute the weak reference callback for a given number of weak references. Re: tc39/proposal-weakrefs#125 (comment) PR-URL: #28428 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
- Loading branch information
Gabriel Schulhof
committed
Oct 13, 2019
1 parent
fce1a51
commit 53ca0b9
Showing
8 changed files
with
288 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
#include <stdlib.h> | ||
#define NAPI_EXPERIMENTAL | ||
#include <node_api.h> | ||
|
||
#define NAPI_CALL(env, call) \ | ||
do { \ | ||
napi_status status = (call); \ | ||
if (status != napi_ok) { \ | ||
napi_throw_error((env), NULL, #call " failed"); \ | ||
return NULL; \ | ||
} \ | ||
} while (0) | ||
|
||
static napi_value | ||
GetCount(napi_env env, napi_callback_info info) { | ||
napi_value result; | ||
size_t* count; | ||
|
||
NAPI_CALL(env, napi_get_instance_data(env, (void**)&count)); | ||
NAPI_CALL(env, napi_create_uint32(env, *count, &result)); | ||
|
||
return result; | ||
} | ||
|
||
static napi_value | ||
SetCount(napi_env env, napi_callback_info info) { | ||
size_t* count; | ||
|
||
NAPI_CALL(env, napi_get_instance_data(env, (void**)&count)); | ||
|
||
// Set the count to zero irrespective of what is passed into the setter. | ||
*count = 0; | ||
|
||
return NULL; | ||
} | ||
|
||
static void | ||
IncrementCounter(napi_env env, void* data, void* hint) { | ||
size_t* count = data; | ||
(*count) = (*count) + 1; | ||
} | ||
|
||
static napi_value | ||
NewWeak(napi_env env, napi_callback_info info) { | ||
napi_value result; | ||
void* instance_data; | ||
|
||
NAPI_CALL(env, napi_create_object(env, &result)); | ||
NAPI_CALL(env, napi_get_instance_data(env, &instance_data)); | ||
NAPI_CALL(env, napi_add_finalizer(env, | ||
result, | ||
instance_data, | ||
IncrementCounter, | ||
NULL, | ||
NULL)); | ||
|
||
return result; | ||
} | ||
|
||
static void | ||
FreeCount(napi_env env, void* data, void* hint) { | ||
free(data); | ||
} | ||
|
||
/* napi_value */ | ||
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) { | ||
napi_property_descriptor props[] = { | ||
{ "count", NULL, NULL, GetCount, SetCount, NULL, napi_enumerable, NULL }, | ||
{ "newWeak", NULL, NewWeak, NULL, NULL, NULL, napi_enumerable, NULL } | ||
}; | ||
|
||
size_t* count = malloc(sizeof(*count)); | ||
*count = 0; | ||
|
||
NAPI_CALL(env, napi_define_properties(env, | ||
exports, | ||
sizeof(props) / sizeof(*props), | ||
props)); | ||
NAPI_CALL(env, napi_set_instance_data(env, count, FreeCount, NULL)); | ||
|
||
return exports; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
'targets': [ | ||
{ | ||
'target_name': 'addon', | ||
'sources': [ | ||
'addon.c' | ||
] | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
'use strict'; | ||
const common = require('../../common'); | ||
const addon = require(`./build/${common.buildType}/addon`); | ||
const bench = common.createBenchmark(main, { n: [1e7] }); | ||
|
||
function callNewWeak() { | ||
addon.newWeak(); | ||
} | ||
|
||
function main({ n }) { | ||
addon.count = 0; | ||
bench.start(); | ||
while (addon.count < n) { | ||
callNewWeak(); | ||
} | ||
bench.end(n); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
'use strict'; | ||
|
||
if (process.argv[2] === 'child') { | ||
const common = require('../../common'); | ||
const test_general = require(`./build/${common.buildType}/test_general`); | ||
|
||
// The second argument to `envCleanupWrap()` is an index into the global | ||
// static string array named `env_cleanup_finalizer_messages` on the native | ||
// side. A reverse mapping is reproduced here for clarity. | ||
const finalizerMessages = { | ||
'simple wrap': 0, | ||
'wrap, removeWrap': 1, | ||
'first wrap': 2, | ||
'second wrap': 3 | ||
}; | ||
|
||
// We attach the three objects we will test to `module.exports` to ensure they | ||
// will not be garbage-collected before the process exits. | ||
|
||
// Make sure the finalizer for a simple wrap will be called at env cleanup. | ||
module.exports['simple wrap'] = | ||
test_general.envCleanupWrap({}, finalizerMessages['simple wrap']); | ||
|
||
// Make sure that a removed wrap does not result in a call to its finalizer at | ||
// env cleanup. | ||
module.exports['wrap, removeWrap'] = | ||
test_general.envCleanupWrap({}, finalizerMessages['wrap, removeWrap']); | ||
test_general.removeWrap(module.exports['wrap, removeWrap']); | ||
|
||
// Make sure that only the latest attached version of a re-wrapped item's | ||
// finalizer gets called at env cleanup. | ||
module.exports['first wrap'] = | ||
test_general.envCleanupWrap({}, finalizerMessages['first wrap']), | ||
test_general.removeWrap(module.exports['first wrap']); | ||
test_general.envCleanupWrap(module.exports['first wrap'], | ||
finalizerMessages['second wrap']); | ||
} else { | ||
const assert = require('assert'); | ||
const { spawnSync } = require('child_process'); | ||
|
||
const child = spawnSync(process.execPath, [__filename, 'child'], { | ||
stdio: [ process.stdin, 'pipe', process.stderr ] | ||
}); | ||
|
||
// Grab the child's output and construct an object whose keys are the rows of | ||
// the output and whose values are `true`, so we can compare the output while | ||
// ignoring the order in which the lines of it were produced. | ||
assert.deepStrictEqual( | ||
child.stdout.toString().split(/\r\n|\r|\n/g).reduce((obj, item) => | ||
Object.assign(obj, item ? { [item]: true } : {}), {}), { | ||
'finalize at env cleanup for simple wrap': true, | ||
'finalize at env cleanup for second wrap': true | ||
}); | ||
|
||
// Ensure that the child exited successfully. | ||
assert.strictEqual(child.status, 0); | ||
} |
Oops, something went wrong.
53ca0b9
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.
Hi, @gabrielschulhof! Looks like this broke
make bench-addons-build && tools/test.py test/benchmark/test-benchmark-napi.js
but that wasn't detected in the regular CI because the benchmark tests are only run during nightly CI. Not a big deal, and I'm guessing the fix is simple but haven't looked....53ca0b9
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.
@Trott checking it out ...
53ca0b9
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.
#29995 should address the breakage.