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..5bc9a0eeb0 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); @@ -752,6 +754,12 @@ void ClassDeclaration::compile_get_hash(CodeGenerator &W, ClassPtr klass) { compile_class_method(FunctionSignatureGenerator(W).set_const_this(), klass, "int get_hash()", klass->get_hash()); } +void ClassDeclaration::compile_class_name_hash(CodeGenerator &W, ClassPtr klass) { + // 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; +} + void ClassDeclaration::compile_accept_visitor(CodeGenerator &W, ClassPtr klass, const char *visitor_type) { compile_class_method(FunctionSignatureGenerator(W), klass, fmt_format("void accept({} &visitor)", visitor_type), "generic_accept(visitor)"); } @@ -907,7 +915,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 +947,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_estimate_size_visitor"); + } + compile_accept_json_visitor(W, klass); } @@ -960,8 +975,14 @@ 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}"); + + 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 +1080,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 +1121,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_estimate_size_visitor"); + } + W << NL; compile_accept_json_visitor(W, klass); 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 9d6fcb96c7..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)); @@ -134,13 +132,9 @@ 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()) { + // in K2 mode fetch just reinterprets the shared memory block, so no visitor codegen is needed klass->deeply_require_instance_cache_visitor(); } } @@ -153,12 +147,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..b579ba7041 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,17 @@ public: inline class_instance& operator=(const Optional& null) noexcept; inline class_instance clone() const; + // 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/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)); inline void destroy() { o.reset(); @@ -97,11 +109,26 @@ public: return o->virtual_builtin_sizeof(); } + template + std::enable_if_t{}, size_t> alignment() const noexcept { + return alignof(T); + } + + 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(); } + template + std::enable_if_t{}, class_instance> virtual_builtin_clone_in(vk::span memory) const noexcept { + return clone_in(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 +140,19 @@ public: return res; } + template + std::enable_if_t{}, class_instance> virtual_builtin_clone_in(vk::span memory) const noexcept { + class_instance res; + if (o) { + 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())}; + res.o->set_refcnt(1); + } + return res; + } + template std::enable_if_t{}, void*> get_base_raw_ptr() const noexcept { return get(); @@ -198,6 +238,8 @@ public: private: class_instance clone_impl(std::true_type /*is empty*/) const; class_instance clone_impl(std::false_type /*is empty*/) const; + 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 1067ea6329..c68e1eb84d 100644 --- a/runtime-common/core/class-instance/class-instance.inl +++ b/runtime-common/core/class-instance/class-instance.inl @@ -26,10 +26,31 @@ class_instance class_instance::clone_impl(std::false_type /*is empty*/) co return res; } +template +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 stays null if the memory is insufficient or misaligned + if (likely(!res.is_null())) { + 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_in(vk::span memory) const noexcept { + return clone_in_impl(memory, std::is_empty{}); +} template template @@ -40,6 +61,19 @@ class_instance class_instance::alloc(Args&&... args) { return *this; } +template +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 (unlikely(memory.size() < sizeof(T) || reinterpret_cast(memory.data()) % alignof(T) != 0)) { + return *this; + } + 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..79a913a285 100644 --- a/runtime-common/core/core-types/decl/array_decl.inl +++ b/runtime-common/core/core-types/decl/array_decl.inl @@ -4,6 +4,10 @@ #pragma once +#include +#include + +#include "common/wrappers/span.h" #include "runtime-common/core/core-types/decl/array_iterator.h" #include "runtime-common/core/include.h" @@ -132,7 +136,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 +189,36 @@ private: inline array_inner& operator=(const array_inner& other) = delete; }; + class allocation { + vk::span mem_; + 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: + // 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): + // 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_; + } + 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 +230,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 bool 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 +267,12 @@ public: template> inline array(array&& other) noexcept __attribute__((always_inline)); + // 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 inline static array create(Args&&... args) __attribute__((always_inline)); @@ -440,6 +485,10 @@ public: size_t estimate_memory_usage() const noexcept; size_t calculate_memory_for_copying() const noexcept; + 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 211d804b0e..4d77845882 100644 --- a/runtime-common/core/core-types/decl/string_decl.inl +++ b/runtime-common/core/core-types/decl/string_decl.inl @@ -1,5 +1,10 @@ #pragma once +#include +#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 +62,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 +75,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 +92,8 @@ private: friend class string_cache; + inline bool 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 +110,11 @@ public: inline string(); inline string(const string& str) noexcept; inline string(string&& str) noexcept; + // 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); // IMPORTANT: this constructor may return read-only strings for n == 0 and n == 1. @@ -246,6 +260,10 @@ public: inline static constexpr size_t inner_sizeof() noexcept { return sizeof(string_inner); } + + 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/core-types/definition/array.inl b/runtime-common/core/core-types/definition/array.inl index ce1270adae..e4eb8eaf6c 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" @@ -247,21 +248,43 @@ 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)); +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)); + return allocation{vk::span{raw_mem, mem_size}, new_int_size, is_vector}; +} + +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 (unlikely(memory.size() < mem_size || reinterpret_cast(memory.data()) % alignof(array_inner) != 0)) { + 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}; +} + +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 +292,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 +693,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::allocate(int_size, true)); const auto size = static_cast(p->size); T* it = (T*)p->entries(); @@ -689,7 +712,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::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)) { @@ -739,7 +762,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::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)) { @@ -770,7 +793,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::allocate(new_int_size, false)); if (is_vector()) { for (uint32_t it = 0; it != p->size; it++) { @@ -845,7 +868,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::allocate(p->size + 4, false)); T* elements = reinterpret_cast(p->entries()); const bool move_values = p->ref_cnt == 0; @@ -867,26 +890,57 @@ 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; } - array_inner* new_array = array_inner::create(other.p->size, other.is_vector()); + copy_from_impl(create_from_allocation(allocation::allocate(other.p->size, other.is_vector())), other); +} + +template +template +bool array::copy_from(vk::span memory, const array& other) noexcept { + if (other.empty()) { + p = array_inner::empty_array(); + return true; + } + + auto alloc{allocation::from_external(memory, other.p->size, other.is_vector())}; + if (unlikely(!alloc.has_value())) { + return false; + } + copy_from_impl(create_from_allocation(*alloc), other); + return true; +} + +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)); } } } @@ -910,7 +964,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::allocate(other.p->size, other.is_vector())); if (new_array->is_vector()) { uint32_t size = other.p->size; @@ -948,7 +1002,7 @@ array::array() template array::array(const array_size& s) - : p(array_inner::create(s.size, s.is_vector)) {} + : p(create_from_allocation(allocation::allocate(s.size, s.is_vector))) {} template template @@ -981,6 +1035,15 @@ array::array(array&& other) noexcept { move_from(std::move(other)); } +template +std::optional> array::copy_in(vk::span memory, const array& other) noexcept { + array res; + if (unlikely(!res.copy_from(memory, other))) { + return std::nullopt; + } + return res; +} + template template inline array array::create(Args&&... args) { @@ -1652,7 +1715,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::allocate(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 +1739,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::allocate(p->size + other.p->size + 4, false)); T* it = (T*)p->entries(); for (uint32_t i = 0; i != p->size; i++) { @@ -1694,7 +1757,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::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)) { @@ -1873,7 +1936,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::allocate(n, true)); for (array_bucket* it = p->begin(); it != p->end(); it = p->next(it)) { res->push_back_vector_value(it->value); } @@ -2030,7 +2093,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::allocate(new_size.size, is_v)); array_bucket* it = p->begin(); T res = it->value; @@ -2069,7 +2132,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::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 0cb008db1a..6fe87fb8d5 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,17 @@ 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)); + if (unlikely(memory.size() < new_size || reinterpret_cast(memory.data()) % alignof(string_inner) != 0)) { + return nullptr; + } + 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 +112,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 +122,19 @@ 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 (unlikely(r == nullptr)) { + return nullptr; + } + 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 +212,14 @@ string::string(string&& str) noexcept str.p = string_cache::empty_string().ref_data(); } +std::optional string::copy_in(vk::span memory, const string& str) noexcept { + string res; + if (unlikely(!res.copy_from(memory, str))) { + return std::nullopt; + } + return res; +} + string::string(const char* s, size_type n) : p(create(s, s + n)) {} @@ -325,6 +357,14 @@ string string::copy_and_make_not_shared() const { return result; } +bool string::copy_from(vk::span memory, const string& other) noexcept { + if (char* new_p{other.inner()->clone(memory, other.size())}; likely(new_p != nullptr)) { + p = new_p; + return true; + } + return false; +} + void string::force_reserve(size_type res) { char* new_p = inner()->clone(res); inner()->dispose(); 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-common/stdlib/visitors/dummy-visitor-methods.h b/runtime-common/stdlib/visitors/dummy-visitor-methods.h index 2758bc009b..1b7377425f 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_estimate_size_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_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 new file mode 100644 index 0000000000..9fb47de859 --- /dev/null +++ b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h @@ -0,0 +1,119 @@ +// 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 + +#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 +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, and 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: + 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} {} + + 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..d71e6e3199 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::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); } @@ -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/diagnostics/exception-types.h b/runtime-light/stdlib/diagnostics/exception-types.h index 5c09359a27..26c34be603 100644 --- a/runtime-light/stdlib/diagnostics/exception-types.h +++ b/runtime-light/stdlib/diagnostics/exception-types.h @@ -16,6 +16,8 @@ #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; @@ -58,10 +60,30 @@ 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_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) } + virtual size_t virtual_builtin_sizeof() const noexcept { + return 0; + } + + virtual size_t virtual_builtin_alignof() 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; @@ -112,14 +134,22 @@ 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}; } - size_t virtual_builtin_sizeof() 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 override { return sizeof(*this); } + size_t virtual_builtin_alignof() const noexcept override { + return alignof(C$Exception); + } + const char* get_class() const noexcept override { return "Exception"; } @@ -161,14 +191,22 @@ 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}; } - size_t virtual_builtin_sizeof() 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 override { 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/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 01c9405f8f..b96efcb7c0 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -4,166 +4,150 @@ #pragma once -#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/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 template -kphp::coro::task f$instance_cache_store(string key, InstanceType instance, int64_t ttl = 0) noexcept { +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 (instance.is_null()) [[unlikely]] { + kphp::log::warning("instance_cache_store. can't store a null instance: key -> {}", key.c_str()); + return false; + } if (ttl < 0) [[unlikely]] { - kphp::log::warning("ttl can't be negative: ttl -> {}, key -> {}", ttl, key.c_str()); - co_return false; + kphp::log::warning("instance_cache_store. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl, key.c_str()); + ttl = 0; } - 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()); + 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; } - 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; + kphp::visitors::instance_deep_estimate_size_visitor estimate_size_visitor{}; + 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; } - - 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); - - auto expected_stream{kphp::component::stream::open(kphp::instance_cache::details::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_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)}; + + 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; } - - 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{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 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; + } + 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 + 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; } - - 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 { - 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); +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)}; + + if (key.empty()) [[unlikely]] { + kphp::log::warning("instance_cache_fetch. empty key is not supported"); + return {}; } + // 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 {}; + } - tl::CacheFetch cache_fetch{.key = tl::string{.value = {key.c_str(), key.size()}}}; - tl::storer tls{cache_fetch.footprint()}; - cache_fetch.store(tls); + uint64_t stored_class_name_hash{}; + std::memcpy(&stored_class_name_hash, mem.data(), sizeof(stored_class_name_hash)); - 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 (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 {}; + } - 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{}; - } + return *reinterpret_cast(mem.data() + hash_size); + }}; - tl::fetcher tlf{response}; - tl::Maybe maybe_string{}; - kphp::log::assertion(maybe_string.fetch(tlf)); - if (!maybe_string.opt_value) [[unlikely]] { - co_return InstanceType{}; + auto& request_cache{InstanceCacheInstanceState::get().request_cache}; + if (auto it{request_cache.find(key)}; it != request_cache.end()) { + return unwrap(it->second); } - 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); + 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, get_result.value()); + return unwrap(get_result.value()); } -inline kphp::coro::task f$instance_cache_update_ttl(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; +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 (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()); + 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; } - - 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; + 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; } - - 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 { - 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; +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; } - - 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; + 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 88f65d3b95..0bcc3bdc8f 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/job-worker/job-worker.h b/runtime-light/stdlib/job-worker/job-worker.h index e82b2cd4b7..c57ec451b6 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_estimate_size_visitor; +} // namespace kphp::visitors + namespace job_worker_impl_ { struct SendableBase : virtual abstract_refcountable_php_interface { @@ -27,10 +32,16 @@ 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_estimate_size_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 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; ~SendableBase() override = default; }; @@ -46,18 +57,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 ================================================================= @@ -79,9 +93,17 @@ 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}; } + + 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 b4a77f14d1..9bc3104bce 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_estimate_size_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,13 +44,21 @@ 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_estimate_size_visitor& /*unused*/) noexcept {} 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; } + 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; @@ -67,13 +80,21 @@ 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_estimate_size_visitor& /*unused*/) noexcept {} 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; } + virtual C$VK$TL$RpcFunctionReturnResult* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } ~C$VK$TL$RpcFunctionReturnResult() override = default; }; @@ -94,13 +115,21 @@ 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_estimate_size_visitor& /*unused*/) noexcept {} 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; } + virtual C$VK$TL$RpcFunctionFetcher* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } ~C$VK$TL$RpcFunctionFetcher() override = default; }; @@ -115,6 +144,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_estimate_size_visitor& /*unused*/) noexcept {} virtual const char* get_class() const noexcept { return "VK\\TL\\RpcResponse"; @@ -127,9 +158,15 @@ 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; } + virtual C$VK$TL$RpcResponse* virtual_builtin_construct_at(void* /*unused*/) const noexcept { + return nullptr; + } ~C$VK$TL$RpcResponse() override = default; }; diff --git a/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h new file mode 100644 index 0000000000..692134b135 --- /dev/null +++ b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h @@ -0,0 +1,155 @@ +// 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 "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" +#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 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; + + 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) 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; + } + + auto copied{array::copy_in(carve(arr.calculate_memory_for_copying(), array::alignment()), 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 + 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{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()); + } + + bool process(string& str) noexcept { + if (str.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + auto copied{string::copy_in(carve(str.estimate_memory_usage(), string::alignment()), 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); + 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; + } + + bool process(mixed& value) noexcept { + if (value.is_object()) { + 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; + } + 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 + class_instance instance_keepalive{instance}; + const bool result{process(instance)}; + this->copied_instances_table.clear(); + return result; + } + +private: + 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; + } + + // 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.alignment())); + 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) { + instance.set_reference_counter_to(extra_ref_cnt); + } + return Basic::process(instance); + } + + // 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 {}; + } + + kphp::log::assertion(std::align(align, size, mem, space)); + return {static_cast(mem), size}; + } + + memory_resource::monotonic_buffer_resource memory_pool; + kphp::stl::unordered_map copied_instances_table; +}; + +} // namespace kphp::visitors diff --git a/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h new file mode 100644 index 0000000000..ae4920e780 --- /dev/null +++ b/runtime-light/stdlib/visitors/instance-deep-estimate-size-visitor.h @@ -0,0 +1,97 @@ +// 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" +#include "runtime-light/stdlib/diagnostics/logs.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_estimate_size_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(); + + 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_estimate_size_visitor() noexcept + : Basic{*this} {} + + template + bool process(array& arr) noexcept { + if (arr.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + 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. + 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->estimated_size += memory_resource::details::align_for_chunk(str.estimate_memory_usage(), string::alignment()); + } + + 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)}; + this->visited_instances_set.clear(); + return result; + } + + size_t get_estimated_size() const noexcept { + return this->estimated_size; + } + +private: + 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->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); + } + + size_t estimated_size{0}; + kphp::stl::unordered_set visited_instances_set; +}; + +} // namespace kphp::visitors 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/instance-copy-processor.h b/runtime/instance-copy-processor.h index 092feb6876..f7aada869d 100644 --- a/runtime/instance-copy-processor.h +++ b/runtime/instance-copy-processor.h @@ -12,112 +12,16 @@ #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" -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; }); - } + friend class kphp::visitors::instance_deep_basic_visitor; - 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: - 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 +51,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 +63,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 { @@ -178,6 +82,14 @@ class InstanceDeepCopyVisitor : impl_::InstanceDeepBasicVisitorget_class()); + return false; + } + return Basic::process(value); + } + bool is_memory_limit_exceeded() const noexcept { return memory_limit_exceeded_; } @@ -290,11 +202,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 +245,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 +272,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/runtime/job-workers/job-interface.h b/runtime/job-workers/job-interface.h index 1126ff313b..aea8eb0eb0 100644 --- a/runtime/job-workers/job-interface.h +++ b/runtime/job-workers/job-interface.h @@ -49,7 +49,9 @@ 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* virtual_builtin_construct_at(void* ptr) const noexcept = 0; virtual ~SendingInstanceBase() = default; }; @@ -62,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; @@ -73,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 { @@ -116,9 +121,17 @@ 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}; } + + 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/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; } 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 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(); diff --git a/tests/phpt/instance_cache/13_instance_cache_serializable.php b/tests/phpt/instance_cache/13_instance_cache_serializable.php deleted file mode 100644 index bd177a8412..0000000000 --- a/tests/phpt/instance_cache/13_instance_cache_serializable.php +++ /dev/null @@ -1,263 +0,0 @@ -@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 shape(b:tuple(int, TestClassB|null), c:shape(arr:TestClassC[]))|null */ + public $ab = null; + + /** @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 +80,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 +95,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 +126,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 +146,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";