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

Preemption [WIP] [RFC] #90

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Conversation

sargun
Copy link
Contributor

@sargun sargun commented Mar 16, 2017

This PR introduces basic preemption support into Gravity. I'd mostly like to get your thoughts on this. I'd also like to make it so that when (certain) native functions are called, it can also trigger preemption.

@marcobambini
Copy link
Owner

May I ask you why you'd like to introduce preemption?

@sargun
Copy link
Contributor Author

sargun commented Mar 16, 2017

I tried to discuss in Gitter. It might be easier there.

@sargun
Copy link
Contributor Author

sargun commented Mar 16, 2017

I'm introducing concurrency to Gravity, with the following semantics:

  1. Every gravity program will belong to a processlet.
  2. Every processlet has a 64-bit integer ID that's derived from incrementing a number.
  3. Processlets do not share memory.
  4. You can communicate between two processlets by doing a send(ProcessletID, Message) OR a receive(timeout)
  5. A processlet can monitor the life of another processlet by using a monitor(ProcessletID). If that foreign processlet fails, a message will be put in the monitoring process's mailbox
  6. A processlet can link is life with another processlet. If the foreign processlet fails, the processlet can either link its lifetime with the foreign processlet, or vice-versa.
  7. A processlet can kill another processlet with a kill signal.

I/O (sockets) will be represented as processlets that take special commands, and are "owned" by a specific processlet.

@@ -158,6 +171,7 @@ int main (int argc, const char* argv[]) {

// create VM
gravity_vm *vm = gravity_vm_new(&delegate);
delegate.preempt_callback = &should_preempt;

Choose a reason for hiding this comment

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

Formatting


DEBUG_STACK();

gravity_preempt_callback preempt_cb = NULL; // Callback to cause preemption

Choose a reason for hiding this comment

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

ditto

@@ -1407,7 +1460,7 @@ void gravity_vm_loadclosure (gravity_vm *vm, gravity_closure_t *closure) {
gravity_fiber_reassign(vm->fiber, closure, 0);

// execute $moduleinit in order to initialize VM
gravity_vm_exec(vm);
gravity_vm_exec(vm, false);

Choose a reason for hiding this comment

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

ditto

@@ -21,12 +21,15 @@ typedef void (*vm_transfer_cb) (gravity_vm *vm, gravity_object_t *obj);
typedef void (*vm_cleanup_cb) (gravity_vm *vm);

gravity_vm *gravity_vm_new (gravity_delegate_t *delegate);
void gravity_vm_init (gravity_vm *vm, gravity_delegate_t *delegate);

Choose a reason for hiding this comment

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

ditto

@marcobambini
Copy link
Owner

Have you measured the impact of performing MAYBE_PREEMPT on each instruction?

@sargun
Copy link
Contributor Author

sargun commented Mar 17, 2017

It's negligible, performance wise to invoke a callback which returns false. It's about a 3.1% overhead versus commenting out MAYBE_PREEMPT entirely.

with preemption callback returning false (median of 50 runs): 797.5753 ms
with no preemption code (median of 50 runs): 773.0993 ms


I think over time, we can introduce lighter weight mechanisms of preemption. Perhaps enabling / disabling it per instruction.

@sargun
Copy link
Contributor Author

sargun commented Mar 17, 2017

The other place where need preemption / resumption support is native calls. I have yet to determine the interface here, but when making a call to a native function, it needs to be able to yield up, and resume. I haven't dug in very much, but I was thinking of introducing a return code from native functions, or another function that's bound to them that allows for yielding.

@marcobambini
Copy link
Owner

Why a callback to check if VM must be interrupted and not a simpler:
gravity_vm_interrupt
that internally set a bool flag?

I would image APIs like:

bool gravity_vm_interrupt(gravity_vm *vm, gravity_vm_state *state);
bool gravity_vm_resume(gravity_vm *vm, gravity_vm_state *state);

In this way no extra parameters are needed and the API is much more cleaner.

@sargun
Copy link
Contributor Author

sargun commented Mar 17, 2017

I thought about such an API. It's not a bad approach, I thought about it initially -- especially invoking it from native C calls. I have some questions though:

  1. What calls vm_interrupt / vm_suspend? If it's not being invoked through a callback, it needs to be a thread. Some of the platforms I intend to target do not readily have threads available -- highly sandboxed VMs, and WebASM / Emscripten.
  2. When gravity_vm_interrupt gets called, does it unwind the stack of the VM? If so, how does the VM get resumed? If not, how does the VM yield?
  3. If the boolean is being read locally, and modified by an external thread, we would have to declare it volatile, and I think that introducing synchronization would actually make it slower in some cases -- I can benchmark this if you want.

As a side note, I tried out one other approach -- rather than a run-time callback, doing a compile-time macro. Unfortunately, this had no large speed difference as compared to the callback (<1% faster).

@marcobambini
Copy link
Owner

marcobambini commented Mar 17, 2017

Something like this should work in my opinion:

bool gravity_vm_interrupt (gravity_vm *vm) {
    // if vm already interrupted return false
    if (vm->interrupted) return false;

    // check for mandatory vm_state callback
    if (!vm->delegate->vm_state) return false;

    // don't think we need to protect this bool flag
    vm->interrupted = true;

    // wait for vm_state callback to be called
    // automatically called by vm when flag is set
    return true;
}

void vm_state (gravity_vm *vm, gravity_vm_state *state) {
    // this callback is automatically called by the vm when it is interrupted
    // state contains info necessary to resume vm (like your vm_state struct)
}

bool gravity_vm_resume(gravity_vm *vm, gravity_vm_state *state) {
    // if vm not interrupted return false
    if (!vm->interrupted) return false;

    // reset interrupted flag 
    vm->interrupted = false;

    // resume execution
    gravity_vm_run_resume_main(...);    

    return true;
}

// your MAYBE_PREEMPT macro is now
#define MAYBE_PREEMPT     if (vm->interrupted) goto DO_PREEMPT;

@marcobambini
Copy link
Owner

The ability to be able to interrupt the VM (and step) will open us the possibility to attach a debugger to it.

@Jezza
Copy link

Jezza commented Mar 17, 2017

If you don't have threads, in which to interrupt the VM, why not look into a yield mechanic, or something similar?
Granted, that doesn't have the exact same functionality, unless you want to introduce a language level yield, but again, you would still differ in functionality.

Why not a "debug/halt" opcode that halts the VM, also potentially reducing back pressure?

}

gravity_vm *gravity_vm_new (gravity_delegate_t *delegate/*, uint32_t context_size, gravity_int_t gcminthreshold, gravity_int_t gcthreshold, gravity_float_t gcratio*/) {
gravity_vm *gravity_vm_new(
gravity_delegate_t *delegate/*, uint32_t context_size, gravity_int_t gcminthreshold, gravity_int_t gcthreshold, gravity_float_t gcratio*/) {

Choose a reason for hiding this comment

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

Formatting

vm->time += millitime(tstart, tend);

if (!gravity_vm_preempted(vm))
PRINT_STATS(vm);

Choose a reason for hiding this comment

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

ditto

@sargun
Copy link
Contributor Author

sargun commented Mar 18, 2017

The ability to be able to interrupt the VM (and step) will open us the possibility to attach a debugger to it.

The callback based method will allow for this as well, specifically, the runtime based callback.

@sargun
Copy link
Contributor Author

sargun commented Mar 18, 2017

If you don't have threads, in which to interrupt the VM, why not look into a yield mechanic, or something similar?
Granted, that doesn't have the exact same functionality, unless you want to introduce a language level yield, but again, you would still differ in functionality.
Why not a "debug/halt" opcode that halts the VM, also potentially reducing back pressure?

A couple things:

  1. To me, this doesn't feel much better than coroutines.
  2. What would the C API look like? Would it unwind the stack? If so, we would still need the resume API.

@sargun
Copy link
Contributor Author

sargun commented Mar 18, 2017

Is the fear of around the callback mechanism about performance?

One of the answers here might be to have an interpreter loop with preemption enabled, and one without. You can then run performance-sensitive & trusted code without preemption enabled at all. This is similar to the Linux kernel's preemptive scheduler.

@sargun
Copy link
Contributor Author

sargun commented Mar 24, 2017

How about we allow people to define a macro instead of a full-on function. This way, if they want to use the flag, they can have a struct like:

struct wrapper_vm {
    bool preempted;
    gravity_vm vm;
}

And their macro can look like this:

static inline bool maybe_preempt(gravity_vm *vm) {
    struct wrapper_vm *wvm = container_of(vm, struct wrapper_vm, vm);
    return wvm->preempted;
}

#define MAYBE_PREEMPT maybe_preempt(vm)
// It an exercise left to the user to reset the state prior to calling resume.

@marcobambini
Copy link
Owner

@sargun I'd like to have the ability to decide the pre-emption based on a runtime bool value instead of a macro that would require recompilation. I think that my latest proposal could be the best compromise between performance degradation (a bool comparison on each instruction) and the flexibility to interrupt the vm.

For debugging purpose some special bytecode instructions like BREAK and STEP would be the best option.

Please let me know what you think.

@sargun
Copy link
Contributor Author

sargun commented Mar 24, 2017

What if we do something like have a config option for PREEMPT_MODE, and have a function bool mark_preempt(gravity_vm *vm) (where the bool returns true / false depending on if the VM was in a preemptible context) to tell a vm to yield if #define PREEMPT_MODE internal and alternatively, you can have #define PREEMPT_MODE callback and #define PREEMPT_CB ...?

@marcobambini
Copy link
Owner

Hi @sargun I am going to re-open it because I'd like to use some of your ideas to implement a built-in debugger.

@marcobambini marcobambini reopened this Nov 27, 2017
@sargun
Copy link
Contributor Author

sargun commented Nov 27, 2017

Ah sounds good, I was just going through and cleaning out my github activity.

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.

4 participants