-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is an implementation of Julia's coroutine/tasking system on top of the binaryen Asyncify transform [1] recently implemented by Alon Zakai. The wasm target is unusual in several ways: 1. It has an implicitly managed call stack that we may not modify directly (i.e. is only modified through calls/returns). 2. The event loop is inverted, in that the browser runs the main event loop and we get callbacks for events, rather than Julia being the main event loop. Asyncify takes care of the first problem by providing a mechanism to explicitly unwind and rewind the implicitly managed stack (essentially copying it to an explicitly managed stack - see the linked implementation for more information). For the second, I am currently using the ptls root_task to represent the browser event loop, i.e. yielding to that task will return control back to the browser and this is the task in which functions called by javascript will run unless they explicitly construct a task to run. As a result, julia code executed in the main task may not perform any blocking operations (though this is currently not enforced). I think this is a sensible setup since the main task will want to run some minor julia code (e.g. to introspect some data structures), but the bulk of the code will run in their own tasks (e.g. the REPL backend task). [1] https://github.com/WebAssembly/binaryen/blob/master/src/passes/Asyncify.cpp
- Loading branch information
Showing
6 changed files
with
252 additions
and
9 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
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,144 @@ | ||
Module.preRun.push(function() { | ||
if (typeof Asyncify !== "undefined") { | ||
Asyncify.instrumentWasmExports = function (exports) { return exports; }; | ||
Asyncify.handleSleep = function (startAsync) { | ||
if (ABORT) return; | ||
Module['noExitRuntime'] = true; | ||
if (Asyncify.state === Asyncify.State.Normal) { | ||
// Prepare to sleep. Call startAsync, and see what happens: | ||
// if the code decided to call our callback synchronously, | ||
// then no async operation was in fact begun, and we don't | ||
// need to do anything. | ||
var reachedCallback = false; | ||
var reachedAfterCallback = false; | ||
var task = get_current_task(); | ||
startAsync(function(returnValue) { | ||
assert(!returnValue || typeof returnValue === 'number'); // old emterpretify API supported other stuff | ||
if (ABORT) return; | ||
Asyncify.returnValue = returnValue || 0; | ||
reachedCallback = true; | ||
if (!reachedAfterCallback) { | ||
// We are happening synchronously, so no need for async. | ||
return; | ||
} | ||
schedule_and_wait(task); | ||
}); | ||
reachedAfterCallback = true; | ||
if (!reachedCallback) { | ||
Module['_jl_task_wait'](); | ||
} | ||
} else if (Asyncify.state === Asyncify.State.Rewinding) { | ||
// Stop a resume. | ||
finish_schedule_task(); | ||
} else { | ||
abort('invalid state: ' + Asyncify.state); | ||
} | ||
return Asyncify.returnValue; | ||
}; | ||
} | ||
}); | ||
|
||
function get_current_task() { | ||
return Module['_jl_get_current_task'](); | ||
} | ||
|
||
function get_root_task() { | ||
return Module['_jl_get_root_task'](); | ||
} | ||
|
||
function task_ctx_ptr(task) { | ||
return Module["_task_ctx_ptr"](task); | ||
} | ||
|
||
function ctx_save(ctx) { | ||
var stackPtr = stackSave(); | ||
|
||
// Save the bottom of the C stack in the task context. It simultaneously | ||
// serves as the top of the asyncify stack. | ||
HEAP32[ctx + 4 >> 2] = stackPtr; | ||
|
||
Asyncify.state = Asyncify.State.Unwinding; | ||
Module['_asyncify_start_unwind'](ctx); | ||
if (Browser.mainLoop.func) { | ||
Browser.mainLoop.pause(); | ||
} | ||
} | ||
|
||
function do_start_task(old_stack) | ||
{ | ||
try { | ||
// start_task is always the entry point for any task | ||
Module['_start_task'](); | ||
} catch(e) { | ||
stackRestore(old_stack) | ||
if (e !== e+0 && e !== 'killed') throw e; | ||
maybe_schedule_next(); | ||
return; | ||
} | ||
// Either unwind or normal exit. In either case, we're back at the main task | ||
if (Asyncify.state === Asyncify.State.Unwinding) { | ||
// We just finished unwinding for a sleep. | ||
Asyncify.state = Asyncify.State.Normal; | ||
Module['_asyncify_stop_unwind'](); | ||
} | ||
stackRestore(old_stack); | ||
maybe_schedule_next(); | ||
} | ||
|
||
function schedule_and_wait(task) { | ||
Module['_jl_schedule_task'](task); | ||
Module['_jl_task_wait'](); | ||
} | ||
|
||
function finish_schedule_task() { | ||
Asyncify.state = Asyncify.State.Normal; | ||
Module['_asyncify_stop_rewind'](); | ||
} | ||
|
||
next_ctx = 0; | ||
next_need_start = true; | ||
function set_next_ctx(ctx, needs_start) { | ||
next_ctx = ctx; | ||
next_need_start = needs_start; | ||
} | ||
|
||
function root_ctx() { | ||
return task_ctx_ptr(get_root_task()) | ||
} | ||
|
||
function ctx_switch(lastt_ctx) { | ||
if (lastt_ctx == root_ctx()) { | ||
// If we're in the root context, switch to | ||
// the new ctx now, else we'll get there after | ||
// unwinding. | ||
return schedule_next() | ||
} else if (lastt_ctx == 0) { | ||
throw 'killed'; | ||
} else { | ||
return ctx_save(lastt_ctx); | ||
} | ||
} | ||
|
||
function schedule_next() | ||
{ | ||
old_stack = stackSave(); | ||
var next_task_stack = HEAP32[next_ctx + 4 >> 2]; | ||
if (!next_need_start) { | ||
Asyncify.state = Asyncify.State.Rewinding; | ||
Module['_asyncify_start_rewind'](next_ctx); | ||
if (Browser.mainLoop.func) { | ||
Browser.mainLoop.resume(); | ||
} | ||
} | ||
next_ctx = -1; | ||
stackRestore(next_task_stack); | ||
do_start_task(old_stack) | ||
} | ||
|
||
function maybe_schedule_next() { | ||
assert(next_ctx != -1); | ||
if (next_ctx == root_ctx() || next_ctx == 0) { | ||
return; | ||
} | ||
schedule_next() | ||
} |
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,15 @@ | ||
mergeInto(LibraryManager.library, { | ||
jl_set_fiber: function(ctx) { | ||
set_next_ctx(ctx, false); | ||
return ctx_switch(0) | ||
}, | ||
jl_swap_fiber: function(lastt_ctx, ctx) { | ||
set_next_ctx(ctx, false); | ||
return ctx_switch(lastt_ctx) | ||
}, | ||
jl_start_fiber: function(lastt_ctx, ctx) { | ||
set_next_ctx(ctx, true); | ||
return ctx_switch(lastt_ctx) | ||
} | ||
}); | ||
|
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