Re: [RFC 01/12] perf record: Add event action support

Arnaldo Carvalho de Melo
2024-11-28T20:19:44Z

On Thu, Nov 28, 2024 at 09:35:42PM +0800, Yang Jihong wrote:
> In perf-record, when an event is triggered, default behavior is to
> save sample data to perf.data. Sometimes, we may just want to do
> some lightweight actions, such as printing a log.
>
> Based on this requirement, add the --action option to the event to
> specify the behavior when the event occurs.
>
> Signed-off-by: Yang Jihong <yangjihong@bytedance.com>
> ---
> tools/perf/Documentation/perf-record.txt | 8 +
> tools/perf/builtin-record.c | 31 +++
> tools/perf/util/Build | 18 ++
> tools/perf/util/parse-action.c | 230 +++++++++++++++++++++++
> tools/perf/util/parse-action.h | 75 ++++++++
> tools/perf/util/parse-action.l | 40 ++++
> tools/perf/util/parse-action.y | 82 ++++++++
> tools/perf/util/record_action.c | 15 ++
> tools/perf/util/record_action.h | 24 +++
> 9 files changed, 523 insertions(+)
> create mode 100644 tools/perf/util/parse-action.c
> create mode 100644 tools/perf/util/parse-action.h
> create mode 100644 tools/perf/util/parse-action.l
> create mode 100644 tools/perf/util/parse-action.y
> create mode 100644 tools/perf/util/record_action.c
> create mode 100644 tools/perf/util/record_action.h
>
> diff --git a/tools/perf/Documentation/perf-record.txt b/tools/perf/Documentation/perf-record.txt
> index 242223240a08..d0d9e0f69f3d 100644
> --- a/tools/perf/Documentation/perf-record.txt
> +++ b/tools/perf/Documentation/perf-record.txt
> @@ -833,6 +833,14 @@ filtered through the mask provided by -C option.
> Prepare BPF filter to be used by regular users. The action should be
> either "pin" or "unpin". The filter can be used after it's pinned.
>
> +--action=<action>::
> + Actions are the programs that run when the sampling event is triggered.
> + The action is a list of expressions separated by semicolons (;).
> + The sample data is saved by bpf prog attached by the event.
> + The call currently supported is print(); some commonly used built-in special
> + variables are also supported
> + For example:
> + # perf record -e sched:sched_switch --action 'print("[%llu]comm=%s, cpu=%d, pid=%d, tid=%d\n", time, comm, cpu, pid, tid)' true

But at this point in the series this isn't available, right?

I.e. when testing this specific patch I can't follow what the
documentation above says and expect anything, right? It will just fail?

> include::intel-hybrid.txt[]
>
> diff --git a/tools/perf/builtin-record.c b/tools/perf/builtin-record.c
> index f83252472921..108d98706873 100644
> --- a/tools/perf/builtin-record.c
> +++ b/tools/perf/builtin-record.c
> @@ -51,6 +51,10 @@
> #include "util/clockid.h"
> #include "util/off_cpu.h"
> #include "util/bpf-filter.h"
> +#ifdef HAVE_BPF_SKEL
> +#include "util/parse-action.h"
> +#include "util/record_action.h"
> +#endif
> #include "asm/bug.h"
> #include "perf.h"
> #include "cputopo.h"
> @@ -182,6 +186,7 @@ struct record {
> struct pollfd_index_map *index_map;
> size_t index_map_sz;
> size_t index_map_cnt;
> + bool custom_action;
> };
>
> static volatile int done;
> @@ -3316,6 +3321,23 @@ static int parse_record_synth_option(const struct option *opt,
> return 0;
> }
>
> +#ifdef HAVE_BPF_SKEL
> +static int parse_record_action_option(const struct option *opt,
> + const char *str,
> + int unset __maybe_unused)
> +{
> + int ret;
> + struct record *rec = (struct record *)opt->value;

There should be no need for casting above, opt->value is void *.

> +
> + ret = parse_record_action(rec->evlist, str);
> + if (ret)
> + return ret;
> +
> + rec->custom_action = true;
> + return 0;
> +}
> +#endif
> +
> /*
> * XXX Ideally would be local to cmd_record() and passed to a record__new
> * because we need to have access to it in record__exit, that is called
> @@ -3564,6 +3586,9 @@ static struct option __record_options[] = {
> OPT_BOOLEAN(0, "off-cpu", &record.off_cpu, "Enable off-cpu analysis"),
> OPT_STRING(0, "setup-filter", &record.filter_action, "pin|unpin",
> "BPF filter action"),
> +#ifdef HAVE_BPF_SKEL
> + OPT_CALLBACK(0, "action", &record, "action", "event action", parse_record_action_option),
> +#endif
> OPT_END()
> };
>
> @@ -4001,6 +4026,12 @@ int cmd_record(int argc, const char **argv)
> if (quiet)
> perf_quiet_option();
>
> +#ifdef HAVE_BPF_SKEL
> + /* Currently, event actions only supported using bpf prog. */
> + if (rec->custom_action)
> + return bpf_perf_record(rec->evlist, argc, argv);
> +#endif
> +
> err = symbol__validate_sym_arguments();
> if (err)
> return err;
> diff --git a/tools/perf/util/Build b/tools/perf/util/Build
> index c06d2ee9024c..db4c4cabc5f8 100644
> --- a/tools/perf/util/Build
> +++ b/tools/perf/util/Build
> @@ -249,6 +249,12 @@ perf-util-$(CONFIG_LIBBPF) += bpf-utils.o
>
> perf-util-$(CONFIG_LIBPFM4) += pfm.o
>
> +# perf record event action
> +perf-util-$(CONFIG_PERF_BPF_SKEL) += parse-action.o
> +perf-util-$(CONFIG_PERF_BPF_SKEL) += parse-action-flex.o
> +perf-util-$(CONFIG_PERF_BPF_SKEL) += parse-action-bison.o
> +perf-util-$(CONFIG_PERF_BPF_SKEL) += record_action.o
> +
> CFLAGS_config.o += -DETC_PERFCONFIG="BUILD_STR($(ETC_PERFCONFIG_SQ))"
>
> # avoid compiler warnings in 32-bit mode
> @@ -294,6 +300,16 @@ $(OUTPUT)util/bpf-filter-bison.c $(OUTPUT)util/bpf-filter-bison.h: util/bpf-filt
> $(Q)$(call echo-cmd,bison)$(BISON) -v $< -d $(PARSER_DEBUG_BISON) $(BISON_FILE_PREFIX_MAP) \
> -o $(OUTPUT)util/bpf-filter-bison.c -p perf_bpf_filter_
>
> +$(OUTPUT)util/parse-action-flex.c $(OUTPUT)util/parse-action-flex.h: util/parse-action.l $(OUTPUT)util/parse-action-bison.c util/parse-action.h
> + $(call rule_mkdir)
> + $(Q)$(call echo-cmd,flex)$(FLEX) -o $(OUTPUT)util/parse-action-flex.c \
> + --header-file=$(OUTPUT)util/parse-action-flex.h $(PARSER_DEBUG_FLEX) $<
> +
> +$(OUTPUT)util/parse-action.c $(OUTPUT)util/parse-action-bison.h: util/parse-action.y util/parse-action.h
> + $(call rule_mkdir)
> + $(Q)$(call echo-cmd,bison)$(BISON) -v $< -d $(PARSER_DEBUG_BISON) $(BISON_FILE_PREFIX_MAP) \
> + -o $(OUTPUT)util/parse-action-bison.c -p parse_action_
> +
> FLEX_VERSION := $(shell $(FLEX) --version | cut -d' ' -f2)
>
> FLEX_GE_260 := $(call version-ge3,$(FLEX_VERSION),2.6.0)
> @@ -345,11 +361,13 @@ CFLAGS_parse-events-flex.o += $(flex_flags) -Wno-unused-label
> CFLAGS_pmu-flex.o += $(flex_flags)
> CFLAGS_expr-flex.o += $(flex_flags)
> CFLAGS_bpf-filter-flex.o += $(flex_flags)
> +CFLAGS_parse-action-flex.o += $(flex_flags)
>
> CFLAGS_parse-events-bison.o += $(bison_flags)
> CFLAGS_pmu-bison.o += -DYYLTYPE_IS_TRIVIAL=0 $(bison_flags)
> CFLAGS_expr-bison.o += -DYYLTYPE_IS_TRIVIAL=0 $(bison_flags)
> CFLAGS_bpf-filter-bison.o += -DYYLTYPE_IS_TRIVIAL=0 $(bison_flags)
> +CFLAGS_parse-action-bison.o += -DYYLTYPE_IS_TRIVIAL=0 $(bison_flags)
>
> $(OUTPUT)util/parse-events.o: $(OUTPUT)util/parse-events-flex.c $(OUTPUT)util/parse-events-bison.c
> $(OUTPUT)util/pmu.o: $(OUTPUT)util/pmu-flex.c $(OUTPUT)util/pmu-bison.c
> diff --git a/tools/perf/util/parse-action.c b/tools/perf/util/parse-action.c
> new file mode 100644
> index 000000000000..01c8c7fdea59
> --- /dev/null
> +++ b/tools/perf/util/parse-action.c
> @@ -0,0 +1,230 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/**
> + * Generic actions for sampling events
> + * Actions are the programs that run when the sampling event is triggered.
> + * The action is a list of expressions separated by semicolons (;).
> + * Each action is an expression, added to actions_head node as list_head node.
> + */
> +
> +#include "util/debug.h"
> +#include "util/parse-action.h"
> +#include "util/parse-action-flex.h"
> +#include "util/parse-action-bison.h"
> +
> +static struct list_head actions_head = LIST_HEAD_INIT(actions_head);
> +
> +int event_actions__for_each_expr(int (*func)(struct evtact_expr *, void *arg),
> + void *arg, bool recursive)
> +{
> + int ret;
> + struct evtact_expr *expr, *opnd;
> +
> + if (list_empty(&actions_head))
> + return (*func)(NULL, arg);
> +
> + list_for_each_entry(expr, &actions_head, list) {
> + ret = (*func)(expr, arg);
> + if (ret)
> + return ret;
> +
> + if (recursive && !list_empty(&expr->opnds)) {
> + list_for_each_entry(opnd, &expr->opnds, list) {
> + ret = (*func)(opnd, arg);
> + if (ret)
> + return ret;
> + }
> + }
> + }
> +
> + return 0;
> +}
> +
> +int event_actions__for_each_expr_safe(int (*func)(struct evtact_expr *, void *arg),
> + void *arg, bool recursive)
> +{
> + int ret;
> + struct evtact_expr *expr, *tmp;
> + struct evtact_expr *opnd, *opnd_tmp;
> +
> + if (list_empty(&actions_head))
> + return (*func)(NULL, arg);
> +
> + list_for_each_entry_safe(expr, tmp, &actions_head, list) {
> + ret = (*func)(expr, arg);
> + if (ret)
> + return ret;
> +
> + if (recursive && !list_empty(&expr->opnds)) {
> + list_for_each_entry_safe(opnd, opnd_tmp, &expr->opnds, list) {
> + ret = (*func)(opnd, arg);
> + if (ret)
> + return ret;
> + }
> + }
> + }
> +
> + return 0;
> +}
> +
> +static int parse_action_option(const char *str)
> +{
> + int ret;
> + YY_BUFFER_STATE buffer;
> +
> + buffer = parse_action__scan_string(str);
> + ret = parse_action_parse(&actions_head);
> +
> + parse_action__flush_buffer(buffer);
> + parse_action__delete_buffer(buffer);
> + parse_action_lex_destroy();
> +
> + return ret;
> +}
> +
> +int parse_record_action(struct evlist *evlist, const char *str)
> +{
> + int ret;
> +
> + if (evlist == NULL) {
> + pr_err("--action option should follow a tracer option\n");
> + return -1;
> + }
> +
> + ret = parse_action_option(str);
> + if (ret) {
> + event_actions__free();
> + pr_err("parse action option failed\n");
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static int do_action_free(struct evtact_expr *action, void *data __maybe_unused)
> +{
> + if (action == NULL)
> + return 0;
> +
> + list_del(&action->list);
> + parse_action_expr__free(action);
> + return 0;
> +}
> +
> +void event_actions__free(void)
> +{
> + (void)event_actions__for_each_expr_safe(do_action_free, NULL, false);
> +}
> +
> +static struct evtact_expr_class *expr_class_list[EVTACT_EXPR_TYPE_MAX] = {
> +};
> +
> +int parse_action_expr__set_class(enum evtact_expr_type type,
> + struct evtact_expr_class *class)
> +{
> + if (type >= EVTACT_EXPR_TYPE_MAX) {
> + pr_err("action expr set class ops type invalid\n");
> + return -EINVAL;
> + }
> +
> + if (expr_class_list[type] != NULL) {
> + pr_err("action expr set class ops type already exists\n");
> + return -EEXIST;
> + }
> +
> + expr_class_list[type] = class;
> + return 0;
> +}
> +
> +static int expr_set_type(struct evtact_expr *expr)
> +{
> + u64 id;
> + int ret;
> + u32 type, opcode;
> + struct evtact_expr_class *class;
> +
> + id = expr->id;
> + evtact_expr_id_decode(id, &type, &opcode);
> +
> + if (type >= EVTACT_EXPR_TYPE_MAX) {
> + pr_err("parse_action_expr type invalid: %u\n", type);
> + return -EINVAL;
> + }
> +
> + class = expr_class_list[type];
> + if (class == NULL) {
> + pr_err("parse_action_expr class not supported: %u\n", type);
> + return -ENOTSUP;
> + }
> +
> + if (class->set_ops != NULL) {
> + ret = class->set_ops(expr, opcode);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +struct evtact_expr *parse_action_expr__new(u64 id, struct list_head *opnds,
> + void *data, int size)
> +{
> + int ret;
> + struct evtact_expr *expr;
> +
> + expr = malloc(sizeof(*expr));
> + if (expr == NULL) {
> + pr_err("parse_action_expr malloc failed\n");
> + goto out_free_opnds;
> + }
> + expr->id = id;
> +
> + if (opnds != NULL)
> + list_add_tail(&expr->opnds, opnds);
> + else
> + INIT_LIST_HEAD(&expr->opnds);
> +
> + ret = expr_set_type(expr);
> + if (ret)
> + goto out_list_del_opnds;
> +
> + if (expr->ops->new != NULL) {
> + ret = expr->ops->new(expr, data, size);
> + if (ret)
> + goto out_free_expr;
> + }
> +
> + return expr;
> +
> +out_free_expr:
> + free(expr);
> +out_list_del_opnds:
> + list_del(&expr->opnds);
> +out_free_opnds:
> + parse_action_expr__free_opnds(opnds);
> +
> + return NULL;
> +}
> +
> +void parse_action_expr__free_opnds(struct list_head *opnds)
> +{
> + struct evtact_expr *opnd, *tmp;
> +
> + if (opnds != NULL && !list_empty(opnds)) {
> + list_for_each_entry_safe(opnd, tmp, opnds, list) {
> + list_del(&opnd->list);
> + parse_action_expr__free(opnd);
> + }
> + }
> +}
> +
> +void parse_action_expr__free(struct evtact_expr *expr)
> +{
> + if (expr == NULL)
> + return;
> +
> + if (expr->ops->free != NULL)
> + expr->ops->free(expr);
> +
> + parse_action_expr__free_opnds(&expr->opnds);
> + free(expr);
> +}
> diff --git a/tools/perf/util/parse-action.h b/tools/perf/util/parse-action.h
> new file mode 100644
> index 000000000000..71a0a166959e
> --- /dev/null
> +++ b/tools/perf/util/parse-action.h
> @@ -0,0 +1,75 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +#ifndef __PERF_UTIL_PARSE_ACTION_H_
> +#define __PERF_UTIL_PARSE_ACTION_H_
> +
> +#include <linux/types.h>
> +
> +#include <subcmd/parse-options.h>
> +
> +#include "evlist.h"
> +
> +enum evtact_expr_type {
> + EVTACT_EXPR_TYPE_MAX,
> +};
> +
> +struct evtact_expr;
> +struct evtact_expr_ops {
> + int (*new)(struct evtact_expr *expr, void *data, int size);
> + int (*eval)(struct evtact_expr *expr,
> + void *in, int in_size, void **out, int *out_size);
> + void (*free)(struct evtact_expr *expr);
> +};
> +
> +struct evtact_expr_class {
> + int (*set_ops)(struct evtact_expr *expr, u32 opcode);
> +};
> +
> +struct evtact_expr {
> + struct list_head list;
> + u64 id;
> + struct evtact_expr_ops *ops;
> + struct list_head opnds;
> + void *priv;
> +};
> +
> +/*
> + * The expr id contains two fileds:
> + * |--------------|----------------|
> + * | type | opcode |
> + * |--------------|----------------|
> + * 32-bit 32-bit
> + */
> +#define EVTACT_EXPR_ID_TYPE_BITS_SHIFT 32
> +static inline u64 evtact_expr_id_encode(u32 type, u32 opcode)
> +{
> + return (u64)type << EVTACT_EXPR_ID_TYPE_BITS_SHIFT | opcode;
> +}
> +
> +static inline void evtact_expr_id_decode(u64 id, u32 *type, u32 *opcode)
> +{
> + if (type != NULL)
> + *type = id >> EVTACT_EXPR_ID_TYPE_BITS_SHIFT;
> +
> + if (opcode != NULL)
> + *opcode = id & GENMASK(EVTACT_EXPR_ID_TYPE_BITS_SHIFT, 0);
> +}
> +
> +int parse_record_action(struct evlist *evlist, const char *str);
> +void event_actions__free(void);
> +
> +int event_actions__for_each_expr(int (*func)(struct evtact_expr *, void *arg),
> + void *arg, bool recursive);
> +
> +int event_actions__for_each_expr_safe(int (*func)(struct evtact_expr *, void *arg),
> + void *arg, bool recursive);
> +
> +struct evtact_expr *parse_action_expr__new(u64 id, struct list_head *opnds,
> + void *data, int size);
> +
> +void parse_action_expr__free_opnds(struct list_head *opnds);
> +void parse_action_expr__free(struct evtact_expr *expr);
> +
> +int parse_action_expr__set_class(enum evtact_expr_type type,
> + struct evtact_expr_class *ops);
> +
> +#endif /* __PERF_UTIL_PARSE_ACTION_H_ */
> diff --git a/tools/perf/util/parse-action.l b/tools/perf/util/parse-action.l
> new file mode 100644
> index 000000000000..3cb72de50372
> --- /dev/null
> +++ b/tools/perf/util/parse-action.l
> @@ -0,0 +1,40 @@
> +%option prefix="parse_action_"
> +%option noyywrap
> +%option stack
> +
> +%{
> +
> +#include <errno.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +
> +#include "util/debug.h"
> +
> +#include "parse-action.h"
> +#include "parse-action-bison.h"
> +
> +%}
> +
> +space [ \t]
> +ident [_a-zA-Z][_a-zA-Z0-9]*
> +
> +%%
> +
> +{space} { }
> +
> +";" { return SEMI; }
> +
> +{ident} {
> + parse_action_lval.str = strdup(parse_action_text);
> + if (parse_action_lval.str == NULL) {
> + pr_err("parse_action malloc ident string failed\n");
> + return ERROR;
> + }
> + return IDENT;
> + }
> +. {
> + pr_err("invalid character: '%s'\n", parse_action_text);
> + return ERROR;
> + }
> +
> +%%
> diff --git a/tools/perf/util/parse-action.y b/tools/perf/util/parse-action.y
> new file mode 100644
> index 000000000000..fade9d093d4a
> --- /dev/null
> +++ b/tools/perf/util/parse-action.y
> @@ -0,0 +1,82 @@
> +%parse-param {struct list_head *actions_head}
> +%define parse.error verbose
> +
> +%{
> +
> +#ifndef NDEBUG
> +#define YYDEBUG 1
> +#endif
> +
> +#include <errno.h>
> +#include <stdio.h>
> +#include <string.h>
> +
> +#include <linux/compiler.h>
> +#include <linux/list.h>
> +
> +#include "util/debug.h"
> +#include "util/parse-action.h"
> +
> +int parse_action_lex(void);
> +
> +static void parse_action_error(struct list_head *expr __maybe_unused,
> + char const *msg)
> +{
> + pr_err("parse_action: %s\n", msg);
> +}
> +
> +%}
> +
> +%union
> +{
> + char *str;
> + struct evtact_expr *expr;
> + struct list_head *list;
> +}
> +
> +%token IDENT ERROR
> +%token SEMI
> +%type <expr> action_term expr_term
> +%destructor { parse_action_expr__free($$); } <expr>
> +%type <str> IDENT
> +
> +%%
> +
> +actions:
> +action_term SEMI actions
> +{
> + list_add(&$1->list, actions_head);
> +}
> +|
> +action_term SEMI
> +{
> + list_add(&$1->list, actions_head);
> +}
> +|
> +action_term
> +{
> + list_add(&$1->list, actions_head);
> +}
> +
> +action_term:
> +expr_term
> +{
> + $$ = $1;
> +}
> +
> +expr_term:
> +IDENT
> +{
> + $$ = NULL;
> + pr_err("unsupported ident: '%s'\n", $1);
> + free($1);
> + YYERROR;
> +}
> +|
> +ERROR
> +{
> + $$ = NULL;
> + YYERROR;
> +}
> +
> +%%
> diff --git a/tools/perf/util/record_action.c b/tools/perf/util/record_action.c
> new file mode 100644
> index 000000000000..44789e0d4678
> --- /dev/null
> +++ b/tools/perf/util/record_action.c
> @@ -0,0 +1,15 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/**
> + * Read event sample data and execute the specified actions.
> + */
> +
> +#include "util/debug.h"
> +#include "util/parse-action.h"
> +#include "util/record_action.h"
> +
> +int bpf_perf_record(struct evlist *evlist __maybe_unused,
> + int argc __maybe_unused, const char **argv __maybe_unused)
> +{
> + event_actions__free();
> + return 0;
> +}
> diff --git a/tools/perf/util/record_action.h b/tools/perf/util/record_action.h
> new file mode 100644
> index 000000000000..289be4befa97
> --- /dev/null
> +++ b/tools/perf/util/record_action.h
> @@ -0,0 +1,24 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +#ifndef __PERF_UTIL_RECORD_ACTION_H_
> +#define __PERF_UTIL_RECORD_ACTION_H_
> +
> +#include <errno.h>
> +#include "evlist.h"
> +
> +#ifdef HAVE_BPF_SKEL
> +
> +int bpf_perf_record(struct evlist *evlist, int argc, const char **argv);
> +
> +
> +#else /* !HAVE_BPF_SKEL */
> +
> +static inline int bpf_perf_record(struct evlist *evlist __maybe_unused,
> + int argc __maybe_unused,
> + const char **argv __maybe_unused)
> +{
> + return -EOPNOTSUPP;
> +}
> +
> +#endif /* !HAVE_BPF_SKEL */
> +
> +#endif /* __PERF_UTIL_RECORD_ACTION_H_ */
> --
> 2.25.1