Skip to content
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

Feat/libjs #59

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

Feat/libjs #59

wants to merge 18 commits into from

Conversation

telamon
Copy link

@telamon telamon commented Jan 24, 2025

moved to libjs and napi-compat

@telamon telamon requested a review from kasperisager January 24, 2025 09:58
Copy link
Contributor

@kasperisager kasperisager left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're mostly missing microtask checkpoints in I/O callbacks so quite a few repeated comments to hopefully catch all the cases.

binding.js Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
binding.c Outdated Show resolved Hide resolved
@telamon
Copy link
Author

telamon commented Jan 24, 2025

@kasperisager

We're mostly missing microtask checkpoints in I/O callbacks so quite a few repeated comments to hopefully catch all the cases.

I'm a little bit upset about the js calls with checkpoints still, because I originally just ported previous calls that used checkpoints (or rather all calls were checkpointed before since most run on I/O) but that blew up..

Is my understanding correct?

  • Scenario A) JS -> native_func() -> js_call_function(); // js already executing, checkpoint forbidden

  • Scenario B) on uv_async|uv_poll|(any non-js entry points incl uv_timers) -> native_func() -> js_call_function_with_checkpoint() // checkpoint required

Also in scenario B; if any recoverable exceptions occurs during js-call;

Then after native-recovery branch taken; the exception must be cleared and forwarded to uncaught handler to avoid process exit

@kasperisager
Copy link
Contributor

That's almost correct, yes, with the exception of the last bit. Exceptions aren't allowed to escape to the outermost call site where no JavaScript remains to handle them. For this reason, by the time the error code reaches the outermost call site, the uncaught exception handler will have already been invoked. This is the distinction between the two possible libjs error codes, js_pending_exception and js_uncaught_exception. Only pending exceptions can be handled and there will never be exceptions pending by the time the outermost call site returns.

Imagine calling into libjs directly from main(): Any function you call will automatically perform a microtask checkpoint and invoke the uncaught exception handler if necessary. There's no risk of forgetting to perform a microtask checkpoint or let an exception go unhandled.

@telamon
Copy link
Author

telamon commented Jan 24, 2025

Ah! I see, yes that makes sense

binding.c Outdated
Comment on lines 729 to 735
// TODO: tricky!
// Due branch linked below
// https://github.com/holepunchto/libudx/blob/de408f4ae9cde4139938b09b4f443f2d45f22c9b/src/udx.c#L2577-L2580
// this function is either called on IO / udx:process_packet()
// or immediately on parent call "udx_napi_stream_change_remote" which is on current js-context.
err = js_call_function_with_checkpoint(env, ctx, callback, 0, NULL, NULL);
clear_pending(env, err);
Copy link
Author

@telamon telamon Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kasperisager what is the best course of action here?
tl;dr on_udx_stream_remote_changed can be called both by JS and I/O.

rewrite to an intermediary callback that uv_async_send() real callback?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mafintosh Anything we can do on the libudx side to avoid this? Having a callback where we can't reason about the state of the JavaScript execution stack makes Node-API compatibility difficult.

Copy link
Author

@telamon telamon Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed via holepunchto/libudx#243 + 6f61b85

Now libUDX's udx_stream_change_remote(..., callback) only every calls the callback from I/O.

If change is possible immediately the udx function returns 1;
In which case we invoke the remote-changed callback without a checkpoint.

@telamon telamon requested a review from kasperisager January 29, 2025 17:15
Comment on lines +1800 to +1825
uint32_t
udx_napi_typed_stream_writev (
js_value_t *receiver,
js_value_t *stream_handle,
js_value_t *req_handle,
uint32_t rid,
js_value_t *buffers,
js_typed_callback_info_t *info
) {
int err;
js_env_t *env = NULL;
err = js_get_typed_callback_info(info, &env, NULL);
assert(err == 0);

udx_stream_t *stream;
size_t stream_len;
err = js_get_typedarray_info(env, stream_handle, NULL, (void **) &stream, &stream_len, NULL, NULL);
assert(err == 0);

udx_stream_write_t *req;
size_t req_len;
err = js_get_typedarray_info(env, req_handle, NULL, (void **) &req, &req_len, NULL, NULL);
assert(err == 0);

return udx_napi__stream_writev(env, stream, req, rid, buffers);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been tested? It'll likely abort if called as both the function itself and udx_napi__stream_writev() allocate local handles which isn't possible without a handle scope.

Copy link
Author

@telamon telamon Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kasperisager yes it's been tested & callgrinded, no aborts were triggered.
But that does not diminish your concern.. which handle allocations do you mean?

(this was my reference for typedarray access in typed function paths)

edit: the callgrind also showed that helper function udx_napi__stream_writev() was not inlined as i had hoped.
So would like to redo this path right.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

js_get_typedarray_info() will allocate a local handle to the underlying ArrayBuffer, which ought to fail if there's no open handle scope. Granted, that does rely on my likely outdated assumption that V8 still doesn't allow active handle scopes in fast functions. That seems to have changed if the callback works just fine though!

Copy link
Author

@telamon telamon Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hang on... which runtime are you referring to? I have a feeling the lack of abort might be accidental
I just re-did the grinds and discovered that node.js never enters the typed calls

image

bare on the other hand does

re-measured without valgrind using counters to verify:
Bare: { write: 5160, write_typed: 2008, writev: 1117, writev_typed: 27555 }
Node: { write: 7168, write_typed: 0, writev: 28672, writev_typed: 0 }

But back to the original topic, would it be safer to js_open_handle_scope() before allocating the local handles?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node.js, specifically Node-API, doesn't support fast calls so that's expected. To keep performance optimal, typed callbacks should ideally never allocate local handles. For TypedArray access, this can be achieved by using js_get_typedarray_view() which is guaranteed to never allocate any handles. The gotcha is that js_get_typedarray_info() is usually faster than js_get_typedarray_view() if you already have an open handle scope so it's probably a good idea to measure the impact.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick measure of typed calls, unsure if my use of js_get_typed_view() is correct.
But fetching views is indeed slower than even the untyped calls:

# bench typedarray access
    # call counters (warmup) { untyped: 127848, scoped: 46928, view: 5224 }
    # peforming 30'000'000 calls on each method
    # untyped ms 1261
    # scoped ms 1232
    # view ms 2525
    # call counters (post bench) { untyped: 30371313, scoped: 29932689, view: 29875998 }

Something that's been bugging me for a while is that js_get_typedarray_info() is only marginally faster than untyped calls.
I don't know if the call-overhead is imagined or we just lose our gains elswhere;

As i said, i don't know if i'm doing something wrong, but all the numbers i've seen so far are a tad bleak.

Comment on lines +1704 to 1731
uint32_t
udx_napi_typed_stream_write(
js_value_t *receiver,
js_value_t *stream_handle,
js_value_t *req_handle,
uint32_t rid,
js_value_t *buffer,
js_typed_callback_info_t *info
) {
int err;
js_env_t *env = NULL;
err = js_get_typed_callback_info(info, &env, NULL);
assert(err == 0);

return return_uint32;
udx_stream_t *stream;
size_t stream_len;
err = js_get_typedarray_info(env, stream_handle, NULL, (void **) &stream, &stream_len, NULL, NULL);
assert(err == 0);

udx_stream_write_t *req;
size_t req_len;
err = js_get_typedarray_info(env, req_handle, NULL, (void **) &req, &req_len, NULL, NULL);
assert(err == 0);
assert(req_len >= sizeof(udx_stream_write_t));


return udx_napi__stream_write(env, stream, req, rid, buffer);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been tested? It'll likely abort if called as both the function itself and udx_napi__stream_write() allocate local handles which isn't possible without a handle scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants