API/hermes/SynthTraceParser.cpp (552 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/SynthTraceParser.h" #include "hermes/Parser/JSLexer.h" #include "hermes/Parser/JSONParser.h" #include "hermes/Support/SourceErrorManager.h" #include <sstream> namespace facebook { namespace hermes { namespace tracing { using namespace ::hermes::parser; namespace { ::hermes::SHA1 parseHashStrAsNumber(llvh::StringRef hashStr) { ::hermes::SHA1 sourceHash{}; // Each byte is 2 characters. if (hashStr.size() != 2 * sourceHash.size()) { throw std::runtime_error("sourceHash is not the right length"); } for (size_t i = 0; i < sourceHash.size(); ++i) { auto byte = llvh::StringRef{hashStr.data() + (i * 2), 2}; sourceHash[i] = ::hermes::parseIntWithRadix</* AllowNumericSeparator */ false>(byte, 16) .getValue(); } return sourceHash; } JSONObject *parseJSON( JSONFactory::Allocator &alloc, std::unique_ptr<llvh::MemoryBuffer> stream) { JSONFactory factory(alloc); ::hermes::SourceErrorManager sm; // Convert surrogates, since JSI deals in UTF-8. JSONParser parser(factory, std::move(stream), sm, /*convertSurrogates*/ true); auto rootObj = parser.parse(); if (!rootObj) { // The source error manager will print to stderr. throw std::invalid_argument("JSON invalid"); } return llvh::cast<JSONObject>(rootObj.getValue()); } /// Get a number from a JSON value. /// \throws invalid_argument if it's not a number. template <typename NumericType> NumericType getNumberAs(const JSONValue *val) { if (!val) { throw std::invalid_argument("value doesn't exist"); } if (val->getKind() != JSONKind::Number) { throw std::invalid_argument("value is not a number"); } return static_cast<NumericType>(llvh::cast<JSONNumber>(val)->getValue()); } /// Get a number from a JSON value. /// Use \param dflt if the JSON value doesn't exist. /// \throws invalid_argument if it's not a number. template <typename NumericType> NumericType getNumberAs(const JSONValue *val, NumericType dflt) { if (!val) { return dflt; } else { return getNumberAs<NumericType>(val); } } ::hermes::vm::GCConfig::Builder getGCConfig(JSONObject *rtConfig) { // This function should extract all fields from GCConfig that can affect // performance metrics. Configs for debugging can be ignored. ::hermes::vm::GCConfig::Builder gcconf; if (!rtConfig) { return gcconf; } auto *val = rtConfig->get("gcConfig"); if (!val) { return gcconf; } if (val->getKind() != JSONKind::Object) { throw std::invalid_argument("gcConfig should be an object"); } auto *gcConfig = llvh::cast<JSONObject>(val); if (auto *sz = gcConfig->get("minHeapSize")) { gcconf.withMinHeapSize(getNumberAs<::hermes::vm::gcheapsize_t>(sz)); } if (auto *sz = gcConfig->get("initHeapSize")) { gcconf.withInitHeapSize(getNumberAs<::hermes::vm::gcheapsize_t>(sz)); } if (auto *sz = gcConfig->get("maxHeapSize")) { gcconf.withMaxHeapSize(getNumberAs<::hermes::vm::gcheapsize_t>(sz)); } if (auto *occupancy = gcConfig->get("occupancyTarget")) { gcconf.withOccupancyTarget(getNumberAs<double>(occupancy)); } if (auto *threshold = gcConfig->get("effectiveOOMThreshold")) { gcconf.withEffectiveOOMThreshold(getNumberAs<unsigned>(threshold)); } if (auto *shouldRelease = gcConfig->get("shouldReleaseUnused")) { gcconf.withShouldReleaseUnused(SynthTrace::releaseUnusedFromName( llvh::cast<JSONString>(shouldRelease)->c_str())); } if (auto *name = gcConfig->get("name")) { gcconf.withName(llvh::cast<JSONString>(name)->str()); } if (auto *allocInYoung = gcConfig->get("allocInYoung")) { gcconf.withAllocInYoung(llvh::cast<JSONBoolean>(allocInYoung)->getValue()); } if (auto *revertAtTTI = gcConfig->get("revertToYGAtTTI")) { gcconf.withRevertToYGAtTTI( llvh::cast<JSONBoolean>(revertAtTTI)->getValue()); } return gcconf; } ::hermes::vm::RuntimeConfig::Builder getRuntimeConfig(JSONObject *rtConfig) { ::hermes::vm::RuntimeConfig::Builder conf; if (!rtConfig) { // If the config doesn't exist, return some default values. return conf; } if (auto *maxNumRegisters = rtConfig->get("maxNumRegisters")) { conf.withMaxNumRegisters(getNumberAs<unsigned>(maxNumRegisters)); } if (auto *promise = rtConfig->get("ES6Promise")) { conf.withES6Promise(llvh::cast<JSONBoolean>(promise)->getValue()); } if (auto *proxy = rtConfig->get("ES6Proxy")) { conf.withES6Proxy(llvh::cast<JSONBoolean>(proxy)->getValue()); } if (auto *intl = rtConfig->get("Intl")) { conf.withIntl(llvh::cast<JSONBoolean>(intl)->getValue()); } if (auto *enableSampledStats = rtConfig->get("enableSampledStats")) { conf.withEnableSampledStats( llvh::cast<JSONBoolean>(enableSampledStats)->getValue()); } if (auto *vmExperimentFlags = rtConfig->get("vmExperimentFlags")) { conf.withVMExperimentFlags(getNumberAs<uint32_t>(vmExperimentFlags)); } return conf; } template <template <typename, typename> class Collection> Collection<std::string, std::allocator<std::string>> getListOfStrings( JSONArray *array) { Collection<std::string, std::allocator<std::string>> strings; std::transform( array->begin(), array->end(), std::back_inserter(strings), [](const JSONValue *value) -> std::string { if (value->getKind() != JSONKind::String) { throw std::invalid_argument("Array should contain only strings"); } return std::string(llvh::cast<JSONString>(value)->c_str()); }); return strings; } template <template <typename, typename> class Collection> Collection< ::hermes::vm::MockedEnvironment::StatsTable, std::allocator<::hermes::vm::MockedEnvironment::StatsTable>> getListOfStatsTable(JSONArray *array) { Collection< ::hermes::vm::MockedEnvironment::StatsTable, std::allocator<::hermes::vm::MockedEnvironment::StatsTable>> calls; if (!array) { return calls; } std::transform( array->begin(), array->end(), std::back_inserter(calls), [](const JSONValue *value) -> ::hermes::vm::MockedEnvironment::StatsTable { if (value->getKind() != JSONKind::Object) { throw std::invalid_argument("Stats table JSON rep is not object"); } const JSONObject *obj = llvh::cast<JSONObject>(value); ::hermes::vm::MockedEnvironment::StatsTable result; for (auto name : *obj->getHiddenClass()) { auto valForName = obj->at(name->str()); if (valForName->getKind() == JSONKind::Number) { result.try_emplace( name->str(), llvh::cast<JSONNumber>(valForName)->getValue()); } else if (valForName->getKind() == JSONKind::String) { result.try_emplace( name->str(), std::string(llvh::cast<JSONString>(valForName)->c_str())); } else { throw std::invalid_argument( "Stats table kind is not num or string."); } } return result; }); return calls; } ::hermes::vm::MockedEnvironment getMockedEnvironment(JSONObject *env) { auto getListOfNumbers = [](JSONArray *array) -> std::deque<uint64_t> { std::deque<uint64_t> calls; std::transform( array->begin(), array->end(), std::back_inserter(calls), [](const JSONValue *value) { return getNumberAs<uint64_t>(value); }); return calls; }; if (!llvh::dyn_cast_or_null<JSONNumber>(env->get("mathRandomSeed"))) { throw std::invalid_argument("env.mathRandomSeed is not a number"); } std::minstd_rand::result_type mathRandomSeed = llvh::cast<JSONNumber>(env->at("mathRandomSeed"))->getValue(); auto callsToDateNow = getListOfNumbers(llvh::cast<JSONArray>(env->at("callsToDateNow"))); auto callsToNewDate = getListOfNumbers(llvh::cast<JSONArray>(env->at("callsToNewDate"))); auto callsToDateAsFunction = getListOfStrings<std::deque>( llvh::cast<JSONArray>(env->at("callsToDateAsFunction"))); auto callsToHermesInternalGetInstrumentedStats = getListOfStatsTable<std::deque>(llvh::cast_or_null<JSONArray>( env->get("callsToHermesInternalGetInstrumentedStats"))); return ::hermes::vm::MockedEnvironment{ mathRandomSeed, callsToDateNow, callsToNewDate, callsToDateAsFunction, callsToHermesInternalGetInstrumentedStats}; } SynthTrace getTrace(JSONArray *array, SynthTrace::ObjectID globalObjID) { using RecordType = SynthTrace::RecordType; SynthTrace trace(globalObjID, ::hermes::vm::RuntimeConfig()); auto getListOfTraceValues = [](JSONArray *array, SynthTrace &trace) -> std::vector<SynthTrace::TraceValue> { std::vector<SynthTrace::TraceValue> values; std::transform( array->begin(), array->end(), std::back_inserter(values), [&trace](const JSONValue *value) -> SynthTrace::TraceValue { if (value->getKind() != JSONKind::String) { throw std::invalid_argument("Array should contain only strings"); } return trace.decode(llvh::cast<JSONString>(value)->c_str()); }); return values; }; for (auto *val : *array) { auto *obj = llvh::cast<JSONObject>(val); auto timeFromStart = std::chrono::milliseconds(getNumberAs<uint64_t>(obj->get("time"), 0)); std::stringstream ss; RecordType kind; ss << llvh::cast<JSONString>(obj->get("type"))->c_str(); ss >> kind; // Common properties, they may not exist on all objects so use a // dynamic cast. auto *objID = llvh::dyn_cast_or_null<JSONNumber>(obj->get("objID")); auto *hostObjID = llvh::dyn_cast_or_null<JSONNumber>(obj->get("hostObjectID")); auto *funcID = llvh::dyn_cast_or_null<JSONNumber>(obj->get("functionID")); auto *propID = llvh::dyn_cast_or_null<JSONNumber>(obj->get("propID")); auto *propNameID = llvh::dyn_cast_or_null<JSONNumber>(obj->get("propNameID")); auto *propName = llvh::dyn_cast_or_null<JSONString>(obj->get("propName")); auto *propValue = llvh::dyn_cast_or_null<JSONString>(obj->get("value")); auto *arrayIndex = llvh::dyn_cast_or_null<JSONNumber>(obj->get("index")); auto *callArgs = llvh::dyn_cast_or_null<JSONArray>(obj->get("args")); auto *thisArg = llvh::dyn_cast_or_null<JSONString>(obj->get("thisArg")); auto *retval = llvh::dyn_cast_or_null<JSONString>(obj->get("retval")); switch (kind) { case RecordType::BeginExecJS: { std::string sourceURL; ::hermes::SHA1 hash{}; bool sourceIsBytecode{false}; if (JSONString *sourceURLJSON = llvh::dyn_cast_or_null<JSONString>(obj->get("sourceURL"))) { sourceURL = sourceURLJSON->str(); } if (JSONString *sourceHash = llvh::dyn_cast_or_null<JSONString>(obj->get("sourceHash"))) { hash = parseHashStrAsNumber(sourceHash->str()); } if (JSONBoolean *sourceIsBytecodeJson = llvh::dyn_cast_or_null<JSONBoolean>( obj->get("sourceIsBytecode"))) { sourceIsBytecode = sourceIsBytecodeJson->getValue(); } trace.emplace_back<SynthTrace::BeginExecJSRecord>( timeFromStart, std::move(sourceURL), std::move(hash), sourceIsBytecode); break; } case RecordType::EndExecJS: trace.emplace_back<SynthTrace::EndExecJSRecord>( timeFromStart, trace.decode(retval->c_str())); break; case RecordType::Marker: trace.emplace_back<SynthTrace::MarkerRecord>( timeFromStart, llvh::cast<JSONString>(obj->get("tag"))->c_str()); break; case RecordType::CreateObject: trace.emplace_back<SynthTrace::CreateObjectRecord>( timeFromStart, objID->getValue()); break; case RecordType::DrainMicrotasks: { int maxMicrotasksHint = getNumberAs<int>(obj->get("maxMicrotasksHint")); trace.emplace_back<SynthTrace::DrainMicrotasksRecord>( timeFromStart, maxMicrotasksHint); break; } case RecordType::CreateString: { auto encoding = llvh::dyn_cast_or_null<JSONString>(obj->get("encoding")); bool isAscii = false; if (encoding->str() == "ASCII") { isAscii = true; } else { assert(encoding->str() == "UTF-8"); } auto str = llvh::dyn_cast_or_null<JSONString>(obj->get("chars")); if (isAscii) { trace.emplace_back<SynthTrace::CreateStringRecord>( timeFromStart, objID->getValue(), str->str().data(), str->str().size()); } else { trace.emplace_back<SynthTrace::CreateStringRecord>( timeFromStart, objID->getValue(), reinterpret_cast<const uint8_t *>(str->str().data()), str->str().size()); } break; } case RecordType::CreatePropNameID: { auto id = llvh::dyn_cast_or_null<JSONNumber>(obj->get("objID")); if (propValue) { trace.emplace_back<SynthTrace::CreatePropNameIDRecord>( timeFromStart, id->getValue(), trace.decode(propValue->c_str())); } else { auto encoding = llvh::dyn_cast_or_null<JSONString>(obj->get("encoding")); bool isAscii = false; if (encoding->str() == "ASCII") { isAscii = true; } else { assert(encoding->str() == "UTF-8"); } auto str = llvh::dyn_cast_or_null<JSONString>(obj->get("chars")); if (isAscii) { trace.emplace_back<SynthTrace::CreatePropNameIDRecord>( timeFromStart, id->getValue(), str->str().data(), str->str().size()); } else { trace.emplace_back<SynthTrace::CreatePropNameIDRecord>( timeFromStart, id->getValue(), reinterpret_cast<const uint8_t *>(str->str().data()), str->str().size()); } } break; } case RecordType::CreateHostObject: trace.emplace_back<SynthTrace::CreateHostObjectRecord>( timeFromStart, objID->getValue()); break; case RecordType::CreateHostFunction: { unsigned paramCount = 0; if (JSONNumber *jsonParamCount = llvh::dyn_cast_or_null<JSONNumber>( obj->get("parameterCount"))) { paramCount = jsonParamCount->getValue(); } std::string functionName; if (JSONString *jsonFunctionName = llvh::dyn_cast_or_null<JSONString>(obj->get("functionName"))) { functionName = jsonFunctionName->str(); } trace.emplace_back<SynthTrace::CreateHostFunctionRecord>( timeFromStart, objID->getValue(), propNameID->getValue(), #ifdef HERMESVM_API_TRACE_DEBUG functionName, #endif paramCount); break; } case RecordType::GetProperty: trace.emplace_back<SynthTrace::GetPropertyRecord>( timeFromStart, objID->getValue(), propID->getValue(), #ifdef HERMESVM_API_TRACE_DEBUG std::string(propName->c_str()), #endif trace.decode(propValue->c_str())); break; case RecordType::SetProperty: trace.emplace_back<SynthTrace::SetPropertyRecord>( timeFromStart, objID->getValue(), propID->getValue(), #ifdef HERMESVM_API_TRACE_DEBUG std::string(propName->c_str()), #endif trace.decode(propValue->c_str())); break; case RecordType::HasProperty: trace.emplace_back<SynthTrace::HasPropertyRecord>( timeFromStart, objID->getValue(), propID->getValue() #ifdef HERMESVM_API_TRACE_DEBUG , std::string(propName->c_str()) #endif ); break; case RecordType::GetPropertyNames: trace.emplace_back<SynthTrace::GetPropertyNamesRecord>( timeFromStart, objID->getValue(), getNumberAs<SynthTrace::ObjectID>(obj->get("propNamesID"))); break; case RecordType::CreateArray: trace.emplace_back<SynthTrace::CreateArrayRecord>( timeFromStart, objID->getValue(), getNumberAs<uint64_t>(obj->get("length"))); break; case RecordType::ArrayRead: trace.emplace_back<SynthTrace::ArrayReadRecord>( timeFromStart, objID->getValue(), arrayIndex->getValue(), trace.decode(propValue->c_str())); break; case RecordType::ArrayWrite: trace.emplace_back<SynthTrace::ArrayWriteRecord>( timeFromStart, objID->getValue(), arrayIndex->getValue(), trace.decode(propValue->c_str())); break; case RecordType::CallFromNative: trace.emplace_back<SynthTrace::CallFromNativeRecord>( timeFromStart, funcID->getValue(), trace.decode(thisArg->c_str()), getListOfTraceValues(callArgs, trace)); break; case RecordType::ConstructFromNative: trace.emplace_back<SynthTrace::ConstructFromNativeRecord>( timeFromStart, funcID->getValue(), trace.decode(thisArg->c_str()), getListOfTraceValues(callArgs, trace)); break; case RecordType::ReturnFromNative: trace.emplace_back<SynthTrace::ReturnFromNativeRecord>( timeFromStart, trace.decode(retval->c_str())); break; case RecordType::ReturnToNative: trace.emplace_back<SynthTrace::ReturnToNativeRecord>( timeFromStart, trace.decode(retval->c_str())); break; case RecordType::CallToNative: trace.emplace_back<SynthTrace::CallToNativeRecord>( timeFromStart, funcID->getValue(), trace.decode(thisArg->c_str()), getListOfTraceValues(callArgs, trace)); break; case RecordType::GetPropertyNative: trace.emplace_back<SynthTrace::GetPropertyNativeRecord>( timeFromStart, hostObjID->getValue(), propNameID->getValue(), propName->c_str()); break; case RecordType::GetPropertyNativeReturn: trace.emplace_back<SynthTrace::GetPropertyNativeReturnRecord>( timeFromStart, trace.decode(retval->c_str())); break; case RecordType::SetPropertyNative: trace.emplace_back<SynthTrace::SetPropertyNativeRecord>( timeFromStart, hostObjID->getValue(), propNameID->getValue(), propName->c_str(), trace.decode(propValue->c_str())); break; case RecordType::SetPropertyNativeReturn: trace.emplace_back<SynthTrace::SetPropertyNativeReturnRecord>( timeFromStart); break; case RecordType::GetNativePropertyNames: trace.emplace_back<SynthTrace::GetNativePropertyNamesRecord>( timeFromStart, hostObjID->getValue()); break; case RecordType::GetNativePropertyNamesReturn: trace.emplace_back<SynthTrace::GetNativePropertyNamesReturnRecord>( timeFromStart, getListOfStrings<std::vector>( llvh::cast<JSONArray>(obj->get("properties")))); break; } } return trace; } } // namespace std::tuple< SynthTrace, ::hermes::vm::RuntimeConfig::Builder, ::hermes::vm::GCConfig::Builder, ::hermes::vm::MockedEnvironment> parseSynthTrace(std::unique_ptr<llvh::MemoryBuffer> trace) { JSLexer::Allocator alloc; JSONObject *root = llvh::cast<JSONObject>(parseJSON(alloc, std::move(trace))); if (!llvh::dyn_cast_or_null<JSONNumber>(root->get("globalObjID"))) { throw std::invalid_argument( "Trace does not have a \"globalObjID\" value that is a number"); } if (!llvh::dyn_cast_or_null<JSONObject>(root->get("env"))) { throw std::invalid_argument( "Trace does not have an \"env\" value that is an object"); } if (auto *ver = root->get("version")) { // Version exists, validate that it is a number, and the correct version. if (auto *verNum = llvh::dyn_cast<JSONNumber>(ver)) { // version is a number. const uint32_t version = verNum->getValue(); if (version != SynthTrace::synthVersion()) { throw std::invalid_argument( "Trace version mismatch, expected " + std::to_string(SynthTrace::synthVersion()) + ", actual: " + std::to_string(static_cast<uint32_t>(verNum->getValue()))); } } else { throw std::invalid_argument( "version exists, but is not a Number. Found instead value of kind: " + std::string(::hermes::parser::JSONKindToString(ver->getKind()))); } } // Else, for backwards compatibility, allow no version to be specified, which // will imply "latest version". auto globalObjID = getNumberAs<SynthTrace::ObjectID>(root->get("globalObjID")); // Get and parse the records list. JSONObject *const rtConfig = llvh::cast_or_null<JSONObject>(root->get("runtimeConfig")); return std::make_tuple( getTrace(llvh::cast<JSONArray>(root->at("trace")), globalObjID), getRuntimeConfig(rtConfig), getGCConfig(rtConfig), getMockedEnvironment(llvh::cast<JSONObject>(root->at("env")))); } std::tuple< SynthTrace, ::hermes::vm::RuntimeConfig::Builder, ::hermes::vm::GCConfig::Builder, ::hermes::vm::MockedEnvironment> parseSynthTrace(const std::string &tracefile) { return parseSynthTrace( std::move(llvh::MemoryBuffer::getFile(tracefile).get())); } } // namespace tracing } // namespace hermes } // namespace facebook