perf report: Add --progress option

Processing a large session with stdio output gives no feedback about
which phase perf is in or how far along it is: ui_progress updates are
only shown by the TUI.  Add --progress, installing a stdio backend
(ui/stdio/progress.c) that prints the phase title, percentage and
counts:

  Processing events... [ 42.3%] 317M / 746M

Phases can be nested, so the backend tracks the ones started to
complete the right one on ui_progress__finish(); that requires
init()/finish() pairs, fixed in ordered-events.c and the pipe and
directory event processing.

With a pager both stdout and stderr lead to it, so isatty(stderr)
turns false and the updates end up printed one per line in the
pager's output: use /dev/tty in that case, so that the updates keep
updating in place, untouched by the pager.

Assisted-by: LLM
Reviewed-by: Namhyung Kim <namhyung@kernel.org>
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
This commit is contained in:
Arnaldo Carvalho de Melo 2026-10-06 16:57:27 +02:00
parent 3f881d589c
commit 44ce6270eb
7 changed files with 214 additions and 7 deletions

View File

@ -29,6 +29,20 @@ OPTIONS
--quiet::
Do not show any warnings or messages. (Suppress -v)
--progress::
Show progress for each of the processing phases, printing the
percentage done and the current/total counts: the first phase
counts the bytes of the perf.data file processed so far, the
merge and sort phases count hist entries, e.g.:
Processing events... [ 42.3%] 4G / 10G
Merging related events... [ 7.1%] 1024 / 14387
Sorting events for output... [ 98.2%] 14132 / 14387
It is a no-op when using the TUI or GTK browsers, that already
present progress information, or when --quiet is used, that asks
for no messages at all.
-n::
--show-nr-samples::
Show the number of samples for each symbol

View File

@ -87,6 +87,7 @@ struct report {
bool use_gtk;
#endif
bool use_stdio;
bool progress;
bool show_full_info;
bool show_threads;
bool inverted_callchain;
@ -1384,6 +1385,8 @@ int cmd_report(int argc, const char **argv)
"Use the stdio interface"),
OPT_BOOLEAN(0, "weights", &symbol_conf.annotate_weight,
"Show or hide weight columns in annotation. Default show if non-zero."),
OPT_BOOLEAN(0, "progress", &report.progress,
"Show progress while processing the perf.data file"),
OPT_BOOLEAN(0, "header", &report.header, "Show data header."),
OPT_BOOLEAN(0, "header-only", &report.header_only,
"Show only data header."),
@ -1790,6 +1793,9 @@ int cmd_report(int argc, const char **argv)
else
use_browser = 0;
if (report.progress && !quiet && use_browser == 0)
stdio_progress__init();
if (report.data_type && use_browser == 1) {
symbol_conf.annotate_data_member = true;
symbol_conf.annotate_data_sample = true;

View File

@ -4,6 +4,7 @@ perf-ui-y += progress.o
perf-ui-y += util.o
perf-ui-y += hist.o
perf-ui-y += stdio/hist.o
perf-ui-y += stdio/progress.o
CFLAGS_setup.o += -DLIBDIR="BUILD_STR($(LIBDIR))"

View File

@ -23,6 +23,8 @@ void __ui_progress__init(struct ui_progress *p, u64 total,
void ui_progress__update(struct ui_progress *p, u64 adv);
void stdio_progress__init(void);
struct ui_progress_ops {
void (*init)(struct ui_progress *p);
void (*update)(struct ui_progress *p);

View File

@ -0,0 +1,170 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Progress feedback for the stdio (non-TUI/GTK) case, enabled with
* 'perf report --progress'.
*/
#include <inttypes.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/kernel.h>
#include <subcmd/pager.h>
#include "../../util/debug.h"
#include "../../util/units.h"
#include "../progress.h"
/*
* Phases can be nested, so keep track of the ones started so far to be
* able to complete the right one on ui_progress__finish(), which gets
* no arguments.
*/
#define STDIO_PROGRESS__MAX_DEPTH 8
struct stdio_progress_phase {
struct ui_progress *p;
u64 last_printed;
size_t last_len;
};
static struct stdio_progress_phase stdio_progress__stack[STDIO_PROGRESS__MAX_DEPTH];
static int stdio_progress__depth;
static FILE *stdio_progress__out;
static bool stdio_progress__is_tty;
/* Phases that didn't fit on the stack are not shown. */
static int stdio_progress__dropped;
static void stdio_progress__print_phase(struct stdio_progress_phase *phase,
u64 curr)
{
struct ui_progress *p = phase->p;
char buf_cur[20], buf_tot[20], buf[128];
double percent = p->total ? 100.0 * (double)curr / (double)p->total : 0.0;
size_t len;
/*
* Only the completion line shows 100.0%: a 99.99% progress would round
* up to it and look like a duplicate at finish time.
*/
if (curr < p->total && percent > 99.9)
percent = 99.9;
if (p->size) {
unit_number__scnprintf(buf_cur, sizeof(buf_cur), curr);
unit_number__scnprintf(buf_tot, sizeof(buf_tot), p->total);
len = scnprintf(buf, sizeof(buf), "%s [%5.1f%%] %s / %s",
p->title, percent, buf_cur, buf_tot);
} else {
len = scnprintf(buf, sizeof(buf), "%s [%5.1f%%] %" PRIu64 " / %" PRIu64,
p->title, percent, curr, p->total);
}
if (!stdio_progress__is_tty) {
fprintf(stdio_progress__out, "%s\n", buf);
goto out;
}
/* Pad to the length of the previous line to erase its leftovers. */
fprintf(stdio_progress__out, "\r%s%*s", buf,
(int)(len < phase->last_len ? phase->last_len - len : 0), "");
phase->last_len = len;
out:
phase->last_printed = curr;
fflush(stdio_progress__out);
}
static void __stdio_progress__init(struct ui_progress *p)
{
/* The default step is meant for the TUI bar, use 1% steps for stdio. */
p->next = p->step = p->total / 100 ?: 1;
if (stdio_progress__depth == STDIO_PROGRESS__MAX_DEPTH) {
/*
* Out of room: don't start this phase, its finish() is
* swallowed and its updates ignored below.
*/
pr_warning("progress phases nested deeper than %d, not showing progress for %s\n",
STDIO_PROGRESS__MAX_DEPTH, p->title);
stdio_progress__dropped++;
return;
}
/* Start a nested phase in a line of its own. */
if (stdio_progress__depth && stdio_progress__is_tty)
fputc('\n', stdio_progress__out);
stdio_progress__stack[stdio_progress__depth++] =
(struct stdio_progress_phase) {
.p = p,
.last_printed = 0,
.last_len = 0,
};
stdio_progress__print_phase(&stdio_progress__stack[stdio_progress__depth - 1],
p->curr);
}
static void stdio_progress__update(struct ui_progress *p)
{
/*
* An update that doesn't match the innermost phase means something
* is out of sync: print nothing rather than another phase's
* numbers, or read past the stack.
*/
if (!stdio_progress__depth ||
stdio_progress__stack[stdio_progress__depth - 1].p != p)
return;
stdio_progress__print_phase(&stdio_progress__stack[stdio_progress__depth - 1],
p->curr);
}
static void stdio_progress__finish(void)
{
struct stdio_progress_phase *phase;
/*
* Being the innermost phase, its finish() comes first: swallow it, or
* it would complete the phase that encloses it.
*/
if (stdio_progress__dropped) {
stdio_progress__dropped--;
return;
}
if (!stdio_progress__depth)
return;
phase = &stdio_progress__stack[--stdio_progress__depth];
/*
* The last line may have stopped short of the total, close this phase
* showing it as complete unless that was already printed.
*/
if (phase->last_printed != phase->p->total)
stdio_progress__print_phase(phase, phase->p->total);
phase->last_printed = 0;
phase->last_len = 0;
if (stdio_progress__is_tty)
fputc('\n', stdio_progress__out);
fflush(stdio_progress__out);
}
static struct ui_progress_ops stdio_progress__ops = {
.init = __stdio_progress__init,
.update = stdio_progress__update,
.finish = stdio_progress__finish,
};
void stdio_progress__init(void)
{
if (pager_in_use()) {
/* stderr leads to the pager, the updates go to the terminal. */
stdio_progress__out = fopen("/dev/tty", "w");
}
if (!stdio_progress__out)
stdio_progress__out = stderr;
stdio_progress__is_tty = isatty(fileno(stdio_progress__out)) == 1;
ui_progress__ops = &stdio_progress__ops;
}

View File

@ -237,14 +237,16 @@ static int do_flush(struct ordered_events *oe, bool show_progress)
ui_progress__init(&prog, oe->nr_events, "Processing time ordered events...");
list_for_each_entry_safe(iter, tmp, head, list) {
if (session_done())
return 0;
if (session_done()) {
ret = 0;
goto out_progress;
}
if (iter->timestamp > limit)
break;
ret = oe->deliver(oe, iter);
if (ret < 0)
return ret;
goto out_progress;
ordered_events__delete(oe, iter);
oe->last_flush = iter->timestamp;
@ -258,10 +260,16 @@ static int do_flush(struct ordered_events *oe, bool show_progress)
else if (last_ts <= limit)
oe->last = list_entry(head->prev, struct ordered_event, list);
ret = 0;
out_progress:
/*
* Always pair ui_progress__init() with ui_progress__finish(), the
* stdio backend tracks the phases on a stack.
*/
if (show_progress)
ui_progress__finish();
return 0;
return ret;
}
static int __ordered_events__flush(struct ordered_events *oe, enum oe_flush how,

View File

@ -3261,8 +3261,12 @@ static int __perf_session__process_pipe_events(struct perf_session *session)
cur_size = sizeof(union perf_event);
buf = malloc(cur_size);
if (!buf)
return -errno;
if (!buf) {
err = -errno;
if (update_prog)
ui_progress__finish();
return err;
}
ordered_events__set_copy_on_queue(oe, true);
more:
event = buf;
@ -3761,8 +3765,10 @@ static int __perf_session__process_dir_events(struct perf_session *session)
}
rd = calloc(nr_readers, sizeof(struct reader));
if (!rd)
if (!rd) {
ui_progress__finish();
return -ENOMEM;
}
rd[0] = (struct reader) {
.fd = perf_data__fd(session->data),