Skip to content

Commit 9b2cd45

Browse files
amanuskclaude
andauthored
Narrow EPP choices reactively when a write is rejected (Intel fix) (amanusk#288)
PR amanusk#287 made the Power Profile menu refresh its EPP list after Apply, relying on the kernel updating energy_performance_available_preferences to match the active governor. amd-pstate does this, but intel_pstate doesn't: that file always lists every EPP value regardless of governor, so on Intel systems with the performance governor active, the menu kept offering choices (balance_performance, power, etc.) that the hardware silently rejects with "Device or resource busy" on Apply. Since Intel gives no dynamic listing to read, use the write's own success/failure as the signal instead: when an EPP write is rejected, trust the real current EPP value over the stale advertised list and narrow to just that value. A successful write keeps trusting the full list as before, so behavior on amd-pstate systems is unchanged. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 82fc92e commit 9b2cd45

2 files changed

Lines changed: 93 additions & 3 deletions

File tree

s_tui/power_profile_menu.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ def on_apply(self, _: object) -> None:
288288
errors.append(str(e))
289289

290290
# Apply EPP
291+
epp_write_failed = False
291292
if self.epp_controllable:
292293
epp = self._get_selected_epp()
293294
if epp:
@@ -297,19 +298,31 @@ def on_apply(self, _: object) -> None:
297298
except OSError as e:
298299
logging.debug("Failed to set EPP: %s", e)
299300
errors.append(str(e))
301+
epp_write_failed = True
300302

301303
# Refresh the UI after gov/epp change
302-
self._refresh_ui_lists()
304+
self._refresh_ui_lists(epp_write_failed)
303305

304306
if errors:
305307
self.status_text.set_text(("high temp txt", "\n".join(errors)))
306308
else:
307309
self.status_text.set_text(("bold text", "Applied successfully."))
308310

309-
def _refresh_ui_lists(self) -> None:
310-
"""Re-read available sysfs values and dynamically rebuild the UI."""
311+
def _refresh_ui_lists(self, epp_write_failed: bool = False) -> None:
312+
"""Re-read available sysfs values and dynamically rebuild the UI.
313+
314+
Some drivers (e.g. intel_pstate) don't shrink
315+
energy_performance_available_preferences to reflect the active
316+
governor the way amd-pstate does, so a rejected EPP write is the only
317+
signal we get. When the just-attempted write failed, trust the
318+
kernel's real, current EPP value over the (stale) advertised list.
319+
"""
311320
self.available_governors = read_available(SYSFS_AVAIL_GOVERNORS)
312321
self.available_epp = read_available(SYSFS_AVAIL_EPP)
322+
if epp_write_failed:
323+
current_epp = _read_current(SYSFS_EPP)
324+
if current_epp:
325+
self.available_epp = [current_epp]
313326

314327
self.governor_controllable = (
315328
self.can_write_governor and len(self.available_governors) > 1

tests/test_power_profile_menu.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,83 @@ def test_no_epp_hides_section(self):
115115
assert m.is_controllable()
116116

117117

118+
class TestEppReactiveNarrowing:
119+
"""Some drivers (e.g. intel_pstate) don't shrink
120+
energy_performance_available_preferences to reflect the active governor
121+
the way amd-pstate does. Rather than guessing driver-specific rules, a
122+
rejected EPP write is treated as the ground truth: narrow the list to
123+
the real current EPP value, and trust the advertised list again as soon
124+
as a write succeeds."""
125+
126+
def test_construction_never_restricts_epp_list(self):
127+
"""No write has been attempted yet, so show whatever sysfs advertises."""
128+
with patch(
129+
"s_tui.power_profile_menu._read_current", return_value="performance"
130+
):
131+
m = PowerProfileMenu(
132+
return_fn=MagicMock(),
133+
powerprofilesctl_exe=None,
134+
can_write_governor=True,
135+
can_write_epp=True,
136+
available_governors=GOVERNORS,
137+
available_epp=EPP_VALUES,
138+
)
139+
assert m.available_epp == EPP_VALUES
140+
141+
def test_failed_epp_write_narrows_to_current_value(self):
142+
with patch("s_tui.power_profile_menu._read_current", return_value="powersave"):
143+
m = PowerProfileMenu(
144+
return_fn=MagicMock(),
145+
powerprofilesctl_exe=None,
146+
can_write_governor=False,
147+
can_write_epp=True,
148+
available_governors=["powersave"],
149+
available_epp=EPP_VALUES,
150+
)
151+
152+
for rb in m.epp_group:
153+
rb.set_state(rb.label == "power", do_callback=False)
154+
155+
with (
156+
patch(
157+
"s_tui.power_profile_menu._write_all_cores",
158+
side_effect=OSError("Device or resource busy"),
159+
),
160+
patch("s_tui.power_profile_menu.read_available", return_value=EPP_VALUES),
161+
patch("s_tui.power_profile_menu._read_current", return_value="performance"),
162+
):
163+
m.on_apply(None)
164+
165+
assert m.available_epp == ["performance"]
166+
assert len(m.epp_buttons) == 1
167+
status = m.status_text.get_text()[0]
168+
assert "busy" in status.lower()
169+
170+
def test_successful_epp_write_keeps_full_list(self):
171+
with patch("s_tui.power_profile_menu._read_current", return_value="powersave"):
172+
m = PowerProfileMenu(
173+
return_fn=MagicMock(),
174+
powerprofilesctl_exe=None,
175+
can_write_governor=False,
176+
can_write_epp=True,
177+
available_governors=["powersave"],
178+
available_epp=EPP_VALUES,
179+
)
180+
181+
for rb in m.epp_group:
182+
rb.set_state(rb.label == "power", do_callback=False)
183+
184+
with (
185+
patch("s_tui.power_profile_menu._write_all_cores"),
186+
patch("s_tui.power_profile_menu.read_available", return_value=EPP_VALUES),
187+
patch("s_tui.power_profile_menu._read_current", return_value="powersave"),
188+
):
189+
m.on_apply(None)
190+
191+
assert m.available_epp == EPP_VALUES
192+
assert len(m.epp_buttons) == len(EPP_VALUES)
193+
194+
118195
# =====================================================================
119196
# get_size
120197
# =====================================================================

0 commit comments

Comments
 (0)