From e02d50ce9388a0cfedcf8a499095a5b58226e4ea Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Wed, 12 Aug 2026 23:30:54 +0200 Subject: [PATCH 1/2] fix: resume the scheduler in stop_task only if start_task paused it start_task stops the periodic scheduler but nothing ever restarted it, so a tracker started with start() lost its periodic measurements after the first task. Restarting it unconditionally would instead leave a 1s scheduler running for pure start_task/stop_task users, so track whether start_task actually paused a running scheduler. Co-Authored-By: Claude Opus 5 (1M context) --- codecarbon/emissions_tracker.py | 9 +++++++++ tests/test_emissions_tracker.py | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 5591f621e..e935fbbc1 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -296,6 +296,7 @@ def _initialize_runtime_state(self) -> None: self._tasks: Dict[str, Task] = {} self._active_task: Optional[str] = None self._active_task_emissions_at_start: Optional[EmissionsData] = None + self._scheduler_paused_by_task = False self._hardware = [] self._hardware_initialized = False @@ -754,6 +755,10 @@ def start_task(self, task_name=None) -> None: # Stop scheduler as we do not want it to interfere with the task measurement if self._scheduler: + # Only resume it in stop_task if it was actually running, i.e. the tracker + # was started with start(). Pure start_task/stop_task usage must not leave + # a periodic measurement running behind. + self._scheduler_paused_by_task = not self._scheduler._stopped self._scheduler.stop() # Task background thread for measuring power @@ -850,6 +855,10 @@ def stop_task(self, task_name: str = None) -> EmissionsData: self._active_task = None self._active_task_emissions_at_start = None # Clear task-specific start data + if self._scheduler is not None and self._scheduler_paused_by_task: + self._scheduler_paused_by_task = False + self._scheduler.start() + return task_emission_data @suppress(Exception) diff --git a/tests/test_emissions_tracker.py b/tests/test_emissions_tracker.py index 8ab12e5d8..f5699b510 100644 --- a/tests/test_emissions_tracker.py +++ b/tests/test_emissions_tracker.py @@ -765,6 +765,32 @@ def test_start_task_returns_when_engine_initialization_fails( any("Tracker not initialized" in message for message in logs.output) ) + def test_stop_task_resumes_scheduler_only_if_start_task_paused_it( + self, + mock_cli_setup, + mock_log_values, + mocked_get_gpu_details, + mocked_env_cloud_details, + mocked_get_gpu_utilization_list, + mocked_is_gpu_details_available, + mocked_is_nvidia_system, + ): + # Pure start_task/stop_task usage: no periodic scheduler must be left running. + tracker = EmissionsTracker(save_to_file=False) + tracker.start_task("task-only") + tracker.stop_task() + self.assertTrue(tracker._scheduler._stopped) + tracker.stop() + + # start() then start_task/stop_task: the paused scheduler must be resumed. + tracker = EmissionsTracker(save_to_file=False) + tracker.start() + tracker.start_task("task-in-run") + self.assertTrue(tracker._scheduler._stopped) + tracker.stop_task() + self.assertFalse(tracker._scheduler._stopped) + tracker.stop() + @mock.patch("codecarbon.external.ram.RAM.measure_power_and_energy") @mock.patch("codecarbon.external.hardware.CPU.measure_power_and_energy") @mock.patch( From 080ed19a4c2e8a52686c6e11fcc2e3ceca721b61 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Thu, 27 Aug 2026 17:54:07 +0200 Subject: [PATCH 2/2] Changes to codecarbon/emissions_tracker.py: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sticky flag in start_task (:755-763) — self._scheduler_paused_by_task or not self._scheduler._stopped, so a second start_task() on an already-measuring tracker can't clear it. New _resume_scheduler_if_paused_by_task() helper (:803-812), called at both stop_task exit points — the unknown-task early return and the normal path. I did not move the resume to the top of stop_task as I originally suggested. That would have restarted the timer before stop_task's own _measure_power_and_energy() runs, letting the scheduler thread mutate _last_measured_time/_previous_emissions concurrently with the task-delta computation — narrow at the 15s default, but real at low measure_power_secs. Calling the helper at each exit point keeps the original placement's safety and covers the early return. Test (tests/test_emissions_tracker.py:786-802) — two cases added: double start_task, and stop_task("unknown-task"). I confirmed both fail against the pre-fix source (AssertionError: True is not false) and pass with it, so they're real regression guards rather than assertions that hold either way. Verification: pre-commit clean on both files. Full suite: 5 failed, 644 passed, 3 skipped. The 5 failures are all in tests/test_config.py and tests/test_electricitymaps_config_backward_compatibility.py — I re-ran them with my changes stashed and got the identical 5, so they're pre-existing locally (they read your real .codecarbon.config), not caused by this work. Worth noting your PR description's "627 passed, 21 skipped, clean" doesn't match this machine's current state either way. --- codecarbon/emissions_tracker.py | 24 +++++++++++++++++++----- tests/test_emissions_tracker.py | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index e935fbbc1..54e95a9fa 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -757,8 +757,11 @@ def start_task(self, task_name=None) -> None: if self._scheduler: # Only resume it in stop_task if it was actually running, i.e. the tracker # was started with start(). Pure start_task/stop_task usage must not leave - # a periodic measurement running behind. - self._scheduler_paused_by_task = not self._scheduler._stopped + # a periodic measurement running behind. The flag is sticky: a second + # start_task call sees an already stopped scheduler and must not clear it. + self._scheduler_paused_by_task = ( + self._scheduler_paused_by_task or not self._scheduler._stopped + ) self._scheduler.stop() # Task background thread for measuring power @@ -799,6 +802,16 @@ def start_task(self, task_name=None) -> None: ) self._active_task = task_name + def _resume_scheduler_if_paused_by_task(self) -> None: + """ + Restart the periodic scheduler if, and only if, start_task paused a running + one. No-op for pure start_task/stop_task usage, and when called from stop() + where the scheduler has already been released. + """ + if self._scheduler is not None and self._scheduler_paused_by_task: + self._scheduler_paused_by_task = False + self._scheduler.start() + def stop_task(self, task_name: str = None) -> EmissionsData: """ Stop tracking a dedicated execution task. Delta energy is computed by task, to isolate its contribution to total @@ -811,6 +824,9 @@ def stop_task(self, task_name: str = None) -> EmissionsData: task_name = task_name if task_name else self._active_task if self._tasks.get(task_name) is None: logger.warning("stop_task : No active task to stop.") + # Still resume, so an unknown task name does not leave the periodic + # scheduler paused for the rest of the run. + self._resume_scheduler_if_paused_by_task() return None self._measure_power_and_energy() emissions_data = ( @@ -855,9 +871,7 @@ def stop_task(self, task_name: str = None) -> EmissionsData: self._active_task = None self._active_task_emissions_at_start = None # Clear task-specific start data - if self._scheduler is not None and self._scheduler_paused_by_task: - self._scheduler_paused_by_task = False - self._scheduler.start() + self._resume_scheduler_if_paused_by_task() return task_emission_data diff --git a/tests/test_emissions_tracker.py b/tests/test_emissions_tracker.py index f5699b510..a2d6b5966 100644 --- a/tests/test_emissions_tracker.py +++ b/tests/test_emissions_tracker.py @@ -791,6 +791,25 @@ def test_stop_task_resumes_scheduler_only_if_start_task_paused_it( self.assertFalse(tracker._scheduler._stopped) tracker.stop() + # A second start_task on an already measuring tracker must not clear the + # "paused by task" flag, otherwise the scheduler is never resumed. + tracker = EmissionsTracker(save_to_file=False) + tracker.start() + tracker.start_task("first-task") + tracker.start_task("ignored-second-task") + self.assertTrue(tracker._scheduler._stopped) + tracker.stop_task() + self.assertFalse(tracker._scheduler._stopped) + tracker.stop() + + # An unknown task name must not leave the scheduler paused either. + tracker = EmissionsTracker(save_to_file=False) + tracker.start() + tracker.start_task("known-task") + self.assertIsNone(tracker.stop_task("unknown-task")) + self.assertFalse(tracker._scheduler._stopped) + tracker.stop() + @mock.patch("codecarbon.external.ram.RAM.measure_power_and_energy") @mock.patch("codecarbon.external.hardware.CPU.measure_power_and_energy") @mock.patch(