Merge branch 'memcpy_speedup' into integration

This commit is contained in:
J. Nick Koston 2025-07-21 10:54:10 -10:00
commit 504c67f59f
No known key found for this signature in database
9 changed files with 241 additions and 154 deletions

View File

@ -34,6 +34,7 @@ from esphome.const import (
CONF_PORT,
CONF_SUBSTITUTIONS,
CONF_TOPIC,
ENV_NOGITIGNORE,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@ -209,6 +210,9 @@ def wrap_to_code(name, comp):
def write_cpp(config):
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
generate_cpp_contents(config)
return write_cpp_file()
@ -225,10 +229,13 @@ def generate_cpp_contents(config):
def write_cpp_file():
writer.write_platformio_project()
code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s)
from esphome.build_gen import platformio
platformio.write_project()
return 0

View File

View File

@ -0,0 +1,102 @@
import os
from esphome.const import __version__
from esphome.core import CORE
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
from esphome.writer import find_begin_end, update_storage_json
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
INI_BASE_FORMAT = (
"""; Auto generated code by esphome
[common]
lib_deps =
build_flags =
upload_flags =
""",
"""
""",
)
def format_ini(data: dict[str, str | list[str]]) -> str:
content = ""
for key, value in sorted(data.items()):
if isinstance(value, list):
content += f"{key} =\n"
for x in value:
content += f" {x}\n"
else:
content += f"{key} = {value}\n"
return content
def get_ini_content():
CORE.add_platformio_option(
"lib_deps",
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
+ ["${common.lib_deps}"],
)
# Sort to avoid changing build flags order
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
# Sort to avoid changing build unflags order
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
# Add extra script for C++ flags
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
content = "[platformio]\n"
content += f"description = ESPHome {__version__}\n"
content += f"[env:{CORE.name}]\n"
content += format_ini(CORE.platformio_options)
return content
def write_ini(content):
update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if os.path.isfile(path):
text = read_file(path)
content_format = find_begin_end(
text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END
)
else:
content_format = INI_BASE_FORMAT
full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}"
full_file += INI_AUTO_GENERATE_END + content_format[1]
write_file_if_changed(path, full_file)
def write_project():
mkdir_p(CORE.build_path)
content = get_ini_content()
write_ini(content)
# Write extra script for C++ specific flags
write_cxx_flags_script()
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
Import("env")
# Add C++ specific flags
"""
def write_cxx_flags_script() -> None:
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
contents = CXX_FLAGS_FILE_CONTENTS
if not CORE.is_host:
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
contents += "\n"
write_file_if_changed(path, contents)

View File

@ -5,6 +5,7 @@
#include "esphome/core/log.h"
#include <cassert>
#include <cstring>
#include <vector>
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
@ -206,8 +207,13 @@ class ProtoWriteBuffer {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len);
auto *data = reinterpret_cast<const uint8_t *>(string);
this->buffer_->insert(this->buffer_->end(), data, data + len);
// Using resize + memcpy instead of insert provides significant performance improvement:
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
// as it avoids iterator checks and potential element moves that insert performs
size_t old_size = this->buffer_->size();
this->buffer_->resize(old_size + len);
std::memcpy(this->buffer_->data() + old_size, string, len);
}
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size(), force);

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import fan
import esphome.config_validation as cv
from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
from .. import CONF_TUYA_ID, Tuya, tuya_ns
@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint"
TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan)
CONFIG_SCHEMA = cv.All(
fan.FAN_SCHEMA.extend(
fan.fan_schema(TuyaFan)
.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan),
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
}
).extend(cv.COMPONENT_SCHEMA),
)
.extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
)
@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
parent = await cg.get_variable(config[CONF_TUYA_ID])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT])
var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT])
await cg.register_component(var, config)
await fan.register_fan(var, config)

View File

@ -470,6 +470,52 @@ class Library:
return self.as_tuple == other.as_tuple
return NotImplemented
def reconcile_with(self, other):
"""Merge two libraries, reconciling any conflicts."""
if self.name != other.name:
# Different libraries, no reconciliation possible
raise ValueError(
f"Cannot reconcile libraries with different names: {self.name} and {other.name}"
)
# repository specificity takes precedence over version specificity
if self.repository is None and other.repository is None:
pass # No repositories, no conflict, continue on
elif self.repository is None:
# incoming library has a repository, use it
self.repository = other.repository
self.version = other.version
return self
elif other.repository is None:
return self # use the repository/version already present
elif self.repository != other.repository:
raise ValueError(
f"Reconciliation failed! Libraries {self} and {other} requested with conflicting repositories!"
)
if self.version is None and other.version is None:
return self # Arduino library reconciled against another Arduino library, current is acceptable
if self.version is None:
# incoming library has a version, use it
self.version = other.version
return self
if other.version is None:
return self # incoming library has no version, current is acceptable
# Same versions, current library is acceptable
if self.version != other.version:
raise ValueError(
f"Version pinning failed! Libraries {other} and {self} "
"requested with conflicting versions!"
)
return self
# pylint: disable=too-many-public-methods
class EsphomeCore:
@ -505,8 +551,8 @@ class EsphomeCore:
self.main_statements: list[Statement] = []
# A list of statements to insert in the global block (includes and global variables)
self.global_statements: list[Statement] = []
# A set of platformio libraries to add to the project
self.libraries: list[Library] = []
# A map of platformio libraries to add to the project (shortname: (name, version, repository))
self.platformio_libraries: dict[str, Library] = {}
# A set of build flags to set in the platformio project
self.build_flags: set[str] = set()
# A set of build unflags to set in the platformio project
@ -550,7 +596,7 @@ class EsphomeCore:
self.variables = {}
self.main_statements = []
self.global_statements = []
self.libraries = []
self.platformio_libraries = {}
self.build_flags = set()
self.build_unflags = set()
self.defines = set()
@ -738,54 +784,22 @@ class EsphomeCore:
_LOGGER.debug("Adding global: %s", expression)
return expression
def add_library(self, library):
def add_library(self, library: Library):
if not isinstance(library, Library):
raise ValueError(
raise TypeError(
f"Library {library} must be instance of Library, not {type(library)}"
)
for other in self.libraries[:]:
if other.name is None or library.name is None:
continue
library_name = (
library.name if "/" not in library.name else library.name.split("/")[1]
)
other_name = (
other.name if "/" not in other.name else other.name.split("/")[1]
)
if other_name != library_name:
continue
if other.repository is not None:
if library.repository is None or other.repository == library.repository:
# Other is using a/the same repository, takes precedence
break
raise ValueError(
f"Adding named Library with repository failed! Libraries {library} and {other} "
"requested with conflicting repositories!"
)
short_name = (
library.name if "/" not in library.name else library.name.split("/")[-1]
)
if library.repository is not None:
# This is more specific since its using a repository
self.libraries.remove(other)
continue
if library.version is None:
# Other requirement is more specific
break
if other.version is None:
# Found more specific version requirement
self.libraries.remove(other)
continue
if other.version == library.version:
break
raise ValueError(
f"Version pinning failed! Libraries {library} and {other} "
"requested with conflicting versions!"
)
else:
if short_name not in self.platformio_libraries:
_LOGGER.debug("Adding library: %s", library)
self.libraries.append(library)
return library
self.platformio_libraries[short_name] = library
return library
self.platformio_libraries[short_name].reconcile_with(library)
return self.platformio_libraries[short_name]
def add_build_flag(self, build_flag: str) -> str:
self.build_flags.add(build_flag)

View File

@ -7,7 +7,6 @@ import re
from esphome import loader
from esphome.config import iter_component_configs, iter_components
from esphome.const import (
ENV_NOGITIGNORE,
HEADER_FILE_EXTENSIONS,
PLATFORM_ESP32,
SOURCE_FILE_EXTENSIONS,
@ -16,8 +15,6 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError
from esphome.helpers import (
copy_file_if_changed,
get_bool_env,
mkdir_p,
read_file,
walk_files,
write_file_if_changed,
@ -30,8 +27,6 @@ CPP_AUTO_GENERATE_BEGIN = "// ========== AUTO GENERATED CODE BEGIN ==========="
CPP_AUTO_GENERATE_END = "// =========== AUTO GENERATED CODE END ============"
CPP_INCLUDE_BEGIN = "// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ==========="
CPP_INCLUDE_END = "// ========== AUTO GENERATED INCLUDE BLOCK END ==========="
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
CPP_BASE_FORMAT = (
"""// Auto generated code by esphome
@ -50,20 +45,6 @@ void loop() {
""",
)
INI_BASE_FORMAT = (
"""; Auto generated code by esphome
[common]
lib_deps =
build_flags =
upload_flags =
""",
"""
""",
)
UPLOAD_SPEED_OVERRIDE = {
"esp210": 57600,
}
@ -140,40 +121,6 @@ def update_storage_json():
new.save(path)
def format_ini(data: dict[str, str | list[str]]) -> str:
content = ""
for key, value in sorted(data.items()):
if isinstance(value, list):
content += f"{key} =\n"
for x in value:
content += f" {x}\n"
else:
content += f"{key} = {value}\n"
return content
def get_ini_content():
CORE.add_platformio_option(
"lib_deps", [x.as_lib_dep for x in CORE.libraries] + ["${common.lib_deps}"]
)
# Sort to avoid changing build flags order
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
# Sort to avoid changing build unflags order
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
# Add extra script for C++ flags
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
content = "[platformio]\n"
content += f"description = ESPHome {__version__}\n"
content += f"[env:{CORE.name}]\n"
content += format_ini(CORE.platformio_options)
return content
def find_begin_end(text, begin_s, end_s):
begin_index = text.find(begin_s)
if begin_index == -1:
@ -201,34 +148,6 @@ def find_begin_end(text, begin_s, end_s):
return text[:begin_index], text[(end_index + len(end_s)) :]
def write_platformio_ini(content):
update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if os.path.isfile(path):
text = read_file(path)
content_format = find_begin_end(
text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END
)
else:
content_format = INI_BASE_FORMAT
full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}"
full_file += INI_AUTO_GENERATE_END + content_format[1]
write_file_if_changed(path, full_file)
def write_platformio_project():
mkdir_p(CORE.build_path)
content = get_ini_content()
if not get_bool_env(ENV_NOGITIGNORE):
write_gitignore()
write_platformio_ini(content)
# Write extra script for C++ specific flags
write_cxx_flags_script()
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
#pragma once
#include "esphome/core/macros.h"
@ -400,20 +319,3 @@ def write_gitignore():
if not os.path.isfile(path):
with open(file=path, mode="w", encoding="utf-8") as f:
f.write(GITIGNORE_CONTENT)
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
Import("env")
# Add C++ specific flags
"""
def write_cxx_flags_script() -> None:
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
contents = CXX_FLAGS_FILE_CONTENTS
if not CORE.is_host:
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
contents += "\n"
write_file_if_changed(path, contents)

View File

@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.9.0
click==8.1.7
esphome-dashboard==20250514.0
aioesphomeapi==37.0.3
aioesphomeapi==37.0.4
zeroconf==0.147.0
puremagic==1.30
ruamel.yaml==0.18.14 # dashboard_import

View File

@ -473,6 +473,61 @@ class TestLibrary:
assert actual == expected
@pytest.mark.parametrize(
"target, other, result, exception",
(
(core.Library("libfoo", None), core.Library("libfoo", None), True, None),
(
core.Library("libfoo", "1.2.3"),
core.Library("libfoo", "1.2.3"),
True, # target is unchanged
None,
),
(
core.Library("libfoo", None),
core.Library("libfoo", "1.2.3"),
False, # Use version from other
None,
),
(
core.Library("libfoo", "1.2.3"),
core.Library("libfoo", "1.2.4"),
False,
ValueError, # Version mismatch
),
(
core.Library("libfoo", "1.2.3"),
core.Library("libbar", "1.2.3"),
False,
ValueError, # Name mismatch
),
(
core.Library(
"libfoo", "1.2.4", "https://github.com/esphome/ESPAsyncWebServer"
),
core.Library("libfoo", "1.2.3"),
True, # target is unchanged due to having a repository
None,
),
(
core.Library("libfoo", "1.2.3"),
core.Library(
"libfoo", "1.2.4", "https://github.com/esphome/ESPAsyncWebServer"
),
False, # use other due to having a repository
None,
),
),
)
def test_reconcile(self, target, other, result, exception):
if exception is not None:
with pytest.raises(exception):
target.reconcile_with(other)
else:
expected = target if result else other
actual = target.reconcile_with(other)
assert actual == expected
class TestEsphomeCore:
@pytest.fixture