On ESPHOME_THREAD_SINGLE platforms (ESP8266), move the
Millis64Impl::compute() body into the header so it can be inlined at
call sites. The function is trivial: two static loads, a rollover
branch (taken every ~49.7 days), a store, and a shift+add.
The static storage (last_millis_, millis_major_) is declared as class
statics in the header and defined in time_64.cpp so all TUs share one
copy. Multi-threaded paths remain out-of-line in time_64.cpp.
Also force-inline millis_64_from_() which wraps compute() — GCC was
creating an $isra$ clone instead of inlining the wrapper.
Verified on ESP8266 (xtensa-lx106):
- Millis64Impl::compute() and millis_64_from_ symbols eliminated
- Scheduler::call() idle path: zero out-of-line calls (was 1)
- compute body inlined into 3 call sites (Scheduler::call,
next_schedule_in, loop/millis_64)
cleanup_() has a two-line fast path (check to_remove count, return
!items_.empty()) with the slow path already out-of-line in
cleanup_slow_path_(). Despite `inline` hint, GCC on Xtensa emits it
as a separate function, adding unnecessary call overhead on every
Scheduler::call() invocation even when there is nothing to clean up.
Use ESPHOME_ALWAYS_INLINE to guarantee inlining. Verified on ESP8266:
- cleanup_() symbol eliminated from binary
- Scheduler::call() grows 4 bytes (inlined fast path)
- Net savings: 23 bytes (27-byte out-of-line body removed)
- Idle loop path: 1 out-of-line call instead of 2
Replace heap-allocated SensorWithDedup wrapper with inline
LazySensorWithDedup that stores the sensor pointer and deduplicator
directly in the component, eliminating heap allocations and reducing
RAM usage for configured sensors.
On ESP32, millis() uses xTaskGetTickCount (tick clock) but
millis_64_from_() discards the 32-bit value and calls millis_64()
(esp_timer clock). Safe because scheduling only compares millis_64
against millis_64. On ESP8266, both use the same accumulator clock.
millis() uses xTaskGetTickCount (tick clock) while millis_64() uses
esp_timer_get_time (hardware timer). Safe because they are never
cross-compared: Scheduler::millis_64_from_() on ESP32 discards the
32-bit millis parameter and calls millis_64() directly, keeping all
64-bit scheduling on the esp_timer clock.