Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement queueMicrotask in Hermes #1332

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions API/hermes/SynthTrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,14 @@ bool SynthTrace::CreateObjectRecord::operator==(const Record &that) const {
return objID_ == thatCasted.objID_;
}

bool SynthTrace::QueueMicrotaskRecord::operator==(const Record &that) const {
if (!Record::operator==(that)) {
return false;
}
const auto &thatCasted = dynamic_cast<const QueueMicrotaskRecord &>(that);
return callbackID_ == thatCasted.callbackID_;
}

bool SynthTrace::DrainMicrotasksRecord::operator==(const Record &that) const {
if (!Record::operator==(that)) {
return false;
Expand Down Expand Up @@ -634,6 +642,11 @@ void SynthTrace::HasPropertyRecord::toJSONInternal(JSONEmitter &json) const {
#endif
}

void SynthTrace::QueueMicrotaskRecord::toJSONInternal(JSONEmitter &json) const {
Record::toJSONInternal(json);
json.emitKeyValue("callbackID", callbackID_);
}

void SynthTrace::DrainMicrotasksRecord::toJSONInternal(
JSONEmitter &json) const {
Record::toJSONInternal(json);
Expand Down Expand Up @@ -831,6 +844,7 @@ llvh::raw_ostream &operator<<(
CASE(CreatePropNameID);
CASE(CreateHostObject);
CASE(CreateHostFunction);
CASE(QueueMicrotask);
CASE(DrainMicrotasks);
CASE(GetProperty);
CASE(SetProperty);
Expand Down Expand Up @@ -877,6 +891,7 @@ std::istream &operator>>(std::istream &is, SynthTrace::RecordType &type) {
CASE(CreatePropNameID)
CASE(CreateHostObject)
CASE(CreateHostFunction)
CASE(QueueMicrotask)
CASE(DrainMicrotasks)
CASE(GetProperty)
CASE(SetProperty)
Expand Down
21 changes: 21 additions & 0 deletions API/hermes/SynthTrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class SynthTrace {
CreatePropNameID,
CreateHostObject,
CreateHostFunction,
QueueMicrotask,
DrainMicrotasks,
GetProperty,
SetProperty,
Expand Down Expand Up @@ -723,6 +724,26 @@ class SynthTrace {
void toJSONInternal(::hermes::JSONEmitter &json) const override;
};

struct QueueMicrotaskRecord : public Record {
static constexpr RecordType type{RecordType::QueueMicrotask};
const ObjectID callbackID_;

QueueMicrotaskRecord(TimeSinceStart time, ObjectID callbackID)
: Record(time), callbackID_(callbackID) {}

bool operator==(const Record &that) const final;

RecordType getType() const override {
return type;
}

void toJSONInternal(::hermes::JSONEmitter &json) const override;

std::vector<ObjectID> uses() const override {
return {callbackID_};
}
};

struct DrainMicrotasksRecord : public Record {
static constexpr RecordType type{RecordType::DrainMicrotasks};
int maxMicrotasksHint_;
Expand Down
7 changes: 7 additions & 0 deletions API/hermes/SynthTraceParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@ SynthTrace getTrace(JSONArray *array, SynthTrace::ObjectID globalObjID) {
trace.emplace_back<SynthTrace::CreateObjectRecord>(
timeFromStart, objID->getValue());
break;
case RecordType::QueueMicrotask: {
auto callbackID =
getNumberAs<SynthTrace::ObjectID>(obj->get("callbackID"));
trace.emplace_back<SynthTrace::QueueMicrotaskRecord>(
timeFromStart, callbackID);
break;
}
case RecordType::DrainMicrotasks: {
int maxMicrotasksHint = getNumberAs<int>(obj->get("maxMicrotasksHint"));
trace.emplace_back<SynthTrace::DrainMicrotasksRecord>(
Expand Down
8 changes: 8 additions & 0 deletions API/hermes/TraceInterpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,14 @@ Value TraceInterpreter::execFunction(
locals);
break;
}
case RecordType::QueueMicrotask: {
const auto &queueRecord =
static_cast<const SynthTrace::QueueMicrotaskRecord &>(*rec);
jsi::Function callback =
getObjForUse(queueRecord.callbackID_).asFunction(rt_);
rt_.queueMicrotask(callback);
break;
}
case RecordType::DrainMicrotasks: {
const auto &drainRecord =
static_cast<const SynthTrace::DrainMicrotasksRecord &>(*rec);
Expand Down
6 changes: 6 additions & 0 deletions API/hermes/TracingRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ jsi::Value TracingRuntime::evaluateJavaScript(
return res;
}

void TracingRuntime::queueMicrotask(const jsi::Function &callback) {
RD::queueMicrotask(callback);
trace_.emplace_back<SynthTrace::QueueMicrotaskRecord>(
getTimeSinceStart(), getUniqueID(callback));
}

bool TracingRuntime::drainMicrotasks(int maxMicrotasksHint) {
auto res = RD::drainMicrotasks(maxMicrotasksHint);
trace_.emplace_back<SynthTrace::DrainMicrotasksRecord>(
Expand Down
1 change: 1 addition & 0 deletions API/hermes/TracingRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class TracingRuntime : public jsi::RuntimeDecorator<jsi::Runtime> {
const std::shared_ptr<const jsi::Buffer> &buffer,
const std::string &sourceURL) override;

void queueMicrotask(const jsi::Function &callback) override;
bool drainMicrotasks(int maxMicrotasksHint = -1) override;

jsi::Object createObject() override;
Expand Down
12 changes: 12 additions & 0 deletions API/hermes/hermes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ class HermesRuntimeImpl final : public HermesRuntime,
jsi::Value evaluateJavaScript(
const std::shared_ptr<const jsi::Buffer> &buffer,
const std::string &sourceURL) override;
void queueMicrotask(const jsi::Function &callback) override;
bool drainMicrotasks(int maxMicrotasksHint = -1) override;
jsi::Object global() override;

Expand Down Expand Up @@ -1502,6 +1503,17 @@ jsi::Value HermesRuntimeImpl::evaluateJavaScript(
return evaluateJavaScriptWithSourceMap(buffer, nullptr, sourceURL);
}

void HermesRuntimeImpl::queueMicrotask(const jsi::Function &callback) {
if (LLVM_UNLIKELY(!runtime_.hasMicrotaskQueue())) {
throw jsi::JSINativeException(
"Could not enqueue microtask because they are disabled in this runtime");
}

vm::Handle<vm::Callable> handle =
vm::Handle<vm::Callable>::vmcast(&phv(callback));
runtime_.enqueueJob(handle.get());
}

bool HermesRuntimeImpl::drainMicrotasks(int maxMicrotasksHint) {
if (runtime_.hasMicrotaskQueue()) {
checkStatus(runtime_.drainJobs());
Expand Down
7 changes: 7 additions & 0 deletions API/jsi/jsi/decorator.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation {
const std::shared_ptr<const PreparedJavaScript>& js) override {
return plain().evaluatePreparedJavaScript(js);
}
void queueMicrotask(const jsi::Function& callback) override {
return plain().queueMicrotask(callback);
}
bool drainMicrotasks(int maxMicrotasksHint) override {
return plain().drainMicrotasks(maxMicrotasksHint);
}
Expand Down Expand Up @@ -544,6 +547,10 @@ class WithRuntimeDecorator : public RuntimeDecorator<Plain, Base> {
Around around{with_};
return RD::evaluatePreparedJavaScript(js);
}
void queueMicrotask(const Function& callback) override {
Around around{with_};
RD::queueMicrotask(callback);
}
bool drainMicrotasks(int maxMicrotasksHint) override {
Around around{with_};
return RD::drainMicrotasks(maxMicrotasksHint);
Expand Down
4 changes: 4 additions & 0 deletions API/jsi/jsi/jsi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ NativeState::~NativeState() {}

Runtime::~Runtime() {}

void Runtime::queueMicrotask(const jsi::Function& /*callback*/) {
throw JSINativeException("queueMicrotask is not implemented in this runtime");
}

Instrumentation& Runtime::instrumentation() {
class NoInstrumentation : public Instrumentation {
std::string getRecordedGCStats() override {
Expand Down
7 changes: 7 additions & 0 deletions API/jsi/jsi/jsi.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,13 @@ class JSI_EXPORT Runtime {
virtual Value evaluatePreparedJavaScript(
const std::shared_ptr<const PreparedJavaScript>& js) = 0;

/// Queues a microtask in the JavaScript VM internal Microtask (a.k.a. Job in
/// ECMA262) queue, to be executed when the host drains microtasks in
/// its event loop implementation.
///
/// \param callback a function to be executed as a microtask.
virtual void queueMicrotask(const jsi::Function& callback);

/// Drain the JavaScript VM internal Microtask (a.k.a. Job in ECMA262) queue.
///
/// \param maxMicrotasksHint a hint to tell an implementation that it should
Expand Down
19 changes: 19 additions & 0 deletions unittests/API/SynthTraceTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ struct SynthTraceTest : public ::testing::Test {
::hermes::vm::RuntimeConfig::Builder()
.withSynthTraceMode(
::hermes::vm::SynthTraceMode::TracingAndReplaying)
.withMicrotaskQueue(true)
.build();
// We pass "forReplay = true" below, to prevent the TracingHermesRuntime
// from interactions it does automatically on non-replay runs.
Expand Down Expand Up @@ -1519,6 +1520,24 @@ HermesInternal.enqueueJob(inc);
}
}

TEST_F(JobQueueReplayTest, QueueMicrotask) {
{
auto &rt = *traceRt;
auto microtask =
eval(rt, "var x = 3; function updateX() { x = 4; }; updateX")
.asObject(rt)
.asFunction(rt);
rt.queueMicrotask(microtask);
rt.drainMicrotasks();
EXPECT_EQ(eval(rt, "x").asNumber(), 4);
}
replay();
{
auto &rt = *replayRt;
EXPECT_EQ(eval(rt, "x").asNumber(), 4);
}
}

using NonDeterminismReplayTest = SynthTraceReplayTest;

TEST_F(NonDeterminismReplayTest, DateNowTest) {
Expand Down