From 200701acda217eed87dc70cb3968f326fbf5dc4e Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 13:13:27 +0300 Subject: [PATCH 01/21] added instance cache + fix tests --- .../kphp-light/stdlib/instance-cache.txt | 5 +- compiler/code-gen/declarations.cpp | 23 +- compiler/pipes/final-check.cpp | 13 +- .../class-instance/class-instance-decl.inl | 30 ++ .../core/class-instance/class-instance.inl | 31 +++ .../core/core-types/decl/array_decl.inl | 41 ++- .../core/core-types/decl/string_decl.inl | 16 +- .../core/core-types/definition/array.inl | 92 ++++-- .../core/core-types/definition/string.inl | 33 ++- .../visitors/instance-deep-basic-visitor.h | 114 ++++++++ runtime-light/k2-platform/k2-api.h | 39 +++ runtime-light/k2-platform/k2-header.h | 43 ++- runtime-light/stdlib/array/array-functions.h | 21 +- .../instance-cache/instance-cache-functions.h | 203 ++++++-------- .../instance-cache/instance-cache-state.h | 7 +- .../visitors/instance-deep-copy-visitor.h | 127 +++++++++ .../instance-deep-size-count-visitor.h | 89 ++++++ .../stdlib/system/system-functions.h | 1 + runtime/instance-copy-processor.h | 134 ++------- .../10_instance_cache_abstract_error.php | 3 +- .../11_instance_cache_polymprphic_field.php | 3 +- ...instance_cache_polymprphic_field_error.php | 2 +- .../13_instance_cache_serializable.php | 263 ------------------ .../phpt/instance_cache/1_instance_cache.php | 103 +++---- .../5_instance_cache_polymorphic_simple.php | 4 +- .../6_instance_cache_polymorphic_error.php | 4 +- .../7_instance_cache_interface.php | 3 +- .../8_instance_cache_interface_error.php | 4 +- .../9_instance_cache_abstract.php | 3 +- tests/phpt/interfaces/Classes/AAA.php | 4 +- .../python/tests/instance_cache/php/index.php | 76 +++-- .../tests/instance_cache/test_polymorphic.py | 1 - .../instance_cache/test_store_fetch_delete.py | 7 +- .../SharedMemoryPieceCopying/SomeContext.php | 2 - .../tests/job_workers/php/SyncJobCommand.php | 4 +- .../tests/shutdown_functions/php/index.php | 4 +- 36 files changed, 854 insertions(+), 698 deletions(-) create mode 100644 runtime-common/stdlib/visitors/instance-deep-basic-visitor.h create mode 100644 runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h create mode 100644 runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h delete mode 100644 tests/phpt/instance_cache/13_instance_cache_serializable.php diff --git a/builtin-functions/kphp-light/stdlib/instance-cache.txt b/builtin-functions/kphp-light/stdlib/instance-cache.txt index d8802bdbeb..25bb8c095a 100644 --- a/builtin-functions/kphp-light/stdlib/instance-cache.txt +++ b/builtin-functions/kphp-light/stdlib/instance-cache.txt @@ -1,13 +1,10 @@ ; -/** @kphp-extern-func-info interruptible */ function instance_cache_store(string $key, object $value, int $ttl = 0) ::: bool; -/** @kphp-extern-func-info interruptible */ function instance_cache_update_ttl(string $key, int $ttl = 0) ::: bool; -/** @kphp-extern-func-info interruptible */ function instance_cache_delete(string $key) ::: bool; diff --git a/compiler/code-gen/declarations.cpp b/compiler/code-gen/declarations.cpp index 0aa48c32da..e0c3165d4c 100644 --- a/compiler/code-gen/declarations.cpp +++ b/compiler/code-gen/declarations.cpp @@ -750,6 +750,8 @@ void ClassDeclaration::compile_get_class(CodeGenerator &W, ClassPtr klass) { void ClassDeclaration::compile_get_hash(CodeGenerator &W, ClassPtr klass) { compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, "int get_hash()", klass->get_hash()); + // static type tag: same value for every object of this class, available without an instance (unlike virtual get_hash()) + W << "constexpr static uint64_t CLASS_NAME_HASH{" << vk::murmur_hash(klass->name.data(), klass->name.size()) << "ULL};" << NL << NL; } void ClassDeclaration::compile_accept_visitor(CodeGenerator &W, ClassPtr klass, const char *visitor_type) { @@ -907,7 +909,7 @@ void ClassDeclaration::compile_accept_json_visitor(CodeGenerator &W, ClassPtr kl void ClassDeclaration::compile_accept_visitor_methods(CodeGenerator &W, ClassPtr klass) { bool need_generic_accept = - klass->need_to_array_debug_visitor || (klass->need_instance_cache_visitors && !G->is_output_mode_k2()) || (klass->need_instance_memory_estimate_visitor); + klass->need_to_array_debug_visitor || klass->need_instance_cache_visitors || (klass->need_instance_memory_estimate_visitor); if (!need_generic_accept && klass->json_encoders.empty()) { return; @@ -939,6 +941,13 @@ void ClassDeclaration::compile_accept_visitor_methods(CodeGenerator &W, ClassPtr compile_accept_visitor(W, klass, "InstanceDeepDestroyVisitor"); } + if (klass->need_instance_cache_visitors && G->is_output_mode_k2()) { + W << NL; + compile_accept_visitor(W, klass, "kphp::visitors::instance_deep_copy_visitor"); + W << NL; + compile_accept_visitor(W, klass, "kphp::visitors::instance_deep_size_count_visitor"); + } + compile_accept_json_visitor(W, klass); } @@ -962,6 +971,9 @@ void ClassDeclaration::compile_virtual_builtin_functions(CodeGenerator &W, Class compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, klass->src_name + "* virtual_builtin_clone()", "new " + klass->src_name + "{*this}"); + + compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, + klass->src_name + "* virtual_builtin_construct_at(void* ptr)", "new (ptr) " + klass->src_name + "{*this}"); } void ClassDeclaration::compile_wakeup(CodeGenerator &W, ClassPtr klass) { @@ -1059,7 +1071,7 @@ void ClassDeclaration::compile_job_worker_shared_memory_piece_methods(CodeGenera void ClassMembersDefinition::compile(CodeGenerator &W) const { bool need_generic_accept = - klass->need_to_array_debug_visitor || (klass->need_instance_cache_visitors && !G->is_output_mode_k2()) || (klass->need_instance_memory_estimate_visitor); + klass->need_to_array_debug_visitor || klass->need_instance_cache_visitors || (klass->need_instance_memory_estimate_visitor); if (!need_generic_accept && !klass->is_serializable && klass->json_encoders.empty()) { return; @@ -1100,6 +1112,13 @@ void ClassMembersDefinition::compile(CodeGenerator &W) const { compile_generic_accept_instantiations(W, klass, "InstanceDeepDestroyVisitor"); } + if (klass->need_instance_cache_visitors && G->is_output_mode_k2()) { + W << NL; + compile_generic_accept_instantiations(W, klass, "kphp::visitors::instance_deep_copy_visitor"); + W << NL; + compile_generic_accept_instantiations(W, klass, "kphp::visitors::instance_deep_size_count_visitor"); + } + W << NL; compile_accept_json_visitor(W, klass); diff --git a/compiler/pipes/final-check.cpp b/compiler/pipes/final-check.cpp index 9d6fcb96c7..243d432403 100644 --- a/compiler/pipes/final-check.cpp +++ b/compiler/pipes/final-check.cpp @@ -134,13 +134,8 @@ void check_instance_cache_fetch_call(VertexAdaptor call) { kphp_error(klass->is_immutable || klass->is_interface(), fmt_format("Can not fetch instance of mutable class {} with instance_cache_fetch call", klass->name)); - kphp_error(klass->is_serializable, - fmt_format("Can not fetch instance of non-serializable class {} with instance_cache_fetch call", klass->name)); - if (G->is_output_mode_k2()) { - // To be able to store instances in request cache - klass->deeply_require_may_be_mixed_base(); - } else { + if (!G->is_output_mode_k2()) { klass->deeply_require_instance_cache_visitor(); } } @@ -153,12 +148,6 @@ void check_instance_cache_store_call(VertexAdaptor call) { kphp_error_return(klass->is_immutable || klass->is_interface(), fmt_format("Can not store instance of mutable class {} with instance_cache_store call", klass->name)); - kphp_error_return(klass->is_serializable, fmt_format("Can not store instance of non-serializable class {} with instance_cache_store call", klass->name)); - - if (G->is_output_mode_k2()) { - // To be able to store instances in request cache - klass->deeply_require_may_be_mixed_base(); - } check_fields_ic_compatibility(klass); check_derivatives_ic_compatibility(klass); diff --git a/runtime-common/core/class-instance/class-instance-decl.inl b/runtime-common/core/class-instance/class-instance-decl.inl index 7fd781541a..6056e76f8f 100644 --- a/runtime-common/core/class-instance/class-instance-decl.inl +++ b/runtime-common/core/class-instance/class-instance-decl.inl @@ -1,6 +1,9 @@ #pragma once +#include + #include "common/smart_ptrs/intrusive_ptr.h" +#include "common/wrappers/span.h" #ifndef INCLUDED_FROM_KPHP_CORE #error "this file must be included only from runtime-core.h" @@ -71,8 +74,16 @@ public: inline class_instance& operator=(const Optional& null) noexcept; inline class_instance clone() const; + // constructs a copy of the instance in externally provided memory (no allocation, no ownership): + // memory must be 8-byte aligned and have at least estimate_memory_usage() bytes; + // the instance never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache) + inline class_instance clone(vk::span memory) const noexcept; template inline class_instance alloc(Args&&... args) __attribute__((always_inline)); + // constructs an instance in externally provided memory (no allocation, no ownership): + // memory must be 8-byte aligned and have at least sizeof(T) bytes + template + inline class_instance alloc(vk::span memory, Args&&... args) __attribute__((always_inline)); inline class_instance empty_alloc() __attribute__((always_inline)); inline void destroy() { o.reset(); @@ -102,6 +113,11 @@ public: return clone(); } + template + std::enable_if_t{}, class_instance> virtual_builtin_clone(vk::span memory) const noexcept { + return clone(memory); + } + template std::enable_if_t{}, class_instance> virtual_builtin_clone() const noexcept { // TODO this is used only for job workers. Should we use this logic for other? @@ -113,6 +129,18 @@ public: return res; } + template + std::enable_if_t{}, class_instance> virtual_builtin_clone(vk::span memory) const noexcept { + // TODO this is used only for job workers. Should we use this logic for other? + class_instance res; + if (o) { + php_assert(memory.size() >= o->virtual_builtin_sizeof()); + res.o = vk::intrusive_ptr{o->virtual_builtin_construct_at(memory.data())}; + res.o->set_refcnt(1); + } + return res; + } + template std::enable_if_t{}, void*> get_base_raw_ptr() const noexcept { return get(); @@ -197,7 +225,9 @@ public: private: class_instance clone_impl(std::true_type /*is empty*/) const; + class_instance clone_impl(vk::span memory, std::true_type /*is empty*/) const noexcept; class_instance clone_impl(std::false_type /*is empty*/) const; + class_instance clone_impl(vk::span memory, std::false_type /*is empty*/) const noexcept; }; template diff --git a/runtime-common/core/class-instance/class-instance.inl b/runtime-common/core/class-instance/class-instance.inl index 1067ea6329..20b0522698 100644 --- a/runtime-common/core/class-instance/class-instance.inl +++ b/runtime-common/core/class-instance/class-instance.inl @@ -16,6 +16,11 @@ class_instance class_instance::clone_impl(std::true_type /*is empty*/) con return class_instance{}.empty_alloc(); } +template +class_instance class_instance::clone_impl(vk::span /* memory */, std::true_type /*is empty*/) const noexcept { + return class_instance{}.empty_alloc(); +} + template class_instance class_instance::clone_impl(std::false_type /*is empty*/) const { class_instance res; @@ -26,10 +31,24 @@ class_instance class_instance::clone_impl(std::false_type /*is empty*/) co return res; } +template +class_instance class_instance::clone_impl(vk::span memory, std::false_type /*is empty*/) const noexcept { + class_instance res; + if (o) { + res.alloc(memory, *o); + res.o->set_refcnt(1); + } + return res; +} + template class_instance class_instance::clone() const { return clone_impl(std::is_empty{}); } +template +class_instance class_instance::clone(vk::span memory) const noexcept { + return clone_impl(memory, std::is_empty{}); +} template template @@ -40,6 +59,18 @@ class_instance class_instance::alloc(Args&&... args) { return *this; } +template +template +class_instance class_instance::alloc(vk::span memory, Args&&... args) { + static_assert(!std::is_empty{}, "class T may not be empty"); + php_assert(!o); + php_assert(memory.size() >= sizeof(T)); + php_assert(reinterpret_cast(memory.data()) % alignof(T) == 0); + T* ptr = new (memory.data()) T{std::forward(args)...}; + new (&o) vk::intrusive_ptr(ptr); + return *this; +} + template inline class_instance class_instance::empty_alloc() { static_assert(std::is_empty{}, "class T must be empty"); diff --git a/runtime-common/core/core-types/decl/array_decl.inl b/runtime-common/core/core-types/decl/array_decl.inl index d8833313dc..3d03ca1ee5 100644 --- a/runtime-common/core/core-types/decl/array_decl.inl +++ b/runtime-common/core/core-types/decl/array_decl.inl @@ -4,6 +4,9 @@ #pragma once +#include + +#include "common/wrappers/span.h" #include "runtime-common/core/core-types/decl/array_iterator.h" #include "runtime-common/core/include.h" @@ -132,7 +135,6 @@ private: inline static size_t sizeof_vector(uint32_t int_size) noexcept __attribute__((always_inline)); inline static size_t sizeof_map(uint32_t int_size) noexcept __attribute__((always_inline)); inline static size_t estimate_size(int64_t& new_int_size, bool is_vector); - inline static array_inner* create(int64_t new_int_size, bool is_vector); inline static array_inner* empty_array() __attribute__((always_inline)); @@ -186,6 +188,30 @@ private: inline array_inner& operator=(const array_inner& other) = delete; }; + class allocation { + vk::span mem_; + int64_t int_size_{0}; + bool is_vector_{false}; + + public: + inline allocation(int64_t new_int_size, bool is_vector) noexcept; + // takes ownership of the beginning of externally provided memory (no allocation): + // memory must be 8-byte aligned and have at least estimate_size(new_int_size, is_vector) bytes + inline allocation(vk::span memory, int64_t new_int_size, bool is_vector) noexcept; + + vk::span memory() const noexcept { + return mem_; + } + int64_t int_size() const noexcept { + return int_size_; + } + bool is_vector() const noexcept { + return is_vector_; + } + }; + + inline static array_inner* create_from_allocation(allocation alloc) noexcept; + inline bool mutate_if_vector_shared(uint32_t mul = 1); inline bool mutate_to_size_if_vector_shared(int64_t int_size); inline void mutate_to_size(int64_t int_size); @@ -197,7 +223,13 @@ private: inline void convert_to_map(); template - inline void copy_from(const array& other); + inline void copy_from(const array& other) noexcept; + + template + inline void copy_from(vk::span memory, const array& other) noexcept; + + template + inline void copy_from_impl(array_inner* new_array, const array& other) noexcept; template inline void move_from(array&& other) noexcept; @@ -228,6 +260,11 @@ public: template> inline array(array&& other) noexcept __attribute__((always_inline)); + // constructs a copy of other in externally provided memory (no allocation, no ownership): + // memory must be 8-byte aligned and have at least other.calculate_memory_for_copying() bytes; + // the array never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache) + inline array(vk::span memory, const array& other) noexcept; + template inline static array create(Args&&... args) __attribute__((always_inline)); diff --git a/runtime-common/core/core-types/decl/string_decl.inl b/runtime-common/core/core-types/decl/string_decl.inl index 211d804b0e..46ad3fb11c 100644 --- a/runtime-common/core/core-types/decl/string_decl.inl +++ b/runtime-common/core/core-types/decl/string_decl.inl @@ -1,5 +1,9 @@ #pragma once +#include + +#include "common/wrappers/span.h" + #ifndef INCLUDED_FROM_KPHP_CORE #error "this file must be included only from runtime-core.h" #endif @@ -57,7 +61,8 @@ private: inline char* ref_data() const; inline static size_type new_capacity(size_type requested_capacity, size_type old_capacity); - inline static string_inner* create(size_type requested_capacity, size_type old_capacity); + inline static string_inner* create(size_type requested_capacity, size_type old_capacity) noexcept; + inline static string_inner* create(vk::span memory, size_type requested_capacity, size_type old_capacity) noexcept; inline char* reserve(size_type requested_capacity); @@ -69,7 +74,8 @@ private: inline char* ref_copy(); - inline char* clone(size_type requested_cap); + inline char* clone(size_type requested_cap) noexcept; + inline char* clone(vk::span memory, size_type requested_cap) noexcept; }; inline string_inner* inner() const; @@ -85,6 +91,8 @@ private: friend class string_cache; + inline void copy_from(vk::span memory, const string& other) noexcept; + public: static constexpr size_type max_size() noexcept { return ((size_type)-1 - sizeof(string_inner) - 1) / 4; @@ -101,6 +109,10 @@ public: inline string(); inline string(const string& str) noexcept; inline string(string&& str) noexcept; + // constructs a copy of str in externally provided memory (no allocation, no ownership): + // memory must be 8-byte aligned and have at least str.estimate_memory_usage() bytes (an upper bound of the copy's footprint); + // the string never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache) + inline string(vk::span memory, const string& str) noexcept; inline string(const char* s, size_type n); inline explicit string(const char* s); // IMPORTANT: this constructor may return read-only strings for n == 0 and n == 1. diff --git a/runtime-common/core/core-types/definition/array.inl b/runtime-common/core/core-types/definition/array.inl index ce1270adae..e75000e167 100644 --- a/runtime-common/core/core-types/definition/array.inl +++ b/runtime-common/core/core-types/definition/array.inl @@ -247,21 +247,42 @@ size_t array::array_inner::estimate_size(int64_t& new_int_size, bool is_vecto } template -typename array::array_inner* array::array_inner::create(int64_t new_int_size, bool is_vector) { - const size_t mem_size = estimate_size(new_int_size, is_vector); - if (is_vector) { - auto p = reinterpret_cast(RuntimeAllocator::get().alloc_script_memory(mem_size)); +array::allocation::allocation(int64_t new_int_size, bool is_vector) noexcept + : int_size_(new_int_size), + is_vector_(is_vector) { + const size_t mem_size = array_inner::estimate_size(int_size_, is_vector_); + auto* raw_mem = + static_cast(is_vector_ ? RuntimeAllocator::get().alloc_script_memory(mem_size) : RuntimeAllocator::get().alloc0_script_memory(mem_size)); + mem_ = vk::span{raw_mem, mem_size}; +} + +template +array::allocation::allocation(vk::span memory, int64_t new_int_size, bool is_vector) noexcept + : int_size_(new_int_size), + is_vector_(is_vector) { + const size_t mem_size = array_inner::estimate_size(int_size_, is_vector_); + php_assert(memory.size() >= mem_size); + php_assert(reinterpret_cast(memory.data()) % alignof(array_inner) == 0); + mem_ = memory.first(mem_size); +} + +template +typename array::array_inner* array::create_from_allocation(allocation alloc) noexcept { + if (alloc.is_vector()) { + auto p = reinterpret_cast(alloc.memory().data()); p->is_vector_internal = true; p->ref_cnt = 0; p->max_key = -1; p->size = 0; - p->buf_size = static_cast(new_int_size); + p->buf_size = static_cast(alloc.int_size()); return p; } - auto shift_pointer_to_array_inner = [](void* mem) { return reinterpret_cast(static_cast(mem) + sizeof(array_inner_fields_for_map)); }; + auto shift_pointer_to_array_inner = [](void* raw_mem) noexcept { + return reinterpret_cast(static_cast(raw_mem) + sizeof(array_inner_fields_for_map)); + }; - array_inner* p = shift_pointer_to_array_inner(RuntimeAllocator::get().alloc0_script_memory(mem_size)); + array_inner* p = shift_pointer_to_array_inner(alloc.memory().data()); p->is_vector_internal = false; p->ref_cnt = 0; p->max_key = -1; @@ -269,7 +290,7 @@ typename array::array_inner* array::array_inner::create(int64_t new_int_si p->end()->prev = p->get_pointer(p->end()); p->size = 0; - p->buf_size = static_cast(new_int_size); + p->buf_size = static_cast(alloc.int_size()); p->fields_for_map().modulo_helper_buf_size = fastmod::computeM_u32(p->buf_size); p->fields_for_map().string_size = 0; @@ -670,7 +691,7 @@ bool array::mutate_if_vector_shared(uint32_t mul) { template bool array::mutate_to_size_if_vector_shared(int64_t int_size) { if (p->ref_cnt > 0) { - array_inner* new_array = array_inner::create(int_size, true); + array_inner* new_array = create_from_allocation(allocation{int_size, true}); const auto size = static_cast(p->size); T* it = (T*)p->entries(); @@ -689,7 +710,7 @@ bool array::mutate_to_size_if_vector_shared(int64_t int_size) { template bool array::mutate_if_map_shared(uint32_t mul) { if (p->ref_cnt > 0) { - array_inner* new_array = array_inner::create(p->size * mul + 1, false); + array_inner* new_array = create_from_allocation(allocation{p->size * mul + 1, false}); for (const array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { if (p->is_string_hash_entry(it)) { @@ -739,7 +760,7 @@ void array::mutate_if_map_needs_space() { // not shared (ref_cnt == 0) if (p->size * 5 > 3 * p->buf_size) { int64_t new_int_size = p->size * 2 + 1; - array_inner* new_array = array_inner::create(new_int_size, false); + array_inner* new_array = create_from_allocation(allocation{new_int_size, false}); for (array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { if (p->is_string_hash_entry(it)) { @@ -770,7 +791,7 @@ void array::reserve(int64_t int_size, bool make_vector_if_possible) { mutate_to_size(int_size); } else { const int64_t new_int_size = std::max(int_size, int64_t{p->buf_size}); - array_inner* new_array = array_inner::create(new_int_size, false); + array_inner* new_array = create_from_allocation(allocation{new_int_size, false}); if (is_vector()) { for (uint32_t it = 0; it != p->size; it++) { @@ -845,7 +866,7 @@ typename array::iterator array::end() { template void array::convert_to_map() { - array_inner* new_array = array_inner::create(p->size + 4, false); + array_inner* new_array = create_from_allocation(allocation{p->size + 4, false}); T* elements = reinterpret_cast(p->entries()); const bool move_values = p->ref_cnt == 0; @@ -867,14 +888,34 @@ void array::convert_to_map() { template template -void array::copy_from(const array& other) { +void array::copy_from(const array& other) noexcept { + if (other.empty()) { + p = array_inner::empty_array(); + return; + } + + copy_from_impl(create_from_allocation(allocation{other.p->size, other.is_vector()}), other); +} + +template +template +void array::copy_from(vk::span memory, const array& other) noexcept { if (other.empty()) { p = array_inner::empty_array(); return; } - array_inner* new_array = array_inner::create(other.p->size, other.is_vector()); + if (!other.is_vector()) { + // map allocations require zeroed memory (hash entries use null pointers as empty markers) + int64_t int_size = other.p->size; + std::memset(memory.data(), 0, array_inner::estimate_size(int_size, false)); + } + copy_from_impl(create_from_allocation(allocation{memory, other.p->size, other.is_vector()}), other); +} +template +template +void array::copy_from_impl(array_inner* new_array, const array& other) noexcept { if (new_array->is_vector()) { uint32_t size = other.p->size; T1* it = reinterpret_cast(other.p->entries()); @@ -910,7 +951,7 @@ void array::move_from(array&& other) noexcept { return; } - array_inner* new_array = array_inner::create(other.p->size, other.is_vector()); + array_inner* new_array = create_from_allocation(allocation{other.p->size, other.is_vector()}); if (new_array->is_vector()) { uint32_t size = other.p->size; @@ -948,7 +989,7 @@ array::array() template array::array(const array_size& s) - : p(array_inner::create(s.size, s.is_vector)) {} + : p(create_from_allocation(allocation{s.size, s.is_vector})) {} template template @@ -981,6 +1022,11 @@ array::array(array&& other) noexcept { move_from(std::move(other)); } +template +array::array(vk::span memory, const array& other) noexcept { + copy_from(memory, other); +} + template template inline array array::create(Args&&... args) { @@ -1652,7 +1698,7 @@ array& array::operator+=(const array& other) { uint32_t my_size = p->size; T* my_it = (T*)p->entries(); - array_inner* new_array = array_inner::create(max(size, my_size), true); + array_inner* new_array = create_from_allocation(allocation{max(size, my_size), true}); for (uint32_t i = 0; i < my_size; i++) { new_array->push_back_vector_value(my_it[i]); @@ -1676,7 +1722,7 @@ array& array::operator+=(const array& other) { return *this; } else { - array_inner* new_array = array_inner::create(p->size + other.p->size + 4, false); + array_inner* new_array = create_from_allocation(allocation{p->size + other.p->size + 4, false}); T* it = (T*)p->entries(); for (uint32_t i = 0; i != p->size; i++) { @@ -1694,7 +1740,7 @@ array& array::operator+=(const array& other) { uint32_t new_int_size = p->size + other.p->size; if (new_int_size * 5 > 3 * p->buf_size || p->ref_cnt > 0) { - array_inner* new_array = array_inner::create(max(new_int_size, 2 * p->size) + 1, false); + array_inner* new_array = create_from_allocation(allocation{max(new_int_size, 2 * p->size) + 1, false}); for (const array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { if (p->is_string_hash_entry(it)) { @@ -1873,7 +1919,7 @@ void array::sort(const T1& compare, bool renumber) { } if (!is_vector()) { - array_inner* res = array_inner::create(n, true); + array_inner* res = create_from_allocation(allocation{n, true}); for (array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { res->push_back_vector_value(it->value); } @@ -2030,7 +2076,7 @@ T array::shift() { array_size new_size = size().cut(count() - 1); const bool is_v = p->has_no_string_keys(); - array_inner* new_array = array_inner::create(new_size.size, is_v); + array_inner* new_array = create_from_allocation(allocation{new_size.size, is_v}); array_bucket* it = p->begin(); T res = it->value; @@ -2069,7 +2115,7 @@ int64_t array::unshift(const T& val) { array_size new_size = size(); const bool is_v = p->has_no_string_keys(); - array_inner* new_array = array_inner::create(new_size.size + 1, is_v); + array_inner* new_array = create_from_allocation(allocation{new_size.size + 1, is_v}); array_bucket* it = p->begin(); if (is_v) { diff --git a/runtime-common/core/core-types/definition/string.inl b/runtime-common/core/core-types/definition/string.inl index 0cb008db1a..39239b6cc4 100644 --- a/runtime-common/core/core-types/definition/string.inl +++ b/runtime-common/core/core-types/definition/string.inl @@ -57,7 +57,7 @@ string::size_type string::string_inner::new_capacity(size_type requested_capacit return requested_capacity; } -string::string_inner* string::string_inner::create(size_type requested_capacity, size_type old_capacity) { +string::string_inner* string::string_inner::create(size_type requested_capacity, size_type old_capacity) noexcept { size_type capacity = new_capacity(requested_capacity, old_capacity); size_type new_size = (size_type)(sizeof(string_inner) + (capacity + 1)); string_inner* p = (string_inner*)RuntimeAllocator::get().alloc_script_memory(new_size); @@ -65,6 +65,16 @@ string::string_inner* string::string_inner::create(size_type requested_capacity, return p; } +string::string_inner* string::string_inner::create(vk::span memory, size_type requested_capacity, size_type old_capacity) noexcept { + size_type capacity = new_capacity(requested_capacity, old_capacity); + size_type new_size = (size_type)(sizeof(string_inner) + (capacity + 1)); + php_assert(memory.size() >= new_size); + php_assert(reinterpret_cast(memory.data()) % alignof(string_inner) == 0); + string_inner* p = (string_inner*)memory.data(); + p->capacity = capacity; + return p; +} + char* string::string_inner::reserve(size_type requested_capacity) { size_type new_cap = new_capacity(requested_capacity, capacity); size_type old_size = (size_type)(sizeof(string_inner) + (capacity + 1)); @@ -101,7 +111,7 @@ char* string::string_inner::ref_copy() { return ref_data(); } -char* string::string_inner::clone(size_type requested_cap) { +char* string::string_inner::clone(size_type requested_cap) noexcept { string_inner* r = string_inner::create(requested_cap, capacity); if (size) { memcpy(r->ref_data(), ref_data(), size); @@ -111,6 +121,16 @@ char* string::string_inner::clone(size_type requested_cap) { return r->ref_data(); } +char* string::string_inner::clone(vk::span memory, size_type requested_cap) noexcept { + string_inner* r = string_inner::create(memory, requested_cap, capacity); + if (size) { + memcpy(r->ref_data(), ref_data(), size); + } + + r->set_length_and_sharable(size); + return r->ref_data(); +} + string::string_inner* string::inner() const { return (string::string_inner*)p - 1; } @@ -188,6 +208,11 @@ string::string(string&& str) noexcept str.p = string_cache::empty_string().ref_data(); } +string::string(vk::span memory, const string& str) noexcept + : p{} { + copy_from(memory, str); +} + string::string(const char* s, size_type n) : p(create(s, s + n)) {} @@ -325,6 +350,10 @@ string string::copy_and_make_not_shared() const { return result; } +void string::copy_from(vk::span memory, const string& other) noexcept { + p = other.inner()->clone(memory, other.size()); +} + void string::force_reserve(size_type res) { char* new_p = inner()->clone(res); inner()->dispose(); diff --git a/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h new file mode 100644 index 0000000000..39b7cb0ea6 --- /dev/null +++ b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h @@ -0,0 +1,114 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include + +#include "common/mixin/not_copyable.h" +#include "runtime-common/core/runtime-core.h" + +namespace kphp::visitors { +// high bit of the per-instance refcnt info word used by the legacy counting/destroy visitors: +// the lower bits store the real reference count, the top bit marks the instance as visited +constexpr uint32_t VISITED_INSTANCE_MASK{0x80000000}; + +// CRTP base for visitors that traverse an instance graph via compiler-generated accept() methods: +// every field is dispatched to Child::process; a false result from any field is accumulated into is_ok() +template +class instance_deep_basic_visitor : vk::not_copyable { +public: + template + void operator()(const char* /*unused*/, T&& value) noexcept { + const bool is_ok{child_.process(std::forward(value))}; + is_ok_ = is_ok_ && is_ok; + } + + template + bool process(T& /*unused*/) noexcept { + return true; + } + + template + bool process(Optional& value) noexcept { + return !value.has_value() || child_.process(value.val()); + } + + template + bool process(class_instance& instance) noexcept { + if (!instance.is_null()) { + instance.get()->accept(child_); + return child_.is_ok(); + } + return true; + } + + template + bool process(std::tuple& value) noexcept { + return process_tuple(value); + } + + template + bool process(shape, T...>& value) noexcept { + const bool child_res[]{child_.process(value.template get())...}; + return std::all_of(std::begin(child_res), std::end(child_res), [](bool r) noexcept { return r; }); + } + + bool process(mixed& value) noexcept { + if (value.is_string()) { + return child_.process(value.as_string()); + } else if (value.is_array()) { + return child_.process(value.as_array()); + } + return true; + } + + bool is_ok() const noexcept { + return is_ok_; + } + + ExtraRefCnt get_memory_ref_cnt() const noexcept { + return memory_ref_cnt_; + } + +protected: + explicit instance_deep_basic_visitor(Child& child, ExtraRefCnt memory_ref_cnt = ExtraRefCnt::extra_ref_cnt_value(0)) noexcept + : memory_ref_cnt_{memory_ref_cnt}, + child_{child} {} + + template + bool process_range(Iterator first, Iterator last) noexcept { + bool res{true}; + for (; first != last; ++first) { + if (!child_.process(first.get_value())) { + res = false; + } + if (first.is_string_key() && !child_.process(first.get_string_key())) { + res = false; + } + } + return res; + } + +private: + template + std::enable_if_t process_tuple(std::tuple& value) noexcept { + bool res = child_.process(std::get(value)); + return process_tuple(value) && res; + } + + template + std::enable_if_t process_tuple(std::tuple& /*unused*/) noexcept { + return true; + } + + bool is_ok_{true}; + const ExtraRefCnt memory_ref_cnt_{ExtraRefCnt::extra_ref_cnt_value(0)}; + Child& child_; +}; + +} // namespace kphp::visitors diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index a066930941..3f791ecad1 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -129,6 +129,45 @@ inline void free_checked(void* ptr, size_t size, size_t align) noexcept { k2_free_checked(ptr, size, align); } +inline std::expected alloc_shared_memory(size_t size, size_t align) noexcept { + void* pointer{nullptr}; + if (auto error_code{k2_alloc_shared_memory(size, align, std::addressof(pointer))}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {pointer}; +} + +inline std::expected publish_shared_memory(std::string_view name, const void* memory, uint64_t ttl, bool as_mut, bool ignore_if_exist) noexcept { + if (auto error_code{k2_publish_shared_memory(name.data(), name.length(), memory, ttl, as_mut, ignore_if_exist)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected, int32_t> get_shared_memory(std::string_view name) noexcept { + const void* pointer{nullptr}; + size_t size{}; + if (auto error_code{k2_get_shared_memory(name.data(), name.length(), std::addressof(pointer), std::addressof(size))}; error_code != k2::errno_ok) + [[unlikely]] { + return std::unexpected{error_code}; + } + return {std::span{static_cast(pointer), size}}; +} + +inline std::expected update_ttl_shared_memory(std::string_view name, uint64_t ttl) noexcept { + if (auto error_code{k2_update_ttl_shared_memory(name.data(), name.length(), ttl)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected delete_shared_memory(std::string_view name) noexcept { + if (auto error_code{k2_delete_shared_memory(name.data(), name.length())}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + [[noreturn]] inline void exit(int32_t exit_code) noexcept { k2_exit(exit_code); } diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 383a551f06..c675cd038a 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -251,8 +251,9 @@ int32_t k2_alloc_shared_memory(size_t size, size_t align, void** pointer); * @return `0` on success. libc-like `errno` on error. * * Possible `errno`: - * `EINVAL` => `name` is NULL, `name_len` is 0, `memory` is NULL, or `name` is - * not valid UTF-8. + * `EINVAL` => `name` is NULL, `name_len` is 0, `memory` is NULL, `name` is + * not valid UTF-8, or `ttl` is too small (non-zero and less than + * 100 ms). * `ENOENT` => `memory` was not allocated by `k2_alloc_shared_memory`. * `EEXIST` => Memory with this name already exists. * `ENOSYS` => Shared memory subsystem is unavailable on this host. @@ -277,6 +278,44 @@ int32_t k2_publish_shared_memory(const char* name, size_t name_len, const void* */ int32_t k2_get_shared_memory(const char* name, size_t name_len, const void** pointer, size_t* size); +/** + * Updates the TTL of published shared memory. + * + * @param `name` Name of the published memory region. + * @param `name_len` Length of the name in bytes. Must be greater than 0. + * @param `ttl` New time-to-live in milliseconds, counted from this call. + * Zero TTL means infinite life. + * + * @return `0` on success. libc-like `errno` on error. + * + * Possible `errno`: + * `EINVAL` => `name` is NULL, `name_len` is 0, `name` is not valid UTF-8, or + * `ttl` is too small (non-zero and less than 100 ms). + * `ENOENT` => No memory found with the given name (or TTL expired and memory was freed). + * `ENOSYS` => Shared memory subsystem is unavailable on this host. + */ +int32_t k2_update_ttl_shared_memory(const char* name, size_t name_len, uint64_t ttl); + +/** + * Deletes published shared memory by name. + * + * The name is removed immediately, so subsequent `k2_get_shared_memory` calls + * with this name will fail with `ENOENT`. The underlying memory is reclaimed + * only when the reference count reaches zero, so instances that already + * retrieved the memory keep valid pointers until they finish. + * + * @param `name` Name of the published memory region to delete. + * @param `name_len` Length of the name in bytes. Must be greater than 0. + * + * @return `0` on success. libc-like `errno` on error. + * + * Possible `errno`: + * `EINVAL` => `name` is NULL, `name_len` is 0, or `name` is not valid UTF-8. + * `ENOENT` => No memory found with the given name (or TTL expired and memory was freed). + * `ENOSYS` => Shared memory subsystem is unavailable on this host. + */ +int32_t k2_delete_shared_memory(const char* name, size_t name_len); + /** * Immediately abort component execution. * Function is `[[noreturn]]` diff --git a/runtime-light/stdlib/array/array-functions.h b/runtime-light/stdlib/array/array-functions.h index 1b1770b5bb..b0dbb8c7bc 100644 --- a/runtime-light/stdlib/array/array-functions.h +++ b/runtime-light/stdlib/array/array-functions.h @@ -78,7 +78,8 @@ template Result async_sort(array& arr, Comparator comparator, bool renumber) noexcept { using array_inner = typename array::array_inner; using array_bucket = typename array::array_bucket; - int64_t n = arr.count(); + using array_allocation = typename array::allocation; + int64_t n{arr.count()}; if (renumber) { if (n == 0) { @@ -86,8 +87,8 @@ Result async_sort(array& arr, Comparator comparator, bool renumber) noexcept } if (!arr.is_vector()) { - array_inner* res = array_inner::create(n, true); - for (array_bucket* it = arr.p->begin(); it != arr.p->end(); it = arr.p->next(it)) { + array_inner* res{array::create_from_allocation(array_allocation{n, true})}; + for (array_bucket* it{arr.p->begin()}; it != arr.p->end(); it = arr.p->next(it)) { res->push_back_vector_value(it->value); } @@ -97,7 +98,7 @@ Result async_sort(array& arr, Comparator comparator, bool renumber) noexcept arr.mutate_if_vector_shared(); } - U* begin = reinterpret_cast(arr.p->entries()); + U* begin{reinterpret_cast(arr.p->entries())}; co_await async_sort(begin, begin + n, std::move(comparator)); co_return; } @@ -113,18 +114,18 @@ Result async_sort(array& arr, Comparator comparator, bool renumber) noexcept } auto& runtimeAllocator{RuntimeAllocator::get()}; - auto** arTmp = static_cast(runtimeAllocator.alloc_script_memory(n * sizeof(array_bucket*))); - uint32_t i = 0; - for (array_bucket* it = arr.p->begin(); it != arr.p->end(); it = arr.p->next(it)) { + auto** arTmp{static_cast(runtimeAllocator.alloc_script_memory(n * sizeof(array_bucket*)))}; + uint32_t i{0}; + for (array_bucket* it{arr.p->begin()}; it != arr.p->end(); it = arr.p->next(it)) { arTmp[i++] = it; } kphp::log::assertion(i == n); - const auto hash_entry_cmp = [](Compare compare, const array_bucket* lhs, const array_bucket* rhs) -> kphp::coro::task { + const auto hash_entry_cmp{[](Compare compare, const array_bucket* lhs, const array_bucket* rhs) noexcept -> kphp::coro::task { co_return (co_await std::invoke(compare, lhs->value, rhs->value)) > 0; - }; + }}; - const auto partial_hash_entry_cmp = std::bind_front(hash_entry_cmp, std::move(comparator)); + const auto partial_hash_entry_cmp{std::bind_front(hash_entry_cmp, std::move(comparator))}; co_await async_sort(arTmp, arTmp + n, partial_hash_entry_cmp); diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 01c9405f8f..e3c9476849 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -4,166 +4,119 @@ #pragma once -#include #include #include -#include +#include +#include +#include #include #include #include -#include "runtime-common/core/allocator/script-allocator.h" +#include "common/algorithms/hashes.h" #include "runtime-common/core/runtime-core.h" -#include "runtime-common/core/std/containers.h" -#include "runtime-common/stdlib/serialization/msgpack-functions.h" -#include "runtime-light/coroutine/task.h" -#include "runtime-light/stdlib/component/component-api.h" +#include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/stdlib/diagnostics/logs.h" -#include "runtime-light/stdlib/fork/fork-functions.h" #include "runtime-light/stdlib/instance-cache/instance-cache-state.h" -#include "runtime-light/stdlib/serialization/msgpack-functions.h" -#include "runtime-light/streams/read-ext.h" -#include "runtime-light/streams/stream.h" -#include "runtime-light/tl/tl-core.h" -#include "runtime-light/tl/tl-functions.h" -#include "runtime-light/tl/tl-types.h" - -namespace kphp::instance_cache::details { - -inline constexpr std::string_view COMPONENT_NAME{"instance_cache"}; - -} // namespace kphp::instance_cache::details +#include "runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h" +#include "runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h" +// shared memory layout: class_name_hash(u64) | class_instance shell | inner data +// (deep copies of all arrays, strings and nested instance bodies reachable from the instance); +// the hash goes first so that fetch can verify the requested class without touching the instance itself template -kphp::coro::task f$instance_cache_store(string key, InstanceType instance, int64_t ttl = 0) noexcept { - if (ttl < 0) [[unlikely]] { - kphp::log::warning("ttl can't be negative: ttl -> {}, key -> {}", ttl, key.c_str()); - co_return false; +bool f$instance_cache_store(const string& key, class_instance instance, int64_t ttl = 0) noexcept { + if (key.empty()) [[unlikely]] { + kphp::log::warning("instance_cache_store. empty key is not supported"); + return false; } - if (ttl > std::numeric_limits::max()) [[unlikely]] { - kphp::log::warning("ttl exceeds maximum allowed value, key will be stored forever: ttl -> {}, max -> {}, key -> {}", ttl, - std::numeric_limits::max(), key.c_str()); - ttl = 0; + if (instance.is_null()) [[unlikely]] { + kphp::log::warning("instance_cache_store. can't store a null instance: key -> {}", key.c_str()); + return false; } - - auto serialized_instance{f$instance_serialize(instance)}; - if (!serialized_instance.has_value()) [[unlikely]] { - kphp::log::warning("can't serialize instance: key -> {}", key.c_str()); - co_return false; + if (ttl < 0) [[unlikely]] { + kphp::log::warning("instance_cache_store. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl, key.c_str()); + ttl = 0; } - tl::CacheStore cache_store{.key = tl::string{.value = {key.c_str(), key.size()}}, - .value = tl::string{.value = {serialized_instance.val().c_str(), serialized_instance.val().size()}}, - .ttl = tl::u32{.value = static_cast(ttl)}}; - tl::storer tls{cache_store.footprint()}; - cache_store.store(tls); + kphp::visitors::instance_deep_size_count_visitor instance_deep_size_count_visitor{}; + instance_deep_size_count_visitor.process_instance(instance); + size_t inner_size{instance_deep_size_count_visitor.get_inner_size()}; + constexpr size_t instance_size{sizeof(class_instance)}; + constexpr size_t hash_size{sizeof(uint64_t)}; - auto expected_stream{kphp::component::stream::open(kphp::instance_cache::details::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return false; + auto alloc_result{k2::alloc_shared_memory(hash_size + instance_size + inner_size, alignof(std::max_align_t))}; + if (!alloc_result.has_value()) [[unlikely]] { + kphp::log::warning("instance_cache_store. failed to allocate shared memory: error -> {}, key -> {}", alloc_result.error(), key.c_str()); + return false; } - - auto stream{*std::move(expected_stream)}; - std::array response{}; - if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), response))) [[unlikely]] { - co_return false; + std::byte* mem{static_cast(alloc_result.value())}; + + const uint64_t class_name_hash{static_cast(InstanceType::CLASS_NAME_HASH)}; + std::memcpy(mem, &class_name_hash, hash_size); + // deep-copies the object graph into the inner area and rewrites the instance's fields to point at the copies, + // so the whole graph ends up inside the shared-memory block; all copies are pinned with ExtraRefCnt::for_instance_cache + // and are never freed individually — the platform owns the block + kphp::visitors::instance_deep_copy_visitor{std::span{mem + hash_size + instance_size, inner_size}, ExtraRefCnt::for_instance_cache}.process_instance( + instance); + std::construct_at(reinterpret_cast*>(mem + hash_size), std::move(instance)); + + // the platform expects ttl in milliseconds, while the PHP API accepts seconds + if (auto publish_result{k2::publish_shared_memory(std::string_view{key.c_str(), key.size()}, mem, ttl * 1000, false, true)}; publish_result.has_value()) { + InstanceCacheInstanceState::get().request_cache.insert_or_assign(key, std::span{mem, hash_size + instance_size + inner_size}); + return true; + } else { + kphp::log::warning("instance_cache_store. failed to publish shared memory: error -> {}, key -> {}", publish_result.error(), key.c_str()); + return false; } - - tl::Bool tl_bool{}; - tl::fetcher tlf{response}; - kphp::log::assertion(tl_bool.fetch(tlf)); - InstanceCacheInstanceState::get().request_cache.emplace(std::move(key), std::move(instance)); - co_return tl_bool.value; } -template -kphp::coro::task f$instance_cache_fetch(string /*class_name*/, string key, bool /*even_if_expired*/ = false) noexcept { +template +ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& key, bool /* even_if_expired */ = false) noexcept { + static_assert(is_class_instance_v, "class_instance<> type expected"); + constexpr size_t hash_size{sizeof(uint64_t)}; + constexpr size_t instance_size{sizeof(ClassInstanceType)}; + + std::span mem{}; + auto& request_cache{InstanceCacheInstanceState::get().request_cache}; if (auto it{request_cache.find(key)}; it != request_cache.end()) { - auto cached_instance{from_mixed(it->second, {})}; - co_return std::move(cached_instance); + mem = it->second; + } else if (auto get_result{k2::get_shared_memory(std::string_view{key.c_str(), key.size()})}; get_result.has_value()) { + mem = get_result.value(); + } else { + return {}; } - tl::CacheFetch cache_fetch{.key = tl::string{.value = {key.c_str(), key.size()}}}; - tl::storer tls{cache_fetch.footprint()}; - cache_fetch.store(tls); - - auto expected_stream{kphp::component::stream::open(kphp::instance_cache::details::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return InstanceType{}; + if (mem.size() < hash_size + instance_size) [[unlikely]] { + // the shared memory under this key was not written by instance_cache_store for this class (or is corrupted) + kphp::log::warning("instance_cache_fetch. shared memory is too small: size -> {}, expected at least -> {}, key -> {}", mem.size(), + hash_size + instance_size, key.c_str()); + return {}; } - auto stream{*std::move(expected_stream)}; - kphp::stl::vector response{}; - if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response)))) [[unlikely]] { - co_return InstanceType{}; - } + uint64_t stored_class_name_hash{}; + std::memcpy(&stored_class_name_hash, mem.data(), sizeof(stored_class_name_hash)); - tl::fetcher tlf{response}; - tl::Maybe maybe_string{}; - kphp::log::assertion(maybe_string.fetch(tlf)); - if (!maybe_string.opt_value) [[unlikely]] { - co_return InstanceType{}; + if (stored_class_name_hash != vk::murmur_hash(class_name.c_str(), class_name.size())) { + kphp::log::warning("instance_cache_fetch. requested class doesn't match the stored one: requested -> {}, key -> {}", class_name.c_str(), key.c_str()); + return {}; } - auto cached_instance{f$instance_deserialize( - string{(*maybe_string.opt_value).value.data(), static_cast((*maybe_string.opt_value).value.size())}, {})}; - request_cache.emplace(std::move(key), cached_instance); - co_return std::move(cached_instance); + request_cache.insert_or_assign(key, mem); + return *reinterpret_cast(mem.data() + hash_size); } -inline kphp::coro::task f$instance_cache_update_ttl(string key, int64_t ttl = 0) noexcept { +inline bool f$instance_cache_update_ttl(const string& key, int64_t ttl = 0) noexcept { if (ttl < 0) [[unlikely]] { - kphp::log::warning("ttl can't be negative: ttl -> {}, key -> {}", ttl, key.c_str()); - co_return false; - } - if (ttl > std::numeric_limits::max()) [[unlikely]] { - kphp::log::warning("ttl exceeds maximum allowed value, key will be stored forever: ttl -> {}, max -> {}, key -> {}", ttl, - std::numeric_limits::max(), key.c_str()); + kphp::log::warning("instance_cache_update_ttl. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl, key.c_str()); ttl = 0; } - - tl::CacheUpdateTtl cache_update_tll{.key = tl::string{.value = {key.c_str(), key.size()}}, .ttl = tl::u32{.value = static_cast(ttl)}}; - tl::storer tls{cache_update_tll.footprint()}; - cache_update_tll.store(tls); - - auto expected_stream{kphp::component::stream::open(kphp::instance_cache::details::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return false; - } - - auto stream{*std::move(expected_stream)}; - std::array response{}; - if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), response))) [[unlikely]] { - co_return false; - } - - tl::Bool tl_bool{}; - tl::fetcher tlf{response}; - kphp::log::assertion(tl_bool.fetch(tlf)); - co_return tl_bool.value; + // the platform expects ttl in milliseconds, while the PHP API accepts seconds + return k2::update_ttl_shared_memory(std::string_view{key.c_str(), key.size()}, ttl * 1000).has_value(); } -inline kphp::coro::task f$instance_cache_delete(string key) noexcept { +inline bool f$instance_cache_delete(const string& key) noexcept { InstanceCacheInstanceState::get().request_cache.erase(key); - - tl::CacheDelete cache_delete{.key = tl::string{.value = {key.c_str(), key.size()}}}; - tl::storer tls{cache_delete.footprint()}; - cache_delete.store(tls); - - auto expected_stream{kphp::component::stream::open(kphp::instance_cache::details::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return false; - } - - auto stream{*std::move(expected_stream)}; - std::array response{}; - if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), response))) [[unlikely]] { - co_return false; - } - - tl::Bool tl_bool{}; - tl::fetcher tlf{response}; - kphp::log::assertion(tl_bool.fetch(tlf)); - co_return tl_bool.value; + return k2::delete_shared_memory(std::string_view{key.c_str(), key.size()}).has_value(); } diff --git a/runtime-light/stdlib/instance-cache/instance-cache-state.h b/runtime-light/stdlib/instance-cache/instance-cache-state.h index 88f65d3b95..9d5b05340a 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-state.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-state.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include "common/mixin/not_copyable.h" #include "runtime-common/core/allocator/script-allocator.h" @@ -12,7 +13,11 @@ #include "runtime-common/core/std/containers.h" struct InstanceCacheInstanceState final : private vk::not_copyable { - kphp::stl::unordered_map(s.hash()); })> + // per-request cache: key -> shared memory region published under that key + // (layout: class_name_hash | class_instance shell | inner data, see f$instance_cache_store); + // spans point to platform-owned memory that stays valid for the whole request lifetime + kphp::stl::unordered_map, kphp::memory::script_allocator, + decltype([](const string& s) noexcept { return static_cast(s.hash()); })> request_cache; InstanceCacheInstanceState() noexcept = default; diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h new file mode 100644 index 0000000000..3fdf981888 --- /dev/null +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h @@ -0,0 +1,127 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include + +#include "common/containers/final_action.h" +#include "runtime-common/core/memory-resource/details/memory_chunk_list.h" +#include "runtime-common/core/memory-resource/monotonic_buffer_resource.h" +#include "runtime-common/core/runtime-core.h" +#include "runtime-common/core/std/containers.h" +#include "runtime-common/stdlib/visitors/instance-deep-basic-visitor.h" +#include "runtime-light/stdlib/diagnostics/logs.h" + +namespace kphp::visitors { + +// deep-copies an instance graph into a caller-provided contiguous memory block (e.g. a shared memory region): +// every reachable array/string/instance body is recreated inside the block via memory_pool, and the original's +// fields are rewritten to point at the copies; all copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache), +// so they are never freed individually — the owner of the block controls their lifetime +class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_visitor { +public: + friend class kphp::visitors::instance_deep_basic_visitor; + + using Basic = kphp::visitors::instance_deep_basic_visitor; + using Basic::process; + using Basic::operator(); + using Basic::get_memory_ref_cnt; + + instance_deep_copy_visitor(const instance_deep_copy_visitor&) = delete; + instance_deep_copy_visitor(instance_deep_copy_visitor&&) = delete; + instance_deep_copy_visitor& operator=(const instance_deep_copy_visitor&) = delete; + instance_deep_copy_visitor& operator=(instance_deep_copy_visitor&&) = delete; + ~instance_deep_copy_visitor() = default; + + explicit instance_deep_copy_visitor(std::span memory_pool_buffer, ExtraRefCnt memory_ref_cnt = ExtraRefCnt::extra_ref_cnt_value(0)) noexcept + : Basic{*this, memory_ref_cnt} { + this->memory_pool.init(memory_pool_buffer.data(), memory_pool_buffer.size()); + } + + template + bool process(array& arr) noexcept { + if (arr.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + array copied_array{carve(arr.calculate_memory_for_copying()), arr}; + const auto commit_copy{vk::finally([&arr, &copied_array]() noexcept { arr = std::move(copied_array); })}; + + // copying an empty array yields the global empty-array singleton instead of a real copy — nothing left to deep-copy + if (copied_array.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + kphp::log::assertion(copied_array.get_reference_counter() == 1); + if (const auto extra_ref_cnt{get_memory_ref_cnt()}; extra_ref_cnt != 0) { + copied_array.set_reference_counter_to(extra_ref_cnt); + } + // values of a primitive array were already memcpy'd by the array copy constructor, and there are no string keys to copy + const bool primitive_array{is_primitive && copied_array.has_no_string_keys()}; + return primitive_array || Basic::process_range(copied_array.begin(), copied_array.end()); + } + + bool process(string& str) noexcept { + if (str.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + string copied_string{carve(str.estimate_memory_usage()), str}; + const auto commit_copy{vk::finally([&str, &copied_string]() noexcept { str = std::move(copied_string); })}; + + kphp::log::assertion(copied_string.get_reference_counter() == 1); + if (const auto extra_ref_cnt{get_memory_ref_cnt()}; extra_ref_cnt != 0) { + copied_string.set_reference_counter_to(extra_ref_cnt); + } + return true; + } + + template + bool process_instance(class_instance& instance) noexcept { + // keep the original instance alive for the whole traversal: copied_instances_table uses raw pointers to originals as keys + class_instance instance_copy{instance}; + const bool result{process(instance)}; + this->copied_instances_table.clear(); + return result; + } + +private: + template + static constexpr bool is_primitive{vk::is_type_in_list, Optional, Optional>::value}; + + template + bool process(class_instance& instance) noexcept { + if (instance.is_null()) { + return true; + } + + auto& copied_instance_ptr{copied_instances_table[instance.get()->get_instance_data_raw_ptr()]}; + + // shared or cyclic references resolve to the same copy, which is created on first visit + if (copied_instance_ptr != nullptr) { + instance = class_instance::create_from_base_raw_ptr(copied_instance_ptr); + return true; + } + + instance = instance.virtual_builtin_clone(carve(instance.estimate_memory_usage())); + copied_instance_ptr = instance.get_base_raw_ptr(); + + if (const auto extra_ref_cnt{get_memory_ref_cnt()}; extra_ref_cnt != 0) { + instance.set_reference_counter_to(extra_ref_cnt); + } + return Basic::process(instance); + } + + vk::span carve(size_t size) noexcept { + const size_t aligned_size{memory_resource::details::align_for_chunk(size)}; + return {static_cast(this->memory_pool.get_from_pool(aligned_size)), aligned_size}; + } + + memory_resource::monotonic_buffer_resource memory_pool; + kphp::stl::unordered_map copied_instances_table; +}; + +} // namespace kphp::visitors diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h new file mode 100644 index 0000000000..96e09d0297 --- /dev/null +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h @@ -0,0 +1,89 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include + +#include "runtime-common/core/memory-resource/details/memory_chunk_list.h" +#include "runtime-common/core/runtime-core.h" +#include "runtime-common/core/std/containers.h" +#include "runtime-common/stdlib/visitors/instance-deep-basic-visitor.h" + +namespace kphp::visitors { + +// computes how many bytes instance_deep_copy_visitor will carve for an instance graph, +// so that the destination memory block can be allocated upfront; the two visitors must stay in sync +class instance_deep_size_count_visitor final : kphp::visitors::instance_deep_basic_visitor { +public: + friend class kphp::visitors::instance_deep_basic_visitor; + + using Basic = kphp::visitors::instance_deep_basic_visitor; + using Basic::process; + using Basic::operator(); + using Basic::get_memory_ref_cnt; + + instance_deep_size_count_visitor(const instance_deep_size_count_visitor&) = delete; + instance_deep_size_count_visitor(instance_deep_size_count_visitor&&) = delete; + instance_deep_size_count_visitor& operator=(const instance_deep_size_count_visitor&) = delete; + instance_deep_size_count_visitor& operator=(instance_deep_size_count_visitor&&) = delete; + ~instance_deep_size_count_visitor() = default; + + explicit instance_deep_size_count_visitor() noexcept + : Basic{*this} {} + + template + bool process(array& arr) noexcept { + if (arr.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + this->inner_size += memory_resource::details::align_for_chunk(arr.calculate_memory_for_copying()); + + // primitive values are already accounted for wholesale above; only non-primitive values and string keys need traversal + const bool primitive_array{is_primitive && arr.has_no_string_keys()}; + return primitive_array || Basic::process_range(arr.begin_no_mutate(), arr.end_no_mutate()); + } + + bool process(string& str) noexcept { + if (!str.is_reference_counter(ExtraRefCnt::for_global_const)) { + this->inner_size += memory_resource::details::align_for_chunk(str.estimate_memory_usage()); + } + + return true; + } + + template + bool process_instance(class_instance& instance) noexcept { + const bool result{process(instance)}; + this->visited_instances_set.clear(); + return result; + } + + size_t get_inner_size() const noexcept { + return this->inner_size; + } + +private: + template + static constexpr bool is_primitive{vk::is_type_in_list, Optional, Optional>::value}; + + template + bool process(class_instance& instance) noexcept { + if (!instance.is_null()) { + void* instance_raw_ptr{instance.get()->get_instance_data_raw_ptr()}; + if (this->visited_instances_set.contains(instance_raw_ptr)) { + return true; + } + this->inner_size += memory_resource::details::align_for_chunk(instance.estimate_memory_usage()); + this->visited_instances_set.emplace(instance_raw_ptr); + } + return Basic::process(instance); + } + + size_t inner_size{0}; + kphp::stl::unordered_set visited_instances_set; +}; + +} // namespace kphp::visitors diff --git a/runtime-light/stdlib/system/system-functions.h b/runtime-light/stdlib/system/system-functions.h index 09cad7370a..2bb88b900c 100644 --- a/runtime-light/stdlib/system/system-functions.h +++ b/runtime-light/stdlib/system/system-functions.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include diff --git a/runtime/instance-copy-processor.h b/runtime/instance-copy-processor.h index 092feb6876..1bbd7f38de 100644 --- a/runtime/instance-copy-processor.h +++ b/runtime/instance-copy-processor.h @@ -12,112 +12,15 @@ #include "runtime-common/core/memory-resource/unsynchronized_pool_resource.h" #include "runtime-common/core/runtime-core.h" +#include "runtime-common/stdlib/visitors/instance-deep-basic-visitor.h" #include "runtime/allocator.h" #include "runtime/critical_section.h" -namespace impl_ { - -template -class InstanceDeepBasicVisitor : vk::not_copyable { +class InstanceReferencesCountingVisitor : kphp::visitors::instance_deep_basic_visitor { public: - template - void operator()(const char*, T&& value) noexcept { - const bool is_ok = child_.process(std::forward(value)); - is_ok_ = is_ok_ && is_ok; - } - - template - bool process(T&) noexcept { - return true; - } - - template - bool process(Optional& value) noexcept { - return value.has_value() ? child_.process(value.val()) : true; - } - - template - bool process(class_instance& instance) noexcept { - if (!instance.is_null()) { - instance.get()->accept(child_); - return child_.is_ok(); - } - return true; - } - - template - bool process(std::tuple& value) noexcept { - return process_tuple(value); - } - - template - bool process(shape, T...>& value) noexcept { - const bool child_res[] = {child_.process(value.template get())...}; - return std::all_of(std::begin(child_res), std::end(child_res), [](bool r) { return r; }); - } - - bool process(mixed& value) noexcept { - if (value.is_string()) { - return child_.process(value.as_string()); - } else if (value.is_array()) { - return child_.process(value.as_array()); - } - return true; - } + friend class kphp::visitors::instance_deep_basic_visitor; - bool is_ok() const noexcept { - return is_ok_; - } - - ExtraRefCnt get_memory_ref_cnt() const noexcept { - return memory_ref_cnt_; - } - -protected: - InstanceDeepBasicVisitor(Child& child, ExtraRefCnt memory_ref_cnt = ExtraRefCnt::extra_ref_cnt_value(0)) noexcept - : memory_ref_cnt_(memory_ref_cnt), - child_(child) {} - - template - bool process_range(Iterator first, Iterator last) noexcept { - bool res = true; - for (; first != last; ++first) { - if (!child_.process(first.get_value())) { - res = false; - } - if (first.is_string_key() && !child_.process(first.get_string_key())) { - res = false; - } - } - return res; - } - -private: - template - std::enable_if_t process_tuple(std::tuple& value) noexcept { - bool res = child_.process(std::get(value)); - return process_tuple(value) && res; - } - - template - std::enable_if_t process_tuple(std::tuple&) noexcept { - return true; - } - - bool is_ok_{true}; - const ExtraRefCnt memory_ref_cnt_{ExtraRefCnt::extra_ref_cnt_value(0)}; - Child& child_; -}; - -constexpr static uint32_t VISITED_INSTANCE_MASK{0x80000000}; - -} // namespace impl_ - -class InstanceReferencesCountingVisitor : impl_::InstanceDeepBasicVisitor { -public: - friend class impl_::InstanceDeepBasicVisitor; - - using Basic = impl_::InstanceDeepBasicVisitor; + using Basic = kphp::visitors::instance_deep_basic_visitor; using Basic::operator(); explicit InstanceReferencesCountingVisitor(std::unordered_map& instances_refcnt_table) @@ -147,8 +50,8 @@ class InstanceReferencesCountingVisitor : impl_::InstanceDeepBasicVisitor& instance) noexcept { if (!instance.is_null()) { uint32_t& refcnt_info = instances_refcnt_table[instance.get()->get_instance_data_raw_ptr()]; - const bool visited = ++refcnt_info & impl_::VISITED_INSTANCE_MASK; - refcnt_info |= impl_::VISITED_INSTANCE_MASK; + const bool visited = ++refcnt_info & kphp::visitors::VISITED_INSTANCE_MASK; + refcnt_info |= kphp::visitors::VISITED_INSTANCE_MASK; if (visited) { return true; } @@ -159,17 +62,17 @@ class InstanceReferencesCountingVisitor : impl_::InstanceDeepBasicVisitor { +class InstanceDeepCopyVisitor : kphp::visitors::instance_deep_basic_visitor { public: - friend class impl_::InstanceDeepBasicVisitor; + friend class kphp::visitors::instance_deep_basic_visitor; - using Basic = impl_::InstanceDeepBasicVisitor; + using Basic = kphp::visitors::instance_deep_basic_visitor; using Basic::process; using Basic::operator(); using Basic::get_memory_ref_cnt; - InstanceDeepCopyVisitor(memory_resource::unsynchronized_pool_resource& memory_pool, ExtraRefCnt memory_ref_cnt = ExtraRefCnt::extra_ref_cnt_value(0), - ResourceCallbackOOM oom_callback = nullptr) noexcept; + explicit InstanceDeepCopyVisitor(memory_resource::unsynchronized_pool_resource& memory_pool, ExtraRefCnt memory_ref_cnt = ExtraRefCnt::extra_ref_cnt_value(0), + ResourceCallbackOOM oom_callback = nullptr) noexcept; template bool process(array& arr) noexcept { @@ -290,11 +193,11 @@ class InstanceDeepCopyVisitor : impl_::InstanceDeepBasicVisitor copied_instances_table; }; -class InstanceDeepDestroyVisitor : impl_::InstanceDeepBasicVisitor { +class InstanceDeepDestroyVisitor : kphp::visitors::instance_deep_basic_visitor { public: - friend class impl_::InstanceDeepBasicVisitor; + friend class kphp::visitors::instance_deep_basic_visitor; - using Basic = impl_::InstanceDeepBasicVisitor; + using Basic = kphp::visitors::instance_deep_basic_visitor; using Basic::process; using Basic::operator(); using Basic::is_ok; @@ -333,8 +236,8 @@ class InstanceDeepDestroyVisitor : impl_::InstanceDeepBasicVisitorget_instance_data_raw_ptr()]; - if (refcnt_info & impl_::VISITED_INSTANCE_MASK) { - refcnt_info ^= impl_::VISITED_INSTANCE_MASK; + if (refcnt_info & kphp::visitors::VISITED_INSTANCE_MASK) { + refcnt_info ^= kphp::visitors::VISITED_INSTANCE_MASK; Basic::process(instance); } @@ -360,6 +263,11 @@ class InstanceCopyistImpl; template class InstanceCopyistImpl> final : public InstanceCopyistBase { public: + InstanceCopyistImpl(const InstanceCopyistImpl&) = delete; + InstanceCopyistImpl(InstanceCopyistImpl&&) = delete; + InstanceCopyistImpl& operator=(const InstanceCopyistImpl&) = delete; + InstanceCopyistImpl& operator=(InstanceCopyistImpl&&) = delete; + explicit InstanceCopyistImpl(const class_instance& instance) noexcept : instance_(instance) {} diff --git a/tests/phpt/instance_cache/10_instance_cache_abstract_error.php b/tests/phpt/instance_cache/10_instance_cache_abstract_error.php index 6358319a32..602fd4997e 100644 --- a/tests/phpt/instance_cache/10_instance_cache_abstract_error.php +++ b/tests/phpt/instance_cache/10_instance_cache_abstract_error.php @@ -4,8 +4,7 @@ require_once 'kphp_tester_include.php'; -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ abstract class AbstractClass { abstract protected function getValue(); diff --git a/tests/phpt/instance_cache/11_instance_cache_polymprphic_field.php b/tests/phpt/instance_cache/11_instance_cache_polymprphic_field.php index 0f9a6f20f7..72aecb5300 100644 --- a/tests/phpt/instance_cache/11_instance_cache_polymprphic_field.php +++ b/tests/phpt/instance_cache/11_instance_cache_polymprphic_field.php @@ -1,5 +1,4 @@ -@kphp_should_fail -/Can not store instance of non-serializable class Y with instance_cache_store call/ +@ok non-idempotent - * @kphp-serialized-field 5 - * - */ - public $y_tuple; - - /** - * @param int $i - * @param string $s - * @param string|false $or_false_str - */ - public function __construct($i, $s, $or_false_str = false) { - $this->x_instance = new X; - $this->y_string = $this->x_instance->x_str . " world" . $s; - $this->y_array = $this->x_instance->x_array; - $this->y_array[] = $i; - $this->y_array_var = $this->x_instance->x_array_var; - $this->y_array_var[] = $s; - $this->y_string_or_false = 1 ? "or_false" : false; - $this->y_tuple = tuple($or_false_str, $this->y_array_var, $this->y_array, $this->y_string, new X); - } -} - -/** @kphp-immutable-class - * @kphp-serializable -*/ -class TreeX { - /** @var int - * @kphp-serialized-field 0 - */ - public $value = 0; - /** @var tuple [] - * @kphp-serialized-field 1 - */ - public $children = []; - - public function __construct(int $value, array $children = [], bool $make_loop = false) { - $this->value = $value; - $this->children = $children; - if ($make_loop) { - $this->children[] = tuple(1, [$this]); - } - } -} - -/** @kphp-immutable-class - * @kphp-serializable -*/ -class VectorY { - /** @var Y[] - * @kphp-serialized-field 0 - */ - public $elements = []; - - public function __construct(int $elements_count, array $elements_array = []) { - if ($elements_count) { - for ($i = 1; $i < $elements_count; ++$i) { - $this->elements[] = new Y($i, " <-"); - } - } else { - $this->elements = $elements_array; - } - } -} - - -function test_empty_fetch() { - $x = instance_cache_fetch(X::class, "key_x0"); - var_dump(!$x); - $y = instance_cache_fetch(Y::class, "key_x0"); - var_dump(!$y); -} - -function test_store_fetch() { - var_dump(instance_cache_store("key_x1", new X)); - var_dump(instance_cache_store("key_y1", new Y(1, "test_store_fetch"))); - - $x = instance_cache_fetch(X::class, "key_x1"); - var_dump(to_array_debug($x)); - - $y = instance_cache_fetch(Y::class, "key_y1"); - var_dump(to_array_debug($y)); -} - -function test_mismatch_classes() { - var_dump(instance_cache_store("key_x2", new X)); - var_dump(instance_cache_store("key_y2", new Y(2, "test_mismatch_classes", "optional"))); - - $x = instance_cache_fetch(Y::class, "key_x2"); - var_dump(!$x); - - $y = instance_cache_fetch(X::class, "key_y2"); - var_dump(!$y); -} - -function test_update_ttl() { - var_dump(instance_cache_update_ttl("key_x_test_update_ttl", 12)); - - var_dump(instance_cache_store("key_x_test_update_ttl", new X, 1)); - var_dump(instance_cache_update_ttl("key_x_test_update_ttl", 3)); - var_dump(instance_cache_update_ttl("key_x_test_update_ttl", 2)); - - var_dump(instance_cache_delete("key_x_test_update_ttl")); - - var_dump(instance_cache_store("key_x_test_update_ttl", new X, 2)); - var_dump(instance_cache_update_ttl("key_x_test_update_ttl")); -} - - -function test_delete() { - var_dump(instance_cache_store("key_x3", new X)); - var_dump(instance_cache_store("key_y3", new Y(3, "test_delete", "super optional"))); - - var_dump(instance_cache_delete("key_x3_unknown")); - var_dump(instance_cache_delete("key_y3_unknown")); - - $x = instance_cache_fetch(X::class, "key_x3"); - var_dump(to_array_debug($x)); - - $y = instance_cache_fetch(Y::class, "key_y3"); - var_dump(to_array_debug($y)); - - var_dump(instance_cache_delete("key_x3")); - var_dump(instance_cache_delete("key_y3")); - - $x = instance_cache_fetch(X::class, "key_x3"); - var_dump(!$x); - - $y = instance_cache_fetch(Y::class, "key_y3"); - var_dump(!$y); -} - -function test_tree() { - $root = new TreeX (0, [tuple(1, [new TreeX(1)])]); - var_dump(instance_cache_store("tree_root", $root)); - - $cached_root1 = instance_cache_fetch(TreeX::class, "tree_root"); - var_dump(to_array_debug($cached_root1)); -} - -function test_same_instance_in_array() { - $y = new Y(10, " <-first"); -#ifndef KPHP - $vector = new VectorY(0, [$y, clone $y, new Y(11, " <-second")]); - if (false) -#endif - $vector = new VectorY(0, [$y, $y, new Y(11, " <-second")]); - - var_dump(instance_cache_store("vector", $vector)); - - $cached_vector = instance_cache_fetch(VectorY::class, "vector"); - var_dump(to_array_debug($cached_vector)); -} - -function test_request_cache() { - // Should work without request cache - // Just ensure that it works if present - var_dump(instance_cache_store("key_x4", new X)); - var_dump(to_array_debug(instance_cache_fetch(X::class, "key_x4"))); - var_dump(to_array_debug(instance_cache_fetch(X::class, "key_x4"))); - var_dump(to_array_debug(instance_cache_fetch(Y::class, "key_x4"))); - - var_dump(instance_cache_delete("key_x_test_update_ttl")); - var_dump(to_array_debug(instance_cache_fetch(X::class, "key_x4"))); - var_dump(to_array_debug(instance_cache_fetch(X::class, "key_x4"))); - var_dump(to_array_debug(instance_cache_fetch(Y::class, "key_x4"))); - - var_dump(instance_cache_store("key_x4", new X)); - var_dump(to_array_debug(instance_cache_fetch(X::class, "key_x4"))); - var_dump(to_array_debug(instance_cache_fetch(Y::class, "key_x4"))); - var_dump(to_array_debug(instance_cache_fetch(X::class, "key_x4"))); -} - -function test_memory_limit_exceed() { - $cnt = 200_000; - -#ifndef K2 - // In K2 mode it takes much more time to deallocate so huge object, so the limit is lower - $cnt = 1_000_000; -#endif - $vector = new VectorY($cnt); - -#ifndef KPHP - var_dump(false); - if (false) -#endif - var_dump(instance_cache_store("large_vector", $vector)); - var_dump(instance_cache_fetch(VectorY::class, "large_vector") ? false : true); -} - -test_empty_fetch(); -test_store_fetch(); -test_mismatch_classes(); -test_update_ttl(); -test_delete(); -test_tree(); -test_same_instance_in_array(); -test_request_cache(); -test_memory_limit_exceed(); - diff --git a/tests/phpt/instance_cache/1_instance_cache.php b/tests/phpt/instance_cache/1_instance_cache.php index 2fd6f26407..d3357f26f4 100644 --- a/tests/phpt/instance_cache/1_instance_cache.php +++ b/tests/phpt/instance_cache/1_instance_cache.php @@ -1,45 +1,33 @@ -@ok non-idempotent k2_skip +@ok non-idempotent - * @kphp-serialized-field 5 */ + /** @var tuple */ public $y_tuple; /** @@ -59,14 +47,11 @@ public function __construct($i, $s, $or_false_str = false) { } } -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ class TreeX { - /** @var int - * @kphp-serialized-field 0 */ + /** @var int */ public $value = 0; - /** @var tuple [] - * @kphp-serialized-field 1 */ + /** @var tuple [] */ public $children = []; public function __construct(int $value, array $children = [], bool $make_loop = false) { @@ -78,11 +63,9 @@ public function __construct(int $value, array $children = [], bool $make_loop = } } -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ class VectorY { - /** @var Y[] - * @kphp-serialized-field 0 */ + /** @var Y[] */ public $elements = []; public function __construct(int $elements_count, array $elements_array = []) { @@ -96,30 +79,26 @@ public function __construct(int $elements_count, array $elements_array = []) { } } -// shape is not serializable. So it is temporary commented -// /** @kphp-immutable-class -// * @kphp-serializable */ -// class HasShape { -// /** @var tuple(int, string) -// * @kphp-serialized-field 0 */ -// var $t; -// /** @var shape(x:int, y:string, z?:int[]) -// * @kphp-serialized-field 1 */ -// var $sh; -// -// /** -// * @param int $sh_x -// * @param bool $with_z -// */ -// function __construct($sh_x, $with_z = false) { -// $this->t = tuple(1, 's'); -// if ($with_z) { -// $this->sh = shape(['y' => 'y', 'x' => 2, 'z' => [1,2,3]]); -// } else { -// $this->sh = shape(['y' => 'y', 'x' => $sh_x]); -// } -// } -// } +/** @kphp-immutable-class */ +class HasShape { + /** @var tuple(int, string) */ + var $t; + /** @var shape(x:int, y:string, z?:int[]) */ + var $sh; + + /** + * @param int $sh_x + * @param bool $with_z + */ + function __construct($sh_x, $with_z = false) { + $this->t = tuple(1, 's'); + if ($with_z) { + $this->sh = shape(['y' => 'y', 'x' => 2, 'z' => [1,2,3]]); + } else { + $this->sh = shape(['y' => 'y', 'x' => $sh_x]); + } + } +} function test_empty_fetch() { $x = instance_cache_fetch(X::class, "key_x0"); @@ -292,6 +271,6 @@ function test_with_shape() { test_tree(); test_loop_in_tree(); test_same_instance_in_array(); -// test_with_shape(); -// this test should be the last! -test_memory_limit_exceed(); +test_with_shape(); +// // this test should be the last! +// test_memory_limit_exceed(); diff --git a/tests/phpt/instance_cache/5_instance_cache_polymorphic_simple.php b/tests/phpt/instance_cache/5_instance_cache_polymorphic_simple.php index a92a678791..4eaf28be7c 100644 --- a/tests/phpt/instance_cache/5_instance_cache_polymorphic_simple.php +++ b/tests/phpt/instance_cache/5_instance_cache_polymorphic_simple.php @@ -3,10 +3,8 @@ require_once 'kphp_tester_include.php'; -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ class A { - /** @kphp-serialized-field 0 */ public $a_id = 1; } diff --git a/tests/phpt/instance_cache/6_instance_cache_polymorphic_error.php b/tests/phpt/instance_cache/6_instance_cache_polymorphic_error.php index 49bd645ff1..da6096b26c 100644 --- a/tests/phpt/instance_cache/6_instance_cache_polymorphic_error.php +++ b/tests/phpt/instance_cache/6_instance_cache_polymorphic_error.php @@ -4,10 +4,8 @@ require_once 'kphp_tester_include.php'; -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ class A { - /** @kphp-serialized-field 0 */ public $a_id = 1; } diff --git a/tests/phpt/instance_cache/7_instance_cache_interface.php b/tests/phpt/instance_cache/7_instance_cache_interface.php index 056ca6a30e..b75a3b49c5 100644 --- a/tests/phpt/instance_cache/7_instance_cache_interface.php +++ b/tests/phpt/instance_cache/7_instance_cache_interface.php @@ -1,5 +1,4 @@ -@kphp_should_fail -/Can not fetch instance of non-serializable class SimpleInterface with instance_cache_fetch call/ +@ok non-idempotent a, $this); } $this->b = tuple([$b1, $b2, $b1, $b1, $b2], 100); -// shape is not serializable. So it is temporary commented -// $this->ab = shape([ -// 'b' => tuple(10, $b2), -// 'c' => shape([ -// 'arr' => [$c1, $c1, $c2, $c2, $c2, $c2] -// ]) -// ]); + $this->ab = shape([ + 'b' => tuple(10, $b2), + 'c' => shape([ + 'arr' => [$c1, $c1, $c2, $c2, $c2, $c2] + ]) + ]); } - /** @var TestClassA[] - * @kphp-serialized-field 0 */ + /** @var TestClassA[] */ public $a = []; - /** @var tuple(TestClassB[]|null, int) - * @kphp-serialized-field 1 */ + /** @var tuple(TestClassB[]|null, int) */ public $b; - /** @var string[] - * @kphp-serialized-field 3 */ + /** @var string[] */ public $huge_arr = []; } -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ class TestClassB { function __construct(int $size, TestClassA $a = null) { while (--$size > 0) { @@ -73,17 +77,14 @@ function __construct(int $size, TestClassA $a = null) { } } - /** @var string[] - * @kphp-serialized-field 0 */ + /** @var string[] */ public $arr = []; - /** @var tuple(string, TestClassA)|null - * @kphp-serialized-field 1 */ + /** @var tuple(string, TestClassA)|null */ public $a = null; } -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ class TestClassC { function __construct(int $size, TestClassB $b = null) { while (--$size > 0) { @@ -91,23 +92,21 @@ function __construct(int $size, TestClassB $b = null) { } if ($b) { -// $this->b = shape([ -// 'key' => TestClassB::class, -// 'value' => tuple(0.1, [$b, $b, $b, $b]) -// ]); + $this->b = shape([ + 'key' => TestClassB::class, + 'value' => tuple(0.1, [$b, $b, $b, $b]) + ]); } } - /** @var string[] - * @kphp-serialized-field 0 */ + /** @var string[] */ public $arr = []; -// /** @var shape(key:string, value:tuple(double, TestClassB[]|null)|false)|null */ -// public $b = null; + /** @var shape(key:string, value:tuple(double, TestClassB[]|null)|false)|null */ + public $b = null; } -/** @kphp-immutable-class - * @kphp-serializable */ +/** @kphp-immutable-class */ class TestClassABC { function __construct() { $a1 = new TestClassA(); @@ -124,14 +123,11 @@ function __construct() { $this->c = [$c1, new TestClassC(15, $this->b[1])]; } - /** @var TestClassA[] - * @kphp-serialized-field 0 */ + /** @var TestClassA[] */ public $a = []; - /** @var TestClassB[] - * @kphp-serialized-field 1 */ + /** @var TestClassB[] */ public $b = []; - /** @var TestClassC[] - * @kphp-serialized-field 2 */ + /** @var TestClassC[] */ public $c = []; } @@ -147,7 +143,7 @@ function test_fetch_and_verify() { echo json_encode([ "a" => $instance->a[0] === $instance->a[2]->a[0], "b" => $instance->b[0] === $instance->b[1]->a[1]->b[0][0], -// "c" => $instance->c[0] === $instance->a[2]->a[1]->ab["c"]["arr"][1], + "c" => $instance->c[0] === $instance->a[2]->a[1]->ab["c"]["arr"][1], ]); } diff --git a/tests/python/tests/instance_cache/test_polymorphic.py b/tests/python/tests/instance_cache/test_polymorphic.py index 8845472996..3c0cd15e40 100644 --- a/tests/python/tests/instance_cache/test_polymorphic.py +++ b/tests/python/tests/instance_cache/test_polymorphic.py @@ -2,7 +2,6 @@ from python.lib.testcase import WebServerAutoTestCase -@pytest.mark.skip @pytest.mark.k2_skip_suite class TestPolymorphic(WebServerAutoTestCase): diff --git a/tests/python/tests/instance_cache/test_store_fetch_delete.py b/tests/python/tests/instance_cache/test_store_fetch_delete.py index f23a52d806..8c983f6202 100644 --- a/tests/python/tests/instance_cache/test_store_fetch_delete.py +++ b/tests/python/tests/instance_cache/test_store_fetch_delete.py @@ -18,12 +18,7 @@ def test_store_fetch_delete(self): uri="/fetch_and_verify", json={"key": "key{}".format(i)}) self.assertEqual(resp.status_code, 200) - self.assertEqual( - resp.json(), - { - "a": True, "b": True # , "c": True - } - ) + self.assertEqual(resp.json(), {"a": True, "b": True, "c": True}) resp = self.web_server.http_post( uri="/delete", diff --git a/tests/python/tests/job_workers/php/SharedMemoryPieceCopying/SomeContext.php b/tests/python/tests/job_workers/php/SharedMemoryPieceCopying/SomeContext.php index 83f98b65e5..686a557d2a 100644 --- a/tests/python/tests/job_workers/php/SharedMemoryPieceCopying/SomeContext.php +++ b/tests/python/tests/job_workers/php/SharedMemoryPieceCopying/SomeContext.php @@ -4,12 +4,10 @@ /** * @kphp-immutable-class - * @kphp-serializable */ class SomeContext { /** * @var int[] - * @kphp-serialized-field 0 */ public $some_data = []; diff --git a/tests/python/tests/job_workers/php/SyncJobCommand.php b/tests/python/tests/job_workers/php/SyncJobCommand.php index 453b572212..b073e77258 100644 --- a/tests/python/tests/job_workers/php/SyncJobCommand.php +++ b/tests/python/tests/job_workers/php/SyncJobCommand.php @@ -1,9 +1,7 @@ payload = "i'm in shared memory"; From fecbd49bc634fe362550c0a6ea93381ff0d88eb4 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 13:28:50 +0300 Subject: [PATCH 02/21] fix --- .../stdlib/instance-cache/visitors/instance-deep-copy-visitor.h | 2 +- .../instance-cache/visitors/instance-deep-size-count-visitor.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h index 3fdf981888..ac5fbf4d14 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h @@ -21,7 +21,7 @@ namespace kphp::visitors { // every reachable array/string/instance body is recreated inside the block via memory_pool, and the original's // fields are rewritten to point at the copies; all copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache), // so they are never freed individually — the owner of the block controls their lifetime -class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_visitor { +class instance_deep_copy_visitor : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h index 96e09d0297..336c3222c1 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h @@ -15,7 +15,7 @@ namespace kphp::visitors { // computes how many bytes instance_deep_copy_visitor will carve for an instance graph, // so that the destination memory block can be allocated upfront; the two visitors must stay in sync -class instance_deep_size_count_visitor final : kphp::visitors::instance_deep_basic_visitor { +class instance_deep_size_count_visitor : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; From 58be4d8bc6cf46611f7d2d7341a212b0f26c7dbc Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 15:11:39 +0300 Subject: [PATCH 03/21] added kphp::visitors to C and C* --- compiler/code-gen/declarations.cpp | 4 ++-- runtime-light/stdlib/diagnostics/exception-types.h | 9 +++++++++ .../visitors/instance-deep-copy-visitor.h | 2 +- .../visitors/instance-deep-size-count-visitor.h | 2 +- runtime-light/stdlib/rpc/rpc-tl-function.h | 13 +++++++++++++ 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/compiler/code-gen/declarations.cpp b/compiler/code-gen/declarations.cpp index e0c3165d4c..9cd1e5b023 100644 --- a/compiler/code-gen/declarations.cpp +++ b/compiler/code-gen/declarations.cpp @@ -909,7 +909,7 @@ void ClassDeclaration::compile_accept_json_visitor(CodeGenerator &W, ClassPtr kl void ClassDeclaration::compile_accept_visitor_methods(CodeGenerator &W, ClassPtr klass) { bool need_generic_accept = - klass->need_to_array_debug_visitor || klass->need_instance_cache_visitors || (klass->need_instance_memory_estimate_visitor); + klass->need_to_array_debug_visitor || klass->need_instance_cache_visitors || klass->need_instance_memory_estimate_visitor; if (!need_generic_accept && klass->json_encoders.empty()) { return; @@ -1071,7 +1071,7 @@ void ClassDeclaration::compile_job_worker_shared_memory_piece_methods(CodeGenera void ClassMembersDefinition::compile(CodeGenerator &W) const { bool need_generic_accept = - klass->need_to_array_debug_visitor || klass->need_instance_cache_visitors || (klass->need_instance_memory_estimate_visitor); + klass->need_to_array_debug_visitor || klass->need_instance_cache_visitors || klass->need_instance_memory_estimate_visitor; if (!need_generic_accept && !klass->is_serializable && klass->json_encoders.empty()) { return; diff --git a/runtime-light/stdlib/diagnostics/exception-types.h b/runtime-light/stdlib/diagnostics/exception-types.h index 5c09359a27..f66b9e87cc 100644 --- a/runtime-light/stdlib/diagnostics/exception-types.h +++ b/runtime-light/stdlib/diagnostics/exception-types.h @@ -21,6 +21,11 @@ class InstanceDeepCopyVisitor; class InstanceDeepDestroyVisitor; class InstanceReferencesCountingVisitor; +namespace kphp::visitors { +class instance_deep_copy_visitor; +class instance_deep_size_count_visitor; +} // namespace kphp::visitors + // ================================================================================================ struct C$Throwable : public refcountable_polymorphic_php_classes_virt<> { @@ -58,6 +63,10 @@ struct C$Throwable : public refcountable_polymorphic_php_classes_virt<> { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + + virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual void accept(ToArrayVisitor& visitor) noexcept { generic_accept(visitor); // don't process raw_trace because `mixed` can't store `void *` (to_array_debug returns array) } diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h index ac5fbf4d14..3fdf981888 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h @@ -21,7 +21,7 @@ namespace kphp::visitors { // every reachable array/string/instance body is recreated inside the block via memory_pool, and the original's // fields are rewritten to point at the copies; all copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache), // so they are never freed individually — the owner of the block controls their lifetime -class instance_deep_copy_visitor : kphp::visitors::instance_deep_basic_visitor { +class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h index 336c3222c1..96e09d0297 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h @@ -15,7 +15,7 @@ namespace kphp::visitors { // computes how many bytes instance_deep_copy_visitor will carve for an instance graph, // so that the destination memory block can be allocated upfront; the two visitors must stay in sync -class instance_deep_size_count_visitor : kphp::visitors::instance_deep_basic_visitor { +class instance_deep_size_count_visitor final : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; diff --git a/runtime-light/stdlib/rpc/rpc-tl-function.h b/runtime-light/stdlib/rpc/rpc-tl-function.h index b4a77f14d1..8748deffe6 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-function.h +++ b/runtime-light/stdlib/rpc/rpc-tl-function.h @@ -19,6 +19,11 @@ class InstanceReferencesCountingVisitor; class InstanceDeepCopyVisitor; class InstanceDeepDestroyVisitor; +namespace kphp::visitors { +class instance_deep_copy_visitor; +class instance_deep_size_count_visitor; +} // namespace kphp::visitors + // The locations of the typed TL related builtin classes that are described in functions.txt // are hardcoded to the folder/namespace \VK\TL because after the code generation // C$VK$TL$... should match that layout @@ -39,6 +44,8 @@ struct C$VK$TL$RpcFunction : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -67,6 +74,8 @@ struct C$VK$TL$RpcFunctionReturnResult : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -94,6 +103,8 @@ struct C$VK$TL$RpcFunctionFetcher : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -115,6 +126,8 @@ struct C$VK$TL$RpcResponse : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} virtual const char* get_class() const noexcept { return "VK\\TL\\RpcResponse"; From 05a1d3a1252f8dc387818403bcb7f13ef1276c41 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 15:49:39 +0300 Subject: [PATCH 04/21] fix --- .../class-instance/class-instance-decl.inl | 1 - .../stdlib/diagnostics/exception-types.h | 20 +++++++++++++++++++ runtime-light/stdlib/job-worker/job-worker.h | 17 ++++++++++++++++ runtime-light/stdlib/rpc/rpc-tl-function.h | 12 +++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/runtime-common/core/class-instance/class-instance-decl.inl b/runtime-common/core/class-instance/class-instance-decl.inl index 6056e76f8f..c308767fe0 100644 --- a/runtime-common/core/class-instance/class-instance-decl.inl +++ b/runtime-common/core/class-instance/class-instance-decl.inl @@ -131,7 +131,6 @@ public: template std::enable_if_t{}, class_instance> virtual_builtin_clone(vk::span memory) const noexcept { - // TODO this is used only for job workers. Should we use this logic for other? class_instance res; if (o) { php_assert(memory.size() >= o->virtual_builtin_sizeof()); diff --git a/runtime-light/stdlib/diagnostics/exception-types.h b/runtime-light/stdlib/diagnostics/exception-types.h index f66b9e87cc..44321fc7fa 100644 --- a/runtime-light/stdlib/diagnostics/exception-types.h +++ b/runtime-light/stdlib/diagnostics/exception-types.h @@ -71,6 +71,18 @@ struct C$Throwable : public refcountable_polymorphic_php_classes_virt<> { generic_accept(visitor); // don't process raw_trace because `mixed` can't store `void *` (to_array_debug returns array) } + virtual size_t virtual_builtin_sizeof() const noexcept { + return 0; + } + + virtual C$Throwable* virtual_builtin_clone() const noexcept { + return nullptr; + } + + virtual C$Throwable* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } + string $message; int64_t $code{}; string $file; @@ -125,6 +137,10 @@ struct C$Exception : public C$Throwable { return new C$Exception{*this}; } + C$Exception* virtual_builtin_construct_at(void* ptr) const noexcept { + return new (ptr) C$Exception{*this}; + } + size_t virtual_builtin_sizeof() const noexcept { return sizeof(*this); } @@ -174,6 +190,10 @@ struct C$Error : public C$Throwable { return new C$Error{*this}; } + C$Error* virtual_builtin_construct_at(void* ptr) const noexcept { + return new (ptr) C$Error{*this}; + } + size_t virtual_builtin_sizeof() const noexcept { return sizeof(*this); } diff --git a/runtime-light/stdlib/job-worker/job-worker.h b/runtime-light/stdlib/job-worker/job-worker.h index e82b2cd4b7..19f71e03fb 100644 --- a/runtime-light/stdlib/job-worker/job-worker.h +++ b/runtime-light/stdlib/job-worker/job-worker.h @@ -19,6 +19,11 @@ inline constexpr int64_t JOB_WORKER_INVALID_JOB_ID = -1; class ToArrayVisitor; class CommonMemoryEstimateVisitor; +namespace kphp::visitors { +class instance_deep_copy_visitor; +class instance_deep_size_count_visitor; +} // namespace kphp::visitors + namespace job_worker_impl_ { struct SendableBase : virtual abstract_refcountable_php_interface { @@ -27,10 +32,15 @@ struct SendableBase : virtual abstract_refcountable_php_interface { virtual void accept(CommonMemoryEstimateVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + + virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual const char* get_class() const noexcept = 0; virtual int32_t get_hash() const noexcept = 0; virtual size_t virtual_builtin_sizeof() const noexcept = 0; virtual SendableBase* virtual_builtin_clone() const noexcept = 0; + virtual SendableBase* virtual_builtin_construct_at(void* /*unused*/) const noexcept = 0; ~SendableBase() override = default; }; @@ -46,18 +56,21 @@ enum class JobWorkerError : int16_t { struct C$KphpJobWorkerSharedMemoryPiece : public job_worker_impl_::SendableBase { C$KphpJobWorkerSharedMemoryPiece* virtual_builtin_clone() const noexcept override = 0; + C$KphpJobWorkerSharedMemoryPiece* virtual_builtin_construct_at(void* /*unused*/) const noexcept override = 0; }; // === KphpJobWorkerRequest ======================================================================= struct C$KphpJobWorkerRequest : public job_worker_impl_::SendableBase { C$KphpJobWorkerRequest* virtual_builtin_clone() const noexcept override = 0; + C$KphpJobWorkerRequest* virtual_builtin_construct_at(void* /*unused*/) const noexcept override = 0; }; // === KphpJobWorkerResponse ====================================================================== struct C$KphpJobWorkerResponse : public job_worker_impl_::SendableBase { C$KphpJobWorkerResponse* virtual_builtin_clone() const noexcept override = 0; + C$KphpJobWorkerResponse* virtual_builtin_construct_at(void* /*unused*/) const noexcept override = 0; }; // === KphpJobWorkerResponseError ================================================================= @@ -82,6 +95,10 @@ struct C$KphpJobWorkerResponseError : public refcountable_polymorphic_php_classe C$KphpJobWorkerResponseError* virtual_builtin_clone() const noexcept override { return new C$KphpJobWorkerResponseError{*this}; } + + C$KphpJobWorkerResponseError* virtual_builtin_construct_at(void* ptr) const noexcept override { + return new (ptr) C$KphpJobWorkerResponseError{*this}; + } }; inline class_instance f$KphpJobWorkerResponseError$$__construct(class_instance v$this) noexcept { diff --git a/runtime-light/stdlib/rpc/rpc-tl-function.h b/runtime-light/stdlib/rpc/rpc-tl-function.h index 8748deffe6..35a35d3ec4 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-function.h +++ b/runtime-light/stdlib/rpc/rpc-tl-function.h @@ -53,6 +53,9 @@ struct C$VK$TL$RpcFunction : abstract_refcountable_php_interface { virtual C$VK$TL$RpcFunction* virtual_builtin_clone() const noexcept { return nullptr; } + virtual C$VK$TL$RpcFunction* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } ~C$VK$TL$RpcFunction() override = default; virtual std::unique_ptr store() const = 0; @@ -83,6 +86,9 @@ struct C$VK$TL$RpcFunctionReturnResult : abstract_refcountable_php_interface { virtual C$VK$TL$RpcFunctionReturnResult* virtual_builtin_clone() const noexcept { return nullptr; } + virtual C$VK$TL$RpcFunctionReturnResult* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } ~C$VK$TL$RpcFunctionReturnResult() override = default; }; @@ -112,6 +118,9 @@ struct C$VK$TL$RpcFunctionFetcher : abstract_refcountable_php_interface { virtual C$VK$TL$RpcFunctionFetcher* virtual_builtin_clone() const noexcept { return nullptr; } + virtual C$VK$TL$RpcFunctionFetcher* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } ~C$VK$TL$RpcFunctionFetcher() override = default; }; @@ -143,6 +152,9 @@ struct C$VK$TL$RpcResponse : abstract_refcountable_php_interface { virtual C$VK$TL$RpcResponse* virtual_builtin_clone() const noexcept { return nullptr; } + virtual C$VK$TL$RpcResponse* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } ~C$VK$TL$RpcResponse() override = default; }; From 55e5de702b66da59f491a11f063b300ecff064bc Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 16:00:04 +0300 Subject: [PATCH 05/21] xif --- runtime-light/stdlib/diagnostics/exception-types.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/runtime-light/stdlib/diagnostics/exception-types.h b/runtime-light/stdlib/diagnostics/exception-types.h index 44321fc7fa..3083465f6f 100644 --- a/runtime-light/stdlib/diagnostics/exception-types.h +++ b/runtime-light/stdlib/diagnostics/exception-types.h @@ -133,15 +133,15 @@ inline void exception_initialize(const Throwable& e, const string& message, int6 struct C$Exception : public C$Throwable { ~C$Exception() override = default; - C$Exception* virtual_builtin_clone() const noexcept { + C$Exception* virtual_builtin_clone() const noexcept override { return new C$Exception{*this}; } - C$Exception* virtual_builtin_construct_at(void* ptr) const noexcept { + C$Exception* virtual_builtin_construct_at(void* ptr) const noexcept override { return new (ptr) C$Exception{*this}; } - size_t virtual_builtin_sizeof() const noexcept { + size_t virtual_builtin_sizeof() const noexcept override { return sizeof(*this); } @@ -186,15 +186,15 @@ inline string f$Exception$$getTraceAsString(const Exception& e) noexcept { struct C$Error : public C$Throwable { ~C$Error() override = default; - C$Error* virtual_builtin_clone() const noexcept { + C$Error* virtual_builtin_clone() const noexcept override { return new C$Error{*this}; } - C$Error* virtual_builtin_construct_at(void* ptr) const noexcept { + C$Error* virtual_builtin_construct_at(void* ptr) const noexcept override { return new (ptr) C$Error{*this}; } - size_t virtual_builtin_sizeof() const noexcept { + size_t virtual_builtin_sizeof() const noexcept override { return sizeof(*this); } From 9c49bd869fa2c8dc1c8f32c74030099b5399038a Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 17:01:21 +0300 Subject: [PATCH 06/21] fix? --- .../stdlib/instance-cache/instance-cache-functions.h | 1 - .../instance-cache/visitors/instance-deep-copy-visitor.h | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index e3c9476849..1cdf3cc88b 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h index 3fdf981888..ce4643f04b 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h @@ -41,6 +41,12 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis this->memory_pool.init(memory_pool_buffer.data(), memory_pool_buffer.size()); } + // arrays of Unknown are always empty (the element type was never inferred, so nothing can be written into them); + // this overload also avoids instantiating array's copy constructor, which is ill-formed (convert_to) + bool process(array& /*unused*/) noexcept { + return true; + } + template bool process(array& arr) noexcept { if (arr.is_reference_counter(ExtraRefCnt::for_global_const)) { From 6d1639df6f05cc2c6421da5b6c23274d23c931e6 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 17:21:57 +0300 Subject: [PATCH 07/21] tmp fix --- .../core/core-types/definition/array.inl | 17 ++++++++++++++--- .../visitors/instance-deep-copy-visitor.h | 6 ------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/runtime-common/core/core-types/definition/array.inl b/runtime-common/core/core-types/definition/array.inl index e75000e167..854792fcf0 100644 --- a/runtime-common/core/core-types/definition/array.inl +++ b/runtime-common/core/core-types/definition/array.inl @@ -916,18 +916,29 @@ void array::copy_from(vk::span memory, const array& other) noe template template void array::copy_from_impl(array_inner* new_array, const array& other) noexcept { + // same-type copies don't need convert_to (it's an identity conversion); + // this also keeps array copyable: convert_to is ill-formed, + // as its convert(const T&) and convert(const Unknown&) overloads collide when T is Unknown + static constexpr auto convert_element{[](const T1& value) noexcept -> decltype(auto) { + if constexpr (std::is_same_v) { + return value; + } else { + return convert_to::convert(value); + } + }}; + if (new_array->is_vector()) { uint32_t size = other.p->size; T1* it = reinterpret_cast(other.p->entries()); for (uint32_t i = 0; i < size; i++) { - new_array->push_back_vector_value(convert_to::convert(it[i])); + new_array->push_back_vector_value(convert_element(it[i])); } } else { for (const typename array::array_bucket* it = other.p->begin(); it != other.p->end(); it = other.p->next(it)) { if (other.p->is_string_hash_entry(it)) { - new_array->set_map_value(overwrite_element::YES, it->int_key, it->string_key, convert_to::convert(it->value)); + new_array->set_map_value(overwrite_element::YES, it->int_key, it->string_key, convert_element(it->value)); } else { - new_array->set_map_value(overwrite_element::YES, it->int_key, convert_to::convert(it->value)); + new_array->set_map_value(overwrite_element::YES, it->int_key, convert_element(it->value)); } } } diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h index ce4643f04b..3fdf981888 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h @@ -41,12 +41,6 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis this->memory_pool.init(memory_pool_buffer.data(), memory_pool_buffer.size()); } - // arrays of Unknown are always empty (the element type was never inferred, so nothing can be written into them); - // this overload also avoids instantiating array's copy constructor, which is ill-formed (convert_to) - bool process(array& /*unused*/) noexcept { - return true; - } - template bool process(array& arr) noexcept { if (arr.is_reference_counter(ExtraRefCnt::for_global_const)) { From b4702822fcac6566ae69f11bf830e01a8aa9ac0c Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 17:37:10 +0300 Subject: [PATCH 08/21] added kphp::visitors to DummyVisitorMethods --- runtime-common/stdlib/visitors/dummy-visitor-methods.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/runtime-common/stdlib/visitors/dummy-visitor-methods.h b/runtime-common/stdlib/visitors/dummy-visitor-methods.h index 2758bc009b..fd61a68f9f 100644 --- a/runtime-common/stdlib/visitors/dummy-visitor-methods.h +++ b/runtime-common/stdlib/visitors/dummy-visitor-methods.h @@ -10,6 +10,11 @@ class InstanceDeepCopyVisitor; class InstanceDeepDestroyVisitor; class InstanceReferencesCountingVisitor; +namespace kphp::visitors { +class instance_deep_copy_visitor; +class instance_deep_size_count_visitor; +} // namespace kphp::visitors + struct DummyVisitorMethods { // for f$estimate_memory_usage() // set at compiler at deeply_require_instance_memory_estimate_visitor() @@ -22,4 +27,7 @@ struct DummyVisitorMethods { void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + // K2 counterparts of the instance cache visitors + void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} }; From 3e5ed80e1cdbb2da606be089cabc3d3e01962cea Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 18:15:32 +0300 Subject: [PATCH 09/21] removed useless warnings --- runtime-light/stdlib/instance-cache/instance-cache-functions.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 1cdf3cc88b..1e71fbc3f6 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -88,7 +88,6 @@ ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& } if (mem.size() < hash_size + instance_size) [[unlikely]] { - // the shared memory under this key was not written by instance_cache_store for this class (or is corrupted) kphp::log::warning("instance_cache_fetch. shared memory is too small: size -> {}, expected at least -> {}, key -> {}", mem.size(), hash_size + instance_size, key.c_str()); return {}; @@ -98,7 +97,6 @@ ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& std::memcpy(&stored_class_name_hash, mem.data(), sizeof(stored_class_name_hash)); if (stored_class_name_hash != vk::murmur_hash(class_name.c_str(), class_name.size())) { - kphp::log::warning("instance_cache_fetch. requested class doesn't match the stored one: requested -> {}, key -> {}", class_name.c_str(), key.c_str()); return {}; } From 9f6df0a0ca27538f049779b4794f712961607cff Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 20 Aug 2026 20:01:56 +0300 Subject: [PATCH 10/21] minor fix --- .../stdlib/instance-cache/instance-cache-functions.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 1e71fbc3f6..26c86f327e 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,10 @@ bool f$instance_cache_store(const string& key, class_instance inst kphp::log::warning("instance_cache_store. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl, key.c_str()); ttl = 0; } + if (constexpr int64_t max_ttl{std::numeric_limits::max() / 1000}; ttl > max_ttl) [[unlikely]] { + kphp::log::warning("instance_cache_store. ttl is too large, key will be stored forever: ttl -> {}, max ttl -> {}, key -> {}", ttl, max_ttl, key.c_str()); + ttl = 0; + } kphp::visitors::instance_deep_size_count_visitor instance_deep_size_count_visitor{}; instance_deep_size_count_visitor.process_instance(instance); @@ -105,6 +110,11 @@ ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& } inline bool f$instance_cache_update_ttl(const string& key, int64_t ttl = 0) noexcept { + if (constexpr int64_t max_ttl{std::numeric_limits::max() / 1000}; ttl > max_ttl) [[unlikely]] { + kphp::log::warning("instance_cache_update_ttl. ttl is too large, key will be stored forever: ttl -> {}, max ttl -> {}, key -> {}", ttl, max_ttl, + key.c_str()); + ttl = 0; + } if (ttl < 0) [[unlikely]] { kphp::log::warning("instance_cache_update_ttl. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl, key.c_str()); ttl = 0; From 57ca38411d02683f8ed603c7fb74d6ef19da726d Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Sun, 23 Aug 2026 22:19:36 +0300 Subject: [PATCH 11/21] fix --- .../stdlib/instance-cache/instance-cache-functions.h | 2 +- .../instance-cache/visitors/instance-deep-copy-visitor.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 26c86f327e..1e220c1ffa 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -56,7 +56,7 @@ bool f$instance_cache_store(const string& key, class_instance inst } std::byte* mem{static_cast(alloc_result.value())}; - const uint64_t class_name_hash{static_cast(InstanceType::CLASS_NAME_HASH)}; + const uint64_t class_name_hash{InstanceType::CLASS_NAME_HASH}; std::memcpy(mem, &class_name_hash, hash_size); // deep-copies the object graph into the inner area and rewrites the instance's fields to point at the copies, // so the whole graph ends up inside the shared-memory block; all copies are pinned with ExtraRefCnt::for_instance_cache diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h index 3fdf981888..e9b7813e7d 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h @@ -20,7 +20,7 @@ namespace kphp::visitors { // deep-copies an instance graph into a caller-provided contiguous memory block (e.g. a shared memory region): // every reachable array/string/instance body is recreated inside the block via memory_pool, and the original's // fields are rewritten to point at the copies; all copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache), -// so they are never freed individually — the owner of the block controls their lifetime +// so they are never freed individually -- the owner of the block controls their lifetime class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; @@ -50,7 +50,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis array copied_array{carve(arr.calculate_memory_for_copying()), arr}; const auto commit_copy{vk::finally([&arr, &copied_array]() noexcept { arr = std::move(copied_array); })}; - // copying an empty array yields the global empty-array singleton instead of a real copy — nothing left to deep-copy + // copying an empty array yields the global empty-array singleton instead of a real copy -- nothing left to deep-copy if (copied_array.is_reference_counter(ExtraRefCnt::for_global_const)) { return true; } @@ -61,7 +61,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis } // values of a primitive array were already memcpy'd by the array copy constructor, and there are no string keys to copy const bool primitive_array{is_primitive && copied_array.has_no_string_keys()}; - return primitive_array || Basic::process_range(copied_array.begin(), copied_array.end()); + return primitive_array || Basic::process_range(copied_array.begin_no_mutate(), copied_array.end_no_mutate()); } bool process(string& str) noexcept { From 7df28c909823423bc4de4c249529b8ae94c97d1b Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Mon, 24 Aug 2026 12:34:44 +0300 Subject: [PATCH 12/21] code_style++? --- compiler/code-gen/declarations.cpp | 13 +++- compiler/code-gen/declarations.h | 1 + compiler/pipes/final-check.cpp | 1 + .../class-instance/class-instance-decl.inl | 23 +++--- .../core/class-instance/class-instance.inl | 29 ++++---- .../core/core-types/decl/array_decl.inl | 20 +++-- .../core/core-types/decl/string_decl.inl | 8 +- .../core/core-types/definition/array.inl | 73 ++++++++++--------- .../core/core-types/definition/string.inl | 25 +++++-- .../stdlib/visitors/dummy-visitor-methods.h | 4 +- .../visitors/instance-deep-basic-visitor.h | 6 +- runtime-light/stdlib/array/array-functions.h | 2 +- .../stdlib/diagnostics/exception-types.h | 9 +-- .../instance-cache/instance-cache-functions.h | 37 +++++++--- .../instance-cache/instance-cache-state.h | 2 +- runtime-light/stdlib/job-worker/job-worker.h | 4 +- runtime-light/stdlib/rpc/rpc-tl-function.h | 10 +-- .../visitors/instance-deep-copy-visitor.h | 38 +++++++--- .../instance-deep-estimate-size-visitor.h} | 35 ++++----- 19 files changed, 205 insertions(+), 135 deletions(-) rename runtime-light/stdlib/{instance-cache => }/visitors/instance-deep-copy-visitor.h (76%) rename runtime-light/stdlib/{instance-cache/visitors/instance-deep-size-count-visitor.h => visitors/instance-deep-estimate-size-visitor.h} (62%) diff --git a/compiler/code-gen/declarations.cpp b/compiler/code-gen/declarations.cpp index 9cd1e5b023..3e03cdb62f 100644 --- a/compiler/code-gen/declarations.cpp +++ b/compiler/code-gen/declarations.cpp @@ -5,6 +5,7 @@ #include "compiler/code-gen/declarations.h" #include "common/algorithms/compare.h" +#include "common/algorithms/hashes.h" #include "compiler/code-gen/common.h" #include "compiler/code-gen/const-globals-batched-mem.h" @@ -578,6 +579,7 @@ void ClassDeclaration::compile_inner_methods(CodeGenerator &W, ClassPtr klass) { compile_has_wakeup_flag(W, klass); compile_get_class(W, klass); compile_get_hash(W, klass); + compile_class_name_hash(W, klass); compile_accept_visitor_methods(W, klass); compile_msgpack_declarations(W, klass); compile_virtual_builtin_functions(W, klass); @@ -750,7 +752,12 @@ void ClassDeclaration::compile_get_class(CodeGenerator &W, ClassPtr klass) { void ClassDeclaration::compile_get_hash(CodeGenerator &W, ClassPtr klass) { compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, "int get_hash()", klass->get_hash()); - // static type tag: same value for every object of this class, available without an instance (unlike virtual get_hash()) +} + +void ClassDeclaration::compile_class_name_hash(CodeGenerator &W, ClassPtr klass) { + // static type tag: same value for every object of this class, available without an instance (unlike virtual get_hash()); + // used by the K2 instance cache as part of the shared memory layout, so the hash function must match + // the one in f$instance_cache_fetch -- changing it makes images unable to read each other's entries W << "constexpr static uint64_t CLASS_NAME_HASH{" << vk::murmur_hash(klass->name.data(), klass->name.size()) << "ULL};" << NL << NL; } @@ -945,7 +952,7 @@ void ClassDeclaration::compile_accept_visitor_methods(CodeGenerator &W, ClassPtr W << NL; compile_accept_visitor(W, klass, "kphp::visitors::instance_deep_copy_visitor"); W << NL; - compile_accept_visitor(W, klass, "kphp::visitors::instance_deep_size_count_visitor"); + compile_accept_visitor(W, klass, "kphp::visitors::instance_deep_estimate_size_visitor"); } compile_accept_json_visitor(W, klass); @@ -1116,7 +1123,7 @@ void ClassMembersDefinition::compile(CodeGenerator &W) const { W << NL; compile_generic_accept_instantiations(W, klass, "kphp::visitors::instance_deep_copy_visitor"); W << NL; - compile_generic_accept_instantiations(W, klass, "kphp::visitors::instance_deep_size_count_visitor"); + compile_generic_accept_instantiations(W, klass, "kphp::visitors::instance_deep_estimate_size_visitor"); } W << NL; diff --git a/compiler/code-gen/declarations.h b/compiler/code-gen/declarations.h index d46adc9cd6..ed1a93a04f 100644 --- a/compiler/code-gen/declarations.h +++ b/compiler/code-gen/declarations.h @@ -125,6 +125,7 @@ struct ClassDeclaration : CodeGenRootCmd { static void compile_has_wakeup_flag(CodeGenerator &W, ClassPtr klass); static void compile_get_class(CodeGenerator &W, ClassPtr klass); static void compile_get_hash(CodeGenerator &W, ClassPtr klass); + static void compile_class_name_hash(CodeGenerator &W, ClassPtr klass); static void compile_accept_visitor_methods(CodeGenerator &W, ClassPtr klass); static void compile_msgpack_declarations(CodeGenerator &W, ClassPtr klass); static void compile_virtual_builtin_functions(CodeGenerator &W, ClassPtr klass); diff --git a/compiler/pipes/final-check.cpp b/compiler/pipes/final-check.cpp index 243d432403..cf20807a01 100644 --- a/compiler/pipes/final-check.cpp +++ b/compiler/pipes/final-check.cpp @@ -136,6 +136,7 @@ void check_instance_cache_fetch_call(VertexAdaptor call) { fmt_format("Can not fetch instance of mutable class {} with instance_cache_fetch call", klass->name)); if (!G->is_output_mode_k2()) { + // in K2 mode fetch just reinterprets the shared memory block, so no visitor codegen is needed klass->deeply_require_instance_cache_visitor(); } } diff --git a/runtime-common/core/class-instance/class-instance-decl.inl b/runtime-common/core/class-instance/class-instance-decl.inl index c308767fe0..ca12962ee9 100644 --- a/runtime-common/core/class-instance/class-instance-decl.inl +++ b/runtime-common/core/class-instance/class-instance-decl.inl @@ -76,14 +76,15 @@ public: inline class_instance clone() const; // constructs a copy of the instance in externally provided memory (no allocation, no ownership): // memory must be 8-byte aligned and have at least estimate_memory_usage() bytes; - // the instance never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache) - inline class_instance clone(vk::span memory) const noexcept; + // the instance never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache); + // returns a null instance if the memory is insufficient or misaligned + inline class_instance clone_in(vk::span memory) const noexcept; template inline class_instance alloc(Args&&... args) __attribute__((always_inline)); // constructs an instance in externally provided memory (no allocation, no ownership): - // memory must be 8-byte aligned and have at least sizeof(T) bytes + // leaves the instance null if the memory is smaller than sizeof(T) or not aligned to alignof(T) template - inline class_instance alloc(vk::span memory, Args&&... args) __attribute__((always_inline)); + inline class_instance alloc(vk::span memory, Args&&... args) noexcept __attribute__((always_inline)); inline class_instance empty_alloc() __attribute__((always_inline)); inline void destroy() { o.reset(); @@ -114,8 +115,8 @@ public: } template - std::enable_if_t{}, class_instance> virtual_builtin_clone(vk::span memory) const noexcept { - return clone(memory); + std::enable_if_t{}, class_instance> virtual_builtin_clone_in(vk::span memory) const noexcept { + return clone_in(memory); } template @@ -130,10 +131,12 @@ public: } template - std::enable_if_t{}, class_instance> virtual_builtin_clone(vk::span memory) const noexcept { + std::enable_if_t{}, class_instance> virtual_builtin_clone_in(vk::span memory) const noexcept { class_instance res; if (o) { - php_assert(memory.size() >= o->virtual_builtin_sizeof()); + if (memory.size() < o->virtual_builtin_sizeof() || reinterpret_cast(memory.data()) % alignof(S) != 0) [[unlikely]] { + return res; + } res.o = vk::intrusive_ptr{o->virtual_builtin_construct_at(memory.data())}; res.o->set_refcnt(1); } @@ -224,9 +227,9 @@ public: private: class_instance clone_impl(std::true_type /*is empty*/) const; - class_instance clone_impl(vk::span memory, std::true_type /*is empty*/) const noexcept; class_instance clone_impl(std::false_type /*is empty*/) const; - class_instance clone_impl(vk::span memory, std::false_type /*is empty*/) const noexcept; + class_instance clone_in_impl(vk::span memory, std::true_type /*is empty*/) const noexcept; + class_instance clone_in_impl(vk::span memory, std::false_type /*is empty*/) const noexcept; }; template diff --git a/runtime-common/core/class-instance/class-instance.inl b/runtime-common/core/class-instance/class-instance.inl index 20b0522698..81c7ec3cdd 100644 --- a/runtime-common/core/class-instance/class-instance.inl +++ b/runtime-common/core/class-instance/class-instance.inl @@ -16,11 +16,6 @@ class_instance class_instance::clone_impl(std::true_type /*is empty*/) con return class_instance{}.empty_alloc(); } -template -class_instance class_instance::clone_impl(vk::span /* memory */, std::true_type /*is empty*/) const noexcept { - return class_instance{}.empty_alloc(); -} - template class_instance class_instance::clone_impl(std::false_type /*is empty*/) const { class_instance res; @@ -32,11 +27,18 @@ class_instance class_instance::clone_impl(std::false_type /*is empty*/) co } template -class_instance class_instance::clone_impl(vk::span memory, std::false_type /*is empty*/) const noexcept { +class_instance class_instance::clone_in_impl(vk::span /* memory */, std::true_type /*is empty*/) const noexcept { + return class_instance{}.empty_alloc(); +} + +template +class_instance class_instance::clone_in_impl(vk::span memory, std::false_type /*is empty*/) const noexcept { class_instance res; if (o) { - res.alloc(memory, *o); - res.o->set_refcnt(1); + res.alloc(memory, *o); // res stays null if the memory is insufficient or misaligned + if (!res.is_null()) [[likely]] { + res.o->set_refcnt(1); + } } return res; } @@ -46,8 +48,8 @@ class_instance class_instance::clone() const { return clone_impl(std::is_empty{}); } template -class_instance class_instance::clone(vk::span memory) const noexcept { - return clone_impl(memory, std::is_empty{}); +class_instance class_instance::clone_in(vk::span memory) const noexcept { + return clone_in_impl(memory, std::is_empty{}); } template @@ -61,11 +63,12 @@ class_instance class_instance::alloc(Args&&... args) { template template -class_instance class_instance::alloc(vk::span memory, Args&&... args) { +class_instance class_instance::alloc(vk::span memory, Args&&... args) noexcept { static_assert(!std::is_empty{}, "class T may not be empty"); php_assert(!o); - php_assert(memory.size() >= sizeof(T)); - php_assert(reinterpret_cast(memory.data()) % alignof(T) == 0); + if (memory.size() < sizeof(T) || reinterpret_cast(memory.data()) % alignof(T) != 0) [[unlikely]] { + return *this; + } T* ptr = new (memory.data()) T{std::forward(args)...}; new (&o) vk::intrusive_ptr(ptr); return *this; diff --git a/runtime-common/core/core-types/decl/array_decl.inl b/runtime-common/core/core-types/decl/array_decl.inl index 3d03ca1ee5..69bce58016 100644 --- a/runtime-common/core/core-types/decl/array_decl.inl +++ b/runtime-common/core/core-types/decl/array_decl.inl @@ -5,6 +5,7 @@ #pragma once #include +#include #include "common/wrappers/span.h" #include "runtime-common/core/core-types/decl/array_iterator.h" @@ -193,11 +194,17 @@ private: int64_t int_size_{0}; bool is_vector_{false}; + inline allocation(vk::span memory, int64_t int_size, bool is_vector) noexcept + : mem_{memory}, + int_size_{int_size}, + is_vector_{is_vector} {} + public: - inline allocation(int64_t new_int_size, bool is_vector) noexcept; + // allocates script memory for an array of the given size + inline static allocation allocate(int64_t new_int_size, bool is_vector) noexcept; // takes ownership of the beginning of externally provided memory (no allocation): - // memory must be 8-byte aligned and have at least estimate_size(new_int_size, is_vector) bytes - inline allocation(vk::span memory, int64_t new_int_size, bool is_vector) noexcept; + // returns std::nullopt if the memory is smaller than estimate_size(new_int_size, is_vector) or misaligned + inline static std::optional from_external(vk::span memory, int64_t new_int_size, bool is_vector) noexcept; vk::span memory() const noexcept { return mem_; @@ -226,7 +233,7 @@ private: inline void copy_from(const array& other) noexcept; template - inline void copy_from(vk::span memory, const array& other) noexcept; + inline bool copy_from(vk::span memory, const array& other) noexcept; template inline void copy_from_impl(array_inner* new_array, const array& other) noexcept; @@ -262,8 +269,9 @@ public: // constructs a copy of other in externally provided memory (no allocation, no ownership): // memory must be 8-byte aligned and have at least other.calculate_memory_for_copying() bytes; - // the array never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache) - inline array(vk::span memory, const array& other) noexcept; + // the array never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache); + // returns std::nullopt if the memory is insufficient or misaligned + inline static std::optional copy_in(vk::span memory, const array& other) noexcept; template inline static array create(Args&&... args) __attribute__((always_inline)); diff --git a/runtime-common/core/core-types/decl/string_decl.inl b/runtime-common/core/core-types/decl/string_decl.inl index 46ad3fb11c..ecde2a87eb 100644 --- a/runtime-common/core/core-types/decl/string_decl.inl +++ b/runtime-common/core/core-types/decl/string_decl.inl @@ -1,6 +1,7 @@ #pragma once #include +#include #include "common/wrappers/span.h" @@ -91,7 +92,7 @@ private: friend class string_cache; - inline void copy_from(vk::span memory, const string& other) noexcept; + inline bool copy_from(vk::span memory, const string& other) noexcept; public: static constexpr size_type max_size() noexcept { @@ -111,8 +112,9 @@ public: inline string(string&& str) noexcept; // constructs a copy of str in externally provided memory (no allocation, no ownership): // memory must be 8-byte aligned and have at least str.estimate_memory_usage() bytes (an upper bound of the copy's footprint); - // the string never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache) - inline string(vk::span memory, const string& str) noexcept; + // the string never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache); + // returns std::nullopt if the memory is insufficient or misaligned + inline static std::optional copy_in(vk::span memory, const string& str) noexcept; inline string(const char* s, size_type n); inline explicit string(const char* s); // IMPORTANT: this constructor may return read-only strings for n == 0 and n == 1. diff --git a/runtime-common/core/core-types/definition/array.inl b/runtime-common/core/core-types/definition/array.inl index 854792fcf0..8dce6f631c 100644 --- a/runtime-common/core/core-types/definition/array.inl +++ b/runtime-common/core/core-types/definition/array.inl @@ -247,23 +247,20 @@ size_t array::array_inner::estimate_size(int64_t& new_int_size, bool is_vecto } template -array::allocation::allocation(int64_t new_int_size, bool is_vector) noexcept - : int_size_(new_int_size), - is_vector_(is_vector) { - const size_t mem_size = array_inner::estimate_size(int_size_, is_vector_); +typename array::allocation array::allocation::allocate(int64_t new_int_size, bool is_vector) noexcept { + const size_t mem_size = array_inner::estimate_size(new_int_size, is_vector); auto* raw_mem = - static_cast(is_vector_ ? RuntimeAllocator::get().alloc_script_memory(mem_size) : RuntimeAllocator::get().alloc0_script_memory(mem_size)); - mem_ = vk::span{raw_mem, mem_size}; + static_cast(is_vector ? RuntimeAllocator::get().alloc_script_memory(mem_size) : RuntimeAllocator::get().alloc0_script_memory(mem_size)); + return allocation{vk::span{raw_mem, mem_size}, new_int_size, is_vector}; } template -array::allocation::allocation(vk::span memory, int64_t new_int_size, bool is_vector) noexcept - : int_size_(new_int_size), - is_vector_(is_vector) { - const size_t mem_size = array_inner::estimate_size(int_size_, is_vector_); - php_assert(memory.size() >= mem_size); - php_assert(reinterpret_cast(memory.data()) % alignof(array_inner) == 0); - mem_ = memory.first(mem_size); +std::optional::allocation> array::allocation::from_external(vk::span memory, int64_t new_int_size, bool is_vector) noexcept { + const size_t mem_size = array_inner::estimate_size(new_int_size, is_vector); + if (memory.size() < mem_size || reinterpret_cast(memory.data()) % alignof(array_inner) != 0) [[unlikely]] { + return std::nullopt; + } + return allocation{memory.first(mem_size), new_int_size, is_vector}; } template @@ -691,7 +688,7 @@ bool array::mutate_if_vector_shared(uint32_t mul) { template bool array::mutate_to_size_if_vector_shared(int64_t int_size) { if (p->ref_cnt > 0) { - array_inner* new_array = create_from_allocation(allocation{int_size, true}); + array_inner* new_array = create_from_allocation(allocation::allocate(int_size, true)); const auto size = static_cast(p->size); T* it = (T*)p->entries(); @@ -710,7 +707,7 @@ bool array::mutate_to_size_if_vector_shared(int64_t int_size) { template bool array::mutate_if_map_shared(uint32_t mul) { if (p->ref_cnt > 0) { - array_inner* new_array = create_from_allocation(allocation{p->size * mul + 1, false}); + array_inner* new_array = create_from_allocation(allocation::allocate(p->size * mul + 1, false)); for (const array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { if (p->is_string_hash_entry(it)) { @@ -760,7 +757,7 @@ void array::mutate_if_map_needs_space() { // not shared (ref_cnt == 0) if (p->size * 5 > 3 * p->buf_size) { int64_t new_int_size = p->size * 2 + 1; - array_inner* new_array = create_from_allocation(allocation{new_int_size, false}); + array_inner* new_array = create_from_allocation(allocation::allocate(new_int_size, false)); for (array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { if (p->is_string_hash_entry(it)) { @@ -791,7 +788,7 @@ void array::reserve(int64_t int_size, bool make_vector_if_possible) { mutate_to_size(int_size); } else { const int64_t new_int_size = std::max(int_size, int64_t{p->buf_size}); - array_inner* new_array = create_from_allocation(allocation{new_int_size, false}); + array_inner* new_array = create_from_allocation(allocation::allocate(new_int_size, false)); if (is_vector()) { for (uint32_t it = 0; it != p->size; it++) { @@ -866,7 +863,7 @@ typename array::iterator array::end() { template void array::convert_to_map() { - array_inner* new_array = create_from_allocation(allocation{p->size + 4, false}); + array_inner* new_array = create_from_allocation(allocation::allocate(p->size + 4, false)); T* elements = reinterpret_cast(p->entries()); const bool move_values = p->ref_cnt == 0; @@ -894,23 +891,27 @@ void array::copy_from(const array& other) noexcept { return; } - copy_from_impl(create_from_allocation(allocation{other.p->size, other.is_vector()}), other); + copy_from_impl(create_from_allocation(allocation::allocate(other.p->size, other.is_vector())), other); } template template -void array::copy_from(vk::span memory, const array& other) noexcept { +bool array::copy_from(vk::span memory, const array& other) noexcept { if (other.empty()) { p = array_inner::empty_array(); - return; + return true; } + auto alloc{allocation::from_external(memory, other.p->size, other.is_vector())}; + if (!alloc.has_value()) [[unlikely]] { + return false; + } if (!other.is_vector()) { // map allocations require zeroed memory (hash entries use null pointers as empty markers) - int64_t int_size = other.p->size; - std::memset(memory.data(), 0, array_inner::estimate_size(int_size, false)); + std::memset(alloc->memory().data(), 0, alloc->memory().size()); } - copy_from_impl(create_from_allocation(allocation{memory, other.p->size, other.is_vector()}), other); + copy_from_impl(create_from_allocation(*alloc), other); + return true; } template @@ -962,7 +963,7 @@ void array::move_from(array&& other) noexcept { return; } - array_inner* new_array = create_from_allocation(allocation{other.p->size, other.is_vector()}); + array_inner* new_array = create_from_allocation(allocation::allocate(other.p->size, other.is_vector())); if (new_array->is_vector()) { uint32_t size = other.p->size; @@ -1000,7 +1001,7 @@ array::array() template array::array(const array_size& s) - : p(create_from_allocation(allocation{s.size, s.is_vector})) {} + : p(create_from_allocation(allocation::allocate(s.size, s.is_vector))) {} template template @@ -1034,8 +1035,12 @@ array::array(array&& other) noexcept { } template -array::array(vk::span memory, const array& other) noexcept { - copy_from(memory, other); +std::optional> array::copy_in(vk::span memory, const array& other) noexcept { + array res; + if (!res.copy_from(memory, other)) [[unlikely]] { + return std::nullopt; + } + return res; } template @@ -1709,7 +1714,7 @@ array& array::operator+=(const array& other) { uint32_t my_size = p->size; T* my_it = (T*)p->entries(); - array_inner* new_array = create_from_allocation(allocation{max(size, my_size), true}); + array_inner* new_array = create_from_allocation(allocation::allocate(max(size, my_size), true)); for (uint32_t i = 0; i < my_size; i++) { new_array->push_back_vector_value(my_it[i]); @@ -1733,7 +1738,7 @@ array& array::operator+=(const array& other) { return *this; } else { - array_inner* new_array = create_from_allocation(allocation{p->size + other.p->size + 4, false}); + array_inner* new_array = create_from_allocation(allocation::allocate(p->size + other.p->size + 4, false)); T* it = (T*)p->entries(); for (uint32_t i = 0; i != p->size; i++) { @@ -1751,7 +1756,7 @@ array& array::operator+=(const array& other) { uint32_t new_int_size = p->size + other.p->size; if (new_int_size * 5 > 3 * p->buf_size || p->ref_cnt > 0) { - array_inner* new_array = create_from_allocation(allocation{max(new_int_size, 2 * p->size) + 1, false}); + array_inner* new_array = create_from_allocation(allocation::allocate(max(new_int_size, 2 * p->size) + 1, false)); for (const array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { if (p->is_string_hash_entry(it)) { @@ -1930,7 +1935,7 @@ void array::sort(const T1& compare, bool renumber) { } if (!is_vector()) { - array_inner* res = create_from_allocation(allocation{n, true}); + array_inner* res = create_from_allocation(allocation::allocate(n, true)); for (array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { res->push_back_vector_value(it->value); } @@ -2087,7 +2092,7 @@ T array::shift() { array_size new_size = size().cut(count() - 1); const bool is_v = p->has_no_string_keys(); - array_inner* new_array = create_from_allocation(allocation{new_size.size, is_v}); + array_inner* new_array = create_from_allocation(allocation::allocate(new_size.size, is_v)); array_bucket* it = p->begin(); T res = it->value; @@ -2126,7 +2131,7 @@ int64_t array::unshift(const T& val) { array_size new_size = size(); const bool is_v = p->has_no_string_keys(); - array_inner* new_array = create_from_allocation(allocation{new_size.size + 1, is_v}); + array_inner* new_array = create_from_allocation(allocation::allocate(new_size.size + 1, is_v)); array_bucket* it = p->begin(); if (is_v) { diff --git a/runtime-common/core/core-types/definition/string.inl b/runtime-common/core/core-types/definition/string.inl index 39239b6cc4..cae11abf24 100644 --- a/runtime-common/core/core-types/definition/string.inl +++ b/runtime-common/core/core-types/definition/string.inl @@ -68,8 +68,9 @@ string::string_inner* string::string_inner::create(size_type requested_capacity, string::string_inner* string::string_inner::create(vk::span memory, size_type requested_capacity, size_type old_capacity) noexcept { size_type capacity = new_capacity(requested_capacity, old_capacity); size_type new_size = (size_type)(sizeof(string_inner) + (capacity + 1)); - php_assert(memory.size() >= new_size); - php_assert(reinterpret_cast(memory.data()) % alignof(string_inner) == 0); + if (memory.size() < new_size || reinterpret_cast(memory.data()) % alignof(string_inner) != 0) [[unlikely]] { + return nullptr; + } string_inner* p = (string_inner*)memory.data(); p->capacity = capacity; return p; @@ -123,6 +124,9 @@ char* string::string_inner::clone(size_type requested_cap) noexcept { char* string::string_inner::clone(vk::span memory, size_type requested_cap) noexcept { string_inner* r = string_inner::create(memory, requested_cap, capacity); + if (r == nullptr) [[unlikely]] { + return nullptr; + } if (size) { memcpy(r->ref_data(), ref_data(), size); } @@ -208,9 +212,12 @@ string::string(string&& str) noexcept str.p = string_cache::empty_string().ref_data(); } -string::string(vk::span memory, const string& str) noexcept - : p{} { - copy_from(memory, str); +std::optional string::copy_in(vk::span memory, const string& str) noexcept { + string res; + if (!res.copy_from(memory, str)) [[unlikely]] { + return std::nullopt; + } + return res; } string::string(const char* s, size_type n) @@ -350,8 +357,12 @@ string string::copy_and_make_not_shared() const { return result; } -void string::copy_from(vk::span memory, const string& other) noexcept { - p = other.inner()->clone(memory, other.size()); +bool string::copy_from(vk::span memory, const string& other) noexcept { + if (char* new_p{other.inner()->clone(memory, other.size())}; new_p != nullptr) [[likely]] { + p = new_p; + return true; + } + return false; } void string::force_reserve(size_type res) { diff --git a/runtime-common/stdlib/visitors/dummy-visitor-methods.h b/runtime-common/stdlib/visitors/dummy-visitor-methods.h index fd61a68f9f..1b7377425f 100644 --- a/runtime-common/stdlib/visitors/dummy-visitor-methods.h +++ b/runtime-common/stdlib/visitors/dummy-visitor-methods.h @@ -12,7 +12,7 @@ class InstanceReferencesCountingVisitor; namespace kphp::visitors { class instance_deep_copy_visitor; -class instance_deep_size_count_visitor; +class instance_deep_estimate_size_visitor; } // namespace kphp::visitors struct DummyVisitorMethods { @@ -29,5 +29,5 @@ struct DummyVisitorMethods { void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} // K2 counterparts of the instance cache visitors void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} - void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} }; diff --git a/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h index 39b7cb0ea6..9f48a0730c 100644 --- a/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h +++ b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h @@ -10,12 +10,13 @@ #include #include "common/mixin/not_copyable.h" +#include "common/type_traits/list_of_types.h" #include "runtime-common/core/runtime-core.h" namespace kphp::visitors { // high bit of the per-instance refcnt info word used by the legacy counting/destroy visitors: // the lower bits store the real reference count, the top bit marks the instance as visited -constexpr uint32_t VISITED_INSTANCE_MASK{0x80000000}; +inline constexpr uint32_t VISITED_INSTANCE_MASK{0x80000000}; // CRTP base for visitors that traverse an instance graph via compiler-generated accept() methods: // every field is dispatched to Child::process; a false result from any field is accumulated into is_ok() @@ -76,6 +77,9 @@ class instance_deep_basic_visitor : vk::not_copyable { } protected: + template + static constexpr bool is_primitive{vk::is_type_in_list, Optional, Optional>::value}; + explicit instance_deep_basic_visitor(Child& child, ExtraRefCnt memory_ref_cnt = ExtraRefCnt::extra_ref_cnt_value(0)) noexcept : memory_ref_cnt_{memory_ref_cnt}, child_{child} {} diff --git a/runtime-light/stdlib/array/array-functions.h b/runtime-light/stdlib/array/array-functions.h index b0dbb8c7bc..d71e6e3199 100644 --- a/runtime-light/stdlib/array/array-functions.h +++ b/runtime-light/stdlib/array/array-functions.h @@ -87,7 +87,7 @@ Result async_sort(array& arr, Comparator comparator, bool renumber) noexcept } if (!arr.is_vector()) { - array_inner* res{array::create_from_allocation(array_allocation{n, true})}; + array_inner* res{array::create_from_allocation(array_allocation::allocate(n, true))}; for (array_bucket* it{arr.p->begin()}; it != arr.p->end(); it = arr.p->next(it)) { res->push_back_vector_value(it->value); } diff --git a/runtime-light/stdlib/diagnostics/exception-types.h b/runtime-light/stdlib/diagnostics/exception-types.h index 3083465f6f..2c36c21e09 100644 --- a/runtime-light/stdlib/diagnostics/exception-types.h +++ b/runtime-light/stdlib/diagnostics/exception-types.h @@ -16,16 +16,13 @@ #include "runtime-common/stdlib/visitors/memory-visitors.h" #include "runtime-light/stdlib/diagnostics/error-handling-functions.h" #include "runtime-light/stdlib/visitors/array-visitors.h" +#include "runtime-light/stdlib/visitors/instance-deep-copy-visitor.h" +#include "runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h" class InstanceDeepCopyVisitor; class InstanceDeepDestroyVisitor; class InstanceReferencesCountingVisitor; -namespace kphp::visitors { -class instance_deep_copy_visitor; -class instance_deep_size_count_visitor; -} // namespace kphp::visitors - // ================================================================================================ struct C$Throwable : public refcountable_polymorphic_php_classes_virt<> { @@ -65,7 +62,7 @@ struct C$Throwable : public refcountable_polymorphic_php_classes_virt<> { virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} - virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual void accept(ToArrayVisitor& visitor) noexcept { generic_accept(visitor); // don't process raw_trace because `mixed` can't store `void *` (to_array_debug returns array) diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 1e220c1ffa..07c0736dd1 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -18,12 +18,11 @@ #include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/instance-cache/instance-cache-state.h" -#include "runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h" -#include "runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h" +#include "runtime-light/stdlib/visitors/instance-deep-copy-visitor.h" +#include "runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h" // shared memory layout: class_name_hash(u64) | class_instance shell | inner data // (deep copies of all arrays, strings and nested instance bodies reachable from the instance); -// the hash goes first so that fetch can verify the requested class without touching the instance itself template bool f$instance_cache_store(const string& key, class_instance instance, int64_t ttl = 0) noexcept { if (key.empty()) [[unlikely]] { @@ -43,13 +42,13 @@ bool f$instance_cache_store(const string& key, class_instance inst ttl = 0; } - kphp::visitors::instance_deep_size_count_visitor instance_deep_size_count_visitor{}; - instance_deep_size_count_visitor.process_instance(instance); - size_t inner_size{instance_deep_size_count_visitor.get_inner_size()}; + kphp::visitors::instance_deep_estimate_size_visitor estimate_size_visitor{}; + estimate_size_visitor.process_instance(instance); + const size_t estimated_size{estimate_size_visitor.get_estimated_size()}; constexpr size_t instance_size{sizeof(class_instance)}; constexpr size_t hash_size{sizeof(uint64_t)}; - auto alloc_result{k2::alloc_shared_memory(hash_size + instance_size + inner_size, alignof(std::max_align_t))}; + auto alloc_result{k2::alloc_shared_memory(hash_size + instance_size + estimated_size, alignof(std::max_align_t))}; if (!alloc_result.has_value()) [[unlikely]] { kphp::log::warning("instance_cache_store. failed to allocate shared memory: error -> {}, key -> {}", alloc_result.error(), key.c_str()); return false; @@ -61,13 +60,16 @@ bool f$instance_cache_store(const string& key, class_instance inst // deep-copies the object graph into the inner area and rewrites the instance's fields to point at the copies, // so the whole graph ends up inside the shared-memory block; all copies are pinned with ExtraRefCnt::for_instance_cache // and are never freed individually — the platform owns the block - kphp::visitors::instance_deep_copy_visitor{std::span{mem + hash_size + instance_size, inner_size}, ExtraRefCnt::for_instance_cache}.process_instance( - instance); + kphp::visitors::instance_deep_copy_visitor copy_visitor{std::span{mem + hash_size + instance_size, estimated_size}, ExtraRefCnt::for_instance_cache}; + if (!copy_visitor.process_instance(instance)) [[unlikely]] { + kphp::log::warning("instance_cache_store. failed to deep-copy instance into shared memory: estimated size -> {}, key -> {}", estimated_size, key.c_str()); + return false; + } std::construct_at(reinterpret_cast*>(mem + hash_size), std::move(instance)); // the platform expects ttl in milliseconds, while the PHP API accepts seconds if (auto publish_result{k2::publish_shared_memory(std::string_view{key.c_str(), key.size()}, mem, ttl * 1000, false, true)}; publish_result.has_value()) { - InstanceCacheInstanceState::get().request_cache.insert_or_assign(key, std::span{mem, hash_size + instance_size + inner_size}); + InstanceCacheInstanceState::get().request_cache.insert_or_assign(key, std::span{mem, hash_size + instance_size + estimated_size}); return true; } else { kphp::log::warning("instance_cache_store. failed to publish shared memory: error -> {}, key -> {}", publish_result.error(), key.c_str()); @@ -81,6 +83,10 @@ ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& constexpr size_t hash_size{sizeof(uint64_t)}; constexpr size_t instance_size{sizeof(ClassInstanceType)}; + if (key.empty()) [[unlikely]] { + kphp::log::warning("instance_cache_fetch. empty key is not supported"); + return {}; + } std::span mem{}; auto& request_cache{InstanceCacheInstanceState::get().request_cache}; @@ -101,7 +107,8 @@ ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& uint64_t stored_class_name_hash{}; std::memcpy(&stored_class_name_hash, mem.data(), sizeof(stored_class_name_hash)); - if (stored_class_name_hash != vk::murmur_hash(class_name.c_str(), class_name.size())) { + if (stored_class_name_hash != vk::murmur_hash(class_name.c_str(), class_name.size())) [[unlikely]] { + kphp::log::warning("instance_cache_fetch. trying to fetch incompatible instance class: class -> {}, key -> {}", class_name.c_str(), key.c_str()); return {}; } @@ -110,6 +117,10 @@ ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& } inline bool f$instance_cache_update_ttl(const string& key, int64_t ttl = 0) noexcept { + if (key.empty()) [[unlikely]] { + kphp::log::warning("instance_cache_update_ttl. empty key is not supported"); + return false; + } if (constexpr int64_t max_ttl{std::numeric_limits::max() / 1000}; ttl > max_ttl) [[unlikely]] { kphp::log::warning("instance_cache_update_ttl. ttl is too large, key will be stored forever: ttl -> {}, max ttl -> {}, key -> {}", ttl, max_ttl, key.c_str()); @@ -124,6 +135,10 @@ inline bool f$instance_cache_update_ttl(const string& key, int64_t ttl = 0) noex } inline bool f$instance_cache_delete(const string& key) noexcept { + if (key.empty()) [[unlikely]] { + kphp::log::warning("instance_cache_delete. empty key is not supported"); + return false; + } InstanceCacheInstanceState::get().request_cache.erase(key); return k2::delete_shared_memory(std::string_view{key.c_str(), key.size()}).has_value(); } diff --git a/runtime-light/stdlib/instance-cache/instance-cache-state.h b/runtime-light/stdlib/instance-cache/instance-cache-state.h index 9d5b05340a..a3fc9e965a 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-state.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-state.h @@ -16,7 +16,7 @@ struct InstanceCacheInstanceState final : private vk::not_copyable { // per-request cache: key -> shared memory region published under that key // (layout: class_name_hash | class_instance shell | inner data, see f$instance_cache_store); // spans point to platform-owned memory that stays valid for the whole request lifetime - kphp::stl::unordered_map, kphp::memory::script_allocator, + kphp::stl::unordered_map, kphp::memory::script_allocator, decltype([](const string& s) noexcept { return static_cast(s.hash()); })> request_cache; diff --git a/runtime-light/stdlib/job-worker/job-worker.h b/runtime-light/stdlib/job-worker/job-worker.h index 19f71e03fb..bcf397aca7 100644 --- a/runtime-light/stdlib/job-worker/job-worker.h +++ b/runtime-light/stdlib/job-worker/job-worker.h @@ -21,7 +21,7 @@ class CommonMemoryEstimateVisitor; namespace kphp::visitors { class instance_deep_copy_visitor; -class instance_deep_size_count_visitor; +class instance_deep_estimate_size_visitor; } // namespace kphp::visitors namespace job_worker_impl_ { @@ -34,7 +34,7 @@ struct SendableBase : virtual abstract_refcountable_php_interface { virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} - virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual const char* get_class() const noexcept = 0; virtual int32_t get_hash() const noexcept = 0; diff --git a/runtime-light/stdlib/rpc/rpc-tl-function.h b/runtime-light/stdlib/rpc/rpc-tl-function.h index 35a35d3ec4..2c56c52ad5 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-function.h +++ b/runtime-light/stdlib/rpc/rpc-tl-function.h @@ -21,7 +21,7 @@ class InstanceDeepDestroyVisitor; namespace kphp::visitors { class instance_deep_copy_visitor; -class instance_deep_size_count_visitor; +class instance_deep_estimate_size_visitor; } // namespace kphp::visitors // The locations of the typed TL related builtin classes that are described in functions.txt @@ -45,7 +45,7 @@ struct C$VK$TL$RpcFunction : abstract_refcountable_php_interface { virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} - virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -78,7 +78,7 @@ struct C$VK$TL$RpcFunctionReturnResult : abstract_refcountable_php_interface { virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} - virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -110,7 +110,7 @@ struct C$VK$TL$RpcFunctionFetcher : abstract_refcountable_php_interface { virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} - virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -136,7 +136,7 @@ struct C$VK$TL$RpcResponse : abstract_refcountable_php_interface { virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} - virtual void accept(kphp::visitors::instance_deep_size_count_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual const char* get_class() const noexcept { return "VK\\TL\\RpcResponse"; diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h similarity index 76% rename from runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h rename to runtime-light/stdlib/visitors/instance-deep-copy-visitor.h index e9b7813e7d..481bbbf907 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h @@ -20,7 +20,10 @@ namespace kphp::visitors { // deep-copies an instance graph into a caller-provided contiguous memory block (e.g. a shared memory region): // every reachable array/string/instance body is recreated inside the block via memory_pool, and the original's // fields are rewritten to point at the copies; all copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache), -// so they are never freed individually -- the owner of the block controls their lifetime +// so they are never freed individually -- the owner of the block controls their lifetime; +// the block must be at least instance_deep_estimate_size_visitor's estimate for the same graph -- the two visitors must stay in sync; +// on pool exhaustion (i.e. the estimate was insufficient) processing fails and false is returned, +// leaving the instance partially rewritten -- its fields may already point into the block class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; @@ -36,7 +39,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis instance_deep_copy_visitor& operator=(instance_deep_copy_visitor&&) = delete; ~instance_deep_copy_visitor() = default; - explicit instance_deep_copy_visitor(std::span memory_pool_buffer, ExtraRefCnt memory_ref_cnt = ExtraRefCnt::extra_ref_cnt_value(0)) noexcept + explicit instance_deep_copy_visitor(std::span memory_pool_buffer, ExtraRefCnt memory_ref_cnt) noexcept : Basic{*this, memory_ref_cnt} { this->memory_pool.init(memory_pool_buffer.data(), memory_pool_buffer.size()); } @@ -47,7 +50,11 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return true; } - array copied_array{carve(arr.calculate_memory_for_copying()), arr}; + auto copied{array::copy_in(carve(arr.calculate_memory_for_copying()), arr)}; + if (!copied.has_value()) [[unlikely]] { + return false; + } + array copied_array{std::move(*copied)}; const auto commit_copy{vk::finally([&arr, &copied_array]() noexcept { arr = std::move(copied_array); })}; // copying an empty array yields the global empty-array singleton instead of a real copy -- nothing left to deep-copy @@ -60,7 +67,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis copied_array.set_reference_counter_to(extra_ref_cnt); } // values of a primitive array were already memcpy'd by the array copy constructor, and there are no string keys to copy - const bool primitive_array{is_primitive && copied_array.has_no_string_keys()}; + const bool primitive_array{Basic::template is_primitive && copied_array.has_no_string_keys()}; return primitive_array || Basic::process_range(copied_array.begin_no_mutate(), copied_array.end_no_mutate()); } @@ -69,7 +76,11 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return true; } - string copied_string{carve(str.estimate_memory_usage()), str}; + auto copied{string::copy_in(carve(str.estimate_memory_usage()), str)}; + if (!copied.has_value()) [[unlikely]] { + return false; + } + string copied_string{std::move(*copied)}; const auto commit_copy{vk::finally([&str, &copied_string]() noexcept { str = std::move(copied_string); })}; kphp::log::assertion(copied_string.get_reference_counter() == 1); @@ -82,16 +93,13 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis template bool process_instance(class_instance& instance) noexcept { // keep the original instance alive for the whole traversal: copied_instances_table uses raw pointers to originals as keys - class_instance instance_copy{instance}; + class_instance instance_keepalive{instance}; const bool result{process(instance)}; this->copied_instances_table.clear(); return result; } private: - template - static constexpr bool is_primitive{vk::is_type_in_list, Optional, Optional>::value}; - template bool process(class_instance& instance) noexcept { if (instance.is_null()) { @@ -106,7 +114,11 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return true; } - instance = instance.virtual_builtin_clone(carve(instance.estimate_memory_usage())); + // the original is known to be non-null here, so a null result means the carved buffer was too small + instance = instance.virtual_builtin_clone_in(carve(instance.estimate_memory_usage())); + if (instance.is_null()) [[unlikely]] { + return false; + } copied_instance_ptr = instance.get_base_raw_ptr(); if (const auto extra_ref_cnt{get_memory_ref_cnt()}; extra_ref_cnt != 0) { @@ -115,9 +127,13 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return Basic::process(instance); } + // returns an empty span when the pool is exhausted, so the caller can fail gracefully vk::span carve(size_t size) noexcept { const size_t aligned_size{memory_resource::details::align_for_chunk(size)}; - return {static_cast(this->memory_pool.get_from_pool(aligned_size)), aligned_size}; + if (void* mem{this->memory_pool.get_from_pool(aligned_size, /*safe=*/true)}; mem != nullptr) [[likely]] { + return {static_cast(mem), aligned_size}; + } + return {}; } memory_resource::monotonic_buffer_resource memory_pool; diff --git a/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h similarity index 62% rename from runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h rename to runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h index 96e09d0297..06e1d99430 100644 --- a/runtime-light/stdlib/instance-cache/visitors/instance-deep-size-count-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h @@ -15,22 +15,22 @@ namespace kphp::visitors { // computes how many bytes instance_deep_copy_visitor will carve for an instance graph, // so that the destination memory block can be allocated upfront; the two visitors must stay in sync -class instance_deep_size_count_visitor final : kphp::visitors::instance_deep_basic_visitor { +class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_basic_visitor { public: - friend class kphp::visitors::instance_deep_basic_visitor; + friend class kphp::visitors::instance_deep_basic_visitor; - using Basic = kphp::visitors::instance_deep_basic_visitor; + using Basic = kphp::visitors::instance_deep_basic_visitor; using Basic::process; using Basic::operator(); using Basic::get_memory_ref_cnt; - instance_deep_size_count_visitor(const instance_deep_size_count_visitor&) = delete; - instance_deep_size_count_visitor(instance_deep_size_count_visitor&&) = delete; - instance_deep_size_count_visitor& operator=(const instance_deep_size_count_visitor&) = delete; - instance_deep_size_count_visitor& operator=(instance_deep_size_count_visitor&&) = delete; - ~instance_deep_size_count_visitor() = default; + instance_deep_estimate_size_visitor(const instance_deep_estimate_size_visitor&) = delete; + instance_deep_estimate_size_visitor(instance_deep_estimate_size_visitor&&) = delete; + instance_deep_estimate_size_visitor& operator=(const instance_deep_estimate_size_visitor&) = delete; + instance_deep_estimate_size_visitor& operator=(instance_deep_estimate_size_visitor&&) = delete; + ~instance_deep_estimate_size_visitor() = default; - explicit instance_deep_size_count_visitor() noexcept + explicit instance_deep_estimate_size_visitor() noexcept : Basic{*this} {} template @@ -39,16 +39,16 @@ class instance_deep_size_count_visitor final : kphp::visitors::instance_deep_bas return true; } - this->inner_size += memory_resource::details::align_for_chunk(arr.calculate_memory_for_copying()); + this->estimated_size += memory_resource::details::align_for_chunk(arr.calculate_memory_for_copying()); // primitive values are already accounted for wholesale above; only non-primitive values and string keys need traversal - const bool primitive_array{is_primitive && arr.has_no_string_keys()}; + const bool primitive_array{Basic::template is_primitive && arr.has_no_string_keys()}; return primitive_array || Basic::process_range(arr.begin_no_mutate(), arr.end_no_mutate()); } bool process(string& str) noexcept { if (!str.is_reference_counter(ExtraRefCnt::for_global_const)) { - this->inner_size += memory_resource::details::align_for_chunk(str.estimate_memory_usage()); + this->estimated_size += memory_resource::details::align_for_chunk(str.estimate_memory_usage()); } return true; @@ -61,14 +61,11 @@ class instance_deep_size_count_visitor final : kphp::visitors::instance_deep_bas return result; } - size_t get_inner_size() const noexcept { - return this->inner_size; + size_t get_estimated_size() const noexcept { + return this->estimated_size; } private: - template - static constexpr bool is_primitive{vk::is_type_in_list, Optional, Optional>::value}; - template bool process(class_instance& instance) noexcept { if (!instance.is_null()) { @@ -76,13 +73,13 @@ class instance_deep_size_count_visitor final : kphp::visitors::instance_deep_bas if (this->visited_instances_set.contains(instance_raw_ptr)) { return true; } - this->inner_size += memory_resource::details::align_for_chunk(instance.estimate_memory_usage()); + this->estimated_size += memory_resource::details::align_for_chunk(instance.estimate_memory_usage()); this->visited_instances_set.emplace(instance_raw_ptr); } return Basic::process(instance); } - size_t inner_size{0}; + size_t estimated_size{0}; kphp::stl::unordered_set visited_instances_set; }; From b2ddb72dfad15c7d28cb816300bcf8e22552bb58 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Tue, 25 Aug 2026 16:51:44 +0300 Subject: [PATCH 13/21] fmt + minor fixes and optimisations --- compiler/code-gen/declarations.cpp | 5 +- compiler/pipes/final-check.cpp | 2 - .../class-instance/class-instance-decl.inl | 14 +++-- .../core/core-types/decl/array_decl.inl | 8 +-- .../core/core-types/decl/string_decl.inl | 8 +-- .../core/core-types/definition/array.inl | 15 ++--- .../visitors/instance-deep-basic-visitor.h | 5 +- .../instance-cache/instance-cache-functions.h | 61 +++++++++++-------- .../instance-cache/instance-cache-state.h | 4 +- .../visitors/instance-deep-copy-visitor.h | 12 ++-- .../instance-deep-estimate-size-visitor.h | 7 ++- 11 files changed, 74 insertions(+), 67 deletions(-) diff --git a/compiler/code-gen/declarations.cpp b/compiler/code-gen/declarations.cpp index 3e03cdb62f..879dbbc328 100644 --- a/compiler/code-gen/declarations.cpp +++ b/compiler/code-gen/declarations.cpp @@ -755,9 +755,8 @@ void ClassDeclaration::compile_get_hash(CodeGenerator &W, ClassPtr klass) { } void ClassDeclaration::compile_class_name_hash(CodeGenerator &W, ClassPtr klass) { - // static type tag: same value for every object of this class, available without an instance (unlike virtual get_hash()); - // used by the K2 instance cache as part of the shared memory layout, so the hash function must match - // the one in f$instance_cache_fetch -- changing it makes images unable to read each other's entries + // hash of the class name, computed once at compile time -- same for every instance, + // unlike the virtual get_hash() it can be read without an instance at hand. W << "constexpr static uint64_t CLASS_NAME_HASH{" << vk::murmur_hash(klass->name.data(), klass->name.size()) << "ULL};" << NL << NL; } diff --git a/compiler/pipes/final-check.cpp b/compiler/pipes/final-check.cpp index cf20807a01..517d8adf1b 100644 --- a/compiler/pipes/final-check.cpp +++ b/compiler/pipes/final-check.cpp @@ -44,7 +44,6 @@ void check_class_immutableness(ClassPtr klass) { std::vector find_not_ic_compatibility_derivatives(ClassPtr klass); void check_fields_ic_compatibility(ClassPtr klass) { - // In case of K2 mode, all checks about serializability have already done bool flag = false; if (!klass->process_fields_ic_compatibility.compare_exchange_strong(flag, true, std::memory_order_acq_rel)) { return; @@ -69,7 +68,6 @@ void check_fields_ic_compatibility(ClassPtr klass) { } void check_derivatives_ic_compatibility(ClassPtr klass) { - // In case of K2 mode, all checks about serializability have already done std::vector descendants = find_not_ic_compatibility_derivatives(klass); for (const auto &element : descendants) { kphp_error(false, fmt_format("Can not store polymorphic type {} with mutable derived class {}", klass->name, element->name)); diff --git a/runtime-common/core/class-instance/class-instance-decl.inl b/runtime-common/core/class-instance/class-instance-decl.inl index ca12962ee9..a5f0eca904 100644 --- a/runtime-common/core/class-instance/class-instance-decl.inl +++ b/runtime-common/core/class-instance/class-instance-decl.inl @@ -74,15 +74,15 @@ public: inline class_instance& operator=(const Optional& null) noexcept; inline class_instance clone() const; - // constructs a copy of the instance in externally provided memory (no allocation, no ownership): - // memory must be 8-byte aligned and have at least estimate_memory_usage() bytes; - // the instance never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache); - // returns a null instance if the memory is insufficient or misaligned + // copies the instance into externally provided memory (no allocation/ownership) + // memory must be aligned to alignof(T) and >= estimate_memory_usage() bytes + // caller must pin it with a special ExtraRefCnt (e.g. for_instance_cache), since the instance never frees it. + // Returns a null instance if memory is unfit. inline class_instance clone_in(vk::span memory) const noexcept; template inline class_instance alloc(Args&&... args) __attribute__((always_inline)); - // constructs an instance in externally provided memory (no allocation, no ownership): - // leaves the instance null if the memory is smaller than sizeof(T) or not aligned to alignof(T) + // constructs an instance in externally provided memory (no allocation/ownership) + // leaves it null if memory is smaller than sizeof(T) or misaligned template inline class_instance alloc(vk::span memory, Args&&... args) noexcept __attribute__((always_inline)); inline class_instance empty_alloc() __attribute__((always_inline)); @@ -134,6 +134,8 @@ public: std::enable_if_t{}, class_instance> virtual_builtin_clone_in(vk::span memory) const noexcept { class_instance res; if (o) { + // alignof(S) is the base type's alignment, not the dynamic type's + // safe because all PHP classes have natural (pointer-sized) alignment, so it always matches the dynamic type's requirement if (memory.size() < o->virtual_builtin_sizeof() || reinterpret_cast(memory.data()) % alignof(S) != 0) [[unlikely]] { return res; } diff --git a/runtime-common/core/core-types/decl/array_decl.inl b/runtime-common/core/core-types/decl/array_decl.inl index 69bce58016..2c9017c282 100644 --- a/runtime-common/core/core-types/decl/array_decl.inl +++ b/runtime-common/core/core-types/decl/array_decl.inl @@ -267,10 +267,10 @@ public: template> inline array(array&& other) noexcept __attribute__((always_inline)); - // constructs a copy of other in externally provided memory (no allocation, no ownership): - // memory must be 8-byte aligned and have at least other.calculate_memory_for_copying() bytes; - // the array never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache); - // returns std::nullopt if the memory is insufficient or misaligned + // copies other into externally provided memory (no allocation/ownership). + // Memory must be aligned to alignof(array_inner) and have at least other.calculate_memory_for_copying() bytes. + // The array never frees this memory, so the caller must protect it with a special ExtraRefCnt (e.g. for_instance_cache). + // Returns std::nullopt if the memory is unfit. inline static std::optional copy_in(vk::span memory, const array& other) noexcept; template diff --git a/runtime-common/core/core-types/decl/string_decl.inl b/runtime-common/core/core-types/decl/string_decl.inl index ecde2a87eb..63138c591a 100644 --- a/runtime-common/core/core-types/decl/string_decl.inl +++ b/runtime-common/core/core-types/decl/string_decl.inl @@ -110,10 +110,10 @@ public: inline string(); inline string(const string& str) noexcept; inline string(string&& str) noexcept; - // constructs a copy of str in externally provided memory (no allocation, no ownership): - // memory must be 8-byte aligned and have at least str.estimate_memory_usage() bytes (an upper bound of the copy's footprint); - // the string never frees this memory, so the caller is expected to protect it with a special ExtraRefCnt (e.g. for_instance_cache); - // returns std::nullopt if the memory is insufficient or misaligned + // copies str into externally provided memory (no allocation/ownership). + // Memory must be aligned to alignof(string_inner) and >= str.estimate_memory_usage() bytes. + // Caller must pin it with a special ExtraRefCnt (e.g. for_instance_cache), since the string never frees it. + // Returns nullopt if memory is unfit. inline static std::optional copy_in(vk::span memory, const string& str) noexcept; inline string(const char* s, size_type n); inline explicit string(const char* s); diff --git a/runtime-common/core/core-types/definition/array.inl b/runtime-common/core/core-types/definition/array.inl index 8dce6f631c..777261f013 100644 --- a/runtime-common/core/core-types/definition/array.inl +++ b/runtime-common/core/core-types/definition/array.inl @@ -4,6 +4,7 @@ #pragma once +#include #include #include "common/algorithms/fastmod.h" @@ -260,6 +261,10 @@ std::optional::allocation> array::allocation::from_external if (memory.size() < mem_size || reinterpret_cast(memory.data()) % alignof(array_inner) != 0) [[unlikely]] { return std::nullopt; } + if (!is_vector) { + // map allocations require zeroed memory + std::memset(memory.data(), 0, mem_size); + } return allocation{memory.first(mem_size), new_int_size, is_vector}; } @@ -906,10 +911,6 @@ bool array::copy_from(vk::span memory, const array& other) noe if (!alloc.has_value()) [[unlikely]] { return false; } - if (!other.is_vector()) { - // map allocations require zeroed memory (hash entries use null pointers as empty markers) - std::memset(alloc->memory().data(), 0, alloc->memory().size()); - } copy_from_impl(create_from_allocation(*alloc), other); return true; } @@ -917,9 +918,9 @@ bool array::copy_from(vk::span memory, const array& other) noe template template void array::copy_from_impl(array_inner* new_array, const array& other) noexcept { - // same-type copies don't need convert_to (it's an identity conversion); - // this also keeps array copyable: convert_to is ill-formed, - // as its convert(const T&) and convert(const Unknown&) overloads collide when T is Unknown + // same-type copies don't need convert_to (it's an identity conversion). + // This also keeps array copyable: convert_to is ill-formed, + // as its convert(const T&) and convert(const Unknown&) overloads collide when T is Unknown. static constexpr auto convert_element{[](const T1& value) noexcept -> decltype(auto) { if constexpr (std::is_same_v) { return value; diff --git a/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h index 9f48a0730c..9fb47de859 100644 --- a/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h +++ b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -18,8 +19,8 @@ namespace kphp::visitors { // the lower bits store the real reference count, the top bit marks the instance as visited inline constexpr uint32_t VISITED_INSTANCE_MASK{0x80000000}; -// CRTP base for visitors that traverse an instance graph via compiler-generated accept() methods: -// every field is dispatched to Child::process; a false result from any field is accumulated into is_ok() +// CRTP base for visitors that traverse an instance graph via compiler-generated accept() methods. +// Every field is dispatched to Child::process, and a false result from any field is accumulated into is_ok(). template class instance_deep_basic_visitor : vk::not_copyable { public: diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 07c0736dd1..ae17ce290f 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -22,7 +22,7 @@ #include "runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h" // shared memory layout: class_name_hash(u64) | class_instance shell | inner data -// (deep copies of all arrays, strings and nested instance bodies reachable from the instance); +// (deep copies of all arrays, strings and nested instance bodies reachable from the instance). template bool f$instance_cache_store(const string& key, class_instance instance, int64_t ttl = 0) noexcept { if (key.empty()) [[unlikely]] { @@ -58,10 +58,12 @@ bool f$instance_cache_store(const string& key, class_instance inst const uint64_t class_name_hash{InstanceType::CLASS_NAME_HASH}; std::memcpy(mem, &class_name_hash, hash_size); // deep-copies the object graph into the inner area and rewrites the instance's fields to point at the copies, - // so the whole graph ends up inside the shared-memory block; all copies are pinned with ExtraRefCnt::for_instance_cache - // and are never freed individually — the platform owns the block + // so the whole graph ends up inside the shared-memory block. + // All copies are pinned with ExtraRefCnt::for_instance_cache and are never freed individually -- the platform owns the block. kphp::visitors::instance_deep_copy_visitor copy_visitor{std::span{mem + hash_size + instance_size, estimated_size}, ExtraRefCnt::for_instance_cache}; if (!copy_visitor.process_instance(instance)) [[unlikely]] { + // estimate_size_visitor and copy_visitor must stay in sync, so this should never actually happen. + // If this warning ever fires, it's a bug in one of the two visitors -- the allocated block above is leaked. kphp::log::warning("instance_cache_store. failed to deep-copy instance into shared memory: estimated size -> {}, key -> {}", estimated_size, key.c_str()); return false; } @@ -72,6 +74,8 @@ bool f$instance_cache_store(const string& key, class_instance inst InstanceCacheInstanceState::get().request_cache.insert_or_assign(key, std::span{mem, hash_size + instance_size + estimated_size}); return true; } else { + // publish is expected to always succeed here (ignore_if_exist=true, valid key/memory), so this should never actually happen. + // If this warning ever fires, the allocated block above is leaked, since it's never published and thus never reclaimed. kphp::log::warning("instance_cache_store. failed to publish shared memory: error -> {}, key -> {}", publish_result.error(), key.c_str()); return false; } @@ -87,33 +91,36 @@ ClassInstanceType f$instance_cache_fetch(const string& class_name, const string& kphp::log::warning("instance_cache_fetch. empty key is not supported"); return {}; } - std::span mem{}; + // unwraps and validates a shared memory block: returns a null instance if the block is malformed or belongs to another class + const auto unwrap{[&class_name, &key](std::span mem) noexcept -> ClassInstanceType { + if (mem.size() < hash_size + instance_size) [[unlikely]] { + kphp::log::warning("instance_cache_fetch. shared memory is too small: size -> {}, expected at least -> {}, key -> {}", mem.size(), + hash_size + instance_size, key.c_str()); + return {}; + } + + uint64_t stored_class_name_hash{}; + std::memcpy(&stored_class_name_hash, mem.data(), sizeof(stored_class_name_hash)); + + if (stored_class_name_hash != vk::murmur_hash(class_name.c_str(), class_name.size())) [[unlikely]] { + kphp::log::warning("instance_cache_fetch. trying to fetch incompatible instance class: class -> {}, key -> {}", class_name.c_str(), key.c_str()); + return {}; + } + + return *reinterpret_cast(mem.data() + hash_size); + }}; auto& request_cache{InstanceCacheInstanceState::get().request_cache}; if (auto it{request_cache.find(key)}; it != request_cache.end()) { - mem = it->second; - } else if (auto get_result{k2::get_shared_memory(std::string_view{key.c_str(), key.size()})}; get_result.has_value()) { - mem = get_result.value(); - } else { - return {}; + return unwrap(it->second); } - if (mem.size() < hash_size + instance_size) [[unlikely]] { - kphp::log::warning("instance_cache_fetch. shared memory is too small: size -> {}, expected at least -> {}, key -> {}", mem.size(), - hash_size + instance_size, key.c_str()); - return {}; - } - - uint64_t stored_class_name_hash{}; - std::memcpy(&stored_class_name_hash, mem.data(), sizeof(stored_class_name_hash)); - - if (stored_class_name_hash != vk::murmur_hash(class_name.c_str(), class_name.size())) [[unlikely]] { - kphp::log::warning("instance_cache_fetch. trying to fetch incompatible instance class: class -> {}, key -> {}", class_name.c_str(), key.c_str()); + auto get_result{k2::get_shared_memory(std::string_view{key.c_str(), key.size()})}; + if (!get_result.has_value()) { return {}; } - - request_cache.insert_or_assign(key, mem); - return *reinterpret_cast(mem.data() + hash_size); + request_cache.insert_or_assign(key, get_result.value()); + return unwrap(get_result.value()); } inline bool f$instance_cache_update_ttl(const string& key, int64_t ttl = 0) noexcept { @@ -121,15 +128,15 @@ inline bool f$instance_cache_update_ttl(const string& key, int64_t ttl = 0) noex kphp::log::warning("instance_cache_update_ttl. empty key is not supported"); return false; } + if (ttl < 0) [[unlikely]] { + kphp::log::warning("instance_cache_update_ttl. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl, key.c_str()); + ttl = 0; + } if (constexpr int64_t max_ttl{std::numeric_limits::max() / 1000}; ttl > max_ttl) [[unlikely]] { kphp::log::warning("instance_cache_update_ttl. ttl is too large, key will be stored forever: ttl -> {}, max ttl -> {}, key -> {}", ttl, max_ttl, key.c_str()); ttl = 0; } - if (ttl < 0) [[unlikely]] { - kphp::log::warning("instance_cache_update_ttl. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl, key.c_str()); - ttl = 0; - } // the platform expects ttl in milliseconds, while the PHP API accepts seconds return k2::update_ttl_shared_memory(std::string_view{key.c_str(), key.size()}, ttl * 1000).has_value(); } diff --git a/runtime-light/stdlib/instance-cache/instance-cache-state.h b/runtime-light/stdlib/instance-cache/instance-cache-state.h index a3fc9e965a..0bcc3bdc8f 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-state.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-state.h @@ -14,8 +14,8 @@ struct InstanceCacheInstanceState final : private vk::not_copyable { // per-request cache: key -> shared memory region published under that key - // (layout: class_name_hash | class_instance shell | inner data, see f$instance_cache_store); - // spans point to platform-owned memory that stays valid for the whole request lifetime + // (layout: class_name_hash | class_instance shell | inner data, see f$instance_cache_store). + // Spans point to platform-owned memory that stays valid for the whole request lifetime. kphp::stl::unordered_map, kphp::memory::script_allocator, decltype([](const string& s) noexcept { return static_cast(s.hash()); })> request_cache; diff --git a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h index 481bbbf907..75147b61c2 100644 --- a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h @@ -8,6 +8,7 @@ #include #include "common/containers/final_action.h" +#include "common/wrappers/span.h" #include "runtime-common/core/memory-resource/details/memory_chunk_list.h" #include "runtime-common/core/memory-resource/monotonic_buffer_resource.h" #include "runtime-common/core/runtime-core.h" @@ -17,13 +18,10 @@ namespace kphp::visitors { -// deep-copies an instance graph into a caller-provided contiguous memory block (e.g. a shared memory region): -// every reachable array/string/instance body is recreated inside the block via memory_pool, and the original's -// fields are rewritten to point at the copies; all copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache), -// so they are never freed individually -- the owner of the block controls their lifetime; -// the block must be at least instance_deep_estimate_size_visitor's estimate for the same graph -- the two visitors must stay in sync; -// on pool exhaustion (i.e. the estimate was insufficient) processing fails and false is returned, -// leaving the instance partially rewritten -- its fields may already point into the block +// deep-copies an instance graph into a caller-provided memory block (e.g. shared memory), rewriting the original's fields to point at the copies. +// Copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache) and never freed individually. +// The block size must match instance_deep_estimate_size_visitor's estimate for the same graph. +// On pool exhaustion, processing fails (returns false) with the instance left partially rewritten. class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; diff --git a/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h index 06e1d99430..2c6944998c 100644 --- a/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h @@ -14,7 +14,8 @@ namespace kphp::visitors { // computes how many bytes instance_deep_copy_visitor will carve for an instance graph, -// so that the destination memory block can be allocated upfront; the two visitors must stay in sync +// so that the destination memory block can be allocated upfront. +// The two visitors must stay in sync. class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_basic_visitor { public: friend class kphp::visitors::instance_deep_basic_visitor; @@ -22,7 +23,6 @@ class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_ using Basic = kphp::visitors::instance_deep_basic_visitor; using Basic::process; using Basic::operator(); - using Basic::get_memory_ref_cnt; instance_deep_estimate_size_visitor(const instance_deep_estimate_size_visitor&) = delete; instance_deep_estimate_size_visitor(instance_deep_estimate_size_visitor&&) = delete; @@ -41,7 +41,8 @@ class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_ this->estimated_size += memory_resource::details::align_for_chunk(arr.calculate_memory_for_copying()); - // primitive values are already accounted for wholesale above; only non-primitive values and string keys need traversal + // primitive values are already accounted for wholesale above. + // Only non-primitive values and string keys need traversal. const bool primitive_array{Basic::template is_primitive && arr.has_no_string_keys()}; return primitive_array || Basic::process_range(arr.begin_no_mutate(), arr.end_no_mutate()); } From fc9bc472e9b18a197c102c7ca8a3eb7e9ed1ca63 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Wed, 26 Aug 2026 17:44:46 +0300 Subject: [PATCH 14/21] added alignment for ic_store --- compiler/code-gen/declarations.cpp | 3 ++ .../class-instance/class-instance-decl.inl | 16 +++++++++-- .../core/core-types/decl/array_decl.inl | 5 ++++ .../core/core-types/decl/string_decl.inl | 4 +++ .../details/memory_chunk_list.h | 14 +++++++++- .../stdlib/diagnostics/exception-types.h | 12 ++++++++ runtime-light/stdlib/job-worker/job-worker.h | 5 ++++ runtime-light/stdlib/rpc/rpc-tl-function.h | 12 ++++++++ .../visitors/instance-deep-copy-visitor.h | 28 ++++++++++++------- .../instance-deep-estimate-size-visitor.h | 6 ++-- runtime/exception.h | 8 ++++++ runtime/job-workers/job-interface.h | 5 ++++ runtime/memcache.h | 4 +++ runtime/tl/rpc_function.h | 12 ++++++++ 14 files changed, 117 insertions(+), 17 deletions(-) diff --git a/compiler/code-gen/declarations.cpp b/compiler/code-gen/declarations.cpp index 879dbbc328..5bc9a0eeb0 100644 --- a/compiler/code-gen/declarations.cpp +++ b/compiler/code-gen/declarations.cpp @@ -975,6 +975,9 @@ void ClassDeclaration::compile_virtual_builtin_functions(CodeGenerator &W, Class compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, "size_t virtual_builtin_sizeof()", "sizeof(*this)"); + compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, + "size_t virtual_builtin_alignof()", "alignof(" + klass->src_name + ")"); + compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, klass->src_name + "* virtual_builtin_clone()", "new " + klass->src_name + "{*this}"); diff --git a/runtime-common/core/class-instance/class-instance-decl.inl b/runtime-common/core/class-instance/class-instance-decl.inl index a5f0eca904..223fed495a 100644 --- a/runtime-common/core/class-instance/class-instance-decl.inl +++ b/runtime-common/core/class-instance/class-instance-decl.inl @@ -109,6 +109,18 @@ public: return o->virtual_builtin_sizeof(); } + template + std::enable_if_t{}, size_t> alignment() const noexcept { + return alignof(T); + } + + // for polymorphic types, alignof(T) is the base type's alignment, not the dynamic type's; + // virtual_builtin_alignof() dispatches to the dynamic type's actual alignment + template + std::enable_if_t{}, size_t> alignment() const noexcept { + return o->virtual_builtin_alignof(); + } + template std::enable_if_t{}, class_instance> virtual_builtin_clone() const noexcept { return clone(); @@ -134,9 +146,7 @@ public: std::enable_if_t{}, class_instance> virtual_builtin_clone_in(vk::span memory) const noexcept { class_instance res; if (o) { - // alignof(S) is the base type's alignment, not the dynamic type's - // safe because all PHP classes have natural (pointer-sized) alignment, so it always matches the dynamic type's requirement - if (memory.size() < o->virtual_builtin_sizeof() || reinterpret_cast(memory.data()) % alignof(S) != 0) [[unlikely]] { + if (memory.size() < o->virtual_builtin_sizeof() || reinterpret_cast(memory.data()) % o->virtual_builtin_alignof() != 0) [[unlikely]] { return res; } res.o = vk::intrusive_ptr{o->virtual_builtin_construct_at(memory.data())}; diff --git a/runtime-common/core/core-types/decl/array_decl.inl b/runtime-common/core/core-types/decl/array_decl.inl index 2c9017c282..8b6e2d1073 100644 --- a/runtime-common/core/core-types/decl/array_decl.inl +++ b/runtime-common/core/core-types/decl/array_decl.inl @@ -485,6 +485,11 @@ public: size_t estimate_memory_usage() const noexcept; size_t calculate_memory_for_copying() const noexcept; + // required alignment of the memory passed to copy_in() + static constexpr size_t alignment() noexcept { + return alignof(array_inner); + } + template static array convert_from(const array&); diff --git a/runtime-common/core/core-types/decl/string_decl.inl b/runtime-common/core/core-types/decl/string_decl.inl index 63138c591a..b39e870763 100644 --- a/runtime-common/core/core-types/decl/string_decl.inl +++ b/runtime-common/core/core-types/decl/string_decl.inl @@ -260,6 +260,10 @@ public: inline static constexpr size_t inner_sizeof() noexcept { return sizeof(string_inner); } + // required alignment of the memory passed to copy_in() + inline static constexpr size_t alignment() noexcept { + return alignof(string_inner); + } inline static string make_const_string_on_memory(const char* str, size_type len, void* memory, size_t memory_size); inline void destroy() __attribute__((always_inline)); diff --git a/runtime-common/core/memory-resource/details/memory_chunk_list.h b/runtime-common/core/memory-resource/details/memory_chunk_list.h index d779009469..b40274e2d9 100644 --- a/runtime-common/core/memory-resource/details/memory_chunk_list.h +++ b/runtime-common/core/memory-resource/details/memory_chunk_list.h @@ -4,9 +4,11 @@ #pragma once +#include #include #include "runtime-common/core/memory-resource/memory_resource.h" +#include "runtime-common/core/utils/kphp-assert-core.h" namespace memory_resource { namespace details { @@ -37,7 +39,17 @@ class memory_chunk_list { static_assert(sizeof(memory_chunk_list) == 8, "sizeof memory_chunk_list should be 8"); inline constexpr size_t align_for_chunk(size_t size) noexcept { - return static_cast((size + 7L) & -8L); + constexpr size_t align{8}; + return (size + (align - 1)) & ~(align - 1); +} + +inline constexpr size_t align_for_chunk(size_t size, size_t align) noexcept { + php_assert(align != 0 && (align & (align - 1)) == 0); // NOLINT + // we need to carve out X bytes such that size + (ptr % align) <= X holds for any ptr the pool may return. + // the pool only guarantees 8-byte aligned ptr, so the worst case is ptr % align == align - 8. + // requesting size + (align - 8), rounded up to the pool's 8-byte granularity, is therefore always enough. + const size_t padding{std::max(align, size_t{8}) - 8}; + return align_for_chunk(size + padding); } inline constexpr size_t get_chunk_id(size_t aligned_size) noexcept { diff --git a/runtime-light/stdlib/diagnostics/exception-types.h b/runtime-light/stdlib/diagnostics/exception-types.h index 2c36c21e09..26c34be603 100644 --- a/runtime-light/stdlib/diagnostics/exception-types.h +++ b/runtime-light/stdlib/diagnostics/exception-types.h @@ -72,6 +72,10 @@ struct C$Throwable : public refcountable_polymorphic_php_classes_virt<> { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } + virtual C$Throwable* virtual_builtin_clone() const noexcept { return nullptr; } @@ -142,6 +146,10 @@ struct C$Exception : public C$Throwable { return sizeof(*this); } + size_t virtual_builtin_alignof() const noexcept override { + return alignof(C$Exception); + } + const char* get_class() const noexcept override { return "Exception"; } @@ -195,6 +203,10 @@ struct C$Error : public C$Throwable { return sizeof(*this); } + size_t virtual_builtin_alignof() const noexcept override { + return alignof(C$Error); + } + const char* get_class() const noexcept override { return "Error"; } diff --git a/runtime-light/stdlib/job-worker/job-worker.h b/runtime-light/stdlib/job-worker/job-worker.h index bcf397aca7..c57ec451b6 100644 --- a/runtime-light/stdlib/job-worker/job-worker.h +++ b/runtime-light/stdlib/job-worker/job-worker.h @@ -39,6 +39,7 @@ struct SendableBase : virtual abstract_refcountable_php_interface { virtual const char* get_class() const noexcept = 0; virtual int32_t get_hash() const noexcept = 0; virtual size_t virtual_builtin_sizeof() const noexcept = 0; + virtual size_t virtual_builtin_alignof() const noexcept = 0; virtual SendableBase* virtual_builtin_clone() const noexcept = 0; virtual SendableBase* virtual_builtin_construct_at(void* /*unused*/) const noexcept = 0; @@ -92,6 +93,10 @@ struct C$KphpJobWorkerResponseError : public refcountable_polymorphic_php_classe return sizeof(*this); } + size_t virtual_builtin_alignof() const noexcept override { + return alignof(C$KphpJobWorkerResponseError); + } + C$KphpJobWorkerResponseError* virtual_builtin_clone() const noexcept override { return new C$KphpJobWorkerResponseError{*this}; } diff --git a/runtime-light/stdlib/rpc/rpc-tl-function.h b/runtime-light/stdlib/rpc/rpc-tl-function.h index 2c56c52ad5..9bc3104bce 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-function.h +++ b/runtime-light/stdlib/rpc/rpc-tl-function.h @@ -50,6 +50,9 @@ struct C$VK$TL$RpcFunction : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcFunction* virtual_builtin_clone() const noexcept { return nullptr; } @@ -83,6 +86,9 @@ struct C$VK$TL$RpcFunctionReturnResult : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcFunctionReturnResult* virtual_builtin_clone() const noexcept { return nullptr; } @@ -115,6 +121,9 @@ struct C$VK$TL$RpcFunctionFetcher : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcFunctionFetcher* virtual_builtin_clone() const noexcept { return nullptr; } @@ -149,6 +158,9 @@ struct C$VK$TL$RpcResponse : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcResponse* virtual_builtin_clone() const noexcept { return nullptr; } diff --git a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h index 75147b61c2..1549f24ac4 100644 --- a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include "common/containers/final_action.h" @@ -20,7 +21,7 @@ namespace kphp::visitors { // deep-copies an instance graph into a caller-provided memory block (e.g. shared memory), rewriting the original's fields to point at the copies. // Copies are pinned with memory_ref_cnt (e.g. ExtraRefCnt::for_instance_cache) and never freed individually. -// The block size must match instance_deep_estimate_size_visitor's estimate for the same graph. +// The block size must match instance_deep_estimate_size_visitor's estimate for the same graph. // On pool exhaustion, processing fails (returns false) with the instance left partially rewritten. class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_visitor { public: @@ -48,7 +49,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return true; } - auto copied{array::copy_in(carve(arr.calculate_memory_for_copying()), arr)}; + auto copied{array::copy_in(carve(arr.calculate_memory_for_copying(), array::alignment()), arr)}; if (!copied.has_value()) [[unlikely]] { return false; } @@ -74,7 +75,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return true; } - auto copied{string::copy_in(carve(str.estimate_memory_usage()), str)}; + auto copied{string::copy_in(carve(str.estimate_memory_usage(), string::alignment()), str)}; if (!copied.has_value()) [[unlikely]] { return false; } @@ -113,7 +114,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis } // the original is known to be non-null here, so a null result means the carved buffer was too small - instance = instance.virtual_builtin_clone_in(carve(instance.estimate_memory_usage())); + instance = instance.virtual_builtin_clone_in(carve(instance.estimate_memory_usage(), instance.alignment())); if (instance.is_null()) [[unlikely]] { return false; } @@ -125,13 +126,20 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return Basic::process(instance); } - // returns an empty span when the pool is exhausted, so the caller can fail gracefully - vk::span carve(size_t size) noexcept { - const size_t aligned_size{memory_resource::details::align_for_chunk(size)}; - if (void* mem{this->memory_pool.get_from_pool(aligned_size, /*safe=*/true)}; mem != nullptr) [[likely]] { - return {static_cast(mem), aligned_size}; + // returns an empty span when the pool is exhausted, so the caller can fail gracefully. + // the returned memory is aligned to `align`, regardless of what alignment the underlying pool happens to guarantee. + vk::span carve(size_t size, size_t align) noexcept { + size_t space{memory_resource::details::align_for_chunk(size, align)}; + void* mem{this->memory_pool.get_from_pool(space, /*safe=*/true)}; + if (mem == nullptr) [[unlikely]] { + return {}; } - return {}; + // align_for_chunk(size, align) reserves enough slack for this to always succeed + if (std::align(align, size, mem, space) == nullptr) [[unlikely]] { + kphp::log::warning("failed to align a carved memory block: size -> {}, align -> {}", size, align); + return {}; + } + return {static_cast(mem), size}; } memory_resource::monotonic_buffer_resource memory_pool; diff --git a/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h index 2c6944998c..0844caef4d 100644 --- a/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h @@ -39,7 +39,7 @@ class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_ return true; } - this->estimated_size += memory_resource::details::align_for_chunk(arr.calculate_memory_for_copying()); + this->estimated_size += memory_resource::details::align_for_chunk(arr.calculate_memory_for_copying(), array::alignment()); // primitive values are already accounted for wholesale above. // Only non-primitive values and string keys need traversal. @@ -49,7 +49,7 @@ class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_ bool process(string& str) noexcept { if (!str.is_reference_counter(ExtraRefCnt::for_global_const)) { - this->estimated_size += memory_resource::details::align_for_chunk(str.estimate_memory_usage()); + this->estimated_size += memory_resource::details::align_for_chunk(str.estimate_memory_usage(), string::alignment()); } return true; @@ -74,7 +74,7 @@ class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_ if (this->visited_instances_set.contains(instance_raw_ptr)) { return true; } - this->estimated_size += memory_resource::details::align_for_chunk(instance.estimate_memory_usage()); + this->estimated_size += memory_resource::details::align_for_chunk(instance.estimate_memory_usage(), instance.alignment()); this->visited_instances_set.emplace(instance_raw_ptr); } return Basic::process(instance); diff --git a/runtime/exception.h b/runtime/exception.h index 4e355e10bf..e5e170f398 100644 --- a/runtime/exception.h +++ b/runtime/exception.h @@ -82,6 +82,10 @@ struct C$Exception : public C$Throwable { return sizeof(*this); } + size_t virtual_builtin_alignof() const noexcept { + return alignof(C$Exception); + } + const char* get_class() const noexcept override { return "Exception"; } @@ -98,6 +102,10 @@ struct C$Error : public C$Throwable { return sizeof(*this); } + size_t virtual_builtin_alignof() const noexcept { + return alignof(C$Error); + } + const char* get_class() const noexcept override { return "Error"; } diff --git a/runtime/job-workers/job-interface.h b/runtime/job-workers/job-interface.h index 1126ff313b..34ef0f1f42 100644 --- a/runtime/job-workers/job-interface.h +++ b/runtime/job-workers/job-interface.h @@ -49,6 +49,7 @@ struct SendingInstanceBase : virtual abstract_refcountable_php_interface { virtual void accept(CommonMemoryEstimateVisitor&) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept = 0; + virtual size_t virtual_builtin_alignof() const noexcept = 0; virtual SendingInstanceBase* virtual_builtin_clone() const noexcept = 0; virtual ~SendingInstanceBase() = default; @@ -116,6 +117,10 @@ struct C$KphpJobWorkerResponseError : public refcountable_polymorphic_php_classe return sizeof(*this); } + size_t virtual_builtin_alignof() const noexcept override { + return alignof(C$KphpJobWorkerResponseError); + } + C$KphpJobWorkerResponseError* virtual_builtin_clone() const noexcept override { return new C$KphpJobWorkerResponseError{*this}; } diff --git a/runtime/memcache.h b/runtime/memcache.h index b35c1a6acf..0c928c0fee 100644 --- a/runtime/memcache.h +++ b/runtime/memcache.h @@ -68,6 +68,10 @@ class C$McMemcache final : public refcountable_polymorphic_php_classes hosts{array_size{1, true}}; }; diff --git a/runtime/tl/rpc_function.h b/runtime/tl/rpc_function.h index 4c6b09e789..ad1fded4b2 100644 --- a/runtime/tl/rpc_function.h +++ b/runtime/tl/rpc_function.h @@ -43,6 +43,9 @@ struct C$VK$TL$RpcFunction : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcFunction* virtual_builtin_clone() const noexcept { return nullptr; } @@ -71,6 +74,9 @@ struct C$VK$TL$RpcFunctionReturnResult : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcFunctionReturnResult* virtual_builtin_clone() const noexcept { return nullptr; } @@ -98,6 +104,9 @@ struct C$VK$TL$RpcFunctionFetcher : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcFunctionFetcher* virtual_builtin_clone() const noexcept { return nullptr; } @@ -127,6 +136,9 @@ struct C$VK$TL$RpcResponse : abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept { return 0; } + virtual size_t virtual_builtin_alignof() const noexcept { + return 0; + } virtual C$VK$TL$RpcResponse* virtual_builtin_clone() const noexcept { return nullptr; } From 924ad06021a8847a3224fd4e3097f0a7ea3dfa71 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Wed, 26 Aug 2026 18:52:36 +0300 Subject: [PATCH 15/21] sigsegv fix --- .../core/class-instance/class-instance-decl.inl | 2 -- .../stdlib/visitors/instance-deep-copy-visitor.h | 16 +++++++++++----- runtime/instance-copy-processor.h | 9 +++++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/runtime-common/core/class-instance/class-instance-decl.inl b/runtime-common/core/class-instance/class-instance-decl.inl index 223fed495a..60d8b18667 100644 --- a/runtime-common/core/class-instance/class-instance-decl.inl +++ b/runtime-common/core/class-instance/class-instance-decl.inl @@ -114,8 +114,6 @@ public: return alignof(T); } - // for polymorphic types, alignof(T) is the base type's alignment, not the dynamic type's; - // virtual_builtin_alignof() dispatches to the dynamic type's actual alignment template std::enable_if_t{}, size_t> alignment() const noexcept { return o->virtual_builtin_alignof(); diff --git a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h index 1549f24ac4..0a617fa7c7 100644 --- a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h @@ -89,6 +89,15 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis return true; } + bool process(mixed& value) noexcept { + if (value.is_object()) { + kphp::log::warning("cannot perform deep copy: mixed contains an instance of {}. copying objects inside mixed is not allowed", + value.as_object()->get_class()); + return false; + } + return Basic::process(value); + } + template bool process_instance(class_instance& instance) noexcept { // keep the original instance alive for the whole traversal: copied_instances_table uses raw pointers to originals as keys @@ -134,11 +143,8 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis if (mem == nullptr) [[unlikely]] { return {}; } - // align_for_chunk(size, align) reserves enough slack for this to always succeed - if (std::align(align, size, mem, space) == nullptr) [[unlikely]] { - kphp::log::warning("failed to align a carved memory block: size -> {}, align -> {}", size, align); - return {}; - } + + kphp::log::assertion(std::align(align, size, mem, space)); return {static_cast(mem), size}; } diff --git a/runtime/instance-copy-processor.h b/runtime/instance-copy-processor.h index 1bbd7f38de..592b054648 100644 --- a/runtime/instance-copy-processor.h +++ b/runtime/instance-copy-processor.h @@ -12,6 +12,7 @@ #include "runtime-common/core/memory-resource/unsynchronized_pool_resource.h" #include "runtime-common/core/runtime-core.h" +#include "runtime-common/core/utils/kphp-assert-core.h" #include "runtime-common/stdlib/visitors/instance-deep-basic-visitor.h" #include "runtime/allocator.h" #include "runtime/critical_section.h" @@ -81,6 +82,14 @@ class InstanceDeepCopyVisitor : kphp::visitors::instance_deep_basic_visitorget_class()); + return false; + } + return Basic::process(value); + } + bool is_memory_limit_exceeded() const noexcept { return memory_limit_exceeded_; } From 5c5d7fbca372992f8070b6df236be6a6f1b5b976 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 27 Aug 2026 10:58:42 +0300 Subject: [PATCH 16/21] fmt --- .../stdlib/instance-cache/instance-cache-functions.h | 6 ++++-- .../stdlib/visitors/instance-deep-copy-visitor.h | 2 +- .../visitors/instance-deep-estimate-size-visitor.h | 10 ++++++++++ runtime/instance-copy-processor.h | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index ae17ce290f..b96efcb7c0 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -22,7 +22,6 @@ #include "runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h" // shared memory layout: class_name_hash(u64) | class_instance shell | inner data -// (deep copies of all arrays, strings and nested instance bodies reachable from the instance). template bool f$instance_cache_store(const string& key, class_instance instance, int64_t ttl = 0) noexcept { if (key.empty()) [[unlikely]] { @@ -43,7 +42,10 @@ bool f$instance_cache_store(const string& key, class_instance inst } kphp::visitors::instance_deep_estimate_size_visitor estimate_size_visitor{}; - estimate_size_visitor.process_instance(instance); + if (!estimate_size_visitor.process_instance(instance)) [[unlikely]] { + kphp::log::warning("instance_cache_store. failed to estimate instance size: key -> {}", key.c_str()); + return false; + } const size_t estimated_size{estimate_size_visitor.get_estimated_size()}; constexpr size_t instance_size{sizeof(class_instance)}; constexpr size_t hash_size{sizeof(uint64_t)}; diff --git a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h index 0a617fa7c7..692134b135 100644 --- a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h @@ -91,7 +91,7 @@ class instance_deep_copy_visitor final : kphp::visitors::instance_deep_basic_vis bool process(mixed& value) noexcept { if (value.is_object()) { - kphp::log::warning("cannot perform deep copy: mixed contains an instance of {}. copying objects inside mixed is not allowed", + kphp::log::warning("cannot deep-copy a mixed value holding an object of class {}: objects inside mixed are not supported", value.as_object()->get_class()); return false; } diff --git a/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h index 0844caef4d..ae4920e780 100644 --- a/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h +++ b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h @@ -10,6 +10,7 @@ #include "runtime-common/core/runtime-core.h" #include "runtime-common/core/std/containers.h" #include "runtime-common/stdlib/visitors/instance-deep-basic-visitor.h" +#include "runtime-light/stdlib/diagnostics/logs.h" namespace kphp::visitors { @@ -55,6 +56,15 @@ class instance_deep_estimate_size_visitor final : kphp::visitors::instance_deep_ return true; } + bool process(mixed& value) noexcept { + if (value.is_object()) { + kphp::log::warning("cannot estimate the size of a mixed value holding an object of class {}: objects inside mixed are not supported", + value.as_object()->get_class()); + return false; + } + return Basic::process(value); + } + template bool process_instance(class_instance& instance) noexcept { const bool result{process(instance)}; diff --git a/runtime/instance-copy-processor.h b/runtime/instance-copy-processor.h index 592b054648..f7aada869d 100644 --- a/runtime/instance-copy-processor.h +++ b/runtime/instance-copy-processor.h @@ -84,7 +84,7 @@ class InstanceDeepCopyVisitor : kphp::visitors::instance_deep_basic_visitorget_class()); + php_warning("cannot deep-copy a mixed value holding an object of class %s: objects inside mixed are not supported", value.as_object()->get_class()); return false; } return Basic::process(value); From 724013aba943cd2f5b6deb8af3e5b2563d6080df Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 27 Aug 2026 12:07:50 +0300 Subject: [PATCH 17/21] fix --- .../core/class-instance/class-instance-decl.inl | 2 +- runtime-common/core/class-instance/class-instance.inl | 4 ++-- runtime-common/core/core-types/definition/array.inl | 6 +++--- runtime-common/core/core-types/definition/string.inl | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/runtime-common/core/class-instance/class-instance-decl.inl b/runtime-common/core/class-instance/class-instance-decl.inl index 60d8b18667..b579ba7041 100644 --- a/runtime-common/core/class-instance/class-instance-decl.inl +++ b/runtime-common/core/class-instance/class-instance-decl.inl @@ -144,7 +144,7 @@ public: std::enable_if_t{}, class_instance> virtual_builtin_clone_in(vk::span memory) const noexcept { class_instance res; if (o) { - if (memory.size() < o->virtual_builtin_sizeof() || reinterpret_cast(memory.data()) % o->virtual_builtin_alignof() != 0) [[unlikely]] { + if (unlikely(memory.size() < o->virtual_builtin_sizeof() || reinterpret_cast(memory.data()) % o->virtual_builtin_alignof() != 0)) { return res; } res.o = vk::intrusive_ptr{o->virtual_builtin_construct_at(memory.data())}; diff --git a/runtime-common/core/class-instance/class-instance.inl b/runtime-common/core/class-instance/class-instance.inl index 81c7ec3cdd..c68e1eb84d 100644 --- a/runtime-common/core/class-instance/class-instance.inl +++ b/runtime-common/core/class-instance/class-instance.inl @@ -36,7 +36,7 @@ class_instance class_instance::clone_in_impl(vk::span memory, s class_instance res; if (o) { res.alloc(memory, *o); // res stays null if the memory is insufficient or misaligned - if (!res.is_null()) [[likely]] { + if (likely(!res.is_null())) { res.o->set_refcnt(1); } } @@ -66,7 +66,7 @@ template class_instance class_instance::alloc(vk::span memory, Args&&... args) noexcept { static_assert(!std::is_empty{}, "class T may not be empty"); php_assert(!o); - if (memory.size() < sizeof(T) || reinterpret_cast(memory.data()) % alignof(T) != 0) [[unlikely]] { + if (unlikely(memory.size() < sizeof(T) || reinterpret_cast(memory.data()) % alignof(T) != 0)) { return *this; } T* ptr = new (memory.data()) T{std::forward(args)...}; diff --git a/runtime-common/core/core-types/definition/array.inl b/runtime-common/core/core-types/definition/array.inl index 777261f013..e4eb8eaf6c 100644 --- a/runtime-common/core/core-types/definition/array.inl +++ b/runtime-common/core/core-types/definition/array.inl @@ -258,7 +258,7 @@ typename array::allocation array::allocation::allocate(int64_t new_int_siz template std::optional::allocation> array::allocation::from_external(vk::span memory, int64_t new_int_size, bool is_vector) noexcept { const size_t mem_size = array_inner::estimate_size(new_int_size, is_vector); - if (memory.size() < mem_size || reinterpret_cast(memory.data()) % alignof(array_inner) != 0) [[unlikely]] { + if (unlikely(memory.size() < mem_size || reinterpret_cast(memory.data()) % alignof(array_inner) != 0)) { return std::nullopt; } if (!is_vector) { @@ -908,7 +908,7 @@ bool array::copy_from(vk::span memory, const array& other) noe } auto alloc{allocation::from_external(memory, other.p->size, other.is_vector())}; - if (!alloc.has_value()) [[unlikely]] { + if (unlikely(!alloc.has_value())) { return false; } copy_from_impl(create_from_allocation(*alloc), other); @@ -1038,7 +1038,7 @@ array::array(array&& other) noexcept { template std::optional> array::copy_in(vk::span memory, const array& other) noexcept { array res; - if (!res.copy_from(memory, other)) [[unlikely]] { + if (unlikely(!res.copy_from(memory, other))) { return std::nullopt; } return res; diff --git a/runtime-common/core/core-types/definition/string.inl b/runtime-common/core/core-types/definition/string.inl index cae11abf24..6fe87fb8d5 100644 --- a/runtime-common/core/core-types/definition/string.inl +++ b/runtime-common/core/core-types/definition/string.inl @@ -68,7 +68,7 @@ string::string_inner* string::string_inner::create(size_type requested_capacity, string::string_inner* string::string_inner::create(vk::span memory, size_type requested_capacity, size_type old_capacity) noexcept { size_type capacity = new_capacity(requested_capacity, old_capacity); size_type new_size = (size_type)(sizeof(string_inner) + (capacity + 1)); - if (memory.size() < new_size || reinterpret_cast(memory.data()) % alignof(string_inner) != 0) [[unlikely]] { + if (unlikely(memory.size() < new_size || reinterpret_cast(memory.data()) % alignof(string_inner) != 0)) { return nullptr; } string_inner* p = (string_inner*)memory.data(); @@ -124,7 +124,7 @@ char* string::string_inner::clone(size_type requested_cap) noexcept { char* string::string_inner::clone(vk::span memory, size_type requested_cap) noexcept { string_inner* r = string_inner::create(memory, requested_cap, capacity); - if (r == nullptr) [[unlikely]] { + if (unlikely(r == nullptr)) { return nullptr; } if (size) { @@ -214,7 +214,7 @@ string::string(string&& str) noexcept std::optional string::copy_in(vk::span memory, const string& str) noexcept { string res; - if (!res.copy_from(memory, str)) [[unlikely]] { + if (unlikely(!res.copy_from(memory, str))) { return std::nullopt; } return res; @@ -358,7 +358,7 @@ string string::copy_and_make_not_shared() const { } bool string::copy_from(vk::span memory, const string& other) noexcept { - if (char* new_p{other.inner()->clone(memory, other.size())}; new_p != nullptr) [[likely]] { + if (char* new_p{other.inner()->clone(memory, other.size())}; likely(new_p != nullptr)) { p = new_p; return true; } From e97dbdfdc7b43d2475fa1bb2721c9c4dd4843ccf Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 27 Aug 2026 13:03:21 +0300 Subject: [PATCH 18/21] fix --- runtime/job-workers/job-interface.h | 8 ++++++++ tests/python/tests/instance_cache/php/index.php | 3 +++ 2 files changed, 11 insertions(+) diff --git a/runtime/job-workers/job-interface.h b/runtime/job-workers/job-interface.h index 34ef0f1f42..aea8eb0eb0 100644 --- a/runtime/job-workers/job-interface.h +++ b/runtime/job-workers/job-interface.h @@ -51,6 +51,7 @@ struct SendingInstanceBase : virtual abstract_refcountable_php_interface { virtual size_t virtual_builtin_sizeof() const noexcept = 0; virtual size_t virtual_builtin_alignof() const noexcept = 0; virtual SendingInstanceBase* virtual_builtin_clone() const noexcept = 0; + virtual SendingInstanceBase* virtual_builtin_construct_at(void* ptr) const noexcept = 0; virtual ~SendingInstanceBase() = default; }; @@ -63,10 +64,12 @@ struct JobSharedMessage; struct C$KphpJobWorkerSharedMemoryPiece : job_workers::SendingInstanceBase { C$KphpJobWorkerSharedMemoryPiece* virtual_builtin_clone() const noexcept override = 0; + C$KphpJobWorkerSharedMemoryPiece* virtual_builtin_construct_at(void* ptr) const noexcept override = 0; }; struct C$KphpJobWorkerRequest : job_workers::SendingInstanceBase { C$KphpJobWorkerRequest* virtual_builtin_clone() const noexcept override = 0; + C$KphpJobWorkerRequest* virtual_builtin_construct_at(void* ptr) const noexcept override = 0; virtual class_instance get_shared_memory_piece() const noexcept = 0; virtual void set_shared_memory_piece(const class_instance&) noexcept = 0; @@ -74,6 +77,7 @@ struct C$KphpJobWorkerRequest : job_workers::SendingInstanceBase { struct C$KphpJobWorkerResponse : job_workers::SendingInstanceBase { C$KphpJobWorkerResponse* virtual_builtin_clone() const noexcept override = 0; + C$KphpJobWorkerResponse* virtual_builtin_construct_at(void* ptr) const noexcept override = 0; }; struct C$KphpJobWorkerResponseError : public refcountable_polymorphic_php_classes { @@ -124,6 +128,10 @@ struct C$KphpJobWorkerResponseError : public refcountable_polymorphic_php_classe C$KphpJobWorkerResponseError* virtual_builtin_clone() const noexcept override { return new C$KphpJobWorkerResponseError{*this}; } + + C$KphpJobWorkerResponseError* virtual_builtin_construct_at(void* ptr) const noexcept override { + return new (ptr) C$KphpJobWorkerResponseError{*this}; + } }; class_instance f$KphpJobWorkerResponseError$$__construct(class_instance const& v$this) noexcept; diff --git a/tests/python/tests/instance_cache/php/index.php b/tests/python/tests/instance_cache/php/index.php index 31d98ce567..8d18ae7bfc 100644 --- a/tests/python/tests/instance_cache/php/index.php +++ b/tests/python/tests/instance_cache/php/index.php @@ -62,6 +62,9 @@ function __construct(TestClassA $a1 = null, TestClassA $a2 = null, /** @var tuple(TestClassB[]|null, int) */ public $b; + /** @var shape(b:tuple(int, TestClassB|null), c:shape(arr:TestClassC[]))|null */ + public $ab = null; + /** @var string[] */ public $huge_arr = []; } From 20652efa48bbb1fe70947f5b20b3471d311bff60 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Thu, 27 Aug 2026 17:43:08 +0300 Subject: [PATCH 19/21] added new test --- .../13_instance_cache_empty_array.php | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/phpt/instance_cache/13_instance_cache_empty_array.php diff --git a/tests/phpt/instance_cache/13_instance_cache_empty_array.php b/tests/phpt/instance_cache/13_instance_cache_empty_array.php new file mode 100644 index 0000000000..4d7e22d9af --- /dev/null +++ b/tests/phpt/instance_cache/13_instance_cache_empty_array.php @@ -0,0 +1,105 @@ +@ok non-idempotent +empty_array)); + } +} + +/** + * @kphp-immutable-class + */ +class EmptyArraysHolder +{ + /** @var int[] */ + public array $empty_int_array = []; + /** @var string[] */ + public array $empty_string_array = []; + /** @var EmptyArrayHolder[] */ + public array $empty_instance_array = []; + /** @var int[][] */ + public array $array_with_empty_nested = [[], [1, 2, 3], []]; + /** @var mixed */ + public $mixed_empty_array = []; +} + +function test_store_fetch_various_empty_arrays() +{ + var_dump(instance_cache_store("test_store_fetch_various_empty_arrays", new EmptyArraysHolder)); + $fetched = instance_cache_fetch(EmptyArraysHolder::class, "test_store_fetch_various_empty_arrays"); + if ($fetched === null) { + var_dump(false); + return; + } + var_dump(empty($fetched->empty_int_array)); + var_dump(empty($fetched->empty_string_array)); + var_dump(empty($fetched->empty_instance_array)); + var_dump(count($fetched->array_with_empty_nested)); + var_dump(empty($fetched->array_with_empty_nested[0])); + var_dump($fetched->array_with_empty_nested[1]); + var_dump(empty($fetched->array_with_empty_nested[2])); + var_dump(is_array($fetched->mixed_empty_array)); + var_dump(empty($fetched->mixed_empty_array)); +} + +function test_store_fetch_empty_array_twice_independent_keys() +{ + var_dump(instance_cache_store("test_store_fetch_empty_array_twice_key1", new EmptyArrayHolder)); + var_dump(instance_cache_store("test_store_fetch_empty_array_twice_key2", new EmptyArraysHolder)); + + $holder1 = instance_cache_fetch(EmptyArrayHolder::class, "test_store_fetch_empty_array_twice_key1"); + $holder2 = instance_cache_fetch(EmptyArraysHolder::class, "test_store_fetch_empty_array_twice_key2"); + if ($holder1 !== null) { + var_dump(empty($holder1->empty_array)); + } + if ($holder2 !== null) { + var_dump(empty($holder2->empty_int_array)); + } +} + +function test_empty_array_refcnt_preserved() +{ + $const_empty_array = []; + $expected_refcnt = get_reference_counter($const_empty_array); + + instance_cache_store("test_empty_array_refcnt_preserved", new EmptyArraysHolder); + $fetched = instance_cache_fetch(EmptyArraysHolder::class, "test_empty_array_refcnt_preserved"); + if ($fetched === null) { + var_dump(false); + return; + } + + var_dump($expected_refcnt === get_reference_counter($fetched->empty_int_array)); + var_dump($expected_refcnt === get_reference_counter($fetched->empty_string_array)); + var_dump($expected_refcnt === get_reference_counter($fetched->empty_instance_array)); + var_dump($expected_refcnt === get_reference_counter($fetched->array_with_empty_nested[0])); + var_dump($expected_refcnt === get_reference_counter($fetched->array_with_empty_nested[2])); + var_dump($expected_refcnt === get_reference_counter($fetched->mixed_empty_array)); +} + +test_store_fetch_empty_array(); +test_store_fetch_various_empty_arrays(); +test_store_fetch_empty_array_twice_independent_keys(); +test_empty_array_refcnt_preserved(); From 80eabdde99fd2ba7fc86fb7d0458bfa248120340 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Fri, 28 Aug 2026 12:04:46 +0300 Subject: [PATCH 20/21] - --- runtime-common/core/core-types/decl/array_decl.inl | 1 - runtime-common/core/core-types/decl/string_decl.inl | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/runtime-common/core/core-types/decl/array_decl.inl b/runtime-common/core/core-types/decl/array_decl.inl index 8b6e2d1073..79a913a285 100644 --- a/runtime-common/core/core-types/decl/array_decl.inl +++ b/runtime-common/core/core-types/decl/array_decl.inl @@ -485,7 +485,6 @@ public: size_t estimate_memory_usage() const noexcept; size_t calculate_memory_for_copying() const noexcept; - // required alignment of the memory passed to copy_in() static constexpr size_t alignment() noexcept { return alignof(array_inner); } diff --git a/runtime-common/core/core-types/decl/string_decl.inl b/runtime-common/core/core-types/decl/string_decl.inl index b39e870763..4d77845882 100644 --- a/runtime-common/core/core-types/decl/string_decl.inl +++ b/runtime-common/core/core-types/decl/string_decl.inl @@ -260,7 +260,7 @@ public: inline static constexpr size_t inner_sizeof() noexcept { return sizeof(string_inner); } - // required alignment of the memory passed to copy_in() + inline static constexpr size_t alignment() noexcept { return alignof(string_inner); } From af8bc778dd5d68793fa2f093eb46c6413dae9027 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Fri, 28 Aug 2026 13:12:19 +0300 Subject: [PATCH 21/21] removed unused include --- runtime-light/stdlib/system/system-functions.h | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime-light/stdlib/system/system-functions.h b/runtime-light/stdlib/system/system-functions.h index 2bb88b900c..09cad7370a 100644 --- a/runtime-light/stdlib/system/system-functions.h +++ b/runtime-light/stdlib/system/system-functions.h @@ -19,7 +19,6 @@ #include #include #include -#include #include #include