Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/js-component-bindgen/src/function_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1928,6 +1928,8 @@ impl Bindgen for FunctionBindgen<'_> {
let get_component_state = self.intrinsic(Intrinsic::Component(
ComponentIntrinsic::GetOrCreateAsyncState,
));
let track_host_operation =
self.intrinsic(Intrinsic::Component(ComponentIntrinsic::TrackHostOperation));
let start_current_task_fn = self.intrinsic(Intrinsic::AsyncTask(
AsyncTaskIntrinsic::CreateNewCurrentTask,
));
Expand Down Expand Up @@ -2105,7 +2107,7 @@ impl Bindgen for FunctionBindgen<'_> {
r#"{call_prefix} {call_wrapper}({{
componentIdx: task.componentIdx(),
taskID: task.id(),
fn: () => {callee_fn_js}({callee_args_js}),
fn: () => {track_host_operation}(() => {callee_fn_js}({callee_args_js})),
}})
"#,
);
Expand Down
119 changes: 119 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ pub enum ComponentIntrinsic {
/// Shared trap state for all component instances in this generated store.
GlobalStoreTrap,

/// Shared scheduling state for all component instances in this generated store.
GlobalStoreAsyncState,

/// Schedule a store-wide deadlock check after queued guest work has drained.
CheckForDeadlock,

/// Track a possibly asynchronous host operation as an external wake source.
TrackHostOperation,

/// Trap if the specified component instance may not currently leave.
CheckMayLeave,

Expand Down Expand Up @@ -78,6 +87,9 @@ impl ComponentIntrinsic {
match self {
Self::GlobalInstanceFlagsMap => "INSTANCE_FLAGS",
Self::GlobalStoreTrap => "STORE_TRAP",
Self::GlobalStoreAsyncState => "STORE_ASYNC_STATE",
Self::CheckForDeadlock => "_checkForDeadlock",
Self::TrackHostOperation => "_trackHostOperation",
Self::CheckMayLeave => "_checkMayLeave",
Self::GuardMayLeave => "_guardMayLeave",
Self::GlobalAsyncStateMap => "ASYNC_STATE",
Expand All @@ -102,6 +114,96 @@ impl ComponentIntrinsic {
uwriteln!(output, r#"const {var_name} = {{ error: null }};"#);
}

Self::GlobalStoreAsyncState => {
let var_name = render_args.require_intrinsic(Self::GlobalStoreAsyncState);
uwriteln!(
output,
r#"const {var_name} = {{ deadlockCheck: null, pendingHostOperations: 0 }};"#
);
}

Self::CheckForDeadlock => {
let check_for_deadlock_fn = render_args.require_intrinsic(Self::CheckForDeadlock);
let async_state_map = render_args.require_intrinsic(Self::GlobalAsyncStateMap);
let store_async_state = render_args.require_intrinsic(Self::GlobalStoreAsyncState);
let store_trap = render_args.require_intrinsic(Self::GlobalStoreTrap);
let runtime_error_class =
render_args.require_intrinsic(Intrinsic::WebAssemblyRuntimeError);
output.push_str(&format!(
r#"
function {check_for_deadlock_fn}() {{
if ({store_async_state}.deadlockCheck !== null || {store_trap}.error !== null) {{ return; }}
{store_async_state}.deadlockCheck = setTimeout(() => {{
{store_async_state}.deadlockCheck = null;
if ({store_trap}.error !== null || {store_async_state}.pendingHostOperations > 0) {{ return; }}

const suspendedTasks = new Set();
for (const state of {async_state_map}.values()) {{
if (state.hasPendingSchedulerWork()) {{
state.runTickLoop();
return;
}}
for (const meta of state.suspendedTaskMetas()) {{
suspendedTasks.add(meta.task);
}}
}}

const unresolvedRoots = new Set();
for (const task of suspendedTasks) {{
const root = task.getRootTask();
if (!root.isResolvedState()) {{ unresolvedRoots.add(root); }}
}}
if (unresolvedRoots.size === 0) {{ return; }}

const err = new {runtime_error_class}('wasm trap: deadlock detected: event loop cannot make further progress');
{store_trap}.error = err;
for (const root of unresolvedRoots) {{
root.setErrored(err);
root.reject(err);
}}
for (const task of suspendedTasks) {{
if (!task.isResolvedState() && unresolvedRoots.has(task.getRootTask())) {{
task.setErrored(err);
task.reject(err);
}}
}}
for (const state of {async_state_map}.values()) {{ state.runTickLoop(); }}
}}, 0);
}}
"#,
));
}

Self::TrackHostOperation => {
let track_host_operation_fn =
render_args.require_intrinsic(Self::TrackHostOperation);
let check_for_deadlock_fn = render_args.require_intrinsic(Self::CheckForDeadlock);
let async_state_map = render_args.require_intrinsic(Self::GlobalAsyncStateMap);
let store_async_state = render_args.require_intrinsic(Self::GlobalStoreAsyncState);
output.push_str(&format!(
r#"
function {track_host_operation_fn}(operation) {{
const result = operation();
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function') ||
typeof result.then !== 'function') {{
return result;
}}

{store_async_state}.pendingHostOperations++;
return Promise.resolve(result).finally(() => {{
{store_async_state}.pendingHostOperations--;
if ({store_async_state}.pendingHostOperations < 0) {{
throw new Error('negative pending host operation count');
}}
for (const state of {async_state_map}.values()) {{ state.runTickLoop(); }}
{check_for_deadlock_fn}();
}});
}}
"#,
));
}

Self::CheckMayLeave => {
let check_may_leave_fn = render_args.require_intrinsic(Self::CheckMayLeave);
let instance_flags = render_args.require_intrinsic(Self::GlobalInstanceFlagsMap);
Expand Down Expand Up @@ -185,6 +287,7 @@ impl ComponentIntrinsic {
render_args.require_intrinsic(Intrinsic::WebAssemblyRuntimeError);
let instance_flags = render_args.require_intrinsic(Self::GlobalInstanceFlagsMap);
let store_trap = render_args.require_intrinsic(Self::GlobalStoreTrap);
let check_for_deadlock_fn = render_args.require_intrinsic(Self::CheckForDeadlock);

output.push_str(&format!(
r#"
Expand Down Expand Up @@ -579,6 +682,7 @@ impl ComponentIntrinsic {
task.notifyProgress();

this.runTickLoop();
{check_for_deadlock_fn}();

return promise;
}}
Expand All @@ -600,12 +704,27 @@ impl ComponentIntrinsic {
return meta.task.isRejected() || meta.readyFn();
}}

suspendedTaskMetas() {{
return this.#suspendedTasksByTaskID.values();
}}

hasPendingSchedulerWork() {{
if (this.#lockHandoffScheduled) {{ return true; }}
for (const meta of this.#suspendedTasksByTaskID.values()) {{
if (meta.task.isRejected() || meta.readyFn()) {{ return true; }}
}}
return false;
}}

async runTickLoop() {{
if (this.#tickLoop !== null) {{ return; }}
this.#tickLoop = 1;
setTimeout(async () => {{
let result = this.tick();
while (result !== {component_async_state_class}.TickResult.DONE) {{
if (result === {component_async_state_class}.TickResult.IDLE) {{
{check_for_deadlock_fn}();
}}
// After resuming a task, re-tick as soon as the resumed
// slice's microtask continuations have drained (timeout 0)
// so queued sibling resumptions aren't charged the idle
Expand Down
42 changes: 42 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,48 @@ mod tests {
assert!(yield_.contains("return keepGoing ? 0 : 1;"));
}

#[test]
fn async_scheduler_detects_store_wide_deadlocks() {
let state = render_intrinsic_body(Intrinsic::Component(
ComponentIntrinsic::ComponentAsyncStateClass,
));
assert!(state.contains("_checkForDeadlock();"));
assert!(state.contains("suspendedTaskMetas()"));
assert!(state.contains("hasPendingSchedulerWork()"));

let check =
render_intrinsic_body(Intrinsic::Component(ComponentIntrinsic::CheckForDeadlock));
assert!(check.contains("for (const state of ASYNC_STATE.values())"));
assert!(check.contains("STORE_ASYNC_STATE.pendingHostOperations > 0"));
assert!(check.contains("const root = task.getRootTask();"));
assert!(check.contains(
"new WebAssemblyRuntimeError('wasm trap: deadlock detected: event loop cannot make further progress')"
));
assert!(check.contains("for (const root of unresolvedRoots)"));
assert!(check.contains("root.reject(err);"));
assert!(check.contains("task.reject(err);"));
}

#[test]
fn host_async_operations_suppress_deadlock_detection() {
let tracker =
render_intrinsic_body(Intrinsic::Component(ComponentIntrinsic::TrackHostOperation));
assert!(tracker.contains("STORE_ASYNC_STATE.pendingHostOperations++;"));
assert!(tracker.contains("Promise.resolve(result).finally(() =>"));
assert!(tracker.contains("STORE_ASYNC_STATE.pendingHostOperations--;"));
assert!(tracker.contains("_checkForDeadlock();"));

let future = render_intrinsic_body(Intrinsic::AsyncFuture(
AsyncFutureIntrinsic::GenFutureHostInjectFn,
));
assert!(future.contains("await _trackHostOperation(() => promise);"));

let stream = render_intrinsic_body(Intrinsic::AsyncStream(
AsyncStreamIntrinsic::PendingValueQueueClass,
));
assert!(stream.contains("await _trackHostOperation(() => this.#readFn());"));
}

#[test]
fn sync_start_fused_adapter_runs_in_the_caller_task() {
let source = render_intrinsic_body(Intrinsic::Host(HostIntrinsic::SyncStartCall));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,8 @@ impl AsyncFutureIntrinsic {
let gen_host_inject_fn = self.name();
let nested_future_symbol = render_args.require_intrinsic(Self::NestedFutureSymbol);
let get_error_payload = render_args.require_intrinsic(Intrinsic::GetErrorPayload);
let track_host_operation =
render_args.require_intrinsic(ComponentIntrinsic::TrackHostOperation);

uwriteln!(
output,
Expand Down Expand Up @@ -1798,7 +1800,7 @@ impl AsyncFutureIntrinsic {

let value;
try {{
value = await promise;
value = await {track_host_operation}(() => promise);
}} catch (err) {{
const elemMeta = hostWriteEnd.getElemMeta();
if (!elemMeta.payloadTypeName?.startsWith('Result(')) {{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1838,6 +1838,8 @@ impl AsyncStreamIntrinsic {

Self::PendingValueQueueClass => {
let pending_value_queue_class = self.name();
let track_host_operation =
render_args.require_intrinsic(ComponentIntrinsic::TrackHostOperation);

output.push_str(&format!(
r#"
Expand Down Expand Up @@ -1882,7 +1884,7 @@ impl AsyncStreamIntrinsic {
async readSource() {{
if (!this.#sourceReadPromise) {{
this.#sourceReadPromise = (async () => {{
const res = await this.#readFn();
const res = await {track_host_operation}(() => this.#readFn());
const appended = this.appendReadValue(res.value);
this.#done = res.done;
return appended;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ const WAST_TESTS: readonly WastTest[] = [
{ relPath: 'async/drop-subtask.wast' },
{ relPath: 'async/async-calls-sync.wast' },
{ relPath: 'async/cancellable.wast' },
{ relPath: 'async/deadlock.wast' },

// Skipped tests
{ relPath: 'async/sync-streams.wast', skip: true },
{ relPath: 'async/deadlock.wast', skip: true },
{ relPath: 'async/trap-if-block-and-sync.wast', skip: true },
{ relPath: 'async/trap-on-reenter.wast', skip: true },
{ relPath: 'async/sync-barges-in.wast', skip: true },
Expand Down
Loading