* [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer
@ 2026-09-12 8:37 Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 01/10] trace: add lock-free stackmap for stack trace deduplication Li Pengfei
` (9 more replies)
0 siblings, 10 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Hi Steven, Masami, all,
This is v7 of the ftrace stackmap series, sent as a new thread.
Previous version:
https://lore.kernel.org/linux-trace-kernel/20260903132409.270195-1-lipengfei28@xiaomi.com/
The series adds stack trace deduplication to ftrace. When the
'stackmap' option is enabled alongside 'stacktrace', the ring buffer
stores a 4-byte stack_id instead of a full kernel stack trace. Stack
records are kept in a separate map and exported through tracefs.
The series is based on v7.2-rc4-102-g4539944e5151.
Motivation
==========
The target use case is long-duration, from-boot kernel tracing where
the same stacks recur enormously often and the bottleneck is ring
buffer space, not CPU.
For example, tracing slab allocation from boot for hours can help find
the allocation backtraces behind a later memory-usage peak. Today each
event carries a full kernel stack, usually 80-160 bytes. With a fixed
ring buffer, early history is overwritten quickly even though most
stacks repeat.
On the same slab workload and fixed per-CPU buffer, stackmap retained
2.17 times as many events and covered 1.85 times the time span:
retained events bytes/event time span
stackmap OFF 645,068 ~104 B 15.0 s
stackmap ON 1,397,741 ~48 B 27.7 s
2.17x 2.17x 1.85x
Both runs used the same kernel configuration, workload, per-CPU
ring-buffer size and tracing filters; only options/stackmap differed.
Retained events and time span were taken from the ring buffer contents
after the buffer had wrapped. These are workload-specific measurements,
not guaranteed ratios: the gain tracks how repetitive the stacks are.
Two questions for maintainers
=============================
These are deliberate security and ABI design choices rather than
unresolved implementation bugs, and I would rather have them settled
before this grows a userspace dependency.
1. stack_map_bin deliberately exports raw kernel instruction pointers,
adjusted by trace_adjust_address() and not sanitized by
kptr_restrict. It is mode 0440, and open() rejects access under
LOCKDOWN_TRACEFS. This mirrors the trust boundary already used by
trace_pipe_raw and available_filter_functions_addrs. Please confirm
that boundary is the one you want here, or say which of stricter
permissions, a capability check, or address sanitization you prefer.
2. A stack_id is only meaningful within one map generation. Reset
clears the map without touching the ring buffer, so an id recorded
earlier can become unresolved or resolve to a reused slot. The
32-bit id carries no generation tag, so a consumer cannot detect
this. Within a single generation no additional kernel ABI is needed:
the TRACE_STACK_ID records plus the stack_map_bin export already carry
enough to reconstruct the mapping, though trace-cmd/libtraceevent
still need userspace support to decode TRACE_STACK_ID and join it with
the export. Merging trace data across a reset would additionally
require a generation-tagged event or map history. I have documented
the single-generation rule rather than growing the ABI now; tell me if
you would rather have the generation in the ABI from the start.
Changes since v6
================
Masami, this version addresses your review by splitting the previous
three patches into ten focused patches and by changing the interfaces
as follows:
- Serialize the complete stack_map_stat sample with reset through
reader_sem.
- Replace the per-open binary payload snapshot with seq_file and
seq_write(). The per-open allocation is now a bitmap of at most
64 KiB at bits=18 rather than a roughly 130 MiB payload.
- Make stackmap_dump.py accept stdin, parse incrementally, and add
its own test target.
- Separate core, recording-path integration, statistics, binary
export, boot sizing, documentation, parser, and three selftests.
Additional changes made while validating the split:
- Return -ESTALE, not -EAGAIN, when reset invalidates a binary
iterator. seq_file reserves -EAGAIN for internal traversal retries.
- Fix binary record membership at open with a bitmap, so the header
count exactly matches the records emitted.
- Use a reader_sem-protected u64 generation and define normal EOF for
an old export that was completely buffered before reset.
- Saturate cross-CPU statistic sums and calculate the integer success
rate correctly across the full u64 range.
- Use per-CPU atomic_long_t success/drop counters so the NMI-capable
hot path uses native-long atomic operations without cross-CPU
cacheline contention. FTRACE_STACKMAP depends on
ARCH_HAVE_NMI_SAFE_CMPXCHG for that guarantee.
- Enter an explicit notrace RCU-sched read-side section in get_id()
before testing the reset flag, and drain both classes of admitted
readers with synchronize_rcu_tasks_rude() followed by
synchronize_rcu(). ftrace_stackmap_reset() now spells out why no
admitted reader can observe a stale flag and still run during the
memsets: such a reader would have to both end before and end after
the same grace period returns.
- Apply one open-time policy to all three tracefs files. stack_map,
stack_map_stat and stack_map_bin each call
tracing_check_open_get_tr(), so LOCKDOWN_TRACEFS and
tracing_disabled are enforced when the file is opened rather than
only when tracefs was populated, and each file pins the owning
trace array until release.
- Scope explicit-stack deduplication to the selected trace_array;
stackmap remains available only on the global trace array.
- Stream parser output with bounded top-N and JSON spooling; limit
addr2line work to 64 batches and 8192 addresses, sanitize terminal
control characters, and validate depths and framing strictly.
- Assert only deterministic properties in the selftests. An active
reset is required to be accepted; no counter sampled while a writer
runs is compared, because such a sample is a moving target. The map
is proven empty by a reset taken once the owned writer is stopped
and tracing is off, and an active reset is proven to start a new
generation by holding a binary fd across it and requiring ESTALE.
- Keep an owned writer alive across the reset/binary test, retry a
failed tracing disable as best effort, and use random UUIDs in
instance names to avoid PID-namespace collisions.
- Reject truncated or malformed zero-exit addr2line output while
preserving normal unresolved-address fallback.
- Finalize stackmap initialization as failed if the top-level tracefs
directory cannot be initialized, rather than leaving it pending.
- Document ftrace_stackmap.bits= in the canonical kernel parameter
reference and Kconfig, including its default, range, and upper-bound
memory cost, and state that allocation happens when the global map
is published rather than on first use.
Binary ABI and reset semantics
==============================
stack_map_bin is native-endian ABI version 1. Userspace detects byte
order from the magic value.
open() records the populated stack records in a bitmap and puts the
exact count in nr_stacks. Iteration emits exactly that record set. The
bitmap fixes membership, not payload values, so ref_count may change
before a selected record is written.
Reset clears the map only. It does not clear the ring buffer and is
allowed while tracing is active. Consequently, a stack_id already in
the ring buffer can become unresolved or can resolve to an unrelated
stack after slot reuse. Userspace must consume the trace before reset
when old ids need to remain meaningful.
seq_file can return bytes buffered by the current pass before noticing
reset. The generation is checked at the next seq pass; a stale iterator
then returns -ESTALE and must be reopened. If the complete old export
was already produced, subsequent reads return normal EOF instead. In
either case no record from the new generation is ever appended to that
fd's stream.
Test results
============
Final v7 candidate d0c09774629e, QEMU aarch64 virt:
- For each of the ten commits, kernel/trace built cleanly with
ARCH=arm64 and CONFIG_FTRACE_STACKMAP=y, with no compiler warnings
- checkpatch.pl --strict: no blocking findings
- tools/tracing parser unit tests (make -C tools/tracing check):
40/40 PASS
- stackmap_bin_test host selftest: PASS
- Host and static arm64 helper builds with -Werror: PASS
- Full arm64 Image build: PASS
- ftracetest: stackmap-basic, stackmap-reset and
stackmap-instance-gate PASS, no alerts
The parser was also checked against real kernel output rather than
synthetic data only: a guest running this tree produced a stack_map_bin
export with nr_stacks=3, and stackmap_dump.py parsed that byte stream
identically from a file and from stdin, and produced valid JSON with
matching record count, depths and ref counts.
The runtime tests cover active reset under an owned PID-filtered writer,
record reuse and post-reset resolution, map-only reset plus the binary
ABI, open-time membership refill, and top-level instance gating. No
KASAN matrix result is claimed for this tree; the reset synchronization
and the tracefs open paths changed after the last such run.
Known limitations
=================
- Per-instance stackmaps are not included. The option is restricted
to the global trace instance.
- Allocation is eager when the global stackmap is published: about
8 MiB at the default bits=14 and about 130 MiB at bits=18. This is
deliberate, so that the hot path never allocates once tracefs
initialization has completed, but the cost is resident even while
options/stackmap stays off.
- Deduplication is best-effort. Concurrent insertion can create
duplicate records and split ref_count; memory remains bounded.
- Reset can make ids already in the trace unresolved or misleading.
- The binary ABI is native-endian and currently version 1.
- Only kernel stacks are covered.
- trace-cmd/libtraceevent integration is left for follow-up.
Usage
=====
echo 1 > /sys/kernel/debug/tracing/options/stackmap
echo 1 > /sys/kernel/debug/tracing/options/stacktrace
Pengfei Li (10):
trace: add lock-free stackmap for stack trace deduplication
trace: use the stackmap from the ftrace stack recording path
trace: add stackmap statistics interface
trace: add stackmap binary export
trace: make the stackmap capacity settable on the kernel command line
Documentation: tracing: document the ftrace stackmap
tools/tracing: add a parser for the stackmap binary export
selftests/ftrace: add a stackmap basic functionality test
selftests/ftrace: add a stackmap reset and binary ABI test
selftests/ftrace: add a stackmap instance gating test
.../admin-guide/kernel-parameters.txt | 7 +
Documentation/trace/ftrace-stackmap.rst | 241 ++++
Documentation/trace/index.rst | 1 +
kernel/trace/Kconfig | 36 +
kernel/trace/Makefile | 1 +
kernel/trace/trace.c | 234 +++-
kernel/trace/trace.h | 16 +
kernel/trace/trace_entries.h | 15 +
kernel/trace/trace_functions_graph.c | 1 +
kernel/trace/trace_output.c | 23 +
kernel/trace/trace_selftest.c | 1 +
kernel/trace/trace_stackmap.c | 1176 +++++++++++++++++
kernel/trace/trace_stackmap.h | 55 +
tools/testing/selftests/ftrace/.gitignore | 1 +
tools/testing/selftests/ftrace/Makefile | 2 +-
.../selftests/ftrace/stackmap_bin_test.c | 383 ++++++
.../ftrace/test.d/ftrace/stackmap-basic.tc | 242 ++++
.../test.d/ftrace/stackmap-instance-gate.tc | 114 ++
.../ftrace/test.d/ftrace/stackmap-reset.tc | 174 +++
tools/tracing/Makefile | 17 +-
tools/tracing/stackmap_dump.py | 354 +++++
tools/tracing/tests/test_stackmap_dump.py | 498 +++++++
22 files changed, 3585 insertions(+), 7 deletions(-)
create mode 100644 Documentation/trace/ftrace-stackmap.rst
create mode 100644 kernel/trace/trace_stackmap.c
create mode 100644 kernel/trace/trace_stackmap.h
create mode 100644 tools/testing/selftests/ftrace/stackmap_bin_test.c
create mode 100644 tools/testing/selftests/ftrace/test.d/ftrace/stackmap-basic.tc
create mode 100644 tools/testing/selftests/ftrace/test.d/ftrace/stackmap-instance-gate.tc
create mode 100644 tools/testing/selftests/ftrace/test.d/ftrace/stackmap-reset.tc
create mode 100755 tools/tracing/stackmap_dump.py
create mode 100644 tools/tracing/tests/test_stackmap_dump.py
base-commit: 4539944e515183668109bdf4d0c3d7d228383d88
--
2.34.1
^ permalink raw reply [flat|nested] 11+ messages in thread
* [RFC PATCH v7 01/10] trace: add lock-free stackmap for stack trace deduplication
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 02/10] trace: use the stackmap from the ftrace stack recording path Li Pengfei
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Add ftrace_stackmap, a lock-free hash map that stores kernel stack
records so the ftrace ring buffer can reference a stack by a 4-byte id
instead of carrying all of its frames. This patch adds the map, its text
export and reset path; the recording path is converted separately.
The implementation follows tracing_map.c's non-blocking design:
- cmpxchg-based insertion is usable from NMI, IRQ and process context
- a preallocated element pool keeps allocation out of the hot path
- linear probing uses a 2x table and a bounded probe length
- one map belongs to one trace_array
The Kconfig entry depends on ARCH_HAVE_NMI_SAFE_CMPXCHG because the hot
path is reachable from NMI context. Per-CPU atomic_long_t counters avoid
cross-CPU cacheline contention, use native-long operations instead of the
generic atomic64_t spinlock fallback on 32-bit systems, and saturate
rather than wrap.
Capacity is fixed here at 2^14 stack records, using about 8 MB for the
element pool. Concurrent insertion races can create duplicate records,
so this is not a strict unique-stack count. Boot-time sizing is added
separately.
ftrace_stackmap_get_id() rejects stacks deeper than 64 frames instead
of truncating them. entry->val uses release/acquire publication and
entry->key is read with READ_ONCE(). Claimed slots without an element
remain bounded gravestones when the pool is exhausted.
Reset clears only map-owned storage. It leaves the ring buffer intact
and can run while tracing is active, so older stack ids may stop
resolving or may resolve to a reused slot. Every valid get_id() call
enters an explicit rcu_read_lock_sched_notrace() section before checking
the resetting flag. Callers that observe an active reset leave without
touching map storage; admitted callers are drained before storage is
cleared. synchronize_rcu_tasks_rude() covers RCU-not-watching tracing
contexts, synchronize_rcu() covers the explicit RCU-sched sections,
including interrupt and NMI handlers, and reader_sem serializes tracefs
readers against clearing.
The text seq_file resolves adjusted addresses, renders the ftrace
trampoline marker, and accepts "0" or "reset" to clear the map.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
kernel/trace/Kconfig | 23 ++
kernel/trace/Makefile | 1 +
kernel/trace/trace_stackmap.c | 718 ++++++++++++++++++++++++++++++++++
kernel/trace/trace_stackmap.h | 34 ++
4 files changed, 776 insertions(+)
create mode 100644 kernel/trace/trace_stackmap.c
create mode 100644 kernel/trace/trace_stackmap.h
diff --git a/kernel/trace/Kconfig b/kernel/trace/Kconfig
index 084f34dc6c9f..6c40535a46e1 100644
--- a/kernel/trace/Kconfig
+++ b/kernel/trace/Kconfig
@@ -412,6 +412,29 @@ config STACK_TRACER
Say N if unsure.
+config FTRACE_STACKMAP
+ bool "Ftrace stack map deduplication"
+ depends on TRACING
+ depends on STACKTRACE
+ depends on ARCH_HAVE_NMI_SAFE_CMPXCHG
+ select KALLSYMS
+ select TASKS_RUDE_RCU
+ help
+ This enables a global stack trace hash table for ftrace, inspired
+ by eBPF's BPF_MAP_TYPE_STACK_TRACE. When enabled, ftrace can store
+ only a stack_id in the ring buffer instead of the full stack trace,
+ significantly reducing trace buffer usage when the same call stacks
+ appear repeatedly.
+
+ The deduplicated stacks are exported via:
+ /sys/kernel/debug/tracing/stack_map
+
+ Writing to this file resets the stack map. Reading shows all stack
+ records with their stack_id and reference count.
+
+ Say Y if you want to reduce ftrace buffer usage for stack traces.
+ Say N if unsure.
+
config TRACE_PREEMPT_TOGGLE
bool
help
diff --git a/kernel/trace/Makefile b/kernel/trace/Makefile
index f934ff586bd4..7710ec2659e9 100644
--- a/kernel/trace/Makefile
+++ b/kernel/trace/Makefile
@@ -86,6 +86,7 @@ obj-$(CONFIG_HWLAT_TRACER) += trace_hwlat.o
obj-$(CONFIG_OSNOISE_TRACER) += trace_osnoise.o
obj-$(CONFIG_NOP_TRACER) += trace_nop.o
obj-$(CONFIG_STACK_TRACER) += trace_stack.o
+obj-$(CONFIG_FTRACE_STACKMAP) += trace_stackmap.o
obj-$(CONFIG_MMIOTRACE) += trace_mmiotrace.o
obj-$(CONFIG_FUNCTION_GRAPH_TRACER) += trace_functions_graph.o
obj-$(CONFIG_TRACE_BRANCH_PROFILING) += trace_branch.o
diff --git a/kernel/trace/trace_stackmap.c b/kernel/trace/trace_stackmap.c
new file mode 100644
index 000000000000..b2e2115a15f9
--- /dev/null
+++ b/kernel/trace/trace_stackmap.c
@@ -0,0 +1,718 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Ftrace Stack Map - Lock-free stack trace deduplication for ftrace
+ *
+ * Modeled after tracing_map.c (used by hist triggers), this provides
+ * a lock-free hash map optimized for the ftrace hot path. The design
+ * is based on Dr. Cliff Click's non-blocking hash table algorithm.
+ *
+ * Key properties:
+ * - Lock-free insert via cmpxchg, safe in NMI/IRQ/any context
+ * - Pre-allocated element pool (zero allocation on hot path)
+ * - Linear probing with 2x over-provisioned table; probe length
+ * bounded by FTRACE_STACKMAP_MAX_PROBE to keep worst-case lookup
+ * cost constant even when the table is heavily loaded
+ * - Single global instance (initialized for the global trace array)
+ *
+ * Reset is a control-path operation that clears the map only. It
+ * does not touch the ring buffer and does not require tracing to be
+ * stopped. The protocol is:
+ *
+ * - Every admitted get_id() operation enters an explicit notrace
+ * RCU-sched read-side section before checking resetting. A caller
+ * that observes resetting=1 returns -EINVAL.
+ * - synchronize_rcu_tasks_rude() drains callers running where RCU is
+ * not watching on architectures that permit tracing there. A normal
+ * RCU grace period then drains the explicit read-side sections in
+ * the RCU-watching tracing path, including interrupt and NMI handlers.
+ *
+ * A trace collected before a reset can therefore still contain
+ * TRACE_STACK_ID records whose id no longer resolves, or resolves to
+ * a slot that has since been reused. That is misleading userspace
+ * output, not corruption; see ftrace_stackmap_reset().
+ *
+ * The 32-bit jhash of the stack IPs is the hash table key. On hash
+ * collision, linear probing finds the next slot and full memcmp
+ * confirms the match.
+ *
+ * Concurrent userspace readers (cat stack_map) get a best-effort
+ * snapshot. They are coherent with the hot path (smp_load_acquire on
+ * entry->val); they are also serialized against reset via
+ * smap->reader_sem (readers take it in shared mode, reset in
+ * exclusive mode), so a reset cannot tear a single seq_file pass --
+ * it waits for the active read() pass to drop the rwsem before clearing
+ * the map. The hot path is coordinated with reset separately, via
+ * acquire/release on smap->resetting.
+ */
+
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/jhash.h>
+#include <linux/seq_file.h>
+#include <linux/kallsyms.h>
+#include <linux/vmalloc.h>
+#include <linux/atomic.h>
+#include <linux/percpu.h>
+#include <linux/random.h>
+#include <linux/rcupdate.h>
+#include <linux/log2.h>
+#include <asm/local.h>
+
+#include "trace.h"
+#include "trace_stackmap.h"
+
+/*
+ * Bound the linear-probe scan length. With a 2x over-provisioned table,
+ * a well-distributed hash gives very short probe chains. Capping at 64
+ * keeps worst-case lookup O(1) even when the table is heavily loaded
+ * with claimed-but-empty slots from pool exhaustion.
+ */
+#define FTRACE_STACKMAP_MAX_PROBE 64
+
+/*
+ * Memory ordering of entry->val: published with smp_store_release()
+ * by the inserter; consumed with smp_load_acquire() by every reader
+ * that dereferences the elt (get_id, seq_show). This pairs
+ * the writes to elt->{nr,ips,ref_count} (initialized BEFORE the
+ * publish) with the reads of those fields (which happen AFTER the
+ * load). seq_start / seq_next only test val for NULL and use the
+ * acquire load purely to keep memory ordering symmetric.
+ */
+
+/*
+ * Each pre-allocated element holds one stack trace record.
+ * Fixed size: MAX_DEPTH entries regardless of actual depth.
+ */
+struct stackmap_elt {
+ u32 nr; /* actual number of IPs */
+ atomic_t ref_count;
+ unsigned long ips[FTRACE_STACKMAP_MAX_DEPTH];
+};
+
+/*
+ * Hash table entry: a 32-bit key (jhash of stack) + pointer to elt.
+ * key == 0 means the slot is free.
+ */
+struct stackmap_entry {
+ u32 key; /* 0 = free, non-zero = jhash */
+ struct stackmap_elt *val; /* NULL until fully published */
+};
+
+static struct stackmap_elt *stackmap_load_elt(struct stackmap_entry *entry)
+{
+ /*
+ * Pairs with the smp_store_release() that publishes entry->val
+ * after fully initializing the element payload.
+ */
+ return smp_load_acquire(&entry->val);
+}
+
+struct ftrace_stackmap {
+ struct trace_array *tr; /* owning trace_array */
+ unsigned int map_bits;
+ unsigned int map_size; /* 1 << (map_bits + 1) */
+ unsigned int max_elts; /* 1 << map_bits */
+ u32 hash_seed; /* per-instance jhash seed */
+ atomic_t next_elt; /* index into elts pool */
+ struct stackmap_entry *entries; /* hash table */
+ struct stackmap_elt *elts; /* flat element pool */
+ atomic_t resetting;
+ /*
+ * Reader/reset serialization. Held in shared mode (read lock)
+ * across seq_file iteration; held in exclusive mode (write
+ * lock) by reset's clearing phase. The hot path (get_id) does
+ * not take this lock — it
+ * uses smp_load_acquire/smp_store_release on entry->val and
+ * the resetting flag for the lock-free protocol.
+ */
+ struct rw_semaphore reader_sem;
+ /*
+ * Per-CPU atomic-long counters keep cross-CPU contention out of the
+ * NMI-capable hot path. atomic_long_add_unless() uses the native long
+ * width, so 32-bit kernels do not enter the generic atomic64_t
+ * hashed-spinlock implementation. ARCH_HAVE_NMI_SAFE_CMPXCHG makes
+ * the saturating update safe against same-CPU NMI interruption.
+ */
+ atomic_long_t __percpu *successes; /* hits + new inserts */
+ atomic_long_t __percpu *drops;
+};
+
+/*
+ * Map capacity: 2^FTRACE_STACKMAP_BITS stack records, with the hash
+ * table over-provisioned 2x on top of that. This gives 16K elements
+ * and a ~8 MB element pool, sized for the repetitive-stack workloads
+ * the map targets.
+ */
+#define FTRACE_STACKMAP_BITS 14
+
+/* --- Element pool --- */
+
+static struct stackmap_elt *stackmap_get_elt(struct ftrace_stackmap *smap)
+{
+ int idx;
+
+ /*
+ * Fast-path early-out once the pool is fully consumed. Avoids
+ * the contended atomic RMW on next_elt for every traced event
+ * after the pool is exhausted.
+ */
+ if (atomic_read(&smap->next_elt) >= smap->max_elts)
+ return NULL;
+
+ idx = atomic_fetch_add_unless(&smap->next_elt, 1, smap->max_elts);
+ if (idx < smap->max_elts)
+ return &smap->elts[idx];
+ return NULL;
+}
+
+/* --- Create / Destroy / Reset --- */
+
+struct ftrace_stackmap *ftrace_stackmap_create(struct trace_array *tr)
+{
+ struct ftrace_stackmap *smap;
+ unsigned int bits = FTRACE_STACKMAP_BITS;
+
+ smap = kzalloc_obj(*smap, GFP_KERNEL);
+ if (!smap)
+ return ERR_PTR(-ENOMEM);
+
+ smap->tr = tr;
+ smap->map_bits = bits;
+ smap->max_elts = 1U << bits;
+ smap->map_size = 1U << (bits + 1); /* 2x over-provision */
+
+ smap->entries = vcalloc(smap->map_size, sizeof(*smap->entries));
+ if (!smap->entries)
+ goto fail;
+
+ /*
+ * Single large vmalloc of the element pool, indexed flat.
+ * At bits=18 this is 256K * sizeof(struct stackmap_elt). The
+ * struct is ~520 B (8 + 4 + 4 + 64*8), so total ~135 MB.
+ */
+ smap->elts = vcalloc(smap->max_elts, sizeof(*smap->elts));
+ if (!smap->elts)
+ goto fail;
+
+ smap->successes = alloc_percpu(atomic_long_t);
+ if (!smap->successes)
+ goto fail;
+ smap->drops = alloc_percpu(atomic_long_t);
+ if (!smap->drops)
+ goto fail;
+
+ smap->hash_seed = get_random_u32();
+ atomic_set(&smap->next_elt, 0);
+ atomic_set(&smap->resetting, 0);
+ init_rwsem(&smap->reader_sem);
+
+ return smap;
+
+fail:
+ /*
+ * free_percpu()/vfree()/kfree() all handle NULL, and smap was
+ * zero-initialized, so unwind in reverse allocation order.
+ */
+ free_percpu(smap->drops);
+ free_percpu(smap->successes);
+ vfree(smap->elts);
+ vfree(smap->entries);
+ kfree(smap);
+ return ERR_PTR(-ENOMEM);
+}
+
+void ftrace_stackmap_destroy(struct ftrace_stackmap *smap)
+{
+ if (!smap || IS_ERR(smap))
+ return;
+ free_percpu(smap->drops);
+ free_percpu(smap->successes);
+ vfree(smap->elts);
+ vfree(smap->entries);
+ kfree(smap);
+}
+
+/**
+ * ftrace_stackmap_reset - clear all entries in the stackmap
+ * @smap: the stackmap to reset
+ *
+ * Returns 0 on success, or -EBUSY if another reset is already in
+ * progress.
+ *
+ * Clears the map only. The ring buffer is left alone and tracing does
+ * not need to be stopped, so a trace can still contain TRACE_STACK_ID
+ * records after the map has been cleared. Such an id either has no
+ * entry in stack_map, or -- once tracing continues and the slot is
+ * reused -- resolves to an unrelated stack. Both are misleading
+ * userspace output rather than corruption: reset frees nothing, it
+ * only memsets storage the map still owns.
+ *
+ * Caller is process context (the tracefs write handler).
+ *
+ * Protocol:
+ * 1. Atomically claim reset rights via cmpxchg on @resetting, which
+ * makes subsequent get_id() callers leave their read-side section
+ * without touching map storage.
+ * 2. synchronize_rcu_tasks_rude() drains callers running where RCU is
+ * not watching on architectures that allow tracing there. Then
+ * synchronize_rcu() drains the explicit notrace RCU-sched sections
+ * in the RCU-watching tracing path, including interrupt and NMI
+ * handlers.
+ * 3. Take @reader_sem exclusively to exclude tracefs readers, then
+ * memset entries, elts, and counters.
+ * 4. Release the resetting flag with release semantics so any new
+ * get_id() observes a fully cleared map.
+ *
+ * Why no get_id() can run concurrently with the memsets, given that its
+ * admission test is a plain flag read rather than a lock. Consider any
+ * get_id() RCU-sched read-side section, and note that the flag store in
+ * step 1 precedes the grace period in step 2:
+ *
+ * - The section ends before synchronize_rcu() returns. The memsets run
+ * after it returns, so the two cannot overlap, whichever value of
+ * resetting that section happened to observe.
+ * - The section ends after synchronize_rcu() returns. RCU then
+ * guarantees a full memory barrier between the start of the grace
+ * period and the start of that section, so the section observes
+ * resetting == 1 and returns -EINVAL without touching map storage.
+ *
+ * A section that reads a stale zero and still runs during the memsets
+ * would have to satisfy both cases at once, so it cannot exist. See
+ * "Memory-Barrier Guarantees" in
+ * Documentation/RCU/Design/Requirements/Requirements.rst.
+ */
+static int ftrace_stackmap_reset(struct ftrace_stackmap *smap)
+{
+ int cpu;
+
+ if (!smap)
+ return 0;
+
+ if (atomic_cmpxchg(&smap->resetting, 0, 1) != 0)
+ return -EBUSY;
+
+ /*
+ * Each get_id() operation enters an explicit notrace RCU-sched
+ * read-side section before checking resetting. On RCU-watching CPUs,
+ * the normal grace period waits for those sections, including callers
+ * from interrupt and NMI context. On architectures that permit tracing
+ * while RCU is not watching, the rude grace period covers the remaining
+ * context; it is intentionally a no-op on ARCH_WANTS_NO_INSTR
+ * architectures.
+ */
+ synchronize_rcu_tasks_rude();
+ synchronize_rcu();
+
+ /*
+ * Take the reader_sem in exclusive mode. This serializes the
+ * memset against any tracefs reader (seq_file iteration) that
+ * may currently hold the rwsem for read. The Tasks RCU grace
+ * periods already drained the hot path; this rwsem covers
+ * process-context export readers.
+ */
+ down_write(&smap->reader_sem);
+
+ memset(smap->entries, 0, sizeof(*smap->entries) * smap->map_size);
+ memset(smap->elts, 0, sizeof(*smap->elts) * (size_t)smap->max_elts);
+
+ atomic_set(&smap->next_elt, 0);
+ for_each_possible_cpu(cpu) {
+ atomic_long_set(per_cpu_ptr(smap->successes, cpu), 0);
+ atomic_long_set(per_cpu_ptr(smap->drops, cpu), 0);
+ }
+
+ up_write(&smap->reader_sem);
+
+ /* Release resetting=0 so new get_id() observes a cleared map. */
+ atomic_set_release(&smap->resetting, 0);
+ return 0;
+}
+
+/* --- Core: get_id (lock-free, NMI-safe) --- */
+
+int ftrace_stackmap_get_id(struct ftrace_stackmap *smap,
+ unsigned long *ips, unsigned int nr_entries)
+{
+ u32 key_hash, idx, test_key, trace_len;
+ struct stackmap_entry *entry;
+ struct stackmap_elt *val;
+ int probes = 0;
+ int ret;
+
+ if (!smap || !nr_entries)
+ return -EINVAL;
+ /*
+ * Never truncate: a stack deeper than the map can hold must not be
+ * silently shortened, or two distinct traces sharing their first
+ * FTRACE_STACKMAP_MAX_DEPTH frames would be merged into one
+ * stack_id. The caller is expected to fall back to a full stack
+ * trace for such events. Reject defensively in case of a future
+ * caller that forgets this contract.
+ */
+ if (nr_entries > FTRACE_STACKMAP_MAX_DEPTH)
+ return -E2BIG;
+
+ /*
+ * Enter the read-side section before checking resetting. If reset
+ * has already claimed the map, reject the operation. Otherwise the
+ * reset grace periods wait until this operation leaves the section
+ * before clearing storage. The notrace variant is required because
+ * this is part of the tracing hot path.
+ */
+ rcu_read_lock_sched_notrace();
+ /*
+ * atomic_read_acquire() pairs with atomic_set_release() in the
+ * reset path. This ensures that subsequent reads of entry->key
+ * and entry->val are ordered after this check; without acquire,
+ * the CPU would only have a control dependency, which orders
+ * subsequent stores but not loads (per LKMM).
+ */
+ if (atomic_read_acquire(&smap->resetting)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ trace_len = nr_entries * sizeof(unsigned long);
+ /*
+ * jhash2() requires the length in u32 units and the data to be
+ * u32-aligned. On 64-bit kernels sizeof(unsigned long)==8, so
+ * trace_len is always a multiple of 8 (hence of 4). Use jhash2
+ * directly; the cast to u32* is safe because ips[] is naturally
+ * aligned to sizeof(unsigned long) >= 4.
+ */
+ key_hash = jhash2((const u32 *)ips, trace_len / sizeof(u32),
+ smap->hash_seed);
+ if (key_hash == 0)
+ key_hash = 1; /* 0 means free slot */
+
+ idx = key_hash >> (32 - (smap->map_bits + 1));
+
+ while (probes < FTRACE_STACKMAP_MAX_PROBE) {
+ idx &= (smap->map_size - 1);
+ entry = &smap->entries[idx];
+ /*
+ * READ_ONCE() to avoid LKMM data race with concurrent
+ * cmpxchg(&entry->key, 0, key_hash) on this slot.
+ */
+ test_key = READ_ONCE(entry->key);
+
+ if (test_key == key_hash) {
+ val = stackmap_load_elt(entry);
+ /*
+ * READ_ONCE(val->nr) keeps style consistent with
+ * the seq_show reader. nr is write-once
+ * (set before publish, never modified afterwards),
+ * so the load is data-race-free, but READ_ONCE
+ * silences any analysis tool that flags a plain
+ * read of a field that is also read under acquire
+ * elsewhere.
+ */
+ if (val && READ_ONCE(val->nr) == nr_entries &&
+ memcmp(val->ips, ips, trace_len) == 0) {
+ /*
+ * ref_count is a best-effort popularity
+ * counter. On a long (from-boot, multi-hour)
+ * trace a hot stack can be hit billions of
+ * times. atomic_add_unless() gives true
+ * saturation at INT_MAX even under concurrent
+ * hits on multiple CPUs (a plain
+ * check-then-inc could let several CPUs past
+ * the check near the cap and still wrap).
+ */
+ atomic_add_unless(&val->ref_count, 1, INT_MAX);
+ /*
+ * successes/drops are saturating throughput
+ * counters. Keep them per-CPU to avoid
+ * cross-CPU cacheline contention.
+ */
+ atomic_long_add_unless(
+ this_cpu_ptr(smap->successes), 1, LONG_MAX);
+ ret = (int)idx;
+ goto out;
+ }
+ /*
+ * val == NULL: another CPU is mid-insert, or this
+ * slot is "claimed but empty" (pool exhausted).
+ * val != NULL but mismatch: 32-bit hash collision
+ * with a different stack. In both cases, advance.
+ */
+ } else if (!test_key) {
+ /*
+ * Free slot: try to claim it.
+ *
+ * If two CPUs race here with the same key_hash
+ * (same stack), one loses the cmpxchg, advances,
+ * and may insert the same stack at a later slot.
+ * This can produce a small number of duplicate
+ * entries under heavy contention. The trade-off
+ * is accepted to keep the hot path lock-free;
+ * ref_count is split across the duplicates and
+ * total memory cost is bounded by the element
+ * pool size.
+ */
+ if (cmpxchg(&entry->key, 0, key_hash) == 0) {
+ struct stackmap_elt *elt;
+
+ elt = stackmap_get_elt(smap);
+ if (!elt) {
+ /*
+ * Pool exhausted. Keep the claimed
+ * slot as a gravestone. Readers skip
+ * a NULL val, and the explicit probe
+ * bound prevents accumulated
+ * gravestones from turning a miss
+ * into an unbounded table walk.
+ */
+ atomic_long_add_unless(
+ this_cpu_ptr(smap->drops), 1,
+ LONG_MAX);
+ ret = -ENOSPC;
+ goto out;
+ }
+
+ elt->nr = nr_entries;
+ atomic_set(&elt->ref_count, 1);
+ memcpy(elt->ips, ips, trace_len);
+
+ /*
+ * Publish elt with release semantics so the
+ * reader's smp_load_acquire can safely
+ * dereference val->nr / val->ips.
+ */
+ smp_store_release(&entry->val, elt);
+ atomic_long_add_unless(
+ this_cpu_ptr(smap->successes), 1, LONG_MAX);
+ ret = (int)idx;
+ goto out;
+ }
+ /* cmpxchg failed; another CPU claimed this slot. */
+ }
+
+ idx++;
+ probes++;
+ }
+
+ atomic_long_add_unless(this_cpu_ptr(smap->drops), 1, LONG_MAX);
+ ret = -ENOSPC;
+out:
+ rcu_read_unlock_sched_notrace();
+ return ret;
+}
+
+/* --- Text export: /sys/kernel/debug/tracing/stack_map --- */
+
+struct stackmap_seq_private {
+ struct ftrace_stackmap *smap;
+};
+
+static void *stackmap_seq_start(struct seq_file *m, loff_t *pos)
+{
+ struct stackmap_seq_private *priv = m->private;
+ struct ftrace_stackmap *smap = priv->smap;
+ loff_t i;
+
+ if (!smap)
+ return NULL;
+ /*
+ * Take the reader_sem to serialize against ftrace_stackmap_reset(),
+ * which holds it for write while clearing the table. Released in
+ * stackmap_seq_stop(), which seq_file calls regardless of whether
+ * start() returned an element or NULL (per Documentation/filesystems
+ * /seq_file.rst: "the iterator value returned by start() or next()
+ * is guaranteed to be passed to a subsequent next() or stop()").
+ */
+ down_read(&smap->reader_sem);
+ for (i = *pos; i < smap->map_size; i++) {
+ if (READ_ONCE(smap->entries[i].key) &&
+ stackmap_load_elt(&smap->entries[i])) {
+ *pos = i;
+ return &smap->entries[i];
+ }
+ }
+ return NULL;
+}
+
+static void *stackmap_seq_next(struct seq_file *m, void *v, loff_t *pos)
+{
+ struct stackmap_seq_private *priv = m->private;
+ struct ftrace_stackmap *smap = priv->smap;
+ loff_t i;
+
+ if (!smap)
+ return NULL;
+ for (i = *pos + 1; i < smap->map_size; i++) {
+ if (READ_ONCE(smap->entries[i].key) &&
+ stackmap_load_elt(&smap->entries[i])) {
+ *pos = i;
+ return &smap->entries[i];
+ }
+ }
+ /*
+ * Advance *pos past the end so that on the next read() the
+ * subsequent stackmap_seq_start() call returns NULL and the
+ * iteration terminates. Without this, seq_read() would loop
+ * on the last element.
+ */
+ *pos = smap->map_size;
+ return NULL;
+}
+
+static void stackmap_seq_stop(struct seq_file *m, void *v)
+{
+ struct stackmap_seq_private *priv = m->private;
+ struct ftrace_stackmap *smap = priv->smap;
+
+ /*
+ * seq_file invokes stop() unconditionally after each iteration
+ * pass (see seq_read_iter / traverse), even when start() returned
+ * NULL. Always release here, balanced against the down_read in
+ * stackmap_seq_start().
+ */
+ if (smap)
+ up_read(&smap->reader_sem);
+}
+
+static int stackmap_seq_show(struct seq_file *m, void *v)
+{
+ struct stackmap_entry *entry = v;
+ struct stackmap_seq_private *priv = m->private;
+ struct stackmap_elt *elt;
+ u32 idx = entry - priv->smap->entries;
+ u32 i, nr;
+
+ elt = stackmap_load_elt(entry);
+ if (!elt)
+ return 0;
+
+ nr = READ_ONCE(elt->nr);
+ if (nr > FTRACE_STACKMAP_MAX_DEPTH)
+ nr = FTRACE_STACKMAP_MAX_DEPTH;
+
+ seq_printf(m, "stack_id %u [ref %u, depth %u]\n",
+ idx, atomic_read(&elt->ref_count), nr);
+ for (i = 0; i < nr; i++) {
+ unsigned long ip = elt->ips[i];
+
+ /*
+ * Mirror trace_stack_print(): __ftrace_trace_stack()
+ * may replace trampoline addresses with
+ * FTRACE_TRAMPOLINE_MARKER before the stack reaches the
+ * map, and normal addresses must go through
+ * trace_adjust_address() (KASLR / module text delta)
+ * before symbolization. Without this the export would
+ * print a bogus symbol for the marker and unadjusted
+ * addresses for everything else.
+ */
+ if (ip == FTRACE_TRAMPOLINE_MARKER) {
+ seq_printf(m, " [%u] [FTRACE TRAMPOLINE]\n", i);
+ continue;
+ }
+ seq_printf(m, " [%u] %pS\n", i,
+ (void *)trace_adjust_address(priv->smap->tr, ip));
+ }
+ seq_putc(m, '\n');
+ return 0;
+}
+
+static const struct seq_operations stackmap_seq_ops = {
+ .start = stackmap_seq_start,
+ .next = stackmap_seq_next,
+ .stop = stackmap_seq_stop,
+ .show = stackmap_seq_show,
+};
+
+static int stackmap_open(struct inode *inode, struct file *file)
+{
+ struct ftrace_stackmap *smap = inode->i_private;
+ struct stackmap_seq_private *priv;
+ struct seq_file *m;
+ int ret;
+
+ if (!smap)
+ return -ENODEV;
+
+ /*
+ * The text export symbolizes recorded kernel text addresses and
+ * doubles as the reset control file, so it takes the same open-time
+ * tracing policy as the other stackmap files: reject the open under
+ * LOCKDOWN_TRACEFS or when tracing is globally disabled, and pin the
+ * owning trace array for the lifetime of the fd.
+ */
+ ret = tracing_check_open_get_tr(smap->tr);
+ if (ret)
+ return ret;
+
+ ret = seq_open_private(file, &stackmap_seq_ops,
+ sizeof(struct stackmap_seq_private));
+ if (ret)
+ goto put_tr;
+
+ m = file->private_data;
+ priv = m->private;
+ priv->smap = smap;
+ return 0;
+
+put_tr:
+ trace_array_put(smap->tr);
+ return ret;
+}
+
+static int stackmap_release(struct inode *inode, struct file *file)
+{
+ struct seq_file *m = file->private_data;
+ struct stackmap_seq_private *priv = m->private;
+ struct ftrace_stackmap *smap = priv->smap;
+ int ret;
+
+ ret = seq_release_private(inode, file);
+ trace_array_put(smap->tr);
+ return ret;
+}
+
+/*
+ * Accept exactly "0" or "reset" (optionally followed by a single newline).
+ */
+static bool stackmap_write_is_reset(const char *buf, size_t n)
+{
+ if (n > 0 && buf[n - 1] == '\n')
+ n--;
+ return (n == 1 && buf[0] == '0') ||
+ (n == 5 && memcmp(buf, "reset", 5) == 0);
+}
+
+static ssize_t stackmap_write(struct file *file, const char __user *ubuf,
+ size_t count, loff_t *ppos)
+{
+ struct seq_file *m = file->private_data;
+ struct stackmap_seq_private *priv = m->private;
+ char buf[8];
+ size_t n = min(count, sizeof(buf) - 1);
+ int ret;
+
+ if (n == 0)
+ return -EINVAL;
+ if (copy_from_user(buf, ubuf, n))
+ return -EFAULT;
+ buf[n] = '\0';
+
+ if (!stackmap_write_is_reset(buf, n))
+ return -EINVAL;
+
+ /*
+ * ftrace_stackmap_reset() atomically claims reset rights via
+ * cmpxchg and returns -EBUSY if another reset is already in
+ * progress.
+ */
+ ret = ftrace_stackmap_reset(priv->smap);
+ if (ret)
+ return ret;
+ return count;
+}
+
+const struct file_operations ftrace_stackmap_fops = {
+ .open = stackmap_open,
+ .read = seq_read,
+ .write = stackmap_write,
+ .llseek = seq_lseek,
+ .release = stackmap_release,
+};
diff --git a/kernel/trace/trace_stackmap.h b/kernel/trace/trace_stackmap.h
new file mode 100644
index 000000000000..979d6fd76460
--- /dev/null
+++ b/kernel/trace/trace_stackmap.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _TRACE_STACKMAP_H
+#define _TRACE_STACKMAP_H
+
+#include <linux/types.h>
+#include <linux/atomic.h>
+
+#define FTRACE_STACKMAP_MAX_DEPTH 64
+
+struct trace_array;
+
+#ifdef CONFIG_FTRACE_STACKMAP
+
+struct ftrace_stackmap;
+
+struct ftrace_stackmap *ftrace_stackmap_create(struct trace_array *tr);
+void ftrace_stackmap_destroy(struct ftrace_stackmap *smap);
+int ftrace_stackmap_get_id(struct ftrace_stackmap *smap,
+ unsigned long *ips, unsigned int nr_entries);
+
+extern const struct file_operations ftrace_stackmap_fops;
+
+#else
+
+struct ftrace_stackmap;
+static inline struct ftrace_stackmap *
+ftrace_stackmap_create(struct trace_array *tr) { return NULL; }
+static inline void ftrace_stackmap_destroy(struct ftrace_stackmap *s) { }
+static inline int ftrace_stackmap_get_id(struct ftrace_stackmap *s,
+ unsigned long *ips, unsigned int n)
+{ return -EOPNOTSUPP; }
+
+#endif
+#endif /* _TRACE_STACKMAP_H */
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 02/10] trace: use the stackmap from the ftrace stack recording path
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 01/10] trace: add lock-free stackmap for stack trace deduplication Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 03/10] trace: add stackmap statistics interface Li Pengfei
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Wire ftrace_stackmap into __ftrace_trace_stack(). With 'stackmap' and
'stacktrace' enabled, the recording path tries to place a 4-byte
stack_id in the ring buffer and stack_map resolves the id.
Reserve the stack-id ring-buffer slot before consulting the map. This
keeps map records and counters untouched when that reservation fails.
If get_id() then fails, discard the reserved slot. Both paths try the
existing full-stack fallback, whose own ring-buffer reservation can
still fail under normal ring-buffer semantics.
Stacks deeper than FTRACE_STACKMAP_MAX_DEPTH bypass the map so a
recorded stack is never silently truncated or merged with another stack
sharing the same prefix. Other map failures, including capacity,
reset, and an unpublished map, also try the full-stack path.
Confine the option to the global trace instance through
TOP_LEVEL_TRACE_FLAGS, ZEROED_TRACE_FLAGS and a set_tracer_flag()
check. This also rejects writes through a secondary instance's
aggregate trace_options file.
Create stack_map before publishing global_trace.stackmap. Boot-time
options may be set while initialization is pending; the recording path
uses full-stack fallback until publication. A permanent initialization
failure clears and rejects the option.
Also add TRACE_STACK_ID, its output handler, and validation needed by
ftrace startup selftests.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
kernel/trace/Kconfig | 9 ++
kernel/trace/trace.c | 218 ++++++++++++++++++++++++++-
kernel/trace/trace.h | 16 ++
kernel/trace/trace_entries.h | 15 ++
kernel/trace/trace_functions_graph.c | 1 +
kernel/trace/trace_output.c | 23 +++
kernel/trace/trace_selftest.c | 1 +
7 files changed, 280 insertions(+), 3 deletions(-)
diff --git a/kernel/trace/Kconfig b/kernel/trace/Kconfig
index 6c40535a46e1..7001453bbfd3 100644
--- a/kernel/trace/Kconfig
+++ b/kernel/trace/Kconfig
@@ -426,6 +426,15 @@ config FTRACE_STACKMAP
significantly reducing trace buffer usage when the same call stacks
appear repeatedly.
+ Automatic per-event stack capture requires the stacktrace option:
+
+ echo 1 > /sys/kernel/debug/tracing/options/stacktrace
+ echo 1 > /sys/kernel/debug/tracing/options/stackmap
+
+ Explicit stack records requested through tracing APIs are also
+ deduplicated when stackmap is enabled for their trace array. Stackmap
+ is currently available only for the global trace array.
+
The deduplicated stacks are exported via:
/sys/kernel/debug/tracing/stack_map
diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c
index 18710c190c92..06ae8ed11475 100644
--- a/kernel/trace/trace.c
+++ b/kernel/trace/trace.c
@@ -57,6 +57,7 @@
#include "trace.h"
#include "trace_output.h"
+#include "trace_stackmap.h"
#ifdef CONFIG_FTRACE_STARTUP_TEST
/*
@@ -509,12 +510,13 @@ EXPORT_SYMBOL_GPL(unregister_ftrace_export);
/* trace_options that are only supported by global_trace */
#define TOP_LEVEL_TRACE_FLAGS (TRACE_ITER(PRINTK) | \
TRACE_ITER(PRINTK_MSGONLY) | TRACE_ITER(RECORD_CMD) | \
- TRACE_ITER(PROF_TEXT_OFFSET) | FPROFILE_DEFAULT_FLAGS)
+ TRACE_ITER(PROF_TEXT_OFFSET) | TRACE_ITER(STACKMAP) | \
+ FPROFILE_DEFAULT_FLAGS)
/* trace_flags that are default zero for instances */
#define ZEROED_TRACE_FLAGS \
(TRACE_ITER(EVENT_FORK) | TRACE_ITER(FUNC_FORK) | TRACE_ITER(TRACE_PRINTK) | \
- TRACE_ITER(COPY_MARKER))
+ TRACE_ITER(COPY_MARKER) | TRACE_ITER(STACKMAP))
/*
* The global_trace is the descriptor that holds the top-level tracing
@@ -2184,6 +2186,76 @@ void __ftrace_trace_stack(struct trace_array *tr,
}
#endif
+#ifdef CONFIG_FTRACE_STACKMAP
+ /*
+ * If stackmap dedup is enabled, try to store only the stack_id
+ * in the ring buffer instead of the full stack trace.
+ *
+ * Reserve the TRACE_STACK_ID ring-buffer slot BEFORE calling
+ * get_id(). This guarantees get_id(), and therefore any map mutation
+ * or counter update it performs, is attempted only after the stack-id
+ * reservation succeeds:
+ * - reservation fails -> try full-stack fallback, map untouched
+ * - get_id() fails -> discard the reserved slot, then try
+ * full-stack fallback
+ * A failed stack-id reservation therefore never consumes a map slot
+ * or updates the map counters.
+ */
+ if (tr->trace_flags & TRACE_ITER(STACKMAP)) {
+ struct ftrace_stackmap *smap;
+ struct stack_id_entry *sid_entry;
+ int sid;
+
+ /*
+ * Pairs with the smp_store_release() that publishes the
+ * fully initialized global stackmap at tracefs init.
+ */
+ smap = smp_load_acquire(&tr->stackmap);
+ if (!smap)
+ goto full_stack;
+
+ /*
+ * The stackmap stores at most FTRACE_STACKMAP_MAX_DEPTH
+ * frames per entry. A deeper trace would be truncated, and
+ * two distinct stacks that share the first MAX_DEPTH frames
+ * would hash and compare equal, silently merging into one
+ * stack_id. Use the conservative full-stack path for deep
+ * traces to avoid truncating or misattributing a recorded stack.
+ * The ring-buffer reservation can still fail.
+ */
+ if (nr_entries > FTRACE_STACKMAP_MAX_DEPTH)
+ goto full_stack;
+
+ event = __trace_buffer_lock_reserve(buffer, TRACE_STACK_ID,
+ sizeof(*sid_entry), trace_ctx);
+ if (!event)
+ goto full_stack;
+
+ sid = ftrace_stackmap_get_id(smap, fstack->calls, nr_entries);
+ if (sid < 0) {
+ /*
+ * If get_id() cannot return an ID, discard the reserved
+ * stack_id slot, then try the full-stack fallback. Its
+ * ring-buffer reservation can still fail.
+ */
+ __trace_event_discard_commit(buffer, event);
+ goto full_stack;
+ }
+
+ sid_entry = ring_buffer_event_data(event);
+ sid_entry->stack_id = sid;
+ /*
+ * stack_id is a synthetic side-event attached to a
+ * primary trace event that was already subject to
+ * filtering. No per-event filter is defined for
+ * TRACE_STACK_ID, so commit unconditionally.
+ */
+ __buffer_unlock_commit(buffer, event);
+ goto out;
+ }
+full_stack:
+#endif
+
event = __trace_buffer_lock_reserve(buffer, TRACE_STACK,
struct_size(entry, caller, nr_entries),
trace_ctx);
@@ -3976,6 +4048,50 @@ int trace_keep_overwrite(struct tracer *tracer, u64 mask, int set)
return 0;
}
+#ifdef CONFIG_FTRACE_STACKMAP
+/*
+ * Tracks tracefs-time initialization of the global stackmap so that
+ * set_tracer_flag() can distinguish "not initialized yet" from
+ * "initialization permanently failed".
+ *
+ * Boot-time options (trace_options=stackmap,stacktrace) are applied
+ * very early, before tracer_init_tracefs() creates and publishes the
+ * map. We must allow the STACKMAP flag to be set during that window
+ * (the hot path falls back to a full stack while tr->stackmap is NULL,
+ * then starts using the map once it is published). We must, however,
+ * reject the enable once init has *failed*, so options/stackmap never
+ * reports an enabled no-op.
+ *
+ * Written once from the tracefs init work before any concurrent
+ * userspace writer to trace_options can run, then only read; a plain
+ * int is therefore sufficient.
+ */
+enum {
+ STACKMAP_INIT_PENDING, /* tracer_init_tracefs() not run yet */
+ STACKMAP_INIT_DONE, /* map published, stack_map file created */
+ STACKMAP_INIT_FAILED, /* permanent failure, never available */
+};
+
+static int stackmap_init_state = STACKMAP_INIT_PENDING;
+
+/*
+ * Mark the global stackmap init as permanently failed.
+ *
+ * Clears any boot-time STACKMAP flag (trace_options=stackmap applied before
+ * the map was created) so options/stackmap does not report an enabled no-op
+ * and later userspace enables return -EINVAL. The flag is cleared under
+ * trace_types_lock because set_tracer_flag() updates trace_flags under that
+ * lock; the tracefs init runs in an unlocked workqueue context that can race
+ * with a concurrent trace_options write.
+ */
+static void __init stackmap_mark_init_failed(void)
+{
+ guard(mutex)(&trace_types_lock);
+ WRITE_ONCE(stackmap_init_state, STACKMAP_INIT_FAILED);
+ global_trace.trace_flags &= ~TRACE_ITER(STACKMAP);
+}
+#endif
+
int set_tracer_flag(struct trace_array *tr, u64 mask, int enabled)
{
switch (mask) {
@@ -3990,6 +4106,33 @@ int set_tracer_flag(struct trace_array *tr, u64 mask, int enabled)
if (!!(tr->trace_flags & mask) == !!enabled)
return 0;
+#ifdef CONFIG_FTRACE_STACKMAP
+ /*
+ * STACKMAP is intentionally global-instance-only: the dedup map,
+ * its tracefs files and the lifetime/reset semantics are tied
+ * to the global trace
+ * array. options/stackmap is hidden on secondary instances via
+ * TOP_LEVEL_TRACE_FLAGS, but writes still reach set_tracer_flag()
+ * through the aggregate trace_options file. Reject the enable on
+ * a secondary instance so it cannot be silently accepted and then
+ * become a no-op in the hot path (where tr->stackmap is NULL and
+ * the code falls back to a full stack trace).
+ *
+ * On the global instance, allow the enable while init is still
+ * pending (boot-time trace_options=stackmap is applied before the
+ * tracefs init work creates the map; the hot path falls back
+ * until the map is published). Only reject once init has
+ * permanently failed, so options/stackmap never reports an
+ * enabled no-op. READ_ONCE() suffices: this only inspects the
+ * init state, it does not dereference the map (the hot path uses
+ * smp_load_acquire(&tr->stackmap) for that).
+ */
+ if (mask == TRACE_ITER(STACKMAP) && enabled &&
+ (tr != &global_trace ||
+ READ_ONCE(stackmap_init_state) == STACKMAP_INIT_FAILED))
+ return -EINVAL;
+#endif
+
/* Give the tracer a chance to approve the change */
if (tr->current_trace->flag_changed)
if (tr->current_trace->flag_changed(tr, mask, !!enabled))
@@ -9222,6 +9365,70 @@ static __init void tracer_init_tracefs_work_func(struct work_struct *work)
NULL, &tracing_dyn_info_fops);
#endif
+#ifdef CONFIG_FTRACE_STACKMAP
+ {
+ struct ftrace_stackmap *smap;
+ struct dentry *map_file;
+
+ smap = ftrace_stackmap_create(&global_trace);
+ if (!IS_ERR(smap)) {
+ /*
+ * Failure-atomic init: stack_map is the single
+ * required tracefs file (it doubles as the reset
+ * interface and the human-readable resolver). If
+ * we cannot create it, the hot path must not be
+ * able to emit <stack_id N> events that no one can
+ * resolve or clear, so refuse to publish the map
+ * and tear it down.
+ *
+ * Create stack_map BEFORE smp_store_release() so an
+ * observed non-NULL global_trace.stackmap implies
+ * its resolver/reset file exists.
+ */
+ map_file = trace_create_file("stack_map",
+ TRACE_MODE_WRITE, NULL,
+ smap,
+ &ftrace_stackmap_fops);
+ if (!map_file) {
+ pr_warn("ftrace stackmap init: stack_map create failed, dedup disabled\n");
+ ftrace_stackmap_destroy(smap);
+ /*
+ * Permanent failure. Record it and clear a
+ * boot-time STACKMAP flag (under
+ * trace_types_lock) so options/stackmap does
+ * not report an enabled no-op and later
+ * userspace enables return -EINVAL.
+ */
+ stackmap_mark_init_failed();
+ } else {
+ /*
+ * smp_store_release pairs with the
+ * smp_load_acquire() in
+ * __ftrace_trace_stack(). Publishing only
+ * after the required file exists keeps
+ * "smap visible" => "resolver/reset
+ * available".
+ */
+ smp_store_release(&global_trace.stackmap, smap);
+ WRITE_ONCE(stackmap_init_state, STACKMAP_INIT_DONE);
+ }
+ } else {
+ pr_warn("ftrace stackmap init failed, dedup disabled\n");
+ /*
+ * global_trace is statically defined; its stackmap
+ * field is zero-initialized via BSS, so leaving it
+ * NULL ensures the smp_load_acquire() in
+ * __ftrace_trace_stack() falls back to full stack.
+ * Mark init failed and clear any boot-time STACKMAP
+ * flag so userspace enables are rejected rather than
+ * becoming silent no-ops. Use the helper so the flag
+ * clear happens under trace_types_lock, matching the
+ * stack_map-create failure path above.
+ */
+ stackmap_mark_init_failed();
+ }
+ }
+#endif
create_trace_instances(NULL);
update_tracer_options();
@@ -9234,8 +9441,13 @@ static __init int tracer_init_tracefs(void)
trace_access_lock_init();
ret = tracing_init_dentry();
- if (ret)
+ if (ret) {
+#ifdef CONFIG_FTRACE_STACKMAP
+ /* No later path can create or publish the global stackmap. */
+ stackmap_mark_init_failed();
+#endif
return 0;
+ }
if (trace_init_wq) {
INIT_WORK(&tracerfs_init_work, tracer_init_tracefs_work_func);
diff --git a/kernel/trace/trace.h b/kernel/trace/trace.h
index 80fe152af1dd..7e7d5e5a35ff 100644
--- a/kernel/trace/trace.h
+++ b/kernel/trace/trace.h
@@ -57,6 +57,7 @@ enum trace_type {
TRACE_TIMERLAT,
TRACE_RAW_DATA,
TRACE_FUNC_REPEATS,
+ TRACE_STACK_ID,
__TRACE_LAST_TYPE,
};
@@ -453,6 +454,9 @@ struct trace_array {
struct cond_snapshot *cond_snapshot;
#endif
struct trace_func_repeats __percpu *last_func_repeats;
+#ifdef CONFIG_FTRACE_STACKMAP
+ struct ftrace_stackmap *stackmap;
+#endif
/*
* On boot up, the ring buffer is set to the minimum size, so that
* we do not waste memory on systems that are not using tracing.
@@ -579,6 +583,8 @@ extern void __ftrace_bad_type(void);
TRACE_GRAPH_RET); \
IF_ASSIGN(var, ent, struct func_repeats_entry, \
TRACE_FUNC_REPEATS); \
+ IF_ASSIGN(var, ent, struct stack_id_entry, \
+ TRACE_STACK_ID); \
__ftrace_bad_type(); \
} while (0)
@@ -1449,7 +1455,16 @@ extern int trace_get_user(struct trace_parser *parser, const char __user *ubuf,
# define STACK_FLAGS
#endif
+#ifdef CONFIG_FTRACE_STACKMAP
+# define STACKMAP_FLAGS \
+ C(STACKMAP, "stackmap"),
+#else
+# define STACKMAP_FLAGS
+# define TRACE_ITER_STACKMAP_BIT -1
+#endif
+
#ifdef CONFIG_FUNCTION_PROFILER
+
# define PROFILER_FLAGS \
C(PROF_TEXT_OFFSET, "prof-text-offset"),
# ifdef CONFIG_FUNCTION_GRAPH_TRACER
@@ -1506,6 +1521,7 @@ extern int trace_get_user(struct trace_parser *parser, const char __user *ubuf,
FUNCTION_FLAGS \
FGRAPH_FLAGS \
STACK_FLAGS \
+ STACKMAP_FLAGS \
BRANCH_FLAGS \
PROFILER_FLAGS \
FPROFILE_FLAGS
diff --git a/kernel/trace/trace_entries.h b/kernel/trace/trace_entries.h
index 54417468fdeb..89ed14b7e5fd 100644
--- a/kernel/trace/trace_entries.h
+++ b/kernel/trace/trace_entries.h
@@ -250,6 +250,21 @@ FTRACE_ENTRY(user_stack, userstack_entry,
(void *)__entry->caller[6], (void *)__entry->caller[7])
);
+/*
+ * Stack ID entry - stores only a stack_id referencing the stackmap.
+ * Used when CONFIG_FTRACE_STACKMAP is enabled to deduplicate stacks.
+ */
+FTRACE_ENTRY(stack_id, stack_id_entry,
+
+ TRACE_STACK_ID,
+
+ F_STRUCT(
+ __field( int, stack_id )
+ ),
+
+ F_printk("<stack_id %d>", __entry->stack_id)
+);
+
/*
* trace_printk entry:
*/
diff --git a/kernel/trace/trace_functions_graph.c b/kernel/trace/trace_functions_graph.c
index 0d2d3a2ea7dd..0e1a390a6130 100644
--- a/kernel/trace/trace_functions_graph.c
+++ b/kernel/trace/trace_functions_graph.c
@@ -1492,6 +1492,7 @@ print_graph_function_flags(struct trace_iterator *iter, u32 flags)
return print_graph_return(field, s, entry, iter, flags);
}
case TRACE_STACK:
+ case TRACE_STACK_ID:
case TRACE_FN:
/* dont trace stack and functions as comments */
return TRACE_TYPE_UNHANDLED;
diff --git a/kernel/trace/trace_output.c b/kernel/trace/trace_output.c
index a5ad76175d10..68678ea88159 100644
--- a/kernel/trace/trace_output.c
+++ b/kernel/trace/trace_output.c
@@ -1517,6 +1517,28 @@ static struct trace_event trace_user_stack_event = {
.funcs = &trace_user_stack_funcs,
};
+/* TRACE_STACK_ID */
+static enum print_line_t trace_stack_id_print(struct trace_iterator *iter,
+ int flags, struct trace_event *event)
+{
+ struct stack_id_entry *field;
+ struct trace_seq *s = &iter->seq;
+
+ trace_assign_type(field, iter->ent);
+ trace_seq_printf(s, "<stack_id %d>\n", field->stack_id);
+
+ return trace_handle_return(s);
+}
+
+static struct trace_event_functions trace_stack_id_funcs = {
+ .trace = trace_stack_id_print,
+};
+
+static struct trace_event trace_stack_id_event = {
+ .type = TRACE_STACK_ID,
+ .funcs = &trace_stack_id_funcs,
+};
+
/* TRACE_HWLAT */
static enum print_line_t
trace_hwlat_print(struct trace_iterator *iter, int flags,
@@ -1908,6 +1930,7 @@ static struct trace_event *events[] __initdata = {
&trace_wake_event,
&trace_stack_event,
&trace_user_stack_event,
+ &trace_stack_id_event,
&trace_bputs_event,
&trace_bprint_event,
&trace_print_event,
diff --git a/kernel/trace/trace_selftest.c b/kernel/trace/trace_selftest.c
index 929c84075315..0c97065b0d68 100644
--- a/kernel/trace/trace_selftest.c
+++ b/kernel/trace/trace_selftest.c
@@ -14,6 +14,7 @@ static inline int trace_valid_entry(struct trace_entry *entry)
case TRACE_CTX:
case TRACE_WAKE:
case TRACE_STACK:
+ case TRACE_STACK_ID:
case TRACE_PRINT:
case TRACE_BRANCH:
case TRACE_GRAPH_ENT:
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 03/10] trace: add stackmap statistics interface
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 01/10] trace: add lock-free stackmap for stack trace deduplication Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 02/10] trace: use the stackmap from the ftrace stack recording path Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 04/10] trace: add stackmap binary export Li Pengfei
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Export stackmap counters through stack_map_stat: entries, table size,
successes, drops and success rate.
entries is the element-pool allocation cursor: the number of element
records claimed since the last reset. It is not a strict unique-stack
count. Concurrent insertion races can claim duplicate records, and a
sample can include a record claimed before it is published in the hash
table.
successes counts map operations that returned a stack id, while drops
counts capacity and probe-limit failures. The rate is
successes / (successes + drops), so it excludes bypasses that never
call the map, including deep stacks, reset windows and ring-buffer
reservation failures. A fresh or reset map reports 0%.
Take the complete sample under reader_sem. Reset clears next_elt and
the per-CPU atomic_long_t counters under the write side; read those
counters with atomic_long_read() under the read side to avoid combining
values from different generations.
The file is auxiliary. Failure to create it does not disable
stackmap because stack_map remains available to resolve and reset ids.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
kernel/trace/trace.c | 14 +++-
kernel/trace/trace_stackmap.c | 137 ++++++++++++++++++++++++++++++++++
kernel/trace/trace_stackmap.h | 1 +
3 files changed, 151 insertions(+), 1 deletion(-)
diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c
index 06ae8ed11475..17df5f85da7a 100644
--- a/kernel/trace/trace.c
+++ b/kernel/trace/trace.c
@@ -2199,7 +2199,7 @@ void __ftrace_trace_stack(struct trace_array *tr,
* - get_id() fails -> discard the reserved slot, then try
* full-stack fallback
* A failed stack-id reservation therefore never consumes a map slot
- * or updates the map counters.
+ * or updates stack_map_stat.
*/
if (tr->trace_flags & TRACE_ITER(STACKMAP)) {
struct ftrace_stackmap *smap;
@@ -9411,6 +9411,18 @@ static __init void tracer_init_tracefs_work_func(struct work_struct *work)
*/
smp_store_release(&global_trace.stackmap, smap);
WRITE_ONCE(stackmap_init_state, STACKMAP_INIT_DONE);
+ /*
+ * stat is an auxiliary observability
+ * surface. If it fails to be created we keep
+ * dedup enabled -- the kernel side still
+ * works and stack_map alone is enough to
+ * resolve and reset; trace_create_file()
+ * already pr_warn()s on failure.
+ */
+ trace_create_file("stack_map_stat",
+ TRACE_MODE_READ, NULL,
+ smap,
+ &ftrace_stackmap_stat_fops);
}
} else {
pr_warn("ftrace stackmap init failed, dedup disabled\n");
diff --git a/kernel/trace/trace_stackmap.c b/kernel/trace/trace_stackmap.c
index b2e2115a15f9..2382c7459712 100644
--- a/kernel/trace/trace_stackmap.c
+++ b/kernel/trace/trace_stackmap.c
@@ -56,6 +56,8 @@
#include <linux/random.h>
#include <linux/rcupdate.h>
#include <linux/log2.h>
+#include <linux/math64.h>
+#include <linux/overflow.h>
#include <asm/local.h>
#include "trace.h"
@@ -716,3 +718,138 @@ const struct file_operations ftrace_stackmap_fops = {
.llseek = seq_lseek,
.release = stackmap_release,
};
+
+/* --- Stats --- */
+
+static u64 stackmap_u64_add_sat(u64 left, u64 right)
+{
+ u64 sum;
+
+ return check_add_overflow(left, right, &sum) ? U64_MAX : sum;
+}
+
+static int stackmap_scaled_cmp(u64 left, u32 left_scale,
+ u64 right, u32 right_scale)
+{
+ u64 left_hi = mul_u64_u64_shr(left, left_scale, 64);
+ u64 right_hi = mul_u64_u64_shr(right, right_scale, 64);
+ u64 left_lo = left * left_scale;
+ u64 right_lo = right * right_scale;
+
+ if (left_hi != right_hi)
+ return left_hi < right_hi ? -1 : 1;
+ if (left_lo != right_lo)
+ return left_lo < right_lo ? -1 : 1;
+ return 0;
+}
+
+static u64 stackmap_success_rate(u64 successes, u64 drops)
+{
+ u32 low = 0, high = 100;
+
+ if (!successes)
+ return 0;
+ if (!drops)
+ return 100;
+
+ /*
+ * Find the largest percentage p satisfying
+ *
+ * p * drops <= (100 - p) * successes
+ *
+ * which is equivalent to p <= 100 * successes / (successes + drops),
+ * without forming the potentially 65-bit denominator. Compare the
+ * products as 128-bit values split into high and low halves.
+ */
+ while (low < high) {
+ u32 mid = (low + high + 1) / 2;
+
+ if (stackmap_scaled_cmp(drops, mid, successes, 100 - mid) <= 0)
+ low = mid;
+ else
+ high = mid - 1;
+ }
+
+ return low;
+}
+
+static int stackmap_stat_show(struct seq_file *m, void *v)
+{
+ struct ftrace_stackmap *smap = m->private;
+ u64 successes = 0, drops = 0;
+ u64 cpu_successes, cpu_drops;
+ u32 entries;
+ int cpu;
+
+ if (!smap) {
+ seq_puts(m, "stackmap not initialized\n");
+ return 0;
+ }
+
+ /*
+ * Sample every counter under the read side of reader_sem. Reset
+ * clears next_elt and the per-CPU counters under the write side,
+ * so without this an unserialized read could straddle a reset and
+ * report a mix of the two generations -- a non-zero entry count
+ * next to counters that have already been zeroed, for instance.
+ */
+ down_read(&smap->reader_sem);
+
+ entries = atomic_read(&smap->next_elt);
+ for_each_possible_cpu(cpu) {
+ cpu_successes = atomic_long_read(
+ per_cpu_ptr(smap->successes, cpu));
+ cpu_drops = atomic_long_read(per_cpu_ptr(smap->drops, cpu));
+ successes = stackmap_u64_add_sat(successes, cpu_successes);
+ drops = stackmap_u64_add_sat(drops, cpu_drops);
+ }
+
+ seq_printf(m, "entries: %u / %u\n", entries, smap->max_elts);
+ seq_printf(m, "table_size: %u\n", smap->map_size);
+ seq_printf(m, "successes: %llu\n", successes);
+ seq_printf(m, "drops: %llu\n", drops);
+ seq_printf(m, "success_rate: %llu%%\n",
+ stackmap_success_rate(successes, drops));
+
+ up_read(&smap->reader_sem);
+ return 0;
+}
+
+static int stackmap_stat_open(struct inode *inode, struct file *file)
+{
+ struct ftrace_stackmap *smap = inode->i_private;
+ int ret;
+
+ if (!smap)
+ return -ENODEV;
+
+ /* Same open-time tracing policy as the other stackmap files. */
+ ret = tracing_check_open_get_tr(smap->tr);
+ if (ret)
+ return ret;
+
+ ret = single_open(file, stackmap_stat_show, smap);
+ if (ret) {
+ trace_array_put(smap->tr);
+ return ret;
+ }
+ return 0;
+}
+
+static int stackmap_stat_release(struct inode *inode, struct file *file)
+{
+ struct seq_file *m = file->private_data;
+ struct ftrace_stackmap *smap = m->private;
+ int ret;
+
+ ret = single_release(inode, file);
+ trace_array_put(smap->tr);
+ return ret;
+}
+
+const struct file_operations ftrace_stackmap_stat_fops = {
+ .open = stackmap_stat_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = stackmap_stat_release,
+};
diff --git a/kernel/trace/trace_stackmap.h b/kernel/trace/trace_stackmap.h
index 979d6fd76460..7615e346dfa6 100644
--- a/kernel/trace/trace_stackmap.h
+++ b/kernel/trace/trace_stackmap.h
@@ -19,6 +19,7 @@ int ftrace_stackmap_get_id(struct ftrace_stackmap *smap,
unsigned long *ips, unsigned int nr_entries);
extern const struct file_operations ftrace_stackmap_fops;
+extern const struct file_operations ftrace_stackmap_stat_fops;
#else
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 04/10] trace: add stackmap binary export
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
` (2 preceding siblings ...)
2026-09-12 8:37 ` [RFC PATCH v7 03/10] trace: add stackmap statistics interface Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 05/10] trace: make the stackmap capacity settable on the kernel command line Li Pengfei
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Add stack_map_bin, a native-endian version 1 binary export containing a
header followed by stack id, depth, reference count and IP records.
Userspace detects byte order from the magic value.
Stream records through seq_write() instead of allocating a private
payload snapshot. open() records populated slot numbers in a bitmap and
sets nr_stacks to its exact count. The bitmap uses at most 64 KiB, and
exactly those records are emitted. It fixes record membership, not
payload values, so ref_count can change before a selected record is
written.
seq_file releases reader_sem between read() calls. Track resets with a
u64 generation sampled at open and checked at the start of each later
seq pass. All runtime generation accesses are under reader_sem, so a
plain u64 is sufficient. A changed generation returns -ESTALE rather
than -EAGAIN, which seq_file reserves for internal traversal retries.
Data already buffered by the current pass can be returned first.
Once iteration has reached the end, a later read returns normal EOF even
if reset intervened because no new-generation record can be appended.
A reader that receives -ESTALE must reopen the file.
Export the trampoline sentinel unchanged and apply
trace_adjust_address() to other addresses, matching the text export.
The records contain raw kernel instruction pointers after
trace_adjust_address(). The auxiliary file has mode 0440 and open() uses
tracing_check_open_get_tr(), matching the access boundary of existing raw
tracing interfaces. This rejects LOCKDOWN_TRACEFS and pins the owning
trace array. All open failure paths and release drop that reference after
seq_file state is released. Failure to create the auxiliary file does not
disable stackmap.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
kernel/trace/trace.c | 18 +-
kernel/trace/trace_stackmap.c | 327 ++++++++++++++++++++++++++++++++--
kernel/trace/trace_stackmap.h | 20 +++
3 files changed, 343 insertions(+), 22 deletions(-)
diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c
index 17df5f85da7a..68f5eb9a1e96 100644
--- a/kernel/trace/trace.c
+++ b/kernel/trace/trace.c
@@ -4109,8 +4109,8 @@ int set_tracer_flag(struct trace_array *tr, u64 mask, int enabled)
#ifdef CONFIG_FTRACE_STACKMAP
/*
* STACKMAP is intentionally global-instance-only: the dedup map,
- * its tracefs files and the lifetime/reset semantics are tied
- * to the global trace
+ * its tracefs files (stack_map / stack_map_stat / stack_map_bin)
+ * and the lifetime/reset semantics are tied to the global trace
* array. options/stackmap is hidden on secondary instances via
* TOP_LEVEL_TRACE_FLAGS, but writes still reach set_tracer_flag()
* through the aggregate trace_options file. Reject the enable on
@@ -9412,17 +9412,21 @@ static __init void tracer_init_tracefs_work_func(struct work_struct *work)
smp_store_release(&global_trace.stackmap, smap);
WRITE_ONCE(stackmap_init_state, STACKMAP_INIT_DONE);
/*
- * stat is an auxiliary observability
- * surface. If it fails to be created we keep
- * dedup enabled -- the kernel side still
- * works and stack_map alone is enough to
- * resolve and reset; trace_create_file()
+ * stat and bin are auxiliary observability
+ * surfaces. If they fail to be created we
+ * keep dedup enabled (the kernel side still
+ * works, and stack_map alone is enough to
+ * resolve and reset); trace_create_file()
* already pr_warn()s on failure.
*/
trace_create_file("stack_map_stat",
TRACE_MODE_READ, NULL,
smap,
&ftrace_stackmap_stat_fops);
+ trace_create_file("stack_map_bin",
+ TRACE_MODE_READ, NULL,
+ smap,
+ &ftrace_stackmap_bin_fops);
}
} else {
pr_warn("ftrace stackmap init failed, dedup disabled\n");
diff --git a/kernel/trace/trace_stackmap.c b/kernel/trace/trace_stackmap.c
index 2382c7459712..439d3be9e351 100644
--- a/kernel/trace/trace_stackmap.c
+++ b/kernel/trace/trace_stackmap.c
@@ -35,17 +35,19 @@
* collision, linear probing finds the next slot and full memcmp
* confirms the match.
*
- * Concurrent userspace readers (cat stack_map) get a best-effort
- * snapshot. They are coherent with the hot path (smp_load_acquire on
- * entry->val); they are also serialized against reset via
- * smap->reader_sem (readers take it in shared mode, reset in
+ * Concurrent userspace readers (cat stack_map / stack_map_bin) get a
+ * best-effort snapshot. They are coherent with the hot path
+ * (smp_load_acquire on entry->val); they are also serialized against
+ * reset via smap->reader_sem (readers take it in shared mode, reset in
* exclusive mode), so a reset cannot tear a single seq_file pass --
* it waits for the active read() pass to drop the rwsem before clearing
- * the map. The hot path is coordinated with reset separately, via
- * acquire/release on smap->resetting.
+ * the map. Binary readers spanning multiple read() calls additionally
+ * use a generation check. The hot path is coordinated with reset
+ * separately, via acquire/release on smap->resetting.
*/
#include <linux/kernel.h>
+#include <linux/bitmap.h>
#include <linux/slab.h>
#include <linux/jhash.h>
#include <linux/seq_file.h>
@@ -74,7 +76,7 @@
/*
* Memory ordering of entry->val: published with smp_store_release()
* by the inserter; consumed with smp_load_acquire() by every reader
- * that dereferences the elt (get_id, seq_show). This pairs
+ * that dereferences the elt (get_id, seq_show, bin_show). This pairs
* the writes to elt->{nr,ips,ref_count} (initialized BEFORE the
* publish) with the reads of those fields (which happen AFTER the
* load). seq_start / seq_next only test val for NULL and use the
@@ -119,11 +121,19 @@ struct ftrace_stackmap {
struct stackmap_entry *entries; /* hash table */
struct stackmap_elt *elts; /* flat element pool */
atomic_t resetting;
+ /*
+ * Bumped by every completed reset, inside the reader_sem write
+ * section. A reader that spans several read() calls samples this
+ * at open() and revalidates it on each pass, so a reset landing
+ * between two reads is detected instead of silently splicing two
+ * generations of the map into one output stream.
+ */
+ u64 generation;
/*
* Reader/reset serialization. Held in shared mode (read lock)
- * across seq_file iteration; held in exclusive mode (write
- * lock) by reset's clearing phase. The hot path (get_id) does
- * not take this lock — it
+ * across each seq_file pass of the text and binary exports;
+ * held in exclusive mode (write lock) by reset's clearing
+ * phase. The hot path (get_id) does not take this lock — it
* uses smp_load_acquire/smp_store_release on entry->val and
* the resetting flag for the lock-free protocol.
*/
@@ -206,6 +216,7 @@ struct ftrace_stackmap *ftrace_stackmap_create(struct trace_array *tr)
smap->hash_seed = get_random_u32();
atomic_set(&smap->next_elt, 0);
atomic_set(&smap->resetting, 0);
+ smap->generation = 0;
init_rwsem(&smap->reader_sem);
return smap;
@@ -307,10 +318,10 @@ static int ftrace_stackmap_reset(struct ftrace_stackmap *smap)
/*
* Take the reader_sem in exclusive mode. This serializes the
- * memset against any tracefs reader (seq_file iteration) that
- * may currently hold the rwsem for read. The Tasks RCU grace
- * periods already drained the hot path; this rwsem covers
- * process-context export readers.
+ * memset against any tracefs reader (a seq_file pass of the text
+ * or binary export) that may currently hold the rwsem for read.
+ * The Tasks RCU grace periods already drained the hot path; this
+ * rwsem covers process-context export readers.
*/
down_write(&smap->reader_sem);
@@ -323,6 +334,13 @@ static int ftrace_stackmap_reset(struct ftrace_stackmap *smap)
atomic_long_set(per_cpu_ptr(smap->drops, cpu), 0);
}
+ /*
+ * Bump inside the write section so a reader holding the read
+ * side either sees the whole pre-reset generation or the whole
+ * post-reset one, never a counter from each.
+ */
+ smap->generation++;
+
up_write(&smap->reader_sem);
/* Release resetting=0 so new get_id() observes a cleared map. */
@@ -402,7 +420,7 @@ int ftrace_stackmap_get_id(struct ftrace_stackmap *smap,
val = stackmap_load_elt(entry);
/*
* READ_ONCE(val->nr) keeps style consistent with
- * the seq_show reader. nr is write-once
+ * the seq_show / bin_show readers. nr is write-once
* (set before publish, never modified afterwards),
* so the load is data-race-free, but READ_ONCE
* silences any analysis tool that flags a plain
@@ -853,3 +871,282 @@ const struct file_operations ftrace_stackmap_stat_fops = {
.llseek = seq_lseek,
.release = stackmap_stat_release,
};
+
+/* --- Binary export --- */
+
+/*
+ * Binary counterpart of the text export, for userspace that would
+ * rather not parse symbols out of stack_map.
+ *
+ * Between resets, the element pool only grows and populated slots are
+ * not recycled. open() records the populated slot numbers in a bitmap,
+ * then the table is streamed in index order through seq_write(). This
+ * fixes the record set and makes header.nr_stacks exact without copying
+ * stack payloads. Even at the largest supported table, the bitmap is
+ * only 64 KiB per open, rather than a roughly 135 MiB element snapshot.
+ * A reset clears and reuses both the table and pool.
+ *
+ * seq_file drops reader_sem between read() calls, so a reset landing
+ * between two reads of the same fd would otherwise splice two
+ * generations of the map into a single stream. Text output would merely
+ * look confusing, but a binary consumer would parse it as one coherent
+ * dump, so open() samples the generation counter and every pass
+ * revalidates it, failing the read with -ESTALE once it has moved. ESTALE
+ * is intentional: seq_file treats -EAGAIN as an internal traverse retry,
+ * which would make lseek() on a stale fd spin forever. A reader that loses
+ * the race must reopen the file.
+ *
+ * The records contain raw kernel instruction pointers after
+ * trace_adjust_address(). VFS mode checks provide the tracefs permission
+ * boundary, and open additionally uses tracing_check_open_get_tr() for the
+ * tracing lockdown check and to pin the owning trace array for the lifetime
+ * of the file.
+ */
+struct stackmap_bin_iter {
+ struct ftrace_stackmap *smap;
+ u64 generation; /* sampled at open() */
+ u32 nr_stacks; /* selected at open() */
+ bool locked; /* reader_sem held by start() */
+ /*
+ * Staging buffer for one full-depth entry, kept here rather than
+ * on the stack: the IP array alone is 512 bytes, which has no
+ * business in a seq_file show() frame. Emitting an entry in a
+ * single seq_write() also keeps it from being torn across a
+ * buffer-growth retry.
+ */
+ char ebuf[sizeof(struct ftrace_stackmap_bin_entry) +
+ FTRACE_STACKMAP_MAX_DEPTH * sizeof(u64)]
+ __aligned(8);
+ /* Populated table slots selected during the open-time scan. */
+ unsigned long *slots;
+};
+
+/*
+ * Advance to the first populated slot at or after *idx. Sets *idx to
+ * map_size when the table is exhausted, so a later call terminates
+ * immediately.
+ */
+static struct stackmap_entry *stackmap_bin_seek(struct stackmap_bin_iter *iter,
+ loff_t *idx)
+{
+ struct ftrace_stackmap *smap = iter->smap;
+ unsigned long i;
+
+ i = find_next_bit(iter->slots, smap->map_size, *idx);
+ if (i >= smap->map_size) {
+ *idx = smap->map_size;
+ return NULL;
+ }
+
+ *idx = i;
+ return &smap->entries[i];
+}
+
+/*
+ * Stream position 0 is the file header; position idx+1 is table slot
+ * idx. Deriving it from the slot index keeps the iteration idempotent
+ * across the retries seq_file performs when its buffer has to grow.
+ */
+static void *stackmap_bin_seq_start(struct seq_file *m, loff_t *pos)
+{
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap *smap = iter->smap;
+ struct stackmap_entry *entry;
+ loff_t idx;
+
+ /*
+ * Serializes against the clearing phase of ftrace_stackmap_reset().
+ * Dropped in stackmap_bin_seq_stop(), which seq_file calls after
+ * every pass, including the ones where start() reports an error.
+ */
+ down_read(&smap->reader_sem);
+ iter->locked = true;
+
+ /*
+ * The table has already been walked to the end. There is nothing
+ * left to hand over, so a reset that happened in the meantime
+ * cannot splice generations into this stream: report EOF rather
+ * than an error. Otherwise a reader that had already consumed the
+ * whole export would take a failure on its final read() and treat
+ * the data it holds as invalid.
+ */
+ if (*pos > smap->map_size)
+ return NULL;
+
+ if (smap->generation != iter->generation)
+ return ERR_PTR(-ESTALE);
+
+ if (*pos == 0)
+ return SEQ_START_TOKEN;
+
+ idx = *pos - 1;
+ entry = stackmap_bin_seek(iter, &idx);
+ *pos = idx + 1;
+ return entry;
+}
+
+static void *stackmap_bin_seq_next(struct seq_file *m, void *v, loff_t *pos)
+{
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap *smap = iter->smap;
+ struct stackmap_entry *entry;
+ loff_t idx;
+
+ if (v == SEQ_START_TOKEN)
+ idx = 0;
+ else
+ idx = ((struct stackmap_entry *)v - smap->entries) + 1;
+
+ entry = stackmap_bin_seek(iter, &idx);
+ *pos = idx + 1;
+ return entry;
+}
+
+static void stackmap_bin_seq_stop(struct seq_file *m, void *v)
+{
+ struct stackmap_bin_iter *iter = m->private;
+
+ if (iter->locked) {
+ up_read(&iter->smap->reader_sem);
+ iter->locked = false;
+ }
+}
+
+static int stackmap_bin_seq_show(struct seq_file *m, void *v)
+{
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap_bin_entry *e;
+ struct stackmap_entry *entry;
+ struct stackmap_elt *elt;
+ u32 i, nr;
+
+ if (v == SEQ_START_TOKEN) {
+ struct ftrace_stackmap_bin_header hdr = {
+ .magic = FTRACE_STACKMAP_BIN_MAGIC,
+ .version = FTRACE_STACKMAP_BIN_VERSION,
+ .nr_stacks = iter->nr_stacks,
+ .reserved = 0,
+ };
+
+ seq_write(m, &hdr, sizeof(hdr));
+ return 0;
+ }
+
+ entry = v;
+ elt = stackmap_load_elt(entry);
+ if (!elt)
+ return 0;
+
+ nr = READ_ONCE(elt->nr);
+ if (nr > FTRACE_STACKMAP_MAX_DEPTH)
+ nr = FTRACE_STACKMAP_MAX_DEPTH;
+
+ e = (struct ftrace_stackmap_bin_entry *)iter->ebuf;
+ e->stack_id = entry - iter->smap->entries;
+ e->nr = nr;
+ e->ref_count = atomic_read(&elt->ref_count);
+ e->reserved = 0;
+
+ for (i = 0; i < nr; i++) {
+ unsigned long ip = elt->ips[i];
+
+ /*
+ * Emit the trampoline marker verbatim so userspace can
+ * render it as [FTRACE TRAMPOLINE]; pass everything else
+ * through trace_adjust_address() so the binary export
+ * follows the same address-adjustment rules as the text
+ * export.
+ */
+ if (ip == FTRACE_TRAMPOLINE_MARKER)
+ e->ips[i] = (u64)FTRACE_TRAMPOLINE_MARKER;
+ else
+ e->ips[i] = (u64)trace_adjust_address(iter->smap->tr, ip);
+ }
+
+ seq_write(m, e, struct_size(e, ips, nr));
+ return 0;
+}
+
+static const struct seq_operations stackmap_bin_seq_ops = {
+ .start = stackmap_bin_seq_start,
+ .next = stackmap_bin_seq_next,
+ .stop = stackmap_bin_seq_stop,
+ .show = stackmap_bin_seq_show,
+};
+
+static int stackmap_bin_open(struct inode *inode, struct file *file)
+{
+ struct ftrace_stackmap *smap = inode->i_private;
+ struct stackmap_bin_iter *iter;
+ struct seq_file *m;
+ u32 nr = 0;
+ loff_t i;
+ int ret;
+
+ if (!smap)
+ return -ENODEV;
+
+ ret = tracing_check_open_get_tr(smap->tr);
+ if (ret)
+ return ret;
+
+ ret = seq_open_private(file, &stackmap_bin_seq_ops, sizeof(*iter));
+ if (ret)
+ goto put_tr;
+
+ m = file->private_data;
+ iter = m->private;
+ iter->smap = smap;
+ iter->slots = kvcalloc(BITS_TO_LONGS(smap->map_size),
+ sizeof(*iter->slots), GFP_KERNEL_ACCOUNT);
+ if (!iter->slots) {
+ ret = -ENOMEM;
+ goto release_seq;
+ }
+
+ /*
+ * Select the populated slots up front so header.nr_stacks exactly
+ * frames the records emitted by this fd. The bitmap copies only
+ * membership, not stack payloads; at the maximum map size it uses
+ * 64 KiB. Inserts after this scan are deliberately excluded.
+ */
+ down_read(&smap->reader_sem);
+ iter->generation = smap->generation;
+ for (i = 0; i < smap->map_size; i++) {
+ if (READ_ONCE(smap->entries[i].key) &&
+ stackmap_load_elt(&smap->entries[i])) {
+ __set_bit(i, iter->slots);
+ nr++;
+ }
+ }
+ up_read(&smap->reader_sem);
+
+ iter->nr_stacks = nr;
+ return 0;
+
+release_seq:
+ seq_release_private(inode, file);
+put_tr:
+ trace_array_put(smap->tr);
+ return ret;
+}
+
+static int stackmap_bin_release(struct inode *inode, struct file *file)
+{
+ struct seq_file *m = file->private_data;
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap *smap = iter->smap;
+ int ret;
+
+ kvfree(iter->slots);
+ ret = seq_release_private(inode, file);
+ trace_array_put(smap->tr);
+ return ret;
+}
+
+const struct file_operations ftrace_stackmap_bin_fops = {
+ .open = stackmap_bin_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = stackmap_bin_release,
+};
diff --git a/kernel/trace/trace_stackmap.h b/kernel/trace/trace_stackmap.h
index 7615e346dfa6..80415b985627 100644
--- a/kernel/trace/trace_stackmap.h
+++ b/kernel/trace/trace_stackmap.h
@@ -7,6 +7,25 @@
#define FTRACE_STACKMAP_MAX_DEPTH 64
+/* Binary export format */
+#define FTRACE_STACKMAP_BIN_MAGIC 0x46534D42 /* 'FSMB' */
+#define FTRACE_STACKMAP_BIN_VERSION 1
+
+struct ftrace_stackmap_bin_header {
+ u32 magic;
+ u32 version;
+ u32 nr_stacks;
+ u32 reserved;
+};
+
+struct ftrace_stackmap_bin_entry {
+ u32 stack_id;
+ u32 nr;
+ u32 ref_count;
+ u32 reserved;
+ u64 ips[]; /* nr entries follow the header */
+};
+
struct trace_array;
#ifdef CONFIG_FTRACE_STACKMAP
@@ -20,6 +39,7 @@ int ftrace_stackmap_get_id(struct ftrace_stackmap *smap,
extern const struct file_operations ftrace_stackmap_fops;
extern const struct file_operations ftrace_stackmap_stat_fops;
+extern const struct file_operations ftrace_stackmap_bin_fops;
#else
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 05/10] trace: make the stackmap capacity settable on the kernel command line
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
` (3 preceding siblings ...)
2026-09-12 8:37 ` [RFC PATCH v7 04/10] trace: add stackmap binary export Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 06/10] Documentation: tracing: document the ftrace stackmap Li Pengfei
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Add ftrace_stackmap.bits=N to size the map at boot, replacing the fixed
capacity the map was created with so far.
N is clamped to [10, 18], and the value is clamped again at create time
so a bogus setting cannot get through even if early_param is bypassed.
The upper bound keeps the worst case bounded: bits=18 means 256K
elements, 512K table slots and a ~130 MB element pool. The default stays
at 14, which gives 16K elements and a ~8 MB pool.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
.../admin-guide/kernel-parameters.txt | 7 ++++
kernel/trace/Kconfig | 4 +++
kernel/trace/trace_stackmap.c | 36 +++++++++++++++----
3 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index b5493a7f8f22..9ca271e21bf0 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -1794,6 +1794,13 @@ Kernel parameters
can be changed at run time by the max_graph_depth file
in the tracefs tracing directory. default: 0 (no limit)
+ ftrace_stackmap.bits=
+ [FTRACE] Set the stackmap capacity to 2^N records.
+ Format: <int>
+ default: 14; values are clamped to the range 10-18.
+ The upper bound reserves roughly 130 MB for the element
+ pool. See Documentation/trace/ftrace-stackmap.rst.
+
fw_devlink= [KNL,EARLY] Create device links between consumer and supplier
devices by scanning the firmware to infer the
consumer/supplier relationships. This feature is
diff --git a/kernel/trace/Kconfig b/kernel/trace/Kconfig
index 7001453bbfd3..b2cde74d803b 100644
--- a/kernel/trace/Kconfig
+++ b/kernel/trace/Kconfig
@@ -435,6 +435,10 @@ config FTRACE_STACKMAP
deduplicated when stackmap is enabled for their trace array. Stackmap
is currently available only for the global trace array.
+ Map capacity defaults to 2^14 records and can be changed with the
+ ftrace_stackmap.bits= boot parameter. Values are clamped to 10-18;
+ the upper bound reserves roughly 130 MB for the element pool.
+
The deduplicated stacks are exported via:
/sys/kernel/debug/tracing/stack_map
diff --git a/kernel/trace/trace_stackmap.c b/kernel/trace/trace_stackmap.c
index 439d3be9e351..49ea403822ba 100644
--- a/kernel/trace/trace_stackmap.c
+++ b/kernel/trace/trace_stackmap.c
@@ -150,12 +150,31 @@ struct ftrace_stackmap {
};
/*
- * Map capacity: 2^FTRACE_STACKMAP_BITS stack records, with the hash
- * table over-provisioned 2x on top of that. This gives 16K elements
- * and a ~8 MB element pool, sized for the repetitive-stack workloads
- * the map targets.
+ * Map capacity: 2^bits stack records, with the hash table
+ * over-provisioned 2x on top of that. The range is capped to keep
+ * worst-case allocations bounded:
+ * bits=18 -> 256K elts, 512K slots, ~130 MB elt pool, and a
+ * stack_map_bin reader walking that many entries.
+ * The default of 14 gives 16K elts and a ~8 MB pool, which suits the
+ * repetitive-stack workloads the map targets; raise it through
+ * ftrace_stackmap.bits= for a higher stack-record capacity.
*/
-#define FTRACE_STACKMAP_BITS 14
+#define FTRACE_STACKMAP_BITS_MIN 10
+#define FTRACE_STACKMAP_BITS_MAX 18
+#define FTRACE_STACKMAP_BITS_DEFAULT 14
+
+static unsigned int stackmap_map_bits = FTRACE_STACKMAP_BITS_DEFAULT;
+static int __init stackmap_bits_setup(char *str)
+{
+ unsigned long val;
+
+ if (kstrtoul(str, 0, &val))
+ return -EINVAL;
+ val = clamp_val(val, FTRACE_STACKMAP_BITS_MIN, FTRACE_STACKMAP_BITS_MAX);
+ stackmap_map_bits = val;
+ return 0;
+}
+early_param("ftrace_stackmap.bits", stackmap_bits_setup);
/* --- Element pool --- */
@@ -182,12 +201,17 @@ static struct stackmap_elt *stackmap_get_elt(struct ftrace_stackmap *smap)
struct ftrace_stackmap *ftrace_stackmap_create(struct trace_array *tr)
{
struct ftrace_stackmap *smap;
- unsigned int bits = FTRACE_STACKMAP_BITS;
+ unsigned int bits;
smap = kzalloc_obj(*smap, GFP_KERNEL);
if (!smap)
return ERR_PTR(-ENOMEM);
+ /* Defensive clamp even if the early parameter path is bypassed. */
+ bits = clamp_val(stackmap_map_bits,
+ FTRACE_STACKMAP_BITS_MIN,
+ FTRACE_STACKMAP_BITS_MAX);
+
smap->tr = tr;
smap->map_bits = bits;
smap->max_elts = 1U << bits;
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 06/10] Documentation: tracing: document the ftrace stackmap
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
` (4 preceding siblings ...)
2026-09-12 8:37 ` [RFC PATCH v7 05/10] trace: make the stackmap capacity settable on the kernel command line Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 07/10] tools/tracing: add a parser for the stackmap binary export Li Pengfei
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Document stackmap configuration, tracefs interfaces, reset semantics,
binary ABI and implementation trade-offs.
Call out the externally visible details:
- reset clears only the map, so ids already present in the ring buffer
may become unresolved or resolve to a reused slot
- boot-time deduplication begins when global_trace.stackmap is
published; the preceding full-stack path still has normal
ring-buffer reservation failure semantics
- capacity is measured in stack records and concurrent duplicates can
consume it
- entries counts claimed element records and can include duplicates or
records not yet published
- stack_map is required while stack_map_stat and stack_map_bin are
auxiliary
- stack_map_bin uses native byte order and version 1
- open-time bitmap membership yields exactly nr_stacks records but is
not a payload snapshot, so ref_count can change
- reset is detected at the next seq pass after buffered bytes drain and
reports -ESTALE; a completed old export still ends with normal EOF
- CONFIG_FTRACE_STACKMAP allocates the map at tracefs initialization even
when the runtime option remains disabled; the default element pool is
about 8 MiB and the maximum is about 130-135 MiB
- stack_map_bin exposes raw adjusted kernel IPs without kptr_restrict
sanitization; mode 0440 plus tracing_check_open_get_tr() and
LOCKDOWN_TRACEFS form its access boundary
- reset keeps the fixed 4-byte, non-generation-tagged stack-id ABI, so an
old id can resolve to a reused slot
Also describe the address-adjustment and offline-symbolization limits,
including KASLR and module addresses.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
Documentation/trace/ftrace-stackmap.rst | 241 ++++++++++++++++++++++++
Documentation/trace/index.rst | 1 +
2 files changed, 242 insertions(+)
create mode 100644 Documentation/trace/ftrace-stackmap.rst
diff --git a/Documentation/trace/ftrace-stackmap.rst b/Documentation/trace/ftrace-stackmap.rst
new file mode 100644
index 000000000000..1857c1d67292
--- /dev/null
+++ b/Documentation/trace/ftrace-stackmap.rst
@@ -0,0 +1,241 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======================
+Ftrace Stack Map
+======================
+
+:Author: Pengfei Li <lipengfei28@xiaomi.com>
+
+Overview
+========
+
+The ftrace stack map provides stack trace deduplication for the ftrace
+ring buffer. When enabled, instead of storing full kernel stack traces
+(typically 80-160 bytes each) in the ring buffer for every event, ftrace
+stores only a 4-byte ``stack_id``. The full stacks are maintained in a
+separate hash table and exported via tracefs for userspace to resolve.
+
+This is inspired by eBPF's ``BPF_MAP_TYPE_STACK_TRACE`` but integrated
+into ftrace's infrastructure, requiring no userspace daemon.
+
+Configuration
+=============
+
+Enable ``CONFIG_FTRACE_STACKMAP=y`` in the kernel config.
+
+Kernel command line parameters:
+
+- ``ftrace_stackmap.bits=N`` - Set map capacity to 2^N stack records
+ (default: 14 → 16384 records; valid range: 10-18). Concurrent duplicate
+ entries can consume this capacity.
+
+ At ``bits=18`` the kernel reserves roughly 130 MB of vmalloc memory
+ for the element pool. The cap is set intentionally to bound memory
+ usage.
+
+The map is allocated when tracefs initializes after
+``CONFIG_FTRACE_STACKMAP=y`` has been built in and the required resolver node
+can be created. This allocation happens even if the runtime ``stackmap`` option
+remains disabled. On a 64-bit kernel, the element pool consumes roughly 8 MiB
+at the default ``bits=14`` and 130-135 MiB at ``bits=18``; the hash table and
+small per-CPU metadata are additional allocations.
+
+Usage
+=====
+
+Enable stack deduplication::
+
+ echo 1 > /sys/kernel/debug/tracing/options/stackmap
+ echo 1 > /sys/kernel/debug/tracing/options/stacktrace
+ echo function > /sys/kernel/debug/tracing/current_tracer
+
+The trace output will show ``<stack_id N>`` instead of full stack traces::
+
+ sh-1234 [006] d.h.. 123.456789: <stack_id 42>
+
+To view the actual stacks::
+
+ cat /sys/kernel/debug/tracing/stack_map
+
+Output format::
+
+ stack_id 42 [ref 1337, depth 8]
+ [0] schedule+0x48/0xc0
+ [1] schedule_timeout+0x1c/0x30
+ ...
+
+To view statistics::
+
+ cat /sys/kernel/debug/tracing/stack_map_stat
+
+Output::
+
+ entries: 2500 / 16384
+ table_size: 32768
+ successes: 148923
+ drops: 0
+ success_rate: 100%
+
+To reset the stack map::
+
+ echo 0 > /sys/kernel/debug/tracing/stack_map
+
+Reset returns ``-EBUSY`` only if another reset is already in progress.
+
+Reset clears the map and nothing else: the trace buffer is left
+untouched and tracing does not have to be stopped. As a result a trace
+can still contain ``<stack_id N>`` records after a reset. Such an id
+either has no entry in ``stack_map``, or -- once tracing continues and
+the slot is reused -- resolves to an unrelated stack. The 4-byte IDs are not
+generation-tagged, so a consumer cannot distinguish that reuse from the ID
+alone. That is misleading output, not corruption. If you need the ids in an
+existing trace to stay meaningful, read the trace out before resetting.
+
+Boot-time activation
+====================
+
+The stackmap option can be enabled from the kernel command line::
+
+ trace_options=stackmap,stacktrace
+
+The regular full-stack fallback remains in use until the map is successfully
+created, the required ``stack_map`` resolver exists, and the map is published
+to ``global_trace.stackmap``. Deduplication starts at that publication
+boundary. While the map is unpublished, the fallback makes the usual
+ring-buffer reservation, which can still fail under the existing ring-buffer
+semantics. Early-boot stacks recorded before publication are not deduplicated.
+
+Tracefs Nodes
+=============
+
+``stack_map`` is the required resolver and reset node. The
+``stack_map_stat`` and ``stack_map_bin`` files are auxiliary observability nodes.
+If tracefs cannot create either auxiliary node, it emits a warning but does
+not disable stackmap; ``stack_map`` remains available to resolve and reset the
+map. The absence of an auxiliary node therefore does not disable stackmap.
+
+The files are owned by root and not world-readable (``stack_map``: 0640;
+``stack_map_stat`` and ``stack_map_bin``: 0440).
+
+``stack_map``, ``stack_map_stat`` and ``stack_map_bin`` all use
+``tracing_check_open_get_tr()`` in ``open()``. It rejects access under
+``LOCKDOWN_TRACEFS`` or when the tracing subsystem is globally disabled
+(``tracing_disabled``), and pins the owning trace array until release. The
+check applies at open time, so raising lockdown after tracefs has been
+populated still blocks later opens, including the ``stack_map`` reset write.
+
+``stack_map_bin`` is additionally a raw tracing ABI: its payload contains
+raw kernel instruction pointers after ``trace_adjust_address()`` and
+does not apply ``kptr_restrict`` sanitization. VFS permissions restrict
+the file to mode 0440.
+This follows the existing trust boundary for raw tracing interfaces such as
+``trace_pipe_raw`` and ``available_filter_functions_addrs``.
+
+``stack_map``
+ Text export of all deduplicated stacks with symbol resolution.
+ Writing ``0`` or ``reset`` clears all entries.
+
+``stack_map_stat``
+ ``entries`` is the number of element records claimed since the last reset.
+ Other statistics are table_size, successes (map operations that returned a
+ stack ID), drops (map capacity or probe-limit failures), and success_rate.
+ Entries is not a strict count of unique stacks: it can include duplicate
+ records created by concurrent insertions and records claimed but not yet
+ published in the hash table.
+ The success_rate is ``successes / (successes + drops)``; it does not include
+ bypasses that never call the map, such as deep stacks, reset windows, or ring
+ buffer reservation failures. The field is always present and reports
+ 0% when no success or drop has occurred. Drops accumulate when the
+ element pool is exhausted; once that happens, slots that won the
+ cmpxchg but failed to allocate an element remain "claimed but empty"
+ and increase probe pressure for any future insert hashing to the same
+ bucket. Reset clears these gravestones.
+
+``stack_map_bin``
+ Binary export for efficient userspace consumption. Format:
+
+ - Header (16 bytes): magic(u32) + version(u32) + nr_stacks(u32) + reserved(u32)
+ - Per stack: stack_id(u32) + nr(u32) + ref_count(u32) + reserved(u32) + ips(u64 × nr)
+
+ All fields are written in the kernel's native byte order.
+ Userspace tools detect endianness by reading the magic value.
+ Magic: ``0x46534D42`` ('FSMB'), Version: 1.
+
+ Trampoline frames are exported as the sentinel value
+ ``0x7fffffff`` (FTRACE_TRAMPOLINE_MARKER); all other addresses are
+ passed through ``trace_adjust_address()`` so they match the
+ ``stack_map`` text output's address-adjustment rules. Note this is
+ the same adjustment ftrace applies to its own trace output (mainly
+ relevant for persistent / last-boot buffers), not a general KASLR
+ un-offset. The dump tool's ``--vmlinux`` mode is therefore valid only
+ when core-kernel addresses already match that vmlinux, for example
+ with ``nokaslr``. It does not resolve KASLR-slid or module addresses.
+
+ The table is streamed rather than copied into a private payload
+ buffer. At ``open()``, the kernel records the currently populated
+ slot numbers in a bitmap and sets ``nr_stacks`` to that exact count.
+ The bitmap is at most 64 KiB at the largest supported table size;
+ stack payloads remain in the map and are emitted through
+ ``seq_file``. Entries inserted after ``open()`` are not part of that
+ fd's record set, so exactly ``nr_stacks`` records follow the header.
+
+ Because the reader lock is released between ``read()`` calls, a
+ reset can land part-way through reading the file. That would
+ otherwise splice two generations of the map into one stream, which a
+ binary consumer could not detect, so the export tracks a generation
+ counter. After any data already buffered by the current ``seq_file``
+ pass has been consumed, the next pass that needs to resume iteration
+ detects a reset and fails with ``-ESTALE``. It never appends records
+ from the new generation. ``ESTALE`` is used instead of ``EAGAIN``
+ because ``seq_file`` treats ``EAGAIN`` as an internal retry during
+ seeks. Reopen the file and read it again.
+
+ A reader that has already reached the end of the export is not
+ affected: the final ``read()`` returns 0 as usual even if a reset
+ happened, because no data remains that could come from a different
+ generation.
+
+The text export has no generation check. It serializes each ``seq_file``
+pass against reset through the same rwsem, but a ``stack_map`` reader that
+spans a reset can still produce text from two generations across separate
+``read()`` calls, and nothing reports that. Symbolized text is meant to be
+read rather than parsed as a record stream, so this is deliberate. A
+consumer that needs a consistent snapshot should either read ``stack_map``
+completely within a window where no reset happens, or use
+``stack_map_bin``, which does detect the change.
+
+Design
+======
+
+The stack map is modeled after ``tracing_map.c`` (used by hist triggers),
+using a lock-free design based on Dr. Cliff Click's non-blocking hash table
+algorithm:
+
+- **Lookup/Insert**: Lock-free via ``cmpxchg``, safe in NMI/IRQ/any context
+- **Memory**: Pre-allocated element pool, zero allocation on the hot path
+ (no GFP_ATOMIC failures under memory pressure)
+- **Collision**: Linear probing with a 2x over-provisioned table; probe
+ length is bounded so worst-case insert/lookup is O(1)
+- **Scope**: Currently supports the global trace instance
+- **Hash**: 32-bit jhash with a per-instance random seed; full ``memcmp``
+ confirms matches
+
+Deduplication is best-effort, not strict: if two CPUs race in the
+insert path with the same ``key_hash`` (i.e. the same stack), the
+``cmpxchg`` loser advances by one slot and may insert the same stack
+again. Under heavy contention this can produce a small number of
+duplicate entries for the same stack; ``ref_count`` is then split
+across the duplicates. Total memory is still bounded by the element
+pool size, and lookup correctness is unaffected (each duplicate is
+a self-consistent entry with its own ``stack_id``). The trade-off is
+intentional and keeps the hot path lock-free.
+
+Performance
+===========
+
+Typical results on an aarch64 SMP system (function tracer, 2 seconds):
+
+- Stack records: ~3000
+- Dedup rate: 84-98% (depends on workload diversity)
+- Ring buffer savings: ~80% for stack data
+- Overhead per event: ~50ns (one jhash + hash table lookup)
diff --git a/Documentation/trace/index.rst b/Documentation/trace/index.rst
index 5d9bf4694d5d..ac8b1141c23a 100644
--- a/Documentation/trace/index.rst
+++ b/Documentation/trace/index.rst
@@ -33,6 +33,7 @@ the Linux kernel.
ftrace
ftrace-design
ftrace-uses
+ ftrace-stackmap
kprobes
kprobetrace
fprobetrace
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 07/10] tools/tracing: add a parser for the stackmap binary export
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
` (5 preceding siblings ...)
2026-09-12 8:37 ` [RFC PATCH v7 06/10] Documentation: tracing: document the ftrace stackmap Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 08/10] selftests/ftrace: add a stackmap basic functionality test Li Pengfei
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Add stackmap_dump.py to decode native-endian version 1 stack_map_bin
streams from a file or stdin and print text or JSON.
Parse incrementally with bounded reads. Validate the header count,
require stack depths from 1 through 64, reject trailing bytes, and
report malformed input without a traceback.
--top rejects negative values, keeps zero as unlimited, and selects a
stable top N with an O(N) heap instead of materializing and sorting the
whole input. Normal text output is emitted record by record.
JSON output is staged in a SpooledTemporaryFile so malformed later
records or symbolization failures leave stdout empty. The spool keeps up
to 1 MiB in memory and then spills to disk.
With --vmlinux, addr2line receives addresses on stdin in batches bounded
by both 128 records and 128 unique addresses. This avoids ARG_MAX and
bounds retained records even when addresses repeat. Document that
KASLR-slid core addresses and module addresses are unsupported.
Install the script from the tools/tracing install target and add parser,
CLI, streaming, memory-bound and error-path tests. Report totals as
stack records rather than strict unique stacks.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
tools/tracing/Makefile | 17 +-
tools/tracing/stackmap_dump.py | 354 +++++++++++++++
tools/tracing/tests/test_stackmap_dump.py | 498 ++++++++++++++++++++++
3 files changed, 866 insertions(+), 3 deletions(-)
create mode 100755 tools/tracing/stackmap_dump.py
create mode 100644 tools/tracing/tests/test_stackmap_dump.py
diff --git a/tools/tracing/Makefile b/tools/tracing/Makefile
index 95e485f12d97..8c2be033e360 100644
--- a/tools/tracing/Makefile
+++ b/tools/tracing/Makefile
@@ -1,11 +1,22 @@
# SPDX-License-Identifier: GPL-2.0
include ../scripts/Makefile.include
+INSTALL ?= install
+BINDIR ?= /usr/bin
+PYTHON ?= python3
+
all: latency rtla
+check:
+ $(PYTHON) -m unittest discover -s tests -v
+
clean: latency_clean rtla_clean
-install: latency_install rtla_install
+install: latency_install rtla_install stackmap_install
+
+stackmap_install:
+ $(call QUIET_INSTALL,stackmap_dump.py)$(INSTALL) -D -m 755 stackmap_dump.py \
+ $(DESTDIR)$(BINDIR)/stackmap_dump.py
latency:
$(call descend,latency)
@@ -25,5 +36,5 @@ rtla_install:
rtla_clean:
$(call descend,rtla,clean)
-.PHONY: all install clean latency latency_install latency_clean \
- rtla rtla_install rtla_clean
+.PHONY: all check install clean stackmap_install latency latency_install \
+ latency_clean rtla rtla_install rtla_clean
diff --git a/tools/tracing/stackmap_dump.py b/tools/tracing/stackmap_dump.py
new file mode 100755
index 000000000000..cf461b49cee7
--- /dev/null
+++ b/tools/tracing/stackmap_dump.py
@@ -0,0 +1,354 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+stackmap_dump.py - Parse and display ftrace stack_map_bin binary export.
+
+Usage:
+ # Read straight from the device over a pipe
+ adb shell cat /sys/kernel/debug/tracing/stack_map_bin | stackmap_dump.py
+
+ # Or pull the file first and parse it
+ adb pull /sys/kernel/debug/tracing/stack_map_bin /tmp/stack_map.bin
+ python3 stackmap_dump.py /tmp/stack_map.bin
+
+ # With vmlinux when addresses already match its link-time range
+ # (for example, a nokaslr core kernel; modules are not supported)
+ python3 stackmap_dump.py /tmp/stack_map.bin --vmlinux vmlinux
+
+ # JSON output for tooling
+ python3 stackmap_dump.py /tmp/stack_map.bin --json
+"""
+
+import argparse
+import heapq
+import io
+import json
+import struct
+import subprocess
+import sys
+import tempfile
+
+MAGIC = 0x46534D42 # 'FSMB'
+HEADER_SIZE = 16 # 4 x u32
+ENTRY_SIZE = 16 # 4 x u32
+MAX_STACK_DEPTH_V1 = 64
+MAX_STACKS_V1 = 1 << 18
+SYMBOL_BATCH_RECORDS = 128
+SYMBOL_BATCH_ADDRESSES = 128
+SYMBOL_MAX_BATCHES = 64
+SYMBOL_MAX_ADDRESSES = 8192
+JSON_SPOOL_MEMORY_LIMIT = 1 << 20
+COPY_BUFFER_SIZE = 64 << 10
+
+# __ftrace_trace_stack() replaces trampoline addresses with this marker
+# before storing the stack, so the binary export carries it verbatim.
+FTRACE_TRAMPOLINE_MARKER = 0x7fffffff
+TRAMPOLINE_LABEL = '[FTRACE TRAMPOLINE]'
+
+
+def detect_endianness(data):
+ """Detect byte order from magic number in header."""
+ if len(data) < 4:
+ raise ValueError("File too small")
+ magic_le = struct.unpack_from('<I', data, 0)[0]
+ if magic_le == MAGIC:
+ return '<'
+ magic_be = struct.unpack_from('>I', data, 0)[0]
+ if magic_be == MAGIC:
+ return '>'
+ raise ValueError(f"Bad magic: 0x{magic_le:08x} (neither LE nor BE)")
+
+
+def batch_addr2line(vmlinux, addrs):
+ """Resolve multiple addresses in one addr2line invocation."""
+ if not addrs:
+ return {}
+ try:
+ # Use stdin to avoid ARG_MAX with large address sets.
+ stdin = '\n'.join(hex(a) for a in addrs) + '\n'
+ result = subprocess.run(
+ ['addr2line', '-f', '-e', vmlinux],
+ input=stdin, capture_output=True, text=True, timeout=60
+ )
+ except subprocess.TimeoutExpired as error:
+ raise RuntimeError("addr2line failed: timed out") from error
+ except FileNotFoundError as error:
+ raise RuntimeError("addr2line failed: executable not found") from error
+ except OSError as error:
+ detail = error.strerror or str(error)
+ raise RuntimeError(f"addr2line failed: {detail}") from error
+
+ if result.returncode:
+ detail = result.stderr.strip() or f"exit status {result.returncode}"
+ raise RuntimeError(f"addr2line failed: {detail}")
+
+ lines = result.stdout.splitlines()
+ expected_lines = len(addrs) * 2
+ if (len(lines) != expected_lines or
+ any(not line for line in lines)):
+ raise RuntimeError(
+ "addr2line returned malformed output: "
+ f"expected {expected_lines} non-empty lines, got {len(lines)}")
+
+ symbols = {}
+ for i, addr in enumerate(addrs):
+ function = lines[i * 2]
+ if function != '??':
+ symbols[addr] = function
+ return symbols
+
+
+def read_exact(stream, size, error_message):
+ """Read exactly size bytes from a stream that may return short reads."""
+ chunks = []
+ remaining = size
+ while remaining:
+ chunk = stream.read(remaining)
+ if not chunk:
+ raise ValueError(error_message)
+ chunks.append(chunk)
+ remaining -= len(chunk)
+ return b''.join(chunks)
+
+
+def parse_stackmap_stream(stream):
+ """Yield (stack_id, ref_count, ips) tuples from a binary stream."""
+ header = read_exact(stream, HEADER_SIZE, "File too small for header")
+ endian = detect_endianness(header)
+ header_fmt = f'{endian}IIII'
+ entry_fmt = f'{endian}IIII'
+
+ _magic, version, nr_stacks, _reserved = struct.unpack(header_fmt, header)
+ if version != 1:
+ raise ValueError(f"Unsupported version: {version}")
+ if nr_stacks > MAX_STACKS_V1:
+ raise ValueError(
+ f"Invalid stack count {nr_stacks}: version 1 maximum is "
+ f"{MAX_STACKS_V1}")
+
+ for _ in range(nr_stacks):
+ entry = read_exact(stream, ENTRY_SIZE,
+ "Truncated stack entry header")
+ stack_id, nr, ref_count, _reserved = struct.unpack(entry_fmt, entry)
+ if nr == 0:
+ raise ValueError(
+ "Invalid stack depth 0: version 1 minimum is 1")
+ if nr > MAX_STACK_DEPTH_V1:
+ raise ValueError(
+ f"Invalid stack depth {nr}: version 1 maximum is "
+ f"{MAX_STACK_DEPTH_V1}")
+
+ ips_data = read_exact(
+ stream, nr * 8,
+ f"Truncated stack IP data for stack_id {stack_id}")
+ ips = struct.unpack(f'{endian}{nr}Q', ips_data)
+ yield stack_id, ref_count, list(ips)
+
+ if stream.read(1):
+ raise ValueError(f"Trailing data after {nr_stacks} stack records")
+
+
+def parse_stackmap_bin(data):
+ """Parse in-memory binary data using the streaming parser."""
+ return list(parse_stackmap_stream(io.BytesIO(data)))
+
+
+def non_negative_int(value):
+ number = int(value)
+ if number < 0:
+ raise argparse.ArgumentTypeError("--top must be non-negative")
+ return number
+
+
+def select_top(records, limit):
+ """Select the largest ref_counts stably using O(limit) space."""
+ heap = []
+ for order, record in enumerate(records):
+ item = (record[1], -order, order, record)
+ if len(heap) < limit:
+ heapq.heappush(heap, item)
+ elif item[:2] > heap[0][:2]:
+ heapq.heapreplace(heap, item)
+
+ heap.sort(key=lambda item: (-item[0], item[2]))
+ return [item[3] for item in heap]
+
+
+def iter_symbol_batches(records):
+ """Group records without exceeding the unique-address limit."""
+ batch = []
+ addresses = set()
+ for record in records:
+ _stack_id, _ref_count, ips = record
+ record_addresses = {
+ ip for ip in ips if ip != FTRACE_TRAMPOLINE_MARKER
+ }
+ if (batch and
+ (len(batch) >= SYMBOL_BATCH_RECORDS or
+ len(addresses | record_addresses) >
+ SYMBOL_BATCH_ADDRESSES)):
+ yield batch, addresses
+ batch.clear()
+ addresses.clear()
+ batch.append(record)
+ addresses.update(record_addresses)
+ if batch:
+ yield batch, addresses
+
+
+class RecordRenderer:
+ """Write records incrementally without changing text or JSON formats."""
+
+ def __init__(self, output, json_output, include_symbols):
+ self.output = output
+ self.json_output = json_output
+ self.include_symbols = include_symbols
+ self.count = 0
+
+ @staticmethod
+ def terminal_safe(text):
+ """Escape terminal controls while preserving printable Unicode."""
+ escaped = []
+ for character in text:
+ codepoint = ord(character)
+ if character.isprintable():
+ escaped.append(character)
+ elif codepoint <= 0xff:
+ escaped.append(f'\\x{codepoint:02x}')
+ elif codepoint <= 0xffff:
+ escaped.append(f'\\u{codepoint:04x}')
+ else:
+ escaped.append(f'\\U{codepoint:08x}')
+ return ''.join(escaped)
+
+ @staticmethod
+ def render_ip(ip, symbols):
+ if ip == FTRACE_TRAMPOLINE_MARKER:
+ return TRAMPOLINE_LABEL
+ return symbols.get(ip, f'0x{ip:x}')
+
+ def emit(self, record, symbols):
+ stack_id, ref_count, ips = record
+ if self.json_output:
+ entry = {
+ 'stack_id': stack_id,
+ 'ref_count': ref_count,
+ 'ips': [f'0x{ip:x}' for ip in ips]
+ }
+ if self.include_symbols:
+ entry['symbols'] = [self.render_ip(ip, symbols)
+ for ip in ips]
+ encoded = json.dumps(entry, indent=2)
+ indented = '\n'.join(f' {line}' for line in encoded.splitlines())
+ if self.count == 0:
+ self.output.write('[\n')
+ else:
+ self.output.write(',\n')
+ self.output.write(indented)
+ else:
+ self.output.write(
+ f"stack_id {stack_id} [ref {ref_count}, depth {len(ips)}]\n")
+ for index, ip in enumerate(ips):
+ if ip == FTRACE_TRAMPOLINE_MARKER:
+ self.output.write(
+ f" [{index}] {TRAMPOLINE_LABEL}\n")
+ continue
+ symbol = symbols.get(ip, '')
+ if symbol:
+ symbol = f' {self.terminal_safe(symbol)}'
+ self.output.write(f" [{index}] 0x{ip:x}{symbol}\n")
+ self.output.write('\n')
+ self.count += 1
+
+ def finish(self):
+ if not self.json_output:
+ return
+ if self.count:
+ self.output.write('\n]\n')
+ else:
+ self.output.write('[]\n')
+
+
+def render_records(records, args, output):
+ renderer = RecordRenderer(output, args.json, bool(args.vmlinux))
+ if args.vmlinux:
+ batch_count = 0
+ address_count = 0
+ for batch, addresses in iter_symbol_batches(records):
+ batch_count += 1
+ address_count += len(addresses)
+ if (batch_count > SYMBOL_MAX_BATCHES or
+ address_count > SYMBOL_MAX_ADDRESSES):
+ raise RuntimeError(
+ "symbolization work limit exceeded: "
+ f"maximum {SYMBOL_MAX_BATCHES} batches and "
+ f"{SYMBOL_MAX_ADDRESSES} addresses")
+ symbols = batch_addr2line(args.vmlinux, list(addresses))
+ for record in batch:
+ renderer.emit(record, symbols)
+ else:
+ for record in records:
+ renderer.emit(record, {})
+ renderer.finish()
+ return renderer.count
+
+
+def main(argv=None, input_file=None, output=None, error=None):
+ parser = argparse.ArgumentParser(description='Parse ftrace stack_map_bin')
+ parser.add_argument('file', nargs='?', default='-',
+ help="Path to stack_map_bin file, or '-' for stdin "
+ "(the default)")
+ parser.add_argument(
+ '--vmlinux',
+ help=('Path to vmlinux for symbol resolution; addresses must already '
+ 'match vmlinux (for example, nokaslr). KASLR runtime addresses '
+ 'and module addresses are not supported. Symbolization is '
+ 'limited to 8192 addresses in 64 batches'))
+ parser.add_argument('--json', action='store_true', help='JSON output')
+ parser.add_argument('--top', type=non_negative_int, default=0,
+ help='Show only top N stacks by ref_count; 0 is unlimited')
+ args = parser.parse_args(argv)
+
+ output = output if output is not None else sys.stdout
+ error = error if error is not None else sys.stderr
+ stream = input_file
+ close_stream = False
+
+ try:
+ # stdin accepts a stack_map_bin stream directly from a pipe.
+ if stream is None:
+ if args.file == '-':
+ stream = sys.stdin.buffer
+ else:
+ stream = open(args.file, 'rb')
+ close_stream = True
+
+ records = parse_stackmap_stream(stream)
+ if args.top:
+ records = select_top(records, args.top)
+ if args.json:
+ with tempfile.SpooledTemporaryFile(
+ max_size=JSON_SPOOL_MEMORY_LIMIT, mode='w+',
+ encoding='utf-8') as staged_output:
+ count = render_records(records, args, staged_output)
+ staged_output.seek(0)
+ while True:
+ chunk = staged_output.read(COPY_BUFFER_SIZE)
+ if not chunk:
+ break
+ output.write(chunk)
+ else:
+ count = render_records(records, args, output)
+ except (ValueError, OSError, RuntimeError) as exception:
+ print(f"error: {exception}", file=error)
+ return 1
+ finally:
+ if close_stream:
+ stream.close()
+
+ print(f"Total: {count} stack records", file=error)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tools/tracing/tests/test_stackmap_dump.py b/tools/tracing/tests/test_stackmap_dump.py
new file mode 100644
index 000000000000..03769cbc190e
--- /dev/null
+++ b/tools/tracing/tests/test_stackmap_dump.py
@@ -0,0 +1,498 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+import importlib.util
+import io
+import json
+import os
+from pathlib import Path
+import struct
+import subprocess
+import sys
+import tempfile
+import unittest
+from unittest import mock
+
+SCRIPT = Path(__file__).resolve().parents[1] / "stackmap_dump.py"
+SPEC = importlib.util.spec_from_file_location("stackmap_dump", SCRIPT)
+stackmap_dump = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(stackmap_dump)
+
+
+def image(endian="<", version=1, entries=()):
+ data = bytearray(struct.pack(f"{endian}IIII", stackmap_dump.MAGIC,
+ version, len(entries), 0))
+ for stack_id, ref_count, ips in entries:
+ data += struct.pack(f"{endian}IIII", stack_id, len(ips),
+ ref_count, 0)
+ data += struct.pack(f"{endian}{len(ips)}Q", *ips)
+ return bytes(data)
+
+
+class ShortReadInput(io.BytesIO):
+ def read(self, size=-1):
+ if size < 0:
+ raise AssertionError("unbounded read() is not allowed")
+ if size > stackmap_dump.MAX_STACK_DEPTH_V1 * 8:
+ raise AssertionError("read() exceeds one version 1 stack")
+ return super().read(min(size, 3))
+
+
+class StreamingGuardInput(io.BytesIO):
+ def __init__(self, data, first_record_end, output):
+ super().__init__(data)
+ self.first_record_end = first_record_end
+ self.output = output
+
+ def read(self, size=-1):
+ if (self.tell() >= self.first_record_end and
+ "stack_id 1" not in self.output.getvalue()):
+ raise AssertionError("next record read before current output")
+ return super().read(size)
+
+
+class TrackedRecord:
+ live_count = 0
+ max_live_count = 0
+
+ def __init__(self, stack_id, ip=None):
+ self.stack_id = stack_id
+ self.ip = 0x1000 + stack_id if ip is None else ip
+ type(self).live_count += 1
+ type(self).max_live_count = max(type(self).max_live_count,
+ type(self).live_count)
+
+ def __iter__(self):
+ return iter((self.stack_id, 1, [self.ip]))
+
+ def __del__(self):
+ type(self).live_count -= 1
+
+
+class StackmapParserTest(unittest.TestCase):
+ def test_little_endian_entry(self):
+ raw = image(entries=[(7, 3, [0x1234, 0x7fffffff])])
+ self.assertEqual(list(stackmap_dump.parse_stackmap_bin(raw)),
+ [(7, 3, [0x1234, 0x7fffffff])])
+
+ def test_big_endian_entry(self):
+ raw = image(">", entries=[(9, 2, [0xabcdef])])
+ self.assertEqual(list(stackmap_dump.parse_stackmap_bin(raw)),
+ [(9, 2, [0xabcdef])])
+
+ def test_bad_magic(self):
+ with self.assertRaisesRegex(ValueError, "Bad magic"):
+ list(stackmap_dump.parse_stackmap_bin(b"BAD!" + bytes(12)))
+
+ def test_unknown_version(self):
+ with self.assertRaisesRegex(ValueError, "Unsupported version"):
+ list(stackmap_dump.parse_stackmap_bin(image(version=2)))
+
+ def test_truncated_header(self):
+ with self.assertRaisesRegex(ValueError, "too small for header"):
+ list(stackmap_dump.parse_stackmap_bin(bytes(15)))
+
+ def test_truncated_entry_header(self):
+ raw = struct.pack("<IIII", stackmap_dump.MAGIC, 1, 1, 0)
+ with self.assertRaisesRegex(ValueError, "Truncated stack entry header"):
+ list(stackmap_dump.parse_stackmap_bin(raw))
+
+ def test_truncated_ip_data(self):
+ raw = image(entries=[(1, 1, [0x1234])])[:-1]
+ with self.assertRaisesRegex(ValueError, "Truncated stack IP data"):
+ list(stackmap_dump.parse_stackmap_bin(raw))
+
+ def test_rejects_records_beyond_header_count(self):
+ raw = image(entries=[(1, 1, [0x1234])])
+ raw = bytearray(raw)
+ struct.pack_into("<I", raw, 8, 0)
+ with self.assertRaisesRegex(ValueError, "Trailing data"):
+ stackmap_dump.parse_stackmap_bin(raw)
+
+ def test_cli_reports_malformed_input_without_traceback(self):
+ result = subprocess.run([sys.executable, str(SCRIPT), "-"],
+ input=b"bad", capture_output=True)
+ stderr = result.stderr.decode()
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn("error: File too small for header", stderr)
+ self.assertNotIn("Traceback", stderr)
+
+ def test_cli_reports_missing_file_without_traceback(self):
+ missing = SCRIPT.parent / "does-not-exist.stackmap"
+ result = subprocess.run([sys.executable, str(SCRIPT), str(missing)],
+ capture_output=True, text=True)
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn("error:", result.stderr)
+ self.assertNotIn("Traceback", result.stderr)
+
+ def test_cli_reports_addr2line_failure(self):
+ raw = image(entries=[(1, 2, [0x1234])])
+ with tempfile.TemporaryDirectory() as temp_dir:
+ addr2line = Path(temp_dir) / "addr2line"
+ addr2line.write_text(
+ "#!/bin/sh\necho controlled addr2line failure >&2\nexit 23\n",
+ encoding="utf-8")
+ addr2line.chmod(0o755)
+ env = os.environ.copy()
+ env["PATH"] = temp_dir
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT), "-", "--vmlinux", "vmlinux"],
+ input=raw, capture_output=True, env=env)
+ stderr = result.stderr.decode()
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn(
+ "error: addr2line failed: controlled addr2line failure", stderr)
+ self.assertNotIn("Traceback", stderr)
+
+ def test_cli_reports_truncated_addr2line_output(self):
+ raw = image(entries=[(1, 2, [0x1234])])
+ with tempfile.TemporaryDirectory() as temp_dir:
+ addr2line = Path(temp_dir) / "addr2line"
+ addr2line.write_text(
+ "#!/bin/sh\nprintf 'resolved_function\\n'\n",
+ encoding="utf-8")
+ addr2line.chmod(0o755)
+ env = os.environ.copy()
+ env["PATH"] = temp_dir
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT), "-", "--vmlinux", "vmlinux"],
+ input=raw, capture_output=True, env=env)
+ stderr = result.stderr.decode()
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn("error: addr2line returned malformed output", stderr)
+ self.assertNotIn("Traceback", stderr)
+
+ def test_cli_accepts_unresolved_addr2line_output(self):
+ raw = image(entries=[(1, 2, [0x1234])])
+ with tempfile.TemporaryDirectory() as temp_dir:
+ addr2line = Path(temp_dir) / "addr2line"
+ addr2line.write_text(
+ "#!/bin/sh\nprintf '??\\n??:0\\n'\n",
+ encoding="utf-8")
+ addr2line.chmod(0o755)
+ env = os.environ.copy()
+ env["PATH"] = temp_dir
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT), "-", "--vmlinux", "vmlinux"],
+ input=raw, capture_output=True, env=env)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ self.assertIn(b"[0] 0x1234", result.stdout)
+
+ def test_cli_rejects_trailing_data(self):
+ raw = image(entries=[(1, 1, [0x1234])]) + b"junk"
+ result = subprocess.run([sys.executable, str(SCRIPT), "-"],
+ input=raw, capture_output=True)
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn(b"error: Trailing data", result.stderr)
+
+ def test_cli_accepts_valid_input(self):
+ raw = image(entries=[(1, 2, [0x1234])])
+ result = subprocess.run([sys.executable, str(SCRIPT), "-", "--json"],
+ input=raw, capture_output=True)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ self.assertIn(b'"stack_id": 1', result.stdout)
+
+ def test_stream_parser_handles_short_reads(self):
+ raw = image(entries=[(7, 3, [0x1234, 0x5678])])
+ self.assertEqual(list(stackmap_dump.parse_stackmap_stream(
+ ShortReadInput(raw))), [(7, 3, [0x1234, 0x5678])])
+
+ def test_rejects_stack_depth_above_version_1_limit(self):
+ raw = (struct.pack("<IIII", stackmap_dump.MAGIC, 1, 1, 0) +
+ struct.pack("<IIII", 7, 65, 1, 0))
+ with self.assertRaisesRegex(ValueError, "stack depth.*64"):
+ list(stackmap_dump.parse_stackmap_stream(io.BytesIO(raw)))
+
+ def test_accepts_stack_depth_at_version_1_limit(self):
+ ips = list(range(stackmap_dump.MAX_STACK_DEPTH_V1))
+ raw = image(entries=[(7, 1, ips)])
+ self.assertEqual(stackmap_dump.parse_stackmap_bin(raw), [(7, 1, ips)])
+
+ def test_rejects_stack_count_above_version_1_limit(self):
+ raw = struct.pack("<IIII", stackmap_dump.MAGIC, 1,
+ (1 << 18) + 1, 0)
+ with self.assertRaisesRegex(ValueError, "stack count.*262144"):
+ list(stackmap_dump.parse_stackmap_stream(io.BytesIO(raw)))
+
+ def test_accepts_stack_count_at_version_1_limit(self):
+ raw = struct.pack("<IIII", stackmap_dump.MAGIC, 1, 1 << 18, 0)
+ with self.assertRaisesRegex(ValueError,
+ "Truncated stack entry header"):
+ list(stackmap_dump.parse_stackmap_stream(io.BytesIO(raw)))
+
+ def test_cli_rejects_negative_top(self):
+ raw = image(entries=[(1, 2, [0x1234])])
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT), "-", "--top", "-1"],
+ input=raw, capture_output=True)
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn(b"--top must be non-negative", result.stderr)
+
+ def test_cli_top_zero_remains_unlimited(self):
+ raw = image(entries=[(1, 2, [0x1000]), (2, 1, [0x2000])])
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT), "-", "--json", "--top", "0"],
+ input=raw, capture_output=True)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ self.assertEqual([entry["stack_id"] for entry in
+ json.loads(result.stdout)], [1, 2])
+
+ def test_cli_top_n_is_descending_and_stable_for_ties(self):
+ raw = image(entries=[(1, 3, [0x1000]),
+ (2, 9, [0x2000]),
+ (3, 9, [0x3000]),
+ (4, 9, [0x4000]),
+ (5, 1, [0x5000])])
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT), "-", "--json", "--top", "2"],
+ input=raw, capture_output=True)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ self.assertEqual([(entry["stack_id"], entry["ref_count"])
+ for entry in json.loads(result.stdout)],
+ [(2, 9), (3, 9)])
+
+ def test_rejects_zero_stack_depth(self):
+ raw = (struct.pack("<IIII", stackmap_dump.MAGIC, 1, 1, 0) +
+ struct.pack("<IIII", 7, 0, 1, 0))
+ with self.assertRaisesRegex(ValueError, "stack depth.*minimum is 1"):
+ list(stackmap_dump.parse_stackmap_stream(io.BytesIO(raw)))
+
+ def test_cli_empty_text_output_format(self):
+ result = subprocess.run([sys.executable, str(SCRIPT), "-"],
+ input=image(), capture_output=True)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ self.assertEqual(result.stdout, b"")
+ self.assertEqual(result.stderr, b"Total: 0 stack records\n")
+
+ def test_cli_empty_json_output_format(self):
+ result = subprocess.run([sys.executable, str(SCRIPT), "-", "--json"],
+ input=image(), capture_output=True)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ self.assertEqual(result.stdout, b"[]\n")
+ self.assertEqual(result.stderr, b"Total: 0 stack records\n")
+
+ def test_cli_multi_record_text_output_format(self):
+ raw = image(entries=[(7, 3, [0x1234,
+ stackmap_dump.FTRACE_TRAMPOLINE_MARKER]),
+ (9, 1, [0xabcd])])
+ result = subprocess.run([sys.executable, str(SCRIPT), "-"],
+ input=raw, capture_output=True)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ self.assertEqual(
+ result.stdout.decode(),
+ "stack_id 7 [ref 3, depth 2]\n"
+ " [0] 0x1234\n"
+ " [1] [FTRACE TRAMPOLINE]\n\n"
+ "stack_id 9 [ref 1, depth 1]\n"
+ " [0] 0xabcd\n\n")
+ self.assertEqual(result.stderr, b"Total: 2 stack records\n")
+
+ def test_cli_multi_record_json_output_format(self):
+ raw = image(entries=[(7, 3, [0x1234]), (9, 1, [0xabcd])])
+ result = subprocess.run([sys.executable, str(SCRIPT), "-", "--json"],
+ input=raw, capture_output=True)
+ self.assertEqual(result.returncode, 0, result.stderr.decode())
+ expected = json.dumps([
+ {"stack_id": 7, "ref_count": 3, "ips": ["0x1234"]},
+ {"stack_id": 9, "ref_count": 1, "ips": ["0xabcd"]},
+ ], indent=2) + "\n"
+ self.assertEqual(result.stdout.decode(), expected)
+ self.assertEqual(result.stderr, b"Total: 2 stack records\n")
+
+ def test_vmlinux_text_output_format(self):
+ raw = image(entries=[(7, 3, [
+ 0x1000, 0x2000, stackmap_dump.FTRACE_TRAMPOLINE_MARKER])])
+ output = io.StringIO()
+ error = io.StringIO()
+ with mock.patch.object(stackmap_dump, "batch_addr2line",
+ return_value={0x1000: "resolved"}):
+ result = stackmap_dump.main(
+ ["--vmlinux", "vmlinux"], input_file=io.BytesIO(raw),
+ output=output, error=error)
+
+ self.assertEqual(result, 0, error.getvalue())
+ self.assertEqual(
+ output.getvalue(),
+ "stack_id 7 [ref 3, depth 3]\n"
+ " [0] 0x1000 resolved\n"
+ " [1] 0x2000\n"
+ " [2] [FTRACE TRAMPOLINE]\n\n")
+ self.assertEqual(error.getvalue(), "Total: 1 stack records\n")
+
+ def test_vmlinux_json_output_format(self):
+ raw = image(entries=[(7, 3, [
+ 0x1000, 0x2000, stackmap_dump.FTRACE_TRAMPOLINE_MARKER])])
+ output = io.StringIO()
+ error = io.StringIO()
+ with mock.patch.object(stackmap_dump, "batch_addr2line",
+ return_value={0x1000: "resolved"}):
+ result = stackmap_dump.main(
+ ["--json", "--vmlinux", "vmlinux"],
+ input_file=io.BytesIO(raw), output=output, error=error)
+
+ expected = json.dumps([{
+ "stack_id": 7,
+ "ref_count": 3,
+ "ips": ["0x1000", "0x2000", "0x7fffffff"],
+ "symbols": ["resolved", "0x2000", "[FTRACE TRAMPOLINE]"],
+ }], indent=2) + "\n"
+ self.assertEqual(result, 0, error.getvalue())
+ self.assertEqual(output.getvalue(), expected)
+ self.assertEqual(error.getvalue(), "Total: 1 stack records\n")
+
+ def test_json_malformed_later_record_leaves_stdout_empty(self):
+ raw = image(entries=[(1, 2, [0x1000]), (2, 1, [0x2000])])[:-1]
+ output = io.StringIO()
+ error = io.StringIO()
+
+ result = stackmap_dump.main(["--json"], input_file=io.BytesIO(raw),
+ output=output, error=error)
+
+ self.assertEqual(result, 1)
+ self.assertEqual(output.getvalue(), "")
+ self.assertIn("error: Truncated stack IP data", error.getvalue())
+
+ def test_json_later_addr2line_failure_leaves_stdout_empty(self):
+ raw = image(entries=[(stack_id, 1, [0x1000 + stack_id])
+ for stack_id in range(129)])
+ output = io.StringIO()
+ error = io.StringIO()
+ with mock.patch.object(
+ stackmap_dump, "batch_addr2line",
+ side_effect=[{}, RuntimeError("addr2line failed: later batch")]):
+ result = stackmap_dump.main(
+ ["--json", "--vmlinux", "vmlinux"],
+ input_file=io.BytesIO(raw), output=output, error=error)
+
+ self.assertEqual(result, 1)
+ self.assertEqual(output.getvalue(), "")
+ self.assertEqual(error.getvalue(),
+ "error: addr2line failed: later batch\n")
+
+ def test_text_streams_before_reading_the_next_record(self):
+ raw = image(entries=[(1, 2, [0x1000]), (2, 1, [0x2000])])
+ output = io.StringIO()
+ first_record_end = (stackmap_dump.HEADER_SIZE +
+ stackmap_dump.ENTRY_SIZE + 8)
+ input_file = StreamingGuardInput(raw, first_record_end, output)
+ error = io.StringIO()
+
+ result = stackmap_dump.main([], input_file=input_file,
+ output=output, error=error)
+
+ self.assertEqual(result, 0, error.getvalue())
+ self.assertIn("stack_id 2", output.getvalue())
+
+ def test_json_input_uses_only_bounded_reads(self):
+ raw = image(entries=[(1, 2, [0x1000]), (2, 1, [0x2000])])
+ output = io.StringIO()
+ error = io.StringIO()
+
+ result = stackmap_dump.main(
+ ["--json"], input_file=ShortReadInput(raw),
+ output=output, error=error)
+
+ self.assertEqual(result, 0, error.getvalue())
+ self.assertEqual([entry["stack_id"]
+ for entry in json.loads(output.getvalue())], [1, 2])
+
+ def test_json_does_not_materialize_all_records(self):
+ TrackedRecord.live_count = 0
+ TrackedRecord.max_live_count = 0
+
+ def records(_stream):
+ for stack_id in range(1000):
+ yield TrackedRecord(stack_id)
+
+ output = io.StringIO()
+ with mock.patch.object(stackmap_dump, "parse_stackmap_stream",
+ side_effect=records):
+ result = stackmap_dump.main(
+ ["--json"], input_file=io.BytesIO(), output=output,
+ error=io.StringIO())
+
+ self.assertEqual(result, 0)
+ self.assertLessEqual(TrackedRecord.max_live_count, 2)
+ self.assertEqual(len(json.loads(output.getvalue())), 1000)
+
+ def test_vmlinux_symbolization_limits_unique_addresses_per_call(self):
+ raw = image(entries=[
+ (stack_id, 1, [0x10000 + stack_id * 4 + frame
+ for frame in range(4)])
+ for stack_id in range(50)])
+ calls = []
+
+ def resolve(_vmlinux, addrs):
+ calls.append(list(addrs))
+ return {}
+
+ with mock.patch.object(stackmap_dump, "batch_addr2line",
+ side_effect=resolve):
+ result = stackmap_dump.main(
+ ["--vmlinux", "vmlinux"], input_file=io.BytesIO(raw),
+ output=io.StringIO(), error=io.StringIO())
+
+ self.assertEqual(result, 0)
+ self.assertLessEqual(max(map(len, calls)), 128)
+ self.assertEqual(stackmap_dump.SYMBOL_BATCH_ADDRESSES, 128)
+
+ def test_vmlinux_duplicate_addresses_keep_record_batches_bounded(self):
+ TrackedRecord.live_count = 0
+ TrackedRecord.max_live_count = 0
+
+ def records(_stream):
+ for stack_id in range(1000):
+ yield TrackedRecord(stack_id, ip=0x1000)
+
+ with mock.patch.object(stackmap_dump, "parse_stackmap_stream",
+ side_effect=records), mock.patch.object(
+ stackmap_dump, "batch_addr2line",
+ return_value={}):
+ result = stackmap_dump.main(
+ ["--json", "--vmlinux", "vmlinux"],
+ input_file=io.BytesIO(), output=io.StringIO(),
+ error=io.StringIO())
+
+ self.assertEqual(result, 0)
+ self.assertLessEqual(TrackedRecord.max_live_count, 130)
+
+ def test_vmlinux_rejects_aggregate_symbolization_work_above_limit(self):
+ raw = image(entries=[(stack_id, 1, [0x10000 + stack_id])
+ for stack_id in range(8193)])
+ error = io.StringIO()
+ with mock.patch.object(stackmap_dump, "batch_addr2line",
+ return_value={}):
+ result = stackmap_dump.main(
+ ["--vmlinux", "vmlinux"], input_file=io.BytesIO(raw),
+ output=io.StringIO(), error=error)
+
+ self.assertNotEqual(result, 0)
+ self.assertIn("symbolization work limit", error.getvalue())
+
+ def test_vmlinux_text_output_escapes_terminal_control_characters(self):
+ raw = image(entries=[(7, 3, [0x1000])])
+ output = io.StringIO()
+ with mock.patch.object(
+ stackmap_dump, "batch_addr2line",
+ return_value={0x1000: "name\x1b[2J\rspoof\tend"}):
+ result = stackmap_dump.main(
+ ["--vmlinux", "vmlinux"], input_file=io.BytesIO(raw),
+ output=output, error=io.StringIO())
+
+ self.assertEqual(result, 0)
+ self.assertIn(r"name\x1b[2J\x0dspoof\x09end", output.getvalue())
+ self.assertNotIn("\x1b", output.getvalue())
+ self.assertNotIn("\r", output.getvalue())
+ self.assertNotIn("\t", output.getvalue())
+
+ def test_help_states_vmlinux_kaslr_and_work_limits(self):
+ result = subprocess.run([sys.executable, str(SCRIPT), "--help"],
+ capture_output=True, text=True, check=True)
+ self.assertIn("KASLR", result.stdout)
+ self.assertIn("module", result.stdout)
+ self.assertIn("8192", result.stdout)
+ self.assertIn("64 batches", result.stdout)
+
+
+if __name__ == "__main__":
+ unittest.main()
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 08/10] selftests/ftrace: add a stackmap basic functionality test
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
` (6 preceding siblings ...)
2026-09-12 8:37 ` [RFC PATCH v7 07/10] tools/tracing: add a parser for the stackmap binary export Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 09/10] selftests/ftrace: add a stackmap reset and binary ABI test Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 10/10] selftests/ftrace: add a stackmap instance gating test Li Pengfei
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Exercise stackmap end to end through sched_switch event stack capture.
Enable stackmap and stacktrace, run a directly owned worker, and limit
the event source to switches whose prev_pid or next_pid is that worker.
Require stack ids, map records and successful lookups.
Reset the map five times while tracing and the filtered writer remain
active. Before each reset, require at least eight successes. Immediately
after reset, send SIGSTOP and wait until /proc reports the worker in a
stopped state before disabling tracing and sampling counters. Require the
post-reset success count to be below the pre-reset count, proving that the
sample belongs to a new generation. Reset again while the writer is
stopped and require exactly zero entries before resuming and refilling.
After the final refill, stop the writer and take one stable statistics
sample. Since insertion increments both entries and successes while a hit
increments successes only, require successes > entries to prove record
reuse without assuming one duplicate-prone record has ref_count > 1.
Require every post-reset stack id in the trace to resolve in the current
map. A nonzero drop count remains valid because failed map lookups try the
normal full-stack fallback.
Own the worker directly so cleanup can kill and wait for the exact
process. HUP, INT and TERM exit through the EXIT cleanup path. Cleanup
disables tracing and the event, removes the filter, turns off both
options, and resets the map.
Use element-record terminology for entries because concurrent duplicate
records are permitted by the lock-free insertion algorithm.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
.../ftrace/test.d/ftrace/stackmap-basic.tc | 242 ++++++++++++++++++
1 file changed, 242 insertions(+)
create mode 100644 tools/testing/selftests/ftrace/test.d/ftrace/stackmap-basic.tc
diff --git a/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-basic.tc b/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-basic.tc
new file mode 100644
index 000000000000..ade29b648a48
--- /dev/null
+++ b/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-basic.tc
@@ -0,0 +1,242 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# description: ftrace - stackmap basic functionality
+# requires: stack_map stack_map_stat options/stackmap options/stacktrace events/sched/sched_switch/enable
+
+# Test that ftrace stackmap deduplication works:
+# 1. Enable stackmap and stack capture for sched_switch events
+# 2. Run a scheduling workload briefly
+# 3. Verify trace contains <stack_id> events
+# 4. Verify stack_map has entries and at least some successes. Drops are
+# a legitimate by-design fallback counter and may be nonzero.
+# 5. Verify reset succeeds while tracing is active (it clears the map
+# only and leaves the ring buffer alone)
+# 6. Verify reset also clears the map when tracing is stopped
+
+fail() {
+ echo "FAIL: $1"
+ exit_fail
+}
+
+wait_for_worker_stopped() {
+ tries=0
+ while [ "$tries" -lt 100 ]; do
+ state=
+ if state=$(awk '/^State:/ { print $2 }' "/proc/$worker/status" \
+ 2>/dev/null); then
+ case "$state" in
+ T|t)
+ return 0
+ ;;
+ esac
+ else
+ return 1
+ fi
+ sleep 0.01
+ tries=$((tries + 1))
+ done
+ return 1
+}
+
+worker=
+
+cleanup() {
+ disable_tracing 2>/dev/null || :
+ if [ -n "$worker" ]; then
+ kill -CONT "$worker" 2>/dev/null || :
+ kill "$worker" 2>/dev/null || :
+ wait "$worker" 2>/dev/null || :
+ fi
+ echo 0 > events/sched/sched_switch/filter 2>/dev/null || :
+ echo 0 > events/sched/sched_switch/enable 2>/dev/null || :
+ echo 0 > options/stackmap 2>/dev/null || :
+ echo 0 > options/stacktrace 2>/dev/null || :
+ echo 0 > stack_map 2>/dev/null || :
+}
+trap cleanup EXIT
+trap 'exit 1' HUP INT TERM
+
+disable_tracing
+clear_trace
+echo 0 > stack_map || fail "initial stackmap reset failed"
+
+# sched_switch gives a bounded, inexpensive writer path through
+# __ftrace_trace_stack(), unlike full function tracing under QEMU.
+echo 1 > options/stackmap
+echo 1 > options/stacktrace
+echo 1 > events/sched/sched_switch/enable
+
+# Keep a directly owned workload runnable while sched_switch records stacks.
+(while :; do :; done) &
+worker=$!
+echo "prev_pid == $worker || next_pid == $worker" > \
+ events/sched/sched_switch/filter || fail "could not filter sched_switch workload"
+
+enable_tracing
+sleep 1
+
+# Lock in the precondition that makes this an active-writer reset test.
+[ "$(cat events/sched/sched_switch/enable)" = "1" ] ||
+ fail "sched_switch event is not enabled before reset"
+[ "$(cat options/stackmap)" = "1" ] ||
+ fail "stackmap option is not enabled before reset"
+[ "$(cat options/stacktrace)" = "1" ] ||
+ fail "stacktrace option is not enabled before reset"
+[ "$(cat tracing_on)" = "1" ] ||
+ fail "tracing is not on before reset"
+kill -0 "$worker" 2>/dev/null || fail "reset workload exited early"
+
+# Reset repeatedly while the owned writer is active, then again once it is
+# quiesced. Only deterministic properties are asserted:
+#
+# - While the writer runs, the reset must be accepted. No counter is
+# compared here: the writer resumes claiming records as soon as reset()
+# returns, so any snapshot taken afterwards is a moving target and would
+# make this test flaky rather than prove anything.
+# - With the writer stopped, sched_switch filtered to it, and tracing off,
+# no stackmap writer remains, so the following reset must leave exactly
+# zero entries and zero successes. That is checked every iteration.
+#
+# That an active reset really starts a new generation is proven separately and
+# deterministically in stackmap-reset.tc, where a binary fd held across the
+# reset must observe its own generation and then ESTALE.
+i=0
+while [ "$i" -lt 5 ]; do
+ tries=0
+ pre_successes=0
+ while [ "$tries" -lt 100 ]; do
+ pre_successes=$(awk '/^successes:/ { print $2 }' stack_map_stat)
+ : "${pre_successes:=0}"
+ [ "$pre_successes" -ge 8 ] && break
+ sleep 0.01
+ tries=$((tries + 1))
+ done
+ [ "$pre_successes" -ge 8 ] ||
+ fail "active writer did not reach 8 successes before reset"
+
+ echo 0 > stack_map || fail "stackmap reset failed with active writer"
+
+ kill -STOP "$worker" || fail "could not stop reset workload"
+ wait_for_worker_stopped ||
+ fail "reset workload did not enter the stopped state"
+ disable_tracing
+
+ echo 0 > stack_map || fail "stopped-writer verification reset failed"
+ reset_entries=$(awk '/^entries:/ { print $2 }' stack_map_stat)
+ : "${reset_entries:=-1}"
+ [ "$reset_entries" -eq 0 ] ||
+ fail "stopped-writer reset left $reset_entries entries"
+ reset_successes=$(awk '/^successes:/ { print $2 }' stack_map_stat)
+ : "${reset_successes:=-1}"
+ [ "$reset_successes" -eq 0 ] ||
+ fail "stopped-writer reset left successes=$reset_successes"
+
+ kill -CONT "$worker" || fail "could not resume reset workload"
+ enable_tracing
+ i=$((i + 1))
+done
+
+# Reset intentionally leaves the ring buffer untouched. Drop those old ids
+# here so every id inspected below belongs to the final map generation.
+clear_trace
+
+# Wait until aggregate counters prove that at least one operation reused an
+# existing element record: every insertion increments both entries and
+# successes, while a hit increments successes only. Stop the worker before
+# taking the decisive sample so both the counters and ring buffer are stable.
+entries_after_active_reset=0
+successes_after_active_reset=0
+i=0
+while [ "$i" -lt 50 ]; do
+ stats=$(cat stack_map_stat)
+ entries_after_active_reset=$(printf '%s\n' "$stats" |
+ awk '/^entries:/ { print $2 }')
+ successes_after_active_reset=$(printf '%s\n' "$stats" |
+ awk '/^successes:/ { print $2 }')
+ : "${entries_after_active_reset:=0}"
+ : "${successes_after_active_reset:=0}"
+ if [ "$entries_after_active_reset" -gt 0 ] &&
+ [ "$successes_after_active_reset" -gt \
+ "$entries_after_active_reset" ]; then
+ break
+ fi
+ sleep 0.1
+ i=$((i + 1))
+done
+
+kill -STOP "$worker" || fail "could not stop final reset workload"
+wait_for_worker_stopped ||
+ fail "final reset workload did not enter the stopped state"
+disable_tracing
+
+stats=$(cat stack_map_stat)
+entries_after_active_reset=$(printf '%s\n' "$stats" |
+ awk '/^entries:/ { print $2 }')
+successes_after_active_reset=$(printf '%s\n' "$stats" |
+ awk '/^successes:/ { print $2 }')
+: "${entries_after_active_reset:=0}"
+: "${successes_after_active_reset:=0}"
+if [ "$entries_after_active_reset" -eq 0 ]; then
+ fail "stackmap did not refill after active-writer reset"
+fi
+if [ "$successes_after_active_reset" -le "$entries_after_active_reset" ]; then
+ fail "aggregate counters show no element-record reuse after reset"
+fi
+
+kill -CONT "$worker" 2>/dev/null || :
+kill "$worker" 2>/dev/null || :
+wait "$worker" 2>/dev/null || :
+worker=
+
+# Stop the event before inspecting the fixed buffer and map contents.
+echo 0 > events/sched/sched_switch/enable
+trace_ids=$(sed -n 's/.*<stack_id \([0-9][0-9]*\)>.*/\1/p' trace | sort -nu)
+if [ -z "$trace_ids" ]; then
+ fail "trace has no <stack_id> events after active-writer reset"
+fi
+
+echo 0 > options/stackmap
+
+# Check stack_map_stat
+entries=$(grep "^entries:" stack_map_stat | awk '{print $2}')
+: "${entries:=0}"
+if [ "$entries" -eq 0 ]; then
+ fail "stackmap has zero entries after active-writer reset"
+fi
+
+successes=$(grep "^successes:" stack_map_stat | awk '{print $2}')
+: "${successes:=0}"
+if [ "$successes" -eq 0 ]; then
+ fail "stackmap has zero successes"
+fi
+
+drops=$(grep "^drops:" stack_map_stat | awk '{print $2}')
+: "${drops:=0}"
+# drops is a legitimate by-design fallback counter: when the map is full
+# or under heavy probe pressure, stackmap falls back to recording a full
+# stack instead of a stack_id. A nonzero drops count is therefore allowed
+# as long as deduplication also produced successful stack_id events.
+
+# Every observed id was emitted after the final reset and must resolve in
+# the current map generation.
+for id in $trace_ids; do
+ grep -q "^stack_id $id " stack_map ||
+ fail "trace stack id $id has no current stack_map record"
+done
+
+# Check stack_map text output is parseable
+first_id=$(grep "^stack_id" stack_map | head -1 | awk '{print $2}')
+if [ -z "$first_id" ]; then
+ fail "stack_map output has no stack_id entries"
+fi
+
+# Test reset works when tracing is stopped as well
+echo 0 > stack_map
+entries_after=$(cat stack_map_stat | grep "^entries:" | awk '{print $2}')
+: "${entries_after:=-1}"
+if [ "$entries_after" -ne 0 ]; then
+ fail "stackmap reset did not clear entries (got $entries_after)"
+fi
+
+echo "stackmap basic test passed: $entries element records, $successes successes, $drops drops"
+exit 0
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 09/10] selftests/ftrace: add a stackmap reset and binary ABI test
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
` (7 preceding siblings ...)
2026-09-12 8:37 ` [RFC PATCH v7 08/10] selftests/ftrace: add a stackmap basic functionality test Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 10/10] selftests/ftrace: add a stackmap instance gating test Li Pengfei
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Test reset semantics, the version 1 binary ABI, generation changes and
open-time record membership. Keep a directly owned busy-loop worker
alive across the initial capture and the helper's later refill so the
test does not depend on incidental system activity.
Build a complete pre-reset binary snapshot, keep a second descriptor
open across reset, and compare every byte returned by that descriptor
with the old snapshot prefix. Two terminal outcomes are valid:
- the complete old snapshot followed by EOF
- an incomplete exact old prefix followed by ESTALE
This accepts ESTALE immediately after the first byte while rejecting any
byte from the new generation. Reopening after reset must yield an empty
16-byte header. An fd opened on an empty map must also keep its empty
record set when the same generation later gains records.
Run the helper's host-side --selftest from the standard ftracetest entry
to cover complete and short mixed-generation data, zero-additional-byte
ESTALE, invalid terminal states and overlong output. If disabling tracing
fails after the refill, retry the disable once as best effort before
reporting the original error.
The shell test also verifies native-endian magic and version, record
depth and reference count, that reset preserves ring-buffer stack ids,
and that reset clears statistics. Cleanup kills and waits for the owned
worker, and signal exits use the normal EXIT cleanup path.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
tools/testing/selftests/ftrace/.gitignore | 1 +
tools/testing/selftests/ftrace/Makefile | 2 +-
.../selftests/ftrace/stackmap_bin_test.c | 383 ++++++++++++++++++
.../ftrace/test.d/ftrace/stackmap-reset.tc | 174 ++++++++
4 files changed, 559 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/ftrace/stackmap_bin_test.c
create mode 100644 tools/testing/selftests/ftrace/test.d/ftrace/stackmap-reset.tc
diff --git a/tools/testing/selftests/ftrace/.gitignore b/tools/testing/selftests/ftrace/.gitignore
index 4d7fcb828850..1661f0bb426f 100644
--- a/tools/testing/selftests/ftrace/.gitignore
+++ b/tools/testing/selftests/ftrace/.gitignore
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
logs
poll
+stackmap_bin_test
diff --git a/tools/testing/selftests/ftrace/Makefile b/tools/testing/selftests/ftrace/Makefile
index 7c12263f8260..9e9bfb4c3f27 100644
--- a/tools/testing/selftests/ftrace/Makefile
+++ b/tools/testing/selftests/ftrace/Makefile
@@ -6,6 +6,6 @@ TEST_PROGS := ftracetest-ktap
TEST_FILES := test.d settings
EXTRA_CLEAN := $(OUTPUT)/logs/*
-TEST_GEN_FILES := poll
+TEST_GEN_FILES := poll stackmap_bin_test
include ../lib.mk
diff --git a/tools/testing/selftests/ftrace/stackmap_bin_test.c b/tools/testing/selftests/ftrace/stackmap_bin_test.c
new file mode 100644
index 000000000000..a77b7440c143
--- /dev/null
+++ b/tools/testing/selftests/ftrace/stackmap_bin_test.c
@@ -0,0 +1,383 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Test stack_map_bin generation handling across read() calls. */
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static ssize_t read_nointr(int fd, void *buf, size_t count)
+{
+ ssize_t n;
+
+ do {
+ n = read(fd, buf, count);
+ } while (n < 0 && errno == EINTR);
+
+ return n;
+}
+
+static ssize_t snapshot_size(const char *path)
+{
+ char buf[4096];
+ ssize_t len = 0;
+ ssize_t n;
+ int fd;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return -1;
+ while ((n = read_nointr(fd, buf, sizeof(buf))) > 0)
+ len += n;
+ if (n < 0)
+ len = -1;
+ close(fd);
+ return len;
+}
+
+static int load_snapshot(const char *path, unsigned char **snapshot,
+ size_t *snapshot_len)
+{
+ unsigned char extra;
+ unsigned char *data;
+ ssize_t measured;
+ ssize_t n;
+ size_t offset = 0;
+ int saved_errno;
+ int fd;
+
+ measured = snapshot_size(path);
+ if (measured < 0)
+ return -1;
+ data = malloc(measured ? (size_t)measured : 1);
+ if (!data)
+ return -1;
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ goto err_free;
+ while (offset < (size_t)measured) {
+ n = read_nointr(fd, data + offset, (size_t)measured - offset);
+ if (n <= 0) {
+ if (!n)
+ errno = EIO;
+ goto err_close;
+ }
+ offset += n;
+ }
+ n = read_nointr(fd, &extra, 1);
+ if (n != 0) {
+ if (n > 0)
+ errno = EIO;
+ goto err_close;
+ }
+ if (close(fd))
+ goto err_free;
+ *snapshot = data;
+ *snapshot_len = offset;
+ return 0;
+
+err_close:
+ saved_errno = errno;
+ close(fd);
+ errno = saved_errno;
+err_free:
+ saved_errno = errno;
+ free(data);
+ errno = saved_errno;
+ return -1;
+}
+
+static int write_control(const char *path, const char *value)
+{
+ ssize_t len = strlen(value);
+ ssize_t n;
+ int saved_errno;
+ int fd;
+
+ fd = open(path, O_WRONLY);
+ if (fd < 0)
+ return -1;
+ do {
+ n = write(fd, value, len);
+ } while (n < 0 && errno == EINTR);
+ if (n != len) {
+ if (n >= 0)
+ errno = EIO;
+ saved_errno = errno;
+ close(fd);
+ errno = saved_errno;
+ return -1;
+ }
+ return close(fd);
+}
+
+static int reset_map(const char *path)
+{
+ return write_control(path, "0\n");
+}
+
+enum old_export_result {
+ OLD_EXPORT_INVALID,
+ OLD_EXPORT_COMPLETE,
+ OLD_EXPORT_STALE,
+};
+
+static enum old_export_result
+validate_old_export(const unsigned char *snapshot, size_t snapshot_len,
+ const unsigned char *old_export, size_t old_export_len,
+ ssize_t n, int read_errno)
+{
+ if (snapshot_len <= 1 || old_export_len < 1 ||
+ old_export_len > snapshot_len ||
+ memcmp(snapshot, old_export, old_export_len))
+ return OLD_EXPORT_INVALID;
+ if (n == 0 && old_export_len == snapshot_len)
+ return OLD_EXPORT_COMPLETE;
+ if (n == -1 && read_errno == ESTALE &&
+ old_export_len < snapshot_len)
+ return OLD_EXPORT_STALE;
+ return OLD_EXPORT_INVALID;
+}
+
+static int run_selftests(void)
+{
+ static const unsigned char snapshot[] = { 0x46, 0x53, 0x4d, 0x42 };
+ static const unsigned char mixed_complete[] = { 0x46, 0x00, 0x00, 0x00 };
+ static const unsigned char mixed_short[] = { 0x46, 0x00 };
+ static const unsigned char too_long[] = {
+ 0x46, 0x53, 0x4d, 0x42, 0x00
+ };
+ int failures = 0;
+
+ if (validate_old_export(snapshot, sizeof(snapshot), mixed_complete,
+ sizeof(mixed_complete), 0, 0) !=
+ OLD_EXPORT_INVALID) {
+ fprintf(stderr, "mixed complete export was not INVALID\n");
+ failures++;
+ }
+ if (validate_old_export(snapshot, sizeof(snapshot), mixed_short,
+ sizeof(mixed_short), -1, ESTALE) !=
+ OLD_EXPORT_INVALID) {
+ fprintf(stderr, "mixed short export was not INVALID\n");
+ failures++;
+ }
+ if (validate_old_export(snapshot, sizeof(snapshot), snapshot, 1,
+ -1, ESTALE) != OLD_EXPORT_STALE) {
+ fprintf(stderr, "zero additional bytes plus ESTALE was not STALE\n");
+ failures++;
+ }
+ if (validate_old_export(snapshot, sizeof(snapshot), snapshot,
+ sizeof(snapshot), 0, 0) != OLD_EXPORT_COMPLETE ||
+ validate_old_export(snapshot, sizeof(snapshot), snapshot, 2,
+ -1, ESTALE) != OLD_EXPORT_STALE ||
+ validate_old_export(snapshot, sizeof(snapshot), snapshot, 2,
+ 0, 0) != OLD_EXPORT_INVALID ||
+ validate_old_export(snapshot, sizeof(snapshot), snapshot,
+ sizeof(snapshot), -1, ESTALE) !=
+ OLD_EXPORT_INVALID ||
+ validate_old_export(snapshot, sizeof(snapshot), too_long,
+ sizeof(too_long), 0, 0) != OLD_EXPORT_INVALID) {
+ fprintf(stderr, "old export terminal validation selftest failed\n");
+ failures++;
+ }
+ if (failures)
+ return 1;
+ printf("old export validation selftests passed\n");
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ char buf[64];
+ unsigned char empty[17];
+ unsigned char *old_export;
+ unsigned char *snapshot;
+ enum old_export_result result;
+ size_t old_export_len;
+ size_t snapshot_len;
+ size_t read_len;
+ ssize_t total, n;
+ uint32_t nr_stacks;
+ int read_errno;
+ int disable_errno;
+ int fd;
+ int i;
+
+ if (argc == 2 && !strcmp(argv[1], "--selftest"))
+ return run_selftests();
+ if (argc != 4) {
+ fprintf(stderr, "Usage: %s STACK_MAP_BIN STACK_MAP TRACING_ON\n",
+ argv[0]);
+ return 1;
+ }
+
+ if (load_snapshot(argv[1], &snapshot, &snapshot_len)) {
+ perror("snapshot stack_map_bin");
+ return 1;
+ }
+ if (snapshot_len <= 16) {
+ fprintf(stderr, "stack_map_bin has no records: %zu bytes\n",
+ snapshot_len);
+ free(snapshot);
+ return 1;
+ }
+ if (snapshot_len == SIZE_MAX) {
+ fprintf(stderr, "stack_map_bin snapshot is too large\n");
+ free(snapshot);
+ return 1;
+ }
+ old_export = malloc(snapshot_len + 1);
+ if (!old_export) {
+ perror("allocate old stack_map_bin export");
+ free(snapshot);
+ return 1;
+ }
+
+ fd = open(argv[1], O_RDONLY);
+ if (fd < 0) {
+ perror("open stack_map_bin");
+ free(old_export);
+ free(snapshot);
+ return 1;
+ }
+ n = read_nointr(fd, old_export, 1);
+ if (n != 1) {
+ if (n < 0)
+ perror("initial stack_map_bin read");
+ else
+ fprintf(stderr, "short initial stack_map_bin read: %zd\n", n);
+ close(fd);
+ free(old_export);
+ free(snapshot);
+ return 1;
+ }
+ if (old_export[0] != snapshot[0]) {
+ fprintf(stderr, "initial stack_map_bin byte differs from snapshot\n");
+ close(fd);
+ free(old_export);
+ free(snapshot);
+ return 1;
+ }
+ if (reset_map(argv[2])) {
+ perror("reset stack_map");
+ close(fd);
+ free(old_export);
+ free(snapshot);
+ return 1;
+ }
+
+ old_export_len = 1;
+ for (;;) {
+ read_len = snapshot_len + 1 - old_export_len;
+ if (read_len > sizeof(buf))
+ read_len = sizeof(buf);
+ errno = 0;
+ n = read_nointr(fd, old_export + old_export_len, read_len);
+ if (n <= 0)
+ break;
+ old_export_len += n;
+ if (old_export_len > snapshot_len)
+ break;
+ }
+ read_errno = errno;
+ result = validate_old_export(snapshot, snapshot_len, old_export,
+ old_export_len, n, read_errno);
+ if (result == OLD_EXPORT_COMPLETE) {
+ printf("old fd completed snapshot: consumed=%zu EOF\n",
+ old_export_len);
+ } else if (result == OLD_EXPORT_STALE) {
+ printf("old fd stopped at snapshot prefix: consumed=%zu ESTALE\n",
+ old_export_len);
+ } else {
+ fprintf(stderr,
+ "invalid old fd result: snapshot=%zu consumed=%zu n=%zd errno=%d\n",
+ snapshot_len, old_export_len, n, read_errno);
+ close(fd);
+ free(old_export);
+ free(snapshot);
+ return 1;
+ }
+ close(fd);
+ free(old_export);
+ free(snapshot);
+
+ fd = open(argv[1], O_RDONLY);
+ if (fd < 0) {
+ perror("reopen stack_map_bin");
+ return 1;
+ }
+ n = read_nointr(fd, empty, sizeof(empty));
+ if (n != 16) {
+ fprintf(stderr, "bad empty export size: %zd\n", n);
+ close(fd);
+ return 1;
+ }
+ memcpy(&nr_stacks, empty + 8, sizeof(nr_stacks));
+ n = read_nointr(fd, buf, 1);
+ if (nr_stacks != 0 || n != 0) {
+ fprintf(stderr, "bad empty export: nr_stacks=%u next_read=%zd\n",
+ nr_stacks, n);
+ close(fd);
+ return 1;
+ }
+ close(fd);
+
+ /*
+ * Open an empty export, then populate the map without resetting it.
+ * The old fd must retain its open-time empty record set even though a
+ * fresh open sees entries from the same generation.
+ */
+ fd = open(argv[1], O_RDONLY);
+ if (fd < 0) {
+ perror("open empty membership snapshot");
+ return 1;
+ }
+ if (write_control(argv[3], "1\n")) {
+ perror("enable tracing");
+ close(fd);
+ return 1;
+ }
+ for (i = 0; i < 500; i++) {
+ total = snapshot_size(argv[1]);
+ if (total > 16)
+ break;
+ usleep(10000);
+ }
+ if (write_control(argv[3], "0\n")) {
+ disable_errno = errno;
+ /* Best effort: a transient write failure must not leak tracing on. */
+ if (write_control(argv[3], "0\n"))
+ perror("retry disable tracing");
+ errno = disable_errno;
+ perror("disable tracing");
+ close(fd);
+ return 1;
+ }
+ if (total <= 16) {
+ fprintf(stderr, "map did not refill for membership test: %zd bytes\n",
+ total);
+ close(fd);
+ return 1;
+ }
+ n = read_nointr(fd, empty, sizeof(empty));
+ if (n != 16) {
+ fprintf(stderr, "old fd exported post-open records: %zd bytes\n", n);
+ close(fd);
+ return 1;
+ }
+ memcpy(&nr_stacks, empty + 8, sizeof(nr_stacks));
+ n = read_nointr(fd, buf, 1);
+ if (nr_stacks != 0 || n != 0) {
+ fprintf(stderr,
+ "old fd record set changed: nr_stacks=%u next_read=%zd\n",
+ nr_stacks, n);
+ close(fd);
+ return 1;
+ }
+ close(fd);
+
+ printf("stack_map_bin generation and membership tests passed\n");
+ return 0;
+}
diff --git a/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-reset.tc b/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-reset.tc
new file mode 100644
index 000000000000..35fc1b1ecf54
--- /dev/null
+++ b/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-reset.tc
@@ -0,0 +1,174 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# description: ftrace - stackmap reset ABI and binary generation handling
+# requires: stack_map stack_map_stat stack_map_bin options/stackmap options/stacktrace events/sched/sched_switch/enable od:program
+
+# Check reset, binary format, and old-fd generation behavior:
+# 1. Reset clears the map and leaves existing trace records intact.
+# 2. The binary header contains magic 'FSMB' and version 1.
+# 3. After reset, an old fd returns only its buffered generation: either
+# the exact remaining export and EOF, or buffered bytes then ESTALE.
+# Reopening after reset yields an empty 16-byte export.
+
+fail() {
+ echo "FAIL: $1"
+ exit_fail
+}
+
+BIN_TEST=${FTRACETEST_ROOT}/stackmap_bin_test
+if [ ! -x "$BIN_TEST" ]; then
+ echo "stackmap_bin_test program is not compiled!"
+ exit_unresolved
+fi
+"$BIN_TEST" --selftest || fail "stack_map_bin helper selftest failed"
+
+worker=
+
+cleanup() {
+ disable_tracing 2>/dev/null || :
+ if [ -n "$worker" ]; then
+ kill "$worker" 2>/dev/null || :
+ wait "$worker" 2>/dev/null || :
+ fi
+ echo 0 > events/sched/sched_switch/enable 2>/dev/null || :
+ echo 0 > options/stackmap 2>/dev/null || :
+ echo 0 > options/stacktrace 2>/dev/null || :
+ echo 0 > stack_map 2>/dev/null || :
+}
+trap cleanup EXIT
+trap 'exit 1' HUP INT TERM
+
+disable_tracing
+clear_trace
+echo 0 > stack_map || fail "initial stackmap reset failed"
+
+# Use sched_switch as a bounded, inexpensive writer through
+# __ftrace_trace_stack(). Full function tracing with per-function stacks is
+# prohibitively slow in small QEMU guests and is not required here.
+echo 1 > options/stackmap
+echo 1 > options/stacktrace
+echo 1 > events/sched/sched_switch/enable
+
+# Keep an owned task runnable for both this capture and the helper's later
+# open-time membership refill; do not depend on incidental host activity.
+(while :; do :; done) &
+worker=$!
+
+enable_tracing
+sleep 1
+disable_tracing
+
+# Sanity: the buffer must contain stack_id events before reset, otherwise
+# the buffer-untouched check below would be meaningless.
+before=$(grep -c "<stack_id" trace || true)
+: "${before:=0}"
+if [ "$before" -eq 0 ]; then
+ fail "no <stack_id> events captured before reset"
+fi
+
+# Validate a non-empty binary export before reset. od -tu4 uses the
+# target's native byte order, matching the kernel ABI on any endianness.
+magic=$(od -An -tx4 -N4 stack_map_bin | tr -d ' \n')
+[ "$magic" = "46534d42" ] ||
+ fail "stack_map_bin bad magic: 0x$magic (expected 46534d42)"
+ver=$(od -An -tu4 -j4 -N4 stack_map_bin | tr -d ' \n')
+[ "$ver" = "1" ] || fail "stack_map_bin version is $ver (expected 1)"
+nr_stacks=$(od -An -tu4 -j8 -N4 stack_map_bin | tr -d ' \n')
+: "${nr_stacks:=0}"
+[ "$nr_stacks" -gt 0 ] || fail "stack_map_bin has zero stacks before reset"
+first_nr=$(od -An -tu4 -j20 -N4 stack_map_bin | tr -d ' \n')
+: "${first_nr:=0}"
+if [ "$first_nr" -lt 1 ] || [ "$first_nr" -gt 64 ]; then
+ fail "first binary entry has invalid depth $first_nr"
+fi
+first_refs=$(od -An -tu4 -j24 -N4 stack_map_bin | tr -d ' \n')
+: "${first_refs:=0}"
+[ "$first_refs" -gt 0 ] || fail "first binary entry has zero ref_count"
+binary_bytes=$(wc -c < stack_map_bin)
+minimum_bytes=$((32 + first_nr * 8))
+if [ "$binary_bytes" -lt "$minimum_bytes" ]; then
+ fail "first binary entry is truncated: $binary_bytes < $minimum_bytes bytes"
+fi
+
+# Keep one binary fd open across reset, and require the stale-iterator path.
+#
+# The helper reads one byte, resets the map through a second fd, then drains
+# the first fd. A one-byte read cannot buffer the whole export: seq_read_iter()
+# stops filling once the buffered amount satisfies the request, and the binary
+# show() emits the 16-byte header as a record of its own. The old fd therefore
+# still has to start a new seq pass to continue, which is where the generation
+# is rechecked, so the drain must end in ESTALE at a strict prefix rather than
+# at EOF. Requiring that line keeps a regression from silently downgrading to
+# the fully buffered case and never exercising the check at all.
+bin_out=$("$BIN_TEST" stack_map_bin stack_map tracing_on)
+ret=$?
+printf '%s\n' "$bin_out"
+[ "$ret" -eq 0 ] || fail "stack_map_bin generation test failed (ret=$ret)"
+case "$bin_out" in
+*"old fd stopped at snapshot prefix"*) ;;
+*) fail "old binary fd did not reach the cross-pass ESTALE check" ;;
+esac
+
+# The helper refills the map for its membership check. Save the actual
+# stack-id records before the stopped reset below; comparing only their count
+# would miss replacement, reordering, or content corruption.
+before_records=$(grep "<stack_id" trace || true)
+if [ -z "$before_records" ]; then
+ fail "no <stack_id> records remain before stopped reset"
+fi
+
+# Reset clears the map only. It must succeed and must not disturb the
+# trace buffer.
+echo 0 > stack_map || fail "reset failed"
+
+after_records=$(grep "<stack_id" trace || true)
+if [ "$after_records" != "$before_records" ]; then
+ fail "reset changed the trace buffer records"
+fi
+before=$(printf '%s\n' "$before_records" | wc -l)
+
+entries=$(cat stack_map_stat | grep "^entries:" | awk '{print $2}')
+: "${entries:=-1}"
+if [ "$entries" -ne 0 ]; then
+ fail "stackmap still has $entries entries after reset"
+fi
+
+successes=$(grep "^successes:" stack_map_stat | awk '{print $2}')
+: "${successes:=-1}"
+if [ "$successes" -ne 0 ]; then
+ fail "reset left successes=$successes (expected 0)"
+fi
+
+drops=$(grep "^drops:" stack_map_stat | awk '{print $2}')
+: "${drops:=-1}"
+if [ "$drops" -ne 0 ]; then
+ fail "reset left drops=$drops (expected 0)"
+fi
+
+rate=$(cat stack_map_stat | grep "^success_rate:" | awk '{print $2}')
+if [ "$rate" != "0%" ]; then
+ fail "stackmap reset success_rate is '$rate' (expected 0%)"
+fi
+
+# Binary export header: magic 'FSMB' (0x46534D42) + version 1.
+# od -tx4 uses the target's native byte order, matching the kernel ABI.
+magic=$(od -An -tx4 -N4 stack_map_bin | tr -d ' \n')
+if [ "$magic" != "46534d42" ]; then
+ fail "stack_map_bin bad magic: 0x$magic (expected 46534d42)"
+fi
+ver=$(od -An -tx4 -j4 -N4 stack_map_bin | tr -d ' \n')
+if [ "$ver" != "00000001" ]; then
+ fail "stack_map_bin bad version: 0x$ver (expected 00000001)"
+fi
+nr_stacks=$(od -An -tu4 -j8 -N4 stack_map_bin | tr -d ' \n')
+: "${nr_stacks:=-1}"
+if [ "$nr_stacks" -ne 0 ]; then
+ fail "stack_map_bin has $nr_stacks stacks after reset"
+fi
+binary_bytes=$(wc -c < stack_map_bin)
+if [ "$binary_bytes" -ne 16 ]; then
+ fail "empty stack_map_bin is $binary_bytes bytes (expected 16)"
+fi
+
+echo "stackmap reset test passed: map cleared, $before stack_id events kept, ABI header ok"
+exit 0
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH v7 10/10] selftests/ftrace: add a stackmap instance gating test
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
` (8 preceding siblings ...)
2026-09-12 8:37 ` [RFC PATCH v7 09/10] selftests/ftrace: add a stackmap reset and binary ABI test Li Pengfei
@ 2026-09-12 8:37 ` Li Pengfei
9 siblings, 0 replies; 11+ messages in thread
From: Li Pengfei @ 2026-09-12 8:37 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: mathieu.desnoyers, mark.rutland, corbet, skhan, lkp,
linux-trace-kernel, linux-kernel, linux-doc, linux-kselftest,
zhangbo56, lipengfei28
From: Pengfei Li <lipengfei28@xiaomi.com>
Verify both enforcement points for the global-instance-only stackmap
option.
Require options/stackmap and stack_map on the top-level instance. Read a
shared-kernel random UUID from /proc/sys/kernel/random/uuid and combine
it with ftracetest's $PID to create collision-resistant owned and foreign
secondary-instance names, including across PID namespaces. Verify that
neither the option nor any stack_map node appears in the owned instance.
Also require writing stackmap through its aggregate trace_options file
to fail and leave the option unset.
Create the foreign instance as a sentinel. Remove the owned instance
explicitly and verify the sentinel remains, proving that cleanup does
not remove unrelated instances.
Defer signal exit across each mkdir and ownership assignment so cleanup
removes a directory only after this invocation has created it. Put mkdir
in an if condition so ftracetest's errexit mode cannot bypass return-code
handling, the explicit failure path, or ownership bookkeeping. Track both
directories separately and remove only those created by this test. Keep
the auxiliary stack_map_stat and stack_map_bin nodes optional on the
global side.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
.../test.d/ftrace/stackmap-instance-gate.tc | 114 ++++++++++++++++++
1 file changed, 114 insertions(+)
create mode 100644 tools/testing/selftests/ftrace/test.d/ftrace/stackmap-instance-gate.tc
diff --git a/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-instance-gate.tc b/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-instance-gate.tc
new file mode 100644
index 000000000000..818dbb7bfeb8
--- /dev/null
+++ b/tools/testing/selftests/ftrace/test.d/ftrace/stackmap-instance-gate.tc
@@ -0,0 +1,114 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# description: ftrace - stackmap option is gated to the top-level trace instance
+# requires: stack_map options/stackmap instances
+
+# The 'stackmap' option is added to TOP_LEVEL_TRACE_FLAGS, matching the
+# convention used for global-only options like 'printk' and 'record-cmd'.
+# Verify that:
+# 1. The global instance exposes options/stackmap and the required
+# stack_map node. stack_map_stat and stack_map_bin are auxiliary and
+# may be absent if their tracefs creation failed.
+# 2. A newly created secondary instance under instances/ does NOT expose
+# options/stackmap or any stack_map* nodes.
+
+fail() {
+ echo "FAIL: $1"
+ exit_fail
+}
+
+instance_token=$(cat /proc/sys/kernel/random/uuid 2>/dev/null) || {
+ echo "could not obtain a collision-resistant instance token"
+ exit_unresolved
+}
+[ -n "$instance_token" ] || exit_unresolved
+owned_instance=stackmap_gate_owned_${PID}_${instance_token}
+owned_dir=instances/$owned_instance
+foreign_instance=stackmap_gate_foreign_${PID}_${instance_token}
+foreign_dir=instances/$foreign_instance
+owned_created=0
+foreign_created=0
+
+remove_owned() {
+ if [ "$owned_created" -eq 1 ]; then
+ rmdir "$owned_dir" || return 1
+ owned_created=0
+ fi
+}
+
+cleanup() {
+ if [ "$owned_created" -eq 1 ]; then
+ rmdir "$owned_dir" 2>/dev/null || :
+ fi
+ if [ "$foreign_created" -eq 1 ]; then
+ rmdir "$foreign_dir" 2>/dev/null || :
+ fi
+}
+trap cleanup EXIT
+trap 'exit 1' HUP INT TERM
+
+# 1. Global instance must expose the option and required map node
+test -e options/stackmap || fail "options/stackmap missing on global instance"
+test -e stack_map || fail "stack_map missing on global instance"
+
+# 2. Create a foreign sentinel and an owned secondary instance. Defer signal
+# exit across each mkdir and ownership assignment: cleanup must remove a path
+# only after this invocation has successfully created it.
+interrupted=0
+trap 'interrupted=1' HUP INT TERM
+create_rc=0
+if mkdir "$foreign_dir"; then
+ foreign_created=1
+else
+ create_rc=$?
+fi
+trap 'exit 1' HUP INT TERM
+[ "$interrupted" -eq 0 ] || exit 1
+[ "$create_rc" -eq 0 ] || fail "could not create foreign sentinel instance"
+
+interrupted=0
+trap 'interrupted=1' HUP INT TERM
+create_rc=0
+if mkdir "$owned_dir"; then
+ owned_created=1
+else
+ create_rc=$?
+fi
+trap 'exit 1' HUP INT TERM
+[ "$interrupted" -eq 0 ] || exit 1
+[ "$create_rc" -eq 0 ] || fail "could not create secondary instance"
+
+if [ -e "$owned_dir/options/stackmap" ]; then
+ fail "secondary instance unexpectedly exposes options/stackmap"
+fi
+
+for f in stack_map stack_map_stat stack_map_bin; do
+ if [ -e "$owned_dir/$f" ]; then
+ fail "secondary instance unexpectedly has $f"
+ fi
+done
+
+# 3. The aggregate trace_options file still reaches set_tracer_flag(),
+# so writing 'stackmap' there must be rejected on a secondary
+# instance. Otherwise the bit could appear set in trace_options
+# while the hot path silently falls back to a full stack trace
+# (tr->stackmap == NULL).
+if echo stackmap > "$owned_dir/trace_options" 2>/dev/null; then
+ fail "secondary instance accepted 'echo stackmap > trace_options'"
+fi
+if grep -qw stackmap "$owned_dir/trace_options"; then
+ fail "secondary instance trace_options reports stackmap as set"
+fi
+
+remove_owned || fail "could not remove owned secondary instance"
+if [ -e "$owned_dir" ]; then
+ fail "owned secondary instance still exists after removal"
+fi
+if [ ! -d "$foreign_dir" ]; then
+ fail "owned cleanup removed foreign sentinel instance"
+fi
+rmdir "$foreign_dir" || fail "could not remove foreign sentinel instance"
+foreign_created=0
+
+echo "stackmap option gating to top-level instance works"
+exit 0
--
2.34.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-09-12 8:40 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 01/10] trace: add lock-free stackmap for stack trace deduplication Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 02/10] trace: use the stackmap from the ftrace stack recording path Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 03/10] trace: add stackmap statistics interface Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 04/10] trace: add stackmap binary export Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 05/10] trace: make the stackmap capacity settable on the kernel command line Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 06/10] Documentation: tracing: document the ftrace stackmap Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 07/10] tools/tracing: add a parser for the stackmap binary export Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 08/10] selftests/ftrace: add a stackmap basic functionality test Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 09/10] selftests/ftrace: add a stackmap reset and binary ABI test Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 10/10] selftests/ftrace: add a stackmap instance gating test Li Pengfei
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox