From: Mykyta Yatsenko <mykyta.yatsenko5@gmail.com>
To: Andrii Nakryiko <andrii.nakryiko@gmail.com>
Cc: bpf@vger.kernel.org, ast@kernel.org, andrii@kernel.org,
daniel@iogearbox.net, kernel-team@meta.com, eddyz87@gmail.com,
memxor@gmail.com, qmo@kernel.org,
linux-perf-users@vger.kernel.org, acme@kernel.org,
namhyung@kernel.org, Mykyta Yatsenko <yatsenko@meta.com>
Subject: Re: [PATCH bpf-next v3 3/3] bpftool: Scale counters and report cycles per run
Date: Wed, 9 Sep 2026 10:49:39 +0100 [thread overview]
Message-ID: <0e948aae-bdbd-4a18-bb87-bdc00d147ef4@gmail.com> (raw)
In-Reply-To: <CAEf4BzZ0GwQS3vHDywzkLPo_FspsuPYeYEXsZ9q5c5Mp6nWVjQ@mail.gmail.com>
On 9/9/26 1:02 AM, Andrii Nakryiko wrote:
> On Tue, Sep 8, 2026 at 7:27 AM Mykyta Yatsenko
> <mykyta.yatsenko5@gmail.com> wrote:
>>
>> From: Mykyta Yatsenko <yatsenko@meta.com>
>>
>> Perf counters can report too few events when the PMU multiplexes them.
>> Scale each per-CPU value before aggregation.
>>
>> Use the same CPU set for derived ratios. Report cycles per included
>> program run, and preserve the total run count in JSON.
>>
>> Fixes: 47c09d6a9f67 ("bpftool: Introduce "prog profile" command")
>> Signed-off-by: Mykyta Yatsenko <yatsenko@meta.com>
>> ---
>> tools/bpf/bpftool/Documentation/bpftool-prog.rst | 13 +-
>> tools/bpf/bpftool/prog.c | 159 +++++++++++++++++------
>> 2 files changed, 133 insertions(+), 39 deletions(-)
>>
>> diff --git a/tools/bpf/bpftool/Documentation/bpftool-prog.rst b/tools/bpf/bpftool/Documentation/bpftool-prog.rst
>> index 90fa2a48cc26..0108a1c8be4d 100644
>> --- a/tools/bpf/bpftool/Documentation/bpftool-prog.rst
>> +++ b/tools/bpf/bpftool/Documentation/bpftool-prog.rst
>> @@ -217,7 +217,16 @@ bpftool prog run *PROG* data_in *FILE* [data_out *FILE* [data_size_out *L*]] [ct
>> bpftool prog profile *PROG* [duration *DURATION*] *METRICs*
>> Profile *METRICs* for bpf program *PROG* for *DURATION* seconds or until
>> user hits <Ctrl+C>. *DURATION* is optional. If *DURATION* is not specified,
>> - the profiling will run up to **UINT_MAX** seconds.
>> + the profiling will run up to **UINT_MAX** seconds. Plain output scales each
>> + per-CPU metric value to correct for perf event multiplexing. When
>> + **cycles** is selected, it also reports cycles per included program run. If
>> + a selected metric was not scheduled on a CPU, all metric values exclude
>> + that CPU so that ratios use a consistent CPU set. The **run_cnt** value
>> + still includes all recorded runs.
>> +
>> + In JSON output, **value** is raw, **value_scaled** is scaled, **run_cnt**
>> + includes all runs, and **run_cnt_valid** includes only runs used for metric
>> + values.
>>
>> bpftool prog help
>> Print short help message.
>> @@ -360,7 +369,7 @@ EXAMPLES
>> ::
>>
>> 51397 run_cnt
>> - 40176203 cycles (83.05%)
>> + 40176203 cycles # 781.68 cycles per run (83.05%)
>> 42518139 instructions # 1.06 insns per cycle (83.39%)
>> 123 llc_misses # 2.89 LLC misses per million insns (83.15%)
>>
>> diff --git a/tools/bpf/bpftool/prog.c b/tools/bpf/bpftool/prog.c
>> index 8c2f9255b36d..9e126f3823ad 100644
>> --- a/tools/bpf/bpftool/prog.c
>> +++ b/tools/bpf/bpftool/prog.c
>> @@ -2062,37 +2062,52 @@ static int do_profile(int argc, char **argv)
>>
>> #include "profiler.skel.h"
>>
>> +enum ratio_metric {
>> + METRIC_NONE = -2,
>> + METRIC_RUN_CNT = -1,
>> + METRIC_CYCLES = 0,
>> + METRIC_INSTRUCTIONS = 1,
>> + METRIC_L1D_LOADS = 2,
>> + METRIC_LLC_MISSES = 3,
>> + METRIC_ITLB_MISSES = 4,
>> + METRIC_DTLB_MISSES = 5,
>> +};
>> +
>> struct profile_metric {
>> const char *name;
>> struct bpf_perf_event_value val;
>> + __u64 scaled_val;
>> struct perf_event_attr attr;
>> bool selected;
>>
>> /* calculate ratios like instructions per cycle */
>> - const int ratio_metric; /* 0 for N/A, 1 for index 0 (cycles) */
>> + const enum ratio_metric ratio_metric;
>> const char *ratio_desc;
>> const float ratio_mul;
>> } metrics[] = {
>> - {
>> + [METRIC_CYCLES] = {
>> .name = "cycles",
>> .attr = {
>> .type = PERF_TYPE_HARDWARE,
>> .config = PERF_COUNT_HW_CPU_CYCLES,
>> .exclude_user = 1,
>> },
>> + .ratio_metric = METRIC_RUN_CNT,
>> + .ratio_desc = "cycles per run",
>> + .ratio_mul = 1.0,
>> },
>> - {
>> + [METRIC_INSTRUCTIONS] = {
>> .name = "instructions",
>> .attr = {
>> .type = PERF_TYPE_HARDWARE,
>> .config = PERF_COUNT_HW_INSTRUCTIONS,
>> .exclude_user = 1,
>> },
>> - .ratio_metric = 1,
>> + .ratio_metric = METRIC_CYCLES,
>> .ratio_desc = "insns per cycle",
>> .ratio_mul = 1.0,
>> },
>> - {
>> + [METRIC_L1D_LOADS] = {
>> .name = "l1d_loads",
>> .attr = {
>> .type = PERF_TYPE_HW_CACHE,
>> @@ -2102,8 +2117,9 @@ struct profile_metric {
>> (PERF_COUNT_HW_CACHE_RESULT_ACCESS << 16),
>> .exclude_user = 1,
>> },
>> + .ratio_metric = METRIC_NONE,
>> },
>> - {
>> + [METRIC_LLC_MISSES] = {
>> .name = "llc_misses",
>> .attr = {
>> .type = PERF_TYPE_HW_CACHE,
>> @@ -2113,11 +2129,11 @@ struct profile_metric {
>> (PERF_COUNT_HW_CACHE_RESULT_MISS << 16),
>> .exclude_user = 1
>> },
>> - .ratio_metric = 2,
>> + .ratio_metric = METRIC_INSTRUCTIONS,
>> .ratio_desc = "LLC misses per million insns",
>> .ratio_mul = 1e6,
>> },
>> - {
>> + [METRIC_ITLB_MISSES] = {
>> .name = "itlb_misses",
>> .attr = {
>> .type = PERF_TYPE_HW_CACHE,
>> @@ -2127,11 +2143,11 @@ struct profile_metric {
>> (PERF_COUNT_HW_CACHE_RESULT_MISS << 16),
>> .exclude_user = 1
>> },
>> - .ratio_metric = 2,
>> + .ratio_metric = METRIC_INSTRUCTIONS,
>> .ratio_desc = "itlb misses per million insns",
>> .ratio_mul = 1e6,
>> },
>> - {
>> + [METRIC_DTLB_MISSES] = {
>> .name = "dtlb_misses",
>> .attr = {
>> .type = PERF_TYPE_HW_CACHE,
>> @@ -2141,13 +2157,14 @@ struct profile_metric {
>> (PERF_COUNT_HW_CACHE_RESULT_MISS << 16),
>> .exclude_user = 1
>> },
>> - .ratio_metric = 2,
>> + .ratio_metric = METRIC_INSTRUCTIONS,
>> .ratio_desc = "dtlb misses per million insns",
>> .ratio_mul = 1e6,
>> },
>> };
>>
>> static __u64 profile_total_count;
>> +static __u64 profile_valid_count;
>>
>> #define MAX_NUM_PROFILE_METRICS 4
>>
>> @@ -2182,9 +2199,39 @@ static int profile_parse_metrics(int argc, char **argv)
>> return selected_cnt;
>> }
>>
>> -static void profile_read_values(struct profiler_bpf *obj)
>> +/*
>> + * Filter out CPUs that have any selected metric not scheduled for them. This makes sure all
>> + * metrics are using the same CPU set, as a result ratio metrics are consistent.
>> + */
>> +static void profile_filter_cpus(__u32 num_cpu,
>> + struct bpf_perf_event_value vals[MAX_NUM_PROFILE_METRICS][num_cpu],
>> + __u64 *counts)
>> +{
>> + __u32 m, cpu, key = 0;
>> +
>> + for (m = 0; m < ARRAY_SIZE(metrics); m++) {
>> + if (!metrics[m].selected)
>> + continue;
>> +
>> + for (cpu = 0; cpu < num_cpu; cpu++) {
>> + /*
>> + * CPU has hits, but this metric never scheduled, set counts[cpu] to 0
>> + * so other metrics ignore this CPU too
>> + */
>> + if (counts[cpu] && !vals[key][cpu].running) {
>> + p_info("%s not scheduled on CPU %u; excluding %llu runs from all metrics",
>> + metrics[m].name, cpu, counts[cpu]);
>> + counts[cpu] = 0;
>> + }
>> + }
>> + key++;
>> + }
>> +}
>
> was this suggested by AI or we actually ran into issues due to this in
> practice? this looks perculiar
>
Suggested by AI is a bit of a simplification:
The initial problem was scaling counters, the counters are collected
and accounted per CPU, so it sounds like a right thing to scale per
CPU as well(?) Because we divide by running, we need to check it for 0.
Now it is possible that some counters have running 0 on some CPU, but
others dont, will it make ratios biased?
this is easy to repro:
in tab 1 run:
```
sudo ./build/tools/bpf/bpftool/bpftool prog profile name myprog duration 80 itlb_misses dtlb_misses cycles instructions
```
in tab 2 run:
```
sudo ./build/tools/bpf/bpftool/bpftool prog profile name myprog duration 1 itlb_misses dtlb_misses cycles instructions
cycles not scheduled on CPU 36; excluding 1 runs from all metrics
instructions not scheduled on CPU 6; excluding 4 runs from all metrics
instructions not scheduled on CPU 39; excluding 1 runs from all metrics
instructions not scheduled on CPU 72; excluding 1 runs from all metrics
itlb_misses not scheduled on CPU 50; excluding 1 runs from all metrics
330 run_cnt
3601259 cycles # 11184.03 cycles per run (73.57%)
682448 instructions # 0.19 insns per cycle (80.85%)
132 itlb_misses # 193.42 itlb misses per million insns (73.66%)
2189 dtlb_misses # 3207.57 dtlb misses per million insns (72.24%)
```
>> +
>> +static int profile_read_values(struct profiler_bpf *obj)
>> {
>> __u32 m, cpu, num_cpu = obj->rodata->num_cpu;
>> + struct bpf_perf_event_value values[MAX_NUM_PROFILE_METRICS][num_cpu], *val;
>> int reading_map_fd, count_map_fd;
>> __u64 counts[num_cpu];
>> __u32 key = 0;
>> @@ -2194,38 +2241,61 @@ static void profile_read_values(struct profiler_bpf *obj)
>> count_map_fd = bpf_map__fd(obj->maps.counts);
>> if (reading_map_fd < 0 || count_map_fd < 0) {
>
> this can't happen if skeleton loaded successfully, just remove this
> check instead of weird min() over fds/errors
>
>> p_err("failed to get fd for map");
>> - return;
>> + return min(reading_map_fd, count_map_fd);
>> }
>>
>> err = bpf_map_lookup_elem(count_map_fd, &key, counts);
>> if (err) {
>> p_err("failed to read count_map: %s", strerror(errno));
>> - return;
>> + return err;
>> + }
>> +
>> + for (m = 0; m < ARRAY_SIZE(metrics); m++) {
>> + if (!metrics[m].selected)
>> + continue;
>> +
>> + err = bpf_map_lookup_elem(reading_map_fd, &key, values[key]);
>> + if (err) {
>> + p_err("failed to read reading_map: %s", strerror(errno));
>> + return err;
>> + }
>> + key++;
>> }
>>
>> profile_total_count = 0;
>> for (cpu = 0; cpu < num_cpu; cpu++)
>> profile_total_count += counts[cpu];
>>
>> + profile_filter_cpus(num_cpu, values, counts);
>> +
>> + profile_valid_count = 0;
>> + for (cpu = 0; cpu < num_cpu; cpu++)
>> + profile_valid_count += counts[cpu];
>> +
>> + key = 0;
>> for (m = 0; m < ARRAY_SIZE(metrics); m++) {
>> - struct bpf_perf_event_value values[num_cpu];
>> + double scale;
>>
>> if (!metrics[m].selected)
>> continue;
>>
>> - err = bpf_map_lookup_elem(reading_map_fd, &key, values);
>> - if (err) {
>> - p_err("failed to read reading_map: %s",
>> - strerror(errno));
>> - return;
>> - }
>> for (cpu = 0; cpu < num_cpu; cpu++) {
>> - metrics[m].val.counter += values[cpu].counter;
>> - metrics[m].val.enabled += values[cpu].enabled;
>> - metrics[m].val.running += values[cpu].running;
>> + /* Skip CPUs with no runs or with an unscheduled metric. */
>> + if (!counts[cpu])
>> + continue;
>> +
>> + val = &values[key][cpu];
>> +
>> + metrics[m].val.enabled += val->enabled;
>> + metrics[m].val.running += val->running;
>> + metrics[m].val.counter += val->counter;
>> + /* Scale counter values to account for perf event multiplexing. */
>> + scale = (double)val->enabled / val->running;
>> + metrics[m].scaled_val += val->counter * scale;
>> }
>> key++;
>> }
>> + return 0;
>> }
>>
>> static void profile_print_readings_json(void)
>> @@ -2239,9 +2309,11 @@ static void profile_print_readings_json(void)
>> jsonw_start_object(json_wtr);
>> jsonw_string_field(json_wtr, "metric", metrics[m].name);
>> jsonw_lluint_field(json_wtr, "run_cnt", profile_total_count);
>> + jsonw_lluint_field(json_wtr, "run_cnt_valid", profile_valid_count);
>
> aren't we just overcomplicating things for no good reason?..
>
>> jsonw_lluint_field(json_wtr, "value", metrics[m].val.counter);
>> jsonw_lluint_field(json_wtr, "enabled", metrics[m].val.enabled);
>> jsonw_lluint_field(json_wtr, "running", metrics[m].val.running);
>> + jsonw_lluint_field(json_wtr, "value_scaled", metrics[m].scaled_val);
>>
>> jsonw_end_object(json_wtr);
>> }
>> @@ -2250,24 +2322,34 @@ static void profile_print_readings_json(void)
>>
>> static void profile_print_readings_plain(void)
>> {
>> - __u32 m;
>> + __u32 i;
>>
>> printf("\n%18llu %-20s\n", profile_total_count, "run_cnt");
>> - for (m = 0; m < ARRAY_SIZE(metrics); m++) {
>> - struct bpf_perf_event_value *val = &metrics[m].val;
>> + for (i = 0; i < ARRAY_SIZE(metrics); i++) {
>> + struct profile_metric *m = &metrics[i];
>> + struct bpf_perf_event_value *val = &m->val;
>> int r;
>> + __u64 ratio;
>>
>> - if (!metrics[m].selected)
>> + if (!m->selected)
>> continue;
>> - printf("%18llu %-20s", val->counter, metrics[m].name);
>> + printf("%18llu %-20s", m->scaled_val, m->name);
>>
>> - r = metrics[m].ratio_metric - 1;
>> - if (r >= 0 && metrics[r].selected &&
>> - metrics[r].val.counter > 0) {
>> + r = m->ratio_metric;
>> + switch (r) {
>> + case METRIC_RUN_CNT:
>> + ratio = profile_valid_count;
>> + break;
>> + case METRIC_NONE:
>> + ratio = 0;
>> + break;
>> + default:
>> + ratio = metrics[r].scaled_val;
>> + }
>> + if (ratio) {
>> printf("# %8.2f %-30s",
>> - val->counter * metrics[m].ratio_mul /
>> - metrics[r].val.counter,
>> - metrics[m].ratio_desc);
>> + m->scaled_val * m->ratio_mul / ratio,
>> + m->ratio_desc);
>> } else {
>> printf("%-41s", "");
>> }
>> @@ -2423,9 +2505,12 @@ static int profile_open_perf_events(struct profiler_bpf *obj)
>>
>> static void profile_print_and_cleanup(void)
>> {
>> + int err;
>> +
>> profile_close_perf_events(profile_obj);
>> - profile_read_values(profile_obj);
>> - profile_print_readings();
>> + err = profile_read_values(profile_obj);
>> + if (!err)
>> + profile_print_readings();
>> profiler_bpf__destroy(profile_obj);
>>
>> close(profile_tgt_fd);
>>
>> --
>> 2.53.0-Meta
>>
next prev parent reply other threads:[~2026-09-09 9:49 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-08 14:27 [PATCH bpf-next v3 0/3] bpftool: Improve perf counter reporting Mykyta Yatsenko
2026-09-08 14:27 ` [PATCH bpf-next v3 1/3] bpftool: Track perf counter snapshot state Mykyta Yatsenko
2026-09-08 14:35 ` sashiko-bot
2026-09-08 22:08 ` Quentin Monnet
2026-09-08 23:56 ` Andrii Nakryiko
2026-09-09 9:19 ` Mykyta Yatsenko
2026-09-11 23:57 ` Andrii Nakryiko
2026-09-08 14:27 ` [PATCH bpf-next v3 2/3] perf bpf_counter: Track valid BPF counter snapshots Mykyta Yatsenko
2026-09-08 14:39 ` sashiko-bot
2026-09-08 14:27 ` [PATCH bpf-next v3 3/3] bpftool: Scale counters and report cycles per run Mykyta Yatsenko
2026-09-08 14:42 ` sashiko-bot
2026-09-08 16:21 ` bot+bpf-ci
2026-09-08 17:37 ` Mykyta Yatsenko
2026-09-08 22:08 ` Quentin Monnet
2026-09-09 0:02 ` Andrii Nakryiko
2026-09-09 9:49 ` Mykyta Yatsenko [this message]
2026-09-12 0:01 ` Andrii Nakryiko
2026-09-08 18:01 ` [PATCH bpf-next v3 0/3] bpftool: Improve perf counter reporting Ihor Solodrai
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=0e948aae-bdbd-4a18-bb87-bdc00d147ef4@gmail.com \
--to=mykyta.yatsenko5@gmail.com \
--cc=acme@kernel.org \
--cc=andrii.nakryiko@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=kernel-team@meta.com \
--cc=linux-perf-users@vger.kernel.org \
--cc=memxor@gmail.com \
--cc=namhyung@kernel.org \
--cc=qmo@kernel.org \
--cc=yatsenko@meta.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox