Monday, 22 January 2018

T440p, Haswell CPUs, bad battery life and CPU frequency-scaling

You have yourself a Haswell CPU (mine is a 4710MQ, but 4600, 4700, 4800 {MQ,HQ} et. al. will probably also apply), and you're running the latest LTS release (16.04) of Ubuntu (I'm actually running Xubuntu), and what you've found is that your batter life is terrible. I should be getting around 4h30m on this (6-cell) battery, but I get more like 1h50m. The reason? CPU frequency scaling.

The problem is that the kernel shipped with *some* 16.04 installations (depends on whether you've installed the refreshed 16.04.[123] LTS or not) have a bug in them that completely ruins the power saving of your laptop.

This is a plot of 70 seconds worth of data from scaling_cur_freq while my laptop is completely idle on the desktop (not a single core goes above 0.3% during this time):

X-axis: second, Y-axis: clock in khz

You can see all the makings of a screwy frequency-scaling algorithm. This is actually a well-known bug, but let's just cut to the chase. The problem is with the intel_pstate frequency-scaling driver. In some kernel versions that were shipped with 16.04 LTS, the intel_pstate actually pushes the CPU into turbo clocks from the most minor of activity.

The solution? Disable intel_pstate. Add intel_pstate=disable to /etc/default/grub.conf such that it looks something like this:

$ cat /etc/default/grub
# If you change this file, run 'update-grub' afterwards to update
# /boot/grub/grub.cfg.
# For full documentation of the options in this file, see:
#   info -f grub -n 'Simple configuration'

GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`

# Uncomment to enable BadRAM filtering, modify to suit your needs
# This works with Linux (no patch required) and with any kernel that obtains
# the memory map information from GRUB (GNU Mach, kernel of FreeBSD ...)

# Uncomment to disable graphical terminal (grub-pc only)

# The resolution used on graphical terminal
# note that you can use only modes which your graphic card supports via VBE
# you can see them in real GRUB with the command `vbeinfo'

# Uncomment if you don't want GRUB to pass "root=UUID=xxx" parameter to Linux

# Uncomment to disable generation of recovery mode menu entries

# Uncomment to get a beep at grub start
#GRUB_INIT_TUNE="480 440 1"

Then update grub:

$ sudo update-grub
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-4.4.0-109-generic
Found initrd image: /boot/initrd.img-4.4.0-109-generic
Found linux image: /boot/vmlinuz-4.4.0-101-generic
Found initrd image: /boot/initrd.img-4.4.0-101-generic
Found linux image: /boot/vmlinuz-4.4.0-87-generic
Found initrd image: /boot/initrd.img-4.4.0-87-generic
Found linux image: /boot/vmlinuz-4.4.0-36-generic
Found initrd image: /boot/initrd.img-4.4.0-36-generic
Found memtest86+ image: /boot/memtest86+.elf
Found memtest86+ image: /boot/memtest86+.bin

Now reboot.

Your scaling driver will now be the acpi-cpufreq one. This one does a much better job of scaling the CPU frequencies than the buggy intel_pstate. You should get significant battery savings out of this change just by itself. If you're happy with what you have now, then feel free to quit reading now.

Who wants a slightly more tuned solution? Read on.

By default Ubuntu puts us into the ondemand governor:

$ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

Disable it:

$ sudo systemctl disable ondemand
ondemand.service is not a native service, redirecting to systemd-sysv-install
Executing /lib/systemd/systemd-sysv-install disable ondemand
insserv: warning: current start runlevel(s) (empty) of script `ondemand' overrides LSB defaults (2 3 4 5).
insserv: warning: current stop runlevel(s) (2 3 4 5) of script `ondemand' overrides LSB defaults (empty).

Let's discover what ACPI events are generated when you unplug and re-plug your AC power supply:

$ acpi_listen
ac_adapter ACPI0003:00 00000080 00000000
ac_adapter ACPI0003:00 00000080 00000001

So we see that we have the AC events. We're not actually going to use them for anything other than a trigger.

Create /etc/acpi/ and put this inside it:

# Update cpufreq scaling-governor based on AC state.

( if [ $(cat /sys/class/power_supply/AC/online) -eq 1 ]; then
echo performance;
echo powersave;
fi ) | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

Make sure to add execute permissions to it (chmod a+x).

Create an ACPI event /etc/acpi/events/ac-adapter and put this inside it:

# /etc/acpi/events/ac-adapter
# Switch CPU frequency-scaling governor depending on AC state.

event=ac_adapter ACPI0003:00 00000080 0000000[01]

This will run the scaling-governor-selection script any time there is a change in the state of the AC power supply. But what about boot? The AC state won't change at boot and we need to have the right governor selected then, too! So we'll create a service for that.

Create a SystemD service /etc/systemd/system/cpufreq-governor.service and put this inside it:

Description=Set the CPU Frequency Scaling governor to powersave/performance, depending on AC state.



Enable it:

$ sudo systemctl daemon-reload
$ sudo systemctl enable cpufreq-governor
Created symlink from /etc/systemd/system/ to /etc/systemd/system/cpufreq-governor.service.


Now your PC will use the right scaling governor when you boot and whenever the AC power supply state changes.