| // 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; | |
| } |