Skip to content

Commit 10e8936

Browse files
komygaapoalas
andauthored
Use Builtin Atomics for wait/waitAsync/notify (#956)
Co-authored-by: Felipe Armoni <komyg@users.noreply.github.com> Co-authored-by: Aapo Alasuutari <aapo.alasuutari@gmail.com>
1 parent dad3390 commit 10e8936

6 files changed

Lines changed: 366 additions & 122 deletions

File tree

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ cliclack = "0.4.0"
2121
console = "0.16.2"
2222
ctrlc = "3.5.0"
2323
ecmascript_atomics = { version = "0.2.3" }
24-
ecmascript_futex = { version = "0.1.0" }
2524
fast-float = "0.2.0"
2625
hashbrown = "0.16.1"
2726
lexical = { version = "7.0.5", default-features = false, features = [

nova_vm/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ categories.workspace = true
1414
[dependencies]
1515
ahash = { workspace = true }
1616
ecmascript_atomics = { workspace = true, optional = true }
17-
ecmascript_futex = { workspace = true, optional = true }
1817
fast-float = { workspace = true }
1918
hashbrown = { workspace = true }
2019
lexical = { workspace = true }
@@ -61,7 +60,7 @@ atomics = [
6160
"array-buffer",
6261
"shared-array-buffer",
6362
"dep:ecmascript_atomics",
64-
"dep:ecmascript_futex",
63+
6564
]
6665
date = []
6766
json = ["dep:sonic-rs"]

nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs

Lines changed: 107 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,29 @@
55
use std::{
66
hint::assert_unchecked,
77
ops::ControlFlow,
8-
sync::{Arc, atomic::AtomicBool},
8+
sync::{
9+
Arc,
10+
atomic::{AtomicBool, Ordering as StdOrdering},
11+
},
912
thread::{self, JoinHandle},
1013
time::Duration,
1114
};
1215

1316
use ecmascript_atomics::Ordering;
14-
use ecmascript_futex::{ECMAScriptAtomicWait, FutexError};
1517

1618
use crate::{
1719
ecmascript::{
1820
Agent, AnyArrayBuffer, AnyTypedArray, ArgumentsList, BUILTIN_STRING_MEMORY, Behaviour,
1921
BigInt, Builtin, ExceptionType, InnerJob, Job, JsResult, Number, Numeric, OrdinaryObject,
2022
Promise, PromiseCapability, Realm, SharedArrayBuffer, SharedDataBlock, SharedTypedArray,
2123
String, TryError, TryResult, TypedArrayAbstractOperations,
22-
TypedArrayWithBufferWitnessRecords, Value, builders::OrdinaryObjectBuilder,
23-
compare_exchange_in_buffer, for_any_typed_array, get_modify_set_value_in_buffer,
24-
get_value_from_buffer, make_typed_array_with_buffer_witness_record,
25-
number_convert_to_integer_or_infinity, set_value_in_buffer, to_big_int, to_big_int64,
26-
to_big_int64_big_int, to_index, to_int32, to_int32_number, to_integer_number_or_infinity,
27-
to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try,
28-
validate_index, validate_typed_array,
24+
TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterRecord,
25+
builders::OrdinaryObjectBuilder, compare_exchange_in_buffer, for_any_typed_array,
26+
get_modify_set_value_in_buffer, get_value_from_buffer,
27+
make_typed_array_with_buffer_witness_record, number_convert_to_integer_or_infinity,
28+
set_value_in_buffer, to_big_int, to_big_int64, to_big_int64_big_int, to_index, to_int32,
29+
to_int32_number, to_integer_number_or_infinity, to_integer_or_infinity, to_number,
30+
try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array,
2931
},
3032
engine::{Bindable, GcScope, Global, NoGcScope, Scopable},
3133
heap::{ObjectEntry, WellKnownSymbols},
@@ -666,36 +668,34 @@ impl AtomicsObject {
666668
};
667669
// 6. Let block be buffer.[[ArrayBufferData]].
668670
// 8. Let WL be GetWaiterList(block, byteIndexInBuffer).
669-
let is_big_int_64_array = matches!(typed_array, AnyTypedArray::SharedBigInt64Array(_));
670-
let slot = buffer.as_slice(agent).slice_from(byte_index_in_buffer);
671-
let n = if is_big_int_64_array {
672-
// SAFETY: offset was checked.
673-
let slot = unsafe { slot.as_aligned::<u64>().unwrap_unchecked() };
674-
if c == usize::MAX {
675-
// Force the notify count down into a reasonable range: the
676-
// ecmascript_futex may return usize::MAX if the OS doesn't
677-
// give us a count number.
678-
slot.notify_all().min(i32::MAX as usize)
679-
} else {
680-
slot.notify_many(c)
681-
}
682-
} else {
683-
// SAFETY: offset was checked.
684-
let slot = unsafe { slot.as_aligned::<u32>().unwrap_unchecked() };
685-
if c == usize::MAX {
686-
// Force the notify count down into a reasonable range: the
687-
// ecmascript_futex may return usize::MAX if the OS doesn't
688-
// give us a count number.
689-
slot.notify_all().min(i32::MAX as usize)
690-
} else {
691-
slot.notify_many(c)
692-
}
693-
};
671+
let data_block = buffer.get_data_block(agent);
694672
// 9. Perform EnterCriticalSection(WL).
673+
// SAFETY: buffer is a valid SharedArrayBuffer and cannot be detached. A 0-sized SAB has a
674+
// dangling data block with no backing allocation, but `get_waiters` returns `None` in that case.
675+
let mut n = 0;
676+
let Some(waiters) = (unsafe { data_block.get_waiters() }) else {
677+
return Ok(0.into());
678+
};
679+
680+
let mut guard = waiters.lock().unwrap();
695681
// 10. Let S be RemoveWaiters(WL, c).
682+
let Some(list) = guard.get_list_mut(byte_index_in_buffer) else {
683+
return Ok(0.into());
684+
};
685+
696686
// 11. For each element W of S, do
697-
// a. Perform NotifyWaiter(WL, W).
687+
while n < c {
688+
let Some(w) = list.pop() else {
689+
break;
690+
};
691+
// a. Perform NotifyWaiter(WL, W).
692+
w.notify_waiters();
693+
n += 1;
694+
}
695+
698696
// 12. Perform LeaveCriticalSection(WL).
697+
drop(guard);
698+
699699
// 13. Let n be the number of elements in S.
700700
// 14. Return 𝔽(n).
701701
Ok(Number::from_usize(agent, n, gc).into())
@@ -1486,37 +1486,47 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>(
14861486
// 28. Perform AddWaiter(WL, waiterRecord).
14871487
// 29. If mode is sync, then
14881488
if !IS_ASYNC {
1489-
// a. Perform SuspendThisAgent(WL, waiterRecord).
1490-
let result = if IS_I64 {
1491-
let v = v as u64;
1492-
// SAFETY: buffer is still live and index was checked.
1489+
let data_block = buffer.get_data_block(agent);
1490+
// SAFETY: buffer is a valid SharedArrayBuffer and cannot be detached. A 0-sized SAB would
1491+
// have a dangling data block, but Atomics.wait requires `byteIndex` to be within bounds,
1492+
// so a 0-sized SAB would have been rejected earlier with a RangeError.
1493+
let waiters = unsafe { data_block.get_or_init_waiters() };
1494+
let waiter_record = WaiterRecord::new_shared();
1495+
let mut guard = waiters.lock().unwrap();
1496+
1497+
// Re-read value under critical section to avoid TOCTOU race.
1498+
let slot = data_block.as_racy_slice().slice_from(byte_index_in_buffer);
1499+
let v_changed = if IS_I64 {
14931500
let slot = unsafe { slot.as_aligned::<u64>().unwrap_unchecked() };
1494-
if t == u64::MAX {
1495-
slot.wait(v)
1496-
} else {
1497-
slot.wait_timeout(v, Duration::from_millis(t))
1498-
}
1501+
v as u64 != slot.load(Ordering::SeqCst)
14991502
} else {
1500-
let v = v as u32;
1501-
// SAFETY: buffer is still live and index was checked.
15021503
let slot = unsafe { slot.as_aligned::<u32>().unwrap_unchecked() };
1503-
if t == u64::MAX {
1504-
slot.wait(v)
1505-
} else {
1506-
slot.wait_timeout(v, Duration::from_millis(t))
1507-
}
1504+
v as i32 as u32 != slot.load(Ordering::SeqCst)
15081505
};
1509-
// 31. Perform LeaveCriticalSection(WL).
1510-
// 32. If mode is sync, return waiterRecord.[[Result]].
1506+
if v_changed {
1507+
return BUILTIN_STRING_MEMORY.not_equal.into();
1508+
}
15111509

1512-
match result {
1513-
Ok(_) => BUILTIN_STRING_MEMORY.ok.into(),
1514-
Err(err) => match err {
1515-
FutexError::Timeout => BUILTIN_STRING_MEMORY.timed_out.into(),
1516-
FutexError::NotEqual => BUILTIN_STRING_MEMORY.not_equal.into(),
1517-
FutexError::Unknown => panic!(),
1518-
},
1510+
// a. Perform SuspendThisAgent(WL, waiterRecord).
1511+
guard.push_to_list(byte_index_in_buffer, waiter_record.clone());
1512+
1513+
if t == u64::MAX {
1514+
waiter_record.wait(guard);
1515+
} else {
1516+
let dur = Duration::from_millis(t);
1517+
let (new_guard, timeout) = waiter_record.wait_timeout(guard, dur);
1518+
guard = new_guard;
1519+
if timeout.timed_out() {
1520+
guard.remove_from_list(byte_index_in_buffer, waiter_record);
1521+
1522+
// 31. Perform LeaveCriticalSection(WL).
1523+
// 32. If mode is sync, return waiterRecord.[[Result]].
1524+
return BUILTIN_STRING_MEMORY.timed_out.into();
1525+
}
15191526
}
1527+
// 31. Perform LeaveCriticalSection(WL).
1528+
// 32. If mode is sync, return waiterRecord.[[Result]].
1529+
BUILTIN_STRING_MEMORY.ok.into()
15201530
} else {
15211531
let promise_capability = PromiseCapability::new(agent, gc);
15221532
let promise = Global::new(agent, promise_capability.promise.unbind());
@@ -1623,7 +1633,7 @@ fn create_wait_result_object<'gc>(
16231633
#[derive(Debug)]
16241634
struct WaitAsyncJobInner {
16251635
promise_to_resolve: Global<Promise<'static>>,
1626-
join_handle: JoinHandle<Result<(), FutexError>>,
1636+
join_handle: JoinHandle<WaitResult>,
16271637
_has_timeout: bool,
16281638
}
16291639

@@ -1662,18 +1672,8 @@ impl WaitAsyncJob {
16621672
// c. Perform LeaveCriticalSection(WL).
16631673
let promise_capability = PromiseCapability::from_promise(promise, true);
16641674
let result = match result {
1665-
Ok(_) => BUILTIN_STRING_MEMORY.ok.into(),
1666-
Err(FutexError::NotEqual) => BUILTIN_STRING_MEMORY.ok.into(),
1667-
Err(FutexError::Timeout) => BUILTIN_STRING_MEMORY.timed_out.into(),
1668-
Err(FutexError::Unknown) => {
1669-
let error = agent.throw_exception_with_static_message(
1670-
ExceptionType::Error,
1671-
"unknown error occurred",
1672-
gc,
1673-
);
1674-
promise_capability.reject(agent, error.value(), gc);
1675-
return Ok(());
1676-
}
1675+
WaitResult::Ok | WaitResult::NotEqual => BUILTIN_STRING_MEMORY.ok.into(),
1676+
WaitResult::TimedOut => BUILTIN_STRING_MEMORY.timed_out.into(),
16771677
};
16781678
unwrap_try(promise_capability.try_resolve(agent, result, gc));
16791679
// d. Return unused.
@@ -1701,28 +1701,45 @@ fn enqueue_atomics_wait_async_job<const IS_I64: bool>(
17011701
let signal = Arc::new(AtomicBool::new(false));
17021702
let s = signal.clone();
17031703
let handle = thread::spawn(move || {
1704+
// SAFETY: buffer is a cloned SharedDataBlock; non-dangling.
1705+
let waiters = unsafe { buffer.get_or_init_waiters() };
1706+
let waiter_record = WaiterRecord::new_shared();
1707+
let mut guard = waiters.lock().unwrap();
1708+
1709+
// Re-check the value under the critical section.
17041710
let slot = buffer.as_racy_slice().slice_from(byte_index_in_buffer);
1705-
if IS_I64 {
1706-
let v = v as u64;
1707-
// SAFETY: buffer is still live and index was checked.
1711+
let v_not_equal = if IS_I64 {
17081712
let slot = unsafe { slot.as_aligned::<u64>().unwrap_unchecked() };
1709-
s.store(true, std::sync::atomic::Ordering::Release);
1710-
if t == u64::MAX {
1711-
slot.wait(v)
1712-
} else {
1713-
slot.wait_timeout(v, Duration::from_millis(t))
1714-
}
1713+
v as u64 != slot.load(Ordering::SeqCst)
17151714
} else {
1716-
let v = v as i32 as u32;
1717-
// SAFETY: buffer is still live and index was checked.
17181715
let slot = unsafe { slot.as_aligned::<u32>().unwrap_unchecked() };
1719-
s.store(true, std::sync::atomic::Ordering::Release);
1720-
if t == u64::MAX {
1721-
slot.wait(v)
1722-
} else {
1723-
slot.wait_timeout(v, Duration::from_millis(t))
1716+
v as i32 as u32 != slot.load(Ordering::SeqCst)
1717+
};
1718+
1719+
// Signal the main thread that we have the lock and are about to sleep.
1720+
s.store(true, StdOrdering::Release);
1721+
1722+
if v_not_equal {
1723+
return WaitResult::NotEqual;
1724+
}
1725+
1726+
guard.push_to_list(byte_index_in_buffer, waiter_record.clone());
1727+
1728+
if t == u64::MAX {
1729+
waiter_record.wait(guard);
1730+
} else {
1731+
let dur = Duration::from_millis(t);
1732+
let (new_guard, timeout) = waiter_record.wait_timeout(guard, dur);
1733+
guard = new_guard;
1734+
if timeout.timed_out() {
1735+
guard.remove_from_list(byte_index_in_buffer, waiter_record);
1736+
1737+
// 31. Perform LeaveCriticalSection(WL).
1738+
// 32. If mode is sync, return waiterRecord.[[Result]].
1739+
return WaitResult::TimedOut;
17241740
}
17251741
}
1742+
WaitResult::Ok
17261743
});
17271744
let wait_async_job = Job {
17281745
realm: Some(Global::new(agent, agent.current_realm(gc).unbind())),
@@ -1732,7 +1749,7 @@ fn enqueue_atomics_wait_async_job<const IS_I64: bool>(
17321749
_has_timeout: t != u64::MAX,
17331750
}))),
17341751
};
1735-
while !signal.load(std::sync::atomic::Ordering::Acquire) {
1752+
while !signal.load(StdOrdering::Acquire) {
17361753
// Wait until the thread has started up and is about to go to sleep.
17371754
}
17381755
// 2. Let now be the time value (UTC) identifying the current time.

0 commit comments

Comments
 (0)