blob: c20054fa224251daf9e578057244da9cd282428b [file]
/*
* Copyright (c) 2024, Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/sys/cpu_load.h>
#include <zephyr/ztest.h>
#include <zephyr/logging/log_ctrl.h>
#include <zephyr/logging/log_backend.h>
#include <zephyr/drivers/counter.h>
/* Tolerance of the load assertions, in per mille.
*
* Generous enough to absorb the tick granularity of slow targets (k_msleep()
* rounds up to a tick, so the busy/idle split is not exactly even) while still
* far tighter than any real failure: a backend that fails to account idle time
* reports 1000, and one that over-accounts it reports 0.
*/
#define DELTA 75
/* CPU whose load is measured. On SMP the worker below is pinned to it so that
* the busy/idle sequence is attributed to a known CPU.
*/
#define TEST_CPU 0
#define WORKER_STACK_SIZE 2048
K_THREAD_STACK_DEFINE(worker_stack, WORKER_STACK_SIZE);
static struct k_thread worker;
static int load_busy;
static int load_half;
static int load_idle;
/* Runs the busy/idle sequence on TEST_CPU and records the measured load.
*
* While this thread busy-waits, TEST_CPU is fully loaded; while it sleeps,
* TEST_CPU is idle, as the test thread is blocked joining it and nothing else
* is runnable there.
*/
static void load_seq(void *p1, void *p2, void *p3)
{
uint32_t t_ms = 100;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
/* Reset the measurement, then keep the CPU busy for the whole window. */
(void)cpu_load_get_cpu(TEST_CPU, true);
k_busy_wait(t_ms * USEC_PER_MSEC);
load_busy = cpu_load_get_cpu(TEST_CPU, false);
/* Idle for the same amount of time without resetting, so that the
* window covers one busy and one idle period.
*/
k_msleep(t_ms);
load_half = cpu_load_get_cpu(TEST_CPU, false);
/* Reset and stay idle for the whole window. */
(void)cpu_load_get_cpu(TEST_CPU, true);
k_msleep(t_ms);
load_idle = cpu_load_get_cpu(TEST_CPU, false);
}
static void run_load_seq(void)
{
k_tid_t tid;
if (IS_ENABLED(CONFIG_SMP) && !IS_ENABLED(CONFIG_SCHED_CPU_MASK)) {
/* Without pinning the worker may migrate between CPUs, which
* makes attributing the busy/idle sequence to a single CPU
* meaningless.
*/
ztest_test_skip();
}
tid = k_thread_create(&worker, worker_stack, WORKER_STACK_SIZE, load_seq,
NULL, NULL, NULL, K_PRIO_PREEMPT(1), 0, K_FOREVER);
#if defined(CONFIG_SMP) && defined(CONFIG_SCHED_CPU_MASK)
/* A running thread cannot be pinned, hence the K_FOREVER delay above:
* pin the worker before starting it.
*/
zassert_ok(k_thread_cpu_pin(tid, TEST_CPU));
#endif
k_thread_start(tid);
zassert_ok(k_thread_join(tid, K_FOREVER));
}
ZTEST(cpu_load, test_load)
{
if (CONFIG_CPU_LOAD_LOG_PERIODICALLY > 0) {
cpu_load_log_control(false);
}
run_load_seq();
/* Results are in per mille. */
zassert_within(load_busy, 1000, DELTA, "busy window: %d", load_busy);
zassert_within(load_half, 500, DELTA, "half-busy window: %d", load_half);
zassert_within(load_idle, 0, DELTA, "idle window: %d", load_idle);
}
/* The per-CPU getter must report every CPU and reject out-of-range ids. */
ZTEST(cpu_load, test_get_cpu)
{
for (unsigned int cpu = 0; cpu < CONFIG_MP_MAX_NUM_CPUS; cpu++) {
int load = cpu_load_get_cpu(cpu, true);
zassert_true(load >= 0, "CPU%u: unexpected error %d", cpu, load);
zassert_true(load <= 1000, "CPU%u: load %d out of range", cpu, load);
}
zassert_equal(cpu_load_get_cpu(CONFIG_MP_MAX_NUM_CPUS, false), -EINVAL);
}
/* The current-CPU getter and the per mille to percent helper. */
ZTEST(cpu_load, test_get_current_cpu)
{
int load = cpu_load_get(true);
zassert_true(load >= 0, "unexpected error %d", load);
zassert_true(load <= 1000, "load %d out of range", load);
zassert_equal(CPU_LOAD_PERMILLE_TO_PERCENT(1000), 100);
zassert_equal(CPU_LOAD_PERMILLE_TO_PERCENT(505), 50);
zassert_equal(CPU_LOAD_PERMILLE_TO_PERCENT(0), 0);
}
#if CONFIG_CPU_LOAD_LOG_PERIODICALLY > 0
/* The periodic report emits one line per CPU per period. */
#define LOGS_PER_PERIOD CONFIG_MP_MAX_NUM_CPUS
#define EXPECTED_LOGS (3 * LOGS_PER_PERIOD)
#define LOGS_TOLERANCE LOGS_PER_PERIOD
static int cpu_load_src_id;
static atomic_t log_cnt;
static void process(const struct log_backend *const backend,
union log_msg_generic *msg)
{
ARG_UNUSED(backend);
const void *source = msg->log.hdr.source;
int source_id = log_const_source_id((const struct log_source_const_data *)source);
if (source_id == cpu_load_src_id) {
atomic_inc(&log_cnt);
}
}
static void init(const struct log_backend *const backend)
{
ARG_UNUSED(backend);
}
const struct log_backend_api mock_log_backend_api = {
.process = process,
.init = init
};
LOG_BACKEND_DEFINE(dummy, mock_log_backend_api, false, NULL);
ZTEST(cpu_load, test_periodic_report)
{
log_backend_enable(&dummy, NULL, LOG_LEVEL_INF);
cpu_load_log_control(true);
cpu_load_src_id = log_source_id_get(STRINGIFY(cpu_load));
atomic_set(&log_cnt, 0);
k_msleep(3 * CONFIG_CPU_LOAD_LOG_PERIODICALLY);
log_flush();
zassert_within(log_cnt, EXPECTED_LOGS, LOGS_TOLERANCE);
cpu_load_log_control(false);
k_msleep(1);
atomic_set(&log_cnt, 0);
k_msleep(3 * CONFIG_CPU_LOAD_LOG_PERIODICALLY);
log_flush();
zassert_equal(log_cnt, 0);
cpu_load_log_control(true);
k_msleep(3 * CONFIG_CPU_LOAD_LOG_PERIODICALLY);
log_flush();
zassert_within(log_cnt, EXPECTED_LOGS, LOGS_TOLERANCE);
cpu_load_log_control(false);
log_backend_disable(&dummy);
}
static uint32_t num_low_callbacks;
static uint8_t last_low_percent;
/* Records rather than asserts: this runs from the periodic timer, i.e. in
* interrupt context, where a failing assertion would be reported out of band.
*/
void low_load_cb(uint8_t percent)
{
last_low_percent = percent;
num_low_callbacks++;
}
static uint32_t num_load_callbacks;
static uint8_t last_cpu_load_percent;
void high_load_cb(uint8_t percent)
{
last_cpu_load_percent = percent;
num_load_callbacks++;
}
ZTEST(cpu_load, test_callback_load_low)
{
int ret;
num_low_callbacks = 0;
last_low_percent = 0;
cpu_load_log_control(true);
ret = cpu_load_cb_reg(low_load_cb, 99);
zassert_equal(ret, 0);
k_msleep(CONFIG_CPU_LOAD_LOG_PERIODICALLY * 4);
ret = cpu_load_cb_reg(NULL, 99);
zassert_equal(ret, 0);
cpu_load_log_control(false);
zassert_equal(num_low_callbacks, 0,
"idle system triggered %u callback(s), highest load %u%%",
num_low_callbacks, last_low_percent);
}
ZTEST(cpu_load, test_callback_load_high)
{
int ret;
cpu_load_log_control(true);
ret = cpu_load_cb_reg(high_load_cb, 99);
zassert_equal(ret, 0);
k_busy_wait(CONFIG_CPU_LOAD_LOG_PERIODICALLY * 4 * 1000);
zassert_between_inclusive(last_cpu_load_percent, 99, 100, "percent: %u",
last_cpu_load_percent);
zassert_between_inclusive(num_load_callbacks, 2, 7, "callbacks: %u",
num_load_callbacks);
/* Reset the callback */
ret = cpu_load_cb_reg(NULL, 99);
zassert_equal(ret, 0);
num_load_callbacks = 0;
k_busy_wait(CONFIG_CPU_LOAD_LOG_PERIODICALLY * 4 * 1000);
zassert_equal(num_load_callbacks, 0, "callbacks: %u", num_load_callbacks);
cpu_load_log_control(false);
}
#endif /* CONFIG_CPU_LOAD_LOG_PERIODICALLY > 0 */
/* Leave no periodic reporting or callback armed behind: a stale callback would
* fire during an unrelated test.
*/
static void cpu_load_after(void *fixture)
{
ARG_UNUSED(fixture);
cpu_load_log_control(false);
(void)cpu_load_cb_reg(NULL, 0);
}
ZTEST_SUITE(cpu_load, NULL, NULL, NULL, cpu_load_after, NULL);