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 }