diff --git a/API/hermes/TracingRuntime.cpp b/API/hermes/TracingRuntime.cpp index 4c5614b9fa6..fa14f500805 100644 --- a/API/hermes/TracingRuntime.cpp +++ b/API/hermes/TracingRuntime.cpp @@ -58,7 +58,29 @@ void TracingRuntime::replaceNondeterministicFuncs() { const jsi::Value *args, size_t count) { auto fun = args[0].getObject(*runtime_).getFunction(*runtime_); - return fun.call(*runtime_); + jsi::Value result; + if (count > 1 && args[1].isObject()) { + result = fun.callWithThis(*runtime_, args[1].asObject(*runtime_)); + } else { + result = fun.call(*runtime_); + } + + if (result.isString()) { + // Recreate the result string via the TracingRuntime, so the string + // appears in the resulting trace. + const std::string resultStr = + result.getString(*runtime_).utf8(*runtime_); + jsi::String tracedResult = jsi::String::createFromUtf8(rt, resultStr); + return jsi::Value(std::move(tracedResult)); + } else { + // Other values must be primitives that will be included directly in + // the trace. + assert( + (result.isUndefined() || result.isNull() || result.isNumber() || + result.isBool()) && + "Result is a pointer"); + return result; + } }); // Below two host functions are for WeakRef hook. @@ -179,6 +201,25 @@ void TracingRuntime::replaceNondeterministicFuncs() { } Date.now = nativeDateNow; globalThis.Date = Date; + + const defineProperty = Object.defineProperty; + const realStackPropertyGetter = Object.getOwnPropertyDescriptor(Error.prototype, 'stack').get; + defineProperty(Error.prototype, 'stack', { + get: function() { + var stack = callUntraced(realStackPropertyGetter, this); + // The real getter stores the stack on the error object, meaning that + // the real getter (and this wrapper) will not be invoked again if the + // stack is accessed again during recording. Mimic that behavior here, + // so the getter is also not invoked again during replay. + defineProperty(this, 'stack', { + value: stack, + writable: true, + configurable: true + }); + return stack; + }, + configurable: true + }); }); )"; global() diff --git a/unittests/API/SynthTraceTest.cpp b/unittests/API/SynthTraceTest.cpp index 371627797db..8027085fd8b 100644 --- a/unittests/API/SynthTraceTest.cpp +++ b/unittests/API/SynthTraceTest.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -1273,15 +1274,31 @@ TEST_F(SynthTraceEnvironmentTest, NonDeterministicFunctionNames) { /// @{ struct SynthTraceReplayTest : public SynthTraceRuntimeTest { std::unique_ptr replayRt; + std::vector sources; + + jsi::Value evalCompiled(jsi::Runtime &rt, std::string source) { + std::string bytecode; + EXPECT_TRUE(hermes::compileJS(source, bytecode)); + this->sources.push_back(std::move(source)); + return rt.evaluateJavaScript( + std::unique_ptr( + new facebook::jsi::StringBuffer(bytecode)), + ""); + } void replay() { traceRt.reset(); + std::vector> sources; + for (const std::string &source : this->sources) { + sources.emplace_back(llvh::MemoryBuffer::getMemBuffer(source)); + } + tracing::TraceInterpreter::ExecuteOptions options; options.useTraceConfig = true; auto [_, rt] = tracing::TraceInterpreter::execFromMemoryBuffer( llvh::MemoryBuffer::getMemBuffer(traceResult), // traceBuf - {}, // codeBufs + std::move(sources), // codeBufs options, // ExecuteOptions makeHermesRuntime); replayRt = std::move(rt); @@ -2224,6 +2241,61 @@ TEST_F(NonDeterminismReplayTest, HermesInternalGetInstrumentedStatsTest) { } } +// Test that traces replay deterministically, even if the error stack changes. +TEST_F(NonDeterminismReplayTest, ErrorStackTest) { + constexpr auto source = R"( +var stack; +var refetchedStack; + +function main() { + function inlineable2() { + var error = new Error('test'); + stack = error.stack; + refetchedStack = error.stack; + } + + function inlineable1() { + inlineable2(); + return 123; + } + + return inlineable1(); +} +main(); + )"; + + // Run with optimizations, producing a stack like: + // Error: test + // at main (:6:18) + // at global (:16:5) + evalCompiled(*traceRt, source); + auto stack = eval(*traceRt, "stack").asString(*traceRt).utf8(*traceRt); + auto refetchedStack = + eval(*traceRt, "refetchedStack").asString(*traceRt).utf8(*traceRt); + // Ensure the stack is correctly fetched after any caching triggered by the + // first fetch. + EXPECT_EQ(stack, refetchedStack); + + // Run without optimizations, producing a stack like: + // Error: test + // at inlineable2 (:6:18) + // at inlineable1 (:10:16) + // at main (:14:21) + // at global (:16:5) + // which should be ignored and replaced by the stack captured in the trace. + replay(); + auto replayedStack = + eval(*replayRt, "stack").asString(*replayRt).utf8(*replayRt); + auto replayedRefetchedStack = + eval(*replayRt, "refetchedStack").asString(*replayRt).utf8(*replayRt); + // Ensure the stack is correctly fetched after any caching triggered by the + // first fetch. + EXPECT_EQ(replayedStack, replayedRefetchedStack); + + // Ensure the stack is replayed identically to the recording. + EXPECT_EQ(stack, replayedStack); +} + // Verify that jsi::Runtime::setExternalMemoryPressure() is properly traced and // replayed TEST(SynthTraceReplayTestNoFixture, ExternalMemoryTest) {