diff --git a/rclcpp/include/rclcpp/experimental/buffers/buffer_implementation_base.hpp b/rclcpp/include/rclcpp/experimental/buffers/buffer_implementation_base.hpp index 1d50b1659e..be5e278976 100644 --- a/rclcpp/include/rclcpp/experimental/buffers/buffer_implementation_base.hpp +++ b/rclcpp/include/rclcpp/experimental/buffers/buffer_implementation_base.hpp @@ -15,6 +15,7 @@ #ifndef RCLCPP__EXPERIMENTAL__BUFFERS__BUFFER_IMPLEMENTATION_BASE_HPP_ #define RCLCPP__EXPERIMENTAL__BUFFERS__BUFFER_IMPLEMENTATION_BASE_HPP_ +#include #include namespace rclcpp @@ -28,12 +29,14 @@ template class BufferImplementationBase { public: + using ForEachFunc = std::function; + virtual ~BufferImplementationBase() {} virtual BufferT dequeue() = 0; virtual void enqueue(BufferT request) = 0; - virtual std::vector get_all_data() = 0; + virtual void for_each(ForEachFunc && f) const = 0; virtual void clear() = 0; virtual bool has_data() const = 0; diff --git a/rclcpp/include/rclcpp/experimental/buffers/intra_process_buffer.hpp b/rclcpp/include/rclcpp/experimental/buffers/intra_process_buffer.hpp index 268c3f6649..779c99c24e 100644 --- a/rclcpp/include/rclcpp/experimental/buffers/intra_process_buffer.hpp +++ b/rclcpp/include/rclcpp/experimental/buffers/intra_process_buffer.hpp @@ -15,6 +15,7 @@ #ifndef RCLCPP__EXPERIMENTAL__BUFFERS__INTRA_PROCESS_BUFFER_HPP_ #define RCLCPP__EXPERIMENTAL__BUFFERS__INTRA_PROCESS_BUFFER_HPP_ +#include #include #include #include @@ -61,6 +62,11 @@ class IntraProcessBuffer : public IntraProcessBufferBase using MessageUniquePtr = std::unique_ptr; using MessageSharedPtr = std::shared_ptr; + using ForEachSharedFunc = std::function; + // Takes ownership by value: the callee must be able to move from it, and a + // buffer storing unique_ptr can only ever hand out an independent copy, never + // a reference to the ring buffer's own stored element. + using ForEachUniqueFunc = std::function; virtual void add_shared(MessageSharedPtr msg) = 0; virtual void add_unique(MessageUniquePtr msg) = 0; @@ -68,8 +74,8 @@ class IntraProcessBuffer : public IntraProcessBufferBase virtual MessageSharedPtr consume_shared() = 0; virtual MessageUniquePtr consume_unique() = 0; - virtual std::vector get_all_data_shared() = 0; - virtual std::vector get_all_data_unique() = 0; + virtual void for_each_shared(ForEachSharedFunc && f) = 0; + virtual void for_each_unique(ForEachUniqueFunc && f) = 0; }; template< @@ -82,6 +88,7 @@ class TypedIntraProcessBuffer : public IntraProcessBuffer; using MessageAllocTraits = allocator::AllocRebind; using MessageAlloc = typename MessageAllocTraits::allocator_type; using MessageUniquePtr = std::unique_ptr; @@ -133,14 +140,14 @@ class TypedIntraProcessBuffer : public IntraProcessBuffer(); } - std::vector get_all_data_shared() override + void for_each_shared(typename Buffer::ForEachSharedFunc && f) override { - return get_all_data_shared_impl(); + for_each_shared_impl(std::move(f)); } - std::vector get_all_data_unique() override + void for_each_unique(typename Buffer::ForEachUniqueFunc && f) override { - return get_all_data_unique_impl(); + for_each_unique_impl(std::move(f)); } bool has_data() const override @@ -260,67 +267,62 @@ class TypedIntraProcessBuffer : public IntraProcessBuffer - typename std::enable_if< - std::is_same::value, - std::vector - >::type - get_all_data_shared_impl() + typename std::enable_if::value>::type + for_each_shared_impl(typename Buffer::ForEachSharedFunc && f) { - return buffer_->get_all_data(); + buffer_->for_each(std::move(f)); } // MessageUniquePtr to MessageSharedPtr template - typename std::enable_if< - std::is_same::value, - std::vector - >::type - get_all_data_shared_impl() + typename std::enable_if::value>::type + for_each_shared_impl(typename Buffer::ForEachSharedFunc && f) { - std::vector result; - auto uni_ptr_vec = buffer_->get_all_data(); - result.reserve(uni_ptr_vec.size()); - for (MessageUniquePtr & uni_ptr : uni_ptr_vec) { - result.emplace_back(std::move(uni_ptr)); - } - return result; + // The buffer keeps ownership of each unique_ptr (needed for future transient-local + // replays), so an independent copy must be made for each shared_ptr view handed out. + buffer_->for_each( + [this, &f](const MessageUniquePtr & msg) { + auto ptr = MessageAllocTraits::allocate(*message_allocator_.get(), 1); + MessageAllocTraits::construct(*message_allocator_.get(), ptr, *msg); + MessageSharedPtr shared_msg(ptr, msg.get_deleter()); + f(shared_msg); + }); } // MessageSharedPtr to MessageUniquePtr template - typename std::enable_if< - std::is_same::value, - std::vector - >::type - get_all_data_unique_impl() + typename std::enable_if::value>::type + for_each_unique_impl(typename Buffer::ForEachUniqueFunc && f) { - std::vector result; - auto shared_ptr_vec = buffer_->get_all_data(); - result.reserve(shared_ptr_vec.size()); - for (MessageSharedPtr shared_msg : shared_ptr_vec) { - MessageUniquePtr unique_msg; - MessageDeleter * deleter = std::get_deleter(shared_msg); - auto ptr = MessageAllocTraits::allocate(*message_allocator_.get(), 1); - MessageAllocTraits::construct(*message_allocator_.get(), ptr, *shared_msg); - if (deleter) { - unique_msg = MessageUniquePtr(ptr, *deleter); - } else { - unique_msg = MessageUniquePtr(ptr); - } - result.push_back(std::move(unique_msg)); - } - return result; + buffer_->for_each( + [this, &f](const MessageSharedPtr & shared_msg) { + MessageUniquePtr unique_msg; + MessageDeleter * deleter = std::get_deleter(shared_msg); + auto ptr = MessageAllocTraits::allocate(*message_allocator_.get(), 1); + MessageAllocTraits::construct(*message_allocator_.get(), ptr, *shared_msg); + if (deleter) { + unique_msg = MessageUniquePtr(ptr, *deleter); + } else { + unique_msg = MessageUniquePtr(ptr); + } + f(std::move(unique_msg)); + }); } // MessageUniquePtr to MessageUniquePtr template - typename std::enable_if< - std::is_same::value, - std::vector - >::type - get_all_data_unique_impl() + typename std::enable_if::value>::type + for_each_unique_impl(typename Buffer::ForEachUniqueFunc && f) { - return buffer_->get_all_data(); + // The buffer keeps ownership of each unique_ptr (needed for future transient-local + // replays), so an independent copy must be made for each unique_ptr handed out. + buffer_->for_each( + [this, &f](const MessageUniquePtr & msg) { + auto ptr = MessageAllocTraits::allocate(*message_allocator_.get(), 1); + MessageAllocTraits::construct(*message_allocator_.get(), ptr, *msg); + MessageUniquePtr unique_msg(ptr, msg.get_deleter()); + f(std::move(unique_msg)); + }); } }; diff --git a/rclcpp/include/rclcpp/experimental/buffers/ring_buffer_implementation.hpp b/rclcpp/include/rclcpp/experimental/buffers/ring_buffer_implementation.hpp index 4867b080f0..7c7c436ad7 100644 --- a/rclcpp/include/rclcpp/experimental/buffers/ring_buffer_implementation.hpp +++ b/rclcpp/include/rclcpp/experimental/buffers/ring_buffer_implementation.hpp @@ -43,6 +43,8 @@ template class RingBufferImplementation : public BufferImplementationBase { public: + using ImplementationBase = BufferImplementationBase; + explicit RingBufferImplementation(size_t capacity) : capacity_(capacity), ring_buffer_(capacity), @@ -114,15 +116,16 @@ class RingBufferImplementation : public BufferImplementationBase return request; } - /// Get all the elements from the ring buffer + /// Iterates over all the elements in the ring buffer /** * This member function is thread-safe. * - * \return a vector containing all the elements from the ring buffer + * \param f a function that is called for every element in the ring buffer */ - std::vector get_all_data() override + void for_each(typename ImplementationBase::ForEachFunc && f) const override { - return get_all_data_impl(); + std::lock_guard lock(mutex_); + return for_each_(std::move(f)); } /// Get the next index value for the ring buffer @@ -236,73 +239,17 @@ class RingBufferImplementation : public BufferImplementationBase write_index_ = capacity_ - 1; } - /// Traits for checking if a type is std::unique_ptr - template - struct is_std_unique_ptr final : std::false_type {}; - template - struct is_std_unique_ptr> final : std::true_type - { - typedef T Ptr_type; - }; - - /// Get all the elements from the ring buffer + /// Iterates over all the elements in the ring buffer /** - * This member function is thread-safe. - * Two versions for the implementation of the function. - * One for buffer containing unique_ptr and the other for other types + * This member function is not thread-safe. * - * \return a vector containing all the elements from the ring buffer + * \param f a function that is called for every element in the ring buffer */ - template::value && - std::is_copy_constructible< - typename is_std_unique_ptr::Ptr_type - >::value, - void> * = nullptr> - std::vector get_all_data_impl() + void for_each_(typename ImplementationBase::ForEachFunc && f) const { - std::lock_guard lock(mutex_); - std::vector result_vtr; - result_vtr.reserve(size_); for (size_t id = 0; id < size_; ++id) { - const auto & elem(ring_buffer_[(read_index_ + id) % capacity_]); - if (elem != nullptr) { - result_vtr.emplace_back(new typename is_std_unique_ptr::Ptr_type( - *elem)); - } else { - result_vtr.emplace_back(nullptr); - } + f(ring_buffer_[(read_index_ + id) % capacity_]); } - return result_vtr; - } - - template::value, void> * = nullptr> - std::vector get_all_data_impl() - { - std::lock_guard lock(mutex_); - std::vector result_vtr; - result_vtr.reserve(size_); - for (size_t id = 0; id < size_; ++id) { - result_vtr.emplace_back(ring_buffer_[(read_index_ + id) % capacity_]); - } - return result_vtr; - } - - template::value && - !std::is_copy_constructible::value, void> * = nullptr> - std::vector get_all_data_impl() - { - throw std::logic_error("Underlined type results in invalid get_all_data_impl()"); - return {}; - } - - template::value && - !std::is_copy_constructible::Ptr_type>::value, - void> * = nullptr> - std::vector get_all_data_impl() - { - throw std::logic_error("Underlined type in unique_ptr results in invalid get_all_data_impl()"); - return {}; } const size_t capacity_; diff --git a/rclcpp/include/rclcpp/experimental/intra_process_manager.hpp b/rclcpp/include/rclcpp/experimental/intra_process_manager.hpp index a8eba4baf7..7589352cf3 100644 --- a/rclcpp/include/rclcpp/experimental/intra_process_manager.hpp +++ b/rclcpp/include/rclcpp/experimental/intra_process_manager.hpp @@ -247,26 +247,14 @@ class IntraProcessManager this->template add_shared_msg_to_buffers( msg, sub_ids.take_shared_subscriptions); - } else if (!sub_ids.take_ownership_subscriptions.empty() && // NOLINT - sub_ids.take_shared_subscriptions.size() <= 1) - { + } else if (!sub_ids.concatenated_take_ownership_subscriptions.empty()) { // There is at maximum 1 buffer that does not require ownership. // So this case is equivalent to all the buffers requiring ownership - - // Merge the two vector of ids into a unique one - std::vector concatenated_vector( - sub_ids.take_shared_subscriptions.begin(), sub_ids.take_shared_subscriptions.end()); - concatenated_vector.insert( - concatenated_vector.end(), - sub_ids.take_ownership_subscriptions.begin(), - sub_ids.take_ownership_subscriptions.end()); this->template add_owned_msg_to_buffers( std::move(message), - concatenated_vector, + sub_ids.concatenated_take_ownership_subscriptions, allocator); - } else if (!sub_ids.take_ownership_subscriptions.empty() && // NOLINT - sub_ids.take_shared_subscriptions.size() > 1) - { + } else { // Construct a new shared pointer from the message // for the buffers that do not require ownership auto shared_msg = std::allocate_shared(allocator, *message); @@ -385,6 +373,10 @@ class IntraProcessManager { std::vector take_shared_subscriptions; std::vector take_ownership_subscriptions; + // If there is at maximum 1 buffer that does not require ownership, + // this case is equivalent to all the buffers requiring ownership + // and this vector will contain all of the subscriptions. + std::vector concatenated_take_ownership_subscriptions; }; /// Hash function for rmw_gid_t to enable use in unordered_map @@ -488,20 +480,20 @@ class IntraProcessManager "subscription use different allocator types, which is not supported"); } if (use_take_shared_method) { - auto data_vec = buffer->get_all_data_shared(); - for (auto shared_data : data_vec) { - this->template add_shared_msg_to_buffer< - ROSMessageType, ROSMessageTypeAllocator, ROSMessageTypeDeleter, ROSMessageType>( - shared_data, sub_id); - } + buffer->for_each_shared( + [this, sub_id](const auto & shared_data) { + this->template add_shared_msg_to_buffer< + ROSMessageType, ROSMessageTypeAllocator, ROSMessageTypeDeleter, ROSMessageType>( + shared_data, sub_id); + }); } else { - auto data_vec = buffer->get_all_data_unique(); - for (auto & owned_data : data_vec) { - auto allocator = ROSMessageTypeAllocator(); - this->template add_owned_msg_to_buffer< - ROSMessageType, ROSMessageTypeAllocator, ROSMessageTypeDeleter, ROSMessageType>( - std::move(owned_data), sub_id, allocator); - } + buffer->for_each_unique( + [this, sub_id](auto owned_data) { + auto allocator = ROSMessageTypeAllocator(); + this->template add_owned_msg_to_buffer< + ROSMessageType, ROSMessageTypeAllocator, ROSMessageTypeDeleter, ROSMessageType>( + std::move(owned_data), sub_id, allocator); + }); } } diff --git a/rclcpp/src/rclcpp/intra_process_manager.cpp b/rclcpp/src/rclcpp/intra_process_manager.cpp index 2cfc5386d8..100dbba713 100644 --- a/rclcpp/src/rclcpp/intra_process_manager.cpp +++ b/rclcpp/src/rclcpp/intra_process_manager.cpp @@ -208,10 +208,37 @@ IntraProcessManager::insert_sub_id_for_pub( uint64_t pub_id, bool use_take_shared_method) { + auto & splitted_subs = pub_to_subs_[pub_id]; + auto & take_shared_subscriptions = splitted_subs.take_shared_subscriptions; + auto & take_ownership_subscriptions = splitted_subs.take_ownership_subscriptions; + auto & concatenated_take_ownership_subscriptions = + splitted_subs.concatenated_take_ownership_subscriptions; + if (use_take_shared_method) { - pub_to_subs_[pub_id].take_shared_subscriptions.push_back(sub_id); + take_shared_subscriptions.push_back(sub_id); } else { - pub_to_subs_[pub_id].take_ownership_subscriptions.push_back(sub_id); + take_ownership_subscriptions.push_back(sub_id); + } + + concatenated_take_ownership_subscriptions.clear(); + if (!take_ownership_subscriptions.empty() && + take_shared_subscriptions.size() <= 1) + { + // There is at maximum 1 buffer that does not require ownership. + // So this case is equivalent to all the buffers requiring ownership + + // Merge the two vector of ids into a unique one + concatenated_take_ownership_subscriptions.reserve( + take_ownership_subscriptions.size() + + take_shared_subscriptions.size()); + concatenated_take_ownership_subscriptions.insert( + concatenated_take_ownership_subscriptions.end(), + take_shared_subscriptions.begin(), + take_shared_subscriptions.end()); + concatenated_take_ownership_subscriptions.insert( + concatenated_take_ownership_subscriptions.end(), + take_ownership_subscriptions.begin(), + take_ownership_subscriptions.end()); } } diff --git a/rclcpp/test/rclcpp/test_intra_process_buffer.cpp b/rclcpp/test/rclcpp/test_intra_process_buffer.cpp index 1cd4d37bb6..fea6446ee9 100644 --- a/rclcpp/test/rclcpp/test_intra_process_buffer.cpp +++ b/rclcpp/test/rclcpp/test_intra_process_buffer.cpp @@ -199,7 +199,11 @@ TEST(TestIntraProcessBuffer, shared_buffer_consume) { intra_process_buffer.add_shared(original_shared_msg); intra_process_buffer.add_shared(original_shared_msg_2); - auto shared_data_vec = intra_process_buffer.get_all_data_shared(); + std::vector shared_data_vec; + intra_process_buffer.for_each_shared( + [&shared_data_vec](const SharedMessageT & msg) { + shared_data_vec.push_back(msg); + }); EXPECT_EQ(2L, shared_data_vec.size()); EXPECT_EQ(3L, original_shared_msg.use_count()); EXPECT_EQ(original_shared_msg.use_count(), shared_data_vec[0].use_count()); @@ -210,7 +214,11 @@ TEST(TestIntraProcessBuffer, shared_buffer_consume) { EXPECT_EQ(*original_shared_msg_2, *shared_data_vec[1]); EXPECT_EQ(original_message_pointer_2, reinterpret_cast(shared_data_vec[1].get())); - auto unique_data_vec = intra_process_buffer.get_all_data_unique(); + std::vector unique_data_vec; + intra_process_buffer.for_each_unique( + [&unique_data_vec](UniqueMessageT msg) { + unique_data_vec.push_back(std::move(msg)); + }); EXPECT_EQ(2L, unique_data_vec.size()); EXPECT_EQ(3L, original_shared_msg.use_count()); EXPECT_EQ(*original_shared_msg, *unique_data_vec[0]); @@ -275,7 +283,11 @@ TEST(TestIntraProcessBuffer, unique_buffer_consume) { intra_process_buffer.add_unique(std::move(original_unique_msg)); intra_process_buffer.add_unique(std::move(original_unique_msg_2)); - auto shared_data_vec = intra_process_buffer.get_all_data_shared(); + std::vector shared_data_vec; + intra_process_buffer.for_each_shared( + [&shared_data_vec](const SharedMessageT & msg) { + shared_data_vec.push_back(msg); + }); EXPECT_EQ(2L, shared_data_vec.size()); EXPECT_EQ(1L, shared_data_vec[0].use_count()); EXPECT_EQ(original_value, *shared_data_vec[0]); @@ -284,7 +296,11 @@ TEST(TestIntraProcessBuffer, unique_buffer_consume) { EXPECT_EQ(original_value_2, *shared_data_vec[1]); EXPECT_NE(original_message_pointer_2, reinterpret_cast(shared_data_vec[1].get())); - auto unique_data_vec = intra_process_buffer.get_all_data_unique(); + std::vector unique_data_vec; + intra_process_buffer.for_each_unique( + [&unique_data_vec](UniqueMessageT msg) { + unique_data_vec.push_back(std::move(msg)); + }); EXPECT_EQ(2L, unique_data_vec.size()); EXPECT_EQ(1L, shared_data_vec[0].use_count()); EXPECT_EQ(original_value, *unique_data_vec[0]); diff --git a/rclcpp/test/rclcpp/test_intra_process_manager.cpp b/rclcpp/test/rclcpp/test_intra_process_manager.cpp index 838b8298b8..41a92be210 100644 --- a/rclcpp/test/rclcpp/test_intra_process_manager.cpp +++ b/rclcpp/test/rclcpp/test_intra_process_manager.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -102,25 +103,22 @@ class IntraProcessBuffer : public IntraProcessBufferBase return num_msgs; } - std::vector get_all_data_shared() + void for_each_shared(std::function f) { if (shared_msg) { - return {shared_msg}; + f(shared_msg); } else if (unique_msg) { - return {std::make_shared(*unique_msg)}; + f(std::make_shared(*unique_msg)); } - return {}; } - std::vector get_all_data_unique() + void for_each_unique(std::function f) { - std::vector result; if (shared_msg) { - result.push_back(std::make_unique(*shared_msg)); + f(std::make_unique(*shared_msg)); } else if (unique_msg) { - result.push_back(std::make_unique(*unique_msg)); + f(std::make_unique(*unique_msg)); } - return result; } private: diff --git a/rclcpp/test/rclcpp/test_ring_buffer_implementation.cpp b/rclcpp/test/rclcpp/test_ring_buffer_implementation.cpp index 687c34428b..60c1fbed96 100644 --- a/rclcpp/test/rclcpp/test_ring_buffer_implementation.cpp +++ b/rclcpp/test/rclcpp/test_ring_buffer_implementation.cpp @@ -65,7 +65,11 @@ TEST(TestRingBufferImplementation, basic_usage) { rb.enqueue('d'); - const auto all_data_vec = rb.get_all_data(); + std::vector all_data_vec; + rb.for_each( + [&all_data_vec](const auto & data) { + all_data_vec.push_back(data); + }); EXPECT_EQ(2u, all_data_vec.size()); EXPECT_EQ('c', all_data_vec[0]); @@ -90,7 +94,7 @@ TEST(TestRingBufferImplementation, basic_usage) { /* * Basic usage with unique_ptr * - insert unique_ptr data and check that it has data - * - get all data + * - iterate over all data * - extract data * - overwrite old data writing over the buffer capacity */ @@ -114,13 +118,20 @@ TEST(TestRingBufferImplementation, basic_usage_unique_ptr) { EXPECT_EQ(true, rb.has_data()); EXPECT_EQ(true, rb.is_full()); - const auto all_data_vec = rb.get_all_data(); - - EXPECT_EQ(2u, all_data_vec.size()); - EXPECT_EQ('b', *all_data_vec[0]); - EXPECT_EQ('c', *all_data_vec[1]); - EXPECT_NE(original_b_pointer, reinterpret_cast(all_data_vec[0].get())); - EXPECT_NE(original_c_pointer, reinterpret_cast(all_data_vec[1].get())); + std::vector values; + std::vector pointers; + rb.for_each( + [&values, &pointers](const auto & data) { + values.push_back(*data); + pointers.push_back(reinterpret_cast(data.get())); + }); + + EXPECT_EQ(2u, values.size()); + EXPECT_EQ('b', values[0]); + EXPECT_EQ('c', values[1]); + // for_each does not copy the elements, it observes the buffer's own storage in place. + EXPECT_EQ(original_b_pointer, pointers[0]); + EXPECT_EQ(original_c_pointer, pointers[1]); EXPECT_EQ(true, rb.has_data()); EXPECT_EQ(true, rb.is_full()); @@ -147,14 +158,14 @@ TEST(TestRingBufferImplementation, test_buffer_clear) { EXPECT_EQ(true, rb.has_data()); EXPECT_EQ(true, rb.is_full()); - const auto all_data_vec = rb.get_all_data(); - EXPECT_EQ(2u, all_data_vec.capacity()); + std::vector all_data_vec; + rb.for_each([&all_data_vec](const auto & data) {all_data_vec.push_back(data);}); EXPECT_EQ(2u, all_data_vec.size()); rb.clear(); EXPECT_EQ(false, rb.has_data()); EXPECT_EQ(false, rb.is_full()); - const auto all_data_vec_empty = rb.get_all_data(); - EXPECT_EQ(0u, all_data_vec_empty.capacity()); + std::vector all_data_vec_empty; + rb.for_each([&all_data_vec_empty](const auto & data) {all_data_vec_empty.push_back(data);}); EXPECT_EQ(0u, all_data_vec_empty.size()); rb.enqueue('c'); rb.enqueue('d'); @@ -206,9 +217,19 @@ TEST(TestRingBufferImplementation, handle_nullptr_deletion) { rb.enqueue(std::make_unique(42)); rb.enqueue(nullptr); // intentionally enqueuing nullptr rb.enqueue(std::make_unique(84)); - auto all_data = rb.get_all_data(); - EXPECT_EQ(3u, all_data.size()); - EXPECT_EQ(42, *(all_data[0])); - EXPECT_EQ(nullptr, all_data[1]); - EXPECT_EQ(84, *(all_data[2])); + + std::vector is_null; + std::vector values; + rb.for_each( + [&is_null, &values](const auto & data) { + is_null.push_back(data == nullptr); + values.push_back(data ? *data : 0); + }); + + ASSERT_EQ(3u, is_null.size()); + EXPECT_FALSE(is_null[0]); + EXPECT_EQ(42, values[0]); + EXPECT_TRUE(is_null[1]); + EXPECT_FALSE(is_null[2]); + EXPECT_EQ(84, values[2]); }