unittests/API/SynthTraceSerializationTest.cpp (435 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <hermes/SynthTrace.h>
#include <hermes/Parser/JSONParser.h>
#include <hermes/Support/Algorithms.h>
#include <hermes/TraceInterpreter.h>
#include <hermes/TracingRuntime.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <hermes/hermes.h>
#include <limits>
#include <memory>
using namespace facebook::hermes::tracing;
using namespace facebook::hermes;
using namespace ::hermes::parser;
namespace jsi = facebook::jsi;
namespace {
class SynthTraceSerializationTest : public ::testing::Test {
protected:
SynthTrace::TimeSinceStart dummyTime{SynthTrace::TimeSinceStart::zero()};
std::string to_string(const SynthTrace::Record &rec) {
std::string result;
llvh::raw_string_ostream resultStream{result};
::hermes::JSONEmitter json{resultStream};
rec.toJSON(json);
resultStream.flush();
return result;
}
private:
std::string traceFilename_;
};
TEST_F(SynthTraceSerializationTest, EncodeNumber) {
EXPECT_EQ(
"number:0x3ff0000000000000",
SynthTrace::encode(SynthTrace::encodeNumber(1)));
}
TEST_F(SynthTraceSerializationTest, EncodeNaN) {
EXPECT_EQ(
"number:0x7ff8000000000000",
SynthTrace::encode(
SynthTrace::encodeNumber(std::numeric_limits<double>::quiet_NaN())));
}
TEST_F(SynthTraceSerializationTest, EncodeInfinity) {
EXPECT_EQ(
"number:0x7ff0000000000000",
SynthTrace::encode(
SynthTrace::encodeNumber(std::numeric_limits<double>::infinity())));
}
TEST_F(SynthTraceSerializationTest, EncodeNegativeInfinity) {
EXPECT_EQ(
"number:0xfff0000000000000",
SynthTrace::encode(
SynthTrace::encodeNumber(-std::numeric_limits<double>::infinity())));
}
TEST_F(SynthTraceSerializationTest, EncodeReallyBigInteger) {
EXPECT_EQ(
"number:0x423d2729eec71f97",
SynthTrace::encode(SynthTrace::encodeNumber(125211111111.1234)));
}
TEST_F(SynthTraceSerializationTest, EncodeString) {
const SynthTrace::ObjectID stringId = 1111;
EXPECT_EQ(
std::string("string:") + std::to_string(stringId),
SynthTrace::encode(SynthTrace::encodeString(stringId)));
}
TEST_F(SynthTraceSerializationTest, CallNoArgs) {
EXPECT_EQ(
R"({"type":"CallFromNativeRecord","time":0,"functionID":1,"thisArg":"undefined:","args":[]})",
to_string(SynthTrace::CallFromNativeRecord(
dummyTime, 1, SynthTrace::encodeUndefined(), {})));
}
TEST_F(SynthTraceSerializationTest, Call) {
EXPECT_EQ(
R"({"type":"CallFromNativeRecord","time":0,"functionID":1,"thisArg":"object:1","args":["undefined:","bool:true"]})",
to_string(SynthTrace::CallFromNativeRecord(
dummyTime,
1,
SynthTrace::encodeObject(1),
{SynthTrace::encodeUndefined(), SynthTrace::encodeBool(true)})));
}
TEST_F(SynthTraceSerializationTest, Construct) {
EXPECT_EQ(
R"({"type":"ConstructFromNativeRecord","time":0,"functionID":1,"thisArg":"undefined:","args":["null:"]})",
to_string(SynthTrace::ConstructFromNativeRecord(
dummyTime,
1,
SynthTrace::encodeUndefined(),
{SynthTrace::encodeNull()})));
}
TEST_F(SynthTraceSerializationTest, Return) {
EXPECT_EQ(
R"({"type":"ReturnFromNativeRecord","time":0,"retval":"bool:true"})",
to_string(SynthTrace::ReturnFromNativeRecord(
dummyTime, SynthTrace::encodeBool(true))));
EXPECT_EQ(
R"({"type":"ReturnToNativeRecord","time":0,"retval":"bool:false"})",
to_string(SynthTrace::ReturnToNativeRecord(
dummyTime, SynthTrace::encodeBool(false))));
}
TEST_F(SynthTraceSerializationTest, ReturnEncodeUTF8String) {
EXPECT_EQ(
R"({"type":"ReturnFromNativeRecord","time":0,"retval":"string:1111"})",
to_string(SynthTrace::ReturnFromNativeRecord{
dummyTime, SynthTrace::encodeString(1111)}));
}
TEST_F(SynthTraceSerializationTest, GetProperty) {
const std::string ex =
std::string(
R"({"type":"GetPropertyRecord","time":0,"objID":1,"propID":1111,)") +
#ifdef HERMESVM_API_TRACE_DEBUG
R"("propName":"x",)" +
#endif
R"("value":"undefined:"})";
auto testRec = SynthTrace::GetPropertyRecord(
dummyTime,
1,
1111,
#ifdef HERMESVM_API_TRACE_DEBUG
"x",
#endif
SynthTrace::encodeUndefined());
EXPECT_EQ(ex, to_string(testRec));
}
TEST_F(SynthTraceSerializationTest, SetProperty) {
const std::string ex =
std::string(
R"({"type":"SetPropertyRecord","time":0,"objID":1,"propID":1111,)") +
#ifdef HERMESVM_API_TRACE_DEBUG
R"("propName":"x",)" +
#endif
R"("value":"string:1112"})";
auto testRec = SynthTrace::SetPropertyRecord(
dummyTime,
1,
1111,
#ifdef HERMESVM_API_TRACE_DEBUG
"x",
#endif
SynthTrace::encodeString(1112));
EXPECT_EQ(ex, to_string(testRec));
}
TEST_F(SynthTraceSerializationTest, HasProperty) {
const std::string ex =
std::string(
R"({"type":"HasPropertyRecord","time":0,"objID":1,"propID":1111)") +
#ifdef HERMESVM_API_TRACE_DEBUG
R"(,"propName":"a")" +
#endif
"}";
auto testRec = SynthTrace::HasPropertyRecord(
dummyTime,
1,
1111
#ifdef HERMESVM_API_TRACE_DEBUG
,
"a"
#endif
);
EXPECT_EQ(ex, to_string(testRec));
}
TEST_F(SynthTraceSerializationTest, GetPropertyNames) {
EXPECT_EQ(
R"({"type":"GetPropertyNamesRecord","time":0,"objID":1,"propNamesID":2})",
to_string(SynthTrace::GetPropertyNamesRecord(dummyTime, 1, 2)));
}
TEST_F(SynthTraceSerializationTest, CreateArray) {
EXPECT_EQ(
R"({"type":"CreateArrayRecord","time":0,"objID":1,"length":10})",
to_string(SynthTrace::CreateArrayRecord(dummyTime, 1, 10)));
}
TEST_F(SynthTraceSerializationTest, ArrayWrite) {
EXPECT_EQ(
R"({"type":"ArrayWriteRecord","time":0,"objID":1,"index":0,"value":"string:1111"})",
to_string(SynthTrace::ArrayWriteRecord(
dummyTime, 1, 0, SynthTrace::encodeString(1111))));
}
TEST_F(SynthTraceSerializationTest, MarkerRecord) {
EXPECT_EQ(
R"({"type":"MarkerRecord","time":0,"tag":"foo"})",
to_string(SynthTrace::MarkerRecord(dummyTime, "foo")));
}
TEST_F(SynthTraceSerializationTest, GetPropertyNative) {
EXPECT_EQ(
R"({"type":"GetPropertyNativeRecord","time":0,"hostObjectID":1,"propNameID":100,"propName":"foo"})",
to_string(SynthTrace::GetPropertyNativeRecord(dummyTime, 1, 100, "foo")));
EXPECT_EQ(
R"({"type":"GetPropertyNativeReturnRecord","time":0,"retval":"null:"})",
to_string(SynthTrace::GetPropertyNativeReturnRecord(
dummyTime, SynthTrace::encodeNull())));
}
TEST_F(SynthTraceSerializationTest, SetPropertyNative) {
EXPECT_EQ(
R"({"type":"SetPropertyNativeRecord","time":0,"hostObjectID":1,"propNameID":100,"propName":"foo","value":"string:1111"})",
to_string(SynthTrace::SetPropertyNativeRecord(
dummyTime, 1, 100, "foo", SynthTrace::encodeString(1111))));
}
TEST_F(SynthTraceSerializationTest, SetPropertyNativeReturn) {
EXPECT_EQ(
R"({"type":"SetPropertyNativeReturnRecord","time":0})",
to_string(SynthTrace::SetPropertyNativeReturnRecord(dummyTime)));
}
TEST_F(SynthTraceSerializationTest, TimeIsPrinted) {
hermes::SHA1 hash{{0x64, 0x40, 0xb5, 0x37, 0xaf, 0x26, 0x79,
0x5e, 0x5f, 0x45, 0x2b, 0xcd, 0x32, 0x0f,
0xac, 0xcb, 0x02, 0x05, 0x5a, 0x4f}};
// JSON emitters escape forward slashes.
EXPECT_EQ(
R"({"type":"BeginExecJSRecord","time":100,"sourceURL":"file:\/\/\/file.js","sourceHash":"6440b537af26795e5f452bcd320faccb02055a4f","sourceIsBytecode":false})",
to_string(SynthTrace::BeginExecJSRecord(
std::chrono::milliseconds(100), "file:///file.js", hash, false)));
}
TEST_F(SynthTraceSerializationTest, EndExecHasRetval) {
EXPECT_EQ(
R"({"type":"EndExecJSRecord","time":0,"tag":"end_global_code","retval":"null:"})",
to_string(
SynthTrace::EndExecJSRecord(dummyTime, SynthTrace::encodeNull())));
}
TEST_F(SynthTraceSerializationTest, TraceHeader) {
std::string result;
auto resultStream = std::make_unique<llvh::raw_string_ostream>(result);
const ::hermes::vm::RuntimeConfig conf;
std::unique_ptr<TracingHermesRuntime> rt(makeTracingHermesRuntime(
makeHermesRuntime(conf), conf, std::move(resultStream)));
SynthTrace::ObjectID globalObjID = rt->getUniqueID(rt->global());
rt->flushAndDisableTrace();
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hermes::SourceErrorManager sm;
JSONParser parser{jsonFactory, result, sm};
auto optTrace = parser.parse();
ASSERT_TRUE(optTrace) << "Trace file is not valid JSON:\n" << result << "\n";
JSONObject *root = llvh::cast<JSONObject>(optTrace.getValue());
EXPECT_EQ(
SynthTrace::synthVersion(),
llvh::cast<JSONNumber>(root->at("version"))->getValue());
EXPECT_EQ(
globalObjID, llvh::cast<JSONNumber>(root->at("globalObjID"))->getValue());
JSONObject *rtConfig = llvh::cast<JSONObject>(root->at("runtimeConfig"));
JSONObject *gcConfig = llvh::cast<JSONObject>(rtConfig->at("gcConfig"));
EXPECT_EQ(
conf.getGCConfig().getMinHeapSize(),
llvh::cast<JSONNumber>(gcConfig->at("minHeapSize"))->getValue());
EXPECT_EQ(
conf.getGCConfig().getInitHeapSize(),
llvh::cast<JSONNumber>(gcConfig->at("initHeapSize"))->getValue());
EXPECT_EQ(
conf.getGCConfig().getMaxHeapSize(),
llvh::cast<JSONNumber>(gcConfig->at("maxHeapSize"))->getValue());
EXPECT_EQ(
conf.getGCConfig().getOccupancyTarget(),
llvh::cast<JSONNumber>(gcConfig->at("occupancyTarget"))->getValue());
EXPECT_EQ(
conf.getGCConfig().getEffectiveOOMThreshold(),
llvh::cast<JSONNumber>(gcConfig->at("effectiveOOMThreshold"))
->getValue());
EXPECT_EQ(
conf.getGCConfig().getShouldReleaseUnused(),
SynthTrace::releaseUnusedFromName(
llvh::cast<JSONString>(gcConfig->at("shouldReleaseUnused"))
->c_str()));
EXPECT_EQ(
conf.getGCConfig().getName(),
llvh::cast<JSONString>(gcConfig->at("name"))->str());
EXPECT_EQ(
conf.getGCConfig().getAllocInYoung(),
llvh::cast<JSONBoolean>(gcConfig->at("allocInYoung"))->getValue());
EXPECT_EQ(
conf.getMaxNumRegisters(),
llvh::cast<JSONNumber>(rtConfig->at("maxNumRegisters"))->getValue());
EXPECT_EQ(
conf.getES6Promise(),
llvh::cast<JSONBoolean>(rtConfig->at("ES6Promise"))->getValue());
EXPECT_EQ(
conf.getES6Proxy(),
llvh::cast<JSONBoolean>(rtConfig->at("ES6Proxy"))->getValue());
EXPECT_EQ(
conf.getIntl(),
llvh::cast<JSONBoolean>(rtConfig->at("Intl"))->getValue());
EXPECT_EQ(
conf.getEnableSampledStats(),
llvh::cast<JSONBoolean>(rtConfig->at("enableSampledStats"))->getValue());
EXPECT_EQ(
conf.getVMExperimentFlags(),
llvh::cast<JSONNumber>(rtConfig->at("vmExperimentFlags"))->getValue());
}
TEST_F(SynthTraceSerializationTest, FullTrace) {
std::string result;
auto resultStream = std::make_unique<llvh::raw_string_ostream>(result);
const ::hermes::vm::RuntimeConfig conf;
std::unique_ptr<TracingHermesRuntime> rt(makeTracingHermesRuntime(
makeHermesRuntime(conf),
conf,
std::move(resultStream),
/* forReplay */ true));
SynthTrace::ObjectID objID;
{
auto obj = jsi::Object(*rt);
objID = rt->getUniqueID(obj);
// Property name doesn't matter, just want to record that some property was
// requested.
auto value = obj.getProperty(*rt, "a");
ASSERT_TRUE(value.isUndefined());
}
rt->flushAndDisableTrace();
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hermes::SourceErrorManager sm;
JSONParser parser{jsonFactory, result, sm};
auto optTrace = parser.parse();
ASSERT_TRUE(optTrace) << "Trace file is not valid JSON:\n" << result << "\n";
// Too verbose to check every key, so let llvh::cast do the checks.
JSONObject *root = llvh::cast<JSONObject>(optTrace.getValue());
JSONObject *environment = llvh::cast<JSONObject>(root->at("env"));
EXPECT_TRUE(llvh::isa<JSONNumber>(environment->at("mathRandomSeed")));
EXPECT_EQ(
0, llvh::cast<JSONArray>(environment->at("callsToDateNow"))->size());
EXPECT_EQ(
0, llvh::cast<JSONArray>(environment->at("callsToNewDate"))->size());
EXPECT_EQ(
0,
llvh::cast<JSONArray>(environment->at("callsToDateAsFunction"))->size());
JSONArray *records = llvh::cast<JSONArray>(root->at("trace"));
const JSONObject *record = llvh::cast<JSONObject>(records->at(0));
EXPECT_EQ(
"CreateObjectRecord", llvh::cast<JSONString>(record->at("type"))->str());
EXPECT_TRUE(llvh::isa<JSONNumber>(record->at("time")));
EXPECT_EQ(objID, llvh::cast<JSONNumber>(record->at("objID"))->getValue());
// The obj.getProperty(*rt, "a") creates a string primitive for "a".
record = llvh::cast<JSONObject>(records->at(1));
EXPECT_EQ(
"CreateStringRecord", llvh::cast<JSONString>(record->at("type"))->str());
EXPECT_TRUE(llvh::isa<JSONNumber>(record->at("time")));
EXPECT_TRUE(llvh::isa<JSONNumber>(record->at("objID")));
auto stringID = llvh::cast<JSONNumber>(record->at("objID"))->getValue();
record = llvh::cast<JSONObject>(records->at(2));
EXPECT_EQ(
"GetPropertyRecord", llvh::cast<JSONString>(record->at("type"))->str());
EXPECT_TRUE(llvh::isa<JSONNumber>(record->at("time")));
EXPECT_EQ(objID, llvh::cast<JSONNumber>(record->at("objID"))->getValue());
EXPECT_EQ(stringID, llvh::cast<JSONNumber>(record->at("propID"))->getValue());
EXPECT_EQ("undefined:", llvh::cast<JSONString>(record->at("value"))->str());
}
TEST_F(SynthTraceSerializationTest, FullTraceWithDateAndMath) {
const ::hermes::vm::RuntimeConfig conf =
::hermes::vm::RuntimeConfig::Builder().withTraceEnabled(true).build();
std::string result;
auto resultStream = std::make_unique<llvh::raw_string_ostream>(result);
std::unique_ptr<TracingHermesRuntime> rt(makeTracingHermesRuntime(
makeHermesRuntime(conf), conf, std::move(resultStream)));
uint64_t dateNow = 0;
uint64_t newDate = 0;
std::string dateAsFunc;
{
jsi::Object math = rt->global().getPropertyAsObject(*rt, "Math");
jsi::Object date = rt->global().getPropertyAsObject(*rt, "Date");
// Don't need the result, just making sure the seed gets set.
math.getPropertyAsFunction(*rt, "random").call(*rt).asNumber();
dateNow = date.getPropertyAsFunction(*rt, "now").call(*rt).asNumber();
jsi::Function dateFunc = date.asFunction(*rt);
auto createdDateObj = dateFunc.callAsConstructor(*rt).asObject(*rt);
newDate = createdDateObj.getPropertyAsFunction(*rt, "getTime")
.callWithThis(*rt, createdDateObj)
.asNumber();
dateAsFunc = dateFunc.call(*rt).asString(*rt).utf8(*rt);
}
rt->flushAndDisableTrace();
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hermes::SourceErrorManager sm;
JSONParser parser{jsonFactory, result, sm};
auto optTrace = parser.parse();
ASSERT_TRUE(optTrace) << "Trace file is not valid JSON:\n" << result << "\n";
// Too verbose to check every key, so let llvh::cast do the checks.
JSONObject *root = llvh::cast<JSONObject>(optTrace.getValue());
JSONObject *environment = llvh::cast<JSONObject>(root->at("env"));
EXPECT_TRUE(llvh::isa<JSONNumber>(environment->at("mathRandomSeed")));
JSONArray *callsToDateNow =
llvh::cast<JSONArray>(environment->at("callsToDateNow"));
JSONArray *callsToNewDate =
llvh::cast<JSONArray>(environment->at("callsToNewDate"));
JSONArray *callsToDateAsFunction =
llvh::cast<JSONArray>(environment->at("callsToDateAsFunction"));
EXPECT_EQ(1, callsToDateNow->size());
EXPECT_EQ(dateNow, llvh::cast<JSONNumber>(callsToDateNow->at(0))->getValue());
EXPECT_EQ(1, callsToNewDate->size());
EXPECT_EQ(newDate, llvh::cast<JSONNumber>(callsToNewDate->at(0))->getValue());
EXPECT_EQ(1, callsToDateAsFunction->size());
EXPECT_EQ(
dateAsFunc, llvh::cast<JSONString>(callsToDateAsFunction->at(0))->str());
// Ignore the elements inside the trace, those are tested elsewhere.
}
// Handle sanitization does extra "FillerCell" allocations that break this test.
#ifndef HERMESVM_SANITIZE_HANDLES
TEST_F(SynthTraceSerializationTest, TracePreservesStringAllocs) {
const ::hermes::vm::RuntimeConfig conf =
::hermes::vm::RuntimeConfig::Builder().withTraceEnabled(true).build();
std::string traceResult;
auto resultStream = std::make_unique<llvh::raw_string_ostream>(traceResult);
std::unique_ptr<TracingHermesRuntime> rt(makeTracingHermesRuntime(
makeHermesRuntime(conf), conf, std::move(resultStream)));
std::string source = R"(
var s1 = "a";
var s2 = "b";
var s3 = s1 + s2;
function f(s) {
return s == s3;
}
)";
rt->evaluateJavaScript(std::make_unique<jsi::StringBuffer>(source), "source");
// Do get property to get "ab" in the string table.
auto s = rt->global().getProperty(*rt, "s3");
auto f = rt->global().getProperty(*rt, "f").asObject(*rt).getFunction(*rt);
auto res = f.call(*rt, {std::move(s)});
EXPECT_TRUE(res.getBool());
const auto &heapInfo = rt->instrumentation().getHeapInfo(false);
// This test is Hermes-specific -- return early if the heapInfo does
// not have the hermes keys we expect.
const std::string kNumAllocObjects{"hermes_numAllocatedObjects"};
auto iter = heapInfo.find(kNumAllocObjects);
if (iter == heapInfo.end()) {
return;
}
auto allocObjsOrig = iter->second;
rt->flushAndDisableTrace();
// Now replay the trace, see if we get extra allocations.
std::vector<std::unique_ptr<llvh::MemoryBuffer>> sources;
sources.emplace_back(llvh::MemoryBuffer::getMemBuffer(source));
tracing::TraceInterpreter::ExecuteOptions options;
std::string replayTraceStr;
auto replayTraceStream =
std::make_unique<llvh::raw_string_ostream>(replayTraceStr);
std::unique_ptr<TracingHermesRuntime> rt2(makeTracingHermesRuntime(
makeHermesRuntime(conf),
conf,
std::move(replayTraceStream),
/* forReplay */ true));
EXPECT_NO_THROW({
tracing::TraceInterpreter::execFromMemoryBuffer(
llvh::MemoryBuffer::getMemBuffer(traceResult),
std::move(sources),
*rt2,
options);
});
rt2->flushAndDisableTrace();
auto allocObjsFinal =
rt2->instrumentation().getHeapInfo(false).at(kNumAllocObjects);
EXPECT_EQ(allocObjsOrig, allocObjsFinal);
}
#endif
} // namespace