* [PATCH v2 0/4] perf script: Bounded and lazy symbol loading
@ 2026-09-20 2:33 Alireza Haghdoost via B4 Relay
2026-09-20 2:33 ` [PATCH v2 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard Alireza Haghdoost via B4 Relay
` (3 more replies)
0 siblings, 4 replies; 10+ messages in thread
From: Alireza Haghdoost via B4 Relay @ 2026-09-20 2:33 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
Ian Rogers, Adrian Hunter, James Clark, Alexei Starovoitov,
Andrii Nakryiko
Cc: linux-perf-users, linux-kernel, Alireza Haghdoost
perf script loads the entire ELF symbol table of every DSO that appears
in a sample, allocating each symbol into an rb-tree held until process
exit. Therefore, a large enough profile turns symbol loading into an
OOM kill.
This is not scalable for profiling a large cgroup with a lot of large
binaries on a production system with limited free memory.
This series adds two independent, opt-in mechanisms and a leading
regression fix they build on:
[1/4] Fix a broken "#ifdef ELF_C_READ_MMAP" guard so perf actually
mmaps ELF files instead of malloc'ing section data. This is a
standalone regression fix that is introduced by 22dd1ac91a77.
[2/4] --max-symbol-bytes <size>: a byte budget on struct symbol
allocations (and the lazy index) enforced at the ELF symbol
loader, degrading to [unknown] with a warning past the cap.
An unbounded profile doesn't just risk OOM-killing itself. It
also forces memory pressure on the whole host, pushing the kernel
to reclaim from co-located latency-sensitive processes. Capping
it lets the user bound that footprint up front and choose the
trade-off explicitly.
[3/4] --lazy-load-symbols: build a compact per-DSO sorted index and
resolve only the sampled addresses, reading names through the DSO
data cache at lookup time. On the production fixture, peak RssAnon
drops from 265 MiB to 39 MiB (6.8x) and wall time from 3.1 s to
1.85 s (1.7x). Memory optimizations usually cost time; this one
does not because lazy loading skips a lot of calloc and demangle
calls.
[4/4] Shell and unit tests for both options.
Lazy loading handles the common userspace ELF symtab/dynsym path. Eager
loading remains available for dense coverage and for PPC64 .opd and
.gnu_debugdata.
Changes in v2:
- Replace direct pread() name reads with the exact symbol source's DSO data
cache, preserving split-debuginfo offsets and descriptor reopen behavior.
- Drop the byte-identical-output claim and retain eager loading for PPC64
.opd and .gnu_debugdata.
- Make the symbol budget atomic and strict, account complete name lengths,
accept a bare 0 as unlimited, and keep partial zero-sized ranges from
covering omitted symbols.
- Align lazy lookup with eager duplicate and IFUNC selection, PLT clipping,
and name-sorted materialization.
- Move option documentation into the feature patches. Add unit and shell
coverage for cache reopen, truncated names, budget truncation, and skip
handling.
Link: https://lore.kernel.org/all/20260915-perf-symbol-memory-send-v1-0-1d3360e21f07@uber.com/
---
Alireza Haghdoost (4):
perf symbols: Fix broken ELF_C_READ_MMAP fallback guard
perf script: Add --max-symbol-bytes to bound ELF symbol memory
perf script: Add --lazy-load-symbols for lazy symbol loading
perf test: Test lazy symbol loading and symbol memory limits
tools/perf/Documentation/perf-script.txt | 28 +
tools/perf/arch/powerpc/util/sym-handling.c | 5 +-
tools/perf/builtin-script.c | 44 ++
tools/perf/tests/Build | 1 +
tools/perf/tests/builtin-test.c | 1 +
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 | 340 ++++++++++
tools/perf/tests/tests.h | 1 +
tools/perf/util/dso.c | 63 +-
tools/perf/util/dso.h | 49 ++
tools/perf/util/map.c | 30 +-
tools/perf/util/symbol-elf.c | 733 ++++++++++++++++++++-
tools/perf/util/symbol-minimal.c | 17 +
tools/perf/util/symbol.c | 140 +++-
tools/perf/util/symbol.h | 24 +-
tools/perf/util/symbol_conf.h | 2 +
17 files changed, 1723 insertions(+), 59 deletions(-)
---
base-commit: aa18964dd64511305de0711fed912054da6f5d18
change-id: 20260915-perf-symbol-memory-send-e7cfca1ac3d9
Best regards,
--
Alireza Haghdoost <haghdoost@uber.com>
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v2 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard
2026-09-20 2:33 [PATCH v2 0/4] perf script: Bounded and lazy symbol loading Alireza Haghdoost via B4 Relay
@ 2026-09-20 2:33 ` Alireza Haghdoost via B4 Relay
2026-09-20 23:52 ` Namhyung Kim
2026-09-20 2:33 ` [PATCH v2 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory Alireza Haghdoost via B4 Relay
` (2 subsequent siblings)
3 siblings, 1 reply; 10+ messages in thread
From: Alireza Haghdoost via B4 Relay @ 2026-09-20 2:33 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
Ian Rogers, Adrian Hunter, James Clark, Alexei Starovoitov,
Andrii Nakryiko
Cc: linux-perf-users, linux-kernel, Alireza Haghdoost
From: Alireza Haghdoost <haghdoost@uber.com>
Commit 22dd1ac91a77 ("tools: Remove feature-libelf-mmap feature
detection") replaced perf's compile-time feature test with an #ifdef on
ELF_C_READ_MMAP. ELF_C_READ_MMAP is an Elf_Cmd enumerator rather than a
preprocessor macro, so the condition is always false and perf silently
uses ELF_C_READ.
Perf already requires a sufficiently recent elfutils version that
provides ELF_C_READ_MMAP. Use the enumerator directly instead of
restoring a feature probe or retaining an unreachable fallback.
Fixes: 22dd1ac91a77 ("tools: Remove feature-libelf-mmap feature detection")
Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
---
tools/perf/util/symbol.h | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
index d0bac824c79c..46b1649c64fc 100644
--- a/tools/perf/util/symbol.h
+++ b/tools/perf/util/symbol.h
@@ -57,15 +57,7 @@ static inline bool is_livepatch_symbol(const char *str)
return strstarts(str, KLP_SYM_PREFIX);
}
-/*
- * libelf 0.8.x and earlier do not support ELF_C_READ_MMAP;
- * for newer versions we can use mmap to reduce memory usage:
- */
-#ifdef ELF_C_READ_MMAP
-# define PERF_ELF_C_READ_MMAP ELF_C_READ_MMAP
-#else
-# define PERF_ELF_C_READ_MMAP ELF_C_READ
-#endif
+#define PERF_ELF_C_READ_MMAP ELF_C_READ_MMAP
#ifdef HAVE_LIBELF_SUPPORT
Elf_Scn *elf_section_by_name(Elf *elf, GElf_Ehdr *ep,
--
Git-157)
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v2 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory
2026-09-20 2:33 [PATCH v2 0/4] perf script: Bounded and lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-20 2:33 ` [PATCH v2 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard Alireza Haghdoost via B4 Relay
@ 2026-09-20 2:33 ` Alireza Haghdoost via B4 Relay
2026-09-21 0:03 ` Namhyung Kim
2026-09-20 2:33 ` [PATCH v2 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-20 2:33 ` [PATCH v2 4/4] perf test: Test lazy symbol loading and symbol memory limits Alireza Haghdoost via B4 Relay
3 siblings, 1 reply; 10+ messages in thread
From: Alireza Haghdoost via B4 Relay @ 2026-09-20 2:33 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
Ian Rogers, Adrian Hunter, James Clark, Alexei Starovoitov,
Andrii Nakryiko
Cc: linux-perf-users, linux-kernel, Alireza Haghdoost
From: Alireza Haghdoost <haghdoost@uber.com>
perf script eagerly materializes every ELF symbol into an rb-tree kept
until process exit. Large profiles can therefore consume substantial
anonymous memory, causing perf script to be OOM-killed or forcing the
kernel to reclaim memory from co-located workloads.
This patch adds --max-symbol-bytes to bound struct symbol allocations.
Once the budget is reached, the ELF loader stops loading symbols, warns
once, and lets unresolved addresses appear as [unknown]. This allows
users to bound the memory footprint upfront and explicitly choose between
complete symbolization and avoiding unbounded host memory pressure. perf
record already provides a similar --max-size option to bound disk usage.
The counter includes every symbol__new() allocation, but this patch
enforces the limit only in the ELF loader, which is the source of the
unbounded memory growth addressed here. In this path, reaching the limit
can safely produce [unknown] symbols. Other loaders currently treat a
failed symbol allocation as an error. Capping those paths would therefore
require separate changes whose complexity may outweigh the potential
memory savings.
The cap applies to userspace DSOs, vmlinux-as-ELF, and kernel modules.
Sizes require a B/K/M/G suffix, except that a bare 0 and the default mean
unlimited. Reservations are atomic so concurrent loaders cannot exceed the
limit; accounting retains complete name lengths and partial zero-sized
symbol ranges do not cover omitted addresses.
Document the option with the code that introduces it and add focused
accounting and concurrent-reservation tests.
Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
---
tools/perf/Documentation/perf-script.txt | 10 +++
tools/perf/builtin-script.c | 42 +++++++++
tools/perf/tests/Build | 1 +
tools/perf/tests/builtin-test.c | 1 +
tools/perf/tests/symbol-bytes.c | 143 +++++++++++++++++++++++++++++++
tools/perf/tests/tests.h | 1 +
tools/perf/util/symbol-elf.c | 66 +++++++++++---
tools/perf/util/symbol.c | 82 +++++++++++++++++-
tools/perf/util/symbol.h | 8 +-
tools/perf/util/symbol_conf.h | 1 +
10 files changed, 340 insertions(+), 15 deletions(-)
diff --git a/tools/perf/Documentation/perf-script.txt b/tools/perf/Documentation/perf-script.txt
index 200ea25891d8..217167a2e56b 100644
--- a/tools/perf/Documentation/perf-script.txt
+++ b/tools/perf/Documentation/perf-script.txt
@@ -412,6 +412,16 @@ include::itrace.txt[]
Default: 127
+--max-symbol-bytes::
+ Limit the bytes held in struct symbol allocations for DSOs on the
+ libelf symbol-loader path: userspace DSOs, vmlinux-as-ELF, and kernel
+ modules. This is not a cap on all symbol memory or RSS: symbols from
+ kallsyms, JIT maps, 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,
+ the ELF loader stops adding symbols; addresses not covered by symbols
+ already loaded are then printed as [unknown]. A warning is printed.
+ Default: 0 (unlimited).
+
--ns::
Use 9 decimal places when displaying time (i.e. show the nanoseconds)
diff --git a/tools/perf/builtin-script.c b/tools/perf/builtin-script.c
index ad8ca08ceb5f..017b39ed6a21 100644
--- a/tools/perf/builtin-script.c
+++ b/tools/perf/builtin-script.c
@@ -68,6 +68,7 @@
#include "util/thread.h"
#include "util/thread_map.h"
#include "util/time-utils.h"
+#include "util/units.h"
#include "util/tool.h"
#include "util/trace-event.h"
#include "util/unwind.h"
@@ -4035,6 +4036,44 @@ static int parse_callret_trace(const struct option *opt __maybe_unused,
return 0;
}
+static int parse_max_symbol_bytes(const struct option *opt,
+ const char *str, int unset)
+{
+ unsigned long *max_bytes = (unsigned long *)opt->value;
+ static struct parse_tag size_tags[] = {
+ { .tag = 'B', .mult = 1 },
+ { .tag = 'K', .mult = 1 << 10 },
+ { .tag = 'M', .mult = 1 << 20 },
+ { .tag = 'G', .mult = 1 << 30 },
+ { .tag = 0 },
+ };
+ unsigned long bytes;
+ size_t len;
+
+ if (unset) {
+ *max_bytes = 0;
+ return 0;
+ }
+
+ if (!strcmp(str, "0")) {
+ *max_bytes = 0;
+ return 0;
+ }
+
+ len = strlen(str);
+ if (len < 2 || !strchr("BKMG", str[len - 1]) ||
+ strspn(str, "0123456789") != len - 1)
+ return -1;
+
+ bytes = parse_tag_value(str, size_tags);
+ if (bytes != (unsigned long)-1) {
+ *max_bytes = bytes;
+ return 0;
+ }
+
+ return -1;
+}
+
int cmd_script(int argc, const char **argv)
{
bool show_full_info = false;
@@ -4135,6 +4174,9 @@ int cmd_script(int argc, const char **argv)
"Set the maximum stack depth when parsing the callchain, "
"anything beyond the specified depth will be ignored. "
"Default: kernel.perf_event_max_stack or " __stringify(PERF_MAX_STACK_DEPTH)),
+ OPT_CALLBACK(0, "max-symbol-bytes", &symbol_conf.max_symbol_bytes,
+ "size", "Limit bytes for ELF struct symbol (e.g. 128M; 0=unlimited)",
+ parse_max_symbol_bytes),
OPT_BOOLEAN(0, "reltime", &reltime, "Show time stamps relative to start"),
OPT_BOOLEAN(0, "deltatime", &deltatime, "Show time stamps relative to previous event"),
OPT_BOOLEAN('I', "show-info", &show_full_info,
diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build
index 66944a4f4968..09b0f4a55c30 100644
--- a/tools/perf/tests/Build
+++ b/tools/perf/tests/Build
@@ -66,6 +66,7 @@ perf-test-y += dlfilter-test.o
perf-test-y += sigtrap.o
perf-test-y += event_groups.o
perf-test-y += symbols.o
+perf-test-y += symbol-bytes.o
perf-test-y += util.o
perf-test-y += hwmon_pmu.o
perf-test-y += tool_pmu.o
diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c
index 4d0784b16723..0bed0f4f076b 100644
--- a/tools/perf/tests/builtin-test.c
+++ b/tools/perf/tests/builtin-test.c
@@ -150,6 +150,7 @@ static struct test_suite *generic_tests[] = {
&suite__sigtrap,
&suite__event_groups,
&suite__symbols,
+ &suite__symbol_bytes,
&suite__util,
&suite__subcmd_help,
&suite__kallsyms_split,
diff --git a/tools/perf/tests/symbol-bytes.c b/tools/perf/tests/symbol-bytes.c
new file mode 100644
index 000000000000..4a4740daa5f5
--- /dev/null
+++ b/tools/perf/tests/symbol-bytes.c
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <limits.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "debug.h"
+#include "symbol.h"
+#include "symbol_conf.h"
+#include "tests.h"
+
+static int test__symbol_bytes_long_name(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ const size_t name_len = 65536;
+ unsigned long saved_max = symbol_conf.max_symbol_bytes;
+ size_t baseline = symbol__bytes_used();
+ struct symbol *sym = NULL;
+ size_t expected = symbol_conf.priv_size + sizeof(*sym) + name_len + 1;
+ char *name;
+ int ret = TEST_FAIL;
+
+ symbol_conf.max_symbol_bytes = 0;
+ name = malloc(name_len + 1);
+ if (!name)
+ goto out;
+ memset(name, 'a', name_len);
+ name[name_len] = '\0';
+
+ sym = symbol__new(0, 1, 0, 0, name);
+ if (!sym)
+ goto out_free_name;
+ if (symbol__bytes_used() != baseline + expected) {
+ pr_debug("long symbol name accounting mismatch\n");
+ goto out_delete;
+ }
+
+ /* Kallsyms splitting can shorten the stored name in place. */
+ sym->name[10] = '\0';
+ symbol__delete(sym);
+ sym = NULL;
+ if (symbol__bytes_used() != baseline) {
+ pr_debug("long symbol name was not fully unaccounted\n");
+ goto out_free_name;
+ }
+ ret = TEST_OK;
+
+out_delete:
+ if (sym)
+ symbol__delete(sym);
+out_free_name:
+ free(name);
+out:
+ symbol_conf.max_symbol_bytes = saved_max;
+ return ret;
+}
+
+struct reserve_arg {
+ size_t bytes;
+ bool success;
+};
+
+static void *reserve_bytes(void *data)
+{
+ struct reserve_arg *arg = data;
+
+ arg->success = symbol__try_account_bytes(arg->bytes);
+ return NULL;
+}
+
+static int test__symbol_bytes_reservation(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ enum { NR_THREADS = 8, NR_ALLOWED = 4 };
+ const size_t reservation = 1024;
+ unsigned long saved_max = symbol_conf.max_symbol_bytes;
+ size_t baseline = symbol__bytes_used();
+ struct reserve_arg args[NR_THREADS];
+ pthread_t threads[NR_THREADS];
+ int created = 0, successful = 0;
+ int ret = TEST_FAIL;
+ int i;
+
+ if (baseline > ULONG_MAX - NR_ALLOWED * reservation)
+ return TEST_SKIP;
+
+ symbol_conf.max_symbol_bytes = baseline + NR_ALLOWED * reservation;
+ if (symbol__try_account_bytes(SIZE_MAX)) {
+ pr_debug("overflowing symbol reservation succeeded\n");
+ symbol__unaccount_bytes(SIZE_MAX);
+ goto out;
+ }
+
+ for (i = 0; i < NR_THREADS; i++) {
+ args[i].bytes = reservation;
+ args[i].success = false;
+ if (pthread_create(&threads[i], NULL, reserve_bytes, &args[i]))
+ goto out_join;
+ created++;
+ }
+
+out_join:
+ for (i = 0; i < created; i++)
+ pthread_join(threads[i], NULL);
+ for (i = 0; i < created; i++) {
+ if (args[i].success)
+ successful++;
+ }
+
+ if (created != NR_THREADS || successful != NR_ALLOWED) {
+ pr_debug("symbol reservation count: created %d, successful %d\n",
+ created, successful);
+ goto out_release;
+ }
+ if (symbol__bytes_used() != baseline + NR_ALLOWED * reservation) {
+ pr_debug("symbol reservation exceeded configured budget\n");
+ goto out_release;
+ }
+ ret = TEST_OK;
+
+out_release:
+ for (i = 0; i < created; i++) {
+ if (args[i].success)
+ symbol__unaccount_bytes(args[i].bytes);
+ }
+out:
+ symbol_conf.max_symbol_bytes = saved_max;
+ if (symbol__bytes_used() != baseline)
+ ret = TEST_FAIL;
+ return ret;
+}
+
+static struct test_case tests__symbol_bytes[] = {
+ TEST_CASE("Long name accounting", symbol_bytes_long_name),
+ TEST_CASE("Concurrent strict reservations", symbol_bytes_reservation),
+ { .name = NULL, }
+};
+
+struct test_suite suite__symbol_bytes = {
+ .desc = "Symbol memory accounting",
+ .test_cases = tests__symbol_bytes,
+};
diff --git a/tools/perf/tests/tests.h b/tools/perf/tests/tests.h
index cee9e6b62dcc..6dfad7ac722b 100644
--- a/tools/perf/tests/tests.h
+++ b/tools/perf/tests/tests.h
@@ -178,6 +178,7 @@ DECLARE_SUITE(dlfilter);
DECLARE_SUITE(sigtrap);
DECLARE_SUITE(event_groups);
DECLARE_SUITE(symbols);
+DECLARE_SUITE(symbol_bytes);
DECLARE_SUITE(util);
DECLARE_SUITE(uncore_event_sorting);
DECLARE_SUITE(subcmd_help);
diff --git a/tools/perf/util/symbol-elf.c b/tools/perf/util/symbol-elf.c
index e955c3feddcd..2f7ea1499cbf 100644
--- a/tools/perf/util/symbol-elf.c
+++ b/tools/perf/util/symbol-elf.c
@@ -561,6 +561,13 @@ static bool get_plt_got_name(GElf_Shdr *shdr, size_t i,
return result;
}
+static void symbol_budget_warning(void)
+{
+ pr_warning_once("perf: symbol memory budget exceeded (%lu bytes), "
+ "remaining symbols will be [unknown]\n",
+ symbol_conf.max_symbol_bytes);
+}
+
static int dso__synthesize_plt_got_symbols(struct dso *dso, Elf *elf,
GElf_Ehdr *ehdr,
char *buf, size_t buf_sz)
@@ -580,11 +587,19 @@ static int dso__synthesize_plt_got_symbols(struct dso *dso, Elf *elf,
get_rela_dyn_info(elf, ehdr, &di, scn);
for (i = 0; i < shdr.sh_size; i += shdr.sh_entsize) {
+ bool budget_exceeded;
+
if (!get_plt_got_name(&shdr, i, &di, buf, buf_sz))
snprintf(buf, buf_sz, "offset_%#" PRIx64 "@plt", (u64)shdr.sh_offset + i);
- sym = symbol__new(shdr.sh_offset + i, shdr.sh_entsize, STB_GLOBAL, STT_FUNC, buf);
- if (!sym)
+ sym = symbol__new_bounded(shdr.sh_offset + i, shdr.sh_entsize,
+ STB_GLOBAL, STT_FUNC, buf, &budget_exceeded);
+ if (!sym) {
+ if (budget_exceeded) {
+ symbol_budget_warning();
+ err = 0;
+ }
goto out;
+ }
symbols__insert(dso__symbols(dso), sym);
}
err = 0;
@@ -615,6 +630,7 @@ int dso__synthesize_plt_symbols(struct dso *dso, struct symsrc *ss)
Elf *elf;
int nr = 0, err = -1;
struct rel_info ri = { .is_rela = false };
+ bool budget_exceeded;
bool lazy_plt;
elf = ss->elf;
@@ -636,9 +652,16 @@ int dso__synthesize_plt_symbols(struct dso *dso, struct symsrc *ss)
return 0;
/* Add a symbol for .plt header */
- plt_sym = symbol__new(shdr_plt.sh_offset, plt_header_size, STB_GLOBAL, STT_FUNC, ".plt");
- if (!plt_sym)
+ plt_sym = symbol__new_bounded(shdr_plt.sh_offset, plt_header_size,
+ STB_GLOBAL, STT_FUNC, ".plt",
+ &budget_exceeded);
+ if (!plt_sym) {
+ if (budget_exceeded) {
+ symbol_budget_warning();
+ return 0;
+ }
goto out_elf_end;
+ }
symbols__insert(dso__symbols(dso), plt_sym);
/* Only x86 has .plt.got */
@@ -756,9 +779,15 @@ int dso__synthesize_plt_symbols(struct dso *dso, struct symsrc *ss)
"offset_%#" PRIx64 "@plt", plt_offset);
free(demangled);
- f = symbol__new(plt_offset, plt_entry_size, STB_GLOBAL, STT_FUNC, sympltname);
- if (!f)
+ f = symbol__new_bounded(plt_offset, plt_entry_size, STB_GLOBAL,
+ STT_FUNC, sympltname, &budget_exceeded);
+ if (!f) {
+ if (budget_exceeded) {
+ symbol_budget_warning();
+ err = 0;
+ }
goto out_elf_end;
+ }
plt_offset += plt_entry_size;
symbols__insert(dso__symbols(dso), f);
@@ -1534,6 +1563,7 @@ dso__load_sym_internal(struct dso *dso, struct map *map, struct symsrc *syms_ss,
Elf *elf;
int nr = 0;
bool remap_kernel = false, adjust_kernel_syms = false;
+ bool budget_truncated = false;
u64 max_text_sh_offset = 0;
if (kmap && !kmaps)
@@ -1633,8 +1663,16 @@ dso__load_sym_internal(struct dso *dso, struct map *map, struct symsrc *syms_ss,
char *demangled = NULL;
int is_label = elf_sym__is_label(&sym);
const char *section_name;
+ bool budget_exceeded = false;
bool used_opd = false;
+ if (symbol_conf.max_symbol_bytes &&
+ symbol__bytes_used() >= symbol_conf.max_symbol_bytes) {
+ symbol_budget_warning();
+ budget_truncated = true;
+ break;
+ }
+
if (!is_label && !elf_sym__filter(&sym))
continue;
@@ -1775,10 +1813,17 @@ dso__load_sym_internal(struct dso *dso, struct map *map, struct symsrc *syms_ss,
if (demangled != NULL)
elf_name = demangled;
- f = symbol__new(sym.st_value, sym.st_size,
- GELF_ST_BIND(sym.st_info),
- GELF_ST_TYPE(sym.st_info), elf_name);
+ f = symbol__new_bounded(sym.st_value, sym.st_size,
+ GELF_ST_BIND(sym.st_info),
+ GELF_ST_TYPE(sym.st_info), elf_name,
+ &budget_exceeded);
+ if (!f && budget_exceeded) {
+ symbol_budget_warning();
+ budget_truncated = true;
+ }
free(demangled);
+ if (!f && budget_truncated)
+ break;
if (!f)
goto out_elf_end;
@@ -1793,7 +1838,8 @@ dso__load_sym_internal(struct dso *dso, struct map *map, struct symsrc *syms_ss,
* For misannotated, zeroed, ASM function sizes.
*/
if (nr > 0) {
- symbols__fixup_end(dso__symbols(dso), false);
+ if (!budget_truncated)
+ symbols__fixup_end(dso__symbols(dso), false);
symbols__fixup_duplicate(dso__symbols(dso));
if (kmap) {
/*
diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
index 3587ad243159..32eef666f748 100644
--- a/tools/perf/util/symbol.c
+++ b/tools/perf/util/symbol.c
@@ -310,13 +310,72 @@ void symbols__fixup_end(struct rb_root_cached *symbols, bool is_kallsyms)
curr->end = roundup(curr->start, 4096) + 4096;
}
-struct symbol *symbol__new(u64 start, u64 len, u8 binding, u8 type, const char *name)
+static _Atomic size_t symbol_bytes_used;
+
+size_t symbol__bytes_used(void)
+{
+ return atomic_load_explicit(&symbol_bytes_used, memory_order_relaxed);
+}
+
+void symbol__account_bytes(size_t bytes)
+{
+ atomic_fetch_add_explicit(&symbol_bytes_used, bytes, memory_order_relaxed);
+}
+
+bool symbol__try_account_bytes(size_t bytes)
+{
+ size_t old = symbol__bytes_used();
+
+ for (;;) {
+ if (old > SIZE_MAX - bytes)
+ return false;
+ if (symbol_conf.max_symbol_bytes &&
+ (old > symbol_conf.max_symbol_bytes ||
+ bytes > symbol_conf.max_symbol_bytes - old))
+ return false;
+ if (atomic_compare_exchange_weak_explicit(&symbol_bytes_used, &old, old + bytes,
+ memory_order_relaxed,
+ memory_order_relaxed))
+ return true;
+ }
+}
+
+void symbol__unaccount_bytes(size_t bytes)
+{
+ atomic_fetch_sub_explicit(&symbol_bytes_used, bytes, memory_order_relaxed);
+}
+
+static struct symbol *__symbol__new(u64 start, u64 len, u8 binding, u8 type,
+ const char *name, bool bounded,
+ bool *budget_exceeded)
{
size_t namelen = strlen(name) + 1;
- struct symbol *sym = calloc(1, (symbol_conf.priv_size +
- sizeof(*sym) + namelen));
- if (sym == NULL)
+ size_t alloc_size;
+ struct symbol *sym;
+
+ if (budget_exceeded)
+ *budget_exceeded = false;
+ if (namelen - 1 > UINT32_MAX ||
+ namelen > SIZE_MAX - sizeof(*sym) ||
+ symbol_conf.priv_size > SIZE_MAX - sizeof(*sym) - namelen)
return NULL;
+ alloc_size = symbol_conf.priv_size + sizeof(*sym) + namelen;
+
+ if (bounded && !symbol__try_account_bytes(alloc_size)) {
+ if (budget_exceeded)
+ *budget_exceeded = true;
+ return NULL;
+ }
+
+ sym = calloc(1, alloc_size);
+ if (sym == NULL) {
+ if (bounded)
+ symbol__unaccount_bytes(alloc_size);
+ return NULL;
+ }
+
+ if (!bounded)
+ symbol__account_bytes(alloc_size);
if (symbol_conf.priv_size) {
if (symbol_conf.init_annotation) {
@@ -339,8 +398,22 @@ struct symbol *symbol__new(u64 start, u64 len, u8 binding, u8 type, const char *
return sym;
}
+struct symbol *symbol__new(u64 start, u64 len, u8 binding, u8 type, const char *name)
+{
+ return __symbol__new(start, len, binding, type, name, false, NULL);
+}
+
+struct symbol *symbol__new_bounded(u64 start, u64 len, u8 binding, u8 type,
+ const char *name, bool *budget_exceeded)
+{
+ return __symbol__new(start, len, binding, type, name, true, budget_exceeded);
+}
+
void symbol__delete(struct symbol *sym)
{
+ size_t alloc_size = symbol_conf.priv_size + sizeof(*sym) +
+ sym->namelen + 1;
+
if (symbol_conf.priv_size) {
if (symbol_conf.init_annotation) {
struct annotation *notes = symbol__annotation(sym);
@@ -348,6 +421,7 @@ void symbol__delete(struct symbol *sym)
annotation__exit(notes);
}
}
+ symbol__unaccount_bytes(alloc_size);
free(((void *)sym) - symbol_conf.priv_size);
}
diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
index 46b1649c64fc..f7331edf0b71 100644
--- a/tools/perf/util/symbol.h
+++ b/tools/perf/util/symbol.h
@@ -90,7 +90,7 @@ struct symbol {
u64 start;
u64 end;
/** Length of the string name. */
- u16 namelen;
+ u32 namelen;
_Atomic uint16_t flags;
/** Architecture specific. Unused except on PPC where it holds st_other. */
u8 arch_sym;
@@ -227,6 +227,12 @@ void symbol__elf_init(void);
int symbol__annotation_init(void);
struct symbol *symbol__new(u64 start, u64 len, u8 binding, u8 type, const char *name);
+struct symbol *symbol__new_bounded(u64 start, u64 len, u8 binding, u8 type,
+ const char *name, bool *budget_exceeded);
+size_t symbol__bytes_used(void);
+void symbol__account_bytes(size_t bytes);
+bool symbol__try_account_bytes(size_t bytes);
+void symbol__unaccount_bytes(size_t bytes);
size_t __symbol__fprintf_symname_offs(const struct symbol *sym,
const struct addr_location *al,
bool unknown_as_addr,
diff --git a/tools/perf/util/symbol_conf.h b/tools/perf/util/symbol_conf.h
index 71f60081a85b..6a16c5badd5e 100644
--- a/tools/perf/util/symbol_conf.h
+++ b/tools/perf/util/symbol_conf.h
@@ -120,6 +120,7 @@ struct symbol_conf {
int pad_output_len_dso;
int group_sort_idx;
int addr_range;
+ unsigned long max_symbol_bytes;
DECLARE_BITMAP(parallelism_filter, MAX_NR_CPUS + 1);
};
--
Git-157)
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v2 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading
2026-09-20 2:33 [PATCH v2 0/4] perf script: Bounded and lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-20 2:33 ` [PATCH v2 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard Alireza Haghdoost via B4 Relay
2026-09-20 2:33 ` [PATCH v2 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory Alireza Haghdoost via B4 Relay
@ 2026-09-20 2:33 ` Alireza Haghdoost via B4 Relay
2026-09-21 0:19 ` Namhyung Kim
2026-09-20 2:33 ` [PATCH v2 4/4] perf test: Test lazy symbol loading and symbol memory limits Alireza Haghdoost via B4 Relay
3 siblings, 1 reply; 10+ messages in thread
From: Alireza Haghdoost via B4 Relay @ 2026-09-20 2:33 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
Ian Rogers, Adrian Hunter, James Clark, Alexei Starovoitov,
Andrii Nakryiko
Cc: linux-perf-users, linux-kernel, Alireza Haghdoost
From: Alireza Haghdoost <haghdoost@uber.com>
perf script eagerly materializes eligible symbols from every DSO
encountered in samples. On a production fixture, it loaded about 765k
symbols to resolve about 45k distinct (DSO, symbol) frames, exceeding the
memory available in a memory-constrained cgroup.
This patch adds --lazy-load-symbols for userspace ELF DSOs. It builds a
compact sorted index, resolves sampled addresses by binary search, reads
symbol names through a private data-source DSO, and caches resolved symbols
in the existing rb-tree. The private DSO uses the normal DSO data cache, so
the exact split-debuginfo source can be reopened after descriptor eviction.
If the source cannot be read during preflight, perf discards the index and
eagerly loads that DSO instead.
On the same fixture, peak RssAnon drops from 265 MiB to 39 MiB and wall
time from 3.1 seconds to 1.85 seconds. Memory optimizations usually cost
time; this one does not because lazy loading skips many unnecessary
calloc() calls and demangling operations.
Lazy loading is most effective when samples reference only a small
fraction of the available symbols, such as profiles spanning many large
DSOs. It still builds an index proportional to the total symbol count.
Eager loading remains available for dense symbol coverage or cases
requiring its broader ELF and architecture support.
This does not claim full parity with the eager loader. Lazy loading
supports the common userspace ELF symtab/dynsym case; .gnu_debugdata and
PPC64 .opd continue through the eager loader.
Materialization is serialized with the DSO lock. Name lookups materialize
the remaining index before constructing the name-sorted array. Lazy loading
shares eager duplicate and IFUNC selection, and clips ranges that cross
.plt before synthesizing PLT symbols.
Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
---
tools/perf/Documentation/perf-script.txt | 34 +-
tools/perf/arch/powerpc/util/sym-handling.c | 5 +-
tools/perf/builtin-script.c | 4 +-
tools/perf/tests/symbol-bytes.c | 139 ++++++
tools/perf/util/dso.c | 63 ++-
tools/perf/util/dso.h | 49 ++
tools/perf/util/map.c | 30 +-
tools/perf/util/symbol-elf.c | 667 ++++++++++++++++++++++++++++
tools/perf/util/symbol-minimal.c | 17 +
tools/perf/util/symbol.c | 58 ++-
tools/perf/util/symbol.h | 6 +-
tools/perf/util/symbol_conf.h | 1 +
12 files changed, 1029 insertions(+), 44 deletions(-)
diff --git a/tools/perf/Documentation/perf-script.txt b/tools/perf/Documentation/perf-script.txt
index 217167a2e56b..136d773778da 100644
--- a/tools/perf/Documentation/perf-script.txt
+++ b/tools/perf/Documentation/perf-script.txt
@@ -412,15 +412,33 @@ 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 through the DSO data cache 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.
+ Operations that look up a symbol by name materialize the remainder
+ of that DSO's index first to preserve name-lookup behavior.
+ Output may differ from the default loader for some targets
+ (e.g. PPC64 .opd or .gnu_debugdata). Default: off.
+
--max-symbol-bytes::
- Limit the bytes held in struct symbol allocations for DSOs on the
- libelf symbol-loader path: userspace DSOs, vmlinux-as-ELF, and kernel
- modules. This is not a cap on all symbol memory or RSS: symbols from
- kallsyms, JIT maps, 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,
- the ELF loader stops adding symbols; addresses not covered by symbols
- already loaded are then printed as [unknown]. A warning is printed.
- Default: 0 (unlimited).
+ 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, 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, the ELF loader stops adding symbols;
+ addresses not covered by the symbols already loaded are then printed
+ as [unknown]. 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/arch/powerpc/util/sym-handling.c b/tools/perf/arch/powerpc/util/sym-handling.c
index 947bfad7aa59..ac8fc787e95d 100644
--- a/tools/perf/arch/powerpc/util/sym-handling.c
+++ b/tools/perf/arch/powerpc/util/sym-handling.c
@@ -10,10 +10,9 @@
#include "probe-event.h"
#include "probe-file.h"
-int arch__choose_best_symbol(struct symbol *syma,
- struct symbol *symb __maybe_unused)
+int arch__choose_best_symbol(const char *syma_name)
{
- char *sym = syma->name;
+ const char *sym = syma_name;
#if !defined(_CALL_ELF) || _CALL_ELF != 2
/* Skip over any initial dot */
diff --git a/tools/perf/builtin-script.c b/tools/perf/builtin-script.c
index 017b39ed6a21..5bdf4a7895df 100644
--- a/tools/perf/builtin-script.c
+++ b/tools/perf/builtin-script.c
@@ -4037,7 +4037,7 @@ static int parse_callret_trace(const struct option *opt __maybe_unused,
}
static int parse_max_symbol_bytes(const struct option *opt,
- const char *str, int unset)
+ const char *str, int unset)
{
unsigned long *max_bytes = (unsigned long *)opt->value;
static struct parse_tag size_tags[] = {
@@ -4177,6 +4177,8 @@ int cmd_script(int argc, const char **argv)
OPT_CALLBACK(0, "max-symbol-bytes", &symbol_conf.max_symbol_bytes,
"size", "Limit bytes for ELF struct symbol (e.g. 128M; 0=unlimited)",
parse_max_symbol_bytes),
+ OPT_BOOLEAN(0, "lazy-load-symbols", &symbol_conf.lazy_load_symbols,
+ "Resolve symbols lazily instead of loading full symtabs"),
OPT_BOOLEAN(0, "reltime", &reltime, "Show time stamps relative to start"),
OPT_BOOLEAN(0, "deltatime", &deltatime, "Show time stamps relative to previous event"),
OPT_BOOLEAN('I', "show-info", &show_full_info,
diff --git a/tools/perf/tests/symbol-bytes.c b/tools/perf/tests/symbol-bytes.c
index 4a4740daa5f5..e5e2e6b9db97 100644
--- a/tools/perf/tests/symbol-bytes.c
+++ b/tools/perf/tests/symbol-bytes.c
@@ -1,11 +1,17 @@
// SPDX-License-Identifier: GPL-2.0
+#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
+#include <unistd.h>
+#include <linux/kernel.h>
+#include <linux/zalloc.h>
#include "debug.h"
+#include "dso.h"
+#include "map.h"
#include "symbol.h"
#include "symbol_conf.h"
#include "tests.h"
@@ -131,9 +137,142 @@ 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 {
+ u64 a_size;
+ u8 a_type;
+ u8 a_binding;
+ const char *a_name;
+ u64 b_size;
+ u8 b_type;
+ u8 b_binding;
+ const char *b_name;
+ int expected;
+ } cases[] = {
+ { 1, STT_FUNC, STB_GLOBAL, "a", 0, STT_FUNC, STB_GLOBAL, "b", SYMBOL_A },
+ { 1, STT_NOTYPE, STB_GLOBAL, "a", 1, STT_FUNC, STB_GLOBAL, "b", SYMBOL_B },
+ { 1, STT_FUNC, STB_WEAK, "a", 1, STT_FUNC, STB_GLOBAL, "b", SYMBOL_B },
+ { 1, STT_FUNC, STB_GLOBAL, "a", 1, STT_FUNC, STB_LOCAL, "b", SYMBOL_A },
+ { 1, STT_FUNC, STB_GLOBAL, "name", 1, STT_FUNC, STB_GLOBAL, "_name", SYMBOL_A },
+ { 1, STT_FUNC, STB_GLOBAL, "a", 1, STT_FUNC, STB_GLOBAL, "long", SYMBOL_B },
+ };
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(cases); i++) {
+ struct duplicate_case *c = &cases[i];
+
+ if (symbol__choose_best(c->a_size, c->a_type, c->a_binding, c->a_name,
+ c->b_size, c->b_type, c->b_binding, c->b_name) !=
+ c->expected)
+ return TEST_FAIL;
+ }
+ return TEST_OK;
+}
+
+#ifdef HAVE_LIBELF_SUPPORT
+static int test__symbol_bytes_lazy_name_lookup(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ static const char names[] = "first\0second\0";
+ unsigned long saved_max = symbol_conf.max_symbol_bytes;
+ size_t baseline = symbol__bytes_used();
+ char path[] = "/tmp/perf-lazy-names-XXXXXX";
+ struct dso_ondemand *od = NULL;
+ struct symbol *sym;
+ struct dso *dso = NULL;
+ struct map *map = NULL;
+ struct rb_node *node;
+ int nr_symbols = 0;
+ int ret = TEST_FAIL;
+ int fd = -1;
+
+ symbol_conf.max_symbol_bytes = 0;
+ fd = mkstemp(path);
+ if (fd < 0 || write(fd, names, sizeof(names)) != (ssize_t)sizeof(names))
+ goto out;
+ close(fd);
+ fd = -1;
+
+ dso = dso__new("/not/the/symbol/source");
+ od = zalloc(sizeof(*od));
+ if (!dso || !od)
+ goto out;
+ od->sorted = zalloc(2 * sizeof(*od->sorted));
+ od->data_dso = dso__new(path);
+ if (!od->sorted || !od->data_dso ||
+ dso__data_set_path(od->data_dso, path) < 0)
+ goto out;
+ dso__set_binary_type(od->data_dso, DSO_BINARY_TYPE__SYSTEM_PATH_DSO);
+ od->strtab_size = sizeof(names);
+ od->nr_sorted = 2;
+ od->nr_alloc = 2;
+ od->sorted[0] = (struct sym_idx) {
+ .start = 0x10,
+ .end = 0x20,
+ .name_off = 0,
+ .binding = STB_GLOBAL,
+ .type = STT_FUNC,
+ };
+ od->sorted[1] = (struct sym_idx) {
+ .start = 0x20,
+ .end = 0x30,
+ .name_off = sizeof("first"),
+ .binding = STB_GLOBAL,
+ .type = STT_FUNC,
+ };
+ if (!symbol__try_account_bytes(od->nr_alloc * sizeof(*od->sorted)))
+ goto out;
+ dso__set_ondemand(dso, od);
+ od = NULL;
+ dso__set_loaded(dso);
+ map = map__new2(0, dso);
+ if (!map)
+ goto out;
+
+ sym = map__find_symbol(map, 0x11);
+ if (!sym || strcmp(sym->name, "first"))
+ goto out;
+
+ dso__data_close(dso__ondemand(dso)->data_dso);
+ sym = map__find_symbol_by_name(map, "second");
+ if (!sym || strcmp(sym->name, "second") || dso__ondemand(dso))
+ goto out;
+
+ for (node = rb_first_cached(dso__symbols(dso)); node; node = rb_next(node))
+ nr_symbols++;
+ if (nr_symbols != 2)
+ goto out;
+ ret = TEST_OK;
+out:
+ if (fd >= 0)
+ close(fd);
+ if (map)
+ map__put(map);
+ if (dso)
+ dso__put(dso);
+ if (od) {
+ if (od->data_dso)
+ dso__put(od->data_dso);
+ free(od->sorted);
+ free(od);
+ }
+ unlink(path);
+ symbol_conf.max_symbol_bytes = saved_max;
+ if (symbol__bytes_used() != baseline)
+ ret = TEST_FAIL;
+ return ret;
+}
+#endif
+
static struct test_case tests__symbol_bytes[] = {
TEST_CASE("Long name accounting", symbol_bytes_long_name),
TEST_CASE("Concurrent strict reservations", symbol_bytes_reservation),
+ TEST_CASE("Shared duplicate selection", symbol_bytes_duplicate_selection),
+#ifdef HAVE_LIBELF_SUPPORT
+ TEST_CASE("Lazy address and name lookup", symbol_bytes_lazy_name_lookup),
+#endif
{ .name = NULL, }
};
diff --git a/tools/perf/util/dso.c b/tools/perf/util/dso.c
index 42bfe30a3b51..af0440cb8449 100644
--- a/tools/perf/util/dso.c
+++ b/tools/perf/util/dso.c
@@ -526,8 +526,11 @@ static void dso__list_add(struct dso *dso) EXCLUSIVE_LOCKS_REQUIRED(_dso__data_o
#ifdef REFCNT_CHECKING
dso__data(dso)->dso = dso__get(dso);
#endif
- /* Assume the dso is part of dsos, hence the optional reference count above. */
- assert(dso__dsos(dso));
+ /*
+ * Most data DSOs belong to a dsos collection. Private data sources,
+ * such as a lazy symbol index's split-debuginfo file, are instead kept
+ * alive by their owner.
+ */
dso__data_open_cnt++;
}
@@ -572,16 +575,22 @@ char *dso__filename_with_chroot(const struct dso *dso, const char *filename)
static char *dso__get_filename(struct dso *dso, const char *root_dir,
bool *decomp)
{
- char *name = malloc(PATH_MAX);
+ char *name;
*decomp = false;
- if (name == NULL)
- return NULL;
-
- if (dso__read_binary_type_filename(dso, dso__binary_type(dso),
- root_dir, name, PATH_MAX))
- goto out;
+ if (dso__data(dso)->path) {
+ name = strdup(dso__data(dso)->path);
+ if (!name)
+ return NULL;
+ } else {
+ name = malloc(PATH_MAX);
+ if (!name)
+ return NULL;
+ if (dso__read_binary_type_filename(dso, dso__binary_type(dso),
+ root_dir, name, PATH_MAX))
+ goto out;
+ }
if (!is_regular_file(name)) {
struct stat st;
@@ -807,6 +816,17 @@ void dso__data_close(struct dso *dso)
mutex_unlock(dso__data_open_lock());
}
+int dso__data_set_path(struct dso *dso, const char *path)
+{
+ char *new_path = strdup(path);
+
+ if (!new_path)
+ return -ENOMEM;
+ free(dso__data(dso)->path);
+ dso__data(dso)->path = new_path;
+ return 0;
+}
+
static void try_to_open_dso(struct dso *dso, struct machine *machine)
EXCLUSIVE_LOCKS_REQUIRED(_dso__data_open_lock)
{
@@ -1674,6 +1694,27 @@ void dso__set_sorted_by_name(struct dso *dso)
RC_CHK_ACCESS(dso)->sorted_by_name = true;
}
+void dso__reset_symbol_names(struct dso *dso)
+{
+ zfree(&RC_CHK_ACCESS(dso)->symbol_names);
+ RC_CHK_ACCESS(dso)->symbol_names_len = 0;
+ RC_CHK_ACCESS(dso)->sorted_by_name = false;
+}
+
+void dso__free_ondemand(struct dso *dso)
+{
+ struct dso_ondemand *od = RC_CHK_ACCESS(dso)->ondemand;
+
+ if (!od)
+ return;
+ RC_CHK_ACCESS(dso)->ondemand = NULL;
+ free(od->sorted);
+ symbol__unaccount_bytes(od->nr_alloc * sizeof(*od->sorted));
+ dso__data_close(od->data_dso);
+ dso__put(od->data_dso);
+ free(od);
+}
+
struct dso *dso__new_id(const char *name, const struct dso_id *id)
{
RC_STRUCT(dso) *dso = zalloc(sizeof(*dso) + strlen(name) + 1);
@@ -1755,7 +1796,11 @@ void dso__delete(struct dso *dso)
dso__data_close(dso);
auxtrace_cache__free(RC_CHK_ACCESS(dso)->auxtrace_cache);
+ mutex_lock(dso__lock(dso));
+ dso__free_ondemand(dso);
+ mutex_unlock(dso__lock(dso));
dso_cache__free(dso);
+ zfree(&RC_CHK_ACCESS(dso)->data.path);
dso__free_a2l(dso);
dso__free_libdw(dso);
dso__free_symsrc_filename(dso);
diff --git a/tools/perf/util/dso.h b/tools/perf/util/dso.h
index 55c4aaa53c38..301997642e6a 100644
--- a/tools/perf/util/dso.h
+++ b/tools/perf/util/dso.h
@@ -264,6 +264,7 @@ struct dso_data {
#ifdef REFCNT_CHECKING
struct dso *dso;
#endif
+ char *path;
int fd;
int status;
u32 status_seen;
@@ -282,6 +283,27 @@ struct dso_bpf_prog {
struct perf_env *env;
};
+struct sym_idx {
+ u64 start; /* adjusted st_value (same space as sym->start) */
+ u64 end; /* start + st_size, or next start if st_size==0 */
+ u32 name_off; /* symbol's st_name: offset into the strtab */
+ u8 binding;
+ u8 type;
+ u8 flags;
+};
+
+#define SYM_IDX_FLAG_IFUNC_ALIAS (1 << 0)
+#define SYM_IDX_FLAG_MATERIALIZED (1 << 1)
+
+struct dso_ondemand {
+ struct dso *data_dso; /* exact symbol source, using the DSO data cache */
+ u64 strtab_offset; /* file offset of strtab section */
+ u64 strtab_size;
+ struct sym_idx *sorted;
+ u32 nr_sorted; /* deduped count */
+ u32 nr_alloc; /* allocated count, for accounting */
+};
+
struct auxtrace_cache;
DECLARE_RC_STRUCT(dso) {
@@ -308,6 +330,7 @@ DECLARE_RC_STRUCT(dso) {
char *symsrc_filename;
struct nsinfo *nsinfo;
struct auxtrace_cache *auxtrace_cache;
+ struct dso_ondemand *ondemand;
union { /* Tool specific area */
void *priv;
u64 db_id;
@@ -448,6 +471,16 @@ static inline void dso__set_auxtrace_cache(struct dso *dso, struct auxtrace_cach
RC_CHK_ACCESS(dso)->auxtrace_cache = cache;
}
+static inline struct dso_ondemand *dso__ondemand(struct dso *dso)
+{
+ return RC_CHK_ACCESS(dso)->ondemand;
+}
+
+static inline void dso__set_ondemand(struct dso *dso, struct dso_ondemand *od)
+{
+ RC_CHK_ACCESS(dso)->ondemand = od;
+}
+
static inline struct dso_bpf_prog *dso__bpf_prog(struct dso *dso)
{
return &RC_CHK_ACCESS(dso)->bpf_prog;
@@ -823,6 +856,21 @@ int dso__read_binary_type_filename(const struct dso *dso, enum dso_binary_type t
const char *root_dir, char *filename, size_t size);
bool is_kernel_module(const char *pathname, int cpumode);
bool dso__needs_decompress(struct dso *dso);
+struct symbol *dso__find_symbol_ondemand(struct dso *dso, u64 addr)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso));
+struct symbol *dso__find_symbol_ondemand_exact(struct dso *dso, u64 addr)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso));
+int dso__materialize_symbols_ondemand(struct dso *dso)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso));
+const char *dso__read_ondemand_symbol_name(struct dso *data_dso,
+ u64 strtab_offset, u64 strtab_size,
+ u64 name_off, char *buf,
+ size_t buflen, char **to_free,
+ unsigned int *nr_reads);
+void dso__free_ondemand(struct dso *dso)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso));
+void dso__reset_symbol_names(struct dso *dso)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso));
int dso__decompress_kmodule_fd(struct dso *dso, const char *name);
int dso__decompress_kmodule_path(struct dso *dso, const char *name,
char *pathname, size_t len);
@@ -896,6 +944,7 @@ bool dso__data_get_fd(struct dso *dso, struct machine *machine, int *fd)
EXCLUSIVE_TRYLOCK_FUNCTION(true, _dso__data_open_lock);
void dso__data_put_fd(struct dso *dso) UNLOCK_FUNCTION(_dso__data_open_lock);
void dso__data_close(struct dso *dso) LOCKS_EXCLUDED(_dso__data_open_lock);
+int dso__data_set_path(struct dso *dso, const char *path);
int dso__data_file_size(struct dso *dso, struct machine *machine);
off_t dso__data_size(struct dso *dso, struct machine *machine);
diff --git a/tools/perf/util/map.c b/tools/perf/util/map.c
index 41cdddc987ee..c44698bc3a65 100644
--- a/tools/perf/util/map.c
+++ b/tools/perf/util/map.c
@@ -382,10 +382,28 @@ int map__load(struct map *map)
struct symbol *map__find_symbol(struct map *map, u64 addr)
{
+ struct dso *dso;
+ struct symbol *sym;
+
if (map__load(map) < 0)
return NULL;
- return dso__find_symbol(map__dso(map), addr);
+ dso = map__dso(map);
+ if (dso__ondemand(dso)) {
+ /*
+ * On-demand lookup may materialize and insert a symbol. Keep
+ * both the lookup and insertion under the DSO lock so another
+ * thread cannot traverse or modify the rb-tree concurrently.
+ */
+ mutex_lock(dso__lock(dso));
+ sym = dso__find_symbol(dso, addr);
+ if (!sym)
+ sym = dso__find_symbol_ondemand(dso, addr);
+ mutex_unlock(dso__lock(dso));
+ } else {
+ sym = dso__find_symbol(dso, addr);
+ }
+ return sym;
}
struct symbol *map__find_symbol_by_name_idx(struct map *map, const char *name, size_t *idx)
@@ -396,6 +414,16 @@ struct symbol *map__find_symbol_by_name_idx(struct map *map, const char *name, s
return NULL;
dso = map__dso(map);
+ if (dso__ondemand(dso)) {
+ mutex_lock(dso__lock(dso));
+ /*
+ * Name lookup requires a complete name-sorted array. Preserve
+ * that API by materializing the remaining address index first.
+ * An explicit symbol-byte limit can leave a partial set.
+ */
+ dso__materialize_symbols_ondemand(dso);
+ mutex_unlock(dso__lock(dso));
+ }
dso__sort_by_name(dso);
return dso__find_symbol_by_name(dso, name, idx);
diff --git a/tools/perf/util/symbol-elf.c b/tools/perf/util/symbol-elf.c
index 2f7ea1499cbf..094d5ffaa1f0 100644
--- a/tools/perf/util/symbol-elf.c
+++ b/tools/perf/util/symbol-elf.c
@@ -2,6 +2,7 @@
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
+#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@@ -12,6 +13,7 @@
#include "libbfd.h"
#include "map.h"
#include "maps.h"
+#include "namespaces.h"
#include "symbol.h"
#include "symsrc.h"
#include "machine.h"
@@ -334,6 +336,7 @@ static bool addend_may_be_ifunc(GElf_Ehdr *ehdr, struct rel_info *ri)
static bool get_ifunc_name(Elf *elf, struct dso *dso, GElf_Ehdr *ehdr,
struct rel_info *ri, char *buf, size_t buf_sz)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso))
{
u64 addr = ri->rela.r_addend;
struct symbol *sym;
@@ -348,6 +351,8 @@ static bool get_ifunc_name(Elf *elf, struct dso *dso, GElf_Ehdr *ehdr,
addr -= phdr.p_vaddr - phdr.p_offset;
sym = dso__find_symbol_nocache(dso, addr);
+ if (!sym && dso__ondemand(dso))
+ sym = dso__find_symbol_ondemand_exact(dso, addr);
/* Expecting the address to be an IFUNC or IFUNC alias */
if (!sym || sym->start != addr ||
@@ -608,6 +613,26 @@ static int dso__synthesize_plt_got_symbols(struct dso *dso, Elf *elf,
return err;
}
+static u32 sym_idx__lower_bound(const struct dso_ondemand *od, u64 addr);
+
+static void dso__clip_ondemand_symbols_at(struct dso *dso, u64 addr)
+{
+ struct dso_ondemand *od = dso__ondemand(dso);
+ u32 lo, i;
+
+ if (!od)
+ return;
+
+ lo = sym_idx__lower_bound(od, addr);
+ if (!lo)
+ return;
+
+ for (i = 0; i < lo; i++) {
+ if (od->sorted[i].end > addr)
+ od->sorted[i].end = addr;
+ }
+}
+
/*
* We need to check if we have a .dynsym, so that we can handle the
* .plt, synthesizing its symbols, that aren't on the symtabs (be it
@@ -616,6 +641,7 @@ static int dso__synthesize_plt_got_symbols(struct dso *dso, Elf *elf,
* have the PLT data stripped out (shdr_rel_plt.sh_type == SHT_NOBITS).
*/
int dso__synthesize_plt_symbols(struct dso *dso, struct symsrc *ss)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso))
{
uint32_t idx;
GElf_Sym sym;
@@ -639,6 +665,13 @@ int dso__synthesize_plt_symbols(struct dso *dso, struct symsrc *ss)
if (!elf_section_by_name(elf, &ehdr, &shdr_plt, ".plt", NULL))
return 0;
+ /*
+ * Zero-sized or oversized ELF symbols can have been extended across
+ * .plt. Clip the index first so lookups cannot attribute PLT addresses
+ * to a preceding symbol before the synthesized PLT symbols are added.
+ */
+ dso__clip_ondemand_symbols_at(dso, shdr_plt.sh_offset);
+
/*
* A symbol from a previous section (e.g. .init) can have been expanded
* by symbols__fixup_end() to overlap .plt. Truncate it before adding
@@ -1544,6 +1577,612 @@ static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
return 0;
}
+static int cmp_sym_idx(const void *a, const void *b)
+{
+ const struct sym_idx *sa = a, *sb = b;
+
+ if (sa->start != sb->start)
+ return sa->start < sb->start ? -1 : 1;
+ /*
+ * qsort is not stable. During sorting name_off temporarily holds
+ * the fill ordinal, preserving eager's symtab insertion order for
+ * equal-start aliases. It is restored to st_name afterwards.
+ */
+ if (sa->name_off != sb->name_off)
+ return sa->name_off < sb->name_off ? -1 : 1;
+ return 0;
+}
+
+/*
+ * Return the first entry whose start is not less than @addr. ISO C bsearch()
+ * does not provide an insertion point or guarantee the first equal entry, so
+ * clipping and exact-start alias lookup use this helper.
+ */
+static u32 sym_idx__lower_bound(const struct dso_ondemand *od, u64 addr)
+{
+ u32 lo = 0, hi = od->nr_sorted;
+
+ while (lo < hi) {
+ u32 mid = lo + (hi - lo) / 2;
+
+ if (od->sorted[mid].start < addr)
+ lo = mid + 1;
+ else
+ hi = mid;
+ }
+ return lo;
+}
+
+static int cmp_addr_to_sym_idx(const void *key, const void *entry)
+{
+ u64 addr = *(const u64 *)key;
+ const struct sym_idx *idx = entry;
+
+ if (addr < idx->start)
+ return -1;
+ if (addr >= idx->end)
+ return 1;
+ return 0;
+}
+
+static bool ondemand_sym_ok(Elf *elf, Elf_Data *secstrs,
+ const GElf_Sym *sym, u32 sh_link,
+ uint16_t e_machine)
+{
+ Elf_Scn *sym_sec;
+ GElf_Shdr sym_shdr;
+ int is_label = elf_sym__is_label(sym);
+ const char *name;
+
+ if (!is_label && !elf_sym__filter((GElf_Sym *)sym))
+ return false;
+
+ if (sym->st_shndx == SHN_ABS)
+ return false;
+
+ sym_sec = elf_getscn(elf, sym->st_shndx);
+ if (!sym_sec)
+ return false;
+ if (!gelf_getshdr(sym_sec, &sym_shdr))
+ return false;
+ if (!(sym_shdr.sh_flags & SHF_ALLOC))
+ return false;
+
+ if (is_label && (!secstrs || !elf_sec__filter(&sym_shdr, secstrs)))
+ return false;
+
+ name = elf_strptr(elf, sh_link, sym->st_name);
+ if (!name)
+ return false;
+
+ /*
+ * Reject ARM/AArch64/RISC-V "mapping symbols" ($a/$d/$t/$x), as
+ * the eager loop does. They are zero-size STT_NOTYPE labels in
+ * allocated sections that would otherwise be indexed and fill
+ * forward over real functions, misattributing everything after
+ * them.
+ */
+ if (e_machine == EM_ARM || e_machine == EM_AARCH64) {
+ if (name[0] == '$' && strchr("adtx", name[1]) &&
+ (name[2] == '\0' || name[2] == '.'))
+ return false;
+ }
+ if (e_machine == EM_RISCV) {
+ if (name[0] == '$' && strchr("dx", name[1]))
+ return false;
+ }
+
+ return true;
+}
+
+static int dso__build_ondemand_index(struct dso *dso, struct symsrc *syms_ss,
+ struct symsrc *runtime_ss,
+ int dynsym)
+{
+ struct dso_ondemand *od;
+ Elf *elf = syms_ss->elf;
+ GElf_Ehdr ehdr = syms_ss->ehdr;
+ GElf_Shdr shdr;
+ GElf_Shdr strshdr;
+ Elf_Scn *strscn, *sec_strndx;
+ Elf_Data *syms;
+ GElf_Sym sym;
+ Elf_Data *secstrs = NULL;
+ size_t i, index_bytes, reservation_peak;
+ u32 count = 0, j;
+ u32 *name_offsets;
+ u64 nr_entries, strtab_offset;
+ u64 probe_off;
+ u8 probe;
+
+ /*
+ * GNU debugdata is backed by a temporary decompressed fd rather than a
+ * reopenable source path. Keep using the eager loader for that case.
+ */
+ if (syms_ss->type == DSO_BINARY_TYPE__GNU_DEBUGDATA)
+ return 0;
+
+ if (dynsym)
+ shdr = syms_ss->dynshdr;
+ else
+ shdr = syms_ss->symshdr;
+
+ syms = elf_getdata(dynsym ? syms_ss->dynsym : syms_ss->symtab, NULL);
+ if (!syms)
+ return -1;
+
+ if (!shdr.sh_entsize)
+ return 0;
+
+ nr_entries = shdr.sh_size / shdr.sh_entsize;
+ if (nr_entries > UINT32_MAX)
+ return -EOVERFLOW;
+
+ strscn = elf_getscn(elf, shdr.sh_link);
+ if (!strscn || !gelf_getshdr(strscn, &strshdr))
+ return -1;
+ strtab_offset = strshdr.sh_offset;
+
+ /*
+ * Section name string table, used to match the eager path's
+ * elf_sec__filter() (text/data section check for STT_NOTYPE labels).
+ */
+ sec_strndx = elf_getscn(elf, ehdr.e_shstrndx);
+ if (sec_strndx)
+ secstrs = elf_getdata(sec_strndx, NULL);
+
+ for (i = 0; i < nr_entries; i++) {
+ if (!gelf_getsym(syms, i, &sym))
+ continue;
+ if (ondemand_sym_ok(elf, secstrs, &sym, shdr.sh_link,
+ ehdr.e_machine))
+ count++;
+ }
+
+ if (!count)
+ return 0;
+ if (check_mul_overflow((size_t)count, sizeof(*od->sorted),
+ &index_bytes))
+ return -EOVERFLOW;
+
+ /*
+ * Account the index against the symbol memory budget: at 24
+ * bytes/symbol it is the dominant on-demand cost and must count
+ * toward --max-symbol-bytes just like struct symbol allocations do.
+ */
+ if (!symbol__try_account_bytes(index_bytes)) {
+ symbol_budget_warning();
+ return 0; /* fall back to the eager loader's per-symbol budget */
+ }
+ reservation_peak = symbol__bytes_used();
+
+ od = zalloc(sizeof(*od));
+ if (!od) {
+ symbol__unaccount_bytes(index_bytes);
+ return -1;
+ }
+
+ od->sorted = zalloc(index_bytes);
+ if (!od->sorted) {
+ symbol__unaccount_bytes(index_bytes);
+ free(od);
+ return -1;
+ }
+ od->nr_alloc = count; /* allocated; the deduped count may shrink */
+ name_offsets = malloc(count * sizeof(*name_offsets));
+ if (!name_offsets) {
+ symbol__unaccount_bytes(index_bytes);
+ free(od->sorted);
+ free(od);
+ return -1;
+ }
+
+ j = 0;
+ for (i = 0; i < nr_entries; i++) {
+ u64 adjusted;
+ GElf_Phdr phdr;
+
+ if (!gelf_getsym(syms, i, &sym))
+ continue;
+ if (!ondemand_sym_ok(elf, secstrs, &sym, shdr.sh_link,
+ ehdr.e_machine))
+ continue;
+
+ adjusted = sym.st_value;
+
+ if ((ehdr.e_machine == EM_ARM) &&
+ (GELF_ST_TYPE(sym.st_info) == STT_FUNC) &&
+ (adjusted & 1))
+ --adjusted;
+
+ /*
+ * Program header adjustment, identical to the eager loop:
+ * read the PT_LOAD containing the symbol from the runtime
+ * ELF (the debug-info file may have zeroed p_offset), and
+ * fall back to the section-header bias when no program
+ * header matches -- exactly what the eager path does when
+ * elf_read_program_header fails.
+ */
+ if (elf_read_program_header(runtime_ss->elf, adjusted,
+ &phdr) == 0) {
+ adjusted -= phdr.p_vaddr - phdr.p_offset;
+ } else {
+ Elf_Scn *sym_sec = elf_getscn(elf, sym.st_shndx);
+ GElf_Shdr sym_shdr;
+
+ if (sym_sec && gelf_getshdr(sym_sec, &sym_shdr))
+ adjusted -= sym_shdr.sh_addr - sym_shdr.sh_offset;
+ }
+
+ od->sorted[j].start = adjusted;
+ od->sorted[j].end = sym.st_size; /* st_size for now, converted later */
+ /*
+ * Sort equal-start aliases in original symtab order to match
+ * rb-tree insertion order. Restore st_name after sorting.
+ */
+ name_offsets[j] = sym.st_name;
+ od->sorted[j].name_off = j;
+ od->sorted[j].binding = GELF_ST_BIND(sym.st_info);
+ od->sorted[j].type = GELF_ST_TYPE(sym.st_info);
+ j++;
+ }
+ count = j;
+ if (!count) {
+ symbol__unaccount_bytes(index_bytes);
+ free(name_offsets);
+ free(od->sorted);
+ free(od);
+ return 0;
+ }
+
+ qsort(od->sorted, count, sizeof(*od->sorted), cmp_sym_idx);
+ for (i = 0; i < count; i++)
+ od->sorted[i].name_off = name_offsets[od->sorted[i].name_off];
+ free(name_offsets);
+
+ /*
+ * Match the eager loader's ordering: fill zero-sized ranges before
+ * choosing among equal-start aliases, so the size preference sees
+ * the same synthesized lengths as symbols__fixup_duplicate().
+ */
+ for (i = 0; i < count; i++) {
+ u64 size = od->sorted[i].end; /* was st_size */
+
+ if (size > 0)
+ od->sorted[i].end = od->sorted[i].start + size;
+ else if (i + 1 < count)
+ od->sorted[i].end = od->sorted[i + 1].start;
+ else
+ od->sorted[i].end = roundup(od->sorted[i].start, 4096) + 4096;
+ }
+
+ if (!symbol_conf.allow_aliases) {
+ u32 out = 0;
+
+ for (i = 0; i < count; i++) {
+ u32 best = i;
+ const char *na = NULL, *nb;
+ char *da = NULL, *db;
+ bool has_ifunc = od->sorted[i].type == STT_GNU_IFUNC;
+
+ na = elf_strptr(elf, shdr.sh_link,
+ od->sorted[best].name_off);
+ if (na) {
+ da = dso__demangle_sym(dso, 0, na);
+ if (da)
+ na = da;
+ }
+
+ for (j = i + 1; j < count &&
+ od->sorted[j].start == od->sorted[i].start; j++) {
+ int choice;
+
+ has_ifunc |= od->sorted[j].type == STT_GNU_IFUNC;
+ nb = elf_strptr(elf, shdr.sh_link,
+ od->sorted[j].name_off);
+ if (!na || !nb)
+ continue;
+
+ db = dso__demangle_sym(dso, 0, nb);
+ if (db)
+ nb = db;
+
+ choice = symbol__choose_best(
+ od->sorted[best].end -
+ od->sorted[best].start,
+ od->sorted[best].type,
+ od->sorted[best].binding, na,
+ od->sorted[j].end -
+ od->sorted[j].start,
+ od->sorted[j].type,
+ od->sorted[j].binding, nb);
+ if (choice == SYMBOL_B) {
+ best = j;
+ free(da);
+ da = db;
+ na = nb;
+ } else {
+ free(db);
+ }
+ }
+
+ free(da);
+ od->sorted[out++] = od->sorted[best];
+ if (has_ifunc && od->sorted[out - 1].type != STT_GNU_IFUNC)
+ od->sorted[out - 1].flags |= SYM_IDX_FLAG_IFUNC_ALIAS;
+ i = j - 1; /* skip past all aliases of this start */
+ }
+
+ if (out < count) {
+ struct sym_idx *shrunk;
+
+ shrunk = realloc(od->sorted, out * sizeof(*od->sorted));
+ if (shrunk) {
+ od->sorted = shrunk;
+ symbol__unaccount_bytes((od->nr_alloc - out) *
+ sizeof(*od->sorted));
+ od->nr_alloc = out;
+ }
+ }
+ count = out;
+ }
+
+ /*
+ * The interval binary search requires non-overlapping ranges. In
+ * the default deduplicated mode, prefer the symbol with the nearest
+ * preceding start when an ELF st_size overlaps the next symbol.
+ */
+ if (!symbol_conf.allow_aliases) {
+ for (i = 0; i + 1 < count; i++) {
+ if (od->sorted[i].end > od->sorted[i + 1].start)
+ od->sorted[i].end = od->sorted[i + 1].start;
+ }
+ }
+
+ /*
+ * Keep an exact-path data DSO for the symbol source. This may differ
+ * from the runtime image (for example, split debuginfo), so using the
+ * primary DSO's data cache could read an unrelated string-table offset.
+ * The standard DSO data cache manages descriptor eviction and reopening.
+ */
+ od->data_dso = dso__new(syms_ss->name);
+ if (!od->data_dso ||
+ dso__data_set_path(od->data_dso, syms_ss->name) < 0)
+ goto out_decline_source;
+ dso__set_binary_type(od->data_dso, DSO_BINARY_TYPE__SYSTEM_PATH_DSO);
+ dso__set_nsinfo(od->data_dso, nsinfo__get(dso__nsinfo(dso)));
+
+ /*
+ * Open the managed source while eager fallback is still possible.
+ * A later failure would otherwise turn materialization into a miss.
+ */
+ if (od->sorted[0].name_off >= strshdr.sh_size)
+ goto out_decline_source;
+ if (check_add_overflow(strtab_offset,
+ (u64)od->sorted[0].name_off, &probe_off))
+ goto out_decline_source;
+ if (dso__data_read_offset(od->data_dso, NULL, probe_off, &probe, 1) != 1)
+ goto out_decline_source;
+
+ od->strtab_offset = strtab_offset;
+ od->strtab_size = strshdr.sh_size;
+ od->nr_sorted = count;
+
+ dso__set_ondemand(dso, od);
+
+ pr_debug("%s: on-demand index: %u symbols (%zu bytes, %zu bytes total) budget=%zu\n",
+ dso__long_name(dso), count,
+ od->nr_alloc * sizeof(*od->sorted), symbol__bytes_used(),
+ reservation_peak);
+
+ return 1;
+
+out_decline_source:
+ if (od->data_dso) {
+ dso__data_close(od->data_dso);
+ dso__put(od->data_dso);
+ }
+ symbol__unaccount_bytes(od->nr_alloc * sizeof(*od->sorted));
+ free(od->sorted);
+ free(od);
+ return 0;
+}
+
+const char *dso__read_ondemand_symbol_name(struct dso *data_dso,
+ u64 strtab_offset, u64 strtab_size,
+ u64 name_off, char *buf,
+ size_t buflen, char **to_free,
+ unsigned int *nr_reads)
+{
+ ssize_t n;
+ u64 remain;
+ u64 file_off;
+ size_t cap, want;
+
+ *to_free = NULL;
+
+ if (name_off >= strtab_size)
+ return NULL;
+ if (check_add_overflow(strtab_offset, name_off, &file_off))
+ return NULL;
+ remain = strtab_size - name_off;
+ if (nr_reads)
+ *nr_reads = 0;
+
+ want = min((u64)(buflen - 1), remain);
+ if (nr_reads)
+ (*nr_reads)++;
+ n = dso__data_read_offset(data_dso, NULL, file_off, (u8 *)buf, want);
+ if (n <= 0)
+ return NULL;
+ buf[n] = '\0';
+ if (memchr(buf, '\0', n))
+ return buf;
+ if ((size_t)n < want)
+ return NULL;
+
+ cap = 4096;
+ for (;;) {
+ char *tmp;
+
+ want = cap;
+ if (want > remain)
+ want = remain;
+ if (want == 0)
+ break;
+
+ tmp = *to_free ? realloc(*to_free, want + 1) : malloc(want + 1);
+ if (!tmp) {
+ free(*to_free);
+ *to_free = NULL;
+ return NULL;
+ }
+ *to_free = tmp;
+
+ if (nr_reads)
+ (*nr_reads)++;
+ n = dso__data_read_offset(data_dso, NULL, file_off,
+ (u8 *)*to_free, want);
+ if (n <= 0) {
+ free(*to_free);
+ *to_free = NULL;
+ return NULL;
+ }
+ (*to_free)[n] = '\0';
+
+ if (memchr(*to_free, '\0', n))
+ return *to_free;
+ if ((size_t)n < want)
+ break;
+
+ if (want >= remain || (u64)n >= remain)
+ break;
+
+ if (cap > SIZE_MAX / 2)
+ break;
+ cap *= 2;
+ }
+
+ free(*to_free);
+ *to_free = NULL;
+ return NULL;
+}
+
+static struct symbol *dso__materialize_symbol_ondemand(struct dso *dso, u32 pos)
+ EXCLUSIVE_LOCKS_REQUIRED(dso__lock(dso))
+{
+ struct dso_ondemand *od = dso__ondemand(dso);
+ struct sym_idx *idx = &od->sorted[pos];
+ const char *name;
+ char namebuf[1024];
+ char *name_heap = NULL;
+ char *demangled;
+ bool budget_exceeded;
+ struct symbol *s = NULL;
+
+ /*
+ * Check the budget before doing any name I/O or demangling, so an
+ * over-budget DSO stops paying pread+demangle on every later miss.
+ */
+ if (symbol_conf.max_symbol_bytes &&
+ symbol__bytes_used() >= symbol_conf.max_symbol_bytes) {
+ symbol_budget_warning();
+ return NULL;
+ }
+
+ name = dso__read_ondemand_symbol_name(od->data_dso, od->strtab_offset,
+ od->strtab_size, idx->name_off,
+ namebuf, sizeof(namebuf),
+ &name_heap, NULL);
+ if (!name)
+ return NULL;
+
+ demangled = dso__demangle_sym(dso, 0, name);
+ if (demangled)
+ name = demangled;
+
+ s = symbol__new_bounded(idx->start, idx->end - idx->start,
+ idx->binding, idx->type, name, &budget_exceeded);
+ free(demangled);
+ free(name_heap);
+ if (!s && budget_exceeded)
+ symbol_budget_warning();
+ if (s) {
+ if (idx->flags & SYM_IDX_FLAG_IFUNC_ALIAS)
+ symbol__set_ifunc_alias(s, true);
+ dso__reset_symbol_names(dso);
+ __symbols__insert(dso__symbols(dso), s);
+ idx->flags |= SYM_IDX_FLAG_MATERIALIZED;
+ }
+ return s;
+}
+
+int dso__materialize_symbols_ondemand(struct dso *dso)
+{
+ struct dso_ondemand *od = dso__ondemand(dso);
+ u32 i;
+
+ if (!od)
+ return 0;
+ for (i = 0; i < od->nr_sorted; i++) {
+ if (od->sorted[i].flags & SYM_IDX_FLAG_MATERIALIZED)
+ continue;
+ if (!dso__materialize_symbol_ondemand(dso, i))
+ return -1;
+ }
+ dso__free_ondemand(dso);
+ return 0;
+}
+
+struct symbol *dso__find_symbol_ondemand(struct dso *dso, u64 addr)
+{
+ struct dso_ondemand *od = dso__ondemand(dso);
+ const struct sym_idx *idx;
+ u32 lo, hi, mid;
+
+ if (!od || !od->sorted || !od->data_dso)
+ return NULL;
+
+ if (!symbol_conf.allow_aliases) {
+ idx = bsearch(&addr, od->sorted, od->nr_sorted,
+ sizeof(*od->sorted), cmp_addr_to_sym_idx);
+ return idx ? dso__materialize_symbol_ondemand(dso, idx - od->sorted) : NULL;
+ }
+
+ lo = 0;
+ hi = od->nr_sorted;
+ while (lo < hi) {
+ mid = (lo + hi) / 2;
+ if (addr < od->sorted[mid].start)
+ hi = mid;
+ else if (addr >= od->sorted[mid].end)
+ lo = mid + 1;
+ else
+ return dso__materialize_symbol_ondemand(dso, mid);
+ }
+ return NULL;
+}
+
+struct symbol *dso__find_symbol_ondemand_exact(struct dso *dso, u64 addr)
+{
+ struct dso_ondemand *od = dso__ondemand(dso);
+ u32 lo, mid;
+
+ if (!od || !od->sorted || !od->data_dso)
+ return NULL;
+
+ lo = sym_idx__lower_bound(od, addr);
+ if (lo >= od->nr_sorted || od->sorted[lo].start != addr)
+ return NULL;
+ for (mid = lo; mid < od->nr_sorted &&
+ od->sorted[mid].start == addr; mid++) {
+ if (od->sorted[mid].type == STT_GNU_IFUNC ||
+ od->sorted[mid].flags & SYM_IDX_FLAG_IFUNC_ALIAS)
+ return dso__materialize_symbol_ondemand(dso, mid);
+ }
+ return dso__materialize_symbol_ondemand(dso, lo);
+}
+
static int
dso__load_sym_internal(struct dso *dso, struct map *map, struct symsrc *syms_ss,
struct symsrc *runtime_ss, int kmodule, int dynsym)
@@ -1656,6 +2295,34 @@ dso__load_sym_internal(struct dso *dso, struct map *map, struct symsrc *syms_ss,
if (kmodule && adjust_kernel_syms)
max_text_sh_offset = max_text_section(runtime_ss->elf, &runtime_ss->ehdr);
+ /*
+ * PPC64 ELFv1 function symbols need the eager loop's .opd descriptor
+ * translation. For symtabs, the selected and runtime sources can differ.
+ */
+ if (symbol_conf.lazy_load_symbols && !dso__kernel(dso) && !kmodule &&
+ !syms_ss->opdsec && (dynsym || !runtime_ss->opdsec)) {
+ int oret = 0;
+
+ if (!dynsym && syms_ss->symtab)
+ oret = dso__build_ondemand_index(dso, syms_ss,
+ runtime_ss, 0);
+ else if (dynsym && !dso__ondemand(dso) && syms_ss->dynsym)
+ oret = dso__build_ondemand_index(dso, syms_ss,
+ runtime_ss, 1);
+
+ /*
+ * On hard error, propagate it. If an index was built, the
+ * DSO resolves on demand; skip the eager loop below. If the
+ * build declined (oret == 0, no index -- e.g. no usable
+ * symbols, or no reopenable data source), continue with the eager
+ * loop so the DSO still gets symbols.
+ */
+ if (oret < 0)
+ return oret;
+ if (dso__ondemand(dso))
+ return 1;
+ }
+
curr_dso = dso__get(dso);
elf_symtab__for_each_symbol(syms, nr_syms, idx, sym) {
struct symbol *f;
diff --git a/tools/perf/util/symbol-minimal.c b/tools/perf/util/symbol-minimal.c
index 0a71d1463952..245aed6b2788 100644
--- a/tools/perf/util/symbol-minimal.c
+++ b/tools/perf/util/symbol-minimal.c
@@ -373,6 +373,23 @@ void symbol__elf_init(void)
{
}
+struct symbol *dso__find_symbol_ondemand(struct dso *dso __maybe_unused,
+ u64 addr __maybe_unused)
+{
+ return NULL;
+}
+
+struct symbol *dso__find_symbol_ondemand_exact(struct dso *dso __maybe_unused,
+ u64 addr __maybe_unused)
+{
+ return NULL;
+}
+
+int dso__materialize_symbols_ondemand(struct dso *dso __maybe_unused)
+{
+ return 0;
+}
+
bool filename__has_section(const char *filename __maybe_unused, const char *sec __maybe_unused)
{
return false;
diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
index 32eef666f748..3ca9655c36fd 100644
--- a/tools/perf/util/symbol.c
+++ b/tools/perf/util/symbol.c
@@ -143,72 +143,80 @@ int __weak arch__compare_symbol_names_n(const char *namea, const char *nameb,
return strncmp(namea, nameb, n);
}
-int __weak arch__choose_best_symbol(struct symbol *syma,
- struct symbol *symb __maybe_unused)
+int __weak arch__choose_best_symbol(const char *syma_name)
{
/* Avoid "SyS" kernel syscall aliases */
- if (strlen(syma->name) >= 3 && !strncmp(syma->name, "SyS", 3))
+ if (strlen(syma_name) >= 3 && !strncmp(syma_name, "SyS", 3))
return SYMBOL_B;
- if (strlen(syma->name) >= 10 && !strncmp(syma->name, "compat_SyS", 10))
+ if (strlen(syma_name) >= 10 && !strncmp(syma_name, "compat_SyS", 10))
return SYMBOL_B;
return SYMBOL_A;
}
-static int choose_best_symbol(struct symbol *syma, struct symbol *symb)
+int symbol__choose_best(u64 a_size, u8 a_type, u8 a_binding,
+ const char *a_name,
+ u64 b_size, u8 b_type, u8 b_binding,
+ const char *b_name)
{
s64 a;
s64 b;
size_t na, nb;
/* Prefer a symbol with non zero length */
- a = syma->end - syma->start;
- b = symb->end - symb->start;
- if ((b == 0) && (a > 0))
+ if ((b_size == 0) && (a_size > 0))
return SYMBOL_A;
- else if ((a == 0) && (b > 0))
+ else if ((a_size == 0) && (b_size > 0))
return SYMBOL_B;
- if (symbol__type(syma) != symbol__type(symb)) {
- if (symbol__type(syma) == STT_NOTYPE)
+ if (a_type != b_type) {
+ if (a_type == STT_NOTYPE)
return SYMBOL_B;
- if (symbol__type(symb) == STT_NOTYPE)
+ if (b_type == STT_NOTYPE)
return SYMBOL_A;
}
/* Prefer a non weak symbol over a weak one */
- a = symbol__binding(syma) == STB_WEAK;
- b = symbol__binding(symb) == STB_WEAK;
+ a = a_binding == STB_WEAK;
+ b = b_binding == STB_WEAK;
if (b && !a)
return SYMBOL_A;
if (a && !b)
return SYMBOL_B;
/* Prefer a global symbol over a non global one */
- a = symbol__binding(syma) == STB_GLOBAL;
- b = symbol__binding(symb) == STB_GLOBAL;
+ a = a_binding == STB_GLOBAL;
+ b = b_binding == STB_GLOBAL;
if (a && !b)
return SYMBOL_A;
if (b && !a)
return SYMBOL_B;
/* Prefer a symbol with less underscores */
- a = prefix_underscores_count(syma->name);
- b = prefix_underscores_count(symb->name);
+ a = prefix_underscores_count(a_name);
+ b = prefix_underscores_count(b_name);
if (b > a)
return SYMBOL_A;
else if (a > b)
return SYMBOL_B;
/* Choose the symbol with the longest name */
- na = strlen(syma->name);
- nb = strlen(symb->name);
+ na = strlen(a_name);
+ nb = strlen(b_name);
if (na > nb)
return SYMBOL_A;
else if (na < nb)
return SYMBOL_B;
- return arch__choose_best_symbol(syma, symb);
+ return arch__choose_best_symbol(a_name);
+}
+
+static int choose_best_symbol(struct symbol *syma, struct symbol *symb)
+{
+ return symbol__choose_best(syma->end - syma->start,
+ symbol__type(syma), symbol__binding(syma), syma->name,
+ symb->end - symb->start,
+ symbol__type(symb), symbol__binding(symb), symb->name);
}
void symbols__fixup_duplicate(struct rb_root_cached *symbols)
@@ -1989,11 +1997,19 @@ int dso__load(struct dso *dso, struct map *map)
}
#ifdef HAVE_LIBBFD_SUPPORT
+#ifdef HAVE_LIBELF_SUPPORT
+ if (is_reg && !symbol_conf.lazy_load_symbols)
+#else
if (is_reg)
+#endif
bfdrc = dso__load_bfd_symbols(dso, name);
#endif
if (is_reg && bfdrc < 0)
sirc = symsrc__init(ss, dso, name, symtab_type);
+#if defined(HAVE_LIBBFD_SUPPORT) && defined(HAVE_LIBELF_SUPPORT)
+ if (is_reg && symbol_conf.lazy_load_symbols && sirc < 0)
+ bfdrc = dso__load_bfd_symbols(dso, name);
+#endif
if (nsexit)
nsinfo__mountns_enter(dso__nsinfo(dso), &nsc);
diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
index f7331edf0b71..99563c9ff610 100644
--- a/tools/perf/util/symbol.h
+++ b/tools/perf/util/symbol.h
@@ -233,6 +233,10 @@ size_t symbol__bytes_used(void);
void symbol__account_bytes(size_t bytes);
bool symbol__try_account_bytes(size_t bytes);
void symbol__unaccount_bytes(size_t bytes);
+int symbol__choose_best(u64 a_size, u8 a_type, u8 a_binding,
+ const char *a_name,
+ u64 b_size, u8 b_type, u8 b_binding,
+ const char *b_name);
size_t __symbol__fprintf_symname_offs(const struct symbol *sym,
const struct addr_location *al,
bool unknown_as_addr,
@@ -308,7 +312,7 @@ const char *arch__normalize_symbol_name(const char *name);
int arch__compare_symbol_names(const char *namea, const char *nameb);
int arch__compare_symbol_names_n(const char *namea, const char *nameb,
unsigned int n);
-int arch__choose_best_symbol(struct symbol *syma, struct symbol *symb);
+int arch__choose_best_symbol(const char *syma_name);
enum symbol_tag_include {
SYMBOL_TAG_INCLUDE__NONE = 0,
diff --git a/tools/perf/util/symbol_conf.h b/tools/perf/util/symbol_conf.h
index 6a16c5badd5e..0f8d044eba20 100644
--- a/tools/perf/util/symbol_conf.h
+++ b/tools/perf/util/symbol_conf.h
@@ -74,6 +74,7 @@ struct symbol_conf {
no_buildid_mmap2,
guest_code,
lazy_load_kernel_maps,
+ lazy_load_symbols,
keep_exited_threads,
annotate_data_member,
annotate_data_sample,
--
Git-157)
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v2 4/4] perf test: Test lazy symbol loading and symbol memory limits
2026-09-20 2:33 [PATCH v2 0/4] perf script: Bounded and lazy symbol loading Alireza Haghdoost via B4 Relay
` (2 preceding siblings ...)
2026-09-20 2:33 ` [PATCH v2 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading Alireza Haghdoost via B4 Relay
@ 2026-09-20 2:33 ` Alireza Haghdoost via B4 Relay
3 siblings, 0 replies; 10+ messages in thread
From: Alireza Haghdoost via B4 Relay @ 2026-09-20 2:33 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
Ian Rogers, Adrian Hunter, James Clark, Alexei Starovoitov,
Andrii Nakryiko
Cc: linux-perf-users, linux-kernel, Alireza Haghdoost
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. Add focused coverage for
skip-status preservation and truncated string-table reads in the lazy
materialization path.
Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
---
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 | 58 +++++
3 files changed, 362 insertions(+)
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 e5e2e6b9db97..0c9424fe4d1c 100644
--- a/tools/perf/tests/symbol-bytes.c
+++ b/tools/perf/tests/symbol-bytes.c
@@ -172,6 +172,63 @@ static int test__symbol_bytes_duplicate_selection(struct test_suite *test __mayb
}
#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;
+}
+
static int test__symbol_bytes_lazy_name_lookup(struct test_suite *test __maybe_unused,
int subtest __maybe_unused)
{
@@ -271,6 +328,7 @@ 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),
#endif
{ .name = NULL, }
--
Git-157)
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard
2026-09-20 2:33 ` [PATCH v2 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard Alireza Haghdoost via B4 Relay
@ 2026-09-20 23:52 ` Namhyung Kim
0 siblings, 0 replies; 10+ messages in thread
From: Namhyung Kim @ 2026-09-20 23:52 UTC (permalink / raw)
To: haghdoost
Cc: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Mark Rutland, Alexander Shishkin, Jiri Olsa, Ian Rogers,
Adrian Hunter, James Clark, Alexei Starovoitov, Andrii Nakryiko,
linux-perf-users, linux-kernel
Hello,
On Sat, Sep 19, 2026 at 07:33:56PM -0700, Alireza Haghdoost via B4 Relay wrote:
> From: Alireza Haghdoost <haghdoost@uber.com>
>
> Commit 22dd1ac91a77 ("tools: Remove feature-libelf-mmap feature
> detection") replaced perf's compile-time feature test with an #ifdef on
> ELF_C_READ_MMAP. ELF_C_READ_MMAP is an Elf_Cmd enumerator rather than a
> preprocessor macro, so the condition is always false and perf silently
> uses ELF_C_READ.
>
> Perf already requires a sufficiently recent elfutils version that
> provides ELF_C_READ_MMAP. Use the enumerator directly instead of
> restoring a feature probe or retaining an unreachable fallback.
>
> Fixes: 22dd1ac91a77 ("tools: Remove feature-libelf-mmap feature detection")
> Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
Reviewed-by: Namhyung Kim <namhyung@kernel.org>
Thanks,
Namhyung
> ---
> tools/perf/util/symbol.h | 10 +---------
> 1 file changed, 1 insertion(+), 9 deletions(-)
>
> diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
> index d0bac824c79c..46b1649c64fc 100644
> --- a/tools/perf/util/symbol.h
> +++ b/tools/perf/util/symbol.h
> @@ -57,15 +57,7 @@ static inline bool is_livepatch_symbol(const char *str)
> return strstarts(str, KLP_SYM_PREFIX);
> }
>
> -/*
> - * libelf 0.8.x and earlier do not support ELF_C_READ_MMAP;
> - * for newer versions we can use mmap to reduce memory usage:
> - */
> -#ifdef ELF_C_READ_MMAP
> -# define PERF_ELF_C_READ_MMAP ELF_C_READ_MMAP
> -#else
> -# define PERF_ELF_C_READ_MMAP ELF_C_READ
> -#endif
> +#define PERF_ELF_C_READ_MMAP ELF_C_READ_MMAP
>
> #ifdef HAVE_LIBELF_SUPPORT
> Elf_Scn *elf_section_by_name(Elf *elf, GElf_Ehdr *ep,
>
> --
> Git-157)
>
>
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory
2026-09-20 2:33 ` [PATCH v2 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory Alireza Haghdoost via B4 Relay
@ 2026-09-21 0:03 ` Namhyung Kim
2026-09-21 4:27 ` Alireza Haghdoost
0 siblings, 1 reply; 10+ messages in thread
From: Namhyung Kim @ 2026-09-21 0:03 UTC (permalink / raw)
To: haghdoost
Cc: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Mark Rutland, Alexander Shishkin, Jiri Olsa, Ian Rogers,
Adrian Hunter, James Clark, Alexei Starovoitov, Andrii Nakryiko,
linux-perf-users, linux-kernel
On Sat, Sep 19, 2026 at 07:33:57PM -0700, Alireza Haghdoost via B4 Relay wrote:
> From: Alireza Haghdoost <haghdoost@uber.com>
>
> perf script eagerly materializes every ELF symbol into an rb-tree kept
> until process exit. Large profiles can therefore consume substantial
> anonymous memory, causing perf script to be OOM-killed or forcing the
> kernel to reclaim memory from co-located workloads.
>
> This patch adds --max-symbol-bytes to bound struct symbol allocations.
> Once the budget is reached, the ELF loader stops loading symbols, warns
> once, and lets unresolved addresses appear as [unknown]. This allows
> users to bound the memory footprint upfront and explicitly choose between
> complete symbolization and avoiding unbounded host memory pressure. perf
> record already provides a similar --max-size option to bound disk usage.
>
> The counter includes every symbol__new() allocation, but this patch
> enforces the limit only in the ELF loader, which is the source of the
> unbounded memory growth addressed here. In this path, reaching the limit
> can safely produce [unknown] symbols. Other loaders currently treat a
> failed symbol allocation as an error. Capping those paths would therefore
> require separate changes whose complexity may outweigh the potential
> memory savings.
>
> The cap applies to userspace DSOs, vmlinux-as-ELF, and kernel modules.
> Sizes require a B/K/M/G suffix, except that a bare 0 and the default mean
> unlimited. Reservations are atomic so concurrent loaders cannot exceed the
> limit; accounting retains complete name lengths and partial zero-sized
> symbol ranges do not cover omitted addresses.
>
> Document the option with the code that introduces it and add focused
> accounting and concurrent-reservation tests.
>
> Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
> ---
[SNIP]
> diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
> index 46b1649c64fc..f7331edf0b71 100644
> --- a/tools/perf/util/symbol.h
> +++ b/tools/perf/util/symbol.h
> @@ -90,7 +90,7 @@ struct symbol {
> u64 start;
> u64 end;
> /** Length of the string name. */
> - u16 namelen;
> + u32 namelen;
Why is this needed? Do you have symbols with a really long name?
Anyway, it should be a separate change then.
Thanks,
Namhyung
> _Atomic uint16_t flags;
> /** Architecture specific. Unused except on PPC where it holds st_other. */
> u8 arch_sym;
> @@ -227,6 +227,12 @@ void symbol__elf_init(void);
> int symbol__annotation_init(void);
>
> struct symbol *symbol__new(u64 start, u64 len, u8 binding, u8 type, const char *name);
> +struct symbol *symbol__new_bounded(u64 start, u64 len, u8 binding, u8 type,
> + const char *name, bool *budget_exceeded);
> +size_t symbol__bytes_used(void);
> +void symbol__account_bytes(size_t bytes);
> +bool symbol__try_account_bytes(size_t bytes);
> +void symbol__unaccount_bytes(size_t bytes);
> size_t __symbol__fprintf_symname_offs(const struct symbol *sym,
> const struct addr_location *al,
> bool unknown_as_addr,
> diff --git a/tools/perf/util/symbol_conf.h b/tools/perf/util/symbol_conf.h
> index 71f60081a85b..6a16c5badd5e 100644
> --- a/tools/perf/util/symbol_conf.h
> +++ b/tools/perf/util/symbol_conf.h
> @@ -120,6 +120,7 @@ struct symbol_conf {
> int pad_output_len_dso;
> int group_sort_idx;
> int addr_range;
> + unsigned long max_symbol_bytes;
> DECLARE_BITMAP(parallelism_filter, MAX_NR_CPUS + 1);
> };
>
>
> --
> Git-157)
>
>
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading
2026-09-20 2:33 ` [PATCH v2 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading Alireza Haghdoost via B4 Relay
@ 2026-09-21 0:19 ` Namhyung Kim
2026-09-21 4:45 ` Alireza Haghdoost
0 siblings, 1 reply; 10+ messages in thread
From: Namhyung Kim @ 2026-09-21 0:19 UTC (permalink / raw)
To: haghdoost
Cc: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Mark Rutland, Alexander Shishkin, Jiri Olsa, Ian Rogers,
Adrian Hunter, James Clark, Alexei Starovoitov, Andrii Nakryiko,
linux-perf-users, linux-kernel
On Sat, Sep 19, 2026 at 07:33:58PM -0700, Alireza Haghdoost via B4 Relay wrote:
> From: Alireza Haghdoost <haghdoost@uber.com>
>
> perf script eagerly materializes eligible symbols from every DSO
> encountered in samples. On a production fixture, it loaded about 765k
> symbols to resolve about 45k distinct (DSO, symbol) frames, exceeding the
> memory available in a memory-constrained cgroup.
>
> This patch adds --lazy-load-symbols for userspace ELF DSOs. It builds a
> compact sorted index, resolves sampled addresses by binary search, reads
> symbol names through a private data-source DSO, and caches resolved symbols
> in the existing rb-tree. The private DSO uses the normal DSO data cache, so
> the exact split-debuginfo source can be reopened after descriptor eviction.
> If the source cannot be read during preflight, perf discards the index and
> eagerly loads that DSO instead.
>
> On the same fixture, peak RssAnon drops from 265 MiB to 39 MiB and wall
> time from 3.1 seconds to 1.85 seconds. Memory optimizations usually cost
> time; this one does not because lazy loading skips many unnecessary
> calloc() calls and demangling operations.
>
> Lazy loading is most effective when samples reference only a small
> fraction of the available symbols, such as profiles spanning many large
> DSOs. It still builds an index proportional to the total symbol count.
> Eager loading remains available for dense symbol coverage or cases
> requiring its broader ELF and architecture support.
>
> This does not claim full parity with the eager loader. Lazy loading
> supports the common userspace ELF symtab/dynsym case; .gnu_debugdata and
> PPC64 .opd continue through the eager loader.
>
> Materialization is serialized with the DSO lock. Name lookups materialize
> the remaining index before constructing the name-sorted array. Lazy loading
> shares eager duplicate and IFUNC selection, and clips ranges that cross
> .plt before synthesizing PLT symbols.
>
> Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
> ---
[SNIP]
> +static int dso__build_ondemand_index(struct dso *dso, struct symsrc *syms_ss,
> + struct symsrc *runtime_ss,
> + int dynsym)
> +{
> + struct dso_ondemand *od;
> + Elf *elf = syms_ss->elf;
> + GElf_Ehdr ehdr = syms_ss->ehdr;
> + GElf_Shdr shdr;
> + GElf_Shdr strshdr;
> + Elf_Scn *strscn, *sec_strndx;
> + Elf_Data *syms;
> + GElf_Sym sym;
> + Elf_Data *secstrs = NULL;
> + size_t i, index_bytes, reservation_peak;
> + u32 count = 0, j;
> + u32 *name_offsets;
> + u64 nr_entries, strtab_offset;
> + u64 probe_off;
> + u8 probe;
> +
> + /*
> + * GNU debugdata is backed by a temporary decompressed fd rather than a
> + * reopenable source path. Keep using the eager loader for that case.
> + */
> + if (syms_ss->type == DSO_BINARY_TYPE__GNU_DEBUGDATA)
> + return 0;
> +
> + if (dynsym)
> + shdr = syms_ss->dynshdr;
> + else
> + shdr = syms_ss->symshdr;
> +
> + syms = elf_getdata(dynsym ? syms_ss->dynsym : syms_ss->symtab, NULL);
> + if (!syms)
> + return -1;
> +
> + if (!shdr.sh_entsize)
> + return 0;
> +
> + nr_entries = shdr.sh_size / shdr.sh_entsize;
> + if (nr_entries > UINT32_MAX)
> + return -EOVERFLOW;
> +
> + strscn = elf_getscn(elf, shdr.sh_link);
> + if (!strscn || !gelf_getshdr(strscn, &strshdr))
> + return -1;
> + strtab_offset = strshdr.sh_offset;
> +
> + /*
> + * Section name string table, used to match the eager path's
> + * elf_sec__filter() (text/data section check for STT_NOTYPE labels).
> + */
> + sec_strndx = elf_getscn(elf, ehdr.e_shstrndx);
> + if (sec_strndx)
> + secstrs = elf_getdata(sec_strndx, NULL);
> +
> + for (i = 0; i < nr_entries; i++) {
> + if (!gelf_getsym(syms, i, &sym))
> + continue;
> + if (ondemand_sym_ok(elf, secstrs, &sym, shdr.sh_link,
> + ehdr.e_machine))
> + count++;
> + }
> +
> + if (!count)
> + return 0;
> + if (check_mul_overflow((size_t)count, sizeof(*od->sorted),
> + &index_bytes))
> + return -EOVERFLOW;
> +
> + /*
> + * Account the index against the symbol memory budget: at 24
> + * bytes/symbol it is the dominant on-demand cost and must count
> + * toward --max-symbol-bytes just like struct symbol allocations do.
> + */
> + if (!symbol__try_account_bytes(index_bytes)) {
> + symbol_budget_warning();
> + return 0; /* fall back to the eager loader's per-symbol budget */
> + }
> + reservation_peak = symbol__bytes_used();
> +
> + od = zalloc(sizeof(*od));
> + if (!od) {
> + symbol__unaccount_bytes(index_bytes);
> + return -1;
> + }
> +
> + od->sorted = zalloc(index_bytes);
> + if (!od->sorted) {
> + symbol__unaccount_bytes(index_bytes);
> + free(od);
> + return -1;
> + }
> + od->nr_alloc = count; /* allocated; the deduped count may shrink */
> + name_offsets = malloc(count * sizeof(*name_offsets));
> + if (!name_offsets) {
> + symbol__unaccount_bytes(index_bytes);
> + free(od->sorted);
> + free(od);
> + return -1;
> + }
> +
> + j = 0;
> + for (i = 0; i < nr_entries; i++) {
> + u64 adjusted;
> + GElf_Phdr phdr;
> +
> + if (!gelf_getsym(syms, i, &sym))
> + continue;
> + if (!ondemand_sym_ok(elf, secstrs, &sym, shdr.sh_link,
> + ehdr.e_machine))
> + continue;
> +
> + adjusted = sym.st_value;
> +
> + if ((ehdr.e_machine == EM_ARM) &&
> + (GELF_ST_TYPE(sym.st_info) == STT_FUNC) &&
> + (adjusted & 1))
> + --adjusted;
> +
> + /*
> + * Program header adjustment, identical to the eager loop:
> + * read the PT_LOAD containing the symbol from the runtime
> + * ELF (the debug-info file may have zeroed p_offset), and
> + * fall back to the section-header bias when no program
> + * header matches -- exactly what the eager path does when
> + * elf_read_program_header fails.
> + */
> + if (elf_read_program_header(runtime_ss->elf, adjusted,
> + &phdr) == 0) {
> + adjusted -= phdr.p_vaddr - phdr.p_offset;
> + } else {
> + Elf_Scn *sym_sec = elf_getscn(elf, sym.st_shndx);
> + GElf_Shdr sym_shdr;
> +
> + if (sym_sec && gelf_getshdr(sym_sec, &sym_shdr))
> + adjusted -= sym_shdr.sh_addr - sym_shdr.sh_offset;
> + }
> +
> + od->sorted[j].start = adjusted;
> + od->sorted[j].end = sym.st_size; /* st_size for now, converted later */
> + /*
> + * Sort equal-start aliases in original symtab order to match
> + * rb-tree insertion order. Restore st_name after sorting.
> + */
> + name_offsets[j] = sym.st_name;
> + od->sorted[j].name_off = j;
> + od->sorted[j].binding = GELF_ST_BIND(sym.st_info);
> + od->sorted[j].type = GELF_ST_TYPE(sym.st_info);
> + j++;
> + }
> + count = j;
> + if (!count) {
> + symbol__unaccount_bytes(index_bytes);
> + free(name_offsets);
> + free(od->sorted);
> + free(od);
> + return 0;
> + }
> +
> + qsort(od->sorted, count, sizeof(*od->sorted), cmp_sym_idx);
> + for (i = 0; i < count; i++)
> + od->sorted[i].name_off = name_offsets[od->sorted[i].name_off];
> + free(name_offsets);
> +
> + /*
> + * Match the eager loader's ordering: fill zero-sized ranges before
> + * choosing among equal-start aliases, so the size preference sees
> + * the same synthesized lengths as symbols__fixup_duplicate().
> + */
> + for (i = 0; i < count; i++) {
> + u64 size = od->sorted[i].end; /* was st_size */
> +
> + if (size > 0)
> + od->sorted[i].end = od->sorted[i].start + size;
> + else if (i + 1 < count)
> + od->sorted[i].end = od->sorted[i + 1].start;
> + else
> + od->sorted[i].end = roundup(od->sorted[i].start, 4096) + 4096;
> + }
> +
> + if (!symbol_conf.allow_aliases) {
> + u32 out = 0;
I think we agreed to factor out this block of code.
> +
> + for (i = 0; i < count; i++) {
> + u32 best = i;
> + const char *na = NULL, *nb;
> + char *da = NULL, *db;
> + bool has_ifunc = od->sorted[i].type == STT_GNU_IFUNC;
> +
> + na = elf_strptr(elf, shdr.sh_link,
> + od->sorted[best].name_off);
> + if (na) {
> + da = dso__demangle_sym(dso, 0, na);
> + if (da)
> + na = da;
> + }
> +
> + for (j = i + 1; j < count &&
> + od->sorted[j].start == od->sorted[i].start; j++) {
> + int choice;
> +
> + has_ifunc |= od->sorted[j].type == STT_GNU_IFUNC;
> + nb = elf_strptr(elf, shdr.sh_link,
> + od->sorted[j].name_off);
> + if (!na || !nb)
> + continue;
> +
> + db = dso__demangle_sym(dso, 0, nb);
> + if (db)
> + nb = db;
> +
> + choice = symbol__choose_best(
> + od->sorted[best].end -
> + od->sorted[best].start,
> + od->sorted[best].type,
> + od->sorted[best].binding, na,
> + od->sorted[j].end -
> + od->sorted[j].start,
> + od->sorted[j].type,
> + od->sorted[j].binding, nb);
> + if (choice == SYMBOL_B) {
> + best = j;
> + free(da);
> + da = db;
> + na = nb;
> + } else {
> + free(db);
> + }
> + }
> +
> + free(da);
> + od->sorted[out++] = od->sorted[best];
> + if (has_ifunc && od->sorted[out - 1].type != STT_GNU_IFUNC)
> + od->sorted[out - 1].flags |= SYM_IDX_FLAG_IFUNC_ALIAS;
> + i = j - 1; /* skip past all aliases of this start */
> + }
> +
> + if (out < count) {
> + struct sym_idx *shrunk;
> +
> + shrunk = realloc(od->sorted, out * sizeof(*od->sorted));
> + if (shrunk) {
> + od->sorted = shrunk;
> + symbol__unaccount_bytes((od->nr_alloc - out) *
> + sizeof(*od->sorted));
> + od->nr_alloc = out;
> + }
> + }
> + count = out;
> + }
> +
> + /*
> + * The interval binary search requires non-overlapping ranges. In
> + * the default deduplicated mode, prefer the symbol with the nearest
> + * preceding start when an ELF st_size overlaps the next symbol.
> + */
> + if (!symbol_conf.allow_aliases) {
> + for (i = 0; i + 1 < count; i++) {
> + if (od->sorted[i].end > od->sorted[i + 1].start)
> + od->sorted[i].end = od->sorted[i + 1].start;
> + }
> + }
> +
> + /*
> + * Keep an exact-path data DSO for the symbol source. This may differ
> + * from the runtime image (for example, split debuginfo), so using the
> + * primary DSO's data cache could read an unrelated string-table offset.
> + * The standard DSO data cache manages descriptor eviction and reopening.
> + */
Yes, it's a known problem and I hope to address it soon. Can you please
make the path handling a separate commit? I think it's an independent
fix for split debuginfo.
> + od->data_dso = dso__new(syms_ss->name);
> + if (!od->data_dso ||
> + dso__data_set_path(od->data_dso, syms_ss->name) < 0)
> + goto out_decline_source;
> + dso__set_binary_type(od->data_dso, DSO_BINARY_TYPE__SYSTEM_PATH_DSO);
> + dso__set_nsinfo(od->data_dso, nsinfo__get(dso__nsinfo(dso)));
> +
> + /*
> + * Open the managed source while eager fallback is still possible.
> + * A later failure would otherwise turn materialization into a miss.
> + */
> + if (od->sorted[0].name_off >= strshdr.sh_size)
> + goto out_decline_source;
> + if (check_add_overflow(strtab_offset,
> + (u64)od->sorted[0].name_off, &probe_off))
> + goto out_decline_source;
> + if (dso__data_read_offset(od->data_dso, NULL, probe_off, &probe, 1) != 1)
> + goto out_decline_source;
> +
> + od->strtab_offset = strtab_offset;
> + od->strtab_size = strshdr.sh_size;
> + od->nr_sorted = count;
> +
> + dso__set_ondemand(dso, od);
> +
> + pr_debug("%s: on-demand index: %u symbols (%zu bytes, %zu bytes total) budget=%zu\n",
> + dso__long_name(dso), count,
> + od->nr_alloc * sizeof(*od->sorted), symbol__bytes_used(),
> + reservation_peak);
> +
> + return 1;
> +
> +out_decline_source:
> + if (od->data_dso) {
> + dso__data_close(od->data_dso);
> + dso__put(od->data_dso);
> + }
> + symbol__unaccount_bytes(od->nr_alloc * sizeof(*od->sorted));
> + free(od->sorted);
> + free(od);
> + return 0;
> +}
[SNIP]
> diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
> index 32eef666f748..3ca9655c36fd 100644
> --- a/tools/perf/util/symbol.c
> +++ b/tools/perf/util/symbol.c
> @@ -143,72 +143,80 @@ int __weak arch__compare_symbol_names_n(const char *namea, const char *nameb,
> return strncmp(namea, nameb, n);
> }
>
> -int __weak arch__choose_best_symbol(struct symbol *syma,
> - struct symbol *symb __maybe_unused)
> +int __weak arch__choose_best_symbol(const char *syma_name)
> {
> /* Avoid "SyS" kernel syscall aliases */
> - if (strlen(syma->name) >= 3 && !strncmp(syma->name, "SyS", 3))
> + if (strlen(syma_name) >= 3 && !strncmp(syma_name, "SyS", 3))
> return SYMBOL_B;
> - if (strlen(syma->name) >= 10 && !strncmp(syma->name, "compat_SyS", 10))
> + if (strlen(syma_name) >= 10 && !strncmp(syma_name, "compat_SyS", 10))
> return SYMBOL_B;
>
> return SYMBOL_A;
> }
>
> -static int choose_best_symbol(struct symbol *syma, struct symbol *symb)
> +int symbol__choose_best(u64 a_size, u8 a_type, u8 a_binding,
> + const char *a_name,
> + u64 b_size, u8 b_type, u8 b_binding,
> + const char *b_name)
> {
> s64 a;
> s64 b;
> size_t na, nb;
>
> /* Prefer a symbol with non zero length */
> - a = syma->end - syma->start;
> - b = symb->end - symb->start;
> - if ((b == 0) && (a > 0))
> + if ((b_size == 0) && (a_size > 0))
> return SYMBOL_A;
> - else if ((a == 0) && (b > 0))
> + else if ((a_size == 0) && (b_size > 0))
> return SYMBOL_B;
>
> - if (symbol__type(syma) != symbol__type(symb)) {
> - if (symbol__type(syma) == STT_NOTYPE)
> + if (a_type != b_type) {
> + if (a_type == STT_NOTYPE)
> return SYMBOL_B;
> - if (symbol__type(symb) == STT_NOTYPE)
> + if (b_type == STT_NOTYPE)
> return SYMBOL_A;
> }
>
> /* Prefer a non weak symbol over a weak one */
> - a = symbol__binding(syma) == STB_WEAK;
> - b = symbol__binding(symb) == STB_WEAK;
> + a = a_binding == STB_WEAK;
> + b = b_binding == STB_WEAK;
> if (b && !a)
> return SYMBOL_A;
> if (a && !b)
> return SYMBOL_B;
>
> /* Prefer a global symbol over a non global one */
> - a = symbol__binding(syma) == STB_GLOBAL;
> - b = symbol__binding(symb) == STB_GLOBAL;
> + a = a_binding == STB_GLOBAL;
> + b = b_binding == STB_GLOBAL;
> if (a && !b)
> return SYMBOL_A;
> if (b && !a)
> return SYMBOL_B;
>
> /* Prefer a symbol with less underscores */
> - a = prefix_underscores_count(syma->name);
> - b = prefix_underscores_count(symb->name);
> + a = prefix_underscores_count(a_name);
> + b = prefix_underscores_count(b_name);
> if (b > a)
> return SYMBOL_A;
> else if (a > b)
> return SYMBOL_B;
>
> /* Choose the symbol with the longest name */
> - na = strlen(syma->name);
> - nb = strlen(symb->name);
> + na = strlen(a_name);
> + nb = strlen(b_name);
> if (na > nb)
> return SYMBOL_A;
> else if (na < nb)
> return SYMBOL_B;
>
> - return arch__choose_best_symbol(syma, symb);
> + return arch__choose_best_symbol(a_name);
> +}
> +
> +static int choose_best_symbol(struct symbol *syma, struct symbol *symb)
> +{
> + return symbol__choose_best(syma->end - syma->start,
> + symbol__type(syma), symbol__binding(syma), syma->name,
> + symb->end - symb->start,
> + symbol__type(symb), symbol__binding(symb), symb->name);
> }
To reduce the size of the patch, I think it's better to split this as a
separate commit. Please consider minimize the patch size in general to
help reviewers. :)
Thanks,
Namhyung
>
> void symbols__fixup_duplicate(struct rb_root_cached *symbols)
> @@ -1989,11 +1997,19 @@ int dso__load(struct dso *dso, struct map *map)
> }
>
> #ifdef HAVE_LIBBFD_SUPPORT
> +#ifdef HAVE_LIBELF_SUPPORT
> + if (is_reg && !symbol_conf.lazy_load_symbols)
> +#else
> if (is_reg)
> +#endif
> bfdrc = dso__load_bfd_symbols(dso, name);
> #endif
> if (is_reg && bfdrc < 0)
> sirc = symsrc__init(ss, dso, name, symtab_type);
> +#if defined(HAVE_LIBBFD_SUPPORT) && defined(HAVE_LIBELF_SUPPORT)
> + if (is_reg && symbol_conf.lazy_load_symbols && sirc < 0)
> + bfdrc = dso__load_bfd_symbols(dso, name);
> +#endif
>
> if (nsexit)
> nsinfo__mountns_enter(dso__nsinfo(dso), &nsc);
> diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
> index f7331edf0b71..99563c9ff610 100644
> --- a/tools/perf/util/symbol.h
> +++ b/tools/perf/util/symbol.h
> @@ -233,6 +233,10 @@ size_t symbol__bytes_used(void);
> void symbol__account_bytes(size_t bytes);
> bool symbol__try_account_bytes(size_t bytes);
> void symbol__unaccount_bytes(size_t bytes);
> +int symbol__choose_best(u64 a_size, u8 a_type, u8 a_binding,
> + const char *a_name,
> + u64 b_size, u8 b_type, u8 b_binding,
> + const char *b_name);
> size_t __symbol__fprintf_symname_offs(const struct symbol *sym,
> const struct addr_location *al,
> bool unknown_as_addr,
> @@ -308,7 +312,7 @@ const char *arch__normalize_symbol_name(const char *name);
> int arch__compare_symbol_names(const char *namea, const char *nameb);
> int arch__compare_symbol_names_n(const char *namea, const char *nameb,
> unsigned int n);
> -int arch__choose_best_symbol(struct symbol *syma, struct symbol *symb);
> +int arch__choose_best_symbol(const char *syma_name);
>
> enum symbol_tag_include {
> SYMBOL_TAG_INCLUDE__NONE = 0,
> diff --git a/tools/perf/util/symbol_conf.h b/tools/perf/util/symbol_conf.h
> index 6a16c5badd5e..0f8d044eba20 100644
> --- a/tools/perf/util/symbol_conf.h
> +++ b/tools/perf/util/symbol_conf.h
> @@ -74,6 +74,7 @@ struct symbol_conf {
> no_buildid_mmap2,
> guest_code,
> lazy_load_kernel_maps,
> + lazy_load_symbols,
> keep_exited_threads,
> annotate_data_member,
> annotate_data_sample,
>
> --
> Git-157)
>
>
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory
2026-09-21 0:03 ` Namhyung Kim
@ 2026-09-21 4:27 ` Alireza Haghdoost
0 siblings, 0 replies; 10+ messages in thread
From: Alireza Haghdoost @ 2026-09-21 4:27 UTC (permalink / raw)
To: Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Mark Rutland, Alexander Shishkin, Jiri Olsa, Ian Rogers,
Adrian Hunter, James Clark, Alexei Starovoitov, Andrii Nakryiko,
linux-perf-users, linux-kernel
> > @@ -90,7 +90,7 @@ struct symbol {
> > u64 start;
> > u64 end;
> > /** Length of the string name. */
> > - u16 namelen;
> > + u32 namelen;
>
> Why is this needed? Do you have symbols with a really long name?
> Anyway, it should be a separate change then.
>
I will drop this change in the v3. This is a theoretical edge case
and I don’t have a real workload
that hits it.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading
2026-09-21 0:19 ` Namhyung Kim
@ 2026-09-21 4:45 ` Alireza Haghdoost
0 siblings, 0 replies; 10+ messages in thread
From: Alireza Haghdoost @ 2026-09-21 4:45 UTC (permalink / raw)
To: Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Mark Rutland, Alexander Shishkin, Jiri Olsa, Ian Rogers,
Adrian Hunter, James Clark, Alexei Starovoitov, Andrii Nakryiko,
linux-perf-users, linux-kernel
> > +
> > + if (!symbol_conf.allow_aliases) {
> > + u32 out = 0;
>
> I think we agreed to factor out this block of code.
>
You’re right. I interpreted the earlier request too narrowly and only
factored out the pairwise selection policy into symbol__choose_best().
I’ll factor out the complete block in v3.
> > +
> > + /*
> > + * Keep an exact-path data DSO for the symbol source. This may differ
> > + * from the runtime image (for example, split debuginfo), so using the
> > + * primary DSO's data cache could read an unrelated string-table offset.
> > + * The standard DSO data cache manages descriptor eviction and reopening.
> > + */
>
> Yes, it's a known problem and I hope to address it soon. Can you please
> make the path handling a separate commit? I think it's an independent
> fix for split debuginfo.
>
Ack. I’ll split the exact-path DSO data-cache support into a separate
split-debuginfo fix and have the lazy-loading patch build on top
of it in v3.
> > +static int choose_best_symbol(struct symbol *syma, struct symbol *symb)
> > +{
> > + return symbol__choose_best(syma->end - syma->start,
> > + symbol__type(syma), symbol__binding(syma), syma->name,
> > + symb->end - symb->start,
> > + symbol__type(symb), symbol__binding(symb), symb->name);
> > }
>
> To reduce the size of the patch, I think it's better to split this as a
> separate commit. Please consider minimize the patch size in general to
> help reviewers. :)
>
Ack. I’ll move the shared duplicate-selection refactoring into a
separate preparatory commit to keep the lazy-loading patch smaller in v3.
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-09-21 4:45 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-20 2:33 [PATCH v2 0/4] perf script: Bounded and lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-20 2:33 ` [PATCH v2 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard Alireza Haghdoost via B4 Relay
2026-09-20 23:52 ` Namhyung Kim
2026-09-20 2:33 ` [PATCH v2 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory Alireza Haghdoost via B4 Relay
2026-09-21 0:03 ` Namhyung Kim
2026-09-21 4:27 ` Alireza Haghdoost
2026-09-20 2:33 ` [PATCH v2 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-21 0:19 ` Namhyung Kim
2026-09-21 4:45 ` Alireza Haghdoost
2026-09-20 2:33 ` [PATCH v2 4/4] perf test: Test lazy symbol loading and symbol memory limits Alireza Haghdoost via B4 Relay
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®