From: Alireza Haghdoost via B4 Relay <devnull+haghdoost.uber.com@kernel.org>
To: Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Namhyung Kim <namhyung@kernel.org>,
Mark Rutland <mark.rutland@arm.com>,
Alexander Shishkin <alexander.shishkin@linux.intel.com>,
Jiri Olsa <jolsa@kernel.org>, Ian Rogers <irogers@google.com>,
Adrian Hunter <adrian.hunter@intel.com>,
James Clark <james.clark@linaro.org>,
Alexei Starovoitov <ast@kernel.org>,
Andrii Nakryiko <andriin@fb.com>
Cc: linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org,
Alireza Haghdoost <haghdoost@uber.com>
Subject: [PATCH v3 6/6] perf test: Test lazy symbol loading and symbol memory limits
Date: Fri, 25 Sep 2026 12:09:43 -0700 [thread overview]
Message-ID: <20260925-perf-symbol-memory-send-v3-6-3e4e234c363b@uber.com> (raw)
In-Reply-To: <20260925-perf-symbol-memory-send-v3-0-3e4e234c363b@uber.com>
From: Alireza Haghdoost <haghdoost@uber.com>
Add a perf script shell test for --lazy-load-symbols and
--max-symbol-bytes.
Record a small callchain fixture, require evidence that the controlled
perf DSO built an on-demand index, and compare only extracted occurrences
of the controlled test_loop symbol. This avoids coupling the test to
addresses, diagnostics, or architecture-specific symbols for which the
eager and lazy loaders have documented differences.
Derive the constrained lazy budget from the unlimited run's
index-reservation peak. This gives a deterministic boundary where the
index fits and later materialization reaches the limit. Also verify eager
limiting, malformed size rejection, and one-time warning behavior.
Report unsupported recording, missing controlled output, unavailable
libelf, and unavailable dependent data as skips without replacing a prior
failure. Keep helper returns safe under set -e, and cover skip-status
preservation separately.
Add unit tests for the shared duplicate-selection policy, truncated
string-table reads, lazy address and name lookup across a closed data
descriptor, and address lookups racing name lookups while the symbol
budget truncates materialization. The last test also checks that a DSO
does not change once its name-sorted array has been built.
Add a unit test that loads a DSO (perf itself, or the one given with
--dso) eagerly and lazily and compares every symbol address and name.
Run it from a shell test on a hand-built split-debuginfo binary whose
function section is NOBITS in the debug file and has no PT_LOAD in the
runtime file, so both loaders must fall back to the runtime section
header.
Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
---
.../tests/shell/lazy_load_symbols_split_debug.sh | 113 +++++
tools/perf/tests/shell/script_lazy_load_symbols.sh | 278 ++++++++++++
.../tests/shell/script_lazy_load_symbols_skip.sh | 26 ++
tools/perf/tests/symbol-bytes.c | 505 +++++++++++++++++++++
4 files changed, 922 insertions(+)
diff --git a/tools/perf/tests/shell/lazy_load_symbols_split_debug.sh b/tools/perf/tests/shell/lazy_load_symbols_split_debug.sh
new file mode 100755
index 000000000000..4e6335d88377
--- /dev/null
+++ b/tools/perf/tests/shell/lazy_load_symbols_split_debug.sh
@@ -0,0 +1,113 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# lazy symbol loading with split debuginfo
+
+# Build a stripped binary whose symbols live in a --only-keep-debug file,
+# where a function's section is NOBITS (with a different sh_offset) and has
+# no PT_LOAD in the runtime ELF. Lazy and eager loading must then both fall
+# back to the runtime section header and agree on the symbol addresses.
+
+set -e
+
+err=0
+tmpdir=$(mktemp -d /tmp/__perf_test.lazy_split_debug.XXXXX)
+
+cleanup() {
+ rm -rf "${tmpdir}"
+ trap - EXIT TERM INT
+}
+
+trap_cleanup() {
+ echo "Unexpected signal in ${FUNCNAME[1]}"
+ cleanup
+ exit 1
+}
+trap trap_cleanup EXIT TERM INT
+
+skip() {
+ echo "Lazy-load split debuginfo [Skipped: $1]"
+ cleanup
+ exit 2
+}
+
+if ! perf check feature -q libelf; then
+ skip "no libelf support"
+fi
+
+for tool in cc objcopy strip readelf dd; do
+ if ! command -v "${tool}" > /dev/null; then
+ skip "${tool} not found"
+ fi
+done
+
+cat > "${tmpdir}/prog.c" << EOF
+__attribute__((section("splittext"), noinline, used))
+int split_func(int x)
+{
+ return x * 3 + 1;
+}
+
+int main(int argc, char **argv)
+{
+ (void)argv;
+ return split_func(argc);
+}
+EOF
+
+prog="${tmpdir}/prog"
+if ! cc -O1 -g -o "${prog}" "${tmpdir}/prog.c" \
+ -Wl,--section-start=splittext=0x800000 2> /dev/null; then
+ skip "cannot build test program"
+fi
+objcopy --only-keep-debug "${prog}" "${prog}.debug"
+strip -s "${prog}"
+objcopy --add-gnu-debuglink="${prog}.debug" "${prog}"
+
+# The debug file must keep splittext as NOBITS at a stale offset.
+debug_sec=$(readelf -SW "${prog}.debug" | grep ' splittext ' || true)
+run_sec=$(readelf -SW "${prog}" | grep ' splittext ' || true)
+if ! echo "${debug_sec}" | grep -q NOBITS; then
+ skip "splittext is not NOBITS in the debug file"
+fi
+debug_off=$(echo "${debug_sec}" | sed 's/.*splittext *//' | awk '{print $3}')
+run_off=$(echo "${run_sec}" | sed 's/.*splittext *//' | awk '{print $3}')
+if [ -z "${run_off}" ] || [ "${debug_off}" = "${run_off}" ]; then
+ skip "splittext offsets do not differ"
+fi
+
+# Drop the PT_LOAD covering splittext so program header lookup fails.
+phoff=$(readelf -hW "${prog}" | awk '/Start of program headers/ {print $5}')
+phentsize=$(readelf -hW "${prog}" | awk '/Size of program headers/ {print $5}')
+idx=$(readelf -lW "${prog}" | awk '
+ /^Program Headers:/ { in_ph = 1; next }
+ in_ph && /^ Type/ { next }
+ in_ph && /^ *$/ { exit }
+ in_ph && /^ [A-Z]/ {
+ if ($1 == "LOAD" && $3 ~ /^0x0*800000$/) { print n; exit }
+ n++
+ }')
+if [ -z "${phoff}" ] || [ -z "${phentsize}" ] || [ -z "${idx}" ]; then
+ skip "no PT_LOAD for splittext"
+fi
+dd if=/dev/zero of="${prog}" bs=1 seek=$((phoff + idx * phentsize)) count=4 \
+ conv=notrunc 2> /dev/null
+if readelf -lW "${prog}" 2> /dev/null | grep -q 'LOAD .*0x0*800000 '; then
+ echo "Lazy-load split debuginfo [Failed to drop PT_LOAD]"
+ err=1
+fi
+
+if [ "${err}" -eq 0 ]; then
+ export PERF_BUILDID_DIR="${tmpdir}/buildid"
+ if ! perf test --dso "${prog}" "Lazy and eager symbol parity" 2>&1 | \
+ grep -q ': Ok$'; then
+ perf test --dso "${prog}" -vv "Lazy and eager symbol parity" 2>&1 | \
+ grep -E 'symbols$|mismatch|no symbols|no lazy' || true
+ echo "Lazy-load split debuginfo [Failed parity]"
+ err=1
+ else
+ echo "Lazy-load split debuginfo [Success]"
+ fi
+fi
+
+cleanup
+exit ${err}
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..7e193c1c3d1f
--- /dev/null
+++ b/tools/perf/tests/shell/script_lazy_load_symbols.sh
@@ -0,0 +1,278 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# perf script lazy symbol loading tests (exclusive)
+#
+# Verifies that --lazy-load-symbols matches the default eager loader for a
+# controlled symbol, and that --max-symbol-bytes caps symbol allocations
+# (emitting [unknown] plus a warning) without crashing.
+
+mark_skip() {
+ if [ "${err}" -eq 0 ]; then
+ err=2
+ fi
+ return 0
+}
+
+if [ "${PERF_LAZY_LOAD_SYMBOLS_TEST_HELPERS:-}" = 1 ]; then
+ return 0
+fi
+
+set -e
+
+shelldir=$(dirname "$0")
+# shellcheck source=lib/perf_has_symbol.sh
+. "${shelldir}"/lib/perf_has_symbol.sh
+
+testsym="test_loop"
+perf_path=$(readlink -f "$(command -v perf)")
+lazy_index_budget=
+
+skip_test_missing_symbol ${testsym}
+
+if ! perf check feature -q libelf
+then
+ echo "Lazy symbol loading [Skipped no libelf support]"
+ exit 2
+fi
+
+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"
+lazy_err="${temp_dir}/lazy.err"
+eager_sym_out="${temp_dir}/eager.sym.out"
+lazy_sym_out="${temp_dir}/lazy.sym.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]"
+ mark_skip
+ return 0
+ fi
+
+ if ! perf script -i "${perfdata}" 2> /dev/null > "${eager_out}" || \
+ ! perf script -v --lazy-load-symbols -i "${perfdata}" \
+ 2> "${lazy_err}" > "${lazy_out}"
+ then
+ echo "Lazy-load identical [Failed perf script error]"
+ err=1
+ return
+ fi
+ if ! grep -q "on-demand index:" "${lazy_err}"
+ then
+ echo "Lazy-load identical [Failed lazy loader fell back to eager]"
+ err=1
+ return
+ fi
+ lazy_index_budget=$(awk -v dso="${perf_path}: on-demand index:" \
+ 'index($0, dso) { sub(/^.* budget=/, ""); print; exit }' \
+ "${lazy_err}")
+ case "${lazy_index_budget}" in
+ ''|*[!0-9]*)
+ echo "Lazy-load identical [Failed controlled DSO has no index]"
+ err=1
+ return
+ ;;
+ esac
+
+ # 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]"
+ mark_skip
+ return 0
+ fi
+
+ grep -w -o "${testsym}" "${eager_out}" > "${eager_sym_out}"
+ if ! grep -w -o "${testsym}" "${lazy_out}" > "${lazy_sym_out}"
+ then
+ echo "Lazy-load identical [Failed no lazy ${testsym} resolved]"
+ err=1
+ return
+ fi
+
+ if ! cmp -s "${eager_sym_out}" "${lazy_sym_out}"
+ then
+ echo "Lazy-load identical [Failed ${testsym} 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]"
+ mark_skip
+ return 0
+ 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 --max-symbol-bytes=1Kjunk -i "${perfdata}" \
+ > /dev/null 2>&1
+ then
+ echo "--max-symbol-bytes budget [Failed malformed size accepted]"
+ err=1
+ return
+ fi
+ if ! perf script --max-symbol-bytes=0 -i "${perfdata}" \
+ > /dev/null 2>&1
+ then
+ echo "--max-symbol-bytes budget [Failed zero not accepted]"
+ err=1
+ return
+ fi
+
+ # The unlimited run logged the peak accounted bytes at the controlled
+ # DSO's index reservation, before alias dedup may have released bytes.
+ # Reuse that peak as the budget: deterministic index construction fits,
+ # while subsequent materialization must hit the limit.
+ if ! perf script -v --lazy-load-symbols \
+ --max-symbol-bytes="${lazy_index_budget}B" \
+ -i "${perfdata}" > /dev/null 2> "${temp_dir}/lazy-budget.err"
+ then
+ echo "--max-symbol-bytes lazy budget [Failed nonzero exit]"
+ err=1
+ return
+ fi
+ if ! grep -Fq "${perf_path}: on-demand index:" \
+ "${temp_dir}/lazy-budget.err" ||
+ ! grep -q "symbol memory budget exceeded" "${temp_dir}/lazy-budget.err"
+ then
+ echo "--max-symbol-bytes lazy budget [Failed no indexed budget case]"
+ 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_budget_truncation_range() {
+ local longsym
+ local first_symbol
+ local trunc_source="${temp_dir}/truncation.S"
+ local trunc_binary="${temp_dir}/truncation"
+ local trunc_data="${temp_dir}/truncation.data"
+ local trunc_out="${temp_dir}/truncation.out"
+ local trunc_err="${temp_dir}/truncation.err"
+
+ echo "--max-symbol-bytes truncation range"
+
+ if [ "$(uname -m)" != x86_64 ]; then
+ echo "--max-symbol-bytes truncation range [Skipped x86_64 only]"
+ mark_skip
+ return 0
+ fi
+
+ longsym=$(printf 's%.0s' {1..900})
+ cat > "${trunc_source}" <<EOF
+ .text
+ .globl ${longsym}
+ .type ${longsym}, @function
+${longsym}:
+ call omitted_symbol
+ mov \$60, %eax
+ xor %edi, %edi
+ syscall
+
+ .globl omitted_symbol
+ .type omitted_symbol, @function
+omitted_symbol:
+ mov \$500000000, %ecx
+1:
+ dec %ecx
+ jnz 1b
+ ret
+ .size omitted_symbol, .-omitted_symbol
+EOF
+ if ! cc -nostdlib -no-pie -Wl,--build-id=none -Wl,-e,"${longsym}" \
+ -o "${trunc_binary}" "${trunc_source}"
+ then
+ echo "--max-symbol-bytes truncation range [Skipped compiler unsupported]"
+ mark_skip
+ return 0
+ fi
+
+ first_symbol=$(readelf -W -s "${trunc_binary}" |
+ awk '$4 == "FUNC" && $7 != "UND" { print $8; exit }')
+ if [ "${first_symbol}" != "${longsym}" ]; then
+ echo "--max-symbol-bytes truncation range [Skipped unexpected symbol order]"
+ mark_skip
+ return 0
+ fi
+
+ if ! perf record -o "${trunc_data}" -e cycles:u -F 1000 -- \
+ "${trunc_binary}" 2> /dev/null
+ then
+ echo "--max-symbol-bytes truncation range [Skipped record not supported]"
+ mark_skip
+ return 0
+ fi
+ if ! perf script --max-symbol-bytes=1K -i "${trunc_data}" -F ip,sym,dso \
+ > "${trunc_out}" 2> "${trunc_err}"
+ then
+ echo "--max-symbol-bytes truncation range [Failed perf script error]"
+ err=1
+ return
+ fi
+
+ if ! grep -q "symbol memory budget exceeded" "${trunc_err}" ||
+ ! grep -F "${trunc_binary}" "${trunc_out}" | grep -q '\[unknown\]' ||
+ grep -Fq "${longsym}" "${trunc_out}"
+ then
+ echo "--max-symbol-bytes truncation range [Failed omitted range resolved]"
+ err=1
+ return
+ fi
+ echo "--max-symbol-bytes truncation range [Success]"
+}
+
+test_lazy_load_identical
+test_max_symbol_bytes
+test_budget_truncation_range
+
+cleanup
+exit $err
diff --git a/tools/perf/tests/shell/script_lazy_load_symbols_skip.sh b/tools/perf/tests/shell/script_lazy_load_symbols_skip.sh
new file mode 100755
index 000000000000..136503863fdd
--- /dev/null
+++ b/tools/perf/tests/shell/script_lazy_load_symbols_skip.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# perf script lazy symbol loading skip status
+
+set -e
+
+shelldir=$(dirname "$0")
+PERF_LAZY_LOAD_SYMBOLS_TEST_HELPERS=1
+. "${shelldir}"/script_lazy_load_symbols.sh
+unset PERF_LAZY_LOAD_SYMBOLS_TEST_HELPERS
+
+err=0
+mark_skip
+if [ "${err}" -ne 2 ]; then
+ echo "Lazy-load skip status [Failed expected 2, got ${err}]"
+ exit 1
+fi
+
+err=1
+mark_skip
+if [ "${err}" -ne 1 ]; then
+ echo "Lazy-load skip status [Failed skip overwrote failure: ${err}]"
+ exit 1
+fi
+
+echo "Lazy-load skip status [Success]"
diff --git a/tools/perf/tests/symbol-bytes.c b/tools/perf/tests/symbol-bytes.c
index d3055f5ed321..eef14e6e573b 100644
--- a/tools/perf/tests/symbol-bytes.c
+++ b/tools/perf/tests/symbol-bytes.c
@@ -1,12 +1,27 @@
// SPDX-License-Identifier: GPL-2.0
+#include <fcntl.h>
+#include <inttypes.h>
#include <limits.h>
#include <pthread.h>
#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <linux/kernel.h>
+#include <linux/zalloc.h>
#include "debug.h"
+#include "dso.h"
+#include "env.h"
+#include "machine.h"
+#include "map.h"
#include "symbol.h"
#include "symbol_conf.h"
#include "tests.h"
+#include "thread.h"
+#include "util.h"
struct reserve_arg {
size_t bytes;
@@ -83,8 +98,498 @@ static int test__symbol_bytes_reservation(struct test_suite *test __maybe_unused
return ret;
}
+static int test__symbol_bytes_duplicate_selection(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ struct duplicate_case {
+ struct symbol_candidate a, b;
+ int expected;
+ } cases[] = {
+ { { 1, "a", STT_FUNC, STB_GLOBAL }, { 0, "b", STT_FUNC, STB_GLOBAL }, SYMBOL_A },
+ { { 1, "a", STT_NOTYPE, STB_GLOBAL }, { 1, "b", STT_FUNC, STB_GLOBAL }, SYMBOL_B },
+ { { 1, "a", STT_FUNC, STB_WEAK }, { 1, "b", STT_FUNC, STB_GLOBAL }, SYMBOL_B },
+ { { 1, "a", STT_FUNC, STB_GLOBAL }, { 1, "b", STT_FUNC, STB_LOCAL }, SYMBOL_A },
+ { { 1, "name", STT_FUNC, STB_GLOBAL },
+ { 1, "_name", STT_FUNC, STB_GLOBAL }, SYMBOL_A },
+ { { 1, "a", STT_FUNC, STB_GLOBAL }, { 1, "long", STT_FUNC, STB_GLOBAL }, SYMBOL_B },
+ };
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(cases); i++) {
+ if (symbol__choose_best(&cases[i].a, &cases[i].b) != cases[i].expected)
+ return TEST_FAIL;
+ }
+ return TEST_OK;
+}
+
+#ifdef HAVE_LIBELF_SUPPORT
+static int truncated_name_case(size_t file_size, unsigned int expected_reads)
+{
+ char path[] = "/tmp/perf-lazy-truncated-XXXXXX";
+ struct dso *data_dso = NULL;
+ char *contents = NULL;
+ char *name_heap = NULL;
+ char namebuf[1024];
+ const char *name;
+ unsigned int nr_reads;
+ int ret = TEST_FAIL;
+ int fd = -1;
+
+ contents = malloc(file_size);
+ if (!contents)
+ goto out;
+ memset(contents, 'a', file_size);
+
+ fd = mkstemp(path);
+ if (fd < 0 || write(fd, contents, file_size) != (ssize_t)file_size)
+ goto out;
+ close(fd);
+ fd = -1;
+
+ data_dso = dso__new(path);
+ if (!data_dso || dso__data_set_path(data_dso, path) < 0)
+ goto out;
+ dso__set_binary_type(data_dso, DSO_BINARY_TYPE__SYSTEM_PATH_DSO);
+ name = dso__read_ondemand_symbol_name(data_dso, 0, 8192, 0,
+ namebuf, sizeof(namebuf),
+ &name_heap, &nr_reads);
+ if (name || name_heap || nr_reads != expected_reads)
+ goto out;
+ ret = TEST_OK;
+out:
+ if (fd >= 0)
+ close(fd);
+ if (data_dso)
+ dso__put(data_dso);
+ unlink(path);
+ free(name_heap);
+ free(contents);
+ return ret;
+}
+
+static int test__symbol_bytes_truncated_name(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ /*
+ * One byte is short in the stack-buffer read. 1023 bytes fills it
+ * exactly, so the following read exercises the heap-buffer path.
+ */
+ if (truncated_name_case(1, 1) != TEST_OK ||
+ truncated_name_case(1023, 2) != TEST_OK)
+ return TEST_FAIL;
+ return TEST_OK;
+}
+
+#define LAZY_SYM_START 0x1000
+#define LAZY_SYM_SIZE 0x10
+#define LAZY_NAME_FMT "lazy_sym_%03u"
+#define LAZY_NAME_LEN sizeof("lazy_sym_000")
+
+static void lazy_name(char *buf, u32 i)
+{
+ snprintf(buf, LAZY_NAME_LEN, LAZY_NAME_FMT, i % 1000);
+}
+
+struct lazy_fixture {
+ char path[32];
+ struct dso *dso;
+ struct map *map;
+ u32 nr;
+};
+
+/*
+ * Build a DSO whose symbols exist only in a lazy index: @nr adjacent
+ * functions named lazy_sym_NNN, with names in a string-table file read
+ * through a private data DSO.
+ */
+static int lazy_fixture__init(struct lazy_fixture *f, u32 nr)
+{
+ struct dso_ondemand *od = NULL;
+ char *strtab;
+ int fd, ret = -1;
+ u32 i;
+
+ memset(f, 0, sizeof(*f));
+ f->nr = nr;
+ strcpy(f->path, "/tmp/perf-lazy-names-XXXXXX");
+
+ strtab = malloc(nr * LAZY_NAME_LEN);
+ if (!strtab)
+ return -1;
+ for (i = 0; i < nr; i++)
+ lazy_name(strtab + i * LAZY_NAME_LEN, i);
+
+ fd = mkstemp(f->path);
+ if (fd < 0) {
+ f->path[0] = '\0';
+ goto out;
+ }
+ if (write(fd, strtab, nr * LAZY_NAME_LEN) != (ssize_t)(nr * LAZY_NAME_LEN)) {
+ close(fd);
+ goto out;
+ }
+ close(fd);
+
+ f->dso = dso__new("/not/the/symbol/source");
+ od = zalloc(sizeof(*od));
+ if (!f->dso || !od)
+ goto out;
+ od->sorted = calloc(nr, sizeof(*od->sorted));
+ od->data_dso = dso__new(f->path);
+ if (!od->sorted || !od->data_dso ||
+ dso__data_set_path(od->data_dso, f->path) < 0)
+ goto out;
+ dso__set_binary_type(od->data_dso, DSO_BINARY_TYPE__SYSTEM_PATH_DSO);
+ od->strtab_size = nr * LAZY_NAME_LEN;
+ od->nr_sorted = nr;
+ od->nr_alloc = nr;
+ for (i = 0; i < nr; i++) {
+ od->sorted[i] = (struct sym_idx) {
+ .start = LAZY_SYM_START + i * LAZY_SYM_SIZE,
+ .end = LAZY_SYM_START + (i + 1) * LAZY_SYM_SIZE,
+ .name_off = i * LAZY_NAME_LEN,
+ .binding = STB_GLOBAL,
+ .type = STT_FUNC,
+ };
+ }
+ if (!symbol__try_account_bytes(od->nr_alloc * sizeof(*od->sorted)))
+ goto out;
+ dso__set_ondemand(f->dso, od);
+ od = NULL;
+ dso__set_loaded(f->dso);
+ f->map = map__new2(0, f->dso);
+ if (f->map)
+ ret = 0;
+out:
+ if (od) {
+ if (od->data_dso)
+ dso__put(od->data_dso);
+ free(od->sorted);
+ free(od);
+ }
+ free(strtab);
+ return ret;
+}
+
+static void lazy_fixture__exit(struct lazy_fixture *f)
+{
+ map__put(f->map);
+ dso__put(f->dso);
+ if (f->path[0])
+ unlink(f->path);
+}
+
+static bool lazy_symbol_ok(const struct symbol *sym, u32 i)
+{
+ char name[LAZY_NAME_LEN];
+
+ lazy_name(name, i);
+ return sym->start == LAZY_SYM_START + i * LAZY_SYM_SIZE && !strcmp(sym->name, name);
+}
+
+static int lazy_nr_symbols(struct dso *dso)
+{
+ struct rb_node *node;
+ int nr = 0;
+
+ for (node = rb_first_cached(dso__symbols(dso)); node; node = rb_next(node))
+ nr++;
+ return nr;
+}
+
+static int test__symbol_bytes_lazy_name_lookup(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ unsigned long saved_max = symbol_conf.max_symbol_bytes;
+ bool saved_lazy = symbol_conf.lazy_load_symbols;
+ size_t baseline = symbol__bytes_used();
+ struct lazy_fixture f;
+ struct symbol *sym;
+ int ret = TEST_FAIL;
+
+ symbol_conf.max_symbol_bytes = 0;
+ symbol_conf.lazy_load_symbols = true;
+ if (lazy_fixture__init(&f, 2))
+ goto out;
+
+ sym = map__find_symbol(f.map, LAZY_SYM_START + 1);
+ if (!sym || !lazy_symbol_ok(sym, 0))
+ goto out;
+
+ /* Name lookup must still work after the data descriptor is closed. */
+ dso__data_close(dso__ondemand(f.dso)->data_dso);
+ sym = map__find_symbol_by_name(f.map, "lazy_sym_001");
+ if (!sym || !lazy_symbol_ok(sym, 1) || dso__ondemand(f.dso))
+ goto out;
+ if (lazy_nr_symbols(f.dso) != 2)
+ goto out;
+ ret = TEST_OK;
+out:
+ lazy_fixture__exit(&f);
+ symbol_conf.max_symbol_bytes = saved_max;
+ symbol_conf.lazy_load_symbols = saved_lazy;
+ if (symbol__bytes_used() != baseline)
+ ret = TEST_FAIL;
+ return ret;
+}
+
+struct lazy_lookup_arg {
+ struct lazy_fixture *f;
+ bool by_name;
+ bool failed;
+};
+
+static void *lazy_lookup(void *data)
+{
+ struct lazy_lookup_arg *arg = data;
+ struct lazy_fixture *f = arg->f;
+ u32 i, round;
+
+ for (round = 0; round < 16; round++) {
+ for (i = 0; i < f->nr; i++) {
+ struct symbol *sym;
+
+ if (arg->by_name) {
+ char name[LAZY_NAME_LEN];
+
+ lazy_name(name, i);
+ sym = map__find_symbol_by_name(f->map, name);
+ } else {
+ sym = map__find_symbol(f->map, LAZY_SYM_START +
+ i * LAZY_SYM_SIZE + 1);
+ }
+ if (sym && !lazy_symbol_ok(sym, i))
+ arg->failed = true;
+ }
+ }
+ return NULL;
+}
+
+/*
+ * Address lookups race with name lookups while the symbol budget stops
+ * materialization part way. Once a name lookup has run, the DSO must no
+ * longer grow, even after budget becomes available again.
+ */
+static int test__symbol_bytes_lazy_budget_race(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ enum { NR_SYMS = 256, NR_ADDR_THREADS = 4, NR_THREADS = NR_ADDR_THREADS + 2 };
+ unsigned long saved_max = symbol_conf.max_symbol_bytes;
+ bool saved_lazy = symbol_conf.lazy_load_symbols;
+ size_t baseline = symbol__bytes_used();
+ size_t charge = symbol_conf.priv_size + sizeof(struct symbol) + LAZY_NAME_LEN;
+ struct lazy_lookup_arg args[NR_THREADS];
+ pthread_t threads[NR_THREADS];
+ struct lazy_fixture f;
+ int created = 0, nr_symbols, i;
+ int ret = TEST_FAIL;
+
+ symbol_conf.max_symbol_bytes = 0;
+ symbol_conf.lazy_load_symbols = true;
+ if (lazy_fixture__init(&f, NR_SYMS))
+ goto out;
+ symbol_conf.max_symbol_bytes = symbol__bytes_used() + NR_SYMS / 4 * charge;
+
+ for (i = 0; i < NR_THREADS; i++) {
+ args[i] = (struct lazy_lookup_arg) {
+ .f = &f,
+ .by_name = i >= NR_ADDR_THREADS,
+ };
+ if (pthread_create(&threads[i], NULL, lazy_lookup, &args[i]))
+ break;
+ created++;
+ }
+ for (i = 0; i < created; i++)
+ pthread_join(threads[i], NULL);
+ if (created != NR_THREADS)
+ goto out;
+ for (i = 0; i < NR_THREADS; i++) {
+ if (args[i].failed) {
+ pr_debug("lazy lookup returned a wrong symbol\n");
+ goto out;
+ }
+ }
+
+ nr_symbols = lazy_nr_symbols(f.dso);
+ if (dso__ondemand(f.dso) || nr_symbols == 0 || nr_symbols > NR_SYMS / 4) {
+ pr_debug("unexpected lazy state: index %p, %d symbols\n",
+ dso__ondemand(f.dso), nr_symbols);
+ goto out;
+ }
+
+ symbol_conf.max_symbol_bytes = 0;
+ for (i = 0; i < NR_SYMS; i++)
+ map__find_symbol(f.map, LAZY_SYM_START + i * LAZY_SYM_SIZE + 1);
+ if (lazy_nr_symbols(f.dso) != nr_symbols) {
+ pr_debug("DSO changed after its name array was built\n");
+ goto out;
+ }
+ ret = TEST_OK;
+out:
+ lazy_fixture__exit(&f);
+ symbol_conf.max_symbol_bytes = saved_max;
+ symbol_conf.lazy_load_symbols = saved_lazy;
+ if (symbol__bytes_used() != baseline)
+ ret = TEST_FAIL;
+ return ret;
+}
+struct sym_entry {
+ u64 start;
+ char *name;
+};
+
+static int cmp_sym_entry(const void *a, const void *b)
+{
+ const struct sym_entry *sa = a, *sb = b;
+
+ if (sa->start != sb->start)
+ return sa->start < sb->start ? -1 : 1;
+ return strcmp(sa->name, sb->name);
+}
+
+static void free_sym_entries(struct sym_entry *entries, size_t nr)
+{
+ size_t i;
+
+ for (i = 0; i < nr; i++)
+ free(entries[i].name);
+ free(entries);
+}
+
+/*
+ * Load @filename on a fresh host machine and return the start and name of
+ * every symbol, sorted. In lazy mode, building the name-sorted array
+ * materializes the whole index.
+ */
+static int load_sym_entries(const char *filename, bool lazy,
+ struct sym_entry **entries_p, size_t *nr_p)
+{
+ struct sym_entry *entries = NULL;
+ struct machine *machine = NULL;
+ struct thread *thread = NULL;
+ struct map *map = NULL;
+ struct perf_env env;
+ struct rb_node *nd;
+ struct dso *dso;
+ size_t nr = 0, alloc = 0;
+ int ret = TEST_FAIL;
+
+ perf_env__init(&env);
+ symbol_conf.lazy_load_symbols = lazy;
+ machine = machine__new_host(&env);
+ if (!machine)
+ goto out;
+ thread = machine__findnew_thread(machine, 100, 100);
+ if (!thread)
+ goto out;
+ map = map__new(machine, 0x100000, 0xffffffff, 0, &dso_id_empty,
+ PROT_EXEC, /*flags=*/0, (char *)filename, thread);
+ if (!map)
+ goto out;
+
+ dso = map__dso(map);
+ if (dso__load(dso, map) <= 0) {
+ pr_debug("%s: no symbols loaded\n", filename);
+ ret = TEST_SKIP;
+ goto out;
+ }
+ if (lazy && !dso__ondemand(dso)) {
+ pr_debug("%s: no lazy index was built\n", filename);
+ ret = TEST_SKIP;
+ goto out;
+ }
+ dso__sort_by_name(dso);
+
+ for (nd = rb_first_cached(dso__symbols(dso)); nd; nd = rb_next(nd)) {
+ struct symbol *sym = rb_entry(nd, struct symbol, rb_node);
+
+ if (nr == alloc) {
+ struct sym_entry *tmp;
+
+ alloc = alloc ? alloc * 2 : 1024;
+ tmp = realloc(entries, alloc * sizeof(*entries));
+ if (!tmp)
+ goto out;
+ entries = tmp;
+ }
+ entries[nr].start = sym->start;
+ entries[nr].name = strdup(sym->name);
+ if (!entries[nr].name)
+ goto out;
+ nr++;
+ }
+ qsort(entries, nr, sizeof(*entries), cmp_sym_entry);
+ *entries_p = entries;
+ *nr_p = nr;
+ entries = NULL;
+ ret = TEST_OK;
+out:
+ if (entries)
+ free_sym_entries(entries, nr);
+ map__put(map);
+ thread__put(thread);
+ machine__delete(machine);
+ perf_env__exit(&env);
+ return ret;
+}
+
+/*
+ * Compare the symbols of a DSO (perf itself, or --dso) loaded eagerly and
+ * lazily. Addresses and names must match.
+ */
+static int test__symbol_bytes_lazy_parity(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ unsigned long saved_max = symbol_conf.max_symbol_bytes;
+ bool saved_lazy = symbol_conf.lazy_load_symbols;
+ struct sym_entry *eager = NULL, *lazy = NULL;
+ size_t nr_eager = 0, nr_lazy = 0, i;
+ char filename[PATH_MAX];
+ int ret;
+
+ if (dso_to_test)
+ strlcpy(filename, dso_to_test, sizeof(filename));
+ else
+ perf_exe(filename, sizeof(filename));
+
+ symbol_conf.max_symbol_bytes = 0;
+ ret = load_sym_entries(filename, false, &eager, &nr_eager);
+ if (ret == TEST_OK)
+ ret = load_sym_entries(filename, true, &lazy, &nr_lazy);
+ if (ret != TEST_OK)
+ goto out;
+
+ pr_debug("%s: %zu eager and %zu lazy symbols\n", filename, nr_eager, nr_lazy);
+ for (i = 0; i < nr_eager && i < nr_lazy; i++) {
+ if (cmp_sym_entry(&eager[i], &lazy[i])) {
+ pr_debug("mismatch: eager %#" PRIx64 " %s, lazy %#" PRIx64 " %s\n",
+ eager[i].start, eager[i].name, lazy[i].start, lazy[i].name);
+ ret = TEST_FAIL;
+ goto out;
+ }
+ }
+ if (nr_eager != nr_lazy)
+ ret = TEST_FAIL;
+out:
+ if (eager)
+ free_sym_entries(eager, nr_eager);
+ if (lazy)
+ free_sym_entries(lazy, nr_lazy);
+ symbol_conf.max_symbol_bytes = saved_max;
+ symbol_conf.lazy_load_symbols = saved_lazy;
+ return ret;
+}
+#endif
+
static struct test_case tests__symbol_bytes[] = {
TEST_CASE("Concurrent strict reservations", symbol_bytes_reservation),
+ TEST_CASE("Shared duplicate selection", symbol_bytes_duplicate_selection),
+#ifdef HAVE_LIBELF_SUPPORT
+ TEST_CASE("Truncated lazy symbol names", symbol_bytes_truncated_name),
+ TEST_CASE("Lazy address and name lookup", symbol_bytes_lazy_name_lookup),
+ TEST_CASE("Lazy lookups racing a truncated name lookup", symbol_bytes_lazy_budget_race),
+ TEST_CASE("Lazy and eager symbol parity", symbol_bytes_lazy_parity),
+#endif
{ .name = NULL, }
};
--
Git-157)
next prev parent reply other threads:[~2026-09-25 19:15 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-25 19:09 [PATCH v3 0/6] perf script: Bounded and lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-25 19:09 ` [PATCH v3 1/6] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard Alireza Haghdoost via B4 Relay
2026-09-25 19:09 ` [PATCH v3 2/6] perf dso: Allow reading DSO data from an explicit file Alireza Haghdoost via B4 Relay
2026-09-25 19:09 ` [PATCH v3 3/6] perf symbols: Factor out duplicate symbol selection Alireza Haghdoost via B4 Relay
2026-09-25 19:40 ` Ian Rogers
2026-09-25 19:55 ` Alireza Haghdoost
2026-09-25 20:21 ` Ian Rogers
2026-09-25 19:09 ` [PATCH v3 4/6] perf script: Add --max-symbol-bytes to bound ELF symbol memory Alireza Haghdoost via B4 Relay
2026-09-25 19:09 ` [PATCH v3 5/6] perf script: Add --lazy-load-symbols for lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-25 19:09 ` Alireza Haghdoost via B4 Relay [this message]
2026-09-25 20:20 ` [PATCH v3 0/6] perf script: Bounded and " Ian Rogers
2026-09-25 21:28 ` Alireza Haghdoost
2026-09-25 21:55 ` Ian Rogers
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=20260925-perf-symbol-memory-send-v3-6-3e4e234c363b@uber.com \
--to=devnull+haghdoost.uber.com@kernel.org \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alexander.shishkin@linux.intel.com \
--cc=andriin@fb.com \
--cc=ast@kernel.org \
--cc=haghdoost@uber.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=mark.rutland@arm.com \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=peterz@infradead.org \
/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
all inboxes | Powered by JetHome®