[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: Yang Guo <yangguo@chromium.org> Reviewed-by: Ben Smith <binji@chromium.org> Cr-Commit-Position: refs/heads/master@{#53517}
This commit is contained in:
parent
10cfe818bd
commit
5eaa03b2d3
81
include/v8.h
81
include/v8.h
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user