| // Copyright 2026 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| #include "src/dawn/node/standalone/EventLoop.h" | |
| #include <algorithm> | |
| #include <deque> | |
| #include <iostream> | |
| #include <optional> | |
| #include <thread> | |
| #include <utility> | |
| #include "src/utils/assert.h" | |
| #include <v8.h> | |
| #include "libplatform/libplatform.h" | |
| namespace dawn::node::standalone { | |
| namespace { | |
| constexpr auto kMaxSleep = std::chrono::milliseconds(10); | |
| // Print an uncaught exception to stderr. | |
| void ReportException(v8::Isolate* isolate, v8::TryCatch* try_catch) { | |
| v8::HandleScope handle_scope(isolate); | |
| v8::Local<v8::Context> context = isolate->GetCurrentContext(); | |
| v8::Local<v8::Value> exception = try_catch->Exception(); | |
| v8::MaybeLocal<v8::Value> maybe_stack = try_catch->StackTrace(context); | |
| if (!maybe_stack.IsEmpty()) { | |
| v8::Local<v8::Value> stack_val = maybe_stack.ToLocalChecked(); | |
| if (stack_val->IsString()) { | |
| v8::String::Utf8Value stack_str(isolate, stack_val); | |
| if (stack_str.length() > 0 && *stack_str != nullptr) { | |
| std::cerr << *stack_str << std::endl; | |
| return; | |
| } | |
| } | |
| } | |
| v8::Local<v8::Message> message = try_catch->Message(); | |
| if (!message.IsEmpty()) { | |
| v8::String::Utf8Value filename(isolate, message->GetScriptOrigin().ResourceName()); | |
| int line_num = message->GetLineNumber(context).FromMaybe(0); | |
| v8::String::Utf8Value exception_str(isolate, exception); | |
| std::cerr << (*filename ? *filename : "<unknown>") << ":" << line_num << ": " | |
| << (*exception_str ? *exception_str : "") << std::endl; | |
| } else { | |
| // V8 omits the message only for a terminated execution, whose value prints as "null". | |
| v8::String::Utf8Value exception_str(isolate, exception); | |
| std::cerr << "Uncaught exception: " << (*exception_str ? *exception_str : "") << std::endl; | |
| } | |
| } | |
| } // namespace | |
| EventLoop::EventLoop(v8::Isolate* isolate, v8::Platform* platform) | |
| : isolate_(isolate), platform_(platform) {} | |
| EventLoop::~EventLoop() = default; | |
| void EventLoop::AssertOnLoopThread() const { | |
| DAWN_ASSERT(std::this_thread::get_id() == thread_id_); | |
| } | |
| void EventLoop::PostTask(Task task) { | |
| AssertOnLoopThread(); | |
| immediates_.push_back(std::move(task)); | |
| } | |
| EventLoop::TimerId EventLoop::PostDelayedTask(Task task, Duration delay) { | |
| AssertOnLoopThread(); | |
| const TimePoint due = Now() + std::max(delay, Duration::zero()); | |
| const TimerId id = next_timer_id_++; | |
| timers_.emplace(TimerKey{due, id}, std::move(task)); | |
| return id; | |
| } | |
| void EventLoop::CancelDelayedTask(TimerId id) { | |
| AssertOnLoopThread(); | |
| const auto it = | |
| std::ranges::find_if(timers_, [id](const auto& entry) { return entry.first.id == id; }); | |
| if (it != timers_.end()) { | |
| timers_.erase(it); | |
| } | |
| } | |
| EventLoop::IterationResult EventLoop::RunOneIteration() { | |
| AssertOnLoopThread(); | |
| if (stopped_) { | |
| return IterationResult::kStopped; | |
| } | |
| bool ran_task = PumpEngineTasks(); | |
| // Timers phase. The phase runs the timers that were already posted and already due when it | |
| // began, and nothing else, so that a timer whose callback arms another timer cannot hold the | |
| // loop here. A timer armed from inside the phase never sorts before the cutoff: its id is at | |
| // least the cutoff id, and its due time is at least the cutoff time because the clock only | |
| // advances. So the first entry that is not below the cutoff ends the phase. | |
| const TimerKey cutoff = {Now(), next_timer_id_}; | |
| while (!stopped_) { | |
| const auto it = timers_.begin(); | |
| if (it == timers_.end() || it->first >= cutoff) { | |
| break; | |
| } | |
| // Take the task out and drop its entry before running it, since running it may arm or | |
| // cancel timers and invalidate `it`. | |
| Task task = std::move(it->second); | |
| timers_.erase(it); | |
| RunTask(std::move(task)); | |
| ran_task = true; | |
| } | |
| if (stopped_) { | |
| return IterationResult::kStopped; | |
| } | |
| // Check phase. Taking the whole queue up front is what makes an immediate posted from inside | |
| // this phase wait for the next iteration. | |
| // | |
| // That deferral is load bearing. Dawn's AsyncRunner polls for completed work by posting a | |
| // fresh immediate from inside its own immediate callback, so a loop that kept draining the | |
| // queue until it emptied would never leave the check phase and would starve every timer. | |
| std::deque<Task> immediates; | |
| immediates.swap(immediates_); | |
| while (!immediates.empty() && !stopped_) { | |
| Task immediate = std::move(immediates.front()); | |
| immediates.pop_front(); | |
| RunTask(std::move(immediate)); | |
| ran_task = true; | |
| } | |
| if (stopped_) { | |
| return IterationResult::kStopped; | |
| } | |
| return ran_task ? IterationResult::kRanTasks : IterationResult::kIdle; | |
| } | |
| void EventLoop::Run() { | |
| AssertOnLoopThread(); | |
| while (true) { | |
| const IterationResult result = RunOneIteration(); | |
| if (result == IterationResult::kStopped) { | |
| return; | |
| } | |
| if (result == IterationResult::kRanTasks) { | |
| continue; | |
| } | |
| // Idle. No immediate can be waiting, because only a task posts one and no task ran, so a | |
| // timer is the only thing left that could become ready. | |
| const std::optional<TimePoint> due = NextDueTime(); | |
| if (!due.has_value()) { | |
| // Nothing can ever become ready: the program has run to completion. | |
| return; | |
| } | |
| WaitUntil(*due); | |
| } | |
| } | |
| void EventLoop::Stop(int exit_code) { | |
| AssertOnLoopThread(); | |
| if (!stopped_) { | |
| stopped_ = true; | |
| exit_code_ = exit_code; | |
| } | |
| } | |
| void EventLoop::RunTask(Task task) { | |
| { | |
| v8::HandleScope task_scope(isolate_); | |
| v8::TryCatch try_catch(isolate_); | |
| task(); | |
| if (try_catch.HasTerminated()) { | |
| isolate_->CancelTerminateExecution(); | |
| return; | |
| } | |
| if (try_catch.HasCaught()) { | |
| ReportException(isolate_, &try_catch); | |
| Stop(1); | |
| return; | |
| } | |
| } | |
| // Outside the handle scope, since a microtask is a separate turn and opens its own. | |
| isolate_->PerformMicrotaskCheckpoint(); | |
| if (stopped_) { | |
| isolate_->CancelTerminateExecution(); | |
| } | |
| } | |
| void EventLoop::WaitUntil(TimePoint due) { | |
| const TimePoint now = Now(); | |
| if (due > now) { | |
| std::this_thread::sleep_for(std::min<Duration>(due - now, kMaxSleep)); | |
| } | |
| } | |
| bool EventLoop::PumpEngineTasks() { | |
| return v8::platform::PumpMessageLoop(platform_, isolate_); | |
| } | |
| EventLoop::TimePoint EventLoop::Now() const { | |
| return Clock::now(); | |
| } | |
| } // namespace dawn::node::standalone |