Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions codecarbon/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
Expand Down Expand Up @@ -339,6 +340,126 @@ def config():
)


@codecarbon.command(
"run",
short_help="Run a command and track its emissions.",
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def run(
ctx: typer.Context,
log_level: Annotated[
str, typer.Option(help="Log level (critical, error, warning, info, debug)")
] = "error",
):
"""
Run a command and track its carbon emissions.

This command wraps any executable and measures the process's total power
consumption during its execution. When the command completes, a summary
report is displayed and emissions data is saved to a CSV file.

Note: This tracks process-level emissions (only the specific command), not the
entire machine. For machine-level tracking, use the `monitor` command.

Examples:

Do not use quotes around the command. Use -- to separate CodeCarbon args.

# Run any shell command:
codecarbon run -- ./benchmark.sh

# Commands with arguments (use single quotes for special chars):
codecarbon run -- python -c 'print("Hello World!")'

# Pipe the command output:
codecarbon run -- npm run test > output.txt

# Display the CodeCarbon detailed logs:
codecarbon run --log-level debug -- python --version

The emissions data is appended to emissions.csv (default) in the current
directory. The file path is shown in the final report.
"""
# Suppress all CodeCarbon logs during execution
from codecarbon.external.logger import set_logger_level

set_logger_level(log_level)

# Get the command from remaining args
command = ctx.args

if not command:
print(
"ERROR: No command provided. Use: codecarbon run -- <command>",
file=sys.stderr,
)
raise typer.Exit(1)

# Initialize tracker with specified logging level
tracker = EmissionsTracker(
log_level=log_level, save_to_logger=False, tracking_mode="process"
)

print("🌱 CodeCarbon: Starting emissions tracking...")
print(f" Command: {' '.join(command)}")
print()

tracker.start()

process = None
try:
# Run the command, streaming output to console
process = subprocess.Popen(
command,
stdout=sys.stdout,
stderr=sys.stderr,
text=True,
)

# Wait for completion
exit_code = process.wait()

except FileNotFoundError:
print(f"❌ Error: Command not found: {command[0]}", file=sys.stderr)
exit_code = 127
except KeyboardInterrupt:
print("\n⚠️ Interrupted by user", file=sys.stderr)
if process is not None:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
exit_code = 130
except Exception as e:
print(f"❌ Error running command: {e}", file=sys.stderr)
exit_code = 1
finally:
emissions = tracker.stop()
print()
print("=" * 60)
print("🌱 CodeCarbon Emissions Report")
print("=" * 60)
print(f" Command: {' '.join(command)}")
if emissions is not None:
print(f" Emissions: {emissions * 1000:.4f} g CO2eq")
else:
print(" Emissions: N/A")

# Show where the data was saved
if hasattr(tracker, "_conf") and "output_file" in tracker._conf:
output_path = tracker._conf["output_file"]
# Make it absolute if it's relative
if not os.path.isabs(output_path):
output_path = os.path.abspath(output_path)
print(f" Saved to: {output_path}")

print(" ⚠️ Note: Tracked the command process and its children")
print("=" * 60)

raise typer.Exit(exit_code)


@codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.")
def monitor(
measure_power_secs: Annotated[
Expand Down
65 changes: 62 additions & 3 deletions codecarbon/external/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import math
import re
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional, Tuple
Expand Down Expand Up @@ -182,6 +183,9 @@ def __init__(
self._pid = psutil.Process().pid
self._cpu_count = count_cpus()
self._process = psutil.Process(self._pid)
# For process tracking: store last measurement time and CPU times
self._last_measurement_time: Optional[float] = None
self._last_cpu_times: Dict[int, float] = {} # pid -> total cpu time

if self._mode == "intel_power_gadget":
self._intel_interface = IntelPowerGadget(self._output_dir)
Expand Down Expand Up @@ -245,11 +249,62 @@ def _get_power_from_cpu_load(self):
f"CPU load {self._tdp} W and {cpu_load:.1f}% {load_factor=} => estimation of {power} W for whole machine."
)
elif self._tracking_mode == "process":
# Use CPU times for accurate process tracking
current_time = time.time()
current_cpu_times: Dict[int, float] = {}

# Get CPU time for main process and all children
try:
processes = [self._process] + self._process.children(recursive=True)
except (psutil.NoSuchProcess, psutil.AccessDenied):
processes = [self._process]

for proc in processes:
try:
cpu_times = proc.cpu_times()
# Total CPU time = user + system time
total_cpu_time = cpu_times.user + cpu_times.system
current_cpu_times[proc.pid] = total_cpu_time
except (psutil.NoSuchProcess, psutil.AccessDenied):
logger.debug(
f"Process {proc.pid} disappeared or access denied when getting CPU times."
)

# Calculate CPU usage based on delta
if self._last_measurement_time is not None:
time_delta = current_time - self._last_measurement_time
if time_delta > 0:
total_cpu_delta = 0.0
for pid, cpu_time in current_cpu_times.items():
last_cpu_time = self._last_cpu_times.get(pid, cpu_time)
cpu_delta = cpu_time - last_cpu_time
if cpu_delta > 0:
total_cpu_delta += cpu_delta
logger.debug(
f"Process {pid} CPU time delta: {cpu_delta:.3f}s"
)

# CPU load as percentage (can be > 100% with multiple cores)
# total_cpu_delta is the CPU time used, time_delta is wall clock time
cpu_load = (total_cpu_delta / time_delta) * 100
logger.debug(
f"Total CPU delta: {total_cpu_delta:.3f}s over {time_delta:.3f}s = {cpu_load:.1f}% (across {self._cpu_count} cores)"
)
else:
cpu_load = 0.0
else:
cpu_load = 0.0
logger.debug("First measurement, no CPU delta available yet")

cpu_load = self._process.cpu_percent(interval=0.5) / self._cpu_count
power = self._tdp * cpu_load / 100
# Store for next measurement
self._last_measurement_time = current_time
self._last_cpu_times = current_cpu_times

# Normalize to percentage of total CPU capacity
cpu_load_normalized = cpu_load / self._cpu_count
power = self._tdp * cpu_load_normalized / 100
logger.debug(
f"CPU load {self._tdp} W and {cpu_load * 100:.1f}% => estimation of {power} W for process {self._pid}."
f"CPU load {self._tdp} W and {cpu_load:.1f}% ({cpu_load_normalized:.1f}% normalized) => estimation of {power:.2f} W for process {self._pid} and {len(current_cpu_times) - 1} children."
)
else:
raise Exception(f"Unknown tracking_mode {self._tracking_mode}")
Expand Down Expand Up @@ -318,9 +373,13 @@ def measure_power_and_energy(self, last_duration: float) -> Tuple[Power, Energy]
def start(self):
if self._mode in ["intel_power_gadget", "intel_rapl", "apple_powermetrics"]:
self._intel_interface.start()
# Reset process tracking state for fresh measurements
self._last_measurement_time = None
self._last_cpu_times = {}
if self._mode == MODE_CPU_LOAD:
# The first time this is called it will return a meaningless 0.0 value which you are supposed to ignore.
_ = self._get_power_from_cpu_load()
_ = self._get_power_from_cpu_load()

def monitor_power(self):
cpu_power = self._get_power_from_cpus()
Expand Down
62 changes: 61 additions & 1 deletion docs/edit/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,67 @@ The command line could also works without internet by providing the country code

codecarbon monitor --offline --country-iso-code FRA

Implementing CodeCarbon in your code allows you to track the emissions of a specific block of code.

Running Any Command with CodeCarbon
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you want to track emissions while running any command or program (not just Python scripts), you can use the ``codecarbon run`` command.
This allows non-Python users to measure machine emissions during the execution of any command:

.. code-block:: console

codecarbon run -- <your_command>

Do not surround ``<your_command>`` with quotes. The double hyphen ``--`` indicates the end of CodeCarbon options and the beginning of the command to run.

**Examples:**

.. code-block:: console

# Run a shell script
codecarbon run -- ./benchmark.sh

# Run a command with arguments (use quotes for special characters)
codecarbon run -- bash -c 'echo "Processing..."; sleep 30; echo "Done!"'

# Run Python scripts
codecarbon run -- python train_model.py

# Run Node.js applications
codecarbon run -- node app.js

# Run tests with output redirection
codecarbon run -- npm run test > output.txt

# Display the CodeCarbon detailed logs
codecarbon run --log-level debug -- python --version

**Output:**

When the command completes, CodeCarbon displays a summary report and saves the emissions data to a CSV file:

.. code-block:: console

🌱 CodeCarbon: Starting emissions tracking...
Command: bash -c echo "Processing..."; sleep 30; echo "Done!"

Processing...
Done!

============================================================
🌱 CodeCarbon Emissions Report
============================================================
Command: bash -c echo "Processing..."; sleep 30; echo "Done!"
Emissions: 0.0317 g CO2eq
Saved to: /home/user/emissions.csv
⚠️ Note: Measured entire machine (includes all system processes)
============================================================

.. note::
The ``codecarbon run`` command tracks process-level emissions (only the specific command), not the
entire machine. For machine-level tracking, use the ``codecarbon monitor`` command.

For more fine-grained tracking, implementing CodeCarbon in your code allows you to track the emissions of a specific block of code.

Explicit Object
~~~~~~~~~~~~~~~
Expand Down
10 changes: 9 additions & 1 deletion examples/command_line_tool.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
"""
This example demonstrates how to use CodeCarbon with command line tools.

Here we measure the emissions of an speech-to-text with WhisperX.
⚠️ IMPORTANT LIMITATION:
CodeCarbon tracks emissions at the MACHINE level when monitoring external commands
via subprocess. It measures total system power during the command execution, which
includes the command itself AND all other system processes.

For accurate process-level tracking, the tracking code must be embedded in the
application being measured (not possible with external binaries like WhisperX).

This example measures emissions during WhisperX execution, but cannot isolate
WhisperX's exact contribution from other system activity.
"""

import subprocess
Expand Down
Loading