Tracker cannot be restarted: start() after stop() is a no-op and the next row's duration is wrong #1328
Description
Problem
A tracker cannot be restarted. After stop(), a subsequent start() is a no-op ("Already started tracking"), and the next stop() still writes a row — but that row's duration covers everything since the original start(), including the idle gap, and the phase was never sampled. This is the natural pattern for benchmarking loops ("measure phase A, stop, measure phase B") and for notebooks where a cell is re-run.
Reproduction
tracker = OfflineEmissionsTracker(country_iso_code="FRA", measure_power_secs=1)
tracker.start(); time.sleep(3); tracker.stop() # row 1: duration ~3
time.sleep(3)
tracker.start() # logs "Already started tracking", does nothing
time.sleep(3); tracker.stop() # row 2: duration ~9, unsampled
assert tracker._scheduler is None # no periodic measurement at all in phase BRoot cause
stop() tears down both schedulers but never clears the start time (codecarbon/emissions_tracker.py:909-914):
if self._scheduler:
self._scheduler.stop()
self._scheduler = None
if self._scheduler_monitor_power:
self._scheduler_monitor_power.stop()
self._scheduler_monitor_power = Nonestart() then bails out because _start_time is still set (emissions_tracker.py:708-710), and even if that guard is bypassed, self._scheduler.start() at emissions_tracker.py:725 raises AttributeError on None, which the @suppress(Exception) decorator on start() turns into a warning. _initialize_scheduler_state() (emissions_tracker.py:344) is only ever called from __init__ (emissions_tracker.py:604), so the schedulers are never rebuilt.
Lifecycle state is split across _start_time, _scheduler and _scheduler_monitor_power with no single reset path.
Expected vs actual
Expected: start() after stop() resumes tracking; the periodic measurement runs again and the next row's duration reflects active tracking time.
Actual: the second start() is silently a no-op, phase B gets a single catch-all measurement from _measure_power_and_energy_if_stale() at stop(), and the row's duration includes the paused interval — so emissions_rate for that row is wrong too.
Measured on current master (measure_power_secs=1, 3 s per phase): row 1 duration=3.088958, row 2 duration=6.104781.
Activity