blob: 2f6a74e43041ee95890e8b33c84b1725a69d868d [file] [edit]
/*
* Copyright (c) 2023 KNS Group LLC (YADRO)
* Copyright (c) 2020 Yonatan Goldschmidt <yon.goldschmidt@gmail.com>
* Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <zephyr/arch/cpu.h>
#include <zephyr/shell/shell.h>
#include <zephyr/shell/shell_uart.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* Return the number of captured frames, zero when the buffer is too small,
* or a negative errno when the current sample should be skipped.
*/
int arch_perf_current_stack_trace(uintptr_t *buf, size_t size);
/*
* The sampling timer only fires on one CPU. The remaining CPUs are sampled by
* broadcasting an IPI to them, which needs architecture support. Without it
* (uniprocessor builds, or SMP without IPI support) only the CPU the timer
* fires on is sampled.
*/
#if defined(CONFIG_SMP) && defined(CONFIG_SCHED_IPI_SUPPORTED)
#define PERF_IPI_SAMPLING 1
#else
#define PERF_IPI_SAMPLING 0
#endif
/** Statistics gathered during a single recording. */
struct perf_stats {
/** Sampling attempts, including the ones filtered out by thread */
atomic_t attempts[CONFIG_MP_MAX_NUM_CPUS];
/** Samples that made it into the buffer */
atomic_t samples[CONFIG_MP_MAX_NUM_CPUS];
atomic_t timer_fires;
atomic_t ipi_calls;
atomic_t ipi_handler_calls;
};
struct perf_data_t {
struct k_timer timer;
struct k_work_delayable dwork;
/* Protects the sample buffer against concurrent samplers */
struct k_spinlock lock;
const struct shell *sh;
size_t idx;
uintptr_t buf[CONFIG_PROFILING_PERF_BUFFER_SIZE];
bool buf_full;
k_tid_t target_thread;
struct perf_stats stats;
#if PERF_IPI_SAMPLING
struct k_ipi_work ipi_work;
bool sampling_active;
#endif
};
static void perf_tracer(struct k_timer *timer);
static void perf_dwork_handler(struct k_work *work);
static struct perf_data_t perf_data = {
.timer = Z_TIMER_INITIALIZER(perf_data.timer, perf_tracer, NULL),
.dwork = Z_WORK_DELAYABLE_INITIALIZER(perf_dwork_handler),
};
/* Sample the CPU this runs on. Called from the timer ISR and from the IPI. */
static void perf_do_sample(void)
{
k_tid_t current_thread = k_current_get();
unsigned int cpu_id = CPU_ID;
int trace_length = 0;
k_spinlock_key_t key;
atomic_inc(&perf_data.stats.attempts[cpu_id]);
/* If target_thread is set, only trace that specific thread */
if (perf_data.target_thread != NULL && perf_data.target_thread != current_thread) {
return;
}
key = k_spin_lock(&perf_data.lock);
if (perf_data.buf_full) {
k_spin_unlock(&perf_data.lock, key);
return;
}
if (++perf_data.idx < CONFIG_PROFILING_PERF_BUFFER_SIZE) {
trace_length = arch_perf_current_stack_trace(perf_data.buf + perf_data.idx,
CONFIG_PROFILING_PERF_BUFFER_SIZE -
perf_data.idx);
}
if (trace_length > 0) {
perf_data.buf[perf_data.idx - 1] = (uintptr_t)trace_length;
perf_data.idx += (size_t)trace_length;
atomic_inc(&perf_data.stats.samples[cpu_id]);
} else {
--perf_data.idx;
if (trace_length == 0) {
perf_data.buf_full = true;
k_work_reschedule(&perf_data.dwork, K_NO_WAIT);
}
}
k_spin_unlock(&perf_data.lock, key);
}
#if PERF_IPI_SAMPLING
/* IPI handler - called on every CPU targeted by perf_sample_other_cpus() */
static void perf_ipi_handler(struct k_ipi_work *work)
{
ARG_UNUSED(work);
atomic_inc(&perf_data.stats.ipi_handler_calls);
/* Only sample if we're actively profiling */
if (perf_data.sampling_active) {
perf_do_sample();
}
}
static void perf_sampling_start(void)
{
k_ipi_work_init(&perf_data.ipi_work);
perf_data.sampling_active = true;
}
static void perf_sampling_stop(void)
{
/* Stop the IPI handler from sampling before the timer is stopped */
perf_data.sampling_active = false;
}
static void perf_sample_other_cpus(void)
{
if (!perf_data.sampling_active || arch_num_cpus() <= 1) {
return;
}
/* The current CPU is filtered out by k_ipi_work_add() */
if (k_ipi_work_add(&perf_data.ipi_work, BIT_MASK(arch_num_cpus()),
perf_ipi_handler) != 0) {
/* Previous broadcast is still in flight, skip this period */
return;
}
atomic_inc(&perf_data.stats.ipi_calls);
k_ipi_work_signal();
}
#else /* !PERF_IPI_SAMPLING */
static inline void perf_sampling_start(void)
{
}
static inline void perf_sampling_stop(void)
{
}
static inline void perf_sample_other_cpus(void)
{
}
#endif /* PERF_IPI_SAMPLING */
static void perf_stats_reset(struct perf_stats *stats)
{
memset(stats, 0, sizeof(*stats));
}
static void perf_stats_print(const struct shell *sh, struct perf_stats *stats)
{
shell_print(sh, "\n=== Perf Statistics ===");
shell_print(sh, "Timer fires: %ld", (long)atomic_get(&stats->timer_fires));
if (PERF_IPI_SAMPLING) {
shell_print(sh, "IPI calls: %ld", (long)atomic_get(&stats->ipi_calls));
shell_print(sh, "IPI handler calls: %ld",
(long)atomic_get(&stats->ipi_handler_calls));
}
shell_print(sh, "");
for (unsigned int i = 0; i < arch_num_cpus(); i++) {
shell_print(sh, "CPU %u: Attempts=%ld, Samples=%ld", i,
(long)atomic_get(&stats->attempts[i]),
(long)atomic_get(&stats->samples[i]));
}
shell_print(sh, "======================\n");
}
static void perf_tracer(struct k_timer *timer)
{
ARG_UNUSED(timer);
atomic_inc(&perf_data.stats.timer_fires);
perf_do_sample();
perf_sample_other_cpus();
}
static void perf_dwork_handler(struct k_work *work)
{
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
struct perf_data_t *perf_data_ptr = CONTAINER_OF(dwork, struct perf_data_t, dwork);
k_spinlock_key_t key;
bool buf_full;
perf_sampling_stop();
k_timer_stop(&perf_data_ptr->timer);
/* A sampler may still be in flight; read the shared flag under the lock */
key = k_spin_lock(&perf_data_ptr->lock);
buf_full = perf_data_ptr->buf_full;
k_spin_unlock(&perf_data_ptr->lock, key);
perf_stats_print(perf_data_ptr->sh, &perf_data_ptr->stats);
if (buf_full) {
shell_error(perf_data_ptr->sh, "Perf buf overflow!");
} else {
shell_print(perf_data_ptr->sh, "Perf done!");
}
}
static int cmd_perf_record(const struct shell *sh, size_t argc, char **argv)
{
if (k_work_delayable_is_pending(&perf_data.dwork)) {
shell_warn(sh, "Perf is running");
return -EINPROGRESS;
}
if (perf_data.buf_full) {
shell_warn(sh, "Perf buffer is full");
return -ENOBUFS;
}
k_timeout_t duration = K_MSEC(strtoll(argv[1], NULL, 10));
k_timeout_t period = K_NSEC(1000000000 / strtoll(argv[2], NULL, 10));
k_tid_t target_thread = NULL;
/* If third argument is provided, use it as thread id */
if (argc >= 4) {
target_thread = (k_tid_t)strtoul(argv[3], NULL, 16);
shell_print(sh, "Tracing thread: %p", target_thread);
}
perf_data.sh = sh;
perf_data.target_thread = target_thread;
perf_stats_reset(&perf_data.stats);
shell_print(sh, "Number of CPUs: %u", arch_num_cpus());
/* Enable sampling before the timer may fire */
perf_sampling_start();
/* Start the timer that samples this CPU and IPIs the other ones */
k_timer_user_data_set(&perf_data.timer, &perf_data);
k_timer_start(&perf_data.timer, K_NO_WAIT, period);
k_work_schedule(&perf_data.dwork, duration);
shell_print(sh, "Enabled perf");
return 0;
}
static int cmd_perf_clear(const struct shell *sh, size_t argc, char **argv)
{
if (sh != NULL) {
if (k_work_delayable_is_pending(&perf_data.dwork)) {
shell_warn(sh, "Perf is running");
return -EINPROGRESS;
}
shell_print(sh, "Perf buffer cleared");
}
perf_data.idx = 0;
perf_data.buf_full = false;
return 0;
}
static int cmd_perf_info(const struct shell *sh, size_t argc, char **argv)
{
if (k_work_delayable_is_pending(&perf_data.dwork)) {
shell_print(sh, "Perf is running");
}
shell_print(sh, "Perf buf: %zu/%d %s", perf_data.idx, CONFIG_PROFILING_PERF_BUFFER_SIZE,
perf_data.buf_full ? "(full)" : "");
return 0;
}
static int cmd_perf_print(const struct shell *sh, size_t argc, char **argv)
{
if (k_work_delayable_is_pending(&perf_data.dwork)) {
shell_warn(sh, "Perf is running");
return -EINPROGRESS;
}
shell_print(sh, "Perf buf length %zu", perf_data.idx);
for (size_t i = 0; i < perf_data.idx; i++) {
shell_print(sh, "%016lx", perf_data.buf[i]);
}
cmd_perf_clear(NULL, 0, NULL);
return 0;
}
#define CMD_HELP_RECORD \
"Start recording for <duration> ms on <frequency> Hz\n" \
"Usage: record <duration> <frequency> [thread_id]\n" \
" thread_id: optional thread ID in hex format to trace specific thread"
SHELL_STATIC_SUBCMD_SET_CREATE(
m_sub_perf, SHELL_CMD_ARG(record, NULL, CMD_HELP_RECORD, cmd_perf_record, 3, 1),
SHELL_CMD_ARG(printbuf, NULL, "Print the perf buffer", cmd_perf_print, 0, 0),
SHELL_CMD_ARG(clear, NULL, "Clear the perf buffer", cmd_perf_clear, 0, 0),
SHELL_CMD_ARG(info, NULL, "Print the perf info", cmd_perf_info, 0, 0),
SHELL_SUBCMD_SET_END);
SHELL_CMD_ARG_REGISTER(perf, &m_sub_perf, "Lightweight profiler", NULL, 0, 0);