summaryrefslogtreecommitdiff
path: root/tools/perf/util/c2c-function.c
diff options
context:
space:
mode:
Diffstat (limited to 'tools/perf/util/c2c-function.c')
-rw-r--r--tools/perf/util/c2c-function.c1642
1 files changed, 1642 insertions, 0 deletions
diff --git a/tools/perf/util/c2c-function.c b/tools/perf/util/c2c-function.c
new file mode 100644
index 000000000000..5b6a06a5a067
--- /dev/null
+++ b/tools/perf/util/c2c-function.c
@@ -0,0 +1,1642 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * C2C function model - function-level cacheline sharing analysis
+ *
+ * Displays a 3-level hierarchy showing which functions share cachelines:
+ * Level 1: Read-side functions sorted by Cycles % (estimated load cycles)
+ * Level 2: Functions sampled writing the shared lines read by level 1
+ * Level 3: The specific cachelines where the two functions contend
+ *
+ * Builds the hierarchy from the existing cacheline histograms
+ * (c2c_hist_entry->hists), reusing the shared c2c data structures.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <string.h>
+#include <tools/libc_compat.h> /* reallocarray */
+#include <linux/list.h>
+#include <linux/rbtree.h>
+#include <linux/zalloc.h>
+
+#include "addr_location.h"
+#include "c2c.h"
+#include "cacheline.h"
+#include "debug.h"
+#include "dso.h"
+#include "hist.h"
+#include "map.h"
+#include "mem-events.h"
+#include "mem-info.h"
+#include "sort.h"
+#include "symbol.h"
+#include "thread.h"
+
+struct c2c_function_model {
+ struct c2c_hists function_hists;
+ /* Total estimated cycles across all level-1 entries. */
+ u64 total_cycles;
+ /* Source cacheline histograms; not owned here. */
+ struct c2c_hists *cl_hists;
+ /* --coalesce field list, used to require iaddr. */
+ const char *cl_sort;
+ /* Do not cap long symbol names. */
+ bool symbol_full;
+};
+
+static struct c2c_function_model c2c_ext;
+
+static inline u64 c2c_hitm_count(const struct c2c_stats *stats)
+{
+ return stats->tot_hitm;
+}
+
+static int64_t c2c_function_cmp(const struct map_symbol *left,
+ const struct map_symbol *right)
+{
+ const struct dso *left_dso = left->map ? map__dso(left->map) : NULL;
+ const struct dso *right_dso = right->map ? map__dso(right->map) : NULL;
+ int ret;
+
+ if (!left_dso || !right_dso) {
+ if (left_dso != right_dso)
+ return left_dso ? 1 : -1;
+ } else {
+ /*
+ * Use the same DSO name as _sort__dso_cmp() (short name unless
+ * verbose), so this matches the DSO comparison the level-1
+ * entries are deduplicated by; otherwise same-basename DSOs
+ * could be split or merged inconsistently across levels.
+ */
+ const char *left_name = verbose > 0 ?
+ dso__long_name(left_dso) : dso__short_name(left_dso);
+ const char *right_name = verbose > 0 ?
+ dso__long_name(right_dso) : dso__short_name(right_dso);
+
+ ret = strcmp(left_name, right_name);
+ if (ret)
+ return ret;
+ }
+
+ return _sort__sym_cmp(left->sym, right->sym);
+}
+
+static inline u64 hist_entry__iaddr(struct hist_entry *he)
+{
+ if (he->mem_info)
+ return mem_info__iaddr(he->mem_info)->addr;
+ return he->ip;
+}
+
+/*
+ * Hierarchy levels (by depth): L1 = read-side function, L2 = the writing
+ * function it contends with, L3 = the specific shared cacheline.
+ */
+static inline bool hist_entry__is_cacheline(struct hist_entry *he)
+{
+ return he->parent_he && he->parent_he->parent_he; /* level 3: cacheline */
+}
+
+/* Spaces of indent per hierarchy level, like the normal report view. */
+#define C2C_FUNC_INDENT 2
+
+/* Width of the folded-sign prefix ("%c ") each identity cell emits. */
+#define C2C_FUNC_FOLD_WIDTH 2
+
+/*
+ * Write he->depth levels of leading indentation into @buf, so lower-level
+ * entries are visually nested under their parent. Returns bytes written.
+ */
+static int hist_entry__indent(struct hist_entry *he, char *buf, size_t size)
+{
+ int indent = he->depth * C2C_FUNC_INDENT;
+
+ if (indent <= 0 || (size_t)indent >= size)
+ return 0;
+
+ return scnprintf(buf, size, "%*s", indent, "");
+}
+
+static int symbol_width(struct hists *hists, struct sort_entry *se)
+{
+ int width = hists__col_len(hists, se->se_width_idx);
+
+ /*
+ * Cap long symbol names as the cacheline view does. The stored column
+ * length is grown up front to fit the deepest, longest identity cell
+ * (including a level-3 cacheline address), so this cap never shrinks the
+ * column below what the cacheline address needs.
+ */
+ if (!c2c_ext.symbol_full && width > SYMBOL_WIDTH)
+ width = SYMBOL_WIDTH;
+
+ return width;
+}
+
+static struct c2c_dimension dim_symbol_view;
+
+/*
+ * c2c_width - Calculate width for a C2C column in function view
+ */
+static int c2c_width(struct perf_hpp_fmt *fmt,
+ struct perf_hpp *hpp __maybe_unused,
+ struct hists *hists)
+{
+ struct c2c_fmt *c2c_fmt;
+ struct c2c_dimension *dim;
+
+ c2c_fmt = container_of(fmt, struct c2c_fmt, fmt);
+ dim = c2c_fmt->dim;
+
+ if (dim == &dim_symbol_view)
+ return symbol_width(hists, dim->se);
+
+ return dim->se ? hists__col_len(hists, dim->se->se_width_idx) :
+ dim->width;
+}
+
+static int c2c_header(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp,
+ struct hists *hists, int line, int *span)
+{
+ struct c2c_fmt *c2c_fmt;
+ struct c2c_dimension *dim;
+ const char *text = NULL;
+ int width = c2c_width(fmt, hpp, hists);
+
+ c2c_fmt = container_of(fmt, struct c2c_fmt, fmt);
+ dim = c2c_fmt->dim;
+
+ if (dim->se) {
+ text = dim->header.line[line].text;
+ /* Use the last line from sort_entry if not defined. */
+ if (!text && line == hists->hpp_list->nr_header_lines - 1)
+ text = dim->se->se_header;
+ } else {
+ text = dim->header.line[line].text;
+
+ if (span) {
+ if (*span) {
+ (*span)--;
+ return 0;
+ }
+
+ *span = dim->header.line[line].span;
+ }
+ }
+
+ if (!text)
+ text = "";
+
+ return scnprintf(hpp->buf, hpp->size, "%*s", width, text);
+}
+
+/*
+ * Return the estimated total cycles for a c2c_hist_entry
+ * (rmt_hitm + lcl_hitm + rmt_peer + lcl_peer + other loads).
+ */
+static u64 c2c_hist_entry__cycles(struct c2c_hist_entry *c2c_he)
+{
+ struct compute_stats *cs = &c2c_he->cstats;
+ double cycles = 0;
+
+ /*
+ * compute_stats() in builtin-c2c.c routes each load sample into exactly
+ * one cstats bucket (rmt_hitm, lcl_hitm, rmt_peer, lcl_peer or plain
+ * load), so each bucket's cycle total is its mean times its own sample
+ * count. Summing the per-bucket totals avoids both dropping peer-snoop
+ * cycles and double counting a sample that carries several data-source
+ * flags (e.g. Arm SPE sets HITM and PEER on the same load), which would
+ * happen if the mean were multiplied by the non-exclusive stats counts.
+ */
+ cycles += avg_stats(&cs->rmt_hitm) * cs->rmt_hitm.n;
+ cycles += avg_stats(&cs->lcl_hitm) * cs->lcl_hitm.n;
+ cycles += avg_stats(&cs->rmt_peer) * cs->rmt_peer.n;
+ cycles += avg_stats(&cs->lcl_peer) * cs->lcl_peer.n;
+ cycles += avg_stats(&cs->load) * cs->load.n;
+
+ return (u64)cycles;
+}
+
+/* Sum c2c_hist_entry__cycles() across all level-1 entries. */
+static u64 c2c_ext__total_cycles(void)
+{
+ struct rb_node *nd;
+ u64 total = 0;
+
+ for (nd = rb_first_cached(&c2c_ext.function_hists.hists.entries); nd;
+ nd = rb_next(nd)) {
+ struct c2c_hist_entry *c2c_he =
+ rb_entry(nd, struct c2c_hist_entry, he.rb_node);
+
+ total += c2c_hist_entry__cycles(c2c_he);
+ }
+ return total;
+}
+
+/*
+ * Store count shown in the column: a level-3 cacheline leaf shows its parent
+ * level-2 writer's stores on that line, not all stores on the line. A level-2
+ * writer shows the sum across its level-3 cachelines. A level-1 reader shows
+ * the sum across all included writers on the cachelines it reads; this is not
+ * the reader function's own store count and is not additive across readers.
+ */
+static u64 hist_entry__displayed_stores(struct hist_entry *he)
+{
+ struct c2c_hist_entry *c2c_he = container_of(he, struct c2c_hist_entry, he);
+ struct rb_node *nd;
+ u64 stores = 0;
+
+ /* Level-2/3 entries already aggregate the stores they represent. */
+ if (he->parent_he)
+ return c2c_he->stats.store;
+
+ for (nd = rb_first_cached(&he->hroot_out); nd; nd = rb_next(nd)) {
+ struct c2c_hist_entry *child_c2c =
+ rb_entry(nd, struct c2c_hist_entry, he.rb_node);
+ stores += child_c2c->stats.store;
+ }
+ return stores;
+}
+
+static int
+total_stores_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp,
+ struct hist_entry *he)
+{
+ int width = c2c_width(fmt, hpp, he->hists);
+ u64 total = hist_entry__displayed_stores(he);
+
+ return scnprintf(hpp->buf, hpp->size, "%*" PRIu64, width, total);
+}
+
+/*
+ * symbol_view_entry - Render the unified, indented identity column.
+ *
+ * All three levels share this single column so the hierarchy reads top-down
+ * with progressive indentation, like the normal report hierarchy view. It is
+ * a function-centric view with no dedicated code-address column. Verbose
+ * function rows can still include a representative address:
+ * L1 read-side function: "- [k] cpupri_set"
+ * L2 writing function: " - [k] pull_rt_task"
+ * L3 shared cacheline: " 0xff2d0082809da080"
+ */
+static int
+symbol_view_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp,
+ struct hist_entry *he)
+{
+ int width = c2c_width(fmt, hpp, he->hists);
+ int text_width;
+ int ret;
+ char folded_sign;
+
+ ret = hist_entry__indent(he, hpp->buf, hpp->size);
+
+ folded_sign = he->has_children ? (he->unfolded ? '-' : '+') : ' ';
+ ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%c ", folded_sign);
+
+ text_width = width - ret;
+ if (text_width <= 0)
+ return ret;
+
+ if (hist_entry__is_cacheline(he)) {
+ /* Level 3: the shared cacheline address. */
+ u64 addr = he->mem_info ?
+ cl_address(mem_info__daddr(he->mem_info)->addr, chk_double_cl) : 0;
+ char symbuf[32];
+
+ scnprintf(symbuf, sizeof(symbuf), "0x%" PRIx64, addr);
+ ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%-*.*s",
+ text_width, text_width, symbuf);
+ } else {
+ /* Level 1 and level 2 are both functions. */
+ size_t cell_size;
+ int len;
+
+ if ((size_t)ret >= hpp->size)
+ return ret;
+
+ cell_size = min_t(size_t, hpp->size - ret,
+ (size_t)text_width + 1);
+ len = sort_sym.se_snprintf(he, hpp->buf + ret, cell_size,
+ text_width);
+ /*
+ * se_snprintf() accumulates repsep_snprintf() calls, which cap
+ * their return at the remaining size - 1 rather than reporting
+ * what the format would have needed, so len stays below
+ * cell_size. Clamp anyway so ret cannot leave hpp->buf.
+ */
+ if (len < 0)
+ len = 0;
+ else
+ len = min_t(size_t, len, cell_size - 1);
+
+ ret += len;
+ if (len < text_width)
+ ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%*s",
+ text_width - len, "");
+ }
+
+ return ret;
+}
+
+/*
+ * cycles_percent_entry - Render cycles percentage column
+ */
+static int
+cycles_percent_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp,
+ struct hist_entry *he)
+{
+ struct c2c_hist_entry *c2c_he;
+ int width = c2c_width(fmt, hpp, he->hists);
+ u64 fn_cycles, total_cycles;
+ char folded_sign;
+ double pct;
+ int ret, pct_width;
+
+ /* Hide Cycles Percent for child functions and cachelines. */
+ if (he->parent_he)
+ return scnprintf(hpp->buf, hpp->size, "%*s", width, "");
+
+ c2c_he = container_of(he, struct c2c_hist_entry, he);
+ fn_cycles = c2c_hist_entry__cycles(c2c_he);
+ /* 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;
+
+ /* Add folded sign only for level-1 entries */
+ folded_sign = he->has_children ? (he->unfolded ? '-' : '+') : ' ';
+ ret = scnprintf(hpp->buf, hpp->size, "%c ", folded_sign);
+
+ pct_width = width - ret;
+ if (pct_width <= 0)
+ return ret;
+ ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%*.2f%%", pct_width - 1, pct);
+ return ret;
+}
+
+/*
+ * cycles_percent_cmp - Comparison function for cycles percentage sorting
+ */
+static int64_t
+cycles_percent_cmp(struct perf_hpp_fmt *fmt __maybe_unused,
+ struct hist_entry *left, struct hist_entry *right)
+{
+ struct c2c_hist_entry *c2c_left = container_of(left, struct c2c_hist_entry, he);
+ struct c2c_hist_entry *c2c_right = container_of(right, struct c2c_hist_entry, he);
+ u64 cycles_left, cycles_right;
+
+ /* Cycles Percent is only shown for level-1 entries; others compare equal. */
+ if (left->parent_he || right->parent_he)
+ return 0;
+
+ cycles_left = c2c_hist_entry__cycles(c2c_left);
+ cycles_right = c2c_hist_entry__cycles(c2c_right);
+
+ return (cycles_left > cycles_right) - (cycles_left < cycles_right);
+}
+
+/*
+ * total_stores_cmp - Comparison function for total stores sorting
+ */
+static int64_t
+total_stores_cmp(struct perf_hpp_fmt *fmt __maybe_unused,
+ struct hist_entry *left, struct hist_entry *right)
+{
+ u64 left_store = hist_entry__displayed_stores(left);
+ u64 right_store = hist_entry__displayed_stores(right);
+
+ return (left_store > right_store) - (left_store < right_store);
+}
+
+/*
+ * Function view dimensions
+ */
+static struct c2c_dimension dim_cycles_percent = {
+ .header = HEADER_BOTH("Cycles", "%"),
+ .name = "cycles_percent",
+ .cmp = cycles_percent_cmp,
+ .entry = cycles_percent_entry,
+ .width = 9,
+};
+
+static struct c2c_dimension dim_total_stores = {
+ .header = HEADER_BOTH("Store", "count"),
+ .name = "total_stores",
+ .cmp = total_stores_cmp,
+ .entry = total_stores_entry,
+ .width = 7,
+};
+
+static struct c2c_dimension dim_symbol_view = {
+ .header = HEADER_LOW("Function / Contending function / Cacheline"),
+ .name = "symbol_view",
+ .se = &sort_sym,
+ .entry = symbol_view_entry,
+ .width = SYMBOL_WIDTH,
+};
+
+static struct c2c_dimension *function_view_dimensions[] = {
+ &dim_cycles_percent,
+ &dim_total_stores,
+ &dim_symbol_view,
+ NULL,
+};
+
+static struct c2c_dimension *get_function_dimension(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; function_view_dimensions[i]; i++) {
+ struct c2c_dimension *dim = function_view_dimensions[i];
+
+ if (!strcmp(dim->name, name))
+ return dim;
+ }
+
+ return NULL;
+}
+
+/* Wrappers so sort_entry-backed dimensions sort/collapse via their se. */
+static int64_t c2c_se_cmp(struct perf_hpp_fmt *fmt,
+ struct hist_entry *a, struct hist_entry *b)
+{
+ struct c2c_fmt *c2c_fmt = container_of(fmt, struct c2c_fmt, fmt);
+ struct c2c_dimension *dim = c2c_fmt->dim;
+
+ return dim->se->se_cmp(a, b);
+}
+
+static int64_t c2c_se_collapse(struct perf_hpp_fmt *fmt,
+ struct hist_entry *a, struct hist_entry *b)
+{
+ struct c2c_fmt *c2c_fmt = container_of(fmt, struct c2c_fmt, fmt);
+ struct c2c_dimension *dim = c2c_fmt->dim;
+ int64_t (*collapse_fn)(struct hist_entry *a, struct hist_entry *b);
+
+ collapse_fn = dim->se->se_collapse ?: dim->se->se_cmp;
+ return collapse_fn(a, b);
+}
+
+static int64_t c2c_se_sort(struct perf_hpp_fmt *fmt,
+ struct hist_entry *a, struct hist_entry *b)
+{
+ struct c2c_fmt *c2c_fmt = container_of(fmt, struct c2c_fmt, fmt);
+ struct c2c_dimension *dim = c2c_fmt->dim;
+ int64_t (*sort_fn)(struct hist_entry *a, struct hist_entry *b);
+
+ sort_fn = dim->se->se_sort ?: dim->se->se_cmp;
+ return sort_fn(a, b);
+}
+
+/*
+ * Build the c2c_fmt for @name. Returns:
+ * 0 and *fmtp set on success;
+ * -ENOENT and *fmtp = NULL if @name is not a function-view dimension;
+ * -ENOMEM if allocation failed (distinct from -ENOENT so
+ * the caller does not misreport it as an
+ * "invalid field").
+ */
+static int get_function_format(const char *name, struct c2c_fmt **fmtp)
+{
+ struct c2c_dimension *dim = get_function_dimension(name);
+ struct c2c_fmt *c2c_fmt;
+ struct perf_hpp_fmt *fmt;
+
+ *fmtp = NULL;
+
+ if (!dim)
+ return -ENOENT;
+
+ c2c_fmt = zalloc(sizeof(*c2c_fmt));
+ if (!c2c_fmt)
+ return -ENOMEM;
+
+ fmt = &c2c_fmt->fmt;
+
+ c2c_fmt->dim = dim;
+ INIT_LIST_HEAD(&fmt->list);
+ INIT_LIST_HEAD(&fmt->sort_list);
+
+ fmt->cmp = dim->se ? c2c_se_cmp : dim->cmp;
+ fmt->sort = dim->se ? c2c_se_sort : dim->cmp;
+ fmt->color = dim->color;
+ fmt->entry = dim->entry;
+ fmt->header = c2c_header;
+ fmt->width = c2c_width;
+ fmt->collapse = dim->se ? c2c_se_collapse : dim->cmp;
+ fmt->equal = c2c_fmt_equal;
+ fmt->free = c2c_fmt_free;
+
+ *fmtp = c2c_fmt;
+ return 0;
+}
+
+static int
+c2c_function_hists__init_output(struct perf_hpp_list *hpp_list, char *name,
+ struct perf_env *env __maybe_unused)
+{
+ struct c2c_fmt *c2c_fmt;
+ int ret;
+
+ ret = get_function_format(name, &c2c_fmt);
+ if (ret == -ENOMEM)
+ return ret;
+ /* The function view only accepts its own dimensions. */
+ if (ret == -ENOENT)
+ return -EINVAL;
+
+ /*
+ * Mark symbol-backed columns so hists__has(hists, sym) is correct.
+ * Only dim_symbol_view carries a sort_entry (.se); the function
+ * view's field strings are fixed and always include symbol_view, so
+ * this single check is sufficient (unlike the user-configurable
+ * cacheline view, which must also test dim_iaddr).
+ */
+ if (c2c_fmt->dim->se == &sort_sym)
+ hpp_list->sym = 1;
+
+ perf_hpp_list__column_register(hpp_list, &c2c_fmt->fmt);
+ return 0;
+}
+
+static int
+c2c_function_hists__init_sort(struct perf_hpp_list *hpp_list, char *name,
+ struct perf_env *env __maybe_unused)
+{
+ struct c2c_fmt *c2c_fmt;
+ int ret;
+
+ ret = get_function_format(name, &c2c_fmt);
+ if (ret == -ENOMEM)
+ return ret;
+ /* The function view only accepts its own dimensions. */
+ if (ret == -ENOENT)
+ return -EINVAL;
+
+ /* Mark symbol-backed sort keys so hists__has(hists, sym) is correct. */
+ if (c2c_fmt->dim->se == &sort_sym)
+ hpp_list->sym = 1;
+
+ perf_hpp_list__register_sort_field(hpp_list, &c2c_fmt->fmt);
+ return 0;
+}
+
+typedef int (*hpp_list_add_fn)(struct perf_hpp_list *hpp_list, char *name,
+ struct perf_env *env);
+
+static int function_hpp_list__add_tokens(struct perf_hpp_list *hpp_list, char *list,
+ struct perf_env *env, hpp_list_add_fn add)
+{
+ char *tok, *tmp;
+ int ret;
+
+ if (!list)
+ return 0;
+
+ for (tok = strtok_r(list, ", ", &tmp); tok; tok = strtok_r(NULL, ", ", &tmp)) {
+ ret = add(hpp_list, tok, env);
+ if (ret) {
+ if (ret == -EINVAL || ret == -ESRCH)
+ pr_err("Invalid c2c function-view field: %s\n", tok);
+ return ret;
+ }
+ }
+ return 0;
+}
+
+/*
+ * Append the function view's sort keys to its own output fields, mirroring
+ * perf_hpp__setup_output_field() but on the local @list. The shared helper
+ * registers onto the global perf_hpp_list, which would leave this local list
+ * without output columns, so the function view keeps its own copy here.
+ */
+static void c2c_function_hists__setup_output_field(struct perf_hpp_list *list)
+{
+ struct perf_hpp_fmt *fmt;
+
+ perf_hpp_list__for_each_sort_list(list, fmt) {
+ struct perf_hpp_fmt *pos;
+
+ if (!fmt->entry && !fmt->color)
+ continue;
+
+ perf_hpp_list__for_each_format(list, pos) {
+ if (c2c_fmt_equal(fmt, pos))
+ goto next;
+ }
+
+ perf_hpp_list__column_register(list, fmt);
+next:
+ continue;
+ }
+}
+
+static int
+function_hpp_list__parse(struct perf_hpp_list *hpp_list,
+ const char *output_str,
+ const char *sort_str,
+ struct perf_env *env)
+{
+ char *output = output_str ? strdup(output_str) : NULL;
+ char *sort = sort_str ? strdup(sort_str) : NULL;
+ int ret = 0;
+
+ if ((output_str && !output) || (sort_str && !sort)) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ ret = function_hpp_list__add_tokens(hpp_list, output, env,
+ c2c_function_hists__init_output);
+ if (ret)
+ goto out;
+
+ ret = function_hpp_list__add_tokens(hpp_list, sort, env,
+ c2c_function_hists__init_sort);
+ if (ret)
+ goto out;
+
+ c2c_function_hists__setup_output_field(hpp_list);
+out:
+ if (ret)
+ perf_hpp__reset_output_field(hpp_list);
+ free(output);
+ free(sort);
+ return ret;
+}
+
+static int
+c2c_function_hists__init(struct c2c_hists *hists,
+ const char *sort,
+ int nr_header_lines,
+ struct perf_env *env)
+{
+ __hists__init(&hists->hists, &hists->list);
+
+ perf_hpp_list__init(&hists->list);
+
+ hists->list.nr_header_lines = nr_header_lines;
+
+ return function_hpp_list__parse(&hists->list, /*output=*/NULL, sort, env);
+}
+
+static int
+c2c_function_hists__reinit(struct c2c_hists *c2c_hists,
+ const char *output,
+ const char *sort,
+ struct perf_env *env)
+{
+ int nr_header_lines = c2c_hists->list.nr_header_lines;
+
+ perf_hpp__reset_output_field(&c2c_hists->list);
+
+ /* Clear stale state flags so a different output/sort set starts fresh. */
+ c2c_hists->list.need_collapse = 0;
+ c2c_hists->list.parent = 0;
+ c2c_hists->list.sym = 0;
+ c2c_hists->list.dso = 0;
+ c2c_hists->list.socket = 0;
+ c2c_hists->list.thread = 0;
+ c2c_hists->list.comm = 0;
+ c2c_hists->list.comm_nodigit = 0;
+ c2c_hists->list.nr_header_lines = nr_header_lines;
+
+ return function_hpp_list__parse(&c2c_hists->list, output, sort, env);
+}
+
+/* Welford online merge of two "stats" (from util/stat.h) accumulators. */
+static void c2c_stats_merge(struct stats *dest, const struct stats *src)
+{
+ double delta;
+
+ if (src->n == 0)
+ return;
+
+ if (dest->n == 0) {
+ *dest = *src;
+ return;
+ }
+
+ delta = src->mean - dest->mean;
+ dest->M2 += src->M2 + delta * delta * dest->n * src->n / (dest->n + src->n);
+ dest->mean = (dest->mean * dest->n + src->mean * src->n) / (dest->n + src->n);
+ dest->n += src->n;
+
+ /* Update min/max */
+ if (src->max > dest->max)
+ dest->max = src->max;
+ if (src->min < dest->min)
+ dest->min = src->min;
+}
+
+/* Merge compute_stats during function aggregation. */
+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);
+ c2c_stats_merge(&dest->rmt_peer, &src->rmt_peer);
+ c2c_stats_merge(&dest->lcl_peer, &src->lcl_peer);
+ c2c_stats_merge(&dest->load, &src->load);
+}
+
+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);
+
+ /*
+ * Allocate before touching he->stat, so a failure here leaves the
+ * entry unmodified and the caller can bail out without having
+ * half-updated the statistics.
+ */
+ if (symbol_conf.cumulate_callchain && !he->stat_acc) {
+ he->stat_acc = calloc(1, sizeof(struct he_stat));
+ if (!he->stat_acc)
+ return false;
+ }
+
+ he->stat.nr_events += nr_events;
+ he->stat.period += nr_events;
+ he->stat.weight1 += weight1;
+
+ if (!symbol_conf.cumulate_callchain)
+ return true;
+
+ he->stat_acc->nr_events += nr_events;
+ he->stat_acc->period += nr_events;
+ he->stat_acc->weight1 += weight1;
+
+ return true;
+}
+
+static void c2c_he__free_hierarchy(struct hist_entry *he);
+
+/*
+ * Free a function-view histogram entry (hist_entry_ops::free).
+ */
+static void c2c_function_he_free(void *ptr)
+{
+ struct hist_entry *he = ptr;
+ struct c2c_hist_entry *c2c_he;
+
+ c2c_he = container_of(he, struct c2c_hist_entry, he);
+
+ if (c2c_he->hists) {
+ perf_hpp__reset_output_field(&c2c_he->hists->list);
+ hists__delete_all_entries(&c2c_he->hists->hists);
+ zfree(&c2c_he->hists);
+ }
+
+ c2c_he__free_hierarchy(he);
+
+ free(c2c_he);
+}
+
+static void c2c_he__free_hierarchy(struct hist_entry *he)
+{
+ struct rb_node *nd;
+ struct hist_entry *child_he;
+
+ /*
+ * A leaf entry stores its callchains in the sorted_chain member, which
+ * shares a union with the hroot_in/hroot_out child trees, so its
+ * hroot_out is not a valid subtree to walk. Leaf entries never have a
+ * child hierarchy here, so stop before touching hroot_out.
+ */
+ if (he->leaf)
+ return;
+
+ if (RB_EMPTY_ROOT(&he->hroot_out.rb_root))
+ return;
+
+ nd = rb_first_cached(&he->hroot_out);
+ while (nd) {
+ struct rb_node *next = rb_next(nd);
+
+ child_he = rb_entry(nd, struct hist_entry, rb_node);
+ rb_erase_cached(&child_he->rb_node, &he->hroot_out);
+ hist_entry__delete(child_he);
+
+ nd = next;
+ }
+
+ /* All children erased; clear the tree (and its cached leftmost). */
+ he->hroot_out = RB_ROOT_CACHED;
+}
+
+/*
+ * Drop level-2 writing functions that carry no stores or
+ * no cacheline children. Writers are only added when they store into a shared
+ * line, so this is mainly a safety net. Returns the number of surviving
+ * writers.
+ */
+static int c2c_he__prune_empty_writers(struct hist_entry *l1_he)
+{
+ struct rb_node *nd;
+ int surviving = 0;
+
+ if (!l1_he->has_children)
+ return 0;
+
+ nd = rb_first_cached(&l1_he->hroot_out);
+ while (nd) {
+ struct rb_node *next = rb_next(nd);
+ struct hist_entry *l2_he = rb_entry(nd, struct hist_entry, rb_node);
+
+ if (l2_he->has_children && hist_entry__displayed_stores(l2_he) > 0) {
+ surviving++;
+ } else {
+ rb_erase_cached(&l2_he->rb_node, &l1_he->hroot_out);
+ hist_entry__delete(l2_he);
+ }
+ nd = next;
+ }
+
+ if (!surviving) {
+ l1_he->hroot_out = RB_ROOT_CACHED;
+ l1_he->has_children = false;
+ l1_he->unfolded = false;
+ }
+ return surviving;
+}
+
+static void *c2c_function_he_zalloc(size_t size)
+{
+ struct c2c_hist_entry *c2c_he = zalloc(sizeof(*c2c_he) + size);
+
+ if (!c2c_he)
+ return NULL;
+
+ init_stats(&c2c_he->cstats.lcl_hitm);
+ init_stats(&c2c_he->cstats.rmt_hitm);
+ init_stats(&c2c_he->cstats.lcl_peer);
+ init_stats(&c2c_he->cstats.rmt_peer);
+ init_stats(&c2c_he->cstats.load);
+
+ return &c2c_he->he;
+}
+
+/* Entry operations for function view */
+static struct hist_entry_ops c2c_function_entry_ops = {
+ .new = c2c_function_he_zalloc,
+ .free = c2c_function_he_free,
+};
+
+static struct c2c_hist_entry *
+c2c_child_entry__alloc(struct hist_entry *parent_he, struct hist_entry *src_he,
+ int depth, u64 ip)
+{
+ struct c2c_hist_entry *child_c2c;
+ struct hist_entry *child_he;
+
+ /* Function-view children never own or display callchains. */
+ child_he = c2c_function_he_zalloc(0);
+ if (!child_he)
+ return NULL;
+
+ child_c2c = container_of(child_he, struct c2c_hist_entry, he);
+ child_he->ops = &c2c_function_entry_ops;
+ map_symbol__copy(&child_he->ms, &src_he->ms);
+
+ if (src_he->mem_info) {
+ child_he->mem_info = mem_info__clone(src_he->mem_info);
+ if (!child_he->mem_info)
+ goto out_free;
+ }
+
+ child_he->thread = thread__get(src_he->thread);
+ child_he->cpumode = src_he->cpumode;
+ child_he->cpu = src_he->cpu;
+ child_he->socket = src_he->socket;
+ child_he->level = src_he->level;
+ child_he->ip = ip;
+
+ child_he->parent_he = parent_he;
+ child_he->depth = depth;
+ child_he->leaf = (depth >= 2);
+ child_he->hists = &c2c_ext.function_hists.hists;
+ child_he->filtered = false;
+ child_he->unfolded = false;
+ child_he->has_children = false;
+ child_he->has_no_entry = false;
+ child_he->nr_rows = 0;
+ child_he->row_offset = 0;
+
+ memset(&child_he->stat, 0, sizeof(child_he->stat));
+ child_he->hroot_in = RB_ROOT_CACHED;
+ child_he->hroot_out = RB_ROOT_CACHED;
+ INIT_LIST_HEAD(&child_he->pairs.node);
+ child_he->hpp_list = &c2c_ext.function_hists.list;
+ if (symbol_conf.cumulate_callchain) {
+ child_he->stat_acc = calloc(1, sizeof(struct he_stat));
+ if (!child_he->stat_acc)
+ goto out_free;
+ }
+
+ return child_c2c;
+
+out_free:
+ hist_entry__delete(child_he);
+ return NULL;
+}
+
+static void
+c2c_child_entry__insert(struct hist_entry *parent_he, struct hist_entry *child_he,
+ struct rb_node **p, struct rb_node *rb_parent, bool leftmost)
+{
+ rb_link_node(&child_he->rb_node, rb_parent, p);
+ rb_insert_color_cached(&child_he->rb_node, &parent_he->hroot_out, leftmost);
+
+ parent_he->has_children = true;
+ parent_he->leaf = false;
+}
+
+static struct hist_entry *
+c2c_function_hists__level1_entry(struct symbol *sym,
+ struct hist_entry *detail_he,
+ struct thread *synthetic_thread)
+{
+ struct addr_location al;
+ struct perf_sample sample = {};
+ struct mem_info *mi;
+ struct hist_entry *he;
+ /*
+ * Key the level-1 entry by the function, not by a specific code
+ * address: use the symbol start so every instruction address inside
+ * the same function collapses into one entry. This makes level 1 a
+ * true "function view" rather than a per-code-address view.
+ */
+ u64 sym_start = (sym && detail_he->ms.map) ?
+ map__unmap_ip(detail_he->ms.map, sym->start) : detail_he->ip;
+
+ mi = mem_info__new();
+ if (!mi)
+ return NULL;
+
+ mem_info__iaddr(mi)->addr = sym_start;
+ /* mem_info__put() will map_symbol__exit() these, so take refs. */
+ mem_info__iaddr(mi)->ms.thread = thread__get(detail_he->ms.thread);
+ mem_info__iaddr(mi)->ms.map = map__get(detail_he->ms.map);
+ mem_info__iaddr(mi)->ms.sym = sym;
+ mem_info__daddr(mi)->addr = 0;
+
+ addr_location__init(&al);
+ al.thread = thread__get(synthetic_thread);
+ al.map = map__get(detail_he->ms.map);
+ al.sym = sym;
+ al.addr = sym_start;
+ al.level = detail_he->level;
+ al.cpumode = detail_he->cpumode;
+ al.cpu = 0;
+ al.socket = 0;
+ al.filtered = 0;
+ al.latency = 0;
+
+ /*
+ * Synthetic sample: period/weight are placeholders only. The real
+ * c2c counters live in c2c_hist_entry::stats and are added via
+ * hist_entry__add_c2c_stats(); no function-view column or sort key
+ * reads he->stat.period/nr_events, so the +1 that __hists__add_entry()
+ * accrues on each dedup hit has no effect on what is displayed.
+ */
+ sample.period = 1;
+ sample.weight = 1;
+ sample.ip = sym_start;
+ sample.pid = thread__pid(synthetic_thread);
+ sample.tid = thread__tid(synthetic_thread);
+ sample.cpu = 0;
+
+ /* Add entry - histogram handles dedup */
+ he = hists__add_entry_ops(&c2c_ext.function_hists.hists,
+ &c2c_function_entry_ops,
+ &al, NULL, NULL, mi,
+ NULL, &sample, true);
+
+ addr_location__exit(&al);
+ mem_info__put(mi);
+
+ if (he)
+ he->hpp_list = &c2c_ext.function_hists.list;
+
+ return he;
+}
+
+/*
+ * Level 2: a function that writes a cacheline the level-1 function reads,
+ * keyed by the DSO display name and symbol, consistently with perf's symbol
+ * sort semantics. All code addresses and cachelines for the same writer
+ * function aggregate into one row.
+ */
+static struct c2c_hist_entry *
+c2c_function_hists__level2_entry(struct c2c_hist_entry *level1_c2c,
+ struct symbol *sym, struct hist_entry *detail_he)
+{
+ struct hist_entry *level1_he = &level1_c2c->he;
+ struct rb_node **p = &level1_he->hroot_out.rb_root.rb_node;
+ struct rb_node *parent = NULL;
+ struct c2c_hist_entry *level2_c2c;
+ bool leftmost = true;
+
+ while (*p) {
+ struct hist_entry *iter = rb_entry(*p, struct hist_entry, rb_node);
+ struct map_symbol key = detail_he->ms;
+ int64_t cmp;
+
+ key.sym = sym;
+ parent = *p;
+ cmp = c2c_function_cmp(&key, &iter->ms);
+
+ if (cmp < 0) {
+ p = &parent->rb_left;
+ } else if (cmp > 0) {
+ p = &parent->rb_right;
+ leftmost = false;
+ } else {
+ return container_of(iter, struct c2c_hist_entry, he);
+ }
+ }
+
+ /* Key by the function symbol start so all code addresses collapse. */
+ level2_c2c = c2c_child_entry__alloc(level1_he, detail_he, 1,
+ (sym && detail_he->ms.map) ?
+ map__unmap_ip(detail_he->ms.map, sym->start) :
+ hist_entry__iaddr(detail_he));
+ if (!level2_c2c)
+ return NULL;
+
+ /* Key this level by the looked-up symbol, not detail_he's. */
+ level2_c2c->he.ms.sym = sym;
+ if (level2_c2c->he.mem_info)
+ mem_info__iaddr(level2_c2c->he.mem_info)->ms.sym = sym;
+
+ c2c_child_entry__insert(level1_he, &level2_c2c->he, p, parent, leftmost);
+
+ return level2_c2c;
+}
+
+/* Level 3: one source cacheline where the L1/L2 functions contend. */
+static struct c2c_hist_entry *
+c2c_function_hists__level3_entry(struct c2c_hist_entry *level2_c2c,
+ struct c2c_hist_entry *cacheline_src_he)
+{
+ struct hist_entry *level2_he = &level2_c2c->he;
+ struct rb_node **p = &level2_he->hroot_out.rb_root.rb_node;
+ struct rb_node *parent = NULL;
+ struct c2c_hist_entry *level3_c2c;
+ bool leftmost = true;
+
+ while (*p) {
+ struct c2c_hist_entry *iter_c2c =
+ rb_entry(*p, struct c2c_hist_entry, he.rb_node);
+
+ parent = *p;
+ if (cacheline_src_he->cacheline_idx < iter_c2c->cacheline_idx) {
+ p = &parent->rb_left;
+ } else if (cacheline_src_he->cacheline_idx > iter_c2c->cacheline_idx) {
+ p = &parent->rb_right;
+ leftmost = false;
+ } else {
+ return iter_c2c;
+ }
+ }
+
+ level3_c2c = c2c_child_entry__alloc(level2_he, &cacheline_src_he->he, 2,
+ hist_entry__iaddr(&cacheline_src_he->he));
+ if (!level3_c2c)
+ return NULL;
+ level3_c2c->cacheline_idx = cacheline_src_he->cacheline_idx;
+
+ c2c_child_entry__insert(level2_he, &level3_c2c->he, p, parent, leftmost);
+
+ return level3_c2c;
+}
+
+struct hist_entry *c2c_function__find_cacheline(struct hist_entry *he_selection)
+{
+ struct c2c_hist_entry *c2c_he;
+ struct rb_node *nd;
+
+ if (!c2c_ext.cl_hists || !he_selection || !he_selection->parent_he ||
+ !he_selection->parent_he->parent_he)
+ return NULL;
+
+ c2c_he = container_of(he_selection, struct c2c_hist_entry, he);
+
+ for (nd = rb_first_cached(&c2c_ext.cl_hists->hists.entries); nd;
+ nd = rb_next(nd)) {
+ struct hist_entry *he = rb_entry(nd, struct hist_entry, rb_node);
+ struct c2c_hist_entry *cacheline_he;
+
+ if (he->filtered)
+ continue;
+
+ cacheline_he = container_of(he, struct c2c_hist_entry, he);
+ if (cacheline_he->hists &&
+ cacheline_he->cacheline_idx == c2c_he->cacheline_idx)
+ return he;
+ }
+
+ 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;
+}