diff --git a/API/hermes/CMakeLists.txt b/API/hermes/CMakeLists.txt index 60312255f36..f7b6918c9f0 100644 --- a/API/hermes/CMakeLists.txt +++ b/API/hermes/CMakeLists.txt @@ -90,6 +90,9 @@ add_hermes_library(hermesapi LINK_OBJLIBS hermesPublic LINK_LIBS jsi ${CDP_DEPS} ) target_include_directories(hermesapi_obj PUBLIC ..) +target_include_directories(hermesapi_obj PRIVATE + ${PROJECT_SOURCE_DIR}/lib/VM + ) # Pass core extensions flag to C++ code. if(HERMES_ENABLE_CORE_EXTENSIONS) diff --git a/API/hermes/hermes.cpp b/API/hermes/hermes.cpp index 90c900f1b8e..6060c2e0e9b 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -7,6 +7,8 @@ #include +#include "JSONValue.h" + #include "llvh/Support/Compiler.h" #include "hermes/ADT/ManagedChunkedList.h" @@ -249,6 +251,7 @@ class HermesRootAPI final : public IHermesRootAPI, public ISetFatalHandler { namespace { class HermesRuntimeImpl final : public HermesRuntime, private IHermesTestHelpers, + private jsi::IJSONValueFactory, private InstallHermesFatalErrorHandler, private jsi::Instrumentation #ifdef JSI_UNSTABLE @@ -700,6 +703,13 @@ class HermesRuntimeImpl final : public HermesRuntime, ICast *castInterface(const jsi::UUID &interfaceUUID) override; + jsi::Value createValueFromJSONTree( + const jsi::JSONValue &value) override; + jsi::Value createValueFromJSONTreeAndConsume( + jsi::JSONValue &value) override; + jsi::JSONValue createJSONTreeFromValue( + const jsi::Value &value) override; + #ifdef JSI_UNSTABLE std::shared_ptr serialize(const jsi::Value &value) override; jsi::Value deserialize( @@ -1573,6 +1583,8 @@ jsi::ICast *HermesRuntimeImpl::castInterface(const jsi::UUID &interfaceUUID) { return static_cast(this); } else if (interfaceUUID == IHermesSHUnit::uuid) { return static_cast(this); + } else if (interfaceUUID == jsi::IJSONValueFactory::uuid) { + return static_cast(this); } #ifdef JSI_UNSTABLE else if (interfaceUUID == ISerialization::uuid) { @@ -1584,6 +1596,34 @@ jsi::ICast *HermesRuntimeImpl::castInterface(const jsi::UUID &interfaceUUID) { return nullptr; } +jsi::Value HermesRuntimeImpl::createValueFromJSONTree( + const jsi::JSONValue &value) { + vm::GCScope gcScope(runtime_); + vm::JSONValueMaterializer materializer; + auto res = materializer.materialize(runtime_, value); + checkStatus(res.getStatus()); + return valueFromHermesValue(*res); +} + +jsi::Value HermesRuntimeImpl::createValueFromJSONTreeAndConsume( + jsi::JSONValue &value) { + vm::GCScope gcScope(runtime_); + vm::JSONValueMaterializer materializer; + auto res = materializer.materializeAndConsume(runtime_, value); + checkStatus(res.getStatus()); + return valueFromHermesValue(*res); +} + +jsi::JSONValue HermesRuntimeImpl::createJSONTreeFromValue( + const jsi::Value &value) { + vm::GCScope gcScope(runtime_); + vm::PinnedHermesValue numberStorage; + auto res = vm::createJSONValueFromHermesValue( + runtime_, vmHandleFromValue(value, &numberStorage)); + checkStatus(res.getStatus()); + return std::move(*res); +} + #ifdef JSI_UNSTABLE std::shared_ptr HermesRuntimeImpl::serialize( const jsi::Value &value) { diff --git a/API/jsi/jsi/jsi.cpp b/API/jsi/jsi/jsi.cpp index 792b5bf5a0b..e58338a2e46 100644 --- a/API/jsi/jsi/jsi.cpp +++ b/API/jsi/jsi/jsi.cpp @@ -273,6 +273,16 @@ MutableBuffer::~MutableBuffer() = default; PreparedJavaScript::~PreparedJavaScript() = default; +JSONValue JSONValue::createFromValue(IRuntime& runtime, const Value& value) { + auto* factory = static_cast( + runtime.castInterface(IJSONValueFactory::uuid)); + if (!factory) { + throw JSINativeException( + "Runtime does not implement jsi::IJSONValueFactory"); + } + return factory->createJSONTreeFromValue(value); +} + Value HostObject::get(Runtime&, const PropNameID&) { return Value(); } diff --git a/API/jsi/jsi/jsi.h b/API/jsi/jsi/jsi.h index bf98028fb3f..1f25e5c0bf9 100644 --- a/API/jsi/jsi/jsi.h +++ b/API/jsi/jsi/jsi.h @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #ifndef JSI_EXPORT @@ -203,6 +205,108 @@ class JSError; class TypedArray; class Uint8Array; +/// A decoded JSON-compatible value tree that native code can use to ask a +/// runtime to materialize a JavaScript value without reparsing JSON text. +/// +/// Strings carry their decoded character encoding so runtimes can avoid +/// unnecessary transcoding when the producer already has the runtime's +/// preferred representation. +struct JSI_EXPORT JSONValue { + enum class Kind { Null, Bool, Number, String, Array, Object }; + enum class StringEncoding { ASCII, UTF8, UTF16 }; + + using Object = std::vector>; + + Kind kind{Kind::Null}; + bool boolValue{false}; + double numberValue{0}; + StringEncoding stringEncoding{StringEncoding::ASCII}; + std::string stringValue; + std::u16string utf16StringValue; + std::vector arrayValue; + Object objectValue; + + static JSONValue null() { + return {}; + } + + static JSONValue boolean(bool value) { + JSONValue result; + result.kind = Kind::Bool; + result.boolValue = value; + return result; + } + + static JSONValue number(double value) { + JSONValue result; + result.kind = Kind::Number; + result.numberValue = value; + return result; + } + + static bool isASCII(std::string_view value) { + for (unsigned char c : value) { + if (c > 0x7f) { + return false; + } + } + return true; + } + + static JSONValue asciiString(std::string value) { + JSONValue result; + result.kind = Kind::String; + result.stringEncoding = + isASCII(value) ? StringEncoding::ASCII : StringEncoding::UTF8; + result.stringValue = std::move(value); + return result; + } + + static JSONValue utf8String(std::string value) { + if (isASCII(value)) { + return asciiString(std::move(value)); + } + JSONValue result; + result.kind = Kind::String; + result.stringEncoding = StringEncoding::UTF8; + result.stringValue = std::move(value); + return result; + } + + static JSONValue utf16String(std::u16string value) { + JSONValue result; + result.kind = Kind::String; + result.stringEncoding = StringEncoding::UTF16; + result.utf16StringValue = std::move(value); + return result; + } + + static JSONValue string(std::string value) { + return utf8String(std::move(value)); + } + + static JSONValue array(std::vector value) { + JSONValue result; + result.kind = Kind::Array; + result.arrayValue = std::move(value); + return result; + } + + static JSONValue object(Object value) { + JSONValue result; + result.kind = Kind::Object; + result.objectValue = std::move(value); + return result; + } + + /// Create a JSONValue tree from a JavaScript value. + /// + /// This walks arrays and own enumerable object properties. Values that do + /// not have a JSONValue representation, such as functions, symbols, bigints, + /// and cyclic objects, throw a JSIException. + static JSONValue createFromValue(IRuntime& runtime, const Value& value); +}; + /// A function which has this type can be registered as a function /// callable from JavaScript using Function::createFromHostFunction(). /// When the function is called, args will point to the arguments, and @@ -329,6 +433,32 @@ class JSI_EXPORT ISerialization : public ICast { #endif // JSI_UNSTABLE +/// Optional interface for runtimes that can efficiently materialize a decoded +/// JSONValue tree into a JavaScript value. +class JSI_EXPORT IJSONValueFactory : public ICast { + public: + static constexpr jsi::UUID uuid{ + 0x69b0d272, + 0x65fa, + 0x4c9a, + 0x9d83, + 0x1c85a5a610ce}; + + /// Materialize \p value without mutating it. + virtual Value createValueFromJSONTree(const JSONValue& value) = 0; + + /// Materialize \p value and allow the runtime to consume string storage. + /// + /// After this returns, \p value must be treated as consumed. + virtual Value createValueFromJSONTreeAndConsume(JSONValue& value) = 0; + + /// Create a JSONValue tree from a JavaScript value. + virtual JSONValue createJSONTreeFromValue(const Value& value) = 0; + + protected: + ~IJSONValueFactory() = default; +}; + /// An interface that provides various functionalities of the JS runtime. /// The APIs must not be called from multiple threads concurrently. It is the /// user's responsibility ensure thread safety when using IRuntime. diff --git a/lib/VM/CMakeLists.txt b/lib/VM/CMakeLists.txt index 663a0455edd..634fb69064e 100644 --- a/lib/VM/CMakeLists.txt +++ b/lib/VM/CMakeLists.txt @@ -44,6 +44,7 @@ set(source_files JSWeakMapImpl.cpp JSWeakRef.cpp JSFinalizationRegistry.cpp + JSONValue.cpp LimitedStorageProvider.cpp DecoratedObject.cpp HostModel.cpp diff --git a/lib/VM/JSONSerializedValueEncoder.h b/lib/VM/JSONSerializedValueEncoder.h new file mode 100644 index 00000000000..51e34362f60 --- /dev/null +++ b/lib/VM/JSONSerializedValueEncoder.h @@ -0,0 +1,258 @@ +/** + * 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. + */ + +#pragma once + +#include "hermes/VM/SerializedValue.h" +#include "jsi/jsi.h" + +#include "llvh/Support/ConvertUTF.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace hermes::vm { + +using JSONValue = ::facebook::jsi::JSONValue; + +/// Encodes the JSON subset directly into Hermes' internal structured-clone +/// SerializedValue representation, without constructing a Runtime. +class JSONSerializedValueEncoder { + public: + SerializedValue encode(const JSONValue &value) { + SerializedValue result; + out_ = &result; + struct ResetOutput { + explicit ResetOutput(JSONSerializedValueEncoder &encoder) + : encoder_(encoder) {} + ~ResetOutput() { + encoder_.out_ = nullptr; + } + + JSONSerializedValueEncoder &encoder_; + } reset{*this}; + + stringIds_.clear(); + writeValue(value); + return result; + } + + private: + SerializedValue *out_{nullptr}; + std::unordered_map stringIds_; + + static uint8_t typeByte(SerializedValue::Type type) { + return static_cast(type); + } + + template + static void appendPod(std::vector &buffer, T value) { + const auto *bytes = reinterpret_cast(&value); + buffer.insert(buffer.end(), bytes, bytes + sizeof(T)); + } + + static void appendDouble(std::vector &buffer, double value) { + uint64_t bits; + static_assert(sizeof(bits) == sizeof(value), "Unexpected double size"); + std::memcpy(&bits, &value, sizeof(bits)); + appendPod(buffer, bits); + } + + static uint32_t checkedUint32(size_t value, const char *message) { + if (value > std::numeric_limits::max()) { + throw std::overflow_error(message); + } + return static_cast(value); + } + + uint32_t nextRecordID() const { + return checkedUint32( + out_->offsets.size(), "Serialized value has too many records"); + } + + void pushOffset(size_t offset) { + out_->offsets.push_back( + checkedUint32(offset, "Serialized value buffer is too large")); + } + + static void appendUTF16( + std::vector &buffer, + std::u16string_view value) { + buffer.insert( + buffer.end(), + reinterpret_cast(value.data()), + reinterpret_cast(value.data() + value.size())); + } + + static std::u16string convertUTF8ToUTF16(std::string_view value) { + if (value.empty()) { + return {}; + } + + std::u16string out; + out.resize(value.size()); + + const auto *sourceStart = + reinterpret_cast(value.data()); + const auto *sourceEnd = sourceStart + value.size(); + auto *targetStart = reinterpret_cast(out.data()); + auto *targetEnd = targetStart + out.size(); + llvh::ConversionResult result = llvh::ConvertUTF8toUTF16( + &sourceStart, + sourceEnd, + &targetStart, + targetEnd, + llvh::strictConversion); + if (result != llvh::ConversionResult::conversionOK) { + throw std::invalid_argument("Invalid UTF-8 JSON string"); + } + + out.resize(reinterpret_cast(targetStart) - out.data()); + return out; + } + + static std::string makeStringKey(const JSONValue &value) { + std::string key; + key.push_back(static_cast(value.stringEncoding)); + if (value.stringEncoding == JSONValue::StringEncoding::UTF16) { + const auto *bytes = + reinterpret_cast(value.utf16StringValue.data()); + key.append(bytes, value.utf16StringValue.size() * sizeof(char16_t)); + } else { + key.append(value.stringValue); + } + return key; + } + + uint32_t internString(const JSONValue &value) { + std::string key = makeStringKey(value); + auto it = stringIds_.find(key); + if (it != stringIds_.end()) { + return it->second; + } + + const uint32_t id = nextRecordID(); + stringIds_.emplace(std::move(key), id); + pushOffset(out_->strings.size()); + + std::u16string utf16Storage; + const bool ascii = value.stringEncoding == JSONValue::StringEncoding::ASCII; + if (value.stringEncoding == JSONValue::StringEncoding::UTF8) { + utf16Storage = convertUTF8ToUTF16(value.stringValue); + } + + out_->strings.push_back(ascii ? 1 : 0); + const uint32_t length = ascii + ? checkedUint32(value.stringValue.size(), "JSON string is too large") + : value.stringEncoding == JSONValue::StringEncoding::UTF16 + ? checkedUint32( + value.utf16StringValue.size(), "JSON string is too large") + : checkedUint32(utf16Storage.size(), "JSON string is too large"); + appendPod(out_->strings, length); + + if (ascii) { + out_->strings.insert( + out_->strings.end(), + value.stringValue.begin(), + value.stringValue.end()); + } else { + while (out_->strings.size() % alignof(char16_t) != 0) { + out_->strings.push_back(0); + } + if (value.stringEncoding == JSONValue::StringEncoding::UTF16) { + appendUTF16(out_->strings, value.utf16StringValue); + } else { + appendUTF16(out_->strings, utf16Storage); + } + } + + return id; + } + + uint32_t internUTF8String(std::string_view value) { + return internString(JSONValue::utf8String(std::string{value})); + } + + void writeValue(const JSONValue &value) { + switch (value.kind) { + case JSONValue::Kind::Null: + out_->content.push_back(typeByte(SerializedValue::Type::Null)); + return; + case JSONValue::Kind::Bool: + out_->content.push_back( + typeByte(SerializedValue::Type::PrimitiveBoolean)); + out_->content.push_back(value.boolValue ? 1 : 0); + return; + case JSONValue::Kind::Number: + if (!std::isfinite(value.numberValue)) { + throw std::invalid_argument("JSON numbers must be finite"); + } + out_->content.push_back( + typeByte(SerializedValue::Type::PrimitiveNumber)); + appendDouble(out_->content, value.numberValue); + return; + case JSONValue::Kind::String: + out_->content.push_back( + typeByte(SerializedValue::Type::PrimitiveString)); + appendPod(out_->content, internString(value)); + return; + case JSONValue::Kind::Array: + writeArray(value.arrayValue); + return; + case JSONValue::Kind::Object: + writeObject(value.objectValue); + return; + } + assert(false && "Unhandled JSON kind"); + } + + uint32_t beginObjectRecord(SerializedValue::Type type) { + const uint32_t id = nextRecordID(); + pushOffset(out_->content.size()); + out_->content.push_back(typeByte(type)); + appendPod(out_->content, id); + return id; + } + + void writeArray(const std::vector &array) { + if (array.size() > std::numeric_limits::max()) { + throw std::overflow_error("JSON array is too large"); + } + beginObjectRecord(SerializedValue::Type::Array); + const auto len = static_cast(array.size()); + appendPod(out_->content, len); + appendPod(out_->content, len); + for (uint32_t i = 0; i < len; ++i) { + appendPod(out_->content, i); + writeValue(array[i]); + } + appendPod(out_->content, 0); + } + + void writeObject(const JSONValue::Object &object) { + if (object.size() > std::numeric_limits::max()) { + throw std::overflow_error("JSON object has too many properties"); + } + beginObjectRecord(SerializedValue::Type::Object); + appendPod(out_->content, static_cast(object.size())); + for (const auto &[key, value] : object) { + appendPod(out_->content, internUTF8String(key)); + writeValue(value); + } + } +}; + +} // namespace hermes::vm diff --git a/lib/VM/JSONValue.cpp b/lib/VM/JSONValue.cpp new file mode 100644 index 00000000000..5dae164f8ba --- /dev/null +++ b/lib/VM/JSONValue.cpp @@ -0,0 +1,203 @@ +/** + * 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 "JSONValue.h" + +#include "JSLib/Object.h" +#include "hermes/Support/UTF8.h" +#include "hermes/VM/Callable.h" +#include "hermes/VM/StringView.h" + +#include +#include +#include + +namespace hermes { +namespace vm { +namespace { + +JSONValue jsonTreeStringToJSONValue( + Runtime &runtime, + Handle str) { + auto view = StringPrimitive::createStringView(runtime, str); + if (view.isASCII()) { + return JSONValue::asciiString( + std::string{view.castToCharPtr(), view.length()}); + } + + return JSONValue::utf16String( + std::u16string{view.castToChar16Ptr(), view.length()}); +} + +std::string jsonTreeStringToUTF8( + Runtime &runtime, + Handle str) { + auto view = StringPrimitive::createStringView(runtime, str); + if (view.isASCII()) { + return std::string{view.castToCharPtr(), view.length()}; + } + + std::string result; + ::hermes::convertUTF16ToUTF8WithReplacements( + result, llvh::ArrayRef{view.castToChar16Ptr(), view.length()}); + return result; +} + +bool jsonTreeContainsAncestor( + const std::vector> &ancestors, + Handle object) { + for (auto ancestor : ancestors) { + if (*ancestor == *object) { + return true; + } + } + return false; +} + +class JSONTreeAncestorScope { + public: + JSONTreeAncestorScope( + std::vector> &ancestors, + Handle object) + : ancestors_(ancestors) { + ancestors_.push_back(object); + } + + ~JSONTreeAncestorScope() { + ancestors_.pop_back(); + } + + private: + std::vector> &ancestors_; +}; + +CallResult createJSONValueFromHermesValueImpl( + Runtime &runtime, + Handle<> value, + std::vector> &ancestors) { + if (value->isUndefined() || value->isNull()) { + return JSONValue::null(); + } + if (value->isBool()) { + return JSONValue::boolean(value->getBool()); + } + if (value->isNumber()) { + const double number = value->getNumber(); + return std::isfinite(number) ? JSONValue::number(number) + : JSONValue::null(); + } + if (value->isString()) { + return jsonTreeStringToJSONValue( + runtime, Handle::vmcast(value)); + } + if (value->isSymbol()) { + return runtime.raiseTypeError("Cannot create JSONValue from symbol"); + } + if (value->isBigInt()) { + return runtime.raiseTypeError("Cannot create JSONValue from bigint"); + } + + assert(value->isObject() && "Unhandled HermesValue kind"); + + struct : public Locals { + PinnedValue object; + PinnedValue keys; + PinnedValue<> key; + PinnedValue<> child; + } lv; + LocalsRAII lraii(runtime, &lv); + + lv.object = vmcast(*value); + if (vmisa(*lv.object)) { + return runtime.raiseTypeError("Cannot create JSONValue from function"); + } + if (jsonTreeContainsAncestor(ancestors, lv.object)) { + return runtime.raiseTypeError("Cannot create JSONValue from cyclic object"); + } + + JSONTreeAncestorScope ancestorScope{ancestors, lv.object}; + + Handle objectHandle = lv.object; + if (auto array = Handle::dyn_vmcast(objectHandle)) { + const uint32_t len = JSArray::getLength(*array, runtime); + std::vector result; + result.reserve(len); + + for (uint32_t i = 0; i < len; ++i) { + GCScopeMarkerRAII marker{runtime}; + auto element = array->at(runtime, i); + if (element.isEmpty()) { + result.push_back(JSONValue::null()); + continue; + } + + lv.child = element.unboxToHV(runtime); + auto childRes = + createJSONValueFromHermesValueImpl(runtime, lv.child, ancestors); + if (childRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + result.push_back(std::move(*childRes)); + } + + return JSONValue::array(std::move(result)); + } + + auto keysRes = enumerableOwnProperties_RJS( + runtime, lv.object, EnumerableOwnPropertiesKind::Key); + if (keysRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + lv.keys.castAndSetHermesValue(*keysRes); + + const uint32_t len = JSArray::getLength(*lv.keys, runtime); + JSONValue::Object result; + result.reserve(len); + + for (uint32_t i = 0; i < len; ++i) { + GCScopeMarkerRAII marker{runtime}; + auto key = lv.keys->at(runtime, i); + if (key.isEmpty()) { + continue; + } + lv.key = key.unboxToHV(runtime); + if (!lv.key->isString()) { + return runtime.raiseTypeError("Enumerable object key is not a string"); + } + + auto propRes = JSObject::getComputed_RJS(lv.object, runtime, lv.key); + if (propRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + lv.child = std::move(*propRes); + + auto childRes = + createJSONValueFromHermesValueImpl(runtime, lv.child, ancestors); + if (childRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + + result.emplace_back( + jsonTreeStringToUTF8( + runtime, Handle::vmcast(&lv.key)), + std::move(*childRes)); + } + + return JSONValue::object(std::move(result)); +} + +} // namespace + +CallResult createJSONValueFromHermesValue( + Runtime &runtime, + Handle<> value) { + std::vector> ancestors; + return createJSONValueFromHermesValueImpl(runtime, value, ancestors); +} + +} // namespace vm +} // namespace hermes diff --git a/lib/VM/JSONValue.h b/lib/VM/JSONValue.h new file mode 100644 index 00000000000..80c08e453dc --- /dev/null +++ b/lib/VM/JSONValue.h @@ -0,0 +1,248 @@ +/** + * 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. + */ + +#pragma once + +#include "hermes/VM/Handle.h" +#include "hermes/VM/JSArray.h" +#include "hermes/VM/JSObject.h" +#include "hermes/VM/Operations.h" +#include "hermes/VM/Runtime.h" +#include "hermes/VM/SmallHermesValue-inline.h" +#include "hermes/VM/StringPrimitive.h" +#include "jsi/jsi.h" + +#include "llvh/ADT/ArrayRef.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace hermes::vm { + +using JSONValue = ::facebook::jsi::JSONValue; + +/// Create a JSONValue tree from a Hermes value. +/// +/// This walks arrays and own enumerable object properties. Values that do not +/// have a JSONValue representation, such as functions, symbols, bigints, and +/// cyclic objects, throw. +CallResult createJSONValueFromHermesValue( + Runtime &runtime, + Handle<> value); + +/// Materializes a JSONValue tree directly into Hermes VM values. +/// +/// This intentionally bypasses Hermes' StructuredDeserialize format. It is +/// useful when native code already has a decoded JSON-like C++ tree and wants +/// to construct the JS graph with less generic structured-clone overhead. +class JSONValueMaterializer { + public: + /// Materialize \p value without mutating it. + CallResult materialize( + Runtime &runtime, + const JSONValue &value) { + MaterializationContext context{/*consumeStringValues*/ false, {}}; + return materializeImpl(runtime, const_cast(value), context); + } + + /// Materialize \p value and allow string storage to be moved into Hermes. + /// + /// After this returns, \p value must be treated as consumed. This is the safe + /// ownership-transfer version of "reuse C string memory": Hermes may adopt + /// moved std::string storage for ASCII strings and moved std::u16string + /// storage for UTF-16 strings. UTF-8 strings still go through Hermes' UTF-8 + /// decoder. + CallResult materializeAndConsume( + Runtime &runtime, + JSONValue &value) { + MaterializationContext context{/*consumeStringValues*/ true, {}}; + return materializeImpl(runtime, value, context); + } + + private: + struct MaterializationContext { + bool consumeStringValues; + std::unordered_map keySymbolCache; + }; + + static CallResult createString( + Runtime &runtime, + JSONValue &value, + bool consume) { + switch (value.stringEncoding) { + case JSONValue::StringEncoding::ASCII: + if (consume) { + return StringPrimitive::createEfficient( + runtime, std::move(value.stringValue)); + } + return StringPrimitive::createEfficient( + runtime, + ASCIIRef{value.stringValue.data(), value.stringValue.size()}); + case JSONValue::StringEncoding::UTF8: { + const auto *data = + reinterpret_cast(value.stringValue.data()); + return StringPrimitive::createEfficient( + runtime, UTF8Ref{data, value.stringValue.size()}); + } + case JSONValue::StringEncoding::UTF16: + if (consume) { + return StringPrimitive::createEfficient( + runtime, std::move(value.utf16StringValue)); + } + return StringPrimitive::createEfficient( + runtime, + UTF16Ref{ + value.utf16StringValue.data(), + value.utf16StringValue.size()}); + } + assert(false && "Unhandled JSON string encoding"); + return HermesValue::encodeUndefinedValue(); + } + + static CallResult createStringFromUTF8( + Runtime &runtime, + const std::string &value) { + if (JSONValue::isASCII(value)) { + return StringPrimitive::createEfficient( + runtime, ASCIIRef{value.data(), value.size()}); + } + + const auto *data = reinterpret_cast(value.data()); + return StringPrimitive::createEfficient( + runtime, UTF8Ref{data, value.size()}); + } + + CallResult materializeImpl( + Runtime &runtime, + JSONValue &value, + MaterializationContext &context) { + switch (value.kind) { + case JSONValue::Kind::Null: + return HermesValue::encodeNullValue(); + case JSONValue::Kind::Bool: + return HermesValue::encodeBoolValue(value.boolValue); + case JSONValue::Kind::Number: + if (!std::isfinite(value.numberValue)) { + return runtime.raiseRangeError("JSON numbers must be finite"); + } + return HermesValue::encodeUntrustedNumberValue(value.numberValue); + case JSONValue::Kind::String: + return createString(runtime, value, context.consumeStringValues); + case JSONValue::Kind::Array: + return materializeArray(runtime, value.arrayValue, context); + case JSONValue::Kind::Object: + return materializeObject(runtime, value.objectValue, context); + } + assert(false && "Unhandled JSON kind"); + return HermesValue::encodeUndefinedValue(); + } + + CallResult materializeArray( + Runtime &runtime, + std::vector &array, + MaterializationContext &context) { + if (array.size() > std::numeric_limits::max()) { + return runtime.raiseRangeError("JSON array is too large"); + } + + struct : Locals { + PinnedValue array; + PinnedValue<> element; + } lv; + LocalsRAII lraii{runtime, &lv}; + + const auto len = static_cast(array.size()); + auto arrayRes = JSArray::create(runtime, len, len); + if (arrayRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + lv.array = std::move(*arrayRes); + + for (uint32_t i = 0; i < len; ++i) { + auto elementRes = materializeImpl(runtime, array[i], context); + if (elementRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + lv.element = *elementRes; + + auto shv = SmallHermesValue::encodeHermesValue( + lv.element.getHermesValue(), runtime); + JSArray::unsafeSetExistingElementAt(*lv.array, runtime, i, shv); + } + + return lv.array.getHermesValue(); + } + + CallResult materializeObject( + Runtime &runtime, + JSONValue::Object &object, + MaterializationContext &context) { + if (object.size() > std::numeric_limits::max()) { + return runtime.raiseRangeError("JSON object has too many properties"); + } + + struct : Locals { + PinnedValue object; + PinnedValue keyString; + PinnedValue keySymbol; + PinnedValue<> value; + } lv; + LocalsRAII lraii{runtime, &lv}; + + lv.object = JSObject::create(runtime, static_cast(object.size())); + + for (auto &[key, child] : object) { + auto cachedSymbol = context.keySymbolCache.find(key); + if (cachedSymbol != context.keySymbolCache.end()) { + lv.keySymbol = cachedSymbol->second; + } else { + auto keyRes = createStringFromUTF8(runtime, key); + if (keyRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + if (!keyRes->isString()) { + return runtime.raiseTypeError("JSON object key is not a string"); + } + lv.keyString = keyRes->getString(); + + auto symbolRes = + stringToSymbolID(runtime, createPseudoHandle(*lv.keyString)); + if (symbolRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + lv.keySymbol = std::move(*symbolRes); + context.keySymbolCache.emplace(key, *lv.keySymbol); + } + + auto valueRes = materializeImpl(runtime, child, context); + if (valueRes == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + lv.value = *valueRes; + + auto status = JSObject::defineNewOwnProperty( + lv.object, + runtime, + *lv.keySymbol, + PropertyFlags().defaultNewNamedPropertyFlags(), + lv.value); + if (status == ExecutionStatus::EXCEPTION) { + return ExecutionStatus::EXCEPTION; + } + } + + return lv.object.getHermesValue(); + } +}; + +} // namespace hermes::vm diff --git a/tools/hvm-bench/CMakeLists.txt b/tools/hvm-bench/CMakeLists.txt index e070f677766..213ade86127 100644 --- a/tools/hvm-bench/CMakeLists.txt +++ b/tools/hvm-bench/CMakeLists.txt @@ -8,3 +8,14 @@ add_hermes_tool(interp-dispatch-bench ${ALL_HEADER_FILES} LINK_LIBS hermesvm_a ) + +set(HERMES_ENABLE_EH ON) +add_hermes_tool(serialization-json-bench + serialization-json-bench.cpp + ${ALL_HEADER_FILES} + LINK_OBJLIBS hermesvm_a timerStats + ) +target_include_directories(serialization-json-bench PRIVATE + ${PROJECT_SOURCE_DIR}/lib/VM + ) +set(HERMES_ENABLE_EH OFF) diff --git a/tools/hvm-bench/serialization-json-bench.cpp b/tools/hvm-bench/serialization-json-bench.cpp new file mode 100644 index 00000000000..71a84675d41 --- /dev/null +++ b/tools/hvm-bench/serialization-json-bench.cpp @@ -0,0 +1,712 @@ +/* + * 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. + */ + +//===----------------------------------------------------------------------===// +/// \file +/// Benchmark JSON.parse-equivalent materialization against Hermes +/// ISerialization deserialization for the same JSON-shaped data. +//===----------------------------------------------------------------------===// + +#include "hermes/Support/OSCompat.h" +#include "hermes/Support/UTF16Stream.h" +#include "hermes/VM/Casting.h" +#include "hermes/VM/Handle.h" +#include "hermes/VM/JSArray.h" +#include "hermes/VM/JSLib/RuntimeJSONParse.h" +#include "hermes/VM/Runtime.h" +#include "hermes/VM/SerializedValue.h" +#include "hermes/hermes.h" + +#include "JSONSerializedValueEncoder.h" +#include "JSONValue.h" + +#include "llvh/Support/CommandLine.h" +#include "llvh/Support/InitLLVM.h" +#include "llvh/Support/PrettyStackTrace.h" +#include "llvh/Support/Signals.h" +#include "llvh/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fbhermes = facebook::hermes; +namespace jsi = facebook::jsi; +namespace ser = hermes::vm; +namespace vm = ::hermes::vm; + +namespace { + +static llvh::cl::opt Iterations{ + "iterations", + llvh::cl::init(1000), + llvh::cl::desc("Number of timed iterations per benchmark")}; + +static llvh::cl::opt Warmup{ + "warmup", + llvh::cl::init(100), + llvh::cl::desc("Number of untimed warmup iterations per benchmark")}; + +static llvh::cl::opt Items{ + "items", + llvh::cl::init(1000), + llvh::cl::desc("Number of objects in the generated JSON array")}; + +static llvh::cl::opt ValuesPerItem{ + "values", + llvh::cl::init(8), + llvh::cl::desc("Values per item, or nesting depth for --shape=deep")}; + +static llvh::cl::opt ValidateRepetitions{ + "validate-repetitions", + llvh::cl::init(0), + llvh::cl::desc("Run cross-runtime validation repeatedly and exit")}; + +static llvh::cl::opt Shape{ + "shape", + llvh::cl::init("mixed"), + llvh::cl::desc("Payload shape: strings, unicode, numbers, mixed, deep")}; + +static volatile uint64_t Sink; + +struct BenchPayload { + std::string json; + ser::JSONValue value; + uint32_t rootLength; +}; + +BenchPayload makeRecordsPayload(uint32_t items, uint32_t valuesPerItem) { + std::ostringstream os; + std::vector array; + array.reserve(items); + + os << '['; + for (uint32_t i = 0; i < items; ++i) { + if (i != 0) { + os << ','; + } + os << "{\"id\":" << i << ",\"name\":\"item-" << i + << "\",\"enabled\":" << ((i % 2) == 0 ? "true" : "false") + << ",\"nested\":{\"group\":" << (i % 17) + << ",\"label\":\"group-" << (i % 17) << "\"},\"values\":["; + for (uint32_t j = 0; j < valuesPerItem; ++j) { + if (j != 0) { + os << ','; + } + os << (i * valuesPerItem + j); + } + os << "]}"; + + std::vector values; + values.reserve(valuesPerItem); + for (uint32_t j = 0; j < valuesPerItem; ++j) { + values.push_back(ser::JSONValue::number(i * valuesPerItem + j)); + } + + ser::JSONValue::Object nested; + nested.reserve(2); + nested.emplace_back("group", ser::JSONValue::number(i % 17)); + nested.emplace_back( + "label", ser::JSONValue::string("group-" + std::to_string(i % 17))); + + ser::JSONValue::Object object; + object.reserve(5); + object.emplace_back("id", ser::JSONValue::number(i)); + object.emplace_back( + "name", ser::JSONValue::string("item-" + std::to_string(i))); + object.emplace_back("enabled", ser::JSONValue::boolean((i % 2) == 0)); + object.emplace_back("nested", ser::JSONValue::object(std::move(nested))); + object.emplace_back("values", ser::JSONValue::array(std::move(values))); + array.push_back(ser::JSONValue::object(std::move(object))); + } + os << ']'; + return {os.str(), ser::JSONValue::array(std::move(array)), items}; +} + +BenchPayload makeNumbersPayload(uint32_t items, uint32_t valuesPerItem) { + const uint32_t count = items * valuesPerItem; + std::ostringstream os; + std::vector array; + array.reserve(count); + + os << '['; + for (uint32_t i = 0; i < count; ++i) { + if (i != 0) { + os << ','; + } + os << i; + array.push_back(ser::JSONValue::number(i)); + } + os << ']'; + return {os.str(), ser::JSONValue::array(std::move(array)), count}; +} + +BenchPayload makeStringsPayload(uint32_t items, uint32_t valuesPerItem) { + const uint32_t count = items * valuesPerItem; + std::ostringstream os; + std::vector array; + array.reserve(count); + + os << '['; + for (uint32_t i = 0; i < count; ++i) { + std::string value = "item-" + std::to_string(i) + "-payload-string"; + if (i != 0) { + os << ','; + } + os << '"' << value << '"'; + array.push_back(ser::JSONValue::string(std::move(value))); + } + os << ']'; + return {os.str(), ser::JSONValue::array(std::move(array)), count}; +} + +BenchPayload makeUnicodeStringsPayload( + uint32_t items, + uint32_t valuesPerItem) { + const uint32_t count = items * valuesPerItem; + std::ostringstream os; + std::vector array; + array.reserve(count); + + os << '['; + for (uint32_t i = 0; i < count; ++i) { + std::string asciiPrefix = "item-" + std::to_string(i) + "-"; + std::u16string value{asciiPrefix.begin(), asciiPrefix.end()}; + value.push_back(u'\u00e9'); + value.push_back(u'-'); + value.push_back(u'\u6771'); + value.push_back(u'-'); + value.push_back(0xd83d); + value.push_back(0xde80); + + if (i != 0) { + os << ','; + } + os << '"' << asciiPrefix << "\\u00e9-\\u6771-\\ud83d\\ude80\""; + array.push_back(ser::JSONValue::utf16String(std::move(value))); + } + os << ']'; + return {os.str(), ser::JSONValue::array(std::move(array)), count}; +} + +ser::JSONValue makeDeepJSONValue(uint32_t item, uint32_t depth) { + ser::JSONValue child = ser::JSONValue::number(item); + for (uint32_t level = 0; level < depth; ++level) { + ser::JSONValue::Object object; + object.reserve(2); + object.emplace_back("level", ser::JSONValue::number(level + 1)); + object.emplace_back("child", std::move(child)); + child = ser::JSONValue::object(std::move(object)); + } + return child; +} + +void appendDeepJSON(std::ostringstream &os, uint32_t item, uint32_t depth) { + for (uint32_t level = 0; level < depth; ++level) { + os << "{\"level\":" << (depth - level) << ",\"child\":"; + } + os << item; + for (uint32_t level = 0; level < depth; ++level) { + os << '}'; + } +} + +BenchPayload makeDeepPayload(uint32_t items, uint32_t depth) { + std::ostringstream os; + std::vector array; + array.reserve(items); + + os << '['; + for (uint32_t i = 0; i < items; ++i) { + if (i != 0) { + os << ','; + } + appendDeepJSON(os, i, depth); + array.push_back(makeDeepJSONValue(i, depth)); + } + os << ']'; + return {os.str(), ser::JSONValue::array(std::move(array)), items}; +} + +BenchPayload makePayload( + llvh::StringRef shape, + uint32_t items, + uint32_t valuesPerItem) { + if (shape == "mixed" || shape == "records") { + return makeRecordsPayload(items, valuesPerItem); + } + if (shape == "numbers") { + return makeNumbersPayload(items, valuesPerItem); + } + if (shape == "strings") { + return makeStringsPayload(items, valuesPerItem); + } + if (shape == "unicode") { + return makeUnicodeStringsPayload(items, valuesPerItem); + } + if (shape == "deep") { + return makeDeepPayload(items, valuesPerItem); + } + throw std::invalid_argument("Unknown --shape value"); +} + +void consumeArrayLength(jsi::Runtime &runtime, const jsi::Value &value) { + Sink += value.getObject(runtime).getArray(runtime).size(runtime); +} + +bool checkGeneratedValue( + jsi::Runtime &runtime, + const jsi::Value &value, + uint32_t rootLength) { + jsi::Array array = value.getObject(runtime).getArray(runtime); + return array.size(runtime) == rootLength; +} + +std::string stringifyValue(jsi::Runtime &runtime, const jsi::Value &value) { + jsi::Scope scope{runtime}; + jsi::Object jsonObject = + runtime.global().getPropertyAsObject(runtime, "JSON"); + jsi::Function stringify = + jsonObject.getPropertyAsFunction(runtime, "stringify"); + auto callWithArgs = static_cast(&jsi::Function::call); + jsi::Value result = (stringify.*callWithArgs)(runtime, &value, 1); + return result.asString(runtime).utf8(runtime); +} + +bool reportMismatch( + llvh::StringRef label, + const std::string &expected, + const std::string &actual) { + if (expected == actual) { + return true; + } + + const auto expectedPrefixLen = std::min(expected.size(), 160); + const auto actualPrefixLen = std::min(actual.size(), 160); + llvh::errs() << label << " mismatch: expected_bytes=" << expected.size() + << ", actual_bytes=" << actual.size() << '\n'; + llvh::errs() << "expected_prefix=" + << llvh::StringRef{expected.data(), expectedPrefixLen} << '\n'; + llvh::errs() << "actual_prefix=" + << llvh::StringRef{actual.data(), actualPrefixLen} << '\n'; + return false; +} + +bool validateCrossRuntimeRoundTrips(const BenchPayload &benchPayload) { + auto sourceRuntime = fbhermes::makeHermesRuntime(); + auto targetRuntime = fbhermes::makeHermesRuntime(); + jsi::Scope sourceScope{*sourceRuntime}; + jsi::Scope targetScope{*targetRuntime}; + + auto *sourceSerialization = + jsi::castInterface(sourceRuntime.get()); + auto *targetSerialization = + jsi::castInterface(targetRuntime.get()); + auto *sourceJSONFactory = + jsi::castInterface(sourceRuntime.get()); + auto *targetJSONFactory = + jsi::castInterface(targetRuntime.get()); + auto *targetTracingHelpers = + jsi::castInterface( + targetRuntime.get()); + + if (!sourceSerialization || !targetSerialization || !sourceJSONFactory || + !targetJSONFactory || !targetTracingHelpers) { + llvh::errs() << "Runtime does not expose validation interfaces.\n"; + return false; + } + + const std::string &json = benchPayload.json; + jsi::Value sourceParsed = sourceRuntime->createValueFromJsonUtf8( + reinterpret_cast(json.data()), json.size()); + const std::string expected = stringifyValue(*sourceRuntime, sourceParsed); + + auto structuredSerialized = sourceSerialization->serialize(sourceParsed); + jsi::Value structuredTarget = + targetSerialization->deserialize(structuredSerialized); + if (!reportMismatch( + "ISerialization cross-runtime", + expected, + stringifyValue(*targetRuntime, structuredTarget))) { + return false; + } + + jsi::JSONValue extractedTree = + sourceJSONFactory->createJSONTreeFromValue(sourceParsed); + jsi::Value treeTarget = + targetJSONFactory->createValueFromJSONTree(extractedTree); + if (!reportMismatch( + "JSI JSON tree cross-runtime", + expected, + stringifyValue(*targetRuntime, treeTarget))) { + return false; + } + + ser::JSONSerializedValueEncoder encoder; + vm::SerializedValue customPayload = encoder.encode(benchPayload.value); + auto customSerialized = targetTracingHelpers->makeSerialized(customPayload); + jsi::Value customTarget = targetSerialization->deserialize(customSerialized); + if (!reportMismatch( + "Runtime-free serialized JSON tree", + expected, + stringifyValue(*targetRuntime, customTarget))) { + return false; + } + + return true; +} + +template +double timeBenchmark( + jsi::Runtime &runtime, + uint32_t warmup, + uint32_t iterations, + Func &&func) { + for (uint32_t i = 0; i < warmup; ++i) { + consumeArrayLength(runtime, func()); + } + + auto begin = std::chrono::steady_clock::now(); + for (uint32_t i = 0; i < iterations; ++i) { + consumeArrayLength(runtime, func()); + } + auto end = std::chrono::steady_clock::now(); + + return std::chrono::duration(end - begin).count(); +} + +uint32_t jsonTreeRootLength(const jsi::JSONValue &value) { + return value.kind == jsi::JSONValue::Kind::Array + ? static_cast(value.arrayValue.size()) + : 0; +} + +template +double timeJSONTreeBenchmark( + uint32_t warmup, + uint32_t iterations, + Func &&func) { + for (uint32_t i = 0; i < warmup; ++i) { + Sink += jsonTreeRootLength(func()); + } + + auto begin = std::chrono::steady_clock::now(); + for (uint32_t i = 0; i < iterations; ++i) { + Sink += jsonTreeRootLength(func()); + } + auto end = std::chrono::steady_clock::now(); + + return std::chrono::duration(end - begin).count(); +} + +template +double timeSerializedBenchmark( + const fbhermes::IHermesTracingHelpers &tracingHelpers, + uint32_t warmup, + uint32_t iterations, + Func &&func) { + for (uint32_t i = 0; i < warmup; ++i) { + auto serialized = func(); + const auto *payload = tracingHelpers.getHermesSerializedValue(*serialized); + Sink += payload->content.size(); + } + + auto begin = std::chrono::steady_clock::now(); + for (uint32_t i = 0; i < iterations; ++i) { + auto serialized = func(); + const auto *payload = tracingHelpers.getHermesSerializedValue(*serialized); + Sink += payload->content.size(); + } + auto end = std::chrono::steady_clock::now(); + + return std::chrono::duration(end - begin).count(); +} + +void consumeVMArrayLength(vm::Runtime &runtime, vm::HermesValue value) { + Sink += vm::JSArray::getLength( + vm::vmcast(value.getObject(runtime)), runtime); +} + +template +double timeVMBenchmark( + vm::Runtime &runtime, + uint32_t warmup, + uint32_t iterations, + Func &&func) { + struct : vm::Locals { + vm::PinnedValue<> value; + } lv; + vm::LocalsRAII lraii{runtime, &lv}; + + for (uint32_t i = 0; i < warmup; ++i) { + vm::GCScopeMarkerRAII marker{runtime}; + auto result = func(); + if (result == vm::ExecutionStatus::EXCEPTION) { + throw std::runtime_error("VM benchmark warmup threw"); + } + lv.value = *result; + consumeVMArrayLength(runtime, lv.value.getHermesValue()); + } + + auto begin = std::chrono::steady_clock::now(); + for (uint32_t i = 0; i < iterations; ++i) { + vm::GCScopeMarkerRAII marker{runtime}; + auto result = func(); + if (result == vm::ExecutionStatus::EXCEPTION) { + throw std::runtime_error("VM benchmark iteration threw"); + } + lv.value = *result; + consumeVMArrayLength(runtime, lv.value.getHermesValue()); + } + auto end = std::chrono::steady_clock::now(); + + return std::chrono::duration(end - begin).count(); +} + +#ifdef JSI_UNSTABLE +vm::SerializedValue clonePayload(const vm::SerializedValue &source) { + vm::SerializedValue clone; + clone.offsets = source.offsets; + clone.content = source.content; + clone.strings = source.strings; + return clone; +} +#endif + +void printResult(llvh::StringRef name, double ms, uint32_t iterations) { + llvh::outs() << name << ": total=" << ms << " ms, per_iter=" + << (ms * 1000.0 / iterations) << " us, ops_per_sec=" + << (iterations * 1000.0 / ms) << '\n'; +} + +} // namespace + +int main(int argc, char **argv) { + llvh::InitLLVM initLLVM(argc, argv); + llvh::sys::PrintStackTraceOnErrorSignal("serialization-json-bench"); + llvh::PrettyStackTraceProgram X(argc, argv); + llvh::llvm_shutdown_obj Y; + llvh::cl::ParseCommandLineOptions( + argc, argv, "Hermes JSON parse vs ISerialization benchmark\n"); + + hermes::oscompat::SigAltStackLeakSuppressor sigAltLeakSuppressor; + +#ifndef JSI_UNSTABLE + llvh::errs() << "This benchmark requires JSI_UNSTABLE.\n"; + return EXIT_FAILURE; +#else + auto runtime = fbhermes::makeHermesRuntime(); + auto *serialization = + jsi::castInterface(runtime.get()); + auto *jsonFactory = + jsi::castInterface(runtime.get()); + auto *tracingHelpers = + jsi::castInterface(runtime.get()); + + if (!serialization || !jsonFactory || !tracingHelpers) { + llvh::errs() << "Runtime does not expose serialization interfaces.\n"; + return EXIT_FAILURE; + } + + BenchPayload benchPayload = makePayload(Shape, Items, ValuesPerItem); + if (ValidateRepetitions != 0) { + for (uint32_t i = 0; i < ValidateRepetitions; ++i) { + if (!validateCrossRuntimeRoundTrips(benchPayload)) { + return EXIT_FAILURE; + } + } + llvh::outs() << "validate_repetitions=" << ValidateRepetitions + << ", shape=" << Shape << ", items=" << Items + << ", values_per_item=" << ValuesPerItem << '\n'; + return EXIT_SUCCESS; + } + + if (!validateCrossRuntimeRoundTrips(benchPayload)) { + return EXIT_FAILURE; + } + + const std::string &json = benchPayload.json; + jsi::Value parsed = runtime->createValueFromJsonUtf8( + reinterpret_cast(json.data()), json.size()); + auto serialized = serialization->serialize(parsed); + const vm::SerializedValue *payload = + tracingHelpers->getHermesSerializedValue(*serialized); + + ser::JSONSerializedValueEncoder encoder; + vm::SerializedValue customPayload = encoder.encode(benchPayload.value); + auto customSerialized = tracingHelpers->makeSerialized(customPayload); + const vm::SerializedValue *customPayloadView = + tracingHelpers->getHermesSerializedValue(*customSerialized); + ser::JSONValueMaterializer materializer; + + jsi::Value customDeserialized = + serialization->deserialize(customSerialized); + if (!checkGeneratedValue( + *runtime, customDeserialized, benchPayload.rootLength)) { + llvh::errs() << "Custom serializer produced an unexpected value.\n"; + return EXIT_FAILURE; + } + jsi::Value jsonTreeValue = + jsonFactory->createValueFromJSONTree(benchPayload.value); + if (!checkGeneratedValue(*runtime, jsonTreeValue, benchPayload.rootLength)) { + llvh::errs() << "JSON tree materializer produced an unexpected value.\n"; + return EXIT_FAILURE; + } + jsi::JSONValue extractedTree = + jsi::JSONValue::createFromValue(*runtime, parsed); + jsi::Value extractedTreeValue = + jsonFactory->createValueFromJSONTree(extractedTree); + if (!checkGeneratedValue( + *runtime, extractedTreeValue, benchPayload.rootLength)) { + llvh::errs() << "JSON tree extractor produced an unexpected value.\n"; + return EXIT_FAILURE; + } + + llvh::outs() << "payload: json_bytes=" << json.size() + << ", offsets=" << payload->offsets.size() + << ", content_bytes=" << payload->content.size() + << ", strings_bytes=" << payload->strings.size() << '\n'; + llvh::outs() << "custom_payload: offsets=" + << customPayloadView->offsets.size() + << ", content_bytes=" << customPayloadView->content.size() + << ", strings_bytes=" << customPayloadView->strings.size() + << '\n'; + llvh::outs() << "iterations=" << Iterations << ", warmup=" << Warmup + << ", shape=" << Shape << ", items=" << Items + << ", values_per_item=" << ValuesPerItem << '\n'; + + double jsonMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + return runtime->createValueFromJsonUtf8( + reinterpret_cast(json.data()), json.size()); + }); + + double serializedSerializeMs = timeSerializedBenchmark( + *tracingHelpers, Warmup, Iterations, [&]() { + return serialization->serialize(parsed); + }); + + double jsonTreeExtractMs = timeJSONTreeBenchmark( + Warmup, Iterations, [&]() -> jsi::JSONValue { + return jsi::JSONValue::createFromValue(*runtime, parsed); + }); + + double deserializeMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + return serialization->deserialize(serialized); + }); + + double cloneAndDeserializeMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + vm::SerializedValue clone = clonePayload(*payload); + auto wrapped = tracingHelpers->makeSerialized(clone); + return serialization->deserialize(wrapped); + }); + + double customDeserializeMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + return serialization->deserialize(customSerialized); + }); + + double customCloneAndDeserializeMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + vm::SerializedValue clone = clonePayload(*customPayloadView); + auto wrapped = tracingHelpers->makeSerialized(clone); + return serialization->deserialize(wrapped); + }); + + double jsonTreeMaterializeMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + return jsonFactory->createValueFromJSONTree(benchPayload.value); + }); + + double jsonTreeExtractMaterializeMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + jsi::JSONValue tree = jsi::JSONValue::createFromValue(*runtime, parsed); + return jsonFactory->createValueFromJSONTree(tree); + }); + + double serializedRoundTripMs = timeBenchmark( + *runtime, Warmup, Iterations, [&]() -> jsi::Value { + auto nextSerialized = serialization->serialize(parsed); + return serialization->deserialize(nextSerialized); + }); + + auto vmRuntime = vm::Runtime::create(vm::RuntimeConfig::Builder().build()); + vm::GCScope vmGCScope{*vmRuntime}; + struct : vm::Locals { + vm::PinnedValue<> checkValue; + } vmLv; + vm::LocalsRAII vmLRAII{*vmRuntime, &vmLv}; + + llvh::ArrayRef jsonRef{ + reinterpret_cast(json.data()), json.size()}; + auto vmCheckRes = materializer.materialize(*vmRuntime, benchPayload.value); + if (vmCheckRes == vm::ExecutionStatus::EXCEPTION) { + llvh::errs() << "JSON tree materializer produced an unexpected value.\n"; + return EXIT_FAILURE; + } + vmLv.checkValue = *vmCheckRes; + if (!vmLv.checkValue->isObject() || + vm::JSArray::getLength( + vm::vmcast( + vmLv.checkValue.getHermesValue().getObject(*vmRuntime)), + *vmRuntime) != + benchPayload.rootLength) { + llvh::errs() << "JSON tree materializer produced an unexpected value.\n"; + return EXIT_FAILURE; + } + + double vmJsonMs = timeVMBenchmark( + *vmRuntime, Warmup, Iterations, [&]() -> vm::CallResult { + return vm::runtimeJSONParseRef( + *vmRuntime, hermes::UTF16Stream(jsonRef)); + }); + + double vmTreeMaterializeMs = timeVMBenchmark( + *vmRuntime, Warmup, Iterations, [&]() -> vm::CallResult { + return materializer.materialize(*vmRuntime, benchPayload.value); + }); + + double vmCustomDeserializeMs = timeVMBenchmark( + *vmRuntime, Warmup, Iterations, [&]() -> vm::CallResult { + return vm::deserialize(*vmRuntime, *customPayloadView); + }); + + printResult("json_parse", jsonMs, Iterations); + printResult("serialized_serialize", serializedSerializeMs, Iterations); + printResult("jsi_json_tree_extract", jsonTreeExtractMs, Iterations); + printResult("serialized_deserialize_reuse", deserializeMs, Iterations); + printResult( + "serialized_clone_wrap_deserialize", cloneAndDeserializeMs, Iterations); + printResult("custom_deserialize_reuse", customDeserializeMs, Iterations); + printResult( + "custom_clone_wrap_deserialize", + customCloneAndDeserializeMs, + Iterations); + printResult("jsi_json_tree_materialize", jsonTreeMaterializeMs, Iterations); + printResult( + "jsi_json_tree_extract_materialize", + jsonTreeExtractMaterializeMs, + Iterations); + printResult("serialized_round_trip", serializedRoundTripMs, Iterations); + printResult("vm_json_parse", vmJsonMs, Iterations); + printResult("vm_json_tree_materialize", vmTreeMaterializeMs, Iterations); + printResult("vm_custom_deserialize", vmCustomDeserializeMs, Iterations); + llvh::outs() << "sink=" << Sink << '\n'; + return EXIT_SUCCESS; +#endif +}