Merge branch 'scheduler-cancel-fast-path' into integration

This commit is contained in:
J. Nick Koston
2026-04-02 12:23:15 -10:00
15 changed files with 271 additions and 114 deletions
+2 -2
View File
@@ -177,7 +177,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
template_ = int(template_ / 1000000)
cg.add(var.set_frequency(template_))
if sens_dist := config.get(CONF_SENSING_DISTANCE):
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
template_ = await cg.templatable(sens_dist, args, int)
cg.add(var.set_sensing_distance(template_))
@@ -209,7 +209,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
template_ = int(template_)
cg.add(var.set_trigger_keep(template_))
if stage_gain := config.get(CONF_STAGE_GAIN):
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
template_ = await cg.templatable(stage_gain, args, int)
cg.add(var.set_stage_gain(template_))
+1 -1
View File
@@ -161,7 +161,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_CANBUS_ID])
if can_id := config.get(CONF_CAN_ID):
if (can_id := config.get(CONF_CAN_ID)) is not None:
can_id = await cg.templatable(can_id, args, cg.uint32)
cg.add(var.set_can_id(can_id))
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
+32 -21
View File
@@ -150,10 +150,12 @@ void Nextion::reset_(bool reset_nextion) {
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
this->nextion_queue_.clear();
#ifdef USE_NEXTION_WAVEFORM
for (auto *entry : this->waveform_queue_) {
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
this->waveform_queue_.clear();
#endif // USE_NEXTION_WAVEFORM
}
void Nextion::dump_config() {
@@ -496,20 +498,21 @@ void Nextion::process_nextion_commands_() {
ESP_LOGW(TAG, "Invalid baud rate");
break;
case 0x12: // invalid Waveform ID or Channel # was used
#ifdef USE_NEXTION_WAVEFORM
if (this->waveform_queue_.empty()) {
ESP_LOGW(TAG, "Waveform ID/ch used but no sensor queued");
} else {
auto &nb = this->waveform_queue_.front();
NextionComponentBase *component = nb->component;
ESP_LOGW(TAG, "Invalid waveform ID %d/ch %d", component->get_component_id(),
component->get_wave_channel_id());
ESP_LOGN(TAG, "Remove waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id());
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
this->waveform_queue_.pop();
}
#else // USE_NEXTION_WAVEFORM
ESP_LOGW(TAG, "Waveform ID/ch error but waveform not enabled");
#endif // USE_NEXTION_WAVEFORM
break;
case 0x1A: // variable name invalid
ESP_LOGW(TAG, "Invalid variable name");
@@ -812,29 +815,30 @@ void Nextion::process_nextion_commands_() {
}
case 0xFD: { // data transparent transmit finished
ESP_LOGVV(TAG, "Data transmit done");
#ifdef USE_NEXTION_WAVEFORM
this->check_pending_waveform_();
#endif // USE_NEXTION_WAVEFORM
break;
}
case 0xFE: { // data transparent transmit ready
ESP_LOGVV(TAG, "Ready for transmit");
#ifdef USE_NEXTION_WAVEFORM
if (this->waveform_queue_.empty()) {
ESP_LOGE(TAG, "No waveforms queued");
break;
}
auto &nb = this->waveform_queue_.front();
auto *component = nb->component;
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
: 255; // ADDT command can only send 255
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255;
this->write_array(component->get_wave_buffer().data(), static_cast<int>(buffer_to_send));
ESP_LOGN(TAG, "Send waveform: component id %d, waveform id %d, size %zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
component->clear_wave_buffer(buffer_to_send);
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
this->waveform_queue_.pop();
#else // USE_NEXTION_WAVEFORM
ESP_LOGW(TAG, "Waveform transmit ready but waveform not enabled");
#endif // USE_NEXTION_WAVEFORM
break;
}
default:
@@ -934,8 +938,13 @@ void Nextion::all_components_send_state_(bool force_update) {
binarysensortype->send_state_to_nextion();
}
for (auto *sensortype : this->sensortype_) {
if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == 0)
#ifdef USE_NEXTION_WAVEFORM
if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == UINT8_MAX) {
#else // USE_NEXTION_WAVEFORM
if (force_update || sensortype->get_needs_to_send_update()) {
#endif // USE_NEXTION_WAVEFORM
sensortype->send_state_to_nextion();
}
}
for (auto *switchtype : this->switchtype_) {
if (force_update || switchtype->get_needs_to_send_update())
@@ -1239,13 +1248,11 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
}
}
#ifdef USE_NEXTION_WAVEFORM
/**
* @brief Add addt command to the queue
* @brief Add addt command to the waveform queue.
*
* @param component_id The waveform component id
* @param wave_chan_id The waveform channel to send it to
* @param buffer_to_send The buffer size
* @param buffer_size The buffer data
* @param component Pointer to the Nextion component with waveform data to send.
*/
void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
@@ -1262,7 +1269,11 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
nextion_queue->component = component;
nextion_queue->queue_time = App.get_loop_component_start_time();
this->waveform_queue_.push_back(nextion_queue);
if (!this->waveform_queue_.push(nextion_queue)) {
ESP_LOGW(TAG, "Waveform queue full, drop");
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
return;
}
if (this->waveform_queue_.size() == 1)
this->check_pending_waveform_();
}
@@ -1273,17 +1284,17 @@ void Nextion::check_pending_waveform_() {
auto *nb = this->waveform_queue_.front();
auto *component = nb->component;
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
: 255; // ADDT command can only send 255
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255;
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
if (!this->send_command_(command)) {
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
this->waveform_queue_.pop();
}
}
#endif // USE_NEXTION_WAVEFORM
void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; }
+15 -1
View File
@@ -9,6 +9,10 @@
#include "esphome/core/defines.h"
#include "esphome/core/time.h"
#ifdef USE_NEXTION_WAVEFORM
#include "esphome/core/helpers.h"
#endif // USE_NEXTION_WAVEFORM
#include "nextion_base.h"
#include "nextion_component.h"
@@ -602,6 +606,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void disable_component_touch(const char *component);
#ifdef USE_NEXTION_WAVEFORM
/**
* Add waveform data to a waveform component
* @param component_id The integer component id.
@@ -611,6 +616,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value);
void open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value);
#endif // USE_NEXTION_WAVEFORM
/**
* Display a picture at coordinates.
@@ -1205,7 +1211,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void add_to_get_queue(NextionComponentBase *component) override;
#ifdef USE_NEXTION_WAVEFORM
void add_addt_command_to_queue(NextionComponentBase *component) override;
#endif // USE_NEXTION_WAVEFORM
void update_components_by_prefix(const std::string &prefix);
@@ -1391,7 +1399,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
#endif // USE_NEXTION_COMMAND_SPACING
std::list<NextionQueue *> nextion_queue_;
std::list<NextionQueue *> waveform_queue_;
#ifdef USE_NEXTION_WAVEFORM
/// Fixed-size ring buffer for waveform queue. Nextion supports at most 4 waveform
/// channels (IDs 0-3), so 4 entries is both the correct maximum and a safe default.
StaticRingBuffer<NextionQueue *, 4> waveform_queue_;
#endif // USE_NEXTION_WAVEFORM
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag);
void all_components_send_state_(bool force_update = false);
uint32_t comok_sent_ = 0;
@@ -1460,7 +1472,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
const std::string &variable_name_to_send,
const std::string &state_value, bool is_sleep_safe = false);
#ifdef USE_NEXTION_WAVEFORM
void check_pending_waveform_();
#endif // USE_NEXTION_WAVEFORM
#ifdef USE_NEXTION_TFT_UPLOAD
#ifdef USE_ESP8266
@@ -33,7 +33,9 @@ class NextionBase {
const std::string &variable_name_to_send,
const std::string &state_value) = 0;
#ifdef USE_NEXTION_WAVEFORM
virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0;
#endif // USE_NEXTION_WAVEFORM
virtual void add_to_get_queue(NextionComponentBase *component) = 0;
@@ -217,6 +217,7 @@ void Nextion::set_component_value(const char *component, int32_t value) {
this->add_no_result_to_queue_with_printf_(".val", "%s.val=%" PRId32, component, value);
}
#ifdef USE_NEXTION_WAVEFORM
void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) {
this->add_no_result_to_queue_with_printf_("add", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number,
value);
@@ -226,6 +227,7 @@ void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number
this->add_no_result_to_queue_with_printf_("addt", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number,
value);
}
#endif // USE_NEXTION_WAVEFORM
void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) {
this->add_no_result_to_queue_with_printf_(".xcen", "%s.xcen=%" PRIu16, component, x);
@@ -64,6 +64,7 @@ class NextionComponentBase {
uint8_t get_component_id() const { return this->component_id_; }
void set_component_id(uint8_t component_id) { this->component_id_ = component_id; }
#ifdef USE_NEXTION_WAVEFORM
uint8_t get_wave_channel_id() const { return this->wave_chan_id_; }
void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; }
@@ -76,6 +77,7 @@ class NextionComponentBase {
this->wave_buffer_.erase(this->wave_buffer_.begin(), this->wave_buffer_.begin() + buffer_sent);
}
}
#endif // USE_NEXTION_WAVEFORM
const std::string &get_variable_name() const { return this->variable_name_; }
const std::string &get_variable_name_to_send() const { return this->variable_name_to_send_; }
@@ -85,19 +87,23 @@ class NextionComponentBase {
virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){};
virtual void send_state_to_nextion(){};
bool get_needs_to_send_update() const { return this->needs_to_send_update_; }
#ifdef USE_NEXTION_WAVEFORM
// Remove before 2026.10.0
ESPDEPRECATED("Use get_wave_channel_id() instead. Will be removed in 2026.10.0", "2026.4.0")
uint8_t get_wave_chan_id() const { return this->get_wave_channel_id(); }
void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; }
#endif // USE_NEXTION_WAVEFORM
protected:
std::string variable_name_;
std::string variable_name_to_send_;
uint8_t component_id_ = 0;
#ifdef USE_NEXTION_WAVEFORM
uint8_t wave_chan_id_ = UINT8_MAX;
std::vector<uint8_t> wave_buffer_;
int wave_max_length_ = 255;
#endif // USE_NEXTION_WAVEFORM
bool needs_to_send_update_;
};
@@ -85,16 +85,16 @@ async def to_code(config):
cg.add(var.set_component_id(config[CONF_COMPONENT_ID]))
if CONF_WAVE_CHANNEL_ID in config:
cg.add_define("USE_NEXTION_WAVEFORM")
cg.add(var.set_wave_channel_id(config[CONF_WAVE_CHANNEL_ID]))
if CONF_WAVEFORM_SEND_LAST_VALUE in config:
cg.add(var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE]))
if CONF_WAVE_MAX_VALUE in config:
cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE]))
if CONF_WAVE_MAX_LENGTH in config:
cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH]))
if CONF_WAVEFORM_SEND_LAST_VALUE in config:
cg.add(
var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE])
)
if CONF_WAVE_MAX_VALUE in config:
cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE]))
if CONF_WAVE_MAX_LENGTH in config:
cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH]))
@automation.register_action(
@@ -10,37 +10,44 @@ void NextionSensor::process_sensor(const std::string &variable_name, int state)
if (!this->nextion_->is_setup())
return;
if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) {
#ifdef USE_NEXTION_WAVEFORM
if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name)
#else // USE_NEXTION_WAVEFORM
if (this->variable_name_ == variable_name)
#endif // USE_NEXTION_WAVEFORM
{
this->publish_state(state);
ESP_LOGD(TAG, "Sensor: %s=%d", variable_name.c_str(), state);
}
}
#ifdef USE_NEXTION_WAVEFORM
void NextionSensor::add_to_wave_buffer(float state) {
this->needs_to_send_update_ = true;
int wave_state = (int) ((state / (float) this->wave_maxvalue_) * 100);
wave_buffer_.push_back(wave_state);
this->wave_buffer_.push_back(wave_state);
if (this->wave_buffer_.size() > (size_t) this->wave_max_length_) {
this->wave_buffer_.erase(this->wave_buffer_.begin());
}
}
#endif // USE_NEXTION_WAVEFORM
void NextionSensor::update() {
if (!this->nextion_->is_setup() || this->nextion_->is_updating())
return;
#ifdef USE_NEXTION_WAVEFORM
if (this->wave_chan_id_ == UINT8_MAX) {
this->nextion_->add_to_get_queue(this);
} else {
if (this->send_last_value_) {
this->add_to_wave_buffer(this->last_value_);
}
this->wave_update_();
}
#else // USE_NEXTION_WAVEFORM
this->nextion_->add_to_get_queue(this);
#endif // USE_NEXTION_WAVEFORM
}
void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
@@ -50,61 +57,60 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
if (std::isnan(state))
return;
if (this->wave_chan_id_ == UINT8_MAX) {
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->needs_to_send_update_ = false;
if (this->precision_ > 0) {
double to_multiply = pow(10, this->precision_);
int state_value = (int) (state * to_multiply);
this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value);
} else {
this->nextion_->add_no_result_to_queue_with_set(this, (int) state);
}
}
}
} else {
#ifdef USE_NEXTION_WAVEFORM
if (this->wave_chan_id_ != UINT8_MAX) {
// Waveform sensor — buffer the value, don't send directly.
if (this->send_last_value_) {
this->last_value_ = state; // Update will handle setting the buffer
} else {
this->add_to_wave_buffer(state);
}
this->update_component_settings();
return;
}
#endif // USE_NEXTION_WAVEFORM
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->needs_to_send_update_ = false;
if (this->precision_ > 0) {
double to_multiply = pow(10, this->precision_);
int state_value = (int) (state * to_multiply);
this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value);
} else {
this->nextion_->add_no_result_to_queue_with_set(this, (int) state);
}
}
}
float published_state = state;
if (this->wave_chan_id_ == UINT8_MAX) {
if (publish) {
if (this->precision_ > 0) {
double to_multiply = pow(10, -this->precision_);
published_state = (float) (state * to_multiply);
}
this->publish_state(published_state);
if (publish) {
if (this->precision_ > 0) {
double to_multiply = pow(10, -this->precision_);
published_state = (float) (state * to_multiply);
}
this->publish_state(published_state);
}
this->update_component_settings();
ESP_LOGN(TAG, "Write: %s=%lf", this->variable_name_.c_str(), published_state);
}
#ifdef USE_NEXTION_WAVEFORM
void NextionSensor::wave_update_() {
if (this->nextion_->is_sleeping() || this->wave_buffer_.empty()) {
return;
}
#ifdef NEXTION_PROTOCOL_LOG
size_t buffer_to_send =
this->wave_buffer_.size() < 255 ? this->wave_buffer_.size() : 255; // ADDT command can only send 255
ESP_LOGN(TAG, "Wave update: %zu/%zu vals to comp %d ch %d", buffer_to_send, this->wave_buffer_.size(),
this->component_id_, this->wave_chan_id_);
#endif
#endif // NEXTION_PROTOCOL_LOG
this->nextion_->add_addt_command_to_queue(this);
}
#endif // USE_NEXTION_WAVEFORM
} // namespace esphome::nextion
@@ -15,22 +15,30 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol
void update_component() override { this->update(); }
void update() override;
void add_to_wave_buffer(float state);
void set_precision(uint8_t precision) { this->precision_ = precision; }
void set_component_id(uint8_t component_id) { this->component_id_ = component_id; }
void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; }
void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; }
void process_sensor(const std::string &variable_name, int state) override;
void set_state(float state) override { this->set_state(state, true, true); }
void set_state(float state, bool publish) override { this->set_state(state, publish, true); }
void set_state(float state, bool publish, bool send_to_nextion) override;
NextionQueueType get_queue_type() const override {
#ifdef USE_NEXTION_WAVEFORM
return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR;
#else // USE_NEXTION_WAVEFORM
return NextionQueueType::SENSOR;
#endif // USE_NEXTION_WAVEFORM
}
#ifdef USE_NEXTION_WAVEFORM
void add_to_wave_buffer(float state);
void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; }
void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; }
void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; }
void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; }
NextionQueueType get_queue_type() const override {
return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR;
}
#endif // USE_NEXTION_WAVEFORM
void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {}
void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override {
this->set_state(state_value, publish, send_to_nextion);
@@ -38,10 +46,11 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol
protected:
uint8_t precision_ = 0;
#ifdef USE_NEXTION_WAVEFORM
uint32_t wave_maxvalue_ = 255;
float last_value_ = 0;
bool send_last_value_ = true;
void wave_update_();
#endif // USE_NEXTION_WAVEFORM
};
} // namespace esphome::nextion
-2
View File
@@ -25,7 +25,6 @@ from esphome.const import (
CONF_TEMPERATURE_COMPENSATION,
CONF_TIME_CONSTANT,
CONF_VOC,
CONF_VOC_BASELINE,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
@@ -165,7 +164,6 @@ CONFIG_SCHEMA = (
gain_factor=230,
),
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
+1
View File
@@ -123,6 +123,7 @@
#define USE_NEXTION_MAX_COMMANDS_PER_LOOP
#define USE_NEXTION_MAX_QUEUE_SIZE
#define USE_NEXTION_TFT_UPLOAD
#define USE_NEXTION_WAVEFORM
#define USE_NUMBER
#define USE_OUTPUT
#define USE_POWER_SUPPLY
+21 -3
View File
@@ -214,8 +214,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
#endif /* ESPHOME_DEBUG_SCHEDULER */
}
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true)
if (!skip_cancel) {
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true or anonymous)
// Anonymous items (STATIC_STRING with nullptr) can never match anything, so skip the scan.
if (!skip_cancel && (name_type != NameType::STATIC_STRING || static_name != nullptr)) {
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false,
/* find_first= */ true);
}
@@ -742,6 +743,23 @@ bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const
// When find_first=false, cancels ALL matches across all containers (needed for
// public cancel path where DelayAction parallel mode can create duplicates).
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
size_t Scheduler::mark_matching_items_removed_slow_locked_(std::vector<SchedulerItem *> &container,
Component *component, NameType name_type,
const char *static_name, uint32_t hash_or_id,
SchedulerItem::Type type, bool match_retry,
bool find_first) {
size_t count = 0;
for (auto *item : container) {
if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) {
this->set_item_removed_(item, true);
if (find_first)
return 1;
count++;
}
}
return count;
}
bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry,
bool find_first) {
@@ -767,7 +785,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
// The main loop may be executing an item's callback right now, and recycling
// would destroy the callback while it's running (use-after-free).
// Only the main loop in call() should recycle items after execution completes.
if (!this->items_.empty()) {
{
size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name,
hash_or_id, type, match_retry, find_first);
total_cancelled += heap_cancelled;
+15 -15
View File
@@ -495,23 +495,23 @@ class Scheduler {
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
// Returns the number of items marked for removal.
// IMPORTANT: Must be called with scheduler lock held
__attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container,
Component *component, NameType name_type,
const char *static_name, uint32_t hash_or_id,
SchedulerItem::Type type, bool match_retry,
bool find_first = false) {
size_t count = 0;
for (auto *item : container) {
if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) {
this->set_item_removed_(item, true);
if (find_first)
return 1;
count++;
}
}
return count;
// Inlined: the fast path (empty container) avoids calling the out-of-line scan.
inline size_t HOT mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, Component *component,
NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry,
bool find_first = false) {
if (container.empty())
return 0;
return this->mark_matching_items_removed_slow_locked_(container, component, name_type, static_name, hash_or_id,
type, match_retry, find_first);
}
// Out-of-line slow path for mark_matching_items_removed_locked_ when container is non-empty.
// IMPORTANT: Must be called with scheduler lock held
__attribute__((noinline)) size_t mark_matching_items_removed_slow_locked_(
std::vector<SchedulerItem *> &container, Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first);
Mutex lock_;
std::vector<SchedulerItem *> items_;
std::vector<SchedulerItem *> to_add_;
+107 -17
View File
@@ -8,7 +8,24 @@ namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
static constexpr int kInnerIterations = 2000;
// Must be divisible by all batch sizes used below (3, 10) to avoid
// pool imbalance at iteration boundaries that causes spurious malloc.
static constexpr int kInnerIterations = 2100;
// Warm the scheduler pool by registering and replacing items twice.
// The first batch allocates fresh items; the second batch cancels them and
// populates the recycling pool with the cancelled items from the first batch.
static void warm_pool(Scheduler &scheduler, Component *component, int batch_size, uint32_t delay) {
uint32_t now = millis();
for (int i = 0; i < batch_size; i++) {
scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {});
}
scheduler.call(++now);
for (int i = 0; i < batch_size; i++) {
scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {});
}
scheduler.call(++now);
}
// --- Scheduler fast path: no work to do ---
@@ -83,11 +100,21 @@ static void Scheduler_SetTimeout(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Register 3 timeouts then call() — realistic worst case where multiple
// components schedule in the same loop iteration. Keeps item count within
// the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 1000, []() {});
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.process_to_add();
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
@@ -99,22 +126,22 @@ BENCHMARK(Scheduler_SetTimeout);
static void Scheduler_SetInterval(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Number of distinct interval keys; controls how many unique timers exist
// simultaneously and the drain cadence for process_to_add().
static constexpr int kKeyCount = 5;
// Register 3 intervals then call() — realistic worst case where multiple
// components schedule in the same loop iteration. Keeps item count within
// the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kKeyCount), 1000, []() {});
// Drain to_add_ periodically to reflect production behavior where
// process_to_add() runs each main loop iteration. Without this,
// cancelled items accumulate in to_add_ causing O(n²) scan cost.
if ((i + 1) % kKeyCount == 0) {
scheduler.process_to_add();
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
// Final drain in case kInnerIterations is not a multiple of 5
scheduler.process_to_add();
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
@@ -128,16 +155,79 @@ static void Scheduler_Defer(benchmark::State &state) {
Component dummy_component;
// defer() is Component::defer which calls set_timeout(delay=0).
// Call set_timeout directly since defer() is protected.
// Component::defer(func) passes nullptr as the name, which skips
// cancel_item_locked_ entirely — matching production behavior where
// defers are anonymous fire-and-forget callbacks.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 0);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 0, []() {});
scheduler.set_timeout(&dummy_component, static_cast<const char *>(nullptr), 0, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.process_to_add();
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_Defer);
// --- Scheduler: defer with same ID (cancel-and-replace pattern) ---
static void Scheduler_Defer_SameID(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Measures defer with a fixed numeric ID — each call cancels the previous
// pending defer before adding the new one. This is the pattern used by
// components that defer work but want to coalesce rapid updates.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 0);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(0), 0, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_Defer_SameID);
// --- Scheduler: set_timeout with batch size exceeding pool (cliff test) ---
static void Scheduler_SetTimeout_ExceedPool(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Register 10 timeouts then call() — exceeds MAX_POOL_SIZE=5 to measure
// the performance cliff when the recycling pool is exhausted and items
// must be malloc'd/freed.
static constexpr int kBatchSize = 10;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_SetTimeout_ExceedPool);
} // namespace esphome::benchmarks