Skip to content

Commit

Permalink
Capture stacks in traces
Browse files Browse the repository at this point in the history
Summary:
Error stacks are non-deterministic across runs, as optimizations can
change the code structure (e.g. via inlining). Intercept calls to
`Error.stack` and redirect them through a `HostFunction`, capturing the
stack string in the trace and returning it in place of whatever stack
exists during replay.

Reviewed By: neildhar

Differential Revision: D60306418

fbshipit-source-id: a0aac7c18f8e64ca451a343767c5bb41870e203a
  • Loading branch information
Matt Blagden authored and facebook-github-bot committed Aug 20, 2024
1 parent 2dde8b0 commit 73cb666
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 2 deletions.
43 changes: 42 additions & 1 deletion API/hermes/TracingRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
74 changes: 73 additions & 1 deletion unittests/API/SynthTraceTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <hermes/CompileJS.h>
#include <hermes/hermes.h>

#include <memory>
Expand Down Expand Up @@ -1273,15 +1274,31 @@ TEST_F(SynthTraceEnvironmentTest, NonDeterministicFunctionNames) {
/// @{
struct SynthTraceReplayTest : public SynthTraceRuntimeTest {
std::unique_ptr<jsi::Runtime> replayRt;
std::vector<std::string> 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<facebook::jsi::StringBuffer>(
new facebook::jsi::StringBuffer(bytecode)),
"");
}

void replay() {
traceRt.reset();

std::vector<std::unique_ptr<llvh::MemoryBuffer>> 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);
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 73cb666

Please sign in to comment.