diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2a93d37fdb..b752701920a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -686,7 +686,7 @@ jobs: ram_usage: ${{ steps.extract.outputs.ram_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }} cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} - skip: ${{ steps.check-script.outputs.skip }} + skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }} steps: - name: Check out target branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -705,10 +705,39 @@ jobs: echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" fi - # All remaining steps only run if script exists + # Check if test files exist on the target branch for the requested + # components and platform. When a PR adds new test files for a platform, + # the target branch won't have them yet, so skip instead of failing. + # This check must be done here (not in determine-jobs.py) because + # determine-jobs runs on the PR branch and cannot see what the target + # branch has. + - name: Check for test files on target branch + id: check-tests + if: steps.check-script.outputs.skip != 'true' + run: | + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + found=false + for component in $(echo "$components" | jq -r '.[]'); do + # Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml) + for f in tests/components/${component}/test*.${platform}.yaml; do + if [ -f "$f" ]; then + found=true + break 2 + fi + done + done + if [ "$found" = false ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + # All remaining steps only run if script and tests exist - name: Generate cache key id: cache-key - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | # Get the commit SHA of the target branch target_sha=$(git rev-parse HEAD) @@ -735,14 +764,14 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} - name: Cache status - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then echo "✓ Cache hit! Using cached memory analysis results." @@ -752,21 +781,21 @@ jobs: fi - name: Restore Python - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} - name: Build, compile, and analyze memory - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' id: build run: | . venv/bin/activate @@ -800,7 +829,7 @@ jobs: --platform "$platform" - name: Save memory analysis to cache - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json @@ -808,7 +837,7 @@ jobs: - name: Extract memory usage for outputs id: extract - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ -f memory-analysis-target.json ]; then ram=$(jq -r '.ram_bytes' memory-analysis-target.json) diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 2048bd6b19b..e252b104566 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -20,7 +20,11 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } +uint64_t millis_64() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return static_cast(spec.tv_sec) * 1000ULL + static_cast(spec.tv_nsec) / 1000000ULL; +} void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 18406fa26eb..c06bbb2a5df 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -4,7 +4,6 @@ #include #include #include -#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" @@ -17,8 +16,8 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); #endif void yield() { ::k_yield(); } -uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } +uint32_t millis() { return static_cast(millis_64()); } +uint64_t millis_64() { return static_cast(k_uptime_get()); } uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 2be5a6c0f58..876fd9338e2 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -744,8 +744,17 @@ void Application::yield_with_select_(uint32_t delay_ms) { #error "Application placement new requires Itanium C++ ABI (GCC/Clang)" #endif static_assert(std::is_default_constructible::value, "Application must be default-constructible"); +// __USER_LABEL_PREFIX__ is "_" on Mach-O (macOS) and empty on ELF (embedded targets). +// String literal concatenation produces the correct platform-specific mangled symbol. +// Two-level macro needed: # stringifies before expansion, so the +// indirection forces __USER_LABEL_PREFIX__ to expand first. +#define ESPHOME_STRINGIFY_IMPL_(x) #x +#define ESPHOME_STRINGIFY_(x) ESPHOME_STRINGIFY_IMPL_(x) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE"); +alignas(Application) char app_storage[sizeof(Application)] asm( + ESPHOME_STRINGIFY_(__USER_LABEL_PREFIX__) "_ZN7esphome3AppE"); +#undef ESPHOME_STRINGIFY_ +#undef ESPHOME_STRINGIFY_IMPL_ #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 06c0bb8b1bf..73b6a6c9b3d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#ifndef USE_ESP32 +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; #endif @@ -475,12 +475,12 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) +#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #else @@ -714,7 +714,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#ifndef USE_ESP32 +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags @@ -872,7 +872,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } -#endif // not USE_ESP32 +#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 92c4c7adfc7..eca669368be 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -286,7 +286,7 @@ class Scheduler { // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). // On non-ESP32, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) (void) now; return millis_64(); #else @@ -294,10 +294,9 @@ class Scheduler { #endif } -#ifndef USE_ESP32 - // On non-ESP32 platforms, millis_64() HAL function delegates to this method - // which tracks 32-bit millis() rollover using millis_major_ and last_millis_. - // On ESP32, millis_64() uses esp_timer_get_time() directly. +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) + // On platforms without native 64-bit time, millis_64() HAL function delegates to this + // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. friend uint64_t millis_64(); uint64_t millis_64_impl_(uint32_t now); #endif @@ -567,8 +566,8 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; -#ifndef USE_ESP32 - // On ESP32, millis_64() uses esp_timer_get_time() directly; no rollover tracking needed. +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) + // On platforms with native 64-bit time (ESP32, Host, Zephyr), no rollover tracking needed. // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* @@ -598,7 +597,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* not USE_ESP32 */ +#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR */ }; } // namespace esphome