From: Arnaldo Carvalho de Melo <acme@kernel.org>
To: Namhyung Kim <namhyung@kernel.org>
Cc: Ingo Molnar <mingo@kernel.org>,
Thomas Gleixner <tglx@linutronix.de>,
James Clark <james.clark@linaro.org>,
Jiri Olsa <jolsa@kernel.org>, Ian Rogers <irogers@google.com>,
Adrian Hunter <adrian.hunter@intel.com>,
Clark Williams <williams@redhat.com>,
linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org,
Arnaldo Carvalho de Melo <acme@redhat.com>
Subject: [PATCH v1 01/12] perf scripts: Add a coverage harness for perf
Date: Fri, 13 Nov 2026 12:05:42 -0300 [thread overview]
Message-ID: <20261113150555.1163049-2-acme@kernel.org> (raw)
In-Reply-To: <20261113150555.1163049-1-acme@kernel.org>
From: Arnaldo Carvalho de Melo <acme@redhat.com>
Build perf with clang source based coverage, run the complete perf test
suite, merge the raw profiles and report line and function coverage for
perf, optionally against a baseline. This is pahole's
coverage/coverage.sh + coverage-diff.sh merged into one script, wired as
'make -C tools/perf coverage' and listed in 'make help'.
Without -f the report lists the files with the most uncovered lines,
which is where new tests should try to get more coverage; -t narrows
the tests, -f focuses on specific files, --html writes a browsable
report and --table/-T, also available as 'make -C tools/perf
coverage-table', prints the last run as an ASCII table with the
per-file line and function coverage, like pahole's coverage_table.py.
The build can be split from the test run: 'make -C tools/perf
coverage-prep' builds the instrumented perf and records the build
directory, and a later run as root, which needs no make, checks that
build, runs the tests and renders the report, or prints the table with
--table. This is for running the tests that need root on the bare
metal host while the instrumented perf is built in the development
container.
It is a maintainer tool and it already paid for itself: the first run
found the clang DWARF 5 data type test failing (the DW_OP_addrx gap,
fixed by the next patch), and the uncovered line list is what drove the
tests in the rest of the series.
$ scripts/coverage.sh
...
Total: 67436 of 201128 lines covered (33.53%)
largest uncovered files (>= 100 lines)
file lines cover missed fn-cover
builtin-trace.c 3657 4.24% 3502 0.51%
util/intel-pt.c 3237 0.00% 3237 0.00%
util/intel-pt-decoder/intel-pt-decoder.c 3208 0.00% 3208 0.00%
builtin-sched.c 3737 19.72% 3000 11.35%
ui/browsers/hists.c 2730 0.00% 2730 0.00%
util/probe-event.c 2765 5.39% 2616 10.68%
builtin-script.c 3212 29.98% 2249 33.06%
...
Report: /tmp/build/perf-coverage/cov/report.txt
The run above was unprivileged, so tests that need root are skipped or
fail; the report still points at the gaps.
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
tools/perf/.gitignore | 1 +
tools/perf/Makefile | 11 +-
tools/perf/Makefile.perf | 4 +
tools/perf/scripts/coverage.sh | 703 +++++++++++++++++++++++++++++++++
4 files changed, 718 insertions(+), 1 deletion(-)
create mode 100755 tools/perf/scripts/coverage.sh
diff --git a/tools/perf/.gitignore b/tools/perf/.gitignore
index 3b968c5158b822ce..9052f25597ac9489 100644
--- a/tools/perf/.gitignore
+++ b/tools/perf/.gitignore
@@ -27,6 +27,7 @@ stats-*.csv
cscope*
config.mak
config.mak.autogen
+.coverage-build-dir
*-bison.*
*-flex.*
*.pyc
diff --git a/tools/perf/Makefile b/tools/perf/Makefile
index 00baf90e5bda7123..d00b63ca011c2f6a 100644
--- a/tools/perf/Makefile
+++ b/tools/perf/Makefile
@@ -140,6 +140,15 @@ check-headers:
sync-headers:
@./scripts/sync-headers.sh $(SYNC_HEADERS_ARGS)
+coverage-prep:
+ @./scripts/coverage.sh --prep $(if $(O),--build-dir=$(O)) $(COVERAGE_ARGS)
+
+coverage:
+ @./scripts/coverage.sh $(COVERAGE_ARGS)
+
+coverage-table:
+ @./scripts/coverage.sh --table $(COVERAGE_ARGS)
+
#
# All other targets get passed through:
#
@@ -147,4 +156,4 @@ sync-headers:
$(print_msg)
$(make)
-.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers
+.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers coverage coverage-prep coverage-table
diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf
index b74b534e1153ffcb..782929b0ba10d8f3 100644
--- a/tools/perf/Makefile.perf
+++ b/tools/perf/Makefile.perf
@@ -873,6 +873,10 @@ help:
@echo ' check-headers - check which header copies are out of sync with the kernel'
@echo ' sync-headers - sync those header copies with the kernel, creating commits'
@echo ' (SYNC_HEADERS_ARGS="..." passes options to the script)'
+ @echo ' coverage-prep - build perf with clang coverage, ready for a root test run'
+ @echo ' coverage - build perf with clang coverage, run the tests and report'
+ @echo ' coverage-table - print an ASCII table of the last coverage run'
+ @echo ' (COVERAGE_ARGS="..." passes options to the script)'
DOC_TARGETS := doc man html info pdf
diff --git a/tools/perf/scripts/coverage.sh b/tools/perf/scripts/coverage.sh
new file mode 100755
index 0000000000000000..19b0da22000c3f0d
--- /dev/null
+++ b/tools/perf/scripts/coverage.sh
@@ -0,0 +1,703 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (C) 2026, Red Hat Inc, Arnaldo Carvalho de Melo <acme@redhat.com>
+#
+# LLVM source-based coverage for perf: build with clang instrumentation,
+# run the complete perf test suite and report line/function coverage for
+# perf, optionally comparing it against a previously saved report. -t
+# narrows the tests and -f adds files to the per-file table; without -f
+# the report lists the files with the most uncovered lines, which is
+# where new tests are needed. --table prints the last run as an ASCII
+# coverage table, like pahole's coverage_table.py. The build can be
+# split from the test run: --prep builds the instrumented perf and
+# records the build directory, and a later run as root uses that build,
+# skips the build and goes straight to the tests. The sampling based
+# tests move the percentages by a percent or two between identical runs.
+
+set -euo pipefail
+
+PERF_DIR=$(cd "$(dirname "$0")/.." && pwd)
+
+DEFAULT_BUILD_DIR=/tmp/build/perf-coverage
+BUILD_DIR=${BUILD_DIR:-}
+COV_DIR=${COV_DIR:-}
+BUILD_DIR_SET=0
+COV_DIR_SET=0
+[ -n "$BUILD_DIR" ] && BUILD_DIR_SET=1
+[ -n "$COV_DIR" ] && COV_DIR_SET=1
+JOBS=${JOBS:-$(nproc 2>/dev/null || echo 1)}
+LLVM_PROFDATA=${LLVM_PROFDATA:-llvm-profdata}
+LLVM_COV=${LLVM_COV:-llvm-cov}
+
+DEBUG_BUILD=1
+SKIP_BUILD=0
+REPORT_ONLY=0
+PREP=0
+TABLE=0
+HTML=0
+USE_BASELINE=1
+BASELINE=
+UNCOVERED=${UNCOVERED:-20}
+TOP=${TOP:-20}
+MIN_LINES=${MIN_LINES:-100}
+DWARF_VERSION=${DWARF_VERSION:-4}
+TESTS=()
+
+FOCUS=()
+
+ROOT=0
+[ "$(id -u)" = 0 ] && ROOT=1
+
+say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
+warn() { printf '\033[0;33mwarning:\033[0m %s\n' "$*" >&2; }
+die() { printf '\033[0;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
+
+usage()
+{
+ cat <<EOF
+Usage: scripts/coverage.sh [options] [test name fragment...]
+
+Builds perf with clang source-based coverage, runs the complete perf
+test suite and reports coverage for perf. Use -t to run a subset and
+-f to focus the per-file table.
+
+Options:
+ -b, --build-dir DIR perf build directory (default: $DEFAULT_BUILD_DIR)
+ -c, --cov-dir DIR profiles and reports (default: BUILD_DIR/cov)
+ -j, --jobs N parallel build jobs (default: $JOBS)
+ -t, --tests "A;B" test name fragments, ';' separated (default: all)
+ -f, --focus FILE add a file to the per-file table (repeatable)
+ --top N when no -f is given, show the N files with the
+ most uncovered lines (default: $TOP, 0 to disable)
+ --baseline FILE baseline report (default: COV_DIR/baseline.txt)
+ --no-baseline do not compare against or update a baseline
+ --uncovered N uncovered line numbers per focus file (default: $UNCOVERED)
+ --skip-build reuse the existing build, just run the tests
+ --report-only reuse build and profiles, just render the report
+ --prep build the instrumented perf and stop; run the
+ tests later as root with this script
+ -T, --table print an ASCII table of the last run and exit,
+ like pahole's coverage_table.py
+ --optimized build with perf's default -O3 instead of -O0 -g
+ --dwarf-version N DWARF version for the instrumented build (default: $DWARF_VERSION)
+ --html also write an HTML report
+ -h, --help show this help
+
+Environment:
+ BUILD_DIR, COV_DIR, JOBS, LLVM_COV, LLVM_PROFDATA, UNCOVERED, TOP,
+ MIN_LINES, DWARF_VERSION
+
+As root, without --prep, the script skips the build, checks the perf
+prepared by 'make coverage-prep', runs the tests and renders the
+report. The prepared build is recorded in tools/perf/.coverage-build-dir.
+
+Through make:
+ make -C tools/perf coverage COVERAGE_ARGS="-t 'perf annotate'"
+ make -C tools/perf coverage-prep O=/tmp/build/perf-tools-next
+ make -C tools/perf coverage-table
+
+Examples:
+ scripts/coverage.sh # full suite, fresh build
+ scripts/coverage.sh --prep # build only, test later as root
+ scripts/coverage.sh --report-only # re-render the last run
+ scripts/coverage.sh --table # ASCII table of the last run
+ scripts/coverage.sh -t "perf annotate" # a single test fragment
+ scripts/coverage.sh -f util/evsel.c # add a file to the table
+EOF
+}
+
+while [ $# -gt 0 ]; do
+ arg=$1
+ optval=
+ case "$arg" in
+ --*=*) optval=${arg#*=}; arg=${arg%%=*} ;;
+ esac
+
+ case "$arg" in
+ -b|--build-dir) BUILD_DIR=${optval:-${2?missing value}}; BUILD_DIR_SET=1
+ [ -z "$optval" ] && shift ;;
+ -c|--cov-dir) COV_DIR=${optval:-${2?missing value}}; COV_DIR_SET=1
+ [ -z "$optval" ] && shift ;;
+ -j|--jobs) JOBS=${optval:-${2?missing value}}; [ -z "$optval" ] && shift ;;
+ -f|--focus) FOCUS+=("${optval:-${2?missing value}}"); [ -z "$optval" ] && shift ;;
+ --baseline) BASELINE=${optval:-${2?missing value}}; [ -z "$optval" ] && shift ;;
+ --uncovered) UNCOVERED=${optval:-${2?missing value}}; [ -z "$optval" ] && shift ;;
+ --top) TOP=${optval:-${2?missing value}}; [ -z "$optval" ] && shift ;;
+ --dwarf-version) DWARF_VERSION=${optval:-${2?missing value}}; [ -z "$optval" ] && shift ;;
+ -t|--tests) IFS=';' read -r -a _t <<< "${optval:-${2?missing value}}"
+ TESTS+=("${_t[@]}"); [ -z "$optval" ] && shift ;;
+ --no-baseline) USE_BASELINE=0 ;;
+ --skip-build) SKIP_BUILD=1 ;;
+ --report-only) REPORT_ONLY=1; SKIP_BUILD=1 ;;
+ --prep) PREP=1 ;;
+ -T|--table) TABLE=1; REPORT_ONLY=1; SKIP_BUILD=1 ;;
+ --optimized) DEBUG_BUILD=0 ;;
+ --html) HTML=1 ;;
+ -h|--help) usage; exit 0 ;;
+ -*) die "unknown option: $arg (see --help)" ;;
+ *) TESTS+=("$arg") ;;
+ esac
+ shift
+done
+
+#
+# The build directory is recorded by --prep (and by a full run) so a
+# later root run, which does not build, finds it without arguments.
+#
+if [ "$BUILD_DIR_SET" = 0 ] && [ -z "$BUILD_DIR" ] && [ "$PREP" = 0 ] &&
+ { [ "$ROOT" = 1 ] || [ "$SKIP_BUILD" = 1 ] ||
+ [ "$REPORT_ONLY" = 1 ] || [ "$TABLE" = 1 ]; } &&
+ [ -f "$PERF_DIR/.coverage-build-dir" ]; then
+ read -r BUILD_DIR < "$PERF_DIR/.coverage-build-dir"
+fi
+[ -n "$BUILD_DIR" ] || BUILD_DIR=$DEFAULT_BUILD_DIR
+
+if [ "$COV_DIR_SET" = 0 ] && [ -z "$COV_DIR" ] &&
+ [ -f "$BUILD_DIR/cov/state" ]; then
+ COV_DIR=$(awk -F= '$1 == "COV_DIR" { print $2; exit }' \
+ "$BUILD_DIR/cov/state")
+fi
+[ -n "$COV_DIR" ] || COV_DIR=$BUILD_DIR/cov
+
+# The tests run from the kernel tree root, so keep these absolute.
+abspath()
+{
+ case "$1" in
+ /*) printf '%s\n' "$1" ;;
+ *) printf '%s/%s\n' "$PWD" "$1" ;;
+ esac
+}
+BUILD_DIR=$(abspath "$BUILD_DIR")
+COV_DIR=$(abspath "$COV_DIR")
+
+[ -n "$BASELINE" ] || BASELINE=$COV_DIR/baseline.txt
+[[ $TOP =~ ^[0-9]+$ ]] || die "--top wants a number, not '$TOP'"
+[[ $MIN_LINES =~ ^[0-9]+$ ]] || die "MIN_LINES wants a number, not '$MIN_LINES'"
+
+# Root does not build: use the perf prepared by coverage-prep.
+if [ "$ROOT" = 1 ] && [ "$PREP" = 0 ] && [ "$TABLE" = 0 ]; then
+ SKIP_BUILD=1
+ say "Running as root, using the coverage build in $BUILD_DIR"
+fi
+
+BUILD_LOG=$COV_DIR/build.log
+TEST_LOG=$COV_DIR/tests.log
+REPORT=$COV_DIR/report.txt
+PROFDATA=$COV_DIR/perf.profdata
+# Discovery point for a later root or --table run, so it stays under
+# BUILD_DIR even when --cov-dir points elsewhere.
+STATE_FILE=$BUILD_DIR/cov/state
+POINTER=$PERF_DIR/.coverage-build-dir
+
+#
+# Sanity checks
+#
+[ -d "$PERF_DIR" ] || die "$PERF_DIR not found"
+
+if [ "$TABLE" = 0 ]; then
+ if [ "$PREP" = 0 ]; then
+ command -v "$LLVM_COV" > /dev/null ||
+ die "$LLVM_COV not found (set LLVM_COV)"
+ fi
+ if [ "$SKIP_BUILD" = 0 ]; then
+ command -v clang > /dev/null ||
+ die "clang not found, it is the only compiler with -fcoverage-mapping"
+ fi
+ if [ "$PREP" = 0 ] && [ "$REPORT_ONLY" = 0 ]; then
+ command -v "$LLVM_PROFDATA" > /dev/null ||
+ die "$LLVM_PROFDATA not found (set LLVM_PROFDATA)"
+ fi
+ mkdir -p "$COV_DIR"
+fi
+
+#
+# Build: clang, DEBUG=1 gives -O0 -g like pahole's coverage build, which
+# keeps the line mapping honest at the cost of slower test runs. DWARF 4
+# is the default; --dwarf-version 5 describes the workloads linked into
+# perf with DW_OP_addrx, the encoding clang uses for globals in DWARF 5.
+#
+check_coverage_build()
+{
+ [ -x "$BUILD_DIR/perf" ] ||
+ die "no $BUILD_DIR/perf, run 'make -C $PERF_DIR O=$BUILD_DIR coverage-prep' first"
+ if command -v nm > /dev/null; then
+ # No -q: grep reads all of nm's output, so pipefail does
+ # not see a SIGPIPE from an early exit.
+ nm "$BUILD_DIR/perf" 2>/dev/null |
+ grep '__llvm_profile' > /dev/null ||
+ die "$BUILD_DIR/perf was not built for coverage, run coverage-prep"
+ else
+ warn "nm not found, cannot verify that $BUILD_DIR/perf is instrumented"
+ fi
+}
+
+write_state()
+{
+ mkdir -p "$(dirname "$STATE_FILE")" ||
+ die "could not create $(dirname "$STATE_FILE")"
+ {
+ printf 'BUILD_DIR=%s\n' "$BUILD_DIR"
+ printf 'COV_DIR=%s\n' "$COV_DIR"
+ printf 'DEBUG_BUILD=%s\n' "$DEBUG_BUILD"
+ printf 'DWARF_VERSION=%s\n' "$DWARF_VERSION"
+ } > "$STATE_FILE" || die "could not write $STATE_FILE"
+
+ if ! printf '%s\n' "$BUILD_DIR" > "$POINTER" 2>/dev/null; then
+ warn "could not write $POINTER, root runs will need -b $BUILD_DIR"
+ fi
+}
+
+build_perf()
+{
+ if [ "$SKIP_BUILD" = 1 ]; then
+ check_coverage_build
+ return
+ fi
+
+ say "Building perf in $BUILD_DIR"
+ mkdir -p "$BUILD_DIR"
+ local debug=()
+ [ "$DEBUG_BUILD" = 1 ] && debug=(DEBUG=1)
+
+ if ! make -C "$PERF_DIR" O="$BUILD_DIR" CC=clang \
+ "${debug[@]+"${debug[@]}"}" \
+ EXTRA_CFLAGS="-fprofile-instr-generate -fcoverage-mapping -gdwarf-$DWARF_VERSION" \
+ -j"$JOBS" > "$BUILD_LOG" 2>&1; then
+ tail -20 "$BUILD_LOG" >&2
+ die "build failed, full log in $BUILD_LOG"
+ fi
+ tail -1 "$BUILD_LOG"
+ write_state
+}
+
+#
+# Run the selected tests with the instrumented perf in PATH: the shell
+# tests, the workloads and 'perf test' itself all come from there.
+#
+run_tests()
+{
+ if [ ${#TESTS[@]} -eq 0 ]; then
+ say "Running the complete perf test suite"
+ else
+ say "Running perf tests: ${TESTS[*]}"
+ fi
+ rm -rf "$COV_DIR/raw"
+ mkdir -p "$COV_DIR/raw"
+
+ set +e
+ # The shell tests only get discovered from the kernel tree root.
+ (cd "$PERF_DIR/../.." &&
+ PATH="$BUILD_DIR:$PATH" \
+ LLVM_PROFILE_FILE="$COV_DIR/raw/perf-%p-%m.profraw" \
+ "$BUILD_DIR/perf" test -v \
+ "${TESTS[@]+"${TESTS[@]}"}") 2>&1 | tee "$TEST_LOG"
+ test_status=${PIPESTATUS[0]}
+ set -e
+
+ # 'perf test' exits 0 even when tests fail, only the summary tells.
+ if [ "$test_status" = 0 ] && grep -qE '^Failed tests +: +[1-9]' "$TEST_LOG"; then
+ test_status=1
+ fi
+
+ grep -E '^(Passed|Skipped|Failed) .*:' "$TEST_LOG" || true
+ [ "$test_status" = 0 ] ||
+ warn "some tests failed or were skipped, coverage is still collected"
+ if [ "$test_status" != 0 ] && grep -q 'Access to performance monitoring' "$TEST_LOG"; then
+ warn "perf_event_paranoid=$(cat /proc/sys/kernel/perf_event_paranoid 2>/dev/null)" \
+ "denied events to tests recording with callchains;" \
+ "run as root or lower it for the full suite"
+ fi
+}
+
+#
+# Merge the per-process profiles and render the text report.
+#
+merge_profiles()
+{
+ local count size
+ count=$(find "$COV_DIR/raw" -name '*.profraw' | wc -l)
+ size=$(du -sh "$COV_DIR/raw" 2>/dev/null | cut -f1)
+ [ "$count" -gt 0 ] ||
+ die "no .profraw files, was perf rebuilt with -fprofile-instr-generate?"
+
+ say "Merging $count profile files ($size)"
+ "$LLVM_PROFDATA" merge -sparse "$COV_DIR"/raw/*.profraw -o "$PROFDATA" ||
+ die "llvm-profdata merge failed"
+}
+
+llvm_cov_objects()
+{
+ echo "$BUILD_DIR/perf"
+ local so
+ for so in perf.so libperf.so libperf-jvmti.so; do
+ [ -f "$BUILD_DIR/$so" ] && echo "--object=$BUILD_DIR/$so"
+ done
+}
+
+render_report()
+{
+ local objects
+ mapfile -t objects < <(llvm_cov_objects)
+
+ say "Generating report"
+ "$LLVM_COV" report "${objects[@]}" --instr-profile="$PROFDATA" > "$REPORT"
+}
+
+#
+# Line coverage of a full report for one file: "lines missed cover fn%".
+# Report paths are relative to the build directory, so match the requested
+# path as a suffix: a build outside the source tree gives
+# ".../tools/perf/..." and one inside gives "tools/perf/...".
+#
+file_cov()
+{
+ awk -v f="$2" '
+ function perf_relpath(p) {
+ if (p ~ /(^|\/)tools\/perf\//)
+ sub(/^.*tools\/perf\//, "", p)
+ return p
+ }
+ {
+ g = f
+ sub(/^.*tools\/perf\//, "", g)
+ rel = perf_relpath($1)
+ if (rel == f || rel == g || $1 == f) {
+ print $8, $9, $10, $7
+ exit
+ }
+ }' "$1"
+}
+
+total_cov()
+{
+ awk '$1 == "TOTAL" { print $8, $9, $10 }' "$1"
+}
+
+total_delta()
+{
+ local prev=$1 cur=$2 old cur_cov
+ [ -f "$prev" ] || return 0
+ old=$(total_cov "$prev" | awk '{print $3}')
+ cur_cov=$(total_cov "$cur" | awk '{print $3}')
+ [ -n "$old" ] && [ -n "$cur_cov" ] || return 0
+ awk -v cur="$cur_cov" -v old="$old" 'BEGIN {
+ sub(/%$/, "", cur); sub(/%$/, "", old)
+ printf "%+.2f%%", cur - old }'
+}
+
+report_delta()
+{
+ local prev=$1 cur=$2 label=$3
+ local cur_cov old_cov delta
+ cur_cov=$(file_cov "$cur" "$label" | awk '{print $3}')
+ [ -n "$cur_cov" ] || return 0
+
+ if [ -f "$prev" ]; then
+ old_cov=$(file_cov "$prev" "$label" | awk '{print $3}')
+ if [ -n "$old_cov" ]; then
+ delta=$(awk -v cur="$cur_cov" -v old="$old_cov" 'BEGIN {
+ sub(/%$/, "", cur); sub(/%$/, "", old)
+ printf "%+.2f%%", cur - old }')
+ else
+ delta="(new)"
+ fi
+ else
+ old_cov="-"
+ delta="-"
+ fi
+ printf " %-8s %8s %8s %9s\n" "${label##*/}" "${old_cov:--}" "$cur_cov" "$delta"
+}
+
+#
+# With no -f, show where the uncovered lines are: the files with the most
+# missed lines are where new tests should try to get more coverage.
+#
+print_worst_files()
+{
+ [ "$TOP" -gt 0 ] || return 0
+ local rows missed lines cover fn path
+ rows=$(awk -v min="$MIN_LINES" '
+ $1 ~ /(^|\/)tools\/perf\// && $8 >= min && $8 > 0 {
+ path = $1
+ sub(/^.*tools\/perf\//, "", path)
+ printf "%s %s %s %s %s\n", $9, $8, $10, $7, path
+ }' "$REPORT" | sort -k1,1nr -k2,2nr | sed -n "1,${TOP}p")
+ [ -n "$rows" ] || return 0
+
+ printf " largest uncovered files (>= %s lines)\n" "$MIN_LINES"
+ printf " %-44s %8s %8s %8s %8s\n" \
+ "file" "lines" "cover" "missed" "fn-cover"
+ while read -r missed lines cover fn path; do
+ printf " %-44s %8s %8s %8s %8s\n" \
+ "$path" "$lines" "$cover" "$missed" "$fn"
+ done <<< "$rows"
+}
+
+print_summary()
+{
+ local lines missed cover delta file cov
+ read -r lines missed cover < <(total_cov "$REPORT")
+ printf "\n\033[1mTotal:\033[0m %s of %s lines covered (%s)" \
+ "$((lines - missed))" "$lines" "$cover"
+ if [ "$USE_BASELINE" = 1 ] && [ -f "$BASELINE" ]; then
+ delta=$(total_delta "$BASELINE" "$REPORT")
+ [ -n "$delta" ] && printf " (previous %s, %s)" \
+ "$(total_cov "$BASELINE" | awk '{print $3}')" "$delta"
+ fi
+ printf "\n\n"
+
+ if [ ${#FOCUS[@]} -eq 0 ]; then
+ print_worst_files
+ return
+ fi
+
+ if [ "$USE_BASELINE" = 1 ] && [ -f "$BASELINE" ]; then
+ printf " %-8s %8s %8s %9s\n" "File" "Previous" "Current" "Delta"
+ printf " %-8s %8s %8s %9s\n" "--------" "--------" "--------" "---------"
+ fi
+
+ printf " \033[1m%-44s\033[0m %8s %8s %8s %8s\n" \
+ "file" "lines" "cover" "missed" "fn-cover"
+ for file in "${FOCUS[@]}"; do
+ cov=$(file_cov "$REPORT" "$file")
+ if [ -z "$cov" ]; then
+ printf " %-36s %8s\n" "$file" "not covered"
+ continue
+ fi
+ printf " %-36s %8s %6s %6s %8s\n" "$file" \
+ "$(echo "$cov" | awk '{print $1}')" \
+ "$(echo "$cov" | awk '{print $3}')" \
+ "$(echo "$cov" | awk '{print $2}')" \
+ "$(echo "$cov" | awk '{print $4}')"
+ done
+
+ if [ "$USE_BASELINE" = 1 ] && [ -f "$BASELINE" ]; then
+ printf "\n per file delta since %s\n" "$BASELINE"
+ for file in "${FOCUS[@]}"; do
+ report_delta "$BASELINE" "$REPORT" "$file"
+ done
+ fi
+}
+
+#
+# --table: render the report of the last run as an ASCII table, like
+# pahole's coverage_table.py. Without -f the rows are the TOP files with
+# the most uncovered lines, sorted by line coverage; --top 0 lists every
+# file with at least MIN_LINES lines and uncovered lines. The total is
+# the whole report, which also counts the libraries perf links with.
+#
+bar()
+{
+ local pct=$1 width=$2 filled i out=
+ filled=$(awk -v p="$pct" -v w="$width" 'BEGIN { printf "%d", p * w / 100 }')
+ for ((i = 0; i < filled; i++)); do out+=█; done
+ for ((i = filled; i < width; i++)); do out+=░; done
+ printf '%s' "$out"
+}
+
+center()
+{
+ local left=$(( ($2 - ${#1}) / 2 ))
+ printf '%*s%s%*s' "$left" '' "$1" "$(( $2 - ${#1} - left ))" ''
+}
+
+dashes()
+{
+ local s
+ printf -v s '%*s' "$1" ''
+ printf '%s' "${s// /─}"
+}
+
+table_row()
+{
+ awk -v f="$2" '
+ function perf_relpath(p) {
+ if (p ~ /(^|\/)tools\/perf\//)
+ sub(/^.*tools\/perf\//, "", p)
+ return p
+ }
+ {
+ g = f
+ sub(/^.*tools\/perf\//, "", g)
+ path = perf_relpath($1)
+ if (path == f || path == g || $1 == f) {
+ printf "%s\t%s\t%d\t%d\t%.2f\t%s\t%d\t%s\n",
+ path, $8, $8 - $9, $9, $10, $5, $5 - $6, $7
+ exit
+ }
+ }' "$1"
+}
+
+table_rows()
+{
+ if [ ${#FOCUS[@]} -gt 0 ]; then
+ local file
+ for file in "${FOCUS[@]}"; do
+ table_row "$REPORT" "$file"
+ done | sort -t$'\t' -k5,5gr
+ return
+ fi
+
+ awk -v min="$MIN_LINES" '
+ $1 ~ /(^|\/)tools\/perf\// && $8 >= min && $9 > 0 {
+ path = $1
+ sub(/^.*tools\/perf\//, "", path)
+ printf "%s\t%s\t%d\t%d\t%.2f\t%s\t%d\t%s\n",
+ path, $8, $8 - $9, $9, $10, $5, $5 - $6, $7
+ }' "$REPORT" |
+ sort -t$'\t' -k4,4nr |
+ if [ "$TOP" -gt 0 ]; then sed -n "1,${TOP}p"; else cat; fi |
+ sort -t$'\t' -k5,5gr
+}
+
+print_table()
+{
+ local rows total path lines hit missed pct funcs funcs_hit func_cov
+ local row_fmt w d_file d29 d13 d10 d8
+
+ rows=$(table_rows)
+ [ -n "$rows" ] ||
+ die "no coverage data in $REPORT for the requested files"
+ total=$(awk '$1 == "TOTAL" {
+ printf "%s\t%d\t%d\t%.2f\t%s\t%d\t%s\n",
+ $8, $8 - $9, $9, $10, $5, $5 - $6, $7
+ }' "$REPORT")
+
+ w=4
+ while IFS=$'\t' read -r path _; do
+ if [ ${#path} -gt "$w" ]; then
+ w=${#path}
+ fi
+ done <<< "$rows"
+
+ d8=$(dashes 8)
+ d10=$(dashes 10)
+ d13=$(dashes 13)
+ d29=$(dashes 29)
+ d_file=$(dashes $((w + 2)))
+
+ printf '┌%s┬%s┬%s┬%s┬%s┬%s┬%s┐\n' \
+ "$d_file" "$d8" "$d8" "$d8" "$d29" "$d13" "$d10"
+ printf '│ %-*s │ %6s │ %6s │ %6s │ %s │ %11s │ %8s │\n' \
+ "$w" "File" "Lines" "Hit" "Miss" \
+ "$(center 'Line Coverage' 27)" "Funcs" "FuncCov"
+ printf '├%s┼%s┼%s┼%s┼%s┼%s┼%s┤\n' \
+ "$d_file" "$d8" "$d8" "$d8" "$d29" "$d13" "$d10"
+
+ row_fmt=$(printf '│ %%-%ds │ %%6s │ %%6s │ %%6s │ %%s %%5.1f%%%%' "$w")
+ row_fmt+=' │ %5s/%-5s │ %8s │\n'
+
+ while IFS=$'\t' read -r path lines hit missed pct funcs funcs_hit func_cov; do
+ printf "$row_fmt" "$path" "$lines" "$hit" "$missed" \
+ "$(bar "$pct" 20)" "$pct" "$funcs_hit" "$funcs" "$func_cov"
+ done <<< "$rows"
+
+ printf '├%s┼%s┼%s┼%s┼%s┼%s┼%s┤\n' \
+ "$d_file" "$d8" "$d8" "$d8" "$d29" "$d13" "$d10"
+ while IFS=$'\t' read -r lines hit missed pct funcs funcs_hit func_cov; do
+ printf "$row_fmt" "TOTAL" "$lines" "$hit" "$missed" \
+ "$(bar "$pct" 20)" "$pct" "$funcs_hit" "$funcs" "$func_cov"
+ done <<< "$total"
+ printf '└%s┴%s┴%s┴%s┴%s┴%s┴%s┘\n' \
+ "$d_file" "$d8" "$d8" "$d8" "$d29" "$d13" "$d10"
+}
+
+print_uncovered()
+{
+ [ "$UNCOVERED" -gt 0 ] || return 0
+ [ ${#FOCUS[@]} -gt 0 ] || return 0
+ local objects file out lines header=0
+ mapfile -t objects < <(llvm_cov_objects)
+
+ for file in "${FOCUS[@]}"; do
+ # No report row means the file was not built: print_summary()
+ # already said so, llvm-cov show would dump the whole profile.
+ [ -n "$(file_cov "$REPORT" "$file")" ] || continue
+ out=$("$LLVM_COV" show "${objects[@]}" --instr-profile="$PROFDATA" \
+ "$PERF_DIR/$file" 2>/dev/null |
+ awk -F'|' '$2 ~ /^[[:space:]]*0[[:space:]]*$/ {
+ gsub(/[[:space:]]/, "", $1); printf "%s ", $1 }')
+ lines=$(echo "$out" | wc -w)
+ [ "$lines" -gt 0 ] || continue
+ if [ "$header" = 0 ]; then
+ printf "\n uncovered lines (first %s per file)\n" "$UNCOVERED"
+ header=1
+ fi
+ printf " %-24s %4s lines: %s\n" "$file" "$lines" \
+ "$(echo "$out" | cut -d' ' -f1-"$UNCOVERED")"
+ done
+}
+
+save_html()
+{
+ [ "$HTML" = 1 ] || return 0
+ local objects
+ mapfile -t objects < <(llvm_cov_objects)
+ say "Writing HTML report to $COV_DIR/html"
+ "$LLVM_COV" show "${objects[@]}" --instr-profile="$PROFDATA" \
+ --format=html --output-dir="$COV_DIR/html" \
+ --show-line-counts-or-regions --show-branches=count
+}
+
+check_stale()
+{
+ [ "$REPORT_ONLY" = 1 ] || return 0
+ if [ ! -f "$PROFDATA" ]; then
+ [ "$TABLE" = 1 ] && return 0
+ die "no $PROFDATA, run without --report-only first"
+ fi
+ local newer
+ newer=$(find "$PERF_DIR" -name '*.c' -newer "$PROFDATA" -print -quit 2>/dev/null)
+ if [ -n "$newer" ]; then
+ warn "profiles are older than source changes (e.g. $newer), results may be stale"
+ fi
+}
+
+update_baseline()
+{
+ [ "$USE_BASELINE" = 1 ] || return 0
+ [ "$REPORT_ONLY" = 1 ] && return 0
+ [ -f "$BASELINE" ] && mv "$BASELINE" "$BASELINE.prev"
+ cp "$REPORT" "$BASELINE"
+ printf "\nBaseline saved: %s\n" "$BASELINE"
+}
+
+test_status=0
+
+if [ "$TABLE" = 1 ]; then
+ [ -f "$REPORT" ] || die "no $REPORT, run coverage first"
+ check_stale
+ print_table
+ exit 0
+fi
+
+if [ "$PREP" = 1 ]; then
+ build_perf
+ write_state
+ say "Coverage build ready: $BUILD_DIR/perf"
+ printf "As root: %s/scripts/coverage.sh\n" "$PERF_DIR"
+ exit 0
+fi
+
+if [ "$REPORT_ONLY" = 0 ]; then
+ build_perf
+ run_tests
+ merge_profiles
+else
+ check_coverage_build
+ check_stale
+fi
+
+[ -f "$PROFDATA" ] || die "no $PROFDATA, run without --report-only first"
+
+render_report
+print_summary
+print_uncovered
+save_html
+update_baseline
+
+printf "\nReport: %s\n" "$REPORT"
+[ "$HTML" = 1 ] && printf "HTML: %s\n" "$COV_DIR/html/index.html"
+
+exit "$test_status"
--
2.56.0
next prev parent reply other threads:[~2026-11-13 15:06 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 15:05 [PATCH v1 0/12] perf coverage: A harness for perf, and the fixes it found Arnaldo Carvalho de Melo
2026-11-13 15:05 ` Arnaldo Carvalho de Melo [this message]
2026-11-13 15:23 ` [PATCH v1 01/12] perf scripts: Add a coverage harness for perf sashiko-bot
2026-11-13 15:05 ` [PATCH v1 02/12] perf annotate-data: Resolve DW_OP_addrx global variables Arnaldo Carvalho de Melo
2026-11-13 15:14 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 03/12] perf tests: Exercise the flexible array member path Arnaldo Carvalho de Melo
2026-11-13 15:12 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 04/12] perf annotate-data: Count flexible array accesses for the whole struct Arnaldo Carvalho de Melo
2026-11-13 15:20 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 05/12] perf tests: Exercise data type resolution from a register Arnaldo Carvalho de Melo
2026-11-13 15:13 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 06/12] perf annotate: Print the stats after the annotations Arnaldo Carvalho de Melo
2026-11-13 15:24 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 07/12] perf tests: Silence the libbfd probe in build-test Arnaldo Carvalho de Melo
2026-11-13 15:08 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 08/12] perf build: Record the display-only features in FEATURE-DUMP Arnaldo Carvalho de Melo
2026-11-13 15:14 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 09/12] perf cs-etm: Fall back to the recorded instruction size on a failed read Arnaldo Carvalho de Melo
2026-11-13 15:11 ` sashiko-bot
2026-11-13 15:15 ` Leo Yan
2026-11-13 15:20 ` Arnaldo Carvalho de Melo
2026-11-13 15:05 ` [PATCH v1 10/12] perf tests: Fix the Intel PT virtual LBR check Arnaldo Carvalho de Melo
2026-11-13 15:25 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 11/12] perf test: Drop samples without an IP in the dlfilter test Arnaldo Carvalho de Melo
2026-11-13 15:13 ` sashiko-bot
2026-11-13 15:05 ` [PATCH v1 12/12] perf test: Skip the LBR inline unwinding check when unsupported Arnaldo Carvalho de Melo
2026-11-13 15:16 ` sashiko-bot
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=20261113150555.1163049-2-acme@kernel.org \
--to=acme@kernel.org \
--cc=acme@redhat.com \
--cc=adrian.hunter@intel.com \
--cc=irogers@google.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mingo@kernel.org \
--cc=namhyung@kernel.org \
--cc=tglx@linutronix.de \
--cc=williams@redhat.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.