Commit 5eaa03b2 authored by Anna Henningsen's avatar Anna Henningsen Committed by Commit Bot

[api] Add `AtomicsWaitCallback` for diagnostics

Add an inspection callback for embedders that allows tracking
of `Atomics.wait()` calls in order to enable diagnostic tooling
around it, as well as providing a way to break out of an
`Atomics.wait()` call without having to fully terminate execution.

The motivation here is that this allows embedders to perform
somewhat customizable deadlock detection.

Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng
Change-Id: Ib6346747aa3cbffb07cf6abd12645e2d98584f0f
Reviewed-on: https://chromium-review.googlesource.com/1080788
Commit-Queue: Yang Guo <yangguo@chromium.org>
Reviewed-by: 's avatarYang Guo <yangguo@chromium.org>
Reviewed-by: 's avatarBen Smith <binji@chromium.org>
Cr-Commit-Position: refs/heads/master@{#53517}
parent 10cfe818
......@@ -7605,6 +7605,87 @@ class V8_EXPORT Isolate {
*/
void SetEmbedderHeapTracer(EmbedderHeapTracer* tracer);
/**
* Use for |AtomicsWaitCallback| to indicate the type of event it receives.
*/
enum class AtomicsWaitEvent {
/** Indicates that this call is happening before waiting. */
kStartWait,
/** `Atomics.wait()` finished because of an `Atomics.wake()` call. */
kWokenUp,
/** `Atomics.wait()` finished because it timed out. */
kTimedOut,
/** `Atomics.wait()` was interrupted through |TerminateExecution()|. */
kTerminatedExecution,
/** `Atomics.wait()` was stopped through |AtomicsWaitWakeHandle|. */
kAPIStopped
};
/**
* Passed to |AtomicsWaitCallback| as a means of stopping an ongoing
* `Atomics.wait` call.
*/
class V8_EXPORT AtomicsWaitWakeHandle {
public:
/**
* Stop this `Atomics.wait()` call and call the |AtomicsWaitCallback|
* with |kAPIStopped|.
*
* This function may be called from another thread. The caller has to ensure
* through proper synchronization that it is not called after
* the finishing |AtomicsWaitCallback|.
*
* Note that the ECMAScript specification does not plan for the possibility
* of wakeups that are neither coming from a timeout or an `Atomics.wake()`
* call, so this may invalidate assumptions made by existing code.
* The embedder may accordingly wish to schedule an exception in the
* finishing |AtomicsWaitCallback|.
*/
void Wake();
};
/**
* Embedder callback for `Atomics.wait()` that can be added through
* |SetAtomicsWaitCallback|.
*
* This will be called just before starting to wait with the |event| value
* |kStartWait| and after finishing waiting with one of the other
* values of |AtomicsWaitEvent| inside of an `Atomics.wait()` call.
*
* |array_buffer| will refer to the underlying SharedArrayBuffer,
* |offset_in_bytes| to the location of the waited-on memory address inside
* the SharedArrayBuffer.
*
* |value| and |timeout_in_ms| will be the values passed to
* the `Atomics.wait()` call. If no timeout was used, |timeout_in_ms|
* will be `INFINITY`.
*
* In the |kStartWait| callback, |stop_handle| will be an object that
* is only valid until the corresponding finishing callback and that
* can be used to stop the wait process while it is happening.
*
* This callback may schedule exceptions, *unless* |event| is equal to
* |kTerminatedExecution|.
*
* This callback is not called if |value| did not match the expected value
* inside the SharedArrayBuffer and `Atomics.wait()` returns immediately
* because of that.
*/
typedef void (*AtomicsWaitCallback)(AtomicsWaitEvent event,
Local<SharedArrayBuffer> array_buffer,
size_t offset_in_bytes, int32_t value,
double timeout_in_ms,
AtomicsWaitWakeHandle* stop_handle,
void* data);
/**
* Set a new |AtomicsWaitCallback|. This overrides an earlier
* |AtomicsWaitCallback|, if there was any. If |callback| is nullptr,
* this unsets the callback. |data| will be passed to the callback
* as its last parameter.
*/
void SetAtomicsWaitCallback(AtomicsWaitCallback callback, void* data);
/**
* Enables the host application to receive a notification after a
* garbage collection. Allocations are allowed in the callback function,
......
......@@ -8596,6 +8596,15 @@ void Isolate::RemoveCallCompletedCallback(
reinterpret_cast<CallCompletedCallback>(callback));
}
void Isolate::AtomicsWaitWakeHandle::Wake() {
reinterpret_cast<i::AtomicsWaitWakeHandle*>(this)->Wake();
}
void Isolate::SetAtomicsWaitCallback(AtomicsWaitCallback callback, void* data) {
i::Isolate* isolate = reinterpret_cast<i::Isolate*>(this);
isolate->SetAtomicsWaitCallback(callback, data);
}
void Isolate::SetPromiseHook(PromiseHook hook) {
i::Isolate* isolate = reinterpret_cast<i::Isolate*>(this);
isolate->SetPromiseHook(hook);
......
......@@ -16,6 +16,8 @@
namespace v8 {
namespace internal {
using AtomicsWaitEvent = v8::Isolate::AtomicsWaitEvent;
base::LazyMutex FutexEmulation::mutex_ = LAZY_MUTEX_INITIALIZER;
base::LazyInstance<FutexWaitList>::type FutexEmulation::wait_list_ =
LAZY_INSTANCE_INITIALIZER;
......@@ -71,6 +73,10 @@ void FutexWaitList::RemoveNode(FutexWaitListNode* node) {
node->prev_ = node->next_ = nullptr;
}
void AtomicsWaitWakeHandle::Wake() {
stopped_ = true;
isolate_->futex_wait_list_node()->NotifyWake();
}
Object* FutexEmulation::Wait(Isolate* isolate,
Handle<JSArrayBuffer> array_buffer, size_t addr,
......@@ -114,7 +120,17 @@ Object* FutexEmulation::Wait(Isolate* isolate,
base::TimeTicks timeout_time = start_time + rel_timeout;
base::TimeTicks current_time = start_time;
AtomicsWaitWakeHandle stop_handle(isolate);
isolate->RunAtomicsWaitCallback(AtomicsWaitEvent::kStartWait, array_buffer,
addr, value, rel_timeout_ms, &stop_handle);
if (isolate->has_scheduled_exception()) {
return isolate->PromoteScheduledException();
}
Object* result;
AtomicsWaitEvent callback_result = AtomicsWaitEvent::kWokenUp;
{
base::LockGuard<base::Mutex> lock_guard(mutex_.Pointer());
......@@ -144,6 +160,7 @@ Object* FutexEmulation::Wait(Isolate* isolate,
Object* interrupt_object = isolate->stack_guard()->HandleInterrupts();
if (interrupt_object->IsException(isolate)) {
result = interrupt_object;
callback_result = AtomicsWaitEvent::kTerminatedExecution;
mutex_.Pointer()->Lock();
break;
}
......@@ -156,6 +173,11 @@ Object* FutexEmulation::Wait(Isolate* isolate,
continue;
}
if (stop_handle.has_stopped()) {
node->waiting_ = false;
callback_result = AtomicsWaitEvent::kAPIStopped;
}
if (!node->waiting_) {
result = isolate->heap()->ok();
break;
......@@ -166,6 +188,7 @@ Object* FutexEmulation::Wait(Isolate* isolate,
current_time = base::TimeTicks::Now();
if (current_time >= timeout_time) {
result = isolate->heap()->timed_out();
callback_result = AtomicsWaitEvent::kTimedOut;
break;
}
......@@ -184,8 +207,16 @@ Object* FutexEmulation::Wait(Isolate* isolate,
wait_list_.Pointer()->RemoveNode(node);
}
isolate->RunAtomicsWaitCallback(callback_result, array_buffer, addr, value,
rel_timeout_ms, nullptr);
node->waiting_ = false;
if (isolate->has_scheduled_exception()) {
CHECK_NE(callback_result, AtomicsWaitEvent::kTerminatedExecution);
result = isolate->PromoteScheduledException();
}
return result;
}
......
......@@ -35,6 +35,18 @@ class Handle;
class Isolate;
class JSArrayBuffer;
class AtomicsWaitWakeHandle {
public:
explicit AtomicsWaitWakeHandle(Isolate* isolate) : isolate_(isolate) {}
void Wake();
inline bool has_stopped() const { return stopped_; }
private:
Isolate* isolate_;
bool stopped_ = false;
};
class FutexWaitListNode {
public:
FutexWaitListNode()
......
......@@ -2513,6 +2513,8 @@ Isolate::Isolate()
fuzzer_rng_(nullptr),
rail_mode_(PERFORMANCE_ANIMATION),
promise_hook_or_debug_is_active_(false),
atomics_wait_callback_(nullptr),
atomics_wait_callback_data_(nullptr),
promise_hook_(nullptr),
load_start_time_ms_(0),
serializer_enabled_(false),
......@@ -3841,6 +3843,27 @@ void Isolate::SetHostInitializeImportMetaObjectCallback(
host_initialize_import_meta_object_callback_ = callback;
}
void Isolate::SetAtomicsWaitCallback(v8::Isolate::AtomicsWaitCallback callback,
void* data) {
atomics_wait_callback_ = callback;
atomics_wait_callback_data_ = data;
}
void Isolate::RunAtomicsWaitCallback(v8::Isolate::AtomicsWaitEvent event,
Handle<JSArrayBuffer> array_buffer,
size_t offset_in_bytes, int32_t value,
double timeout_in_ms,
AtomicsWaitWakeHandle* stop_handle) {
DCHECK(array_buffer->is_shared());
if (atomics_wait_callback_ == nullptr) return;
HandleScope handle_scope(this);
atomics_wait_callback_(
event, v8::Utils::ToLocalShared(array_buffer), offset_in_bytes, value,
timeout_in_ms,
reinterpret_cast<v8::Isolate::AtomicsWaitWakeHandle*>(stop_handle),
atomics_wait_callback_data_);
}
void Isolate::SetPromiseHook(PromiseHook hook) {
promise_hook_ = hook;
DebugStateUpdated();
......
......@@ -1255,6 +1255,14 @@ class Isolate : private HiddenFactory {
void DebugStateUpdated();
void SetAtomicsWaitCallback(v8::Isolate::AtomicsWaitCallback callback,
void* data);
void RunAtomicsWaitCallback(v8::Isolate::AtomicsWaitEvent event,
Handle<JSArrayBuffer> array_buffer,
size_t offset_in_bytes, int32_t value,
double timeout_in_ms,
AtomicsWaitWakeHandle* stop_handle);
void SetPromiseHook(PromiseHook hook);
void RunPromiseHook(PromiseHookType type, Handle<JSPromise> promise,
Handle<Object> parent);
......@@ -1542,6 +1550,8 @@ class Isolate : private HiddenFactory {
base::RandomNumberGenerator* fuzzer_rng_;
base::AtomicValue<RAILMode> rail_mode_;
bool promise_hook_or_debug_is_active_;
v8::Isolate::AtomicsWaitCallback atomics_wait_callback_;
void* atomics_wait_callback_data_;
PromiseHook promise_hook_;
HostImportModuleDynamicallyCallback host_import_module_dynamically_callback_;
HostInitializeImportMetaObjectCallback
......
......@@ -28053,3 +28053,207 @@ TEST(WasmStreamingAbortNoReject) {
streaming.Abort({});
CHECK_EQ(streaming.GetPromise()->State(), v8::Promise::kPending);
}
enum class AtomicsWaitCallbackAction {
Interrupt,
StopAndThrowInFirstCall,
StopAndThrowInSecondCall,
StopFromThreadAndThrow,
KeepWaiting
};
class StopAtomicsWaitThread;
struct AtomicsWaitCallbackInfo {
v8::Isolate* isolate;
v8::Isolate::AtomicsWaitWakeHandle* wake_handle;
std::unique_ptr<StopAtomicsWaitThread> stop_thread;
AtomicsWaitCallbackAction action;
Local<v8::SharedArrayBuffer> expected_sab;
v8::Isolate::AtomicsWaitEvent expected_event;
double expected_timeout;
int32_t expected_value;
size_t expected_offset;
size_t ncalls = 0;
};
class StopAtomicsWaitThread : public v8::base::Thread {
public:
explicit StopAtomicsWaitThread(AtomicsWaitCallbackInfo* info)
: Thread(Options("StopAtomicsWaitThread")), info_(info) {}
virtual void Run() {
CHECK_NOT_NULL(info_->wake_handle);
info_->wake_handle->Wake();
}
private:
AtomicsWaitCallbackInfo* info_;
};
void AtomicsWaitCallbackForTesting(
v8::Isolate::AtomicsWaitEvent event, Local<v8::SharedArrayBuffer> sab,
size_t offset_in_bytes, int32_t value, double timeout_in_ms,
v8::Isolate::AtomicsWaitWakeHandle* wake_handle, void* data) {
AtomicsWaitCallbackInfo* info = static_cast<AtomicsWaitCallbackInfo*>(data);
info->ncalls++;
info->wake_handle = wake_handle;
CHECK(sab->StrictEquals(info->expected_sab));
CHECK_EQ(timeout_in_ms, info->expected_timeout);
CHECK_EQ(value, info->expected_value);
CHECK_EQ(offset_in_bytes, info->expected_offset);
auto ThrowSomething = [&]() {
info->isolate->ThrowException(v8::Integer::New(info->isolate, 42));
};
if (event == v8::Isolate::AtomicsWaitEvent::kStartWait) {
CHECK_NOT_NULL(wake_handle);
switch (info->action) {
case AtomicsWaitCallbackAction::Interrupt:
info->isolate->TerminateExecution();
break;
case AtomicsWaitCallbackAction::StopAndThrowInFirstCall:
ThrowSomething();
V8_FALLTHROUGH;
case AtomicsWaitCallbackAction::StopAndThrowInSecondCall:
wake_handle->Wake();
break;
case AtomicsWaitCallbackAction::StopFromThreadAndThrow:
info->stop_thread = v8::base::make_unique<StopAtomicsWaitThread>(info);
info->stop_thread->Start();
break;
case AtomicsWaitCallbackAction::KeepWaiting:
break;
}
} else {
CHECK_EQ(event, info->expected_event);
CHECK_NULL(wake_handle);
if (info->stop_thread) {
info->stop_thread->Join();
info->stop_thread.reset();
}
if (info->action == AtomicsWaitCallbackAction::StopAndThrowInSecondCall ||
info->action == AtomicsWaitCallbackAction::StopFromThreadAndThrow) {
ThrowSomething();
}
}
}
TEST(AtomicsWaitCallback) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope scope(isolate);
Local<Value> sab = CompileRun(
"sab = new SharedArrayBuffer(12);"
"int32arr = new Int32Array(sab, 4);"
"sab");
CHECK(sab->IsSharedArrayBuffer());
AtomicsWaitCallbackInfo info;
info.isolate = isolate;
info.expected_sab = sab.As<v8::SharedArrayBuffer>();
isolate->SetAtomicsWaitCallback(AtomicsWaitCallbackForTesting, &info);
{
v8::TryCatch try_catch(isolate);
info.expected_offset = 4;
info.expected_timeout = std::numeric_limits<double>::infinity();
info.expected_value = 0;
info.expected_event = v8::Isolate::AtomicsWaitEvent::kTerminatedExecution;
info.action = AtomicsWaitCallbackAction::Interrupt;
info.ncalls = 0;
CompileRun("Atomics.wait(int32arr, 0, 0);");
CHECK_EQ(info.ncalls, 2);
CHECK(try_catch.HasTerminated());
}
{
v8::TryCatch try_catch(isolate);
CompileRun("Atomics.wait(int32arr, 1, 1);"); // real value is 0 != 1
CHECK_EQ(info.ncalls, 2);
CHECK(!try_catch.HasCaught());
}
{
v8::TryCatch try_catch(isolate);
info.expected_offset = 8;
info.expected_timeout = 0.125;
info.expected_value = 0;
info.expected_event = v8::Isolate::AtomicsWaitEvent::kTimedOut;
info.action = AtomicsWaitCallbackAction::KeepWaiting;
info.ncalls = 0;
CompileRun("Atomics.wait(int32arr, 1, 0, 0.125);"); // timeout
CHECK_EQ(info.ncalls, 2);
CHECK(!try_catch.HasCaught());
}
{
v8::TryCatch try_catch(isolate);
info.expected_offset = 8;
info.expected_timeout = std::numeric_limits<double>::infinity();
info.expected_value = 0;
info.expected_event = v8::Isolate::AtomicsWaitEvent::kAPIStopped;
info.action = AtomicsWaitCallbackAction::StopAndThrowInFirstCall;
info.ncalls = 0;
CompileRun("Atomics.wait(int32arr, 1, 0);");
CHECK_EQ(info.ncalls, 1); // Only one extra call
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsInt32());
CHECK_EQ(try_catch.Exception().As<v8::Int32>()->Value(), 42);
}
{
v8::TryCatch try_catch(isolate);
info.expected_offset = 8;
info.expected_timeout = std::numeric_limits<double>::infinity();
info.expected_value = 0;
info.expected_event = v8::Isolate::AtomicsWaitEvent::kAPIStopped;
info.action = AtomicsWaitCallbackAction::StopAndThrowInSecondCall;
info.ncalls = 0;
CompileRun("Atomics.wait(int32arr, 1, 0);");
CHECK_EQ(info.ncalls, 2);
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsInt32());
CHECK_EQ(try_catch.Exception().As<v8::Int32>()->Value(), 42);
}
{
// Same test as before, but with a different `expected_value`.
v8::TryCatch try_catch(isolate);
info.expected_offset = 8;
info.expected_timeout = std::numeric_limits<double>::infinity();
info.expected_value = 200;
info.expected_event = v8::Isolate::AtomicsWaitEvent::kAPIStopped;
info.action = AtomicsWaitCallbackAction::StopAndThrowInSecondCall;
info.ncalls = 0;
CompileRun(
"int32arr[1] = 200;"
"Atomics.wait(int32arr, 1, 200);");
CHECK_EQ(info.ncalls, 2);
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsInt32());
CHECK_EQ(try_catch.Exception().As<v8::Int32>()->Value(), 42);
}
{
// Wake the `Atomics.wait()` call from a thread.
v8::TryCatch try_catch(isolate);
info.expected_offset = 4;
info.expected_timeout = std::numeric_limits<double>::infinity();
info.expected_value = 0;
info.expected_event = v8::Isolate::AtomicsWaitEvent::kAPIStopped;
info.action = AtomicsWaitCallbackAction::StopFromThreadAndThrow;
info.ncalls = 0;
CompileRun("Atomics.wait(int32arr, 0, 0);");
CHECK_EQ(info.ncalls, 2);
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsInt32());
CHECK_EQ(try_catch.Exception().As<v8::Int32>()->Value(), 42);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment