blob: 5b6a06a5a067c794a30affabea62e4a0e35e00cc [file] [edit]
// 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;
}