Re: [PATCH v9 7/9] perf c2c: build and finalize the function view hierarchy
From: Ian Rogers
Date: Mon Aug 17 2026 - 17:11:30 EST
On Mon, Aug 17, 2026 at 2:40 AM Jiebin Sun <jiebin.sun@xxxxxxxxx> wrote:
>
> Add the builder that walks the top-level cacheline entries and, for each
> read-side function, correlates the functions that write the same lines
> (level 2) and the specific cachelines they contend over (level 3) within
> each retained detail histogram. Aggregate the write traffic per contending
> function, resort by store count, and prune writers/functions with no
> contention. The finalize pass then computes the Cycles % denominator from
> the surviving level-1 entries after pruning, so the column shows each
> function's share of the functions retained in the table rather than of the
> whole recording -- the semantics documented for Cycles % in perf-c2c.txt.
>
> Expose c2c_function__build() and c2c_function__reset() for the TUI front
> end added by the next patch. The builder requires iaddr in the cacheline
> coalescing fields and returns the completed hists through an output
> argument. Validate the inputs before replacing an existing model.
>
> Function-view entries do not carry callchains. Suppress callchain handling
> while building and tearing down the model so the common API does not depend
> on the caller's current callchain setting.
>
> Signed-off-by: Jiebin Sun <jiebin.sun@xxxxxxxxx>
> Cc: Adrian Hunter <adrian.hunter@xxxxxxxxx>
> Cc: Alexander Shishkin <alexander.shishkin@xxxxxxxxxxxxxxx>
> Cc: Arnaldo Carvalho de Melo <acme@xxxxxxxxxx>
> Cc: Dapeng Mi <dapeng1.mi@xxxxxxxxxxxxxxx>
> Cc: Ian Rogers <irogers@xxxxxxxxxx>
> Cc: Ingo Molnar <mingo@xxxxxxxxxx>
> Cc: James Clark <james.clark@xxxxxxxxxx>
> Cc: Jiri Olsa <jolsa@xxxxxxxxxx>
> Cc: Mark Rutland <mark.rutland@xxxxxxx>
> Cc: Namhyung Kim <namhyung@xxxxxxxxxx>
> Cc: Peter Zijlstra <peterz@xxxxxxxxxxxxx>
> Cc: Thomas Falcon <thomas.falcon@xxxxxxxxx>
> Reviewed-by: Tianyou Li <tianyou.li@xxxxxxxxx>
> Reviewed-by: Wangyang Guo <wangyang.guo@xxxxxxxxx>
Reviewed-by: Ian Rogers <irogers@xxxxxxxxxx>
Thanks,
Ian
> ---
> tools/perf/util/c2c-function.c | 530 ++++++++++++++++++++++++++++++++-
> tools/perf/util/c2c.h | 10 +
> 2 files changed, 525 insertions(+), 15 deletions(-)
>
> diff --git a/tools/perf/util/c2c-function.c b/tools/perf/util/c2c-function.c
> index c005fb5454a7..5b6a06a5a067 100644
> --- a/tools/perf/util/c2c-function.c
> +++ b/tools/perf/util/c2c-function.c
> @@ -45,9 +45,9 @@ struct c2c_function_model {
> bool symbol_full;
> };
>
> -static struct c2c_function_model c2c_ext __maybe_unused;
> +static struct c2c_function_model c2c_ext;
>
> -static inline __maybe_unused u64 c2c_hitm_count(const struct c2c_stats *stats)
> +static inline u64 c2c_hitm_count(const struct c2c_stats *stats)
> {
> return stats->tot_hitm;
> }
> @@ -82,7 +82,7 @@ static int64_t c2c_function_cmp(const struct map_symbol *left,
> return _sort__sym_cmp(left->sym, right->sym);
> }
>
> -static inline __maybe_unused u64 hist_entry__iaddr(struct hist_entry *he)
> +static inline u64 hist_entry__iaddr(struct hist_entry *he)
> {
> if (he->mem_info)
> return mem_info__iaddr(he->mem_info)->addr;
> @@ -219,7 +219,7 @@ static u64 c2c_hist_entry__cycles(struct c2c_hist_entry *c2c_he)
> }
>
> /* Sum c2c_hist_entry__cycles() across all level-1 entries. */
> -static u64 __maybe_unused c2c_ext__total_cycles(void)
> +static u64 c2c_ext__total_cycles(void)
> {
> struct rb_node *nd;
> u64 total = 0;
> @@ -359,7 +359,7 @@ cycles_percent_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp,
>
> c2c_he = container_of(he, struct c2c_hist_entry, he);
> fn_cycles = c2c_hist_entry__cycles(c2c_he);
> - /* Populated by build_function_view_hierarchy() once the L1 tree is built. */
> + /* Populated by c2c_function__build() once the L1 tree is built. */
> total_cycles = c2c_ext.total_cycles;
> pct = total_cycles > 0 ? (double)fn_cycles / total_cycles * 100.0 : 0.0;
>
> @@ -665,7 +665,7 @@ function_hpp_list__parse(struct perf_hpp_list *hpp_list,
> return ret;
> }
>
> -static int __maybe_unused
> +static int
> c2c_function_hists__init(struct c2c_hists *hists,
> const char *sort,
> int nr_header_lines,
> @@ -680,7 +680,7 @@ c2c_function_hists__init(struct c2c_hists *hists,
> return function_hpp_list__parse(&hists->list, /*output=*/NULL, sort, env);
> }
>
> -static int __maybe_unused
> +static int
> c2c_function_hists__reinit(struct c2c_hists *c2c_hists,
> const char *output,
> const char *sort,
> @@ -730,8 +730,8 @@ static void c2c_stats_merge(struct stats *dest, const struct stats *src)
> }
>
> /* Merge compute_stats during function aggregation. */
> -static void __maybe_unused c2c_add_cstats(struct compute_stats *dest,
> - const struct compute_stats *src)
> +static void c2c_add_cstats(struct compute_stats *dest,
> + const struct compute_stats *src)
> {
> c2c_stats_merge(&dest->rmt_hitm, &src->rmt_hitm);
> c2c_stats_merge(&dest->lcl_hitm, &src->lcl_hitm);
> @@ -740,8 +740,8 @@ static void __maybe_unused c2c_add_cstats(struct compute_stats *dest,
> c2c_stats_merge(&dest->load, &src->load);
> }
>
> -static bool __maybe_unused hist_entry__add_c2c_stats(struct hist_entry *he,
> - const struct c2c_stats *stats)
> +static bool hist_entry__add_c2c_stats(struct hist_entry *he,
> + const struct c2c_stats *stats)
> {
> u64 nr_events = c2c_hitm_count(stats) + stats->rmt_peer + stats->lcl_peer;
> u64 weight1 = c2c_hitm_count(stats);
> @@ -832,7 +832,7 @@ static void c2c_he__free_hierarchy(struct hist_entry *he)
> * line, so this is mainly a safety net. Returns the number of surviving
> * writers.
> */
> -static int __maybe_unused c2c_he__prune_empty_writers(struct hist_entry *l1_he)
> +static int c2c_he__prune_empty_writers(struct hist_entry *l1_he)
> {
> struct rb_node *nd;
> int surviving = 0;
> @@ -953,7 +953,7 @@ c2c_child_entry__insert(struct hist_entry *parent_he, struct hist_entry *child_h
> parent_he->leaf = false;
> }
>
> -static __maybe_unused struct hist_entry *
> +static struct hist_entry *
> c2c_function_hists__level1_entry(struct symbol *sym,
> struct hist_entry *detail_he,
> struct thread *synthetic_thread)
> @@ -1029,7 +1029,7 @@ c2c_function_hists__level1_entry(struct symbol *sym,
> * sort semantics. All code addresses and cachelines for the same writer
> * function aggregate into one row.
> */
> -static __maybe_unused struct c2c_hist_entry *
> +static struct c2c_hist_entry *
> c2c_function_hists__level2_entry(struct c2c_hist_entry *level1_c2c,
> struct symbol *sym, struct hist_entry *detail_he)
> {
> @@ -1077,7 +1077,7 @@ c2c_function_hists__level2_entry(struct c2c_hist_entry *level1_c2c,
> }
>
> /* Level 3: one source cacheline where the L1/L2 functions contend. */
> -static __maybe_unused struct c2c_hist_entry *
> +static struct c2c_hist_entry *
> c2c_function_hists__level3_entry(struct c2c_hist_entry *level2_c2c,
> struct c2c_hist_entry *cacheline_src_he)
> {
> @@ -1140,3 +1140,503 @@ struct hist_entry *c2c_function__find_cacheline(struct hist_entry *he_selection)
>
> return NULL;
> }
> +
> +/*
> + * Re-sort child entries of @parent_he by total store count, descending.
> + */
> +static void c2c_he__resort_by_stores(struct hist_entry *parent_he)
> +{
> + struct rb_root_cached new_root = RB_ROOT_CACHED;
> + struct rb_node *nd;
> +
> + if (!parent_he->has_children)
> + return;
> +
> + /* Extract all nodes and re-insert sorted by displayed store count */
> + while ((nd = rb_first_cached(&parent_he->hroot_out))) {
> + struct hist_entry *he = rb_entry(nd, struct hist_entry, rb_node);
> + u64 he_store = hist_entry__displayed_stores(he);
> + struct rb_node **p = &new_root.rb_root.rb_node;
> + struct rb_node *parent = NULL;
> + bool leftmost = true;
> + int cmp;
> +
> + /* Remove from current tree */
> + rb_erase_cached(&he->rb_node, &parent_he->hroot_out);
> +
> + /*
> + * Insert sorted by store count, descending. Use the displayed
> + * store count so a level-1 function and level-2 writer (whose own
> + * stats.store is 0 / partial) sort by the aggregated write traffic
> + * beneath them, not by their own store field.
> + */
> + while (*p) {
> + struct hist_entry *iter = rb_entry(*p, struct hist_entry, rb_node);
> + u64 iter_store = hist_entry__displayed_stores(iter);
> +
> + parent = *p;
> + if (he_store != iter_store) {
> + cmp = he_store > iter_store ? -1 : 1;
> + } else {
> + /* Stable tie-break: instruction address, name, then cacheline. */
> + u64 a = hist_entry__iaddr(he), b = hist_entry__iaddr(iter);
> +
> + if (a != b)
> + cmp = a < b ? -1 : 1;
> + else if (he->ms.sym && iter->ms.sym)
> + cmp = strcmp(he->ms.sym->name,
> + iter->ms.sym->name);
> + else
> + cmp = (iter->ms.sym ? 1 : 0) - (he->ms.sym ? 1 : 0);
> +
> + if (!cmp) {
> + struct c2c_hist_entry *he_c2c;
> + struct c2c_hist_entry *iter_c2c;
> +
> + he_c2c = container_of(he, struct c2c_hist_entry, he);
> + iter_c2c = container_of(iter, struct c2c_hist_entry, he);
> + if (he_c2c->cacheline_idx != iter_c2c->cacheline_idx)
> + cmp = he_c2c->cacheline_idx <
> + iter_c2c->cacheline_idx ? -1 : 1;
> + }
> + }
> +
> + if (cmp < 0) {
> + p = &parent->rb_left;
> + } else {
> + p = &parent->rb_right;
> + leftmost = false;
> + }
> + }
> +
> + rb_link_node(&he->rb_node, parent, p);
> + rb_insert_color_cached(&he->rb_node, &new_root, leftmost);
> + }
> +
> + parent_he->hroot_out = new_root;
> +}
> +
> +/* Initial per-cacheline capacity for the seen[] set; grown on demand. */
> +#define DEFAULT_SYMBOLS_PER_CL 64
> +
> +struct function_seen {
> + struct map_symbol ms;
> +};
> +
> +static bool function_seen__find(const struct function_seen *seen, int nr,
> + const struct map_symbol *ms)
> +{
> + int i;
> +
> + for (i = 0; i < nr; i++) {
> + if (!c2c_function_cmp(&seen[i].ms, ms))
> + return true;
> + }
> + return false;
> +}
> +
> +/* Aggregate stats from the cacheline-side entry @c2c_b into level 2/3 @dst. */
> +static bool c2c_he__add_sharing(struct c2c_hist_entry *dst, struct c2c_hist_entry *src)
> +{
> + /* Do the fallible update first so a failure leaves dst unmodified. */
> + if (!hist_entry__add_c2c_stats(&dst->he, &src->stats))
> + return false;
> +
> + c2c_add_stats(&dst->stats, &src->stats);
> + c2c_add_cstats(&dst->cstats, &src->cstats);
> + return true;
> +}
> +
> +/*
> + * Process one cacheline: for every function reading it, create/update its
> + * level-1 function entry, then for each function that writes the line
> + * add it as a level-2 writer and add this cacheline as a level-3 child.
> + */
> +static int c2c_function__process_cl(struct c2c_hist_entry *cacheline_he,
> + struct thread *synthetic_thread)
> +{
> + struct rb_node *nd_a, *nd_b;
> + struct function_seen *seen = NULL;
> + int nr_seen = 0, nr_alloc = 0;
> + int ret = 0;
> +
> + for (nd_a = rb_first_cached(&cacheline_he->hists->hists.entries); nd_a;
> + nd_a = rb_next(nd_a)) {
> + struct hist_entry *he_a = rb_entry(nd_a, struct hist_entry, rb_node);
> + struct c2c_hist_entry *c2c_a;
> + struct hist_entry *level1_he;
> + struct c2c_hist_entry *level1_c2c;
> +
> + if (!he_a->ms.sym || he_a->filtered)
> + continue;
> +
> + c2c_a = container_of(he_a, struct c2c_hist_entry, he);
> + if (c2c_a->stats.load == 0)
> + continue;
> +
> + level1_he = c2c_function_hists__level1_entry(he_a->ms.sym,
> + he_a, synthetic_thread);
> + if (!level1_he) {
> + ret = -ENOMEM;
> + goto out;
> + }
> +
> + level1_c2c = container_of(level1_he, struct c2c_hist_entry, he);
> +
> + /*
> + * Aggregate every source entry into its level-1 function parent.
> + * level1_he is keyed by symbol, so all code addresses inside the
> + * same function collapse into one parent. When the cacheline view
> + * splits a function into siblings (different code addresses, or
> + * --coalesce pid/tid/dso), each sibling holds a DISJOINT slice of the
> + * traffic, so summing them here is correct accumulation, not
> + * double counting. The seen[] set below therefore guards only the
> + * subtree build (to avoid building a function's level-2/3 subtree
> + * twice for the same cacheline), never this L1 update. Update
> + * he->stat first; on failure leave the aggregates untouched.
> + */
> + if (!hist_entry__add_c2c_stats(level1_he, &c2c_a->stats)) {
> + ret = -ENOMEM;
> + goto out;
> + }
> + c2c_add_stats(&level1_c2c->stats, &c2c_a->stats);
> + c2c_add_cstats(&level1_c2c->cstats, &c2c_a->cstats);
> + c2c_add_stats(&c2c_ext.function_hists.stats, &c2c_a->stats);
> +
> + /* Build this function's subtree for this cacheline only once. */
> + if (function_seen__find(seen, nr_seen, &he_a->ms))
> + continue;
> +
> + if (nr_seen == nr_alloc) {
> + struct function_seen *tmp;
> + int new_alloc = nr_alloc ? nr_alloc * 2 : DEFAULT_SYMBOLS_PER_CL;
> +
> + tmp = reallocarray(seen, new_alloc, sizeof(*seen));
> + if (!tmp) {
> + ret = -ENOMEM;
> + goto out;
> + }
> + seen = tmp;
> + nr_alloc = new_alloc;
> + }
> + seen[nr_seen].ms = he_a->ms;
> + nr_seen++;
> +
> + for (nd_b = rb_first_cached(&cacheline_he->hists->hists.entries); nd_b;
> + nd_b = rb_next(nd_b)) {
> + struct hist_entry *he_b = rb_entry(nd_b, struct hist_entry, rb_node);
> + struct c2c_hist_entry *c2c_b, *level2_c2c, *level3_c2c;
> +
> + if (!he_b->ms.sym || he_b->filtered)
> + continue;
> +
> + c2c_b = container_of(he_b, struct c2c_hist_entry, he);
> +
> + /*
> + * The level-1 function contributes read-side load weight for this
> + * cacheline. Associate it with functions sampled storing to the
> + * same line.
> + * The writer can be the same function; after detail coalescing and
> + * function-level grouping there is not enough identity to attribute
> + * that case to a specific thread.
> + * Only writers are contending functions, so keep the ones
> + * that actually store into the line.
> + */
> + if (c2c_b->stats.store == 0)
> + continue;
> +
> + /* Level 2: the writing function (aggregated across cachelines). */
> + level2_c2c = c2c_function_hists__level2_entry(level1_c2c, he_b->ms.sym,
> + he_b);
> + if (!level2_c2c || !c2c_he__add_sharing(level2_c2c, c2c_b)) {
> + ret = -ENOMEM;
> + goto out;
> + }
> +
> + /* Level 3: the specific cacheline they contend over. */
> + level3_c2c = c2c_function_hists__level3_entry(level2_c2c,
> + cacheline_he);
> + if (!level3_c2c || !c2c_he__add_sharing(level3_c2c, c2c_b)) {
> + ret = -ENOMEM;
> + goto out;
> + }
> + }
> + }
> +
> +out:
> + free(seen);
> + return ret;
> +}
> +
> +/*
> + * Remove a level-1 function that has no contended cachelines left. It is a
> + * normal (owned) hist_entry in function_hists, so mirror hists__delete_entry()
> + * for the no-collapse case: unlink from both trees, fix the counters, then
> + * delete. Its hroot_out is already empty after pruning.
> + */
> +static void c2c_function__drop_level1(struct hist_entry *he)
> +{
> + struct hists *hists = &c2c_ext.function_hists.hists;
> +
> + rb_erase_cached(&he->rb_node_in, hists->entries_in);
> + rb_erase_cached(&he->rb_node, &hists->entries);
> +
> + --hists->nr_entries;
> + if (!he->filtered)
> + --hists->nr_non_filtered_entries;
> +
> + hist_entry__delete(he);
> +}
> +
> +/* Length of the identity text (symbol name or cacheline address) at @he. */
> +static int c2c_function__ident_len(struct hist_entry *he)
> +{
> + char buf[512];
> + char *symbuf;
> + size_t size;
> + int len;
> +
> + if (hist_entry__is_cacheline(he)) {
> + u64 addr = he->mem_info ?
> + cl_address(mem_info__daddr(he->mem_info)->addr, chk_double_cl) : 0;
> +
> + return scnprintf(buf, sizeof(buf), "0x%" PRIx64, addr);
> + }
> +
> + if (!he->ms.sym)
> + return 0;
> +
> + /*
> + * Match symbol_view_entry(): sort_sym adds the cpumode prefix and, in
> + * verbose mode, the address and DSO origin before the symbol name.
> + */
> + size = strlen(he->ms.sym->name) + 64;
> + symbuf = malloc(size);
> + if (!symbuf)
> + return size - 1;
> +
> + len = sort_sym.se_snprintf(he, symbuf, size, size - 1);
> + free(symbuf);
> + return len;
> +}
> +
> +/*
> + * Grow the symbol column so the deepest, longest identity cell fits. The
> + * generic hists__calc_col_len() only measures the top-level (L1) entries; the
> + * hand-linked L2 writers and L3 cacheline addresses live in hroot_out and are
> + * never measured, so with a short L1 name the indented L2/L3 text would be
> + * truncated. Account for the per-level indent and the folded-sign prefix.
> + */
> +static void c2c_function__update_symbol_width(struct hist_entry *he)
> +{
> + struct hists *hists = &c2c_ext.function_hists.hists;
> + int need = he->depth * C2C_FUNC_INDENT + C2C_FUNC_FOLD_WIDTH +
> + c2c_function__ident_len(he);
> +
> + if (need > hists__col_len(hists, HISTC_SYMBOL))
> + hists__set_col_len(hists, HISTC_SYMBOL, need);
> +}
> +
> +/*
> + * Prune writers with no stores, drop functions left with no contending
> + * writer, sort the survivors by store count, then compute the global total.
> + */
> +static void c2c_function__finalize(void)
> +{
> + struct rb_node *nd_l1;
> +
> + nd_l1 = rb_first_cached(&c2c_ext.function_hists.hists.entries);
> + while (nd_l1) {
> + struct hist_entry *he_l1 = rb_entry(nd_l1, struct hist_entry, rb_node);
> + struct rb_node *next_l1 = rb_next(nd_l1);
> + struct rb_node *nd_l2;
> +
> + /* Drop writers with no stores before sorting. */
> + if (!he_l1->has_children || !c2c_he__prune_empty_writers(he_l1)) {
> + /* No contending writer: this function is not shared. */
> + c2c_function__drop_level1(he_l1);
> + nd_l1 = next_l1;
> + continue;
> + }
> +
> + c2c_he__resort_by_stores(he_l1);
> + c2c_function__update_symbol_width(he_l1);
> +
> + for (nd_l2 = rb_first_cached(&he_l1->hroot_out); nd_l2;
> + nd_l2 = rb_next(nd_l2)) {
> + struct hist_entry *he_l2 = rb_entry(nd_l2, struct hist_entry, rb_node);
> + struct rb_node *nd_l3;
> +
> + c2c_function__update_symbol_width(he_l2);
> +
> + if (he_l2->has_children)
> + c2c_he__resort_by_stores(he_l2);
> +
> + for (nd_l3 = rb_first_cached(&he_l2->hroot_out); nd_l3;
> + nd_l3 = rb_next(nd_l3)) {
> + struct hist_entry *he_l3 = rb_entry(nd_l3, struct hist_entry,
> + rb_node);
> +
> + c2c_function__update_symbol_width(he_l3);
> + }
> + }
> +
> + nd_l1 = next_l1;
> + }
> +
> + /*
> + * Compute the Cycles % denominator from the surviving level-1 entries
> + * after pruning, so the column shows each function's share of the
> + * functions retained in this table -- not of the whole recording. See
> + * the Cycles % description in perf-c2c.txt.
> + */
> + c2c_ext.total_cycles = c2c_ext__total_cycles();
> +}
> +
> +/*
> + * Release all per-visit function-view state. Keep the hists object itself
> + * initialized so its mutex is initialized exactly once across TAB re-entry.
> + */
> +void c2c_function__reset(void)
> +{
> + bool saved_use_callchain = symbol_conf.use_callchain;
> +
> + /*
> + * Function-view entries never carry callchains. Keep their generic
> + * destructor independent of the caller's current callchain setting.
> + */
> + symbol_conf.use_callchain = false;
> + hists__delete_all_entries(&c2c_ext.function_hists.hists);
> + if (c2c_ext.function_hists.list.fields.next)
> + perf_hpp__reset_output_field(&c2c_ext.function_hists.list);
> +
> + memset(&c2c_ext.function_hists.stats, 0,
> + sizeof(c2c_ext.function_hists.stats));
> + c2c_ext.total_cycles = 0;
> + c2c_ext.cl_hists = NULL;
> + c2c_ext.cl_sort = NULL;
> + c2c_ext.symbol_full = false;
> + symbol_conf.use_callchain = saved_use_callchain;
> +}
> +
> +static bool c2c_function__has_iaddr(const char *cl_sort)
> +{
> + const char *field = cl_sort;
> +
> + while (field && *field) {
> + const char *end = strchr(field, ',');
> + size_t len = end ? (size_t)(end - field) : strlen(field);
> +
> + if (len == sizeof("iaddr") - 1 && !strncmp(field, "iaddr", len))
> + return true;
> + field = end ? end + 1 : NULL;
> + }
> + return false;
> +}
> +
> +/*
> + * Build the three-level function view in a single pass over the cacheline
> + * entries:
> + * L1: read-side functions (aggregated across all their code addresses)
> + * L2: writing functions contending with each level-1 function
> + * L3: shared cachelines for each function pair
> + */
> +int c2c_function__build(struct c2c_hists *cl_hists, const char *cl_sort,
> + bool symbol_full, struct hists **hists)
> +{
> + static const char output_fields[] =
> + "cycles_percent,total_stores,symbol_view";
> + static bool hists_initialized;
> + struct rb_node *nd_cl;
> + bool saved_use_callchain;
> + int ret;
> +
> + if (!hists)
> + return -EINVAL;
> + *hists = NULL;
> +
> + if (!cl_hists || !cl_sort)
> + return -EINVAL;
> + if (!c2c_function__has_iaddr(cl_sort))
> + return -EOPNOTSUPP;
> +
> + saved_use_callchain = symbol_conf.use_callchain;
> + symbol_conf.use_callchain = false;
> + c2c_function__reset();
> +
> + c2c_ext.cl_hists = cl_hists;
> + c2c_ext.cl_sort = cl_sort;
> + c2c_ext.symbol_full = symbol_full;
> +
> + /*
> + * __hists__init() (called by c2c_function_hists__init()) also
> + * mutex_init()s the hists lock, so only run it once for this static
> + * hists; on re-entry just re-parse the columns via reinit().
> + */
> + if (!hists_initialized) {
> + ret = c2c_function_hists__init(&c2c_ext.function_hists,
> + "symbol_view", 2, NULL);
> + hists_initialized = true;
> + } else {
> + ret = c2c_function_hists__reinit(&c2c_ext.function_hists,
> + /*output=*/NULL, "symbol_view", NULL);
> + }
> + if (ret)
> + goto out_reset;
> +
> + nd_cl = rb_first_cached(&c2c_ext.cl_hists->hists.entries);
> +
> + /* An empty C2C report yields an empty (but valid) function view. */
> + for (; nd_cl; nd_cl = rb_next(nd_cl)) {
> + struct hist_entry *he_cl = rb_entry(nd_cl, struct hist_entry, rb_node);
> + struct c2c_hist_entry *cacheline_he = container_of(he_cl,
> + struct c2c_hist_entry, he);
> + struct thread *synthetic_thread = he_cl->thread;
> +
> + /*
> + * Detail hists are finalized only for cachelines retained by the
> + * top-level C2C filter. Among those, include any line with sharing
> + * activity, not just HITM.
> + */
> + if (he_cl->filtered ||
> + (c2c_hitm_count(&cacheline_he->stats) == 0 &&
> + cacheline_he->stats.tot_peer == 0 &&
> + cacheline_he->stats.store == 0 &&
> + cacheline_he->stats.load == 0) ||
> + !cacheline_he->hists ||
> + RB_EMPTY_ROOT(&cacheline_he->hists->hists.entries.rb_root) ||
> + !he_cl->mem_info || !synthetic_thread)
> + continue;
> +
> + ret = c2c_function__process_cl(cacheline_he, synthetic_thread);
> + if (ret)
> + goto out_err;
> + }
> +
> + ret = c2c_function_hists__reinit(&c2c_ext.function_hists, output_fields,
> + "cycles_percent", NULL);
> + if (ret)
> + goto out_err;
> +
> + hists__collapse_resort(&c2c_ext.function_hists.hists, NULL);
> + hists__output_resort(&c2c_ext.function_hists.hists, NULL);
> +
> + c2c_function__finalize();
> +
> + *hists = &c2c_ext.function_hists.hists;
> + symbol_conf.use_callchain = saved_use_callchain;
> + return 0;
> +
> +out_err:
> + /*
> + * On error, migrate any entries still in entries_in to entries and
> + * delete them, so a later rebuild does not strand them (the top-level
> + * __hists__init() memset would otherwise lose the pointers).
> + */
> + hists__collapse_resort(&c2c_ext.function_hists.hists, NULL);
> + hists__output_resort(&c2c_ext.function_hists.hists, NULL);
> +out_reset:
> + c2c_function__reset();
> + symbol_conf.use_callchain = saved_use_callchain;
> + return ret;
> +}
> diff --git a/tools/perf/util/c2c.h b/tools/perf/util/c2c.h
> index f95e636edc0f..6548b9ba81f1 100644
> --- a/tools/perf/util/c2c.h
> +++ b/tools/perf/util/c2c.h
> @@ -98,6 +98,16 @@ struct c2c_fmt {
> void c2c_fmt_free(struct perf_hpp_fmt *fmt);
> bool c2c_fmt_equal(struct perf_hpp_fmt *a, struct perf_hpp_fmt *b);
>
> +/*
> + * Build the function-view hierarchy. Returns -EOPNOTSUPP when @cl_sort lacks
> + * iaddr. On success, *@hists remains valid until the next
> + * c2c_function__build() or c2c_function__reset(). On failure, *@hists is
> + * NULL.
> + */
> +int c2c_function__build(struct c2c_hists *cl_hists, const char *cl_sort,
> + bool symbol_full, struct hists **hists);
> +void c2c_function__reset(void);
> +/* Valid only between a successful build and c2c_function__reset(). */
> struct hist_entry *c2c_function__find_cacheline(struct hist_entry *he);
>
> #endif /* __PERF_UTIL_C2C_H */
> --
> 2.52.0
>