[PATCH 4/4] perf script: Document and test --lazy-load-symbols and --max-symbol-bytes

From: Alireza Haghdoost via B4 Relay

Date: Tue Sep 15 2026 - 14:45:07 EST


From: Alireza Haghdoost <haghdoost@xxxxxxxx>

Document both new options in perf-script.txt: their interaction, the
memory tradeoff, that lazy loading applies only to userspace ELF DSOs
(kernel DSOs and modules always load eagerly), and that output may
differ from the default loader for some targets.

Add a shell test that records a small profile with callchains and
asserts:

- --lazy-load-symbols produces output byte-identical to the eager
loader, and that at least one symbol actually resolved (so the
comparison can't pass vacuously on all-[unknown] output);
- --max-symbol-bytes=1K, with and without lazy loading, forces
[unknown] resolution with a single warning and exit code 0 (no crash).

Signed-off-by: Alireza Haghdoost <haghdoost@xxxxxxxx>
Assisted-by: Kimi:K3
---
tools/perf/Documentation/perf-script.txt | 24 +++++
tools/perf/tests/shell/script_lazy_load_symbols.sh | 120 +++++++++++++++++++++
2 files changed, 144 insertions(+)

diff --git a/tools/perf/Documentation/perf-script.txt b/tools/perf/Documentation/perf-script.txt
index 200ea25891d8..b5a90ff22342 100644
--- a/tools/perf/Documentation/perf-script.txt
+++ b/tools/perf/Documentation/perf-script.txt
@@ -412,6 +412,30 @@ include::itrace.txt[]

Default: 127

+--lazy-load-symbols::
+ Resolve symbols lazily instead of eagerly loading the full
+ symbol table of every DSO that appears in a sample. A compact
+ sorted index is built per DSO and only the addresses that appear
+ in samples are materialized into symbols, with names read from the
+ file's string table at lookup time. This sharply reduces memory
+ (and usually time) for profiles of large binaries where only a
+ small fraction of the symbol table is referenced. This applies only
+ to userspace ELF DSOs; kernel DSOs and modules always load eagerly.
+ Output may differ from the default loader for some targets
+ (e.g. PPC64 .opd, .gnu_debugdata, or split debuginfo). Default: off.
+
+--max-symbol-bytes::
+ Limit the bytes held in struct symbol allocations (and, with
+ --lazy-load-symbols, the lazy index) for DSOs on the ELF symbol
+ loader path -- userspace DSOs plus vmlinux-as-ELF and kernel
+ modules. This is not a cap on all symbol memory or RSS: symbols
+ from kallsyms, JIT maps, PLT synthesis, and libbfd are counted
+ but not capped. Accepts a size with a B/K/M/G suffix (e.g. 128M).
+ When the budget is exceeded, further symbols resolve to [unknown]
+ and a warning is printed. This is a safety net independent of
+ --lazy-load-symbols and can be used with or without it. Default: 0
+ (unlimited).
+
--ns::
Use 9 decimal places when displaying time (i.e. show the nanoseconds)

diff --git a/tools/perf/tests/shell/script_lazy_load_symbols.sh b/tools/perf/tests/shell/script_lazy_load_symbols.sh
new file mode 100755
index 000000000000..799c61e2f3e8
--- /dev/null
+++ b/tools/perf/tests/shell/script_lazy_load_symbols.sh
@@ -0,0 +1,120 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# perf script lazy symbol loading tests
+#
+# Verifies that --lazy-load-symbols produces output identical to the default
+# eager symbol loader, and that --max-symbol-bytes caps symbol allocations
+# (emitting [unknown] plus a warning) without crashing.
+
+set -e
+
+shelldir=$(dirname "$0")
+# shellcheck source=lib/perf_has_symbol.sh
+. "${shelldir}"/lib/perf_has_symbol.sh
+
+testsym="test_loop"
+
+skip_test_missing_symbol ${testsym}
+
+err=0
+temp_dir=$(mktemp -d /tmp/__perf_test.lazy_load.XXXXX)
+perfdata="${temp_dir}/perf.data"
+eager_out="${temp_dir}/eager.out"
+lazy_out="${temp_dir}/lazy.out"
+
+cleanup() {
+ rm -rf "${temp_dir}"
+ trap - EXIT TERM INT
+}
+
+trap_cleanup() {
+ echo "Unexpected signal in ${FUNCNAME[1]}"
+ cleanup
+ exit 1
+}
+trap trap_cleanup EXIT TERM INT
+
+test_lazy_load_identical() {
+ echo "Lazy-load output matches eager loader"
+
+ # Record a small profile with callchains so symbol resolution runs.
+ if ! perf record -o "${perfdata}" -g -- perf test -w thloop 2> /dev/null
+ then
+ echo "Lazy-load identical [Skipped record not supported]"
+ return
+ fi
+
+ if ! perf script -i "${perfdata}" 2> /dev/null > "${eager_out}" || \
+ ! perf script --lazy-load-symbols -i "${perfdata}" 2> /dev/null > "${lazy_out}"
+ then
+ echo "Lazy-load identical [Failed perf script error]"
+ err=1
+ return
+ fi
+
+ # The comparison is only meaningful if something actually resolved;
+ # two all-[unknown] outputs would also match.
+ if ! grep -q "${testsym}" "${eager_out}"
+ then
+ echo "Lazy-load identical [Skipped no ${testsym} resolved]"
+ return
+ fi
+
+ if ! cmp -s "${eager_out}" "${lazy_out}"
+ then
+ echo "Lazy-load identical [Failed output differs]"
+ err=1
+ return
+ fi
+ echo "Lazy-load identical [Success]"
+}
+
+test_max_symbol_bytes() {
+ echo "--max-symbol-bytes budget enforcement"
+
+ # Depends on ${perfdata} from test_lazy_load_identical.
+ if [ ! -s "${perfdata}" ]
+ then
+ echo "--max-symbol-bytes budget [Skipped record not supported]"
+ return
+ fi
+
+ # A tiny budget forces most symbols to be dropped as [unknown],
+ # with a single warning, and must not crash.
+ if ! perf script --max-symbol-bytes=1K -i "${perfdata}" > /dev/null \
+ 2> "${temp_dir}/budget.err"
+ then
+ echo "--max-symbol-bytes budget [Failed nonzero exit]"
+ err=1
+ return
+ fi
+ if ! grep -q "symbol memory budget exceeded" "${temp_dir}/budget.err"
+ then
+ echo "--max-symbol-bytes budget [Failed missing warning]"
+ err=1
+ return
+ fi
+
+ if ! perf script --lazy-load-symbols --max-symbol-bytes=1K \
+ -i "${perfdata}" > /dev/null 2> "${temp_dir}/lazy-budget.err"
+ then
+ echo "--max-symbol-bytes lazy budget [Failed nonzero exit]"
+ err=1
+ return
+ fi
+ warnings=$(grep -c "symbol memory budget exceeded" \
+ "${temp_dir}/lazy-budget.err" || true)
+ if [ "${warnings}" -ne 1 ]
+ then
+ echo "--max-symbol-bytes lazy budget [Failed warning count: ${warnings}]"
+ err=1
+ return
+ fi
+ echo "--max-symbol-bytes budget [Success]"
+}
+
+test_lazy_load_identical
+test_max_symbol_bytes
+
+cleanup
+exit $err

--
Git-155)