Contributors: 14
Author Tokens Token Proportion Commits Commit Proportion
Meng Li 850 34.94% 3 9.09%
Gautham R. Shenoy 589 24.21% 2 6.06%
Mario Limonciello 409 16.81% 13 39.39%
Mario Limonciello (AMD) 401 16.48% 1 3.03%
K Prateek Nayak 99 4.07% 2 6.06%
Swapnil Sapkal 33 1.36% 1 3.03%
Perry Yuan 18 0.74% 1 3.03%
Wyes Karny 13 0.53% 1 3.03%
Huang Rui 9 0.37% 2 6.06%
Ingo Molnar 3 0.12% 2 6.06%
Dave Jones 3 0.12% 2 6.06%
Nathan Chancellor 2 0.08% 1 3.03%
Arnd Bergmann 2 0.08% 1 3.03%
Qianqiang Liu 2 0.08% 1 3.03%
Total 2433 33


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * AMD Processor P-state Frequency Driver Unit Test
 *
 * Copyright (C) 2022 Advanced Micro Devices, Inc. All Rights Reserved.
 *
 * Author: Meng Li <li.meng@amd.com>
 *
 * The AMD P-State Unit Test is a test module for testing the amd-pstate
 * driver. 1) It can help all users to verify their processor support
 * (SBIOS/Firmware or Hardware). 2) Kernel can have a basic function
 * test to avoid the kernel regression during the update. 3) We can
 * introduce more functional or performance tests to align the result
 * together, it will benefit power and performance scale optimization.
 *
 * This driver implements basic framework with plans to enhance it with
 * additional test cases to improve the depth and coverage of the test.
 *
 * See Documentation/admin-guide/pm/amd-pstate.rst Unit Tests for
 * amd-pstate to get more detail.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/bitfield.h>
#include <linux/cpufeature.h>
#include <linux/cpufreq.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/cleanup.h>

#include <acpi/cppc_acpi.h>

#include <asm/msr.h>

#include "amd-pstate.h"

static char *test_list;
module_param(test_list, charp, 0444);
MODULE_PARM_DESC(test_list,
	"Comma-delimited list of tests to run (empty means run all tests)");
DEFINE_FREE(cleanup_page, void *, if (_T) free_page((unsigned long)_T))

struct amd_pstate_ut_struct {
	const char *name;
	int (*func)(u32 index);
};

/*
 * Kernel module for testing the AMD P-State unit test
 */
static int amd_pstate_ut_acpi_cpc_valid(u32 index);
static int amd_pstate_ut_check_enabled(u32 index);
static int amd_pstate_ut_check_perf(u32 index);
static int amd_pstate_ut_check_freq(u32 index);
static int amd_pstate_ut_epp(u32 index);
static int amd_pstate_ut_check_driver(u32 index);
static int amd_pstate_ut_check_freq_attrs(u32 index);

static struct amd_pstate_ut_struct amd_pstate_ut_cases[] = {
	{"amd_pstate_ut_acpi_cpc_valid",    amd_pstate_ut_acpi_cpc_valid   },
	{"amd_pstate_ut_check_enabled",     amd_pstate_ut_check_enabled    },
	{"amd_pstate_ut_check_perf",        amd_pstate_ut_check_perf       },
	{"amd_pstate_ut_check_freq",        amd_pstate_ut_check_freq       },
	{"amd_pstate_ut_epp",               amd_pstate_ut_epp              },
	{"amd_pstate_ut_check_driver",      amd_pstate_ut_check_driver     },
	{"amd_pstate_ut_check_freq_attrs",  amd_pstate_ut_check_freq_attrs },
};

static bool test_in_list(const char *list, const char *name)
{
	size_t name_len = strlen(name);
	const char *p = list;

	while (*p) {
		const char *sep = strchr(p, ',');
		size_t token_len = sep ? sep - p : strlen(p);

		if (token_len == name_len && !strncmp(p, name, token_len))
			return true;
		if (!sep)
			break;
		p = sep + 1;
	}

	return false;
}

static bool get_shared_mem(void)
{
	bool result = false;

	if (!boot_cpu_has(X86_FEATURE_CPPC))
		result = true;

	return result;
}

/*
 * check the _CPC object is present in SBIOS.
 */
static int amd_pstate_ut_acpi_cpc_valid(u32 index)
{
	if (!acpi_cpc_valid()) {
		pr_err("%s the _CPC object is not present in SBIOS!\n", __func__);
		return -EINVAL;
	}

	return 0;
}

/*
 * check if amd pstate is enabled
 */
static int amd_pstate_ut_check_enabled(u32 index)
{
	u64 cppc_enable = 0;
	int ret;

	if (get_shared_mem())
		return 0;

	ret = rdmsrq_safe(MSR_AMD_CPPC_ENABLE, &cppc_enable);
	if (ret) {
		pr_err("%s rdmsrq_safe MSR_AMD_CPPC_ENABLE ret=%d error!\n", __func__, ret);
		return ret;
	}

	if (!cppc_enable) {
		pr_err("%s amd pstate must be enabled!\n", __func__);
		return -EINVAL;
	}

	return 0;
}

/*
 * check if performance values are reasonable.
 * highest_perf >= nominal_perf > lowest_nonlinear_perf > lowest_perf > 0
 */
static int amd_pstate_ut_check_perf(u32 index)
{
	int cpu = 0, ret = 0;
	u32 highest_perf = 0, nominal_perf = 0, lowest_nonlinear_perf = 0, lowest_perf = 0;
	u64 cap1 = 0;
	struct cppc_perf_caps cppc_perf;
	union perf_cached cur_perf;

	for_each_online_cpu(cpu) {
		struct cpufreq_policy *policy __free(put_cpufreq_policy) = NULL;
		struct amd_cpudata *cpudata;

		policy = cpufreq_cpu_get(cpu);
		if (!policy)
			continue;
		cpudata = policy->driver_data;

		if (get_shared_mem()) {
			ret = cppc_get_perf_caps(cpu, &cppc_perf);
			if (ret) {
				pr_err("%s cppc_get_perf_caps ret=%d error!\n", __func__, ret);
				return ret;
			}

			highest_perf = cppc_perf.highest_perf;
			nominal_perf = cppc_perf.nominal_perf;
			lowest_nonlinear_perf = cppc_perf.lowest_nonlinear_perf;
			lowest_perf = cppc_perf.lowest_perf;
		} else {
			ret = rdmsrq_safe_on_cpu(cpu, MSR_AMD_CPPC_CAP1, &cap1);
			if (ret) {
				pr_err("%s read CPPC_CAP1 ret=%d error!\n", __func__, ret);
				return ret;
			}

			highest_perf = FIELD_GET(AMD_CPPC_HIGHEST_PERF_MASK, cap1);
			nominal_perf = FIELD_GET(AMD_CPPC_NOMINAL_PERF_MASK, cap1);
			lowest_nonlinear_perf = FIELD_GET(AMD_CPPC_LOWNONLIN_PERF_MASK, cap1);
			lowest_perf = FIELD_GET(AMD_CPPC_LOWEST_PERF_MASK, cap1);
		}

		cur_perf = READ_ONCE(cpudata->perf);
		if (highest_perf != cur_perf.highest_perf && !cpudata->hw_prefcore) {
			pr_err("%s cpu%d highest=%d %d highest perf doesn't match\n",
				__func__, cpu, highest_perf, cur_perf.highest_perf);
			return -EINVAL;
		}
		if (nominal_perf != cur_perf.nominal_perf ||
		   (lowest_nonlinear_perf != cur_perf.lowest_nonlinear_perf) ||
		   (lowest_perf != cur_perf.lowest_perf)) {
			pr_err("%s cpu%d nominal=%d %d lowest_nonlinear=%d %d lowest=%d %d, they should be equal!\n",
				__func__, cpu, nominal_perf, cur_perf.nominal_perf,
				lowest_nonlinear_perf, cur_perf.lowest_nonlinear_perf,
				lowest_perf, cur_perf.lowest_perf);
			return -EINVAL;
		}

		if (!((highest_perf >= nominal_perf) &&
			(nominal_perf > lowest_nonlinear_perf) &&
			(lowest_nonlinear_perf >= lowest_perf) &&
			(lowest_perf > 0))) {
			pr_err("%s cpu%d highest=%d >= nominal=%d > lowest_nonlinear=%d > lowest=%d > 0, the formula is incorrect!\n",
				__func__, cpu, highest_perf, nominal_perf,
				lowest_nonlinear_perf, lowest_perf);
			return -EINVAL;
		}
	}

	return 0;
}

/*
 * Check if frequency values are reasonable.
 * max_freq >= nominal_freq > lowest_nonlinear_freq > min_freq > 0
 * check max freq when set support boost mode.
 */
static int amd_pstate_ut_check_freq(u32 index)
{
	int cpu = 0;

	for_each_online_cpu(cpu) {
		struct cpufreq_policy *policy __free(put_cpufreq_policy) = NULL;
		struct amd_cpudata *cpudata;

		policy = cpufreq_cpu_get(cpu);
		if (!policy)
			continue;
		cpudata = policy->driver_data;

		if (!((policy->cpuinfo.max_freq >= cpudata->nominal_freq) &&
			(cpudata->nominal_freq > cpudata->lowest_nonlinear_freq) &&
			(cpudata->lowest_nonlinear_freq >= policy->cpuinfo.min_freq) &&
			(policy->cpuinfo.min_freq > 0))) {
			pr_err("%s cpu%d max=%d >= nominal=%d > lowest_nonlinear=%d > min=%d > 0, the formula is incorrect!\n",
				__func__, cpu, policy->cpuinfo.max_freq, cpudata->nominal_freq,
				cpudata->lowest_nonlinear_freq, policy->cpuinfo.min_freq);
			return -EINVAL;
		}

		if (cpudata->lowest_nonlinear_freq != policy->min) {
			pr_err("%s cpu%d cpudata_lowest_nonlinear_freq=%d policy_min=%d, they should be equal!\n",
				__func__, cpu, cpudata->lowest_nonlinear_freq, policy->min);
			return -EINVAL;
		}

		if (cpudata->boost_supported) {
			if ((policy->max != policy->cpuinfo.max_freq) &&
			    (policy->max != cpudata->nominal_freq)) {
				pr_err("%s cpu%d policy_max=%d should be equal cpu_max=%d or cpu_nominal=%d !\n",
					__func__, cpu, policy->max, policy->cpuinfo.max_freq,
					cpudata->nominal_freq);
				return -EINVAL;
			}
		} else {
			pr_err("%s cpu%d must support boost!\n", __func__, cpu);
			return -EINVAL;
		}
	}

	return 0;
}

static int amd_pstate_set_mode(enum amd_pstate_mode mode)
{
	const char *mode_str = amd_pstate_get_mode_string(mode);

	pr_debug("->setting mode to %s\n", mode_str);

	return amd_pstate_update_status(mode_str, strlen(mode_str));
}

static int amd_pstate_ut_epp(u32 index)
{
	static const char * const epp_strings[] = {
		"power",
		"balance_power",
		"balance_performance",
		"performance",
	};
	char *buf __free(cleanup_page) = NULL;
	struct cpufreq_policy *policy = NULL;
	enum amd_pstate_mode orig_mode;
	struct amd_cpudata *cpudata;
	unsigned long orig_policy;
	bool orig_dynamic_epp;
	int ret, cpu = 0;
	u16 epp;
	int i;

	policy = cpufreq_cpu_get(cpu);
	if (!policy)
		return -ENODEV;

	cpudata = policy->driver_data;
	orig_mode = amd_pstate_get_status();
	orig_dynamic_epp = cpudata->dynamic_epp;

	/* Drop reference before potential driver change. */
	cpufreq_cpu_put(policy);
	policy = NULL;

	buf = (char *)__get_free_page(GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	ret = amd_pstate_set_mode(AMD_PSTATE_ACTIVE);
	if (ret)
		goto out;

	policy = cpufreq_cpu_get(cpu);
	if (!policy) {
		ret = -ENODEV;
		goto out;
	}

	down_write(&policy->rwsem);
	cpudata = policy->driver_data;
	orig_policy = cpudata->policy;
	cpudata->policy = CPUFREQ_POLICY_POWERSAVE;

	/*
	 * Disable dynamic EPP before running test. If "orig_dynamic_epp" is
	 * true, the  driver will do a redundant switch at the end and there
	 * is no need for enabling it again at the end of the test.
	 */
	if (cpudata->dynamic_epp) {
		pr_debug("Dynamic EPP is enabled, disabling it\n");
		amd_pstate_clear_dynamic_epp(policy);
	}

	for (epp = 0; epp <= U8_MAX; epp++) {
		u8 val;

		/* write all EPP values */
		memset(buf, 0, PAGE_SIZE);
		snprintf(buf, PAGE_SIZE, "%d", epp);
		ret = store_energy_performance_preference(policy, buf, strlen(buf));
		if (ret < 0)
			goto out;

		/* check if the EPP value reads back correctly for raw numbers */
		memset(buf, 0, PAGE_SIZE);
		ret = show_energy_performance_preference(policy, buf);
		if (ret < 0)
			goto out;
		strreplace(buf, '\n', '\0');
		ret = kstrtou8(buf, 0, &val);
		if (!ret && epp != val) {
			pr_err("Raw EPP value mismatch: %d != %d\n", epp, val);
			ret = -EINVAL;
			goto out;
		}
	}

	for (i = 0; i < ARRAY_SIZE(epp_strings); i++) {
		memset(buf, 0, PAGE_SIZE);
		snprintf(buf, PAGE_SIZE, "%s", epp_strings[i]);
		ret = store_energy_performance_preference(policy, buf, strlen(buf));
		if (ret < 0)
			goto out;

		memset(buf, 0, PAGE_SIZE);
		ret = show_energy_performance_preference(policy, buf);
		if (ret < 0)
			goto out;
		strreplace(buf, '\n', '\0');

		if (strcmp(buf, epp_strings[i])) {
			pr_err("String EPP value mismatch: %s != %s\n", buf, epp_strings[i]);
			ret = -EINVAL;
			goto out;
		}
	}

	ret = 0;

out:
	if (policy) {
		cpudata->policy = orig_policy;
		up_write(&policy->rwsem);
		cpufreq_cpu_put(policy);
	}

	if (orig_dynamic_epp) {
		int ret2;

		ret2 = amd_pstate_set_mode(AMD_PSTATE_DISABLE);
		if (!ret && ret2)
			ret = ret2;
	}

	if (orig_mode != amd_pstate_get_status()) {
		int ret2;

		ret2 = amd_pstate_set_mode(orig_mode);
		if (!ret && ret2)
			ret = ret2;
	}

	return ret;
}

static int amd_pstate_ut_check_driver(u32 index)
{
	enum amd_pstate_mode mode1, mode2 = AMD_PSTATE_DISABLE;
	enum amd_pstate_mode orig_mode = amd_pstate_get_status();
	int ret;

	for (mode1 = AMD_PSTATE_DISABLE; mode1 < AMD_PSTATE_MAX; mode1++) {
		ret = amd_pstate_set_mode(mode1);
		if (ret)
			return ret;
		for (mode2 = AMD_PSTATE_DISABLE; mode2 < AMD_PSTATE_MAX; mode2++) {
			if (mode1 == mode2)
				continue;
			ret = amd_pstate_set_mode(mode2);
			if (ret)
				goto out;
		}
	}

out:
	if (ret)
		pr_warn("%s: failed to update status for %s->%s: %d\n", __func__,
			amd_pstate_get_mode_string(mode1),
			amd_pstate_get_mode_string(mode2), ret);

	amd_pstate_set_mode(orig_mode);
	return ret;
}

enum attr_category {
	ATTR_ALWAYS,
	ATTR_PREFCORE,
	ATTR_EPP,
	ATTR_FLOOR_FREQ,
};

static const struct {
	const char	*name;
	enum attr_category category;
} expected_freq_attrs[] = {
	{"amd_pstate_max_freq",				ATTR_ALWAYS},
	{"amd_pstate_lowest_nonlinear_freq",		ATTR_ALWAYS},
	{"amd_pstate_highest_perf",			ATTR_ALWAYS},
	{"amd_pstate_prefcore_ranking",			ATTR_PREFCORE},
	{"amd_pstate_hw_prefcore",			ATTR_PREFCORE},
	{"energy_performance_preference",		ATTR_EPP},
	{"energy_performance_available_preferences",	ATTR_EPP},
	{"amd_pstate_floor_freq",			ATTR_FLOOR_FREQ},
	{"amd_pstate_floor_count",			ATTR_FLOOR_FREQ},
};

static bool attr_in_driver(struct freq_attr **driver_attrs, const char *name)
{
	int j;

	for (j = 0; driver_attrs[j]; j++) {
		if (!strcmp(driver_attrs[j]->attr.name, name))
			return true;
	}
	return false;
}

/*
 * Verify that for each mode the driver's live ->attr array contains exactly
 * the attributes that should be visible.  Expected visibility is derived
 * independently from hw_prefcore, cpu features, and the current mode —
 * not from the driver's own visibility functions.
 */
static int amd_pstate_ut_check_freq_attrs(u32 index)
{
	enum amd_pstate_mode orig_mode = amd_pstate_get_status();
	static const enum amd_pstate_mode modes[] = {
		AMD_PSTATE_PASSIVE, AMD_PSTATE_ACTIVE, AMD_PSTATE_GUIDED,
	};
	bool has_prefcore, has_floor_freq;
	int m, i, ret;

	has_floor_freq = cpu_feature_enabled(X86_FEATURE_CPPC_PERF_PRIO);

	/*
	 * Determine prefcore support from any online CPU's cpudata.
	 * hw_prefcore reflects the platform-wide decision made at init.
	 */
	has_prefcore = false;
	for_each_online_cpu(i) {
		struct cpufreq_policy *policy __free(put_cpufreq_policy) = NULL;
		struct amd_cpudata *cpudata;

		policy = cpufreq_cpu_get(i);
		if (!policy)
			continue;
		cpudata = policy->driver_data;
		has_prefcore = cpudata->hw_prefcore;
		break;
	}

	for (m = 0; m < ARRAY_SIZE(modes); m++) {
		struct freq_attr **driver_attrs;

		ret = amd_pstate_set_mode(modes[m]);
		if (ret)
			goto out;

		driver_attrs = amd_pstate_get_current_attrs();
		if (!driver_attrs) {
			pr_err("%s: no driver attrs in mode %s\n",
			       __func__, amd_pstate_get_mode_string(modes[m]));
			ret = -EINVAL;
			goto out;
		}

		for (i = 0; i < ARRAY_SIZE(expected_freq_attrs); i++) {
			bool expected, found;

			switch (expected_freq_attrs[i].category) {
			case ATTR_ALWAYS:
				expected = true;
				break;
			case ATTR_PREFCORE:
				expected = has_prefcore;
				break;
			case ATTR_EPP:
				expected = (modes[m] == AMD_PSTATE_ACTIVE);
				break;
			case ATTR_FLOOR_FREQ:
				expected = has_floor_freq;
				break;
			default:
				expected = false;
				break;
			}

			found = attr_in_driver(driver_attrs,
					       expected_freq_attrs[i].name);

			if (expected != found) {
				pr_err("%s: mode %s: attr %s expected %s but is %s\n",
				       __func__,
				       amd_pstate_get_mode_string(modes[m]),
				       expected_freq_attrs[i].name,
				       expected ? "visible" : "hidden",
				       found ? "visible" : "hidden");
				ret = -EINVAL;
				goto out;
			}
		}
	}

	ret = 0;
out:
	amd_pstate_set_mode(orig_mode);
	return ret;
}

static int __init amd_pstate_ut_init(void)
{
	u32 i = 0, arr_size = ARRAY_SIZE(amd_pstate_ut_cases);

	for (i = 0; i < arr_size; i++) {
		int ret;

		if (test_list && *test_list &&
		    !test_in_list(test_list, amd_pstate_ut_cases[i].name))
			continue;

		ret = amd_pstate_ut_cases[i].func(i);

		if (ret)
			pr_err("%-4d %-20s\t fail: %d!\n", i+1, amd_pstate_ut_cases[i].name, ret);
		else
			pr_info("%-4d %-20s\t success!\n", i+1, amd_pstate_ut_cases[i].name);
	}

	return 0;
}

static void __exit amd_pstate_ut_exit(void)
{
}

module_init(amd_pstate_ut_init);
module_exit(amd_pstate_ut_exit);

MODULE_AUTHOR("Meng Li <li.meng@amd.com>");
MODULE_DESCRIPTION("AMD P-state driver Test module");
MODULE_LICENSE("GPL");