Mitigating WiFi upload speed issues on Lenovo IdeaPad S340 running Linux

By | August 3, 2019

I have a Lenovo IdeaPad S340 laptop with an Intel WiFi adapter (Intel Dual Band Wireless AC 9462, to be precise) running Ubuntu Linux 19.04. When I use most WiFi networks, download and upload speeds are both fine, but on one network in particular (which uses WiFi access points from Cisco), my upload speed is atrocious, less than 1Mbps even when my phone connected to the same network gets upload speeds of more than 50Mbps.

I Googled around trying to find other people with similar issues and how they were able to solve them, and I found a lot of people recommending changing the configuration of the iwlwifi kernel module to specify 11n_disable=1 or 11n_disable=8 to solve the problem. I tried them both:

  • 11n_disable=1 improved my upload speed, but it also cut my download speed in half, which is obviously unacceptable.
  • 11n_disable=8 didn’t improve my upload speed.

So no luck there. However, with further testing, I discovered that 11n_disable=2 makes my upload speed seven times faster. At first glance, then, that would seem to be the solution, but alas, it’s not that simple.

Unfortunately, in addition to improving my upload speed, that setting also decreases my download speed, by about 12%. That’s an acceptable trade-off on a WiFi network whose upload speed is otherwise unusable, but it also decreases my upload speed on WiFi networks that don’t have upload speed issues. I don’t want to make my download speed 12% slower when I don’t need to.

Alas, 11n_disable is not something you can tweak in the settings for a particular WiFi network. It’s a kernel module setting that can only be set when the kernel module is loaded. To change the setting, therefore, you have to unload the kernel module (thus taking down your WiFi connection) and then reload it with the new setting.

I therefore decided to install a script which runs whenever I connect to a WiFi network, determines based on the name of the network whether 11n_disable needs to be changed, and does the needful:

11n_disable.sh script

#!/bin/bash -e

WHOAMI=$(basename $0)
IFACE="$1"; shift
ACTION="$1"; shift

log() {
    level="$1"; shift

    logger -p daemon.$level -t "$WHOAMI" [email protected]
}

if [ "$ACTION" != "up" ]; then
    log debug ignoring action $ACTION
    exit 0
fi

state=$(cat /sys/module/iwlwifi/parameters/11n_disable)

log notice previous 11n_disable state is $state

if [ "$CONNECTION_ID" = "bad-wifi-network-name" ]; then
    want_state=2
else
    want_state=0
fi

if [ "$state" != "$want_state" ]; then
    log notice reloading iwlwifi with 11n_disable=$want_state
    if ! rmmod iwlmvm iwlwifi; then
        log err rmmod iwlmvm iwlwifi failed
        exit 1
    fi
    if ! modprobe iwlwifi 11n_disable=$want_state; then
        log err modprobe iwlwifi 11n_disable=$want_state failed
        exit 1
    fi
    if ! modprobe iwlmvm; then
        log err modprobe iwlmvm failed
        exit 1
    fi
    log notice finished reloading iwlwifi with 11n_disable=$want_state
else
    log notice 11n_disable is correct, taking no action
fi

This script is installed on my laptop as /etc/NetworkManager/dispatcher.d/02-11n_disable.sh, owned by root, mode 0755. Obviously on my laptop I’ve replaced bad-wifi-network-name with the name of the network with the bad upload speed.

This script is run automatically by NetworkManager whenever a network is connected (actually, it’s run more often than that, but the script checks the action specified on the command line and exits if it isn’t “up”).

The script determines what value we want 11n_disable to have based on the name of the WiFi network, checks if the current value matches what we want, and if not, unloads the relevant kernel modules and then reloads them with the desired value.

There is one potential problem with the script which might impact you. You could end up flapping between different WiFi networks if you have multiple networks in range and the script is configured to use different 11n_disable values for different networks. Caveat emptor.

By the way, here’s a different script I put together which automates the process of testing which 11n_disable setting is the most performant. Perhaps you will find it useful.

iwlwifi_11n_tester.py script

#!/usr/bin/env python3

# This script tests all the possible values for the 11n_disable
# parameter of the iwlwifi kernel module to determine which value
# gives you the best performance for any particular WiFi network.
#
# The script will bounce your WiFi up and down many times while it is
# running. It assumes you're using NetworkManager. It also assumes you
# have only one WiFi device.
#
# You need to have speedtest-cli installed for the script to work.
#
# If you don't specify a WiFi network name on the command line, then
# the script will just assume that whatever network gets connected to
# automatically when WiFi is enabled is the one you're testing.
#
# When the script is done running it will do one final rest of
# 11n_disable to whatever value it determined was the best.

import argparse
import csv
import io
import re
import requests
import statistics
import subprocess
import time

default_iterations_per_value = 1


def parse_args():
    parser = argparse.ArgumentParser(description="Test all 11n_disable values "
                                     "for iwlwifi to find the best one")
    parser.add_argument("--wifi-network", action="store", help="WiFi network "
                        "to test (if not specified, whichever network is "
                        "is connected to automatically is used)")
    parser.add_argument("--iterations-per-value", type=int, action="store",
                        default=default_iterations_per_value,
                        help="How many iterations of the test to do for each "
                        "value of 11n_disable (default is {}, increase for "
                        "more accurate results)".format(
                            default_iterations_per_value))
    return parser.parse_args()


def get_csv_header():
    output = subprocess.check_output(["speedtest-cli", "--csv-header"]).\
        decode('utf-8')
    f = io.StringIO(output)
    reader = csv.reader(f)
    row = next(reader)
    return row


def wait_for_wifi():
    print("Waiting for WiFi to come back up")
    while True:
        output = subprocess.check_output(["nmcli", "c", "show", "--active"]).\
            decode('utf-8')
        match = re.search(r'\n(.*\S)\s+\S+\s+wifi\s+(\S+)', output)
        if match:
            break
        time.sleep(1)
    return(match.group(1), match.group(2))


def reset_wifi(value, network):
    print("Resetting WiFi with 11n_disable={}".format(value))
    subprocess.check_call(["sudo", "rmmod", "iwlmvm"])
    subprocess.check_call(["sudo", "rmmod", "iwlwifi"])
    subprocess.check_call(["sudo", "modprobe", "iwlwifi",
                           "11n_disable={}".format(value)])
    subprocess.check_call(["sudo", "modprobe", "iwlmvm"])
    connected_network, device = wait_for_wifi()
    if network and connected_network != network:
        print("Reconnecting to WiFi network {}".format(network))
        subprocess.check_call(["nmcli", "d", "disconnect", device])
        subprocess.check_call(["nmcli", "d", "wifi", "connect", network])
        wait_for_wifi()
    # A delay is needed for the network to "settle"
    while True:
        try:
            response = requests.get('http://neverssl.com')
        except requests.exceptions.ConnectionError:
            status_code = 500
        else:
            status_code = response.status_code
        if status_code == 200:
            break
        time.sleep(1)


def test_11n_disable_value(value, args, rows):
    reset_wifi(value, args.wifi_network)
    for iteration in range(args.iterations_per_value):
        do_iteration(value, iteration, args, rows)


def do_iteration(value, iteration, args, rows):
    print("Running iteration {} of {} for 11n_disable={}".format(
        iteration + 1, args.iterations_per_value, value))
    output = subprocess.check_output(["speedtest-cli", "--csv"]).\
        decode("utf-8")
    f = io.StringIO(output)
    reader = csv.reader(f)
    row = next(reader)
    download = int(float(row[args.download_index]))
    upload = int(float(row[args.upload_index]))
    print("  11n_disable={}, download={}, upload={}".format(
        value, download, upload))
    rows.append([value, download, upload])


def analyze_results(args, rows):
    download_speeds = []
    upload_speeds = []
    for value in range(8):
        try:
            download_speeds.append(statistics.mean(
                r[1] for r in rows if r[0] == value))
        except statistics.StatisticsError:
            # We haven't done that value yet.
            break
        upload_speeds.append(statistics.mean(
            r[2] for r in rows if r[0] == value))
    best_download = max(download_speeds)
    best_upload = max(upload_speeds)
    overall_scores = []
    for value in range(len(download_speeds)):
        overall_scores.append(download_speeds[value] / best_download +
                              upload_speeds[value] / best_upload)
    best_overall = max(overall_scores)
    for value in range(len(download_speeds)):
        download_pct = int(100 * download_speeds[value] / best_download)
        upload_pct = int(100 * upload_speeds[value] / best_upload)
        print("11n_disable={} has {}% of best download speed, {}% of best "
              "upload speed".format(value, download_pct, upload_pct))
    best_overall_value = next(v for v in range(len(overall_scores))
                              if overall_scores[v] == best_overall)
    print("Best overall is 11n_disable={}".format(best_overall_value))
    reset_wifi(best_overall_value, args.wifi_network)


def main():
    args = parse_args()
    header = get_csv_header()
    args.download_index = next(i for i in range(len(header))
                               if header[i] == 'Download')
    args.upload_index = next(i for i in range(len(header))
                             if header[i] == 'Upload')
    rows = []
    for value in range(8):
        test_11n_disable_value(value, args, rows)
    analyze_results(args, rows)


if __name__ == '__main__':
    main()

Comment below if you find this useful!

Print Friendly, PDF & Email
Share

Leave a Reply

Your email address will not be published.