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

Yang Jihong posted 12 patches 1 year, 2 months ago
[RFC 01/12] perf record: Add event action support
Posted by Yang Jihong 1 year, 2 months ago
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
 
 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;
+
+	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
Re: [RFC 01/12] perf record: Add event action support
Posted by Arnaldo Carvalho de Melo 1 year, 2 months ago
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
Re: [External] Re: [RFC 01/12] perf record: Add event action support
Posted by Yang Jihong 1 year, 2 months ago
Hello,

On 11/29/24 04:19, Arnaldo Carvalho de Melo wrote:
> 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?

Yes, The entire patchset needs to be applied to fully support this feature.

This patch simply implements the data organization structure.

Thanks,
Yang