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 7f86f4306d..7fc08bd056 100644 --- a/compiler/code-gen/declarations.cpp +++ b/compiler/code-gen/declarations.cpp @@ -17,6 +17,7 @@ #include "common/algorithms/compare.h" #include "common/algorithms/find.h" +#include "common/algorithms/hashes.h" #include "common/tlo-parsing/tl-objects.h" #include "common/wrappers/fmt_format.h" #include "common/wrappers/iterator_range.h" @@ -588,6 +589,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); @@ -759,6 +761,13 @@ 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. + FunctionSignatureGenerator(W) << "static uint64_t get_class_name_hash()" << BEGIN; + W << "return " << vk::murmur_hash(klass->name.data(), klass->name.size()) << "ULL;" << NL << END << 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)"); } @@ -914,8 +923,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); + bool need_generic_accept = 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; @@ -947,6 +955,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); } @@ -1063,8 +1078,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); + bool need_generic_accept = 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; @@ -1105,6 +1119,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 b88b01dc36..f6eb0d9798 100644 --- a/compiler/code-gen/declarations.h +++ b/compiler/code-gen/declarations.h @@ -131,6 +131,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/compiler-settings.cpp b/compiler/compiler-settings.cpp index 2eab8d9f83..9107d281ab 100644 --- a/compiler/compiler-settings.cpp +++ b/compiler/compiler-settings.cpp @@ -340,6 +340,8 @@ void CompilerSettings::init() { ss << " -I" << kphp_src_path.get() + "objs/include "; if (is_k2_mode) { + // Generated code and its precompiled header must use the light runtime declarations. + ss << " -DRUNTIME_LIGHT"; // for now k2-component must be compiled with clang and statically linked libc++ ss << " -stdlib=libc++"; if (!dynamic_incremental_linkage.get()) { diff --git a/compiler/pipes/final-check.cpp b/compiler/pipes/final-check.cpp index 60ab7dfa1f..eba96ee954 100644 --- a/compiler/pipes/final-check.cpp +++ b/compiler/pipes/final-check.cpp @@ -72,7 +72,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; @@ -97,7 +96,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)); @@ -164,12 +162,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(); } } @@ -182,12 +177,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/allocator/pool-allocator.h b/runtime-common/core/allocator/pool-allocator.h index a92b7cd476..fe85ebd2c1 100644 --- a/runtime-common/core/allocator/pool-allocator.h +++ b/runtime-common/core/allocator/pool-allocator.h @@ -12,17 +12,25 @@ namespace kphp::memory { struct pool_allocator : private vk::not_copyable { + struct external_memory {}; + private: + enum class memory_mode { + owned_growable, + external_fixed, // Never grows or releases the externally owned backing buffer. + }; + memory_resource::unsynchronized_pool_resource memory_resource; + memory_mode m_memory_mode{memory_mode::owned_growable}; size_t m_min_extra_mem_size{0}; auto request_extra_memory(size_t requested_size) noexcept -> void; public: - pool_allocator() = default; pool_allocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept; + // Borrows the buffer without growing it or releasing it in free(). + pool_allocator(external_memory, void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept; - auto init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void; auto free() noexcept -> void; auto alloc_script_memory(size_t size) noexcept -> void*; diff --git a/runtime-common/core/allocator/runtime-allocator.h b/runtime-common/core/allocator/runtime-allocator.h index 46b948b130..72da66f458 100644 --- a/runtime-common/core/allocator/runtime-allocator.h +++ b/runtime-common/core/allocator/runtime-allocator.h @@ -1,24 +1,37 @@ -// Compiler for PHP (aka KPHP) -// Copyright (c) 2024 LLC «V Kontakte» -// Distributed under the GPL v3 License, see LICENSE.notice.txt +// Compiler for PHP (aka KPHP) +// Copyright (c) 2024 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt #pragma once #include +#ifdef RUNTIME_LIGHT +#include +#include +#include +#include +#include "common/containers/final_action.h" #include "runtime-common/core/allocator/pool-allocator.h" +#endif struct RuntimeAllocator final { +#ifdef RUNTIME_LIGHT private: kphp::memory::pool_allocator m_allocator; + std::reference_wrapper m_allocator_ref{m_allocator}; +#endif public: static auto get() noexcept -> RuntimeAllocator&; - RuntimeAllocator() = default; +#ifdef RUNTIME_LIGHT RuntimeAllocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept; - +#else + RuntimeAllocator() = default; auto init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void; +#endif + auto free() noexcept -> void; auto alloc_script_memory(size_t size) noexcept -> void*; @@ -26,7 +39,20 @@ struct RuntimeAllocator final { auto realloc_script_memory(void* mem, size_t new_size, size_t old_size) noexcept -> void*; auto free_script_memory(void* mem, size_t size) noexcept -> void; +#ifdef RUNTIME_LIGHT auto get_memory_resource() noexcept -> memory_resource::unsynchronized_pool_resource& { - return m_allocator.get_memory_resource(); + return m_allocator_ref.get().get_memory_resource(); + } + + // The callback must run synchronously without yielding. Objects allocated by it + // may outlive the scope, but later operations that allocate or deallocate their + // memory must install the same allocator. The replacement must outlive the callback. + template && std::is_same_v, void>, int32_t> = 0> + auto with_allocator(kphp::memory::pool_allocator& replacement, callback_type&& callback) noexcept -> void { + const auto previous_allocator{std::exchange(m_allocator_ref, std::ref(replacement))}; + const auto restore_allocator{vk::finally([this, previous_allocator]() noexcept { m_allocator_ref = previous_allocator; })}; + std::invoke(std::forward(callback)); } +#endif }; 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..2044e4b0ca --- /dev/null +++ b/runtime-common/stdlib/visitors/instance-deep-basic-visitor.h @@ -0,0 +1,116 @@ +// 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 { + +// 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/allocator/allocator.h b/runtime-light/allocator/allocator.h index bd625d9ebc..14c7921fa0 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -5,7 +5,8 @@ #pragma once #include -#include +#include +#include #include "runtime-common/core/allocator/script-allocator-managed.h" #include "runtime-light/allocator/allocator-state.h" @@ -17,6 +18,7 @@ auto make_unique_on_script_memory(Args&&... args) noexcept { } namespace kphp::memory { + struct libc_alloc_guard final { libc_alloc_guard() noexcept { AllocatorState::get_mutable().enable_libc_alloc(); diff --git a/runtime-light/allocator/pool-allocator.cpp b/runtime-light/allocator/pool-allocator.cpp index f75fa2fc76..c487c29799 100644 --- a/runtime-light/allocator/pool-allocator.cpp +++ b/runtime-light/allocator/pool-allocator.cpp @@ -16,25 +16,22 @@ namespace kphp::memory { pool_allocator::pool_allocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept : m_min_extra_mem_size(min_extra_mem_size) { - // kphp::log::debug("create pool allocator -> {:p}: script memory -> {}, oom handling size -> {}", reinterpret_cast(this), script_mem_size, - // oom_handling_mem_size); void* buffer{kphp::memory::platform::alloc(script_mem_size)}; - kphp::log::assertion(buffer != nullptr); - memory_resource.init(buffer, script_mem_size, oom_handling_mem_size); } -auto pool_allocator::init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void { +pool_allocator::pool_allocator(external_memory /*unused*/, void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept + : m_memory_mode{memory_mode::external_fixed} { kphp::log::assertion(buffer != nullptr); - - // kphp::log::debug("init pool allocator -> {:p}: buffer -> {:p}, script memory -> {}, oom handling size -> {}", reinterpret_cast(this), buffer, - // script_mem_size, oom_handling_mem_size); memory_resource.init(buffer, script_mem_size, oom_handling_mem_size); } auto pool_allocator::free() noexcept -> void { - // kphp::log::debug("free pool allocator -> {:p}", reinterpret_cast(this)); + if (m_memory_mode == memory_mode::external_fixed) { + return; + } + auto* extra_memory{memory_resource.get_extra_memory_head()}; while (extra_memory->get_pool_payload_size() != 0) { auto* extra_memory_to_release{extra_memory}; @@ -94,6 +91,9 @@ auto pool_allocator::free_script_memory(void* mem, size_t size) noexcept -> void } auto pool_allocator::request_extra_memory(size_t requested_size) noexcept -> void { + // Fixed pools must fail on exhaustion instead of allocating outside their buffer. + kphp::log::assertion(m_memory_mode == memory_mode::owned_growable); + // Extra mem size have to be greater than max chunk block const auto min_size{std::max(m_min_extra_mem_size, memory_resource::unsynchronized_pool_resource::MAX_CHUNK_BLOCK_SIZE)}; @@ -103,12 +103,8 @@ auto pool_allocator::request_extra_memory(size_t requested_size) noexcept -> voi // The smallest power of two that is not smaller than `extra_mem_size` extra_mem_size = std::bit_ceil(extra_mem_size); - // kphp::log::debug("requested extra memory pool with size {} bytes, will be allocated {} bytes", requested_size, extra_mem_size); - auto* extra_mem{kphp::memory::platform::alloc(extra_mem_size)}; - kphp::log::assertion(extra_mem != nullptr); - memory_resource.add_extra_memory(new (extra_mem) memory_resource::extra_memory_pool{extra_mem_size}); } diff --git a/runtime-light/allocator/runtime-light-allocator.cpp b/runtime-light/allocator/runtime-light-allocator.cpp index 0bcfc019d5..9c7aac979b 100644 --- a/runtime-light/allocator/runtime-light-allocator.cpp +++ b/runtime-light/allocator/runtime-light-allocator.cpp @@ -3,7 +3,6 @@ // Distributed under the GPL v3 License, see LICENSE.notice.txt #include "runtime-common/core/allocator/runtime-allocator.h" - #include "runtime-light/allocator/allocator-state.h" auto RuntimeAllocator::get() noexcept -> RuntimeAllocator& { @@ -13,26 +12,22 @@ auto RuntimeAllocator::get() noexcept -> RuntimeAllocator& { RuntimeAllocator::RuntimeAllocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept : m_allocator{script_mem_size, min_extra_mem_size, oom_handling_mem_size} {} -auto RuntimeAllocator::init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void { - m_allocator.init(buffer, script_mem_size, oom_handling_mem_size); -} - auto RuntimeAllocator::free() noexcept -> void { m_allocator.free(); } auto RuntimeAllocator::alloc_script_memory(size_t size) noexcept -> void* { - return m_allocator.alloc_script_memory(size); + return m_allocator_ref.get().alloc_script_memory(size); } auto RuntimeAllocator::calloc_script_memory(size_t size) noexcept -> void* { - return m_allocator.calloc_script_memory(size); + return m_allocator_ref.get().calloc_script_memory(size); } auto RuntimeAllocator::realloc_script_memory(void* mem, size_t new_size, size_t old_size) noexcept -> void* { - return m_allocator.realloc_script_memory(mem, new_size, old_size); + return m_allocator_ref.get().realloc_script_memory(mem, new_size, old_size); } auto RuntimeAllocator::free_script_memory(void* mem, size_t size) noexcept -> void { - m_allocator.free_script_memory(mem, size); + m_allocator_ref.get().free_script_memory(mem, size); } diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index a066930941..768c2512f5 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -129,6 +129,54 @@ 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 = k2::details::DEFAULT_MEMORY_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_ms, bool as_mut, + bool ignore_if_exist) noexcept { + if (auto error_code{k2_publish_shared_memory(name.data(), name.length(), memory, ttl_ms, 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 republish_shared_memory(std::string_view name, uint64_t ttl) noexcept { + if (auto error_code{k2_republish_shared_memory(name.data(), name.length(), ttl)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected seek_ttl_to_shared_memory(std::string_view name, uint8_t percentile, uint64_t remaining_lifetime_limit) noexcept { + if (auto error_code{k2_seek_ttl_to_shared_memory(name.data(), name.length(), percentile, remaining_lifetime_limit)}; error_code != k2::errno_ok) + [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected release_shared_memory(const void* ptr) noexcept { + if (auto error_code{k2_release_shared_memory(ptr)}; 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..4a5b9a49b4 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -209,9 +209,12 @@ void k2_free_checked(void* ptr, size_t size, size_t align); * * Reference counting: * - Calling `k2_publish_shared_memory` sets the reference count to one - * - Calling `k2_get_shared_memory` increments the reference count - * - Reference count is decremented automatically when instance finishes - * - No explicit release function is needed + * - Calling `k2_get_shared_memory` increments the reference count by one if + * this instance does not already hold a reference; repeated calls for the + * same underlying pointer while already held do not increment it further + * - Calling `k2_release_shared_memory` clears the held reference, if `pointer` + * is currently held; unpublished memory from `k2_alloc_shared_memory` is + * just freed */ /** @@ -251,8 +254,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. @@ -260,7 +264,9 @@ int32_t k2_alloc_shared_memory(size_t size, size_t align, void** pointer); int32_t k2_publish_shared_memory(const char* name, size_t name_len, const void* memory, uint64_t ttl, bool as_mut, bool ignore_if_exist); /** - * Retrieves shared memory by name and increments its reference count. + * Retrieves shared memory by name and, unless already held by this + * instance, acquires a reference to it (see the reference counting notes + * above). * * @param `name` Name of the published memory region to retrieve. * @param `name_len` Length of the name in bytes. Must be greater than 0. @@ -277,6 +283,85 @@ 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); +/** + * Republishes shared memory, resetting its TTL and restarting the age counter from this call. + * + * @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_republish_shared_memory(const char* name, size_t name_len, uint64_t ttl); + +/** + * Fast-forwards the expiration of published shared memory ahead of its TTL, by name. + * Its expiration is recomputed so that, at the moment of this call, the entry's age + * already amounts to `percentile` of its (recomputed) TTL. + * + * Timeline right after the call, where `S` = `stored_at`, `C` = the current instant and `E` = the recomputed `expires_at`: + * + * S C E + * |-----------------------------------|--------| + * |<---------- age = C - S ---------->| + * |<-------------- ttl = E - S --------------->| + * + * i.e. `age / ttl == percentile / 100`. The higher `percentile` is, the shorter the + * remaining slice `E - C` is (closer to `100` puts `E` right next to `C`, expiring almost immediately); + * the lower it is, the further away `E` ends up. + * `remaining_lifetime_limit` additionally clamps `E` to no later than `C + remaining_lifetime_limit`, regardless of `percentile`, when non-zero. + * + * @param `name` Name of the published memory region to seek. + * @param `name_len` Length of the name in bytes. Must be greater than 0. + * @param `percentile` Target percentile (entry age / TTL * 100) to seek the entry to, + * in `[0, 100]`. E.g. `80` makes the entry appear 80% of the way through + * its lifetime as of this call. + * @param `remaining_lifetime_limit` Upper bound, in milliseconds, on how much longer the entry may + * live from this call, regardless of `percentile`. Zero means no limit. + * + * @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 + * `percentile` is outside of `[0, 100]`. + * `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_seek_ttl_to_shared_memory(const char* name, size_t name_len, uint8_t percentile, uint64_t remaining_lifetime_limit); + +/** + * Frees shared memory allocated via `k2_alloc_shared_memory` that was never + * published, or releases the reference to shared memory held by this + * instance, acquired via `k2_publish_shared_memory` or + * `k2_get_shared_memory` (see the reference counting notes above). + * + * Releasing a reference does not free the underlying memory immediately: the + * memory is only physically reclaimed once its TTL has expired AND the + * reference count has reached zero (see the lifecycle notes above). + * + * @param `pointer` Pointer to shared memory, as returned by + * `k2_alloc_shared_memory` or `k2_get_shared_memory`, or + * passed as the `memory` argument to `k2_publish_shared_memory`. + * + * @return `0` on success: either the unpublished allocation was freed, or the + * reference held by this instance was released. libc-like `errno` + * on error. + * + * Possible `errno`: + * `EINVAL` => `pointer` is NULL. + * `ENOENT` => `pointer` does not correspond to memory allocated by this + * instance via `k2_alloc_shared_memory`, nor to a reference + * currently held by this instance. + */ +int32_t k2_release_shared_memory(const void* pointer); + /** * Immediately abort component execution. * Function is `[[noreturn]]` diff --git a/runtime-light/runtime-light.cmake b/runtime-light/runtime-light.cmake index 736fa8cec2..c16371b353 100644 --- a/runtime-light/runtime-light.cmake +++ b/runtime-light/runtime-light.cmake @@ -2,7 +2,7 @@ include(${THIRD_PARTY_DIR}/pcre2-cmake/pcre2.cmake) # ================================================================================================= -set(RUNTIME_LIGHT_COMPILE_FLAGS -stdlib=libc++ -fcoro-aligned-allocation ${RUNTIME_LIGHT_VISIBILITY}) +set(RUNTIME_LIGHT_COMPILE_FLAGS -DRUNTIME_LIGHT -stdlib=libc++ -fcoro-aligned-allocation ${RUNTIME_LIGHT_VISIBILITY}) set(RUNTIME_LIGHT_PLATFORM_SPECIFIC_LINK_FLAGS) if(APPLE) diff --git a/runtime-light/stdlib/array/array-functions.h b/runtime-light/stdlib/array/array-functions.h index 1b1770b5bb..e137ecba3b 100644 --- a/runtime-light/stdlib/array/array-functions.h +++ b/runtime-light/stdlib/array/array-functions.h @@ -78,7 +78,7 @@ 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(); + int64_t n{arr.count()}; if (renumber) { if (n == 0) { @@ -86,8 +86,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_inner::create(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 +97,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 +113,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..3343edfd76 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,6 +60,10 @@ struct C$Throwable : public refcountable_polymorphic_php_classes_virt<> { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} + virtual void accept(ToArrayVisitor& visitor) noexcept { generic_accept(visitor); // don't process raw_trace because `mixed` can't store `void *` (to_array_debug returns array) } diff --git a/runtime-light/stdlib/instance-cache/instance-cache-functions.h b/runtime-light/stdlib/instance-cache/instance-cache-functions.h index 01c9405f8f..d7c9174ea5 100644 --- a/runtime-light/stdlib/instance-cache/instance-cache-functions.h +++ b/runtime-light/stdlib/instance-cache/instance-cache-functions.h @@ -4,166 +4,157 @@ #pragma once -#include #include #include +#include +#include #include +#include #include #include #include -#include "runtime-common/core/allocator/script-allocator.h" +#include "common/algorithms/hashes.h" #include "runtime-common/core/runtime-core.h" -#include "runtime-common/core/std/containers.h" -#include "runtime-common/stdlib/serialization/msgpack-functions.h" -#include "runtime-light/coroutine/task.h" -#include "runtime-light/stdlib/component/component-api.h" +#include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/stdlib/diagnostics/logs.h" -#include "runtime-light/stdlib/fork/fork-functions.h" #include "runtime-light/stdlib/instance-cache/instance-cache-state.h" -#include "runtime-light/stdlib/serialization/msgpack-functions.h" -#include "runtime-light/streams/read-ext.h" -#include "runtime-light/streams/stream.h" -#include "runtime-light/tl/tl-core.h" -#include "runtime-light/tl/tl-functions.h" -#include "runtime-light/tl/tl-types.h" - -namespace kphp::instance_cache::details { - -inline constexpr std::string_view COMPONENT_NAME{"instance_cache"}; - -} // namespace kphp::instance_cache::details +#include "runtime-light/stdlib/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 { - if (ttl < 0) [[unlikely]] { - kphp::log::warning("ttl can't be negative: ttl -> {}, key -> {}", ttl, key.c_str()); - co_return false; +bool f$instance_cache_store(const string& key, class_instance instance, int64_t ttl_sec = 0) noexcept { + if (key.empty()) [[unlikely]] { + kphp::log::warning("instance_cache_store. empty key is not supported"); + return false; } - if (ttl > std::numeric_limits::max()) [[unlikely]] { - kphp::log::warning("ttl exceeds maximum allowed value, key will be stored forever: ttl -> {}, max -> {}, key -> {}", ttl, - std::numeric_limits::max(), key.c_str()); - ttl = 0; + if (instance.is_null()) [[unlikely]] { + kphp::log::warning("instance_cache_store. can't store a null instance: key -> {}", key.c_str()); + return false; } - - auto serialized_instance{f$instance_serialize(instance)}; - if (!serialized_instance.has_value()) [[unlikely]] { - kphp::log::warning("can't serialize instance: key -> {}", key.c_str()); - co_return false; + if (ttl_sec < 0) [[unlikely]] { + kphp::log::warning("instance_cache_store. ttl less than 0, key will be stored forever: ttl -> {}, key -> {}", ttl_sec, key.c_str()); + ttl_sec = 0; } - - tl::CacheStore cache_store{.key = tl::string{.value = {key.c_str(), key.size()}}, - .value = tl::string{.value = {serialized_instance.val().c_str(), serialized_instance.val().size()}}, - .ttl = tl::u32{.value = static_cast(ttl)}}; - tl::storer tls{cache_store.footprint()}; - cache_store.store(tls); - - auto expected_stream{kphp::component::stream::open(kphp::instance_cache::details::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return false; + if (constexpr int64_t max_ttl_sec{std::numeric_limits::max() / 1000}; ttl_sec > max_ttl_sec) [[unlikely]] { + kphp::log::warning("instance_cache_store. ttl is too large, key will be stored forever: ttl -> {}, max ttl -> {}, key -> {}", ttl_sec, max_ttl_sec, + key.c_str()); + ttl_sec = 0; } - 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; + 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; + } + 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)}; + 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; + } + std::byte* mem{static_cast(alloc_result.value())}; + + const uint64_t class_name_hash{InstanceType::get_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{std::next(mem, hash_size + instance_size), estimated_size}, + ExtraRefCnt::for_instance_cache}; + if (!copy_visitor.process_instance(instance)) [[unlikely]] { + kphp::log::assertion(k2::release_shared_memory(mem).has_value()); + 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*>(std::next(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_sec * 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. + kphp::log::assertion(k2::release_shared_memory(mem).has_value()); + 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 {}; } + // materialize and validates a shared memory block: returns a null instance if the block is malformed or belongs to another class + const auto materialize{[&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(std::next(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 materialize(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 materialize(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::republish_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); +inline bool f$instance_cache_delete(const string& key) noexcept { + constexpr uint8_t EARLY_EXPIRATION_ELEMENT_PERCENTILE{80}; + constexpr uint64_t EXPIRED_ELEMENT_REMAINING_LIFETIME_LIMIT_MS{1000}; - auto expected_stream{kphp::component::stream::open(kphp::instance_cache::details::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return false; + 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::seek_ttl_to_shared_memory(std::string_view{key.c_str(), key.size()}, EARLY_EXPIRATION_ELEMENT_PERCENTILE, + EXPIRED_ELEMENT_REMAINING_LIFETIME_LIMIT_MS) + .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..caedf2e309 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,6 +32,10 @@ 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; diff --git a/runtime-light/stdlib/rpc/rpc-tl-function.h b/runtime-light/stdlib/rpc/rpc-tl-function.h index b4a77f14d1..9e294c8796 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,6 +44,8 @@ struct C$VK$TL$RpcFunction : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -67,6 +74,8 @@ struct C$VK$TL$RpcFunctionReturnResult : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -94,6 +103,8 @@ struct C$VK$TL$RpcFunctionFetcher : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual size_t virtual_builtin_sizeof() const noexcept { return 0; @@ -115,6 +126,8 @@ struct C$VK$TL$RpcResponse : abstract_refcountable_php_interface { virtual void accept(InstanceReferencesCountingVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepCopyVisitor& /*unused*/) noexcept {} virtual void accept(InstanceDeepDestroyVisitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_copy_visitor& /*unused*/) noexcept {} + virtual void accept(kphp::visitors::instance_deep_estimate_size_visitor& /*unused*/) noexcept {} virtual const char* get_class() const noexcept { return "VK\\TL\\RpcResponse"; 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..4abadabd9d --- /dev/null +++ b/runtime-light/stdlib/visitors/instance-deep-copy-visitor.h @@ -0,0 +1,128 @@ +// 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 "runtime-common/core/allocator/pool-allocator.h" +#include "runtime-common/core/allocator/runtime-allocator.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. +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}, + allocator{kphp::memory::pool_allocator::external_memory{}, memory_pool_buffer.data(), memory_pool_buffer.size(), /*oom_handling_mem_size=*/0} {} + + template + bool process(array& arr) noexcept { + if (arr.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + RuntimeAllocator::get().with_allocator(allocator, [&arr]() noexcept { arr.mutate_if_shared(); }); + + // copying an empty array yields the global empty-array singleton instead of a real copy -- nothing left to deep-copy + if (arr.is_reference_counter(ExtraRefCnt::for_global_const)) { + kphp::log::assertion(arr.begin_no_mutate() == arr.end_no_mutate()); + return true; + } + + kphp::log::assertion(arr.get_reference_counter() == 1); + if (const auto extra_ref_cnt{get_memory_ref_cnt()}; extra_ref_cnt != 0) { + arr.set_reference_counter_to(extra_ref_cnt); + } + // values of a primitive array were already memcpy'd by the forced copy above, and there are no string keys to copy + 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)) { + return true; + } + + RuntimeAllocator::get().with_allocator(allocator, [&str]() noexcept { str.make_not_shared(); }); + + // make_not_shared may turn str back into a constant (e.g. empty or single-char strings are cached globally) -- check again + if (str.is_reference_counter(ExtraRefCnt::for_global_const)) { + return true; + } + + kphp::log::assertion(str.get_reference_counter() == 1); + if (const auto extra_ref_cnt{get_memory_ref_cnt()}; extra_ref_cnt != 0) { + str.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; + } + + RuntimeAllocator::get().with_allocator(allocator, [&instance]() noexcept { instance = instance.virtual_builtin_clone(); }); + 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); + } + + kphp::memory::pool_allocator allocator; + 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..84b062b2e5 --- /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()); + + // 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()); + } + + 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()); + 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/instance-copy-processor.h b/runtime/instance-copy-processor.h index 092feb6876..d55da4976f 100644 --- a/runtime/instance-copy-processor.h +++ b/runtime/instance-copy-processor.h @@ -12,112 +12,20 @@ #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_ { +namespace kphp::visitors { +inline constexpr uint32_t VISITED_INSTANCE_MASK{1U << 31}; +} // namespace kphp::visitors -template -class InstanceDeepBasicVisitor : vk::not_copyable { +class InstanceReferencesCountingVisitor : kphp::visitors::instance_deep_basic_visitor { public: - template - void operator()(const char*, T&& value) noexcept { - const bool is_ok = child_.process(std::forward(value)); - is_ok_ = is_ok_ && is_ok; - } - - template - bool process(T&) noexcept { - return true; - } - - template - bool process(Optional& value) noexcept { - return value.has_value() ? child_.process(value.val()) : true; - } - - template - bool process(class_instance& instance) noexcept { - if (!instance.is_null()) { - instance.get()->accept(child_); - return child_.is_ok(); - } - return true; - } - - template - bool process(std::tuple& value) noexcept { - return process_tuple(value); - } - - template - bool process(shape, T...>& value) noexcept { - const bool child_res[] = {child_.process(value.template get())...}; - return std::all_of(std::begin(child_res), std::end(child_res), [](bool r) { return r; }); - } - - bool process(mixed& value) noexcept { - if (value.is_string()) { - return child_.process(value.as_string()); - } else if (value.is_array()) { - return child_.process(value.as_array()); - } - return true; - } - - 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; - } + friend class kphp::visitors::instance_deep_basic_visitor; -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 +55,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 +67,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 +86,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 +206,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 +249,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 +276,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/runtime-builtin-stats.h b/runtime/runtime-builtin-stats.h index e8a0306a38..a5d351b10e 100644 --- a/runtime/runtime-builtin-stats.h +++ b/runtime/runtime-builtin-stats.h @@ -12,6 +12,7 @@ #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/std/containers.h" +#include "runtime-common/core/utils/kphp-assert-core.h" template<> struct std::hash> { 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..9c936e17f2 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"); @@ -185,11 +164,20 @@ function test_delete() { var_dump(instance_cache_delete("key_x3")); var_dump(instance_cache_delete("key_y3")); + #ifndef K2 $x = instance_cache_fetch(X::class, "key_x3"); var_dump(!$x); + if (0) + #endif + var_dump(true); // в K2 сейчас нет такой оптимизации + + #ifndef K2 $y = instance_cache_fetch(Y::class, "key_y3"); var_dump(!$y); + if (0) + #endif + var_dump(true); // в K2 сейчас нет такой оптимизации // delete на самом деле не удаляет элемент, // а лишь меняет ему ttl так, что бы следующий fetch вернул false @@ -292,6 +280,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";