From 45f95f21c5f8a388983b704774bfe19fd004db9e Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Wed, 20 May 2026 19:28:39 +0200 Subject: [PATCH 1/6] Add JSI JSON tree materialization benchmark --- API/hermes/CMakeLists.txt | 1 + API/hermes/hermes.cpp | 218 ++++++ API/jsi/jsi/jsi.cpp | 10 + API/jsi/jsi/jsi.h | 129 ++++ .../HermesJSONSerializedValueEncoder.h | 275 +++++++ experimental/HermesJSONValueMaterializer.h | 232 ++++++ tools/hvm-bench/CMakeLists.txt | 9 + tools/hvm-bench/serialization-json-bench.cpp | 702 ++++++++++++++++++ 8 files changed, 1576 insertions(+) create mode 100644 experimental/HermesJSONSerializedValueEncoder.h create mode 100644 experimental/HermesJSONValueMaterializer.h create mode 100644 tools/hvm-bench/serialization-json-bench.cpp diff --git a/API/hermes/CMakeLists.txt b/API/hermes/CMakeLists.txt index 60312255f36..2ff349af70e 100644 --- a/API/hermes/CMakeLists.txt +++ b/API/hermes/CMakeLists.txt @@ -90,6 +90,7 @@ 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}) # 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..288cd8bb59f 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -7,6 +7,8 @@ #include +#include "experimental/HermesJSONValueMaterializer.h" + #include "llvh/Support/Compiler.h" #include "hermes/ADT/ManagedChunkedList.h" @@ -33,6 +35,7 @@ #include "hermes/VM/JSError.h" #include "hermes/VM/JSLib.h" #include "hermes/VM/JSLib/JSLibStorage.h" +#include "lib/VM/JSLib/Object.h" #include "hermes/VM/JSLib/RuntimeJSONParse.h" #include "hermes/VM/JSTypedArray.h" #include "hermes/VM/NativeState.h" @@ -249,6 +252,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 +704,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 +1584,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 +1597,211 @@ jsi::ICast *HermesRuntimeImpl::castInterface(const jsi::UUID &interfaceUUID) { return nullptr; } +namespace { + +jsi::JSONValue jsonTreeStringToJSONValue( + vm::Runtime &runtime, + vm::Handle str) { + auto view = vm::StringPrimitive::createStringView(runtime, str); + if (view.isASCII()) { + return jsi::JSONValue::asciiString( + std::string{view.castToCharPtr(), view.length()}); + } + + return jsi::JSONValue::utf16String( + std::u16string{view.castToChar16Ptr(), view.length()}); +} + +std::string jsonTreeStringToUTF8( + vm::Runtime &runtime, + vm::Handle str) { + auto view = vm::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, + vm::Handle object) { + for (auto ancestor : ancestors) { + if (*ancestor == *object) { + return true; + } + } + return false; +} + +class JSONTreeAncestorScope { + public: + JSONTreeAncestorScope( + std::vector> &ancestors, + vm::Handle object) + : ancestors_(ancestors) { + ancestors_.push_back(object); + } + + ~JSONTreeAncestorScope() { + ancestors_.pop_back(); + } + + private: + std::vector> &ancestors_; +}; + +vm::CallResult createJSONTreeFromHermesValue( + vm::Runtime &runtime, + vm::Handle<> value, + std::vector> &ancestors) { + if (value->isUndefined() || value->isNull()) { + return jsi::JSONValue::null(); + } + if (value->isBool()) { + return jsi::JSONValue::boolean(value->getBool()); + } + if (value->isNumber()) { + const double number = value->getNumber(); + return std::isfinite(number) ? jsi::JSONValue::number(number) + : jsi::JSONValue::null(); + } + if (value->isString()) { + return jsonTreeStringToJSONValue( + runtime, vm::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 vm::Locals { + vm::PinnedValue object; + vm::PinnedValue array; + vm::PinnedValue keys; + vm::PinnedValue<> key; + vm::PinnedValue<> child; + } lv; + vm::LocalsRAII lraii(runtime, &lv); + + lv.object = vm::vmcast(*value); + if (vm::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}; + + vm::Handle objectHandle = lv.object; + if (auto array = vm::Handle::dyn_vmcast(objectHandle)) { + const uint32_t len = vm::JSArray::getLength(*array, runtime); + std::vector result; + result.reserve(len); + + for (uint32_t i = 0; i < len; ++i) { + vm::GCScopeMarkerRAII marker{runtime}; + auto element = array->at(runtime, i); + if (element.isEmpty()) { + result.push_back(jsi::JSONValue::null()); + continue; + } + + lv.child = element.unboxToHV(runtime); + auto childRes = createJSONTreeFromHermesValue( + runtime, lv.child, ancestors); + if (childRes == vm::ExecutionStatus::EXCEPTION) { + return vm::ExecutionStatus::EXCEPTION; + } + result.push_back(std::move(*childRes)); + } + + return jsi::JSONValue::array(std::move(result)); + } + + auto keysRes = vm::enumerableOwnProperties_RJS( + runtime, lv.object, vm::EnumerableOwnPropertiesKind::Key); + if (keysRes == vm::ExecutionStatus::EXCEPTION) { + return vm::ExecutionStatus::EXCEPTION; + } + lv.keys.castAndSetHermesValue(*keysRes); + + const uint32_t len = vm::JSArray::getLength(*lv.keys, runtime); + jsi::JSONValue::Object result; + result.reserve(len); + + for (uint32_t i = 0; i < len; ++i) { + vm::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 = vm::JSObject::getComputed_RJS( + lv.object, runtime, lv.key); + if (propRes == vm::ExecutionStatus::EXCEPTION) { + return vm::ExecutionStatus::EXCEPTION; + } + lv.child = std::move(*propRes); + + auto childRes = createJSONTreeFromHermesValue( + runtime, lv.child, ancestors); + if (childRes == vm::ExecutionStatus::EXCEPTION) { + return vm::ExecutionStatus::EXCEPTION; + } + + result.emplace_back( + jsonTreeStringToUTF8( + runtime, vm::Handle::vmcast(&lv.key)), + std::move(*childRes)); + } + + return jsi::JSONValue::object(std::move(result)); +} + +} // namespace + +jsi::Value HermesRuntimeImpl::createValueFromJSONTree( + const jsi::JSONValue &value) { + vm::GCScope gcScope(runtime_); + vm::experimental::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::experimental::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; + std::vector> ancestors; + auto res = createJSONTreeFromHermesValue( + runtime_, vmHandleFromValue(value, &numberStorage), ancestors); + 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..0a271ff2281 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,107 @@ 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 = StringEncoding::ASCII; + 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 +432,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/experimental/HermesJSONSerializedValueEncoder.h b/experimental/HermesJSONSerializedValueEncoder.h new file mode 100644 index 00000000000..844fb9ac799 --- /dev/null +++ b/experimental/HermesJSONSerializedValueEncoder.h @@ -0,0 +1,275 @@ +/** + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace hermes::vm::experimental { + +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; + stringIds_.clear(); + writeValue(value); + out_ = nullptr; + 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 bool isASCII(std::string_view value) { + for (unsigned char c : value) { + if (c > 0x7f) { + return false; + } + } + return true; + } + + static void appendUTF8AsUTF16( + std::vector &buffer, + std::string_view value) { + auto appendCodeUnit = [&buffer](char16_t unit) { + appendPod(buffer, unit); + }; + + for (size_t i = 0; i < value.size();) { + const uint8_t c = static_cast(value[i++]); + uint32_t codePoint; + if (c < 0x80) { + codePoint = c; + } else if ((c >> 5) == 0x6 && i < value.size()) { + const uint8_t c1 = static_cast(value[i++]); + codePoint = ((c & 0x1f) << 6) | (c1 & 0x3f); + } else if ((c >> 4) == 0xe && i + 1 < value.size()) { + const uint8_t c1 = static_cast(value[i++]); + const uint8_t c2 = static_cast(value[i++]); + codePoint = ((c & 0x0f) << 12) | ((c1 & 0x3f) << 6) | (c2 & 0x3f); + } else if ((c >> 3) == 0x1e && i + 2 < value.size()) { + const uint8_t c1 = static_cast(value[i++]); + const uint8_t c2 = static_cast(value[i++]); + const uint8_t c3 = static_cast(value[i++]); + codePoint = ((c & 0x07) << 18) | ((c1 & 0x3f) << 12) | + ((c2 & 0x3f) << 6) | (c3 & 0x3f); + } else { + throw std::invalid_argument("Invalid UTF-8 JSON string"); + } + + if (codePoint <= 0xffff) { + appendCodeUnit(static_cast(codePoint)); + } else if (codePoint <= 0x10ffff) { + codePoint -= 0x10000; + appendCodeUnit(static_cast(0xd800 + (codePoint >> 10))); + appendCodeUnit(static_cast(0xdc00 + (codePoint & 0x3ff))); + } else { + throw std::invalid_argument("Invalid UTF-8 JSON string"); + } + } + } + + 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 uint32_t utf16LengthFromUTF8(std::string_view value) { + uint32_t len = 0; + for (size_t i = 0; i < value.size();) { + const uint8_t c = static_cast(value[i++]); + uint32_t codePoint; + if (c < 0x80) { + codePoint = c; + } else if ((c >> 5) == 0x6 && i < value.size()) { + const uint8_t c1 = static_cast(value[i++]); + codePoint = ((c & 0x1f) << 6) | (c1 & 0x3f); + } else if ((c >> 4) == 0xe && i + 1 < value.size()) { + const uint8_t c1 = static_cast(value[i++]); + const uint8_t c2 = static_cast(value[i++]); + codePoint = ((c & 0x0f) << 12) | ((c1 & 0x3f) << 6) | (c2 & 0x3f); + } else if ((c >> 3) == 0x1e && i + 2 < value.size()) { + const uint8_t c1 = static_cast(value[i++]); + const uint8_t c2 = static_cast(value[i++]); + const uint8_t c3 = static_cast(value[i++]); + codePoint = ((c & 0x07) << 18) | ((c1 & 0x3f) << 12) | + ((c2 & 0x3f) << 6) | (c3 & 0x3f); + } else { + throw std::invalid_argument("Invalid UTF-8 JSON string"); + } + len += codePoint <= 0xffff ? 1 : 2; + } + return len; + } + + 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 = out_->offsets.size(); + stringIds_.emplace(std::move(key), id); + out_->offsets.push_back(out_->strings.size()); + + const bool ascii = value.stringEncoding == JSONValue::StringEncoding::ASCII; + out_->strings.push_back(ascii ? 1 : 0); + const uint32_t length = ascii + ? static_cast(value.stringValue.size()) + : value.stringEncoding == JSONValue::StringEncoding::UTF16 + ? static_cast(value.utf16StringValue.size()) + : utf16LengthFromUTF8(value.stringValue); + 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 { + appendUTF8AsUTF16(out_->strings, value.stringValue); + } + } + + 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 = out_->offsets.size(); + out_->offsets.push_back(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::experimental diff --git a/experimental/HermesJSONValueMaterializer.h b/experimental/HermesJSONValueMaterializer.h new file mode 100644 index 00000000000..9e9ba104cc6 --- /dev/null +++ b/experimental/HermesJSONValueMaterializer.h @@ -0,0 +1,232 @@ +/** + * 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 "HermesJSONSerializedValueEncoder.h" + +#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 "llvh/ADT/ArrayRef.h" + +#include +#include +#include +#include +#include +#include + +namespace hermes::vm::experimental { + +/// 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 non-ASCII UTF-8 still + /// goes 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: + 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::experimental diff --git a/tools/hvm-bench/CMakeLists.txt b/tools/hvm-bench/CMakeLists.txt index e070f677766..84bd648dc4d 100644 --- a/tools/hvm-bench/CMakeLists.txt +++ b/tools/hvm-bench/CMakeLists.txt @@ -8,3 +8,12 @@ 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 ${CMAKE_SOURCE_DIR}) +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..9b87f0fe568 --- /dev/null +++ b/tools/hvm-bench/serialization-json-bench.cpp @@ -0,0 +1,702 @@ +/* + * 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 "experimental/HermesJSONValueMaterializer.h" +#include "experimental/HermesJSONSerializedValueEncoder.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::experimental; +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 LeakCheckRepetitions{ + "leak-check-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 (LeakCheckRepetitions != 0) { + for (uint32_t i = 0; i < LeakCheckRepetitions; ++i) { + if (!validateCrossRuntimeRoundTrips(benchPayload)) { + return EXIT_FAILURE; + } + } + llvh::outs() << "leak_check_repetitions=" << LeakCheckRepetitions + << ", 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}; + llvh::ArrayRef jsonRef{ + reinterpret_cast(json.data()), json.size()}; + auto vmCheckRes = materializer.materialize(*vmRuntime, benchPayload.value); + if (vmCheckRes == vm::ExecutionStatus::EXCEPTION || + !vmCheckRes->isObject() || + vm::JSArray::getLength( + vm::vmcast(vmCheckRes->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 +} From 300ce54e45b87daea94d51d61346700659c459ea Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Wed, 20 May 2026 19:41:52 +0200 Subject: [PATCH 2/6] Polish JSON tree serialization benchmark --- API/hermes/CMakeLists.txt | 5 +- API/hermes/hermes.cpp | 4 +- API/jsi/jsi/jsi.h | 3 +- .../HermesJSONSerializedValueEncoder.h | 139 ++++++++---------- experimental/HermesJSONValueMaterializer.h | 9 +- tools/hvm-bench/CMakeLists.txt | 4 +- tools/hvm-bench/serialization-json-bench.cpp | 14 +- 7 files changed, 86 insertions(+), 92 deletions(-) diff --git a/API/hermes/CMakeLists.txt b/API/hermes/CMakeLists.txt index 2ff349af70e..bffac74c0c2 100644 --- a/API/hermes/CMakeLists.txt +++ b/API/hermes/CMakeLists.txt @@ -90,7 +90,10 @@ 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}) +target_include_directories(hermesapi_obj PRIVATE + ${PROJECT_SOURCE_DIR}/experimental + ${PROJECT_SOURCE_DIR}/lib/VM/JSLib + ) # 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 288cd8bb59f..5cd8d38e513 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -7,7 +7,7 @@ #include -#include "experimental/HermesJSONValueMaterializer.h" +#include "HermesJSONValueMaterializer.h" #include "llvh/Support/Compiler.h" @@ -35,7 +35,7 @@ #include "hermes/VM/JSError.h" #include "hermes/VM/JSLib.h" #include "hermes/VM/JSLib/JSLibStorage.h" -#include "lib/VM/JSLib/Object.h" +#include "Object.h" #include "hermes/VM/JSLib/RuntimeJSONParse.h" #include "hermes/VM/JSTypedArray.h" #include "hermes/VM/NativeState.h" diff --git a/API/jsi/jsi/jsi.h b/API/jsi/jsi/jsi.h index 0a271ff2281..1f25e5c0bf9 100644 --- a/API/jsi/jsi/jsi.h +++ b/API/jsi/jsi/jsi.h @@ -256,7 +256,8 @@ struct JSI_EXPORT JSONValue { static JSONValue asciiString(std::string value) { JSONValue result; result.kind = Kind::String; - result.stringEncoding = StringEncoding::ASCII; + result.stringEncoding = + isASCII(value) ? StringEncoding::ASCII : StringEncoding::UTF8; result.stringValue = std::move(value); return result; } diff --git a/experimental/HermesJSONSerializedValueEncoder.h b/experimental/HermesJSONSerializedValueEncoder.h index 844fb9ac799..71049a9dd16 100644 --- a/experimental/HermesJSONSerializedValueEncoder.h +++ b/experimental/HermesJSONSerializedValueEncoder.h @@ -10,6 +10,8 @@ #include "hermes/VM/SerializedValue.h" #include "jsi/jsi.h" +#include "llvh/Support/ConvertUTF.h" + #include #include #include @@ -33,9 +35,18 @@ class JSONSerializedValueEncoder { 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); - out_ = nullptr; return result; } @@ -60,54 +71,21 @@ class JSONSerializedValueEncoder { appendPod(buffer, bits); } - static bool isASCII(std::string_view value) { - for (unsigned char c : value) { - if (c > 0x7f) { - return false; - } + static uint32_t checkedUint32(size_t value, const char *message) { + if (value > std::numeric_limits::max()) { + throw std::overflow_error(message); } - return true; + return static_cast(value); } - static void appendUTF8AsUTF16( - std::vector &buffer, - std::string_view value) { - auto appendCodeUnit = [&buffer](char16_t unit) { - appendPod(buffer, unit); - }; - - for (size_t i = 0; i < value.size();) { - const uint8_t c = static_cast(value[i++]); - uint32_t codePoint; - if (c < 0x80) { - codePoint = c; - } else if ((c >> 5) == 0x6 && i < value.size()) { - const uint8_t c1 = static_cast(value[i++]); - codePoint = ((c & 0x1f) << 6) | (c1 & 0x3f); - } else if ((c >> 4) == 0xe && i + 1 < value.size()) { - const uint8_t c1 = static_cast(value[i++]); - const uint8_t c2 = static_cast(value[i++]); - codePoint = ((c & 0x0f) << 12) | ((c1 & 0x3f) << 6) | (c2 & 0x3f); - } else if ((c >> 3) == 0x1e && i + 2 < value.size()) { - const uint8_t c1 = static_cast(value[i++]); - const uint8_t c2 = static_cast(value[i++]); - const uint8_t c3 = static_cast(value[i++]); - codePoint = ((c & 0x07) << 18) | ((c1 & 0x3f) << 12) | - ((c2 & 0x3f) << 6) | (c3 & 0x3f); - } else { - throw std::invalid_argument("Invalid UTF-8 JSON string"); - } + uint32_t nextRecordID() const { + return checkedUint32( + out_->offsets.size(), "Serialized value has too many records"); + } - if (codePoint <= 0xffff) { - appendCodeUnit(static_cast(codePoint)); - } else if (codePoint <= 0x10ffff) { - codePoint -= 0x10000; - appendCodeUnit(static_cast(0xd800 + (codePoint >> 10))); - appendCodeUnit(static_cast(0xdc00 + (codePoint & 0x3ff))); - } else { - throw std::invalid_argument("Invalid UTF-8 JSON string"); - } - } + void pushOffset(size_t offset) { + out_->offsets.push_back( + checkedUint32(offset, "Serialized value buffer is too large")); } static void appendUTF16( @@ -119,32 +97,31 @@ class JSONSerializedValueEncoder { reinterpret_cast(value.data() + value.size())); } - static uint32_t utf16LengthFromUTF8(std::string_view value) { - uint32_t len = 0; - for (size_t i = 0; i < value.size();) { - const uint8_t c = static_cast(value[i++]); - uint32_t codePoint; - if (c < 0x80) { - codePoint = c; - } else if ((c >> 5) == 0x6 && i < value.size()) { - const uint8_t c1 = static_cast(value[i++]); - codePoint = ((c & 0x1f) << 6) | (c1 & 0x3f); - } else if ((c >> 4) == 0xe && i + 1 < value.size()) { - const uint8_t c1 = static_cast(value[i++]); - const uint8_t c2 = static_cast(value[i++]); - codePoint = ((c & 0x0f) << 12) | ((c1 & 0x3f) << 6) | (c2 & 0x3f); - } else if ((c >> 3) == 0x1e && i + 2 < value.size()) { - const uint8_t c1 = static_cast(value[i++]); - const uint8_t c2 = static_cast(value[i++]); - const uint8_t c3 = static_cast(value[i++]); - codePoint = ((c & 0x07) << 18) | ((c1 & 0x3f) << 12) | - ((c2 & 0x3f) << 6) | (c3 & 0x3f); - } else { - throw std::invalid_argument("Invalid UTF-8 JSON string"); - } - len += codePoint <= 0xffff ? 1 : 2; + 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"); } - return len; + + out.resize(reinterpret_cast(targetStart) - out.data()); + return out; } static std::string makeStringKey(const JSONValue &value) { @@ -167,17 +144,23 @@ class JSONSerializedValueEncoder { return it->second; } - const uint32_t id = out_->offsets.size(); + const uint32_t id = nextRecordID(); stringIds_.emplace(std::move(key), id); - out_->offsets.push_back(out_->strings.size()); + 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 - ? static_cast(value.stringValue.size()) + ? checkedUint32(value.stringValue.size(), "JSON string is too large") : value.stringEncoding == JSONValue::StringEncoding::UTF16 - ? static_cast(value.utf16StringValue.size()) - : utf16LengthFromUTF8(value.stringValue); + ? checkedUint32( + value.utf16StringValue.size(), "JSON string is too large") + : checkedUint32(utf16Storage.size(), "JSON string is too large"); appendPod(out_->strings, length); if (ascii) { @@ -192,7 +175,7 @@ class JSONSerializedValueEncoder { if (value.stringEncoding == JSONValue::StringEncoding::UTF16) { appendUTF16(out_->strings, value.utf16StringValue); } else { - appendUTF8AsUTF16(out_->strings, value.stringValue); + appendUTF16(out_->strings, utf16Storage); } } @@ -237,8 +220,8 @@ class JSONSerializedValueEncoder { } uint32_t beginObjectRecord(SerializedValue::Type type) { - const uint32_t id = out_->offsets.size(); - out_->offsets.push_back(out_->content.size()); + const uint32_t id = nextRecordID(); + pushOffset(out_->content.size()); out_->content.push_back(typeByte(type)); appendPod(out_->content, id); return id; diff --git a/experimental/HermesJSONValueMaterializer.h b/experimental/HermesJSONValueMaterializer.h index 9e9ba104cc6..e0b4ab0c11c 100644 --- a/experimental/HermesJSONValueMaterializer.h +++ b/experimental/HermesJSONValueMaterializer.h @@ -20,6 +20,7 @@ #include "llvh/ADT/ArrayRef.h" #include +#include #include #include #include @@ -47,8 +48,9 @@ class JSONValueMaterializer { /// /// 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 non-ASCII UTF-8 still - /// goes through Hermes' UTF-8 decoder. + /// 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) { @@ -119,6 +121,9 @@ class JSONValueMaterializer { 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); diff --git a/tools/hvm-bench/CMakeLists.txt b/tools/hvm-bench/CMakeLists.txt index 84bd648dc4d..828132d09cf 100644 --- a/tools/hvm-bench/CMakeLists.txt +++ b/tools/hvm-bench/CMakeLists.txt @@ -15,5 +15,7 @@ add_hermes_tool(serialization-json-bench ${ALL_HEADER_FILES} LINK_OBJLIBS hermesvm_a timerStats ) -target_include_directories(serialization-json-bench PRIVATE ${CMAKE_SOURCE_DIR}) +target_include_directories(serialization-json-bench PRIVATE + ${PROJECT_SOURCE_DIR}/experimental + ) set(HERMES_ENABLE_EH OFF) diff --git a/tools/hvm-bench/serialization-json-bench.cpp b/tools/hvm-bench/serialization-json-bench.cpp index 9b87f0fe568..166ae8fc6ac 100644 --- a/tools/hvm-bench/serialization-json-bench.cpp +++ b/tools/hvm-bench/serialization-json-bench.cpp @@ -21,8 +21,8 @@ #include "hermes/VM/SerializedValue.h" #include "hermes/hermes.h" -#include "experimental/HermesJSONValueMaterializer.h" -#include "experimental/HermesJSONSerializedValueEncoder.h" +#include "HermesJSONSerializedValueEncoder.h" +#include "HermesJSONValueMaterializer.h" #include "llvh/Support/CommandLine.h" #include "llvh/Support/InitLLVM.h" @@ -67,8 +67,8 @@ static llvh::cl::opt ValuesPerItem{ llvh::cl::init(8), llvh::cl::desc("Values per item, or nesting depth for --shape=deep")}; -static llvh::cl::opt LeakCheckRepetitions{ - "leak-check-repetitions", +static llvh::cl::opt ValidateRepetitions{ + "validate-repetitions", llvh::cl::init(0), llvh::cl::desc("Run cross-runtime validation repeatedly and exit")}; @@ -522,13 +522,13 @@ int main(int argc, char **argv) { } BenchPayload benchPayload = makePayload(Shape, Items, ValuesPerItem); - if (LeakCheckRepetitions != 0) { - for (uint32_t i = 0; i < LeakCheckRepetitions; ++i) { + if (ValidateRepetitions != 0) { + for (uint32_t i = 0; i < ValidateRepetitions; ++i) { if (!validateCrossRuntimeRoundTrips(benchPayload)) { return EXIT_FAILURE; } } - llvh::outs() << "leak_check_repetitions=" << LeakCheckRepetitions + llvh::outs() << "validate_repetitions=" << ValidateRepetitions << ", shape=" << Shape << ", items=" << Items << ", values_per_item=" << ValuesPerItem << '\n'; return EXIT_SUCCESS; From d14aae828d50a44b99497f414fa8895ace8f48d8 Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Thu, 21 May 2026 10:25:20 +0200 Subject: [PATCH 3/6] Root benchmark VM materialization check --- tools/hvm-bench/serialization-json-bench.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/hvm-bench/serialization-json-bench.cpp b/tools/hvm-bench/serialization-json-bench.cpp index 166ae8fc6ac..b4033ce5fa7 100644 --- a/tools/hvm-bench/serialization-json-bench.cpp +++ b/tools/hvm-bench/serialization-json-bench.cpp @@ -647,13 +647,23 @@ int main(int argc, char **argv) { 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 || - !vmCheckRes->isObject() || + 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(vmCheckRes->getObject(*vmRuntime)), + vm::vmcast( + vmLv.checkValue.getHermesValue().getObject(*vmRuntime)), *vmRuntime) != benchPayload.rootLength) { llvh::errs() << "JSON tree materializer produced an unexpected value.\n"; From d8226855b67fda396c9920902028cd0d37727a6c Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Fri, 22 May 2026 19:31:31 +0200 Subject: [PATCH 4/6] Match VM style for JSLib object include --- API/hermes/CMakeLists.txt | 2 +- API/hermes/hermes.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/API/hermes/CMakeLists.txt b/API/hermes/CMakeLists.txt index bffac74c0c2..18db2bb7038 100644 --- a/API/hermes/CMakeLists.txt +++ b/API/hermes/CMakeLists.txt @@ -92,7 +92,7 @@ add_hermes_library(hermesapi target_include_directories(hermesapi_obj PUBLIC ..) target_include_directories(hermesapi_obj PRIVATE ${PROJECT_SOURCE_DIR}/experimental - ${PROJECT_SOURCE_DIR}/lib/VM/JSLib + ${PROJECT_SOURCE_DIR}/lib/VM ) # Pass core extensions flag to C++ code. diff --git a/API/hermes/hermes.cpp b/API/hermes/hermes.cpp index 5cd8d38e513..74294a4f510 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -35,7 +35,7 @@ #include "hermes/VM/JSError.h" #include "hermes/VM/JSLib.h" #include "hermes/VM/JSLib/JSLibStorage.h" -#include "Object.h" +#include "JSLib/Object.h" #include "hermes/VM/JSLib/RuntimeJSONParse.h" #include "hermes/VM/JSTypedArray.h" #include "hermes/VM/NativeState.h" From 1ef5d6eb9d0468220cf94c5e3a57e656796117f1 Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Fri, 22 May 2026 19:34:17 +0200 Subject: [PATCH 5/6] Move JSON tree helpers next to VM serialization --- API/hermes/CMakeLists.txt | 1 - API/hermes/hermes.cpp | 6 +++--- .../VM/JSONSerializedValueEncoder.h | 4 ++-- .../VM/JSONValueMaterializer.h | 9 +++++---- tools/hvm-bench/CMakeLists.txt | 2 +- tools/hvm-bench/serialization-json-bench.cpp | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) rename experimental/HermesJSONSerializedValueEncoder.h => lib/VM/JSONSerializedValueEncoder.h (99%) rename experimental/HermesJSONValueMaterializer.h => lib/VM/JSONValueMaterializer.h (98%) diff --git a/API/hermes/CMakeLists.txt b/API/hermes/CMakeLists.txt index 18db2bb7038..f7b6918c9f0 100644 --- a/API/hermes/CMakeLists.txt +++ b/API/hermes/CMakeLists.txt @@ -91,7 +91,6 @@ add_hermes_library(hermesapi ) target_include_directories(hermesapi_obj PUBLIC ..) target_include_directories(hermesapi_obj PRIVATE - ${PROJECT_SOURCE_DIR}/experimental ${PROJECT_SOURCE_DIR}/lib/VM ) diff --git a/API/hermes/hermes.cpp b/API/hermes/hermes.cpp index 74294a4f510..7643ce200dc 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -7,7 +7,7 @@ #include -#include "HermesJSONValueMaterializer.h" +#include "JSONValueMaterializer.h" #include "llvh/Support/Compiler.h" @@ -1776,7 +1776,7 @@ vm::CallResult createJSONTreeFromHermesValue( jsi::Value HermesRuntimeImpl::createValueFromJSONTree( const jsi::JSONValue &value) { vm::GCScope gcScope(runtime_); - vm::experimental::JSONValueMaterializer materializer; + vm::JSONValueMaterializer materializer; auto res = materializer.materialize(runtime_, value); checkStatus(res.getStatus()); return valueFromHermesValue(*res); @@ -1785,7 +1785,7 @@ jsi::Value HermesRuntimeImpl::createValueFromJSONTree( jsi::Value HermesRuntimeImpl::createValueFromJSONTreeAndConsume( jsi::JSONValue &value) { vm::GCScope gcScope(runtime_); - vm::experimental::JSONValueMaterializer materializer; + vm::JSONValueMaterializer materializer; auto res = materializer.materializeAndConsume(runtime_, value); checkStatus(res.getStatus()); return valueFromHermesValue(*res); diff --git a/experimental/HermesJSONSerializedValueEncoder.h b/lib/VM/JSONSerializedValueEncoder.h similarity index 99% rename from experimental/HermesJSONSerializedValueEncoder.h rename to lib/VM/JSONSerializedValueEncoder.h index 71049a9dd16..51e34362f60 100644 --- a/experimental/HermesJSONSerializedValueEncoder.h +++ b/lib/VM/JSONSerializedValueEncoder.h @@ -24,7 +24,7 @@ #include #include -namespace hermes::vm::experimental { +namespace hermes::vm { using JSONValue = ::facebook::jsi::JSONValue; @@ -255,4 +255,4 @@ class JSONSerializedValueEncoder { } }; -} // namespace hermes::vm::experimental +} // namespace hermes::vm diff --git a/experimental/HermesJSONValueMaterializer.h b/lib/VM/JSONValueMaterializer.h similarity index 98% rename from experimental/HermesJSONValueMaterializer.h rename to lib/VM/JSONValueMaterializer.h index e0b4ab0c11c..7a9d25f6d92 100644 --- a/experimental/HermesJSONValueMaterializer.h +++ b/lib/VM/JSONValueMaterializer.h @@ -7,8 +7,6 @@ #pragma once -#include "HermesJSONSerializedValueEncoder.h" - #include "hermes/VM/Handle.h" #include "hermes/VM/JSArray.h" #include "hermes/VM/JSObject.h" @@ -16,6 +14,7 @@ #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" @@ -27,7 +26,9 @@ #include #include -namespace hermes::vm::experimental { +namespace hermes::vm { + +using JSONValue = ::facebook::jsi::JSONValue; /// Materializes a JSONValue tree directly into Hermes VM values. /// @@ -234,4 +235,4 @@ class JSONValueMaterializer { } }; -} // namespace hermes::vm::experimental +} // namespace hermes::vm diff --git a/tools/hvm-bench/CMakeLists.txt b/tools/hvm-bench/CMakeLists.txt index 828132d09cf..213ade86127 100644 --- a/tools/hvm-bench/CMakeLists.txt +++ b/tools/hvm-bench/CMakeLists.txt @@ -16,6 +16,6 @@ add_hermes_tool(serialization-json-bench LINK_OBJLIBS hermesvm_a timerStats ) target_include_directories(serialization-json-bench PRIVATE - ${PROJECT_SOURCE_DIR}/experimental + ${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 index b4033ce5fa7..c8202239537 100644 --- a/tools/hvm-bench/serialization-json-bench.cpp +++ b/tools/hvm-bench/serialization-json-bench.cpp @@ -21,8 +21,8 @@ #include "hermes/VM/SerializedValue.h" #include "hermes/hermes.h" -#include "HermesJSONSerializedValueEncoder.h" -#include "HermesJSONValueMaterializer.h" +#include "JSONSerializedValueEncoder.h" +#include "JSONValueMaterializer.h" #include "llvh/Support/CommandLine.h" #include "llvh/Support/InitLLVM.h" @@ -42,7 +42,7 @@ namespace fbhermes = facebook::hermes; namespace jsi = facebook::jsi; -namespace ser = hermes::vm::experimental; +namespace ser = hermes::vm; namespace vm = ::hermes::vm; namespace { From f3cbc9bc433d13138117ed21965ea5674646749c Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Fri, 22 May 2026 19:43:35 +0200 Subject: [PATCH 6/6] Move JSON tree extraction into VM helper --- API/hermes/hermes.cpp | 184 +--------------- lib/VM/CMakeLists.txt | 1 + lib/VM/JSONValue.cpp | 203 ++++++++++++++++++ .../{JSONValueMaterializer.h => JSONValue.h} | 10 + tools/hvm-bench/serialization-json-bench.cpp | 2 +- 5 files changed, 218 insertions(+), 182 deletions(-) create mode 100644 lib/VM/JSONValue.cpp rename lib/VM/{JSONValueMaterializer.h => JSONValue.h} (95%) diff --git a/API/hermes/hermes.cpp b/API/hermes/hermes.cpp index 7643ce200dc..6060c2e0e9b 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -7,7 +7,7 @@ #include -#include "JSONValueMaterializer.h" +#include "JSONValue.h" #include "llvh/Support/Compiler.h" @@ -35,7 +35,6 @@ #include "hermes/VM/JSError.h" #include "hermes/VM/JSLib.h" #include "hermes/VM/JSLib/JSLibStorage.h" -#include "JSLib/Object.h" #include "hermes/VM/JSLib/RuntimeJSONParse.h" #include "hermes/VM/JSTypedArray.h" #include "hermes/VM/NativeState.h" @@ -1597,182 +1596,6 @@ jsi::ICast *HermesRuntimeImpl::castInterface(const jsi::UUID &interfaceUUID) { return nullptr; } -namespace { - -jsi::JSONValue jsonTreeStringToJSONValue( - vm::Runtime &runtime, - vm::Handle str) { - auto view = vm::StringPrimitive::createStringView(runtime, str); - if (view.isASCII()) { - return jsi::JSONValue::asciiString( - std::string{view.castToCharPtr(), view.length()}); - } - - return jsi::JSONValue::utf16String( - std::u16string{view.castToChar16Ptr(), view.length()}); -} - -std::string jsonTreeStringToUTF8( - vm::Runtime &runtime, - vm::Handle str) { - auto view = vm::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, - vm::Handle object) { - for (auto ancestor : ancestors) { - if (*ancestor == *object) { - return true; - } - } - return false; -} - -class JSONTreeAncestorScope { - public: - JSONTreeAncestorScope( - std::vector> &ancestors, - vm::Handle object) - : ancestors_(ancestors) { - ancestors_.push_back(object); - } - - ~JSONTreeAncestorScope() { - ancestors_.pop_back(); - } - - private: - std::vector> &ancestors_; -}; - -vm::CallResult createJSONTreeFromHermesValue( - vm::Runtime &runtime, - vm::Handle<> value, - std::vector> &ancestors) { - if (value->isUndefined() || value->isNull()) { - return jsi::JSONValue::null(); - } - if (value->isBool()) { - return jsi::JSONValue::boolean(value->getBool()); - } - if (value->isNumber()) { - const double number = value->getNumber(); - return std::isfinite(number) ? jsi::JSONValue::number(number) - : jsi::JSONValue::null(); - } - if (value->isString()) { - return jsonTreeStringToJSONValue( - runtime, vm::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 vm::Locals { - vm::PinnedValue object; - vm::PinnedValue array; - vm::PinnedValue keys; - vm::PinnedValue<> key; - vm::PinnedValue<> child; - } lv; - vm::LocalsRAII lraii(runtime, &lv); - - lv.object = vm::vmcast(*value); - if (vm::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}; - - vm::Handle objectHandle = lv.object; - if (auto array = vm::Handle::dyn_vmcast(objectHandle)) { - const uint32_t len = vm::JSArray::getLength(*array, runtime); - std::vector result; - result.reserve(len); - - for (uint32_t i = 0; i < len; ++i) { - vm::GCScopeMarkerRAII marker{runtime}; - auto element = array->at(runtime, i); - if (element.isEmpty()) { - result.push_back(jsi::JSONValue::null()); - continue; - } - - lv.child = element.unboxToHV(runtime); - auto childRes = createJSONTreeFromHermesValue( - runtime, lv.child, ancestors); - if (childRes == vm::ExecutionStatus::EXCEPTION) { - return vm::ExecutionStatus::EXCEPTION; - } - result.push_back(std::move(*childRes)); - } - - return jsi::JSONValue::array(std::move(result)); - } - - auto keysRes = vm::enumerableOwnProperties_RJS( - runtime, lv.object, vm::EnumerableOwnPropertiesKind::Key); - if (keysRes == vm::ExecutionStatus::EXCEPTION) { - return vm::ExecutionStatus::EXCEPTION; - } - lv.keys.castAndSetHermesValue(*keysRes); - - const uint32_t len = vm::JSArray::getLength(*lv.keys, runtime); - jsi::JSONValue::Object result; - result.reserve(len); - - for (uint32_t i = 0; i < len; ++i) { - vm::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 = vm::JSObject::getComputed_RJS( - lv.object, runtime, lv.key); - if (propRes == vm::ExecutionStatus::EXCEPTION) { - return vm::ExecutionStatus::EXCEPTION; - } - lv.child = std::move(*propRes); - - auto childRes = createJSONTreeFromHermesValue( - runtime, lv.child, ancestors); - if (childRes == vm::ExecutionStatus::EXCEPTION) { - return vm::ExecutionStatus::EXCEPTION; - } - - result.emplace_back( - jsonTreeStringToUTF8( - runtime, vm::Handle::vmcast(&lv.key)), - std::move(*childRes)); - } - - return jsi::JSONValue::object(std::move(result)); -} - -} // namespace - jsi::Value HermesRuntimeImpl::createValueFromJSONTree( const jsi::JSONValue &value) { vm::GCScope gcScope(runtime_); @@ -1795,9 +1618,8 @@ jsi::JSONValue HermesRuntimeImpl::createJSONTreeFromValue( const jsi::Value &value) { vm::GCScope gcScope(runtime_); vm::PinnedHermesValue numberStorage; - std::vector> ancestors; - auto res = createJSONTreeFromHermesValue( - runtime_, vmHandleFromValue(value, &numberStorage), ancestors); + auto res = vm::createJSONValueFromHermesValue( + runtime_, vmHandleFromValue(value, &numberStorage)); checkStatus(res.getStatus()); return std::move(*res); } 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/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/JSONValueMaterializer.h b/lib/VM/JSONValue.h similarity index 95% rename from lib/VM/JSONValueMaterializer.h rename to lib/VM/JSONValue.h index 7a9d25f6d92..80c08e453dc 100644 --- a/lib/VM/JSONValueMaterializer.h +++ b/lib/VM/JSONValue.h @@ -25,11 +25,21 @@ #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 diff --git a/tools/hvm-bench/serialization-json-bench.cpp b/tools/hvm-bench/serialization-json-bench.cpp index c8202239537..71a84675d41 100644 --- a/tools/hvm-bench/serialization-json-bench.cpp +++ b/tools/hvm-bench/serialization-json-bench.cpp @@ -22,7 +22,7 @@ #include "hermes/hermes.h" #include "JSONSerializedValueEncoder.h" -#include "JSONValueMaterializer.h" +#include "JSONValue.h" #include "llvh/Support/CommandLine.h" #include "llvh/Support/InitLLVM.h"