Re: [PATCH v3 00/27] function_graph: Allow multiple users for function graph tracing

From: Mark Rutland
Date: Tue Jun 04 2024 - 10:52:31 EST


Hi Steve, Masami,

On Tue, Jun 04, 2024 at 08:18:50AM -0400, Steven Rostedt wrote:
>
> Masami,
>
> This series passed all my tests, are you comfortable with me pushing
> them to linux-next?

As a heads-up (and not to block pushing this into next), I just gave
this a spin on arm64 atop v6.10-rc2, and running the selftests I see:

ftrace - function pid filters
(instance) ftrace - function pid filters

... both go from [PASS] to [FAIL].

Everything else looks good -- I'll go dig into why that's happening.

It's possible that's just something odd with the filesystem I'm using
(e.g. the wnership test failed because this lacks 'stat').

Mark.

>
> -- Steve
>
>
> On Mon, 03 Jun 2024 15:07:04 -0400
> Steven Rostedt <rostedt@xxxxxxxxxxx> wrote:
>
> > This is a continuation of the function graph multi user code.
> > I wrote a proof of concept back in 2019 of this code[1] and
> > Masami started cleaning it up. I started from Masami's work v10
> > that can be found here:
> >
> > https://lore.kernel.org/linux-trace-kernel/171509088006.162236.7227326999861366050.stgit@devnote2/
> >
> > This is *only* the code that allows multiple users of function
> > graph tracing. This is not the fprobe work that Masami is working
> > to add on top of it. As Masami took my proof of concept, there
> > was still several things I disliked about that code. Instead of
> > having Masami clean it up even more, I decided to take over on just
> > my code and change it up a bit.
> >
> > Changes since v2: https://lore.kernel.org/linux-trace-kernel/20240602033744.563858532@xxxxxxxxxxx
> >
> > - Added comments describing which hashes the append and intersect
> > functions were used for.
> >
> > - Replaced checks of (NULL or EMPTY_HASH) with ftrace_hash_empty()
> > helper function.
> >
> > - Added check at the end of intersect_hash() to convert the hash
> > to EMPTY hash if it doesn't have any functions.
> >
> > - Renamed compare_ops() to ops_equal() and return boolean (inversed return
> > value).
> >
> > - Broke out __ftrace_hash_move_and_update_ops() to use in both
> > ftrace_hash_move_and_update_ops() and ftrace_hash_move_and_update_subops().
> >
> > Diff between last version at end of this email.
> >
> > Masami Hiramatsu (Google) (3):
> > function_graph: Handle tail calls for stack unwinding
> > function_graph: Use a simple LRU for fgraph_array index number
> > ftrace: Add multiple fgraph storage selftest
> >
> > Steven Rostedt (Google) (9):
> > ftrace: Add subops logic to allow one ops to manage many
> > ftrace: Allow subops filtering to be modified
> > function_graph: Add pid tracing back to function graph tracer
> > function_graph: Use for_each_set_bit() in __ftrace_return_to_handler()
> > function_graph: Use bitmask to loop on fgraph entry
> > function_graph: Use static_call and branch to optimize entry function
> > function_graph: Use static_call and branch to optimize return function
> > selftests/ftrace: Add function_graph tracer to func-filter-pid test
> > selftests/ftrace: Add fgraph-multi.tc test
> >
> > Steven Rostedt (VMware) (15):
> > function_graph: Convert ret_stack to a series of longs
> > fgraph: Use BUILD_BUG_ON() to make sure we have structures divisible by long
> > function_graph: Add an array structure that will allow multiple callbacks
> > function_graph: Allow multiple users to attach to function graph
> > function_graph: Remove logic around ftrace_graph_entry and return
> > ftrace/function_graph: Pass fgraph_ops to function graph callbacks
> > ftrace: Allow function_graph tracer to be enabled in instances
> > ftrace: Allow ftrace startup flags to exist without dynamic ftrace
> > function_graph: Have the instances use their own ftrace_ops for filtering
> > function_graph: Add "task variables" per task for fgraph_ops
> > function_graph: Move set_graph_function tests to shadow stack global var
> > function_graph: Move graph depth stored data to shadow stack global var
> > function_graph: Move graph notrace bit to shadow stack global var
> > function_graph: Implement fgraph_reserve_data() and fgraph_retrieve_data()
> > function_graph: Add selftest for passing local variables
> >
> > ----
> > include/linux/ftrace.h | 43 +-
> > include/linux/sched.h | 2 +-
> > include/linux/trace_recursion.h | 39 -
> > kernel/trace/fgraph.c | 1044 ++++++++++++++++----
> > kernel/trace/ftrace.c | 522 +++++++++-
> > kernel/trace/ftrace_internal.h | 5 +-
> > kernel/trace/trace.h | 94 +-
> > kernel/trace/trace_functions.c | 8 +
> > kernel/trace/trace_functions_graph.c | 96 +-
> > kernel/trace/trace_irqsoff.c | 10 +-
> > kernel/trace/trace_sched_wakeup.c | 10 +-
> > kernel/trace/trace_selftest.c | 259 ++++-
> > .../selftests/ftrace/test.d/ftrace/fgraph-multi.tc | 103 ++
> > .../ftrace/test.d/ftrace/func-filter-pid.tc | 27 +-
> > 14 files changed, 1945 insertions(+), 317 deletions(-)
> > create mode 100644 tools/testing/selftests/ftrace/test.d/ftrace/fgraph-multi.tc
> >
> >
> > diff --git a/kernel/trace/ftrace.c b/kernel/trace/ftrace.c
> > index 41fabc6d30e4..da7e6abf48b4 100644
> > --- a/kernel/trace/ftrace.c
> > +++ b/kernel/trace/ftrace.c
> > @@ -3170,7 +3170,7 @@ int ftrace_shutdown(struct ftrace_ops *ops, int command)
> > /* Simply make a copy of @src and return it */
> > static struct ftrace_hash *copy_hash(struct ftrace_hash *src)
> > {
> > - if (!src || src == EMPTY_HASH)
> > + if (ftrace_hash_empty(src))
> > return EMPTY_HASH;
> >
> > return alloc_and_copy_ftrace_hash(src->size_bits, src);
> > @@ -3187,6 +3187,9 @@ static struct ftrace_hash *copy_hash(struct ftrace_hash *src)
> > *
> > * Otherwise, go through all of @new_hash and add anything that @hash
> > * doesn't already have, to @hash.
> > + *
> > + * The filter_hash updates uses just the append_hash() function
> > + * and the notrace_hash does not.
> > */
> > static int append_hash(struct ftrace_hash **hash, struct ftrace_hash *new_hash)
> > {
> > @@ -3195,11 +3198,11 @@ static int append_hash(struct ftrace_hash **hash, struct ftrace_hash *new_hash)
> > int i;
> >
> > /* An empty hash does everything */
> > - if (!*hash || *hash == EMPTY_HASH)
> > + if (ftrace_hash_empty(*hash))
> > return 0;
> >
> > /* If new_hash has everything make hash have everything */
> > - if (!new_hash || new_hash == EMPTY_HASH) {
> > + if (ftrace_hash_empty(new_hash)) {
> > free_ftrace_hash(*hash);
> > *hash = EMPTY_HASH;
> > return 0;
> > @@ -3217,7 +3220,12 @@ static int append_hash(struct ftrace_hash **hash, struct ftrace_hash *new_hash)
> > return 0;
> > }
> >
> > -/* Add to @hash only those that are in both @new_hash1 and @new_hash2 */
> > +/*
> > + * Add to @hash only those that are in both @new_hash1 and @new_hash2
> > + *
> > + * The notrace_hash updates uses just the intersect_hash() function
> > + * and the filter_hash does not.
> > + */
> > static int intersect_hash(struct ftrace_hash **hash, struct ftrace_hash *new_hash1,
> > struct ftrace_hash *new_hash2)
> > {
> > @@ -3229,8 +3237,7 @@ static int intersect_hash(struct ftrace_hash **hash, struct ftrace_hash *new_has
> > * If new_hash1 or new_hash2 is the EMPTY_HASH then make the hash
> > * empty as well as empty for notrace means none are notraced.
> > */
> > - if (!new_hash1 || new_hash1 == EMPTY_HASH ||
> > - !new_hash2 || new_hash2 == EMPTY_HASH) {
> > + if (ftrace_hash_empty(new_hash1) || ftrace_hash_empty(new_hash2)) {
> > free_ftrace_hash(*hash);
> > *hash = EMPTY_HASH;
> > return 0;
> > @@ -3245,6 +3252,11 @@ static int intersect_hash(struct ftrace_hash **hash, struct ftrace_hash *new_has
> > return -ENOMEM;
> > }
> > }
> > + /* If nothing intersects, make it the empty set */
> > + if (ftrace_hash_empty(*hash)) {
> > + free_ftrace_hash(*hash);
> > + *hash = EMPTY_HASH;
> > + }
> > return 0;
> > }
> >
> > @@ -3266,7 +3278,7 @@ static struct ftrace_hash *append_hashes(struct ftrace_ops *ops)
> > return NULL;
> > }
> > /* Nothing more to do if new_hash is empty */
> > - if (new_hash == EMPTY_HASH)
> > + if (ftrace_hash_empty(new_hash))
> > break;
> > }
> > return new_hash;
> > @@ -3300,59 +3312,76 @@ static struct ftrace_hash *intersect_hashes(struct ftrace_ops *ops)
> > return NULL;
> > }
> > /* Nothing more to do if new_hash is empty */
> > - if (new_hash == EMPTY_HASH)
> > + if (ftrace_hash_empty(new_hash))
> > break;
> > }
> > return new_hash;
> > }
> >
> > -/* Returns 0 on equal or non-zero on non-equal */
> > -static int compare_ops(struct ftrace_hash *A, struct ftrace_hash *B)
> > +static bool ops_equal(struct ftrace_hash *A, struct ftrace_hash *B)
> > {
> > struct ftrace_func_entry *entry;
> > int size;
> > int i;
> >
> > - if (!A || A == EMPTY_HASH)
> > - return !(!B || B == EMPTY_HASH);
> > + if (ftrace_hash_empty(A))
> > + return ftrace_hash_empty(B);
> >
> > - if (!B || B == EMPTY_HASH)
> > - return !(!A || A == EMPTY_HASH);
> > + if (ftrace_hash_empty(B))
> > + return ftrace_hash_empty(A);
> >
> > if (A->count != B->count)
> > - return 1;
> > + return false;
> >
> > size = 1 << A->size_bits;
> > for (i = 0; i < size; i++) {
> > hlist_for_each_entry(entry, &A->buckets[i], hlist) {
> > if (!__ftrace_lookup_ip(B, entry->ip))
> > - return 1;
> > + return false;
> > }
> > }
> >
> > - return 0;
> > + return true;
> > }
> >
> > -static int ftrace_hash_move_and_update_ops(struct ftrace_ops *ops,
> > - struct ftrace_hash **orig_hash,
> > - struct ftrace_hash *hash,
> > - int enable);
> > +static void ftrace_ops_update_code(struct ftrace_ops *ops,
> > + struct ftrace_ops_hash *old_hash);
> > +
> > +static int __ftrace_hash_move_and_update_ops(struct ftrace_ops *ops,
> > + struct ftrace_hash **orig_hash,
> > + struct ftrace_hash *hash,
> > + int enable)
> > +{
> > + struct ftrace_ops_hash old_hash_ops;
> > + struct ftrace_hash *old_hash;
> > + int ret;
> > +
> > + old_hash = *orig_hash;
> > + old_hash_ops.filter_hash = ops->func_hash->filter_hash;
> > + old_hash_ops.notrace_hash = ops->func_hash->notrace_hash;
> > + ret = ftrace_hash_move(ops, enable, orig_hash, hash);
> > + if (!ret) {
> > + ftrace_ops_update_code(ops, &old_hash_ops);
> > + free_ftrace_hash_rcu(old_hash);
> > + }
> > + return ret;
> > +}
> >
> > static int ftrace_update_ops(struct ftrace_ops *ops, struct ftrace_hash *filter_hash,
> > struct ftrace_hash *notrace_hash)
> > {
> > int ret;
> >
> > - if (compare_ops(filter_hash, ops->func_hash->filter_hash)) {
> > - ret = ftrace_hash_move_and_update_ops(ops, &ops->func_hash->filter_hash,
> > - filter_hash, 1);
> > + if (!ops_equal(filter_hash, ops->func_hash->filter_hash)) {
> > + ret = __ftrace_hash_move_and_update_ops(ops, &ops->func_hash->filter_hash,
> > + filter_hash, 1);
> > if (ret < 0)
> > return ret;
> > }
> >
> > - if (compare_ops(notrace_hash, ops->func_hash->notrace_hash)) {
> > - ret = ftrace_hash_move_and_update_ops(ops, &ops->func_hash->notrace_hash,
> > - notrace_hash, 0);
> > + if (!ops_equal(notrace_hash, ops->func_hash->notrace_hash)) {
> > + ret = __ftrace_hash_move_and_update_ops(ops, &ops->func_hash->notrace_hash,
> > + notrace_hash, 0);
> > if (ret < 0)
> > return ret;
> > }
> > @@ -3438,8 +3467,8 @@ int ftrace_startup_subops(struct ftrace_ops *ops, struct ftrace_ops *subops, int
> > * o If either notrace_hash is empty then the final stays empty
> > * o Otherwise, the final is an intersection between the hashes
> > */
> > - if (ops->func_hash->filter_hash == EMPTY_HASH ||
> > - subops->func_hash->filter_hash == EMPTY_HASH) {
> > + if (ftrace_hash_empty(ops->func_hash->filter_hash) ||
> > + ftrace_hash_empty(subops->func_hash->filter_hash)) {
> > filter_hash = EMPTY_HASH;
> > } else {
> > size_bits = max(ops->func_hash->filter_hash->size_bits,
> > @@ -3454,8 +3483,8 @@ int ftrace_startup_subops(struct ftrace_ops *ops, struct ftrace_ops *subops, int
> > }
> > }
> >
> > - if (ops->func_hash->notrace_hash == EMPTY_HASH ||
> > - subops->func_hash->notrace_hash == EMPTY_HASH) {
> > + if (ftrace_hash_empty(ops->func_hash->notrace_hash) ||
> > + ftrace_hash_empty(subops->func_hash->notrace_hash)) {
> > notrace_hash = EMPTY_HASH;
> > } else {
> > size_bits = max(ops->func_hash->filter_hash->size_bits,
> > @@ -3591,7 +3620,7 @@ static int ftrace_hash_move_and_update_subops(struct ftrace_ops *subops,
> > }
> >
> > /* Move the hash over to the new hash */
> > - ret = ftrace_hash_move_and_update_ops(ops, orig_hash, new_hash, enable);
> > + ret = __ftrace_hash_move_and_update_ops(ops, orig_hash, new_hash, enable);
> >
> > free_ftrace_hash(new_hash);
> >
> > @@ -4822,11 +4851,6 @@ static int ftrace_hash_move_and_update_ops(struct ftrace_ops *ops,
> > struct ftrace_hash *hash,
> > int enable)
> > {
> > - struct ftrace_ops_hash old_hash_ops;
> > - struct ftrace_hash *old_hash;
> > - struct ftrace_ops *op;
> > - int ret;
> > -
> > if (ops->flags & FTRACE_OPS_FL_SUBOP)
> > return ftrace_hash_move_and_update_subops(ops, orig_hash, hash, enable);
> >
> > @@ -4838,6 +4862,8 @@ static int ftrace_hash_move_and_update_ops(struct ftrace_ops *ops,
> > * it will not affect subops that share it.
> > */
> > if (!(ops->flags & FTRACE_OPS_FL_ENABLED)) {
> > + struct ftrace_ops *op;
> > +
> > /* Check if any other manager subops maps to this hash */
> > do_for_each_ftrace_op(op, ftrace_ops_list) {
> > struct ftrace_ops *subops;
> > @@ -4851,15 +4877,7 @@ static int ftrace_hash_move_and_update_ops(struct ftrace_ops *ops,
> > } while_for_each_ftrace_op(op);
> > }
> >
> > - old_hash = *orig_hash;
> > - old_hash_ops.filter_hash = ops->func_hash->filter_hash;
> > - old_hash_ops.notrace_hash = ops->func_hash->notrace_hash;
> > - ret = ftrace_hash_move(ops, enable, orig_hash, hash);
> > - if (!ret) {
> > - ftrace_ops_update_code(ops, &old_hash_ops);
> > - free_ftrace_hash_rcu(old_hash);
> > - }
> > - return ret;
> > + return __ftrace_hash_move_and_update_ops(ops, orig_hash, hash, enable);
> > }
> >
> > static bool module_exists(const char *module)
>