blob: b8aba4752097cf7a81056548b3675cdb57e297c3 [file] [edit]
// 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.
#ifndef SRC_DAWN_NODE_STANDALONE_EVENTLOOP_H_
#define SRC_DAWN_NODE_STANDALONE_EVENTLOOP_H_
#include <chrono>
#include <cstdint>
#include <deque>
#include <optional>
#include <thread>
#include <utility>
#include "absl/container/btree_map.h"
#include "absl/functional/any_invocable.h"
namespace v8 {
class Isolate;
class Platform;
} // namespace v8
namespace dawn::node::standalone {
// A Node.js-shaped event loop. Each iteration runs the timers that have come due, then the
// immediates that were already queued when the phase began.
//
// Every task runs inside a handle scope, with the microtask queue drained afterwards, so a task is
// free to call into JavaScript. An uncaught exception is printed and stops the loop with exit code
// 1, which is why the loop rather than its caller owns the exit code.
//
// Node's phase order is not an implementation detail we are free to change. `setTimeout` and
// `setImmediate` callbacks observe it, and Dawn's AsyncRunner relies on the check phase deferring
// work to the next iteration; see the comment on RunOneIteration(). The order is documented at
// https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick
//
// Every method must be called on the thread that constructed the loop, which is the thread the
// isolate runs on. Tasks only ever arrive by way of a JavaScript call - the timer polyfills, and
// AsyncRunner, which posts by calling the global setImmediate() rather than by reaching in here -
// and a JavaScript call is already confined to that thread. Debug builds check this.
class EventLoop {
public:
using Task = absl::AnyInvocable<void()>;
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
using Duration = Clock::duration;
using TimerId = uint64_t;
// What a single iteration found to do.
enum class IterationResult {
// At least one task ran. More may already be ready.
kRanTasks,
// Nothing was ready to run. The caller may wait until NextDueTime().
kIdle,
// Stop() has been called.
kStopped,
};
// `isolate` must be entered, with a context entered, whenever a task runs. `platform` is the
// one the isolate was created with; the loop pumps its foreground work each iteration. Neither
// is owned, and both must outlive the loop.
EventLoop(v8::Isolate* isolate, v8::Platform* platform);
virtual ~EventLoop();
EventLoop(const EventLoop&) = delete;
EventLoop& operator=(const EventLoop&) = delete;
// Queues `task` for the next check phase. Backs setImmediate().
void PostTask(Task task);
// Queues `task` to run once `delay` has elapsed, and returns an identifier that
// CancelDelayedTask() accepts. Backs setTimeout() and setInterval().
TimerId PostDelayedTask(Task task, Duration delay);
// Drops a delayed task that has not run yet. Unknown and already-run identifiers are ignored,
// because clearTimeout() on an expired timer is not an error.
void CancelDelayedTask(TimerId id);
// Runs one timers phase followed by one check phase.
IterationResult RunOneIteration();
// Runs iterations until Stop() is called or there is no work left to wait for.
void Run();
// Asks the loop to finish. Takes effect between tasks. Once stopped, later calls do not
// overwrite the exit code. Backs process.exit().
void Stop(int exit_code);
bool stopped() const { return stopped_; }
int exit_code() const { return exit_code_; }
// When the earliest pending timer comes due, or nullopt when no timer is pending.
std::optional<TimePoint> NextDueTime() const {
if (timers_.empty()) {
return std::nullopt;
}
return timers_.begin()->first.due;
}
protected:
// The loop's only reading of the clock. Virtual so that tests can control the passage of time
// without depending on real time; every other seam this class used to have is gone.
virtual TimePoint Now() const;
private:
// Runs a single task and then drains the microtask queue, mirroring what a browser or Node.js
// does between task callbacks. An uncaught exception is reported and stops the loop.
void RunTask(Task task);
// Blocks until `due`, or until it is worth looking for work again. Capped so that
// PumpEngineTasks() gets a turn while a distant timer is pending.
void WaitUntil(TimePoint due);
// Runs any foreground work V8 has queued for this thread, reporting whether it ran any.
bool PumpEngineTasks();
// Checks, in debug builds, that the caller is on the thread that constructed the loop.
void AssertOnLoopThread() const;
// Orders timers by the time they come due, and timers that share a deadline by the order they
// were posted, since identifiers are handed out in that order.
struct TimerKey {
TimePoint due;
TimerId id;
auto operator<=>(const TimerKey&) const = default;
};
v8::Isolate* const isolate_;
v8::Platform* const platform_;
// The thread the loop belongs to. Only read by AssertOnLoopThread().
const std::thread::id thread_id_ = std::this_thread::get_id();
// Tasks waiting for the next check phase, in the order they were posted.
std::deque<Task> immediates_;
absl::btree_map<TimerKey, Task> timers_;
TimerId next_timer_id_ = 1;
bool stopped_ = false;
int exit_code_ = 0;
};
} // namespace dawn::node::standalone
#endif // SRC_DAWN_NODE_STANDALONE_EVENTLOOP_H_