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
6 changes: 4 additions & 2 deletions daemon/clearpowerd/charge_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,13 @@ def _write_thresholds(self, start, end):
order = order[::-1]
try:
for path, value in order:
write_str(path, value)
if os.path.exists(path):
write_str(path, value)
except OSError:
# firmware quirk: try the other order once before giving up
for path, value in order[::-1]:
write_str(path, value)
if os.path.exists(path):
write_str(path, value)

def _write_behaviour(self, b):
if not self.behaviour_supported:
Expand Down
2 changes: 1 addition & 1 deletion daemon/clearpowerd/display_cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _save(self):

@property
def calibrated(self):
return len(self.table) >= 2
return len(self.table) >= 2 and any(p[1] > 0.001 for p in self.table)

# ---- content level from the UI -------------------------------------------
def set_content(self, apl, now):
Expand Down
62 changes: 46 additions & 16 deletions daemon/clearpowerd/sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
from .sources.rapl import Rapl
from .sources.backlight import Backlight
from .sources.hwmon import Hwmon
from .sources.nvidia import NvidiaGpu
from .sources.procs import Procs
from .smoothing import Ema
from .sysfs import read_str

SMOOTHED = ("bat_w", "psys", "package", "core", "uncore", "dram")
SMOOTHED = ("bat_w", "psys", "package", "core", "uncore", "dram", "dgpu")


class Sampler:
Expand All @@ -26,6 +27,7 @@ def __init__(self, cfg, display_cal=None):
self.battery = Battery(cfg["battery"])
self.adapter = Adapter()
self.rapl = Rapl()
self.nvidia = NvidiaGpu()
self.backlight = Backlight()
self.hwmon = Hwmon()
self.procs = Procs(cfg["procs_interval_s"])
Expand Down Expand Up @@ -65,23 +67,32 @@ def _smooth(self, key, value, now):
return self.ema[key].update(value, now)

@staticmethod
def _rest(psys, package, dram):
"""Platform power not attributable to the SoC or memory (display + peripherals)."""
if psys is None or psys < 0:
def _rest(psys, package, dram, bat_w=None, on_ac=True, dgpu=0.0):
"""Platform power not attributable to the SoC, memory, or dGPU (display + peripherals)."""
plat = None
if not on_ac and bat_w is not None and bat_w < -0.05:
plat = -bat_w
elif psys is not None and psys > 0 and (package is None or package <= 0 or psys >= package * 0.7):
plat = psys
if plat is None or plat < 0:
return -1.0
return max(psys - max(package or 0.0, 0.0) - max(dram or 0.0, 0.0), 0.0)
return max(plat - max(package or 0.0, 0.0) - max(dram or 0.0, 0.0) - max(dgpu or 0.0, 0.0), 0.0)

def sample(self, hot=True):
now = time.monotonic()
snap = {"ts": time.time()}
bat = self.battery.read()
ad = self.adapter.read()
rapl = self.rapl.read()
nv = self.nvidia.read() if getattr(self, "nvidia", None) is not None else {"gpu_power_w": 0.0, "temp_gpu": -1.0}
bl = self.backlight.read()
snap.update(bat)
snap.update(ad)
snap.update(bl)
snap.update(self._thermal_read(hot))
thermal = self._thermal_read(hot)
if nv.get("temp_gpu", -1.0) > 0 and thermal.get("temp_gpu", -1.0) < 0:
thermal["temp_gpu"] = nv["temp_gpu"]
snap.update(thermal)
on_ac = ad["on_ac"]

# ---- raw inputs (for history / runtime / calibration) ----
Expand All @@ -92,8 +103,9 @@ def sample(self, hot=True):
"core": rapl.get("core", -1.0),
"uncore": rapl.get("uncore", -1.0),
"dram": rapl.get("dram", -1.0),
"dgpu": nv.get("gpu_power_w", 0.0),
}
raw["rest"] = self._rest(raw["psys"], raw["package"], raw["dram"])
raw["rest"] = self._rest(raw["psys"], raw["package"], raw["dram"], raw["bat_w"], on_ac, raw["dgpu"])
self.raw = raw

# ---- smoothed inputs ----
Expand All @@ -104,13 +116,26 @@ def sample(self, hot=True):
core = self._smooth("core", raw["core"], now)
uncore = self._smooth("uncore", raw["uncore"], now)
dram = self._smooth("dram", raw["dram"], now)
dgpu = self._smooth("dgpu", raw["dgpu"], now) if "dgpu" in self.ema else 0.0
dgpu = max(dgpu, 0.0)

# ---- whole-machine draw ----
psys_valid = psys > 0 and (package <= 0 or psys >= package * 0.7)
disp_est = 0.0
if not on_ac and bat_w < -0.05:
sys_w, sys_source = -bat_w, "battery" # physical truth incl. all losses
elif psys > 0:
elif psys_valid:
sys_w, sys_source = psys, "psys"
elif package > 0:
sys_w, sys_source = package + 3.0, "estimate"
if bl.get("display_on", True):
if self.display_cal is not None and bl.get("brightness_raw") is not None:
e = self.display_cal.emission_w(bl.get("brightness_raw"), now)
disp_est = max(e, 0.0) if e >= 0 else (bl.get("brightness_pct", 50.0) / 100.0 * 5.0)
else:
disp_est = bl.get("brightness_pct", 50.0) / 100.0 * 5.0
base_other = 3.0
sys_w = package + max(dram or 0.0, 0.0) + dgpu + disp_est + base_other
sys_source = "estimate"
else:
sys_w, sys_source = -1.0, "none"

Expand All @@ -119,10 +144,11 @@ def sample(self, hot=True):
rest_w = -1.0
if package > 0 and sys_w > 0:
cpu_w = max(core, 0.0)
gpu_w = max(uncore, 0.0)
soc_w = max(package - cpu_w - gpu_w, 0.0)
igpu_w = max(uncore, 0.0)
soc_w = max(package - cpu_w - igpu_w, 0.0)
gpu_w = igpu_w + dgpu
mem_w = max(dram, 0.0)
measured = package + mem_w
measured = package + mem_w + dgpu
if measured > sys_w: # psys occasionally undershoots; keep the total authoritative
k = sys_w / measured
cpu_w, gpu_w, soc_w, mem_w = cpu_w * k, gpu_w * k, soc_w * k, mem_w * k
Expand All @@ -133,10 +159,14 @@ def sample(self, hot=True):

display_w = -1.0
other_w = rest_w
if self.display_cal is not None and rest_w >= 0:
e = self.display_cal.emission_w(bl.get("brightness_raw"), now)
if e >= 0:
display_w = 0.0 if not bl["display_on"] else min(e, rest_w)
if rest_w >= 0:
if self.display_cal is not None:
e = self.display_cal.emission_w(bl.get("brightness_raw"), now)
if e >= 0:
display_w = 0.0 if not bl.get("display_on", True) else min(e, rest_w)
other_w = rest_w - display_w
if display_w < 0 and sys_source == "estimate":
display_w = min(disp_est, rest_w)
other_w = rest_w - display_w

# Adapter supplies the machine (minus whatever the battery is contributing when the
Expand Down
5 changes: 4 additions & 1 deletion daemon/clearpowerd/sources/hwmon.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ def _labelled(h, prefix):
class Hwmon:
def __init__(self):
self.thinkpad = _find("thinkpad")
self.asus = _find("asus")
self.coretemp = _find("coretemp")
self.nvme = _find("nvme")
self.tp_temps = _labelled(self.thinkpad, "temp")
self.tp_fans = _labelled(self.thinkpad, "fan")
self.asus_fans = _labelled(self.asus, "fan")
self.core_temps = _labelled(self.coretemp, "temp")
self.nvme_temps = _labelled(self.nvme, "temp")

Expand All @@ -52,7 +54,8 @@ def read(self):
if lbl in self.nvme_temps:
nvme = self._c(self.nvme_temps[lbl])
break
fans = sorted(self.tp_fans.items())
fans_map = self.tp_fans if self.tp_fans else self.asus_fans
fans = sorted(fans_map.items())
fan1 = read_int(fans[0][1]) if len(fans) > 0 else -1
fan2 = read_int(fans[1][1]) if len(fans) > 1 else -1
return {
Expand Down
98 changes: 98 additions & 0 deletions daemon/clearpowerd/sources/nvidia.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""NVIDIA GPU power and temperature readings via libnvidia-ml.so.1."""
import ctypes
import glob
import os


class NvidiaGpu:
def __init__(self):
self._dev_path = None
for d in glob.glob("/sys/bus/pci/devices/*"):
vpath = os.path.join(d, "vendor")
cpath = os.path.join(d, "class")
if os.path.exists(vpath) and os.path.exists(cpath):
try:
with open(vpath) as vf, open(cpath) as cf:
if vf.read().strip() == "0x10de" and cf.read().strip().startswith("0x03"):
self._dev_path = d
break
except OSError:
pass
self._rpath = os.path.join(self._dev_path, "power", "runtime_status") if self._dev_path else None
self._nvml = None
self._dev = None
self._last_power_w = 0.0
self._max_power_w = 150.0
self.available = self._dev_path is not None

def is_suspended(self):
if not self._rpath or not os.path.exists(self._rpath):
return False
try:
with open(self._rpath) as f:
return f.read().strip() == "suspended"
except OSError:
return False

def _ensure_nvml(self):
if self._dev is not None:
return True
try:
self._nvml = ctypes.CDLL("libnvidia-ml.so.1")
self._nvml.nvmlInit.restype = ctypes.c_int
self._nvml.nvmlDeviceGetHandleByIndex.argtypes = [ctypes.c_uint, ctypes.POINTER(ctypes.c_void_p)]
self._nvml.nvmlDeviceGetHandleByIndex.restype = ctypes.c_int
self._nvml.nvmlDeviceGetPowerUsage.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint)]
self._nvml.nvmlDeviceGetPowerUsage.restype = ctypes.c_int
self._nvml.nvmlDeviceGetTemperature.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint)]
self._nvml.nvmlDeviceGetTemperature.restype = ctypes.c_int
if self._nvml.nvmlInit() == 0:
self._dev = ctypes.c_void_p()
if self._nvml.nvmlDeviceGetHandleByIndex(0, ctypes.byref(self._dev)) == 0:
try:
pmin = ctypes.c_uint()
pmax = ctypes.c_uint()
self._nvml.nvmlDeviceGetPowerManagementLimitConstraints.argtypes = [
ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint)
]
self._nvml.nvmlDeviceGetPowerManagementLimitConstraints.restype = ctypes.c_int
if self._nvml.nvmlDeviceGetPowerManagementLimitConstraints(self._dev, ctypes.byref(pmin), ctypes.byref(pmax)) == 0:
self._max_power_w = max(pmax.value / 1000.0 * 1.2, 100.0)
except Exception:
pass
return True
except Exception:
pass
self._dev = None
return False

def read(self):
if not self.available:
return {"gpu_power_w": 0.0, "temp_gpu": -1.0}
# If suspended in D3cold/D3hot, do NOT touch NVML to avoid waking the card up!
if self.is_suspended():
self._last_power_w = 0.0
return {"gpu_power_w": 0.0, "temp_gpu": -1.0}
if not self._ensure_nvml():
return {"gpu_power_w": 0.0, "temp_gpu": -1.0}
power_w = 0.0
temp_gpu = -1.0
try:
mw = ctypes.c_uint()
if self._nvml.nvmlDeviceGetPowerUsage(self._dev, ctypes.byref(mw)) == 0:
val_w = mw.value / 1000.0
# Filter out EC/SBIOS phantom reading glitch (e.g. 752W / 753W on mobile GPUs)
if 0.0 <= val_w <= self._max_power_w:
power_w = val_w
self._last_power_w = val_w
else:
power_w = self._last_power_w
except Exception:
pass
try:
temp = ctypes.c_uint()
if self._nvml.nvmlDeviceGetTemperature(self._dev, 0, ctypes.byref(temp)) == 0:
temp_gpu = float(temp.value)
except Exception:
pass
return {"gpu_power_w": power_w, "temp_gpu": temp_gpu}
2 changes: 2 additions & 0 deletions daemon/clearpowerd/sources/rapl.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def __init__(self):
if not name:
continue
key = name.split("-")[0] # package-0 -> package
if key == "psys" and read_int(os.path.join(d, "enabled")) == 0:
continue
self.domains[key] = d
self._wrap = {k: read_int(os.path.join(p, "max_energy_range_uj")) or (1 << 32)
for k, p in self.domains.items()}
Expand Down