The laptop model's hardware module and a memory-pressure module for any machine (hq research 027/03, 026/05, to-be 42 phase 3). The predecessor's polling auto-profile and mem-guard user scripts become each module's own Go code launched by the node runtime (ADR 0198): a profile switcher woken by the kernel's power-supply uevents, and a guard that warns on RAM, swap or PSI before systemd-oomd acts, on the desktop over the account's bus and always as an event. supergfxctl and triggerhappy are kept as found (research 027 Q1).
138 lines
4.8 KiB
Go
138 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The policy, as constants until the mesh has settings a module can read (novox/hq issue 168). The
|
|
// values are the predecessor's, made explicit, and two of its behaviours are changed on purpose:
|
|
//
|
|
// - **Sustained, not momentary.** The predecessor boosted on one five-second sample above 50 %: a
|
|
// compile's first second, a browser's tab restore. Here the load must stay above the line for
|
|
// SustainSamples samples in a row, and below the lower line as long, before the profile moves.
|
|
// - **Waiting on a disk is not load.** iowait is counted as idle: a machine stalled on its SSD does
|
|
// not get faster with a higher power limit, only hotter.
|
|
const (
|
|
ProfileOnBattery = "Quiet"
|
|
ProfileOnAC = "Balanced"
|
|
ProfileUnderLoad = "Performance"
|
|
|
|
CPUHighPercent = 50.0 // on mains, sustained at or above this boosts to ProfileUnderLoad
|
|
CPULowPercent = 20.0 // and sustained at or below this goes back to ProfileOnAC
|
|
|
|
SampleEvery = 10 * time.Second // CPU is sampled only on mains; on battery nothing is sampled
|
|
SustainSamples = 3 // 30 s above CPUHighPercent to boost
|
|
RelaxSamples = 3 // 30 s below CPULowPercent to relax
|
|
|
|
// SafetyRecheck is how often the power source is read when no event has said it changed: the
|
|
// kernel's event is the trigger, and this only covers one lost across a suspend.
|
|
SafetyRecheck = 5 * time.Minute
|
|
|
|
// DefaultHold is how long a profile chosen through the profile tool is kept before the switcher
|
|
// may move it again. A change of power source ends a hold at once.
|
|
DefaultHold = 60 * time.Minute
|
|
|
|
// ChargeLimitPercent is the battery charge limit the module asserts through asusd at start.
|
|
ChargeLimitPercent = 80
|
|
)
|
|
|
|
// Policy is the switcher's memory of recent load: how many samples in a row were above the upper line
|
|
// or below the lower one, and whether it is boosted.
|
|
type Policy struct {
|
|
Boosted bool `json:"boosted"`
|
|
Above int `json:"samples_above"`
|
|
Below int `json:"samples_below"`
|
|
Recent []float64 `json:"recent_cpu_percent"`
|
|
BoostedSince time.Time `json:"boosted_since,omitempty"`
|
|
}
|
|
|
|
// Observe takes one CPU sample (busy percent since the previous one) taken on mains.
|
|
func (p *Policy) Observe(cpu float64, at time.Time) {
|
|
p.Recent = append(p.Recent, round1(cpu))
|
|
if len(p.Recent) > 6 {
|
|
p.Recent = p.Recent[len(p.Recent)-6:]
|
|
}
|
|
switch {
|
|
case cpu >= CPUHighPercent:
|
|
p.Above++
|
|
p.Below = 0
|
|
if !p.Boosted && p.Above >= SustainSamples {
|
|
p.Boosted = true
|
|
p.BoostedSince = at
|
|
}
|
|
case cpu <= CPULowPercent:
|
|
p.Below++
|
|
p.Above = 0
|
|
if p.Boosted && p.Below >= RelaxSamples {
|
|
p.Boosted = false
|
|
p.BoostedSince = time.Time{}
|
|
}
|
|
default:
|
|
// Between the lines: no direction is sustained, and the profile stays where it is.
|
|
p.Above, p.Below = 0, 0
|
|
}
|
|
}
|
|
|
|
// Reset forgets the load, for a change of power source.
|
|
func (p *Policy) Reset() { *p = Policy{} }
|
|
|
|
// Decision is what the switcher would choose, and why.
|
|
type Decision struct {
|
|
Profile string `json:"profile"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// Decide is the policy: battery → ProfileOnBattery; mains → ProfileOnAC, or ProfileUnderLoad while
|
|
// boosted.
|
|
func (p *Policy) Decide(src Source) Decision {
|
|
if !src.OnAC {
|
|
return Decision{ProfileOnBattery, "on battery (" + src.Reason + ")"}
|
|
}
|
|
if p.Boosted {
|
|
return Decision{ProfileUnderLoad, fmt.Sprintf("on mains (%s) and CPU load sustained at or above %s%% for %d samples of %s",
|
|
src.Reason, strconv.FormatFloat(CPUHighPercent, 'f', -1, 64), SustainSamples, SampleEvery)}
|
|
}
|
|
return Decision{ProfileOnAC, fmt.Sprintf("on mains (%s), and CPU load not sustained at or above %s%%",
|
|
src.Reason, strconv.FormatFloat(CPUHighPercent, 'f', -1, 64))}
|
|
}
|
|
|
|
// CPUTimes is the first line of /proc/stat: total and idle jiffies (iowait counted as idle).
|
|
type CPUTimes struct{ Total, Idle uint64 }
|
|
|
|
// ParseProcStat reads the aggregate cpu line of /proc/stat.
|
|
func ParseProcStat(text string) (CPUTimes, error) {
|
|
line := strings.SplitN(text, "\n", 2)[0]
|
|
f := strings.Fields(line)
|
|
if len(f) < 6 || f[0] != "cpu" {
|
|
return CPUTimes{}, fmt.Errorf("/proc/stat does not start with the cpu line")
|
|
}
|
|
var t CPUTimes
|
|
for i, s := range f[1:] {
|
|
if i >= 8 { // user nice system idle iowait irq softirq steal; guest is already in user
|
|
break
|
|
}
|
|
n, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return CPUTimes{}, fmt.Errorf("/proc/stat: %v", err)
|
|
}
|
|
t.Total += n
|
|
if i == 3 || i == 4 {
|
|
t.Idle += n
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// Busy is the percentage of time not idle between two readings.
|
|
func Busy(before, after CPUTimes) (float64, bool) {
|
|
if after.Total <= before.Total {
|
|
return 0, false
|
|
}
|
|
total := float64(after.Total - before.Total)
|
|
idle := float64(after.Idle - before.Idle)
|
|
return (total - idle) / total * 100, true
|
|
}
|