duktape icon indicating copy to clipboard operation
duktape copied to clipboard

Extending the internal interrupt mechanism

Open svaarala opened this issue 7 years ago • 5 comments

There's an internal interrupt mechanism which is integrated into the bytecode executor, and is used for execution timeout and debugger integration. It can be extended to the public API but there are still some open design issues to figure out.

Having it available in the public API would allow e.g. user code to implement custom script timeout handling (even in Ecmascript code), implementing pre-emptive script multitasking without explicit yields (when combined with duk_suspend() and duk_resume()), possibly sample based limited profiling, etc.

Some design questions:

  • Current interrupt mechanism is based on opcode count only; this results in very uneven call rate. Could there be a time component, e.g. a two level structure where interval A is used to check current time and control triggering based on an approximate time interval? Debugger already uses this approach, also see https://github.com/svaarala/duktape/issues/1520#issuecomment-301247047.
  • Should the callback be able to control the next interrupt time either in opcode counts or time? Conceptually there are multiple consumers to the interrupt mechanism and the next interrupt value would need to be computed based on their needs (e.g. interrupt counter initialized to the smallest "next need").
  • Should the callback be guaranteed some steady rate of calls, or is it OK to sometimes call it a few times per second and sometimes for every opcode (when single stepping for example)?
  • Should the callback be able to throw? Should it be able to throw an uncatchable error, as is now used for execution timeout?
  • Should the callback be a Duktape/C call or not? There are differences in performance, value stack robustness, etc.
  • Should multiple callbacks be supported, from a modularity perspective?
  • Should the callback be specific to a global scope or the entire heap? Depending on what it is used for, both answers may be right.
  • How does the callback relate to generalizing the execution timeout mechanism? In particular, how does it relate to doing nested timeout checks?
  • How does the callback relate to profiling? Sample based profiling can be implemented on top of a simple interrupt, but providing useful guarantees about the call rate may matter more than in other applications. See #1307.

Also see:

  • https://github.com/svaarala/duktape/issues/1520#issuecomment-301247047
  • https://github.com/svaarala/duktape/issues/1626#issuecomment-318228593

svaarala avatar Jul 27 '17 01:07 svaarala

As an example of where this would be useful in a practical scenario, take this example in miniSphere:

const TAU = 2 * Math.PI;

var stream = new SoundStream(44100, 32, 1);
var numSamples = 2.0 * 44100;
var divisor = 44100 / 1000;
var samples = new Float32Array(numSamples);
for (i = 0; i < numSamples; ++i) {
	var angle = TAU * (i % divisor) / divisor;
	samples[i] = Math.sin(angle);
}
stream.buffer(samples);
stream.play(Mixer.Default);
while (true) {
	screen.flip();
}

miniSphere is currently single-threaded. As written, the code above plays a (very annoying) 1kHz sine wave for 2 seconds. If one were to remove the call to screen.flip() from the loop, however, the program falls silent. What happened? Well, with the event pump completely starved, the SoundStream's audio buffer never gets filled and no sound comes out.

Now it would of course be possible to handle audio buffering on a separate thread. However, that brings with it a lot of synchronization concerns. If I had the ability to fire the event pump from an interrupt callback, I could trade a bit of raw performance and sidestep the issue entirely. (I do note that in this particular case, buffering in a background thread is the better solution for several reasons, but it illustrates the point nicely :)

fatcerberus avatar Jul 27 '17 06:07 fatcerberus

@svaarala From our perspective we just want to pass control to back to the C program every now and then. https://github.com/svaarala/duktape/issues/2214#issue-535712955 https://github.com/svaarala/duktape/issues/90#issuecomment-70165648 Actually we dont mind if the opcodes executing are not even or the timing is not always exactly the same. Can you give us an idea of what is the worst case scenario btw? What would the executor be doing that would take the longest time? I would have thought that its 1 - 100 instructions worst case before being able to call back?

From your replies on other posts in #90 https://github.com/svaarala/duktape/issues/1626#issuecomment-318228593 and if understand this correctly, the executor can "easily" fire a C callback, allowing the host program to do some things. Now how does the C program regain control of a rogue JS execution ?

Can we control the execution from C? Will we have to do this from the callback only?

API wise maybe we can have a call that we use to setup the executor?

duk_config_executor_cb(strategy, c_handler) //executor calls us in C

OR

 duk_config_executor_tick(strategy);
 duk_next_tick(ctx); // we call the executor from C to execute the next tick/step/slice

Strategy (bunch of enums or a number) can be for every e.g. 1000 opcodes or a 350ms period (is there a notion of time in the executor? How does it know its running at 1MHz or 600MHz). Right now any is ok.

Some answers to your other questions

Should the callback be able to throw? Should it be able to throw an uncatchable error, as is now used for execution timeout? Interesting, shall we have a few callbacks that are registered or a master super callback? What calls would we need?

  • timeout cb
  • an exception cb (like process.on('uncaughtException' in node)
  • tick/step cb

A master callback will be easier to implement, check that it was registered and easier on beginers as they only need to setup one handler right? We can pass a struct or a interrupt id to know what happened when our handler is called.

Should the callback be a Duktape/C call or not? There are differences in performance, value stack robustness, etc. Should multiple callbacks be supported, from a modularity perspective? Apart from the C Executor calling a C handler, what other options we have?

Should the callback be specific to a global scope or the entire heap? Depending on what it is used for, both answers may be right. Can it be per context?

How does the callback relate to generalizing the execution timeout mechanism? In particular, how does it relate to doing nested timeout checks? What is a nested timeout check? You mean if the timeout cb triggers while we are in the handler for another reason?

dazhbog avatar Jul 15 '20 18:07 dazhbog

Re: the worst case, it is dominated by native functions that can take an arbitrary amount of time. For example, the String(arr) here takes a long time:

var arr = []; arr.length = 1e9; var result = String(arr);

The result is a ~1GB string (consisting of just commas) and there are no interrupt calls while that operation runs.

Similar cases happen in various C functions, and each would need to be augmented with some form of "cooperate with timeout" call now and then. These would need to be added to all C functions inside Duktape, but also to any user-registered native functions (the public API obviously needs some addition like duk_cooperate() to allow this).

svaarala avatar Jul 21 '20 19:07 svaarala

I have a use case for your consideration. I'm designing a game engine where I was going to use a scripting engine like Duktape for cutscene and level scripting. The idea was that the normal game loop would be running and when a cutscene or other script was triggered, it would call to the scripting engine. The script, in turn, would make native calls to set scene variables, display text, and/or start an animation, and then make a call to wait for text to finish printing, or the animation to end, or simply wait for a button press. At that point, script execution would pause while the main game loop is still running, taking in input and rendering things as normal, until which time the condition to resume is fulfilled and the script is resumed where it left off. So my use case is less "interrupts at a regular interval" and more "waiting for a trigger to continue".

I thought perhaps duk_suspend() and duk_resume() was what I was looking for, but there's notes in the API about how the stack shouldn't be unwound after a suspend, which goes against the idea above. Writing it out, it sounds like something that could be done with a coroutine and yield, but as was noted in #2214, Duktape's coroutine support is extremely limited to only JS-domain stuff. I was hoping for some means of calling to duktape to suspend from a native function it has called to (the native function doing what it should with the value stack before returning) and having duktape unwind gracefully without any more execution, and then later, after several loops around the main game loop, calling to resume and have duktape pick up on the next bytecode operation.

tustin2121 avatar Jan 08 '21 07:01 tustin2121

The main limitation for Duktape w.r.t. duk_suspend/duk_resume and coroutines is that Duktape doesn't have the capability to switch native stacks -- i.e. to suspend a certain native stack, establish a new one, and later resume a previous native stack. This is mainly to limit portability issues, as stack handling is inherently difficult to make trivially portable.

Regarding coroutine limitations, note that while a coroutine can only yield if it's call stack consists of JS call stack entries only, it can quite freely call into native functions as long as they don't (indirectly) yield.

So it's quite possible (and normal), for example, to:

  • Call into a native helper to render something and initialize state to detect a keypress.
  • Return from the native helper and then yield the coroutine.
  • When a keypress happens, resume the coroutine with a value indicating what key was pressed.

This can be driven in a non-blocking fashion from a native game loop.

svaarala avatar Jan 10 '21 21:01 svaarala