mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'heap_scheduler_stress_component' into integration
This commit is contained in:
		| @@ -68,7 +68,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type | |||||||
|     // Still need to cancel existing timer if name is not empty |     // Still need to cancel existing timer if name is not empty | ||||||
|     if (name_cstr != nullptr && name_cstr[0] != '\0') { |     if (name_cstr != nullptr && name_cstr[0] != '\0') { | ||||||
|       LockGuard guard{this->lock_}; |       LockGuard guard{this->lock_}; | ||||||
|       this->cancel_item_locked_(component, name_cstr, type, delay == 0 && type == SchedulerItem::TIMEOUT); |       this->cancel_item_locked_(component, name_cstr, type, false); | ||||||
|     } |     } | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @@ -242,6 +242,10 @@ void HOT Scheduler::call() { | |||||||
|   // - No deferred items exist in to_add_, so processing order doesn't affect correctness |   // - No deferred items exist in to_add_, so processing order doesn't affect correctness | ||||||
|   // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach |   // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach | ||||||
|   // (ESP8266: single-core, RP2040: empty mutex implementation). |   // (ESP8266: single-core, RP2040: empty mutex implementation). | ||||||
|  |   // | ||||||
|  |   // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still | ||||||
|  |   // processed here. They are removed from the queue normally via pop_front() but skipped | ||||||
|  |   // during execution by should_skip_item_(). This is intentional - no memory leak occurs. | ||||||
|   while (!this->defer_queue_.empty()) { |   while (!this->defer_queue_.empty()) { | ||||||
|     // The outer check is done without a lock for performance. If the queue |     // The outer check is done without a lock for performance. If the queue | ||||||
|     // appears non-empty, we lock and process an item. We don't need to check |     // appears non-empty, we lock and process an item. We don't need to check | ||||||
| @@ -273,10 +277,12 @@ void HOT Scheduler::call() { | |||||||
|     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, |     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, | ||||||
|              this->last_millis_); |              this->last_millis_); | ||||||
|     while (!this->empty_()) { |     while (!this->empty_()) { | ||||||
|       this->lock_.lock(); |       std::unique_ptr<SchedulerItem> item; | ||||||
|       auto item = std::move(this->items_[0]); |       { | ||||||
|  |         LockGuard guard{this->lock_}; | ||||||
|  |         item = std::move(this->items_[0]); | ||||||
|         this->pop_raw_(); |         this->pop_raw_(); | ||||||
|       this->lock_.unlock(); |       } | ||||||
|  |  | ||||||
|       const char *name = item->get_name(); |       const char *name = item->get_name(); | ||||||
|       ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, |       ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, | ||||||
| @@ -290,6 +296,8 @@ void HOT Scheduler::call() { | |||||||
|     { |     { | ||||||
|       LockGuard guard{this->lock_}; |       LockGuard guard{this->lock_}; | ||||||
|       this->items_ = std::move(old_items); |       this->items_ = std::move(old_items); | ||||||
|  |       // Rebuild heap after moving items back | ||||||
|  |       std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| #endif  // ESPHOME_DEBUG_SCHEDULER | #endif  // ESPHOME_DEBUG_SCHEDULER | ||||||
| @@ -314,6 +322,8 @@ void HOT Scheduler::call() { | |||||||
|  |  | ||||||
|     // Replace items_ with the filtered list |     // Replace items_ with the filtered list | ||||||
|     this->items_ = std::move(valid_items); |     this->items_ = std::move(valid_items); | ||||||
|  |     // Rebuild the heap structure since items are no longer in heap order | ||||||
|  |     std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); | ||||||
|     this->to_remove_ = 0; |     this->to_remove_ = 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -441,7 +451,7 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co | |||||||
|  |  | ||||||
| // Helper to cancel items by name - must be called with lock held | // Helper to cancel items by name - must be called with lock held | ||||||
| bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type, | bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type, | ||||||
|                                         bool defer_only) { |                                         bool check_defer_only) { | ||||||
|   size_t total_cancelled = 0; |   size_t total_cancelled = 0; | ||||||
|  |  | ||||||
|   // Check all containers for matching items |   // Check all containers for matching items | ||||||
| @@ -454,7 +464,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c | |||||||
|         total_cancelled++; |         total_cancelled++; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     if (defer_only) { |     if (check_defer_only) { | ||||||
|       return total_cancelled > 0; |       return total_cancelled > 0; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -99,13 +99,7 @@ class Scheduler { | |||||||
|     SchedulerItem(const SchedulerItem &) = delete; |     SchedulerItem(const SchedulerItem &) = delete; | ||||||
|     SchedulerItem &operator=(const SchedulerItem &) = delete; |     SchedulerItem &operator=(const SchedulerItem &) = delete; | ||||||
|  |  | ||||||
|     // Delete move operations to prevent accidental moves of SchedulerItem objects. |     // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly | ||||||
|     // This is intentional because: |  | ||||||
|     // 1. SchedulerItem contains a dynamically allocated name that requires careful ownership management |  | ||||||
|     // 2. The scheduler only moves unique_ptr<SchedulerItem>, never SchedulerItem objects directly |  | ||||||
|     // 3. Moving unique_ptr only transfers pointer ownership without moving the pointed-to object |  | ||||||
|     // 4. Deleting these operations makes it explicit that SchedulerItem objects should not be moved |  | ||||||
|     // 5. This prevents potential double-free bugs if the code is refactored to move SchedulerItem objects |  | ||||||
|     SchedulerItem(SchedulerItem &&) = delete; |     SchedulerItem(SchedulerItem &&) = delete; | ||||||
|     SchedulerItem &operator=(SchedulerItem &&) = delete; |     SchedulerItem &operator=(SchedulerItem &&) = delete; | ||||||
|  |  | ||||||
| @@ -149,7 +143,7 @@ class Scheduler { | |||||||
|  |  | ||||||
|  private: |  private: | ||||||
|   // Helper to cancel items by name - must be called with lock held |   // Helper to cancel items by name - must be called with lock held | ||||||
|   bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type, bool defer_only); |   bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type, bool check_defer_only); | ||||||
|  |  | ||||||
|   // Helper to extract name as const char* from either static string or std::string |   // Helper to extract name as const char* from either static string or std::string | ||||||
|   inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) { |   inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) { | ||||||
|   | |||||||
| @@ -36,26 +36,35 @@ void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { | |||||||
|   // At this point we have 25 items marked for removal |   // At this point we have 25 items marked for removal | ||||||
|   // The next scheduler.call() should trigger the bulk cleanup path |   // The next scheduler.call() should trigger the bulk cleanup path | ||||||
|  |  | ||||||
|   // Schedule an interval that will execute multiple times to ensure cleanup happens |   // The bulk cleanup should happen on the next scheduler.call() after cancelling items | ||||||
|  |   // Log that we expect bulk cleanup to be triggered | ||||||
|  |   ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25); | ||||||
|  |   ESP_LOGI(TAG, "Items before cleanup: 25+, after: <unknown>"); | ||||||
|  |  | ||||||
|  |   // Schedule an interval that will execute multiple times to verify scheduler still works | ||||||
|   static int cleanup_check_count = 0; |   static int cleanup_check_count = 0; | ||||||
|   App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() { |   App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() { | ||||||
|     cleanup_check_count++; |     cleanup_check_count++; | ||||||
|     ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count); |     ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count); | ||||||
|  |  | ||||||
|     if (cleanup_check_count >= 5) { |     if (cleanup_check_count >= 5) { | ||||||
|       // Cancel the interval and complete the test |       // Cancel the interval | ||||||
|       App.scheduler.cancel_interval(this, "cleanup_checker"); |       App.scheduler.cancel_interval(this, "cleanup_checker"); | ||||||
|       ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25); |       ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup"); | ||||||
|       ESP_LOGI(TAG, "Items before cleanup: 25+, after: <unknown>"); |  | ||||||
|       ESP_LOGI(TAG, "Bulk cleanup test complete"); |  | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup |   // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup | ||||||
|  |   static int post_cleanup_count = 0; | ||||||
|   for (int i = 0; i < 5; i++) { |   for (int i = 0; i < 5; i++) { | ||||||
|     std::string name = "post_cleanup_" + std::to_string(i); |     std::string name = "post_cleanup_" + std::to_string(i); | ||||||
|     App.scheduler.set_timeout(this, name, 50 + i * 25, |     App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() { | ||||||
|                               [i]() { ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i); }); |       ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i); | ||||||
|  |       post_cleanup_count++; | ||||||
|  |       if (post_cleanup_count >= 5) { | ||||||
|  |         ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished"); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -64,14 +64,13 @@ async def test_scheduler_bulk_cleanup( | |||||||
|             match = re.search(r"Post-cleanup timeout (\d+) executed correctly", line) |             match = re.search(r"Post-cleanup timeout (\d+) executed correctly", line) | ||||||
|             if match: |             if match: | ||||||
|                 post_cleanup_executed += 1 |                 post_cleanup_executed += 1 | ||||||
|                 # All 5 post-cleanup timeouts have executed |  | ||||||
|                 if post_cleanup_executed >= 5 and not test_complete_future.done(): |  | ||||||
|                     test_complete_future.set_result(None) |  | ||||||
|  |  | ||||||
|         # Check for bulk cleanup completion (but don't end test yet) |         # Check for final test completion | ||||||
|         if "Bulk cleanup test complete" in line: |         if ( | ||||||
|             # This just means the interval finished, not that all timeouts executed |             "All post-cleanup timeouts completed - test finished" in line | ||||||
|             pass |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|     async with ( |     async with ( | ||||||
|         run_compiled(yaml_config, line_callback=on_log_line), |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user