07 - Monitor PID¶
Attribute system energy to a specific process tree, using cgroup_v2 where available and psutil cpu_percent as a fallback. Demonstrates the two-orders-of-magnitude difference these backends can produce on a busy multi-core host.
What you learn¶
Monitor(pid=..., attribution_mode="auto")PID attribution.- The
attribution_backendfield on every sample. - Why cgroup_v2 is dramatically more accurate than cpu_percent under contention.
Prerequisites¶
pip install codegreen psutil- Linux with cgroup_v2 unified hierarchy for the high-accuracy backend.
Source¶
examples/07_monitor_pid/main.py
"""07 Per-PID energy attribution: cgroup_v2 (preferred) vs cpu_percent.
Spawns a CPU-burn worker, monitors it with both backends, and prints the
attributed-joules delta. cgroup_v2 reads `cpu.stat:usage_usec` directly;
cpu_percent uses psutil sampling. On a single-PID workload the difference
is typically <10%; under heavy multi-tenant contention cgroup_v2 is
significantly more accurate.
"""
import json
import subprocess
import sys
import time
def _spawn_burn(seconds: float):
code = (
f"import time\n"
f"t=time.time()+{seconds}\n"
f"x=0\n"
f"while time.time()<t:\n"
f" x=sum(i*i for i in range(50_000))\n"
)
return subprocess.Popen([sys.executable, "-c", code])
def _measure(pid: int, duration: float, mode: str) -> dict:
import codegreen
m = codegreen.Monitor(interval_ms=200, duration_s=duration, pid=pid,
attribution_mode=mode).start()
m._thread.join()
return m.stop()
def main() -> int:
p = _spawn_burn(4.0)
time.sleep(0.2)
cg = _measure(p.pid, 3.0, "auto")
p.wait(timeout=5)
p = _spawn_burn(4.0)
time.sleep(0.2)
cp = _measure(p.pid, 3.0, "cpu_percent")
p.wait(timeout=5)
delta_pct = 100.0 * (cg["proc_attributed_joules"] - cp["proc_attributed_joules"]) / max(cp["proc_attributed_joules"], 1e-6)
print(json.dumps({
"cgroup_backend": cg["attribution_backend"],
"cgroup_attributed_j": cg["proc_attributed_joules"],
"cgroup_total_j": cg["total_joules"],
"cpu_percent_attributed_j": cp["proc_attributed_joules"],
"cpu_percent_total_j": cp["total_joules"],
"delta_pct_vs_cpu_percent": round(delta_pct, 2),
}, indent=2))
print(f"OK: monitor_pid; backend={cg['attribution_backend']}; "
f"cgroup_j={cg['proc_attributed_joules']:.3f}; cpu_pct_j={cp['proc_attributed_joules']:.3f}; "
f"delta_pct={delta_pct:.2f}")
return 0
if __name__ == "__main__":
sys.exit(main())
Run¶
Equivalent CLI¶
# Auto: cgroup_v2 if available, then cgroup_v1, then cpu_percent
codegreen monitor --pid 1234 -d 30 -o pid.jsonl
# Force a specific backend (useful for benchmarking)
codegreen monitor --pid 1234 --attribution-mode cgroup_v2
codegreen monitor --pid 1234 --attribution-mode cpu_percent
Expected output¶
Why it matters¶
System RAPL is per-socket, not per-process. To attribute a slice of
that system energy to your service, you need a per-process CPU usage
counter. cgroup_v2's cpu.stat is monotonic, kernel-attested, and
race-free; psutil cpu_percent is a sampled estimate that normalises against
full system capacity (so a single-threaded burner on a 64-core host
appears as 1.5% of the system instead of 100% of one core).
Always check attribution_backend
If the header says cpu_percent, treat the per-PID joules as a
lower-bound estimate -- the real number can be 100x higher on a
many-core idle host.
See also: User guide: Continuous Monitoring, 08 - Monitor socket.