-
-
Notifications
You must be signed in to change notification settings - Fork 564
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
async/await and callback support #514
Comments
Can you not convert simple callback into promise using following? public class AsyncService {
public void LoadAsync(JValue input, JValue then, JValue failed) {
Task.Run(async () => {
try {
var result = await ..... async processing in C#
then.Invoke( null, result );
} catch(Exception ex) {
failed.Invoke(null, ex.ToString());
}
});
}
} loadAsync( input ) : Promise<any> {
return new Promise((resolve,reject) => {
asyncService.loadAsync( input, resolve, reject);
});
}
// now you can use loadAsync method with async await |
@ackava thanks for sharing your ideas. It looks like you are using TypeScript, which would explain why using the I realize that supporting async/await would pretty much be supporting ES2017, which has it's own open issue: #343 - this would probably (I can only guess) solve my issue, depending on how @sebastienros decided to implement "await" for C#-backed functions. For now, the If someone else wonders how to do transpiling in Jint: #274 (did not try it yet) |
Hi everyone, I echo @christianrondeau in the desire for async/await support. While promises would work, it would be even simpler if the Engine could just "await" a function delegate passed to "SetValue" - if that delegate wrapped an asynchronous method. Suppose we had the following C# code:
The ideal scenario would then be for an "ExecuteAsync" method on the engine like:
...where internally the engine would "await" the function delegate at the time of invocation. @sebastienros , is this at all possible? If not, are there any other workarounds you might suggest? Jint is a phenomenal piece of software though in my particular case, I'm dealing with heavy network I/O. As such, async/await support is critical. |
Has anybody had any progress on this issue that's worth sharing? I'm in a situation where I need to call some async methods. |
Hello, C# function to call from JS :
C# Class "Promise" :
Javascript code :
The catch function is not implemented but you'll can do with this example... This code is a part of a Xamarin project |
Bumping this one for a question: Since Jint is an interpreter, are there any reasons that C# methods cannot be async/return Task, but made look synchronously to the user-script? I have a case with very many small simple scripts, that should call async methods. I'd like to avoid the complexity of introducing await/Promise objects to the user scripts, because none of the scripts have parallelism requiring actual async code. |
You want to be able to do |
Yes, but the user scripts in my case don't really need it. Still, the host process needs it to avoid blocking the thread. |
That might actually be a good idea to support, so the API isn't async based to the script, but is async for the usage. |
Promise support has been implemented, but no support for async/await keywords yet. |
May I ask what is block on this ? I wasn't see any problem from C# side (both js->c# and C# -> js) |
@John0King Its been long since I answered, I have created my own JavaScript engine in C# which supports most of features of ES6 through ES2019. You can try and let me know. |
Jint now has |
so Jint supports native async / await, but doesn't convert c# Task and others to Promise and vice versa yet? |
Native for .NET doesn't exist yet, only the JS fullfill/reject chaining. Some brave soul could implement wrapping Task into Promise. |
ok, will try to have a look at it, as that's now the missing blocker for async usage 😅 |
Probably something like adding task with GetAwaiter().GetResult() to event loop and resolving to its result. |
Pretty please don't do that :) The whole point of await is freeing the thread and allowing the use of synchronization contexts. Really the only desirable implementation will be painful but very valuable : async all the way (TM) meaning ExecuteAsync, AwaitAsync and friends. If you want I can provide more material to back this, we've been through this in the past too :) |
@christianrondeau got your attention, now waiting for the PR 😉 |
but we need to make sure there are no real parallel tasks, as it's not expected from JavaScript. there should only be one thread at a time, otherwise it would make interoperability much more complicated. |
I think that would mean adding the tasks to main event loop and blocking and processing with |
Haha yeah I'd love to, but I'm also struggling with time :) This being said, the reason I didn't even try is because I don't understand promises internally, and I know it'll be another huge PR with tons of breaking signature changes. Or duplicate every involved method to avoid the (minimal) overhead of the Task. JS can be single threaded, but the promise itself can definitely wait on a C# task that is bound to multiple async tasks (Task.WhenAll). What's important is that after the promise completes, control is given back to the synchronous single threaded javascript code (like node). My idea that I didn't validate yet was to have a duplicated Execute and Evaluate methods as well as a duplicated RunAvailableContinuations (async and not async), and throw in the non async RunAvailableContinuations if a promise is bound to a Task that is not completed. Not sure if it's that simple in practice though. I guess the very first question to ask is whether you want to offer only async execute and evaluate methods and pay the small overhead, or provide both signatures and potentially have duplicated methods and tests. |
I can definitely support @viceice and maybe even try a prototype branch, since I'm the one who opened the issue one year ago... And to be honest, I have to do some gymnastics to work around this so I'd definitely still make use of it. Can't promise anything though. |
I do have interest in performance and solutions supporting it, but I'm afraid that the true async paths and dispatch to pool would lose the benefits just by the overhead of the interpreter. Of course hard to measure when only sync path available (in Jint). Async shines in high concurrency but I'm unsure if Jint or any JS interpreting runtime will benefit of it. |
Actually the main value is to use IO ports instead of locking a thread in the ThreadPool (asp.net being the most obvious example). That was also the great selling point of NodeJS when it came out, not concurrency (well, concurrency of the server, not of the script itself). It's also very true for Jint, as right now loading a file or accessing resources on a blob storage or any remote location (or use SQL etc) will lock the web thread and eventually kill your web server scalability if you use GetAwaiter as a workaround to provide synchronous methods to Jint. So implementing fake async using GetAwaiter would actually be a trap for novice and uninformed developers using Jint, who may discover they suddenly require more machines with idle CPUs to support the same load. I hope this makes sense, I can try and provide some more thorough explanation and references if you're not convinced yet :) |
I think a demo using Jint with different approaches would be great. That would clarify the benefits and probably show the benefits. I have no idea what percent of users jump to actual waiting I/O from engine. |
I'll try and do two things then (I knew this would come back and bite me!)
What I cannot do is estimate how many people would benefit, but anyone who runs Jint in a web server and has any kind of file, http, tcp wait will greatly benefit under scale (not under low volume though). |
Awesome! One thing to note is that we probably cannot have |
Also linking earlier discussion here: #1081 |
Thanks for the link, I can see I'm not the only one who thought of this ;) But I just thought of another MUCH simpler approach... I'll try and confirm if it could work but I don't see why not.
And that's it. If I didn't miss something, this would rid of all the complexity surrounding the event loop. After all, .NET already has a lot of the mechanisms we need for this. Then, adding actual async to this becomes quite simple, we only need to have a Promise implementation that contains the Task object and await it. Do you see something wrong with this? I'll try and see if it's as simple as that in practice. |
Wait a second, you already implemented some of it as part of #1323 - that'll make things easier :) |
When touching Maybe also worth a look how other engines handle such things, like Nill.JS.
Oh did I 😆 |
Soooo I made a quick and dirty test, and changed the implementation of
and
So, nothing fancy but this should capture the state of the executing code and therefore have most of the memory impact. With simple foreach
With IEnumerator
But I think the IEnumerator idea still makes sense, but by implementing it manually in every JintStatement class would have pretty much zero impact, and might be necessary anyway. The problem is that JintStatementList is called from multiple, potentially deep places, and we need to halt execution in the middle of something. So everything that can run a JintStatementList needs to also pause and return back to the Engine. So, here's what I'm going to try next:
I'm still unclear as to what the event loop implementation does though. Maybe it'll be useless after this or maybe I'm completely off track. I'll report back. |
I think you are nearing the generators problem zone (unfinished PR). Statement list needs to be able to pause and snapshot state to continue from when called next time. Generally "resume from index 3". |
Aaah so you're saying that instead of actually pausing the JintStatementList and resuming it, that would be breaking the whole parsed script in chunks up front, each chunk being a piece of code that can run until an await statement? I hope that made sense, I'm not aware of how generators work but I didn't think it would span across functions. Do you believe I should stop what I'm currently doing? For example, you should be able to do things like:
So the await can actually return control back to C# and resume later. Oh and by the way, this is mean:
Right now, the For now I'll try and see how far I can go with it and reach out when I have an actual working example of what I'm going for. |
So, less words, more code. Here's the idea (just a small piece to make sure I'm not going into a wall). The
The idea from this point is to replace the Everything that calls So, what do you think? Is that worth pursuing? |
To keep you in the loop.
For the sake of the prototype, I'll instead make a |
All right I'll have to stop for a while. Instead of returning a weird SuspendPromise, I set NiL.JS have an elegant solution to this, they wrap (at parse time I think) all those operations in a SuspendableExpression, which takes care of checking whether post-execute, the engine is suspended. However most of Jint expressions would require the ability to deal with resuming anyway, unless we decompose things even more than they currently are. I'm out for now :) I'll probably continue investigating this sometime in the future though. |
I'm trying to answer/comment at least some bits, please re-ask the parts I'm missing 😉 I'll be mixing generators concepts here too so be aware...
I was thinking for generators case to use same construct as Nill.JS has, basically a Generators need this in all funky places, like in try-catch-finally, it's ok to even
So far your
Currently the suspend for
I think there shouldn't be need for completion type for expression. For
This is now "competing" a bit with EDIT
That's a good point. Each invocation has its own execution context so that might help with this, not sure how though. |
Thanks for taking the time. You can ignore piggybacking on Completion or JsValue, that's clearly wrong (it was more of a way to make a cheap and fast proof of concept). In the end, the challenge I see is mostly suspending execution. For example, here: https://github.com/sebastienros/jint/blob/main/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs#L70 you'd need to wait for the value before continuing. You have to options. Either you store the execution context with the closure, save the state, and write code to resume it later, OR you take advantage of .NET doing this through Task and async, but you have to pay a small memory and perf cost (maybe acceptable, we'd have to try). Going the "async all the way" approach (which is what I'd strongly recommend in almost every project using any kind of file or network IO) would solve all problems at once, really. The only "but" is the overhead, and it's significant enough to consider in cases of Going the "suspend" approach means every expression must have a state (or instantiate something that holds the state, like I did for the statements list). Whether you use an enumerator-like approach or a state dictionary (enumerators would be better IMHO since you avoid having to clean up state and you don't have to do a hash check at all) works anyway. If I got this right, "generators" are similar to C#'s "yield" statements on IEnumerable functions; however this is limited to function invocation, it won't actually suspend execution, right? In other words, should I hold for some additional work on Jint or is it worth it to continue exploring? As usual, I want to help, not generate additional noise :) |
Great discussion. I have a bit of input: Please don't go for a synchronous approach (something like GetAwaiter().GetResult). In fact it's better to not support await at all than such approach, or atleast throw a NotSupportedException indicating that ExexuteAsync must be used to execute this expression. Tasks don't imply concurrency. You can use tasks in a singlethreaded environment. The purpose of a task is to yield the thread for something else to do useful work while the script is waiting for some event to occur (such as a network response), so that the script can continue in the same singlethreaded context (but doesn't need to be the same thread) when completed. My usage of JINT is running a bunch of small user scripts (1000's). If we support async and the user wants to e.g. call await API.Delay(30000); to wait 30 seconds before doing something, a synchronous approach would starve the application by needing to spawn 1000 threads just idling around doing nothing. When in fact 1 thread could do all the work, and other useful stuff while the scripts are just waiting. NET has a very powerful system already for handling and queuing tasks. By utilizing it, in practice, a few threads could run 1000 separate JINT scripts, and not neccessarily restricting one thread to one JINT engine. The script could start on one thread, await some network IO, and then continue to run on another thread. Still singlethreaded from the engine/script's point of view because there is no concurrency. |
Agreed @ChrML. To be clear on the "suspend" approach, it doesn't suspend the thread, it just stops evaluating where it is and returns to the caller. Meaning you could do:
So, no thread locking and no actual Task/async/await support in Jint but the ability to suspend would already allow async, despite not in a nice and user friendly way. Right now to work around this, I'm doing (simplified):
It complicates things, but it works. I do think that many people won't hit high server concurrency nor be under the risk of running malicious user scripts, but I still wouldn't make thread locking a default behavior. Now the challenge is to make it work ;) |
@christianrondeau With such approach, one could implement a more user-friendly ExecuteAsync with this pseudo-code: public Task<JsValue> ExecuteAsync(Script script, CancellationToken ct)
{
JsValue result = this.Execute(script);
while (this.HasAsyncContinuation)
{
ct.ThrowIfCancellationRequested();
await this.CallCurrentAwaitedTask(ct); // Pass ct automatically to the user's async method if it's the last argument for cancellation.
result = this.Resume();
}
return result;
} |
That's exactly it, yes; this (the suspend approach) has the advantage of not introducing Tasks everywhere in the code (async all the way), but it does increase complexity since you need every expression to be able to suspend and resume (unless there's some generator magic I'm missing ;) ) |
I suspect the suspend logic will already be needed to implement JS generators, to some extent. So this could be an extension of that. I agree that tasks all the way would be unneccessary, and probably make the performance lower for scripts that don't need it. So I'm all for the suspend approach. |
I went through lot of answers and couldn't help putting few points, I hope it helps.
var a = async () => {
return await {
then(n) {
console.log("Then called");
n("result");
}
};
}
a().then((r) => console.log(r));
Task<JSValue> ToTask(this JSValue @this) {
var then = @this["then"];
if (!then?.IsFunction) {
return Task.FromResult(@this);
}
var tcs = new TaskCompletionSource();
then.InvokeFunction(
new JSFunction( ... => tcs.TrySetResult(arg0) ),
new JSFunction( ... => tcs.TrySetException(arg0.ToException()) );
return tcs.Task;
}
|
Thanks, in my personal case, anything that helps me better understand is welcome! I'm not clear on 2 however, I tried to re-read about generators and it looks like C#'s yield return statements. Maybe I should just wait for @lahma to write the generators ;) I'd argue against using a thread though (I feel like everyone's agreeing already), since it would be worse than GetAwaiter(), since we'd use two threads now instead! Transpiling in a flat scope sounds like the best option, though I'm not in a position to comment since I'm not familiar of what this actually involves. But it would make pausing and resuming straightforward. For now, I still feel (emphasis on me not feeling comfortable enough to back this up with verifiable claims) that implementing the enumerable pattern on expressions to track state, and allowing to pause the engine and resume it within each expression would have both a low performance cost and wouldn't require too much refactoring; I also think that waiting to see how generators will be implemented would be wiser than trying to short-circuit that discussion. So for now, I'll watch but will be available-ish if you need more info on my PoC branch's approach (I'll leave it as-is for now). |
Yes, here! Just to add another vote to please not use In case it is relevant to this discussion as an example use case, my approach to supporting async C# code with Jint promises is to capture all C# Tasks in a companion object to the Engine ( Basically there's an ExecuteAsync method that does this: (prototype code)
From what I gathered in the discussion, this is similar to what @christianrondeau describes on a lower level, i.e. storing any "contiuations" that were created by a script and then executing them. From my point of view, ideally Jint would provide an async overload of Execute/Evaluate for users who want to use async/Tasks ("async all the way" style) and the existing methods simply throw if a Task was captured. |
I would love to see something like what EnCey prototyped implemented. Right now, there are no easy ways to schedule task continuations, and wait for them to all finish asynchronously. |
In case anyone wants to try it: my prototype doesn't work with the The reason is that Jint's |
@EnCey I attempted to tackle this issue on my own, and this was the problem I ran into as well. There isn't some magical way to stop execution in the javascript engine, and asynchronously wait the result. The solution I came up with, unfortunately does use the When I attempted to do it the "right way", which involved async ALL the way down, the code became sloppy. All of the |
It might be better to consider using ValueTask instead of task, but yeah either we have async all the way (simple but doubles everything if we really want to keep a non async version) OR we have a mechanism to save execution state and resume it later (this would be the optimal solution but it's not obvious to implement) |
A third option might be to transpile the JavaScript code first to not use async/await, but regular promises. There's an issue here somewhere that describes using the TypeScript compiler to transform JS code. I've played with that and it did work, allowing me to use async/await in TypeScript code and my prototype to execute the resulting JS code. One problem here is that without source maps, any error messages are not helpful to users, as they refer to the (messy) generated code. The elephant in the room of course being how (in)efficient it is to run the entire TypeScript compiler with Jint to pre-process code (although once transpiled, the resulting JS code can be cached to reduce that impact). |
This makes another case for implementing source maps support. Tracked here. |
This is my code var jintEngine = new Engine();
jintEngine.SetValue("myAsyncMethod", new Func<Task>(async () =>
{
await Task.Delay(1000);
Debug.Log("myAsyncMethod");
}));
jintEngine.SetValue("myAsyncMethod2", new Action(() =>
{
Debug.Log("myAsyncMethod2");
}));
jintEngine.Execute("async function hello() {await myAsyncMethod();myAsyncMethod2();} hello();"); I expect the output to be myAsyncMethod
myAsyncMethod2 But await does not seem to wait on a C# function. myAsyncMethod2
myAsyncMethod I am using v3.0.0-beta-2049 |
@2763071736, i believe that's separate from the whole async/await discussion here. It looks to me like the task doesn't get converted to a promise. Run this in roslynpad: #r "nuget: Jint, 3.0.0-beta-2049"
using Jint;
var jintEngine = new Engine();
jintEngine.SetValue("myAsyncMethod", new Func<Task<string>>(async () =>
{
await Task.Delay(1000);
"myAsyncMethod".Dump();
return "testvalue";
}));
jintEngine.SetValue("myAsyncMethod2", new Action<string>((string value) =>
{
value.Dump();
"myAsyncMethod2".Dump();
}));
jintEngine.Execute("async function hello() { const test = await myAsyncMethod(); myAsyncMethod2(test); } hello();");
await Task.Delay(2000); // give roslynpad time to wait for the delay And you'll get the dumps: followed by but the hint here is the AsyncTaskMethodBuilder. based on this discussion you can use the TryConvert code to wrap the Task.Delay in a promise: #r "nuget: Jint, 3.0.0-beta-2049"
using Jint;
using Jint.Native;
using Jint.Native.Promise;
var tcs = new TaskCompletionSource<object>();
var jintEngine = new Engine();
jintEngine.SetValue("_resolveFinal", tcs.SetResult);
Thread.CurrentThread.Dump("starting thread");
jintEngine.SetValue("dump", new Func<object, object>(obj => obj.Dump("jintDump")));
jintEngine.SetValue("myAsyncMethod", new Func<JsValue>(() =>
{
var (promise, resolve, reject) = jintEngine.RegisterPromise();
_ = Task.Delay(1000).ContinueWith(_ =>
{
Thread.CurrentThread.Dump("continuation thread");
resolve(JsValue.FromObject(jintEngine, "testvalue"));
"myAsyncMethod".Dump();
});
return promise;
}));
jintEngine.Execute("myAsyncMethod().then(_resolveFinal)");
var final = await tcs.Task;
final.Dump("final"); do mind, you'll be resolving your promise from the timer thread. That's quickly resolved by using a SynchronizationContext from the nuget Nito.AsyncEx.Context: #r "nuget: Nito.AsyncEx.Context, 5.1.2"
#r "nuget: Jint, 3.0.0-beta-2049"
using Jint;
using Jint.Native;
using Jint.Native.Promise;
using Nito.AsyncEx;
var tcs = new TaskCompletionSource<object>();
var jintEngine = new Engine();
jintEngine.SetValue("_resolveFinal", tcs.SetResult);
AsyncContext.Run(async () =>
{
Thread.CurrentThread.Dump("starting thread");
SynchronizationContext.Current.Dump();
var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
jintEngine.SetValue("dump", new Func<object, object>(obj => obj.Dump("jintDump")));
jintEngine.SetValue("myAsyncMethod", new Func<JsValue>(() =>
{
var (promise, resolve, reject) = jintEngine.RegisterPromise();
_ = Task.Delay(1000).ContinueWith(_ =>
{
Thread.CurrentThread.Dump("continuation thread");
resolve(JsValue.FromObject(jintEngine, "testvalue"));
"myAsyncMethod".Dump();
}, scheduler);
return promise;
}));
jintEngine.Execute("myAsyncMethod().then(_resolveFinal)");
var final = await tcs.Task;
final.Dump("final");
}); But when trying to use the await keyword version: We get a Jint exception that it wants to unwrap the promise before it was resolved, indicating to me that the |
I was surprised to find no issues nor pull requests to support C#/JS async/await and Task support.
In a perfect world, I could make that transparent and "halt" the calling method (like #192), or use a callback mechanism (which would make scripts much more complex however).
I'd prefer to avoid using
return task.Result
and friends, so that I don't get into a deadlock eventually.Before investigating this further, did anyone actually attempt any async/await support in Jint? @sebastienros is this something you'd be interested in supporting, e.g. through the native ECMA-262 async/await (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)? Any failed or partial attempt?
To avoid any confusion, given this C# code:
I'd consider any of these JavaScript equivalents to be correct (in order of preference):
Any ideas or feedback on that is welcome; I'd like to avoid spending any time on this if it would not be merged, or if the effort is too large for what I can afford.
The text was updated successfully, but these errors were encountered: