Re: [PATCH v1] perf top: Merge hybrid common events

Ian Rogers <[email protected]>
Newsgroups org.kernel.vger.linux-perf-users
Message-ID <CAP-5=fW112qVVdZGNpdRH0n-B2at4ENDjjNv4Hdehn5bEB9BSA@mail.gmail.com>
On Thu, Aug 13, 2026 at 6:33 AM Andi Kleen <[email protected]> wrote:
>
> From: Andi Kleen <[email protected]>
>
> One annoyance with perf top on a hybrid system is that it requires to
> chose which hybrid PMU to sample on. Normally I want to sample the whole
> system and don't know on which cores my workload ends up.
>
> This patch automatically merges the two PMUs when the event is present
> in both PMUs. For now it only handles simple TYPE_HARDWARE cases, like
> cycles
> (could be later extended for TYPE_RAW too by checking the json name is the same)
> The behavior can be disabled with --no-hybrid-merge

Thanks Andi, I agree with the frustration. For an event like
instructions I would like things merged. Does merging make sense for
cycles given the different clock frequencies of p-cores and e-cores?
Should the e-core cycles be scaled for this reason as there are fewer
cycles within a second compared to a p-core?

Perhaps we need a new json field to describe the hybrid merge-ability
of events, say enabled on events like instructions or cache misses.
Note the legacy events now have JSON descriptions:
https://web.git.kernel.org/pub/scm/linux/kernel/git/perf/perf-tools-next.git/tree/tools/perf/pmu-events/arch/common/common/legacy-hardware.json?h=perf-tools-next

It would be nice if this were a generic feature and not just for perf top.

Thanks,
Ian

> Assisted-by: omp:GLM-5.2
> Signed-off-by: Andi Kleen <[email protected]>
> ---
>  tools/perf/Documentation/perf-top.txt |  6 ++++
>  tools/perf/builtin-top.c              | 16 ++++++++++
>  tools/perf/ui/hist.c                  | 19 ++++++++++++
>  tools/perf/util/evlist.c              | 43 +++++++++++++++++++++++++++
>  tools/perf/util/evlist.h              |  1 +
>  tools/perf/util/hist.h                | 15 +++++++++-
>  tools/perf/util/top.c                 |  8 ++++-
>  tools/perf/util/top.h                 |  1 +
>  8 files changed, 107 insertions(+), 2 deletions(-)
>
> diff --git a/tools/perf/Documentation/perf-top.txt b/tools/perf/Documentation/perf-top.txt
> index af3e4230c72f4..e560d6b1634d8 100644
> --- a/tools/perf/Documentation/perf-top.txt
> +++ b/tools/perf/Documentation/perf-top.txt
> @@ -43,6 +43,12 @@ Default is to monitor all CPUS.
>         encoding with the layout of the event control registers as described
>         by entries in /sys/bus/event_source/devices/cpu/format/*.
>
> +--hybrid-merge::
> +    Merge matching legacy hardware events from all hybrid core PMUs into one
> +    display. This is enabled by default when the same event is available on
> +    each core PMU. Use `--no-hybrid-merge` to display the existing per-event
> +    selection menu instead.
> +
>  --filter=<filter>::
>         Event filter.  This option should follow an event selector (-e). For
>         syntax see linkperf:perf-record[1].
> diff --git a/tools/perf/builtin-top.c b/tools/perf/builtin-top.c
> index 1211401616ee3..19dec094683b3 100644
> --- a/tools/perf/builtin-top.c
> +++ b/tools/perf/builtin-top.c
> @@ -1334,6 +1334,19 @@ static int __cmd_top(struct perf_top *top)
>          */
>          if (!target__none(&opts->target))
>                 evlist__enable(top->evlist);
> +       if (top->hybrid_merge && !symbol_conf.report_hierarchy &&
> +           evlist__can_merge_hybrid(top->evlist)) {
> +               struct evsel *leader = evlist__first(top->evlist);
> +
> +               /*
> +                * Merged events are not true groups, but can use
> +                * the existing group display code to display them
> +                * anyways.
> +                */
> +               __perf_evlist__set_leader(&top->evlist->core.entries, &leader->core);
> +               evsel__hists(leader)->merge_entries = true;
> +               symbol_conf.event_group = true;
> +       }
>
>         ret = -1;
>         if (pthread_create(&thread_process, NULL, process_thread, top)) {
> @@ -1457,6 +1470,7 @@ int cmd_top(int argc, const char **argv)
>         struct perf_top top = {
>                 .count_filter        = 5,
>                 .delay_secs          = 2,
> +               .hybrid_merge        = true,
>                 .record_opts = {
>                         .mmap_pages     = UINT_MAX,
>                         .user_freq      = UINT_MAX,
> @@ -1490,6 +1504,8 @@ int cmd_top(int argc, const char **argv)
>         OPT_CALLBACK('e', "event", &parse_events_option_args, "event",
>                      "event selector. use 'perf list' to list available events",
>                      parse_events_option),
> +       OPT_BOOLEAN(0, "hybrid-merge", &top.hybrid_merge,
> +                   "merge the same event across hybrid core PMUs"),
>         OPT_CALLBACK(0, "filter", &top.evlist, "filter",
>                      "event filter", parse_filter),
>         OPT_U64('c', "count", &opts->user_interval, "event period to sample"),
> diff --git a/tools/perf/ui/hist.c b/tools/perf/ui/hist.c
> index e58327595d37d..aee43b33e4e1e 100644
> --- a/tools/perf/ui/hist.c
> +++ b/tools/perf/ui/hist.c
> @@ -287,6 +287,25 @@ static int __hpp__sort(struct hist_entry *a, struct hist_entry *b,
>                 return __hpp__group_sort_idx(a, b, get_field,
>                                              symbol_conf.group_sort_idx);
>         }
> +       /*
> +        * Relies on merge_entries being only enabled if there are
> +        * only matching events. If that is ever relaxed will need
> +        * more logic here.
> +        */
> +       if (a->hists->merge_entries && b->hists->merge_entries) {
> +               u64 val_a = get_field(a), val_b = get_field(b);
> +               struct hist_entry *pair;
> +
> +               list_for_each_entry(pair, &a->pairs.head, pairs.node)
> +                       val_a += get_field(pair);
> +               list_for_each_entry(pair, &b->pairs.head, pairs.node)
> +                       val_b += get_field(pair);
> +
> +               ret = field_cmp(val_a, val_b);
> +               if (ret)
> +                       return ret;
> +               /* fall through to per-member tiebreaker */
> +       }
>
>         ret = field_cmp(get_field(a), get_field(b));
>         if (ret || !symbol_conf.event_group)
> diff --git a/tools/perf/util/evlist.c b/tools/perf/util/evlist.c
> index 1a238b245b3a0..4140f998e8a9b 100644
> --- a/tools/perf/util/evlist.c
> +++ b/tools/perf/util/evlist.c
> @@ -142,6 +142,49 @@ struct evlist *evlist__new_default(const struct target *target, bool sample_call
>         return NULL;
>  }
>
> +bool evlist__can_merge_hybrid(struct evlist *evlist)
> +{
> +       struct evsel *pos, *other;
> +       u64 config = 0;
> +       unsigned int nr = 0;
> +       bool first = true;
> +       int nr_core_pmus;
> +
> +       nr_core_pmus = perf_pmus__num_core_pmus();
> +       if (nr_core_pmus <= 1)
> +               return false;
> +
> +       evlist__for_each_entry(evlist, pos) {
> +               if (evsel__is_dummy_event(pos))
> +                       continue;
> +
> +               /* Initial support is for legacy hardware events, such as cycles. */
> +               if (!pos->pmu || !pos->pmu->is_core ||
> +                   pos->core.attr.type != PERF_TYPE_HARDWARE)
> +                       return false;
> +
> +               if (first) {
> +                       /*
> +                        * Filter out the PMU bits. May need something else
> +                        * for other types.
> +                        */
> +                       config = pos->core.attr.config & UINT32_MAX;
> +                       first = false;
> +               } else if ((pos->core.attr.config & UINT32_MAX) != config) {
> +                       return false;
> +               }
> +
> +               evlist__for_each_entry(evlist, other) {
> +                       if (other != pos && !evsel__is_dummy_event(other) &&
> +                           other->pmu == pos->pmu)
> +                               return false;
> +               }
> +
> +               nr++;
> +       }
> +       return !first && nr == (unsigned int)nr_core_pmus;
> +}
> +
>  struct evlist *evlist__new_dummy(void)
>  {
>         struct evlist *evlist = evlist__new();
> diff --git a/tools/perf/util/evlist.h b/tools/perf/util/evlist.h
> index e507f5f20ef61..ad0e6e7399d2a 100644
> --- a/tools/perf/util/evlist.h
> +++ b/tools/perf/util/evlist.h
> @@ -105,6 +105,7 @@ struct evsel_str_handler {
>
>  struct evlist *evlist__new(void);
>  struct evlist *evlist__new_default(const struct target *target, bool sample_callchains);
> +bool evlist__can_merge_hybrid(struct evlist *evlist);
>  struct evlist *evlist__new_dummy(void);
>  void evlist__init(struct evlist *evlist, struct perf_cpu_map *cpus,
>                   struct perf_thread_map *threads);
> diff --git a/tools/perf/util/hist.h b/tools/perf/util/hist.h
> index b830cbe7f95bf..a9dd423ded5e1 100644
> --- a/tools/perf/util/hist.h
> +++ b/tools/perf/util/hist.h
> @@ -130,6 +130,7 @@ struct hists {
>         struct hists_stats      stats;
>         u64                     event_stream;
>         u16                     col_len[HISTC_NR_COLS];
> +       bool                    merge_entries;
>         bool                    has_callchains;
>         int                     socket_filter;
>         struct perf_hpp_list    *hpp_list;
> @@ -435,14 +436,26 @@ int hists__unlink(struct hists *hists);
>
>  static inline float hist_entry__get_percent_limit(struct hist_entry *he)
>  {
> +       struct hist_entry *pair;
>         u64 period = he->stat.period;
>         u64 total_period = hists__total_period(he->hists);
>
> +       if (he->hists->merge_entries) {
> +               list_for_each_entry(pair, &he->pairs.head, pairs.node) {
> +                       period += pair->stat.period;
> +                       total_period += hists__total_period(pair->hists);
> +               }
> +       }
> +
>         if (unlikely(total_period == 0))
>                 return 0;
>
> -       if (symbol_conf.cumulate_callchain)
> +       if (symbol_conf.cumulate_callchain) {
>                 period = he->stat_acc->period;
> +               if (he->hists->merge_entries)
> +                       list_for_each_entry(pair, &he->pairs.head, pairs.node)
> +                               period += pair->stat_acc->period;
> +       }
>
>         return period * 100.0 / total_period;
>  }
> diff --git a/tools/perf/util/top.c b/tools/perf/util/top.c
> index b06e10a116bb3..b7ae51fbc9541 100644
> --- a/tools/perf/util/top.c
> +++ b/tools/perf/util/top.c
> @@ -78,7 +78,13 @@ size_t perf_top__header_snprintf(struct perf_top *top, char *bf, size_t size)
>                                 opts->freq ? "Hz" : "");
>         }
>
> -       ret += SNPRINTF(bf + ret, size - ret, "%s", evsel__name(top->sym_evsel));
> +       if (evsel__is_group_event(top->sym_evsel)) {
> +               char buf[256];
> +               evsel__group_desc(top->sym_evsel, buf, sizeof(buf));
> +               ret += SNPRINTF(bf + ret, size - ret, "%s", buf);
> +       } else {
> +               ret += SNPRINTF(bf + ret, size - ret, "%s", evsel__name(top->sym_evsel));
> +       }
>
>         ret += SNPRINTF(bf + ret, size - ret, "], ");
>
> diff --git a/tools/perf/util/top.h b/tools/perf/util/top.h
> index 04ff926846be0..b42e23066fc0d 100644
> --- a/tools/perf/util/top.h
> +++ b/tools/perf/util/top.h
> @@ -32,6 +32,7 @@ struct perf_top {
>         u64                guest_us_samples, guest_kernel_samples;
>         int                print_entries, count_filter, delay_secs;
>         int                max_stack;
> +       bool               hybrid_merge;
>         bool               hide_kernel_symbols, hide_user_symbols, zero;
>  #ifdef HAVE_SLANG_SUPPORT
>         bool               use_tui;
> --
> 2.54.0
>
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.