mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Marco Elver <elver@google.com>
To: Caleb Kan <calebkan1106@gmail.com>
Cc: Andrew Morton <akpm@linux-foundation.org>,
	linux-mm@kvack.org, linux-kernel@vger.kernel.org,
	kasan-dev@googlegroups.com, Vlastimil Babka <vbabka@kernel.org>,
	Alexander Potapenko <glider@google.com>,
	Dmitry Vyukov <dvyukov@google.com>,
	Andrey Konovalov <andreyknvl@gmail.com>,
	Oscar Salvador <osalvador@suse.de>,
	Caleb Kan <ckan@cloudflare.com>,
	kernel-team@cloudflare.com
Subject: Re: [PATCH RFC v2 10/11] stackdepot: share persistent stack prefixes with trie storage
Date: Tue, 20 Oct 2026 15:04:54 +0000	[thread overview]
Message-ID: <ateDFtLVwMByzZPG@elver.google.com> (raw)
In-Reply-To: <20260908-stackdepot-trie-v2-10-1996d5cef732@cloudflare.com>

On Tue, Sep 08, 2026 at 02:13PM +0100, Caleb Kan wrote:
> From: Caleb Kan <ckan@cloudflare.com>
> 
> Stack depot's hash backend deduplicates identical traces, but stores every
> different trace in full. Persistent traces often share most of their
> frames, so this wastes memory and can exhaust the pool limit. Once the
> pools are full, a new trace returns 0 and later diagnostics can lose useful
> stack information.
> 
> Add an opt-in path-compressed trie for persistent records saved without
> STACK_DEPOT_FLAG_GET or STACK_DEPOT_FLAG_COUNTABLE. Store a run of frames
> in each node and branch only where traces differ. Insertion can descend
> through an existing path, split a partial match, make an internal node a
> stored trace, or attach a new suffix.
> 
> Encode dense stack IDs in pool-index values that cannot name a hash pool.
> A sparse side table maps each ID to the node where the trace ends. Fetching
> a trace follows parent links from that node. Stored traces and IDs are not
> recycled.
> 
> Use the architecture hooks from the preceding patch to store a frame in 32
> bits only when it can be reconstructed exactly. Keep all other frames at
> full width.
> 
> Allocate trie nodes and child arrays from contiguous runs of 16-byte slots
> in the existing order-2 pools. Each pool records an upper bound on its
> largest free run so the allocator can skip pools that are too fragmented.
> Search from a next-fit cursor first, then scan the whole pool so released
> holes and runs that cross the cursor remain visible. Record the exact
> longest run only after a complete scan fails.
> 
> A child array must fit in one pool. On a 4 KiB, 64-bit system, one node can
> have at most 1,024 children. An insertion that would add a 1,025th child
> returns 0, while existing traces remain valid. The largest node observed on
> a pre-production server had about 835 children.
> 
> Take the writer lock before the existing pool lock. Reserve everything
> that can fail before publishing a trace. Release unpublished node and
> child slots immediately, but keep registered pools, side-table space,
> stored traces, and IDs. When a node or child array is replaced, keep it
> until a later allocating insertion sees that its RCU grace period has
> ended.
> 
> An NMI save only looks for an existing trie record and returns 0 on a miss.
> Outside NMI, a save gets one insertion attempt when
> STACK_DEPOT_FLAG_CAN_ALLOC is clear or its GFP flags do not allow spinning
> on raw locks. The attempt uses only pool and side-table space already
> available. If spinning is allowed, take the writer and pool locks;
> otherwise try each lock once. Access to the side-table page cache also uses
> a trylock. The attempt does not allocate, retry, wait for RCU, or fall back
> to the hash backend.
> 
> Allocating saves try at most three times. One try can skip preallocation
> after seeing new_pool, then lose that pool to another save. A maximum-depth
> insertion can also need two new pools. Trie insertion failure returns 0
> instead of consuming hash capacity.
> 
> Extend stack_depot_fetch_into(), stack_depot_print(), and
> stack_depot_snprint() to support both backends. Keep stack_depot_fetch()
> hash-only because it returns a pointer to contiguous depot-owned storage.
> Print 16 frames at a time so stack use stays fixed instead of growing with
> CONFIG_STACKDEPOT_MAX_FRAMES.
> 
> The hash and trie backends share stack_pools and the configured pool limit.
> A pool used by the trie is unavailable for hash-backed GET and COUNTABLE
> records. Hash handles reserve pool-index values through
> stack_depot_max_pools, and trie handles use the remaining values for stack
> IDs. With 64 KiB pages, the default limit leaves no values for trie IDs, so
> stack_depot_max_pools must be lowered before enabling the trie.
> 
> Rename the persistent_count and persistent_bytes debugfs counters with a
> hash prefix because they do not include trie storage.
> 
> Add the stackdepot.trie_enabled boot parameter and leave it disabled by
> default. Initialize the trie root before enabling the static key. If no
> handle values are available for trie IDs or allocating the root metadata
> fails, leave the hash backend initialized at its configured capacity.
> 
> I collected stack depot state from four live KASAN servers using the same
> kernel revision, 4 KiB pages, and an 8,192-pool limit. The servers ran for
> 61 to 67 hours and remained active while the values were collected, so the
> figures below are rounded.
> 
>                                arm64                 x86-64
>                         trie off trie on      trie off trie on
> Run time (hours)              61      64            65      67
> Stored records              162k     87k          498k    217k
> Registered pools           2,630     925         8,192   1,940
> Pool budget used             32%     11%          100%     24%
> 
> The servers ran different workloads and stored different traces, so this
> is not a controlled comparison. Exact memory savings depend on the
> workload.
> 
> The trie figures came from RFC v1. That version did not insert a new trace
> when a save could not allocate, so RFC v2 may store more traces and use
> more pools than shown here.
> 
> On arm64, the final allocator reduced the median number of bitmap probes
> from 36,939,363 to 1,529,482 without increasing pool use. The first-fit
> allocator used a median of 1,101 pools, while the final allocator used
> 1,100.
> 
> I also pinned a benchmark to one CPU on KASAN-enabled arm64 and x86-64
> systems running the Linux 6.18.48 port. It inserted 32,768 distinct
> 32-frame traces. Within each group of 64 traces, 75% of the frames were
> shared, and all frames could be compressed. The insertion order was
> shuffled. The warm save-hit and fetch tests then repeated the same traces
> 32 times.
> 
>                          arm64 CPU ns/op         x86-64 CPU ns/op
>                          hash       trie        hash         trie
> Insertion                 777      9,417         792       11,269
> Warm save hit             945      1,287         623        1,012
> Fetch                     251        511         123          476
> 
> First insertion was about 12 times slower on arm64 and 14 times slower on
> x86-64. A warm save hit was 1.4 to 1.6 times slower, and fetch was 2 to 4
> times slower. All four runs completed without save failures or validation
> errors.
> 
> Signed-off-by: Caleb Kan <ckan@cloudflare.com>
> ---
>  Documentation/admin-guide/kernel-parameters.txt |    7 +
>  include/linux/stackdepot.h                      |   23 +-
>  lib/stackdepot.c                                | 1600 ++++++++++++++++++++++-
>  3 files changed, 1616 insertions(+), 14 deletions(-)
> 
> diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
> index 68647ff4bdd2..b02bcbaef5dc 100644
> --- a/Documentation/admin-guide/kernel-parameters.txt
> +++ b/Documentation/admin-guide/kernel-parameters.txt
> @@ -7449,6 +7449,13 @@ Kernel parameters
>  			stack traces. Pools are allocated on-demand up to this
>  			limit. Default value is 8191 pools.
>  
> +	stackdepot.trie_enabled= [KNL]
> +			Format: <bool>
> +			Enable trie storage for persistent, non-refcounted
> +			stack depot records at boot. Disabled by default.
> +			stack_depot_max_pools must leave unused pool-index
> +			values for trie handles.
> +
>  	stacktrace	[FTRACE]
>  			Enable the stack tracer on boot up.
>  
> diff --git a/include/linux/stackdepot.h b/include/linux/stackdepot.h
> index 7ff67c70d727..3126d17b9265 100644
> --- a/include/linux/stackdepot.h
> +++ b/include/linux/stackdepot.h
> @@ -151,6 +151,12 @@ static inline int stack_depot_early_init(void)	{ return 0; }
>   * access. This flag does not imply %STACK_DEPOT_FLAG_CAN_ALLOC and is mutually
>   * exclusive with %STACK_DEPOT_FLAG_GET.
>   *
> + * When trie storage is enabled, persistent non-refcounted saves use trie
> + * storage. Constrained callers first look up an existing stack, then make one
> + * best-effort insertion attempt without allocating. NMI callers stop after the
> + * lookup. Other callers that cannot spin use trylocks and fail if a required
> + * lock is unavailable. Trie failures do not fall back to hash storage.
> + *
>   * If the provided stack trace comes from the interrupt context, only the part
>   * up to the interrupt entry is saved.
>   *
> @@ -159,7 +165,7 @@ static inline int stack_depot_early_init(void)	{ return 0; }
>   *          this is the case for contexts where neither %GFP_ATOMIC nor
>   *          %GFP_NOWAIT can be used (NMI, raw_spin_lock).
>   *
> - * Return: Handle of the stack struct stored in depot, 0 on failure
> + * Return: Handle of the stack trace stored in depot, 0 on failure
>   */
>  depot_stack_handle_t stack_depot_save_flags(unsigned long *entries,
>  					    unsigned int nr_entries,
> @@ -176,6 +182,10 @@ depot_stack_handle_t stack_depot_save_flags(unsigned long *entries,
>   * Does not increment the refcount on the saved stack trace; see
>   * stack_depot_save_flags() for more details.
>   *
> + * When trie storage is enabled, this can return trie-backed handles. Use
> + * stack_depot_fetch_into(), stack_depot_print(), or stack_depot_snprint() for
> + * backend-independent access to the stack contents.
> + *
>   * Context: Contexts where allocations via alloc_pages() are allowed;
>   *          see stack_depot_save_flags() for more details.
>   *
> @@ -199,9 +209,14 @@ struct stack_record *__stack_depot_get_stack_record(depot_stack_handle_t handle)
>  /**
>   * stack_depot_fetch - Fetch a stack trace from stack depot
>   *
> - * @handle:	Stack depot handle returned from stack_depot_save()
> + * @handle:	Hash-backed stack depot handle
>   * @entries:	Pointer to store the address of the stack trace
>   *
> + * This helper returns a pointer to stackdepot-owned contiguous storage for
> + * legacy hash-backed handles. Callers that need backend-independent access to
> + * stack contents should use stack_depot_fetch_into(), stack_depot_print(), or
> + * stack_depot_snprint(). Passing a trie-backed handle is invalid and may WARN.
> + *
>   * Return: Number of frames for the fetched stack
>   */
>  unsigned int stack_depot_fetch(depot_stack_handle_t handle,
> @@ -270,8 +285,8 @@ int stack_depot_snprint(depot_stack_handle_t handle, char *buf, size_t size,
>   *
>   * Drop a reference acquired by stack_depot_save_flags() with
>   * %STACK_DEPOT_FLAG_GET. Calling this for a handle saved without
> - * %STACK_DEPOT_FLAG_GET is invalid; persistent handles are owned by stack depot
> - * for the lifetime of the system.
> + * %STACK_DEPOT_FLAG_GET is invalid; persistent handles, including trie-backed
> + * handles, are owned by stack depot for the lifetime of the system.
>   *
>   * The stack trace is evicted once the number of stack_depot_put() calls matches
>   * the number of successful stack_depot_save_flags() calls with
> diff --git a/lib/stackdepot.c b/lib/stackdepot.c
> index 66c5e8594566..33e475d94131 100644
> --- a/lib/stackdepot.c
> +++ b/lib/stackdepot.c
> @@ -2,9 +2,11 @@
>  /*
>   * Stack depot - a stack trace storage that avoids duplication.
>   *
> - * Internally, stack depot maintains a hash table of unique stacktraces. The
> - * stack traces themselves are stored contiguously one after another in a set
> - * of separate page allocations.
> + * Internally, stack depot has two storage backends. Refcounted entries and
> + * callers that request STACK_DEPOT_FLAG_COUNTABLE use the legacy hash table with
> + * contiguous stack records in stack pools. Persistent non-refcounted entries
> + * can use trie storage when enabled; trie nodes share common frame prefixes and
> + * are published through RCU children containers.
>   *
>   * Author: Alexander Potapenko <glider@google.com>
>   * Copyright (C) 2016 Google, Inc.
> @@ -14,13 +16,19 @@
>  
>  #define pr_fmt(fmt) "stackdepot: " fmt
>  
> +#include <linux/bitmap.h>
> +#include <linux/build_bug.h>
>  #include <linux/debugfs.h>
> +#include <linux/errno.h>
>  #include <linux/gfp.h>
>  #include <linux/jhash.h>
> +#include <linux/jump_label.h>
>  #include <linux/kernel.h>
> +#include <linux/log2.h>
>  #include <linux/kmsan.h>
>  #include <linux/list.h>
>  #include <linux/mm.h>
> +#include <linux/moduleparam.h>
>  #include <linux/mutex.h>
>  #include <linux/poison.h>
>  #include <linux/printk.h>
> @@ -36,9 +44,12 @@
>  #include <linux/memblock.h>
>  #include <linux/kasan-enabled.h>
>  
> +#include <asm/stackdepot.h>
> +
>  /*
>   * The pool_index is offset by 1 so the first record does not have a 0 handle.
>   */
> +/* Parsed before mm_core_init(); trie handle decoding assumes this is then fixed. */
>  static unsigned int stack_max_pools __read_mostly =
>  	MIN((1LL << DEPOT_POOL_INDEX_BITS) - 1, 8192);
>  
> @@ -54,6 +65,9 @@ static bool __stack_depot_early_init_passed __initdata;
>  /* Initial seed for jhash2. */
>  #define STACK_HASH_SEED 0x9747b28c
>  
> +/* Bound 64-bit print scratch to 128 bytes while amortizing trie walks. */
> +#define STACK_DEPOT_PRINT_CHUNK_FRAMES 16
> +
>  /* Hash table of stored stack records. */
>  static struct list_head *stack_table;
>  /* Fixed order of the number of table buckets. Used when KASAN is enabled. */
> @@ -63,18 +77,18 @@ static unsigned int stack_hash_mask;
>  
>  /* The lock must be held when performing pool or freelist modifications. */
>  static DEFINE_RAW_SPINLOCK(pool_lock);
> -/* Array of memory regions that store stack records. */
> +/* Array of memory regions used by both stack depot backends. */
>  static void **stack_pools __pt_guarded_by(&pool_lock);
>  /* Newly allocated pool that is not yet added to stack_pools. */
>  static void *new_pool;
>  /* Number of pools in stack_pools. */
>  static int pools_num;
> -/* Offset to the unused space in the currently used pool. */
> +/* Offset to unused hash storage in the current pool. */
>  static size_t pool_offset __guarded_by(&pool_lock) = DEPOT_POOL_SIZE;
>  /* Freelist of stack records within stack_pools. */
>  static __guarded_by(&pool_lock) LIST_HEAD(free_stacks);
>  
> -/* Statistics counters for debugfs. */
> +/* Hash-backend statistics counters for debugfs. */
>  enum depot_counter_id {
>  	DEPOT_COUNTER_REFD_ALLOCS,
>  	DEPOT_COUNTER_REFD_FREES,
> @@ -90,12 +104,695 @@ static const char *const counter_names[] = {
>  	[DEPOT_COUNTER_REFD_FREES]	= "refcounted_frees",
>  	[DEPOT_COUNTER_REFD_INUSE]	= "refcounted_in_use",
>  	[DEPOT_COUNTER_FREELIST_SIZE]	= "freelist_size",
> -	[DEPOT_COUNTER_PERSIST_COUNT]	= "persistent_count",
> -	[DEPOT_COUNTER_PERSIST_BYTES]	= "persistent_bytes",
> +	[DEPOT_COUNTER_PERSIST_COUNT]	= "hash_persistent_count",
> +	[DEPOT_COUNTER_PERSIST_BYTES]	= "hash_persistent_bytes",
>  };
>  static_assert(ARRAY_SIZE(counter_names) == DEPOT_COUNTER_COUNT);
> +
> +enum stack_depot_frame_mode {
> +	STACK_DEPOT_FRAME_RAW,
> +	STACK_DEPOT_FRAME_COMPRESSED,
> +};
> +
> +/*
> + * A trie node stores one run of frames that all use the same payload format.
> + * Architectures may compress some frames to 32-bit payloads; mixed raw and
> + * compressed input is split across multiple trie nodes so each node has one
> + * decoding mode.
> + */
> +struct stack_depot_frame_run {
> +	u16 nr_entries;
> +	u8 mode;
> +};
> +
>  static_assert(CONFIG_STACKDEPOT_MAX_FRAMES <= U16_MAX);
>  
> +struct stack_depot_trie_children;
> +
> +struct stack_depot_trie_node {
> +	/* Parent links let fetch rebuild a full stack from a node to the root. */
> +	const struct stack_depot_trie_node __rcu *parent;
> +	/* Children are RCU-published containers. */
> +	const struct stack_depot_trie_children __rcu *children;
> +	/* Non-zero when a stored stack ends at this node. */
> +	u32 stack_id;
> +	struct stack_depot_frame_run run;
> +	unsigned char data[];
> +};
> +
> +/*
> + * Child nodes are sorted by first frame and searched by insertion position.
> + * Existing child pointers are immutable. Writers may publish into unused tail
> + * capacity; other updates publish a replacement container.
> + */
> +struct stack_depot_trie_children {
> +	unsigned int nr_children;
> +	unsigned int capacity;
> +	const struct stack_depot_trie_node __rcu *nodes[];
> +};
> +
> +/* Retired children carry an optional node through their RCU grace period. */
> +struct stack_depot_trie_retired_children {
> +	struct list_head list;
> +	unsigned long rcu_state;
> +	const struct stack_depot_trie_node *pending_node;
> +	unsigned char data[];
> +};
> +
> +static_assert(IS_ALIGNED(offsetof(struct stack_depot_trie_retired_children, data),
> +			 1UL << DEPOT_STACK_ALIGN));
> +
> +#define STACK_DEPOT_TRIE_SLOT_SIZE BIT(DEPOT_STACK_ALIGN)
> +#define STACK_DEPOT_TRIE_POOL_SLOTS \
> +	(DEPOT_POOL_SIZE / STACK_DEPOT_TRIE_SLOT_SIZE)
> +
> +static_assert(STACK_DEPOT_TRIE_POOL_SLOTS - 1 <= U16_MAX);
> +
> +struct stack_depot_trie_pool {
> +	struct list_head list;
> +	unsigned int free_slots;
> +	/* Conservative upper bound on the largest free run. */
> +	u16 free_run_upper_bound;
> +	/* First physical slot considered by the next reservation. */
> +	u16 next_slot;
> +	DECLARE_BITMAP(used, STACK_DEPOT_TRIE_POOL_SLOTS);
> +};
> +
> +#define STACK_DEPOT_TRIE_POOL_FIRST_SLOT \
> +	DIV_ROUND_UP(sizeof(struct stack_depot_trie_pool), \
> +		     STACK_DEPOT_TRIE_SLOT_SIZE)
> +#define STACK_DEPOT_TRIE_POOL_USABLE_SIZE \
> +	((STACK_DEPOT_TRIE_POOL_SLOTS - STACK_DEPOT_TRIE_POOL_FIRST_SLOT) * \
> +	 STACK_DEPOT_TRIE_SLOT_SIZE)
> +
> +static_assert(STACK_DEPOT_TRIE_POOL_FIRST_SLOT < STACK_DEPOT_TRIE_POOL_SLOTS);
> +
> +static DEFINE_STATIC_KEY_FALSE(stack_depot_trie_enabled);
> +static const struct stack_depot_trie_children __rcu *stack_depot_trie_root;
> +static DEFINE_RAW_SPINLOCK(stack_depot_trie_writer_lock);
> +static bool stack_depot_trie_requested;
> +
> +module_param_named(trie_enabled, stack_depot_trie_requested, bool, 0);
> +MODULE_PARM_DESC(trie_enabled, "Enable stack depot trie storage at boot");
> +
> +#define DEPOT_POOL_INDEX_MASK ((1U << DEPOT_POOL_INDEX_BITS) - 1)
> +#define DEPOT_OFFSET_MASK ((1U << DEPOT_OFFSET_BITS) - 1)
> +
> +/* Retired fixed-size slots remain reserved until their RCU grace period ends. */
> +static LIST_HEAD(stack_depot_trie_pools);
> +static LIST_HEAD(pending_trie_children);
> +
> +/*
> + * stack_max_pools is the split point between hash and trie handle encodings.
> + * A handle with pool_index_plus_1 in 1..stack_max_pools names a hash-backed
> + * stack pool. Larger pool-index values cannot refer to hash pools, so trie
> + * storage uses that handle space to encode a dense stack ID. The side table
> + * maps each stack ID to its trie node.
> + */
> +static inline u32 trie_max_stack_id(void)
> +{
> +	return (DEPOT_POOL_INDEX_MASK - stack_max_pools) <<
> +		DEPOT_OFFSET_BITS;
> +}

With PAGE_SHIFT=16, DEPOT_POOL_INDEX_BITS is 13
(DEPOT_POOL_INDEX_MASK = 8191), which equals the default
stack_max_pools (8191). trie_max_stack_id() therefore returns 0 and
stackdepot.trie_enabled=1 always fails to initialize unless
stack_depot_max_pools is also manually lowered.

> +
> +static depot_stack_handle_t trie_handle(u32 stack_id)
> +{
> +	union handle_parts parts = {};
> +	u64 pool_index_plus_1;
> +	u32 pool_delta;
> +	u32 index;
> +
> +	index = stack_id - 1;
> +	pool_delta = index >> DEPOT_OFFSET_BITS;
> +	pool_index_plus_1 = (u64)stack_max_pools + 1 + pool_delta;
> +
> +	parts.pool_index_plus_1 = pool_index_plus_1;
> +	parts.offset = index & DEPOT_OFFSET_MASK;
> +	return parts.handle;
> +}
> +
> +static inline bool stack_depot_handle_is_trie(depot_stack_handle_t handle)
> +{
> +	union handle_parts parts = { .handle = handle };
> +
> +	return parts.pool_index_plus_1 > stack_max_pools;
> +}
> +
> +static u32 trie_stack_id(depot_stack_handle_t handle)
> +{
> +	union handle_parts parts = { .handle = handle };
> +	u32 pool_delta;
> +
> +	pool_delta = parts.pool_index_plus_1 - stack_max_pools - 1;
> +	return (pool_delta << DEPOT_OFFSET_BITS) + parts.offset + 1;
> +}
> +
> +/*
> + * Trie handles encode a dense stack ID. The side table maps that ID to a node
> + * pointer for lockless fetch and print paths, which can run from diagnostic
> + * contexts where taking a lock would be unsafe. Initialization installs the
> + * root; early initialization also installs the first directory and chunk.
> + * Additional directories and chunks are published lazily as stack IDs grow.
> + */
> +#define STACK_DEPOT_TRIE_SIDE_TABLE_CHUNK_SIZE \
> +	(PAGE_SIZE / sizeof(struct stack_depot_trie_node *))
> +#define STACK_DEPOT_TRIE_SIDE_TABLE_DIR_SIZE \
> +	(PAGE_SIZE / sizeof(struct stack_depot_trie_node **))
> +
> +struct stack_depot_trie_side_dir {
> +	/* Both the chunk pointer and each node pointer in it are RCU-published. */
> +	const struct stack_depot_trie_node __rcu * __rcu *
> +		chunks[STACK_DEPOT_TRIE_SIDE_TABLE_DIR_SIZE];
> +};
> +
> +struct stack_depot_trie_side_root {
> +	unsigned int dir_capacity;
> +	struct stack_depot_trie_side_dir __rcu *dirs[];
> +};
> +
> +struct stack_depot_trie_side_prealloc {
> +	/* Preallocated side-table directory page for sparse growth. */
> +	struct stack_depot_trie_side_dir *dir;
> +	/* Preallocated side-table pointer chunk for sparse growth. */
> +	const struct stack_depot_trie_node __rcu **chunk;
> +};
> +
> +static struct stack_depot_trie_side_root *trie_side_table_root;
> +static DEFINE_RAW_SPINLOCK(trie_side_table_cache_lock);
> +/* Zeroed unpublished pages; get/put transfer ownership under the cache lock. */
> +static struct stack_depot_trie_side_prealloc trie_side_table_cache;
> +static u32 trie_side_table_last_stack_id;
> +
> +/* Lock order: writer_lock -> pool_lock -> side-table cache lock. */
> +
> +static inline size_t stack_depot_frame_run_entry_bytes(enum stack_depot_frame_mode mode)
> +{
> +	if (mode == STACK_DEPOT_FRAME_COMPRESSED)
> +		return sizeof(u32);
> +	return sizeof(unsigned long);
> +}
> +
> +static inline size_t stack_depot_frame_run_bytes(const struct stack_depot_frame_run *run)
> +{
> +	return run->nr_entries * stack_depot_frame_run_entry_bytes(run->mode);
> +}
> +
> +static inline size_t trie_node_bytes(const struct stack_depot_frame_run *run)
> +{
> +	return ALIGN(offsetof(struct stack_depot_trie_node, data) +
> +		     stack_depot_frame_run_bytes(run), sizeof(unsigned long));
> +}
> +
> +static size_t trie_children_alloc_size(unsigned int capacity)
> +{
> +	size_t size;
> +
> +	size = struct_size_t(struct stack_depot_trie_children, nodes,
> +			     capacity);
> +	return offsetof(struct stack_depot_trie_retired_children, data) +
> +		ALIGN(size, sizeof(unsigned long));
> +}
> +
> +static inline unsigned int trie_side_table_root_index(u32 id)
> +{
> +	return ((id - 1) / STACK_DEPOT_TRIE_SIDE_TABLE_CHUNK_SIZE) /
> +		STACK_DEPOT_TRIE_SIDE_TABLE_DIR_SIZE;
> +}
> +
> +static inline unsigned int trie_side_table_dir_index(u32 id)
> +{
> +	return ((id - 1) / STACK_DEPOT_TRIE_SIDE_TABLE_CHUNK_SIZE) %
> +		STACK_DEPOT_TRIE_SIDE_TABLE_DIR_SIZE;
> +}
> +
> +static inline unsigned int trie_side_table_slot_index(u32 id)
> +{
> +	return (id - 1) % STACK_DEPOT_TRIE_SIDE_TABLE_CHUNK_SIZE;
> +}
> +
> +static struct stack_depot_trie_side_dir *trie_side_table_load_dir(unsigned int root)
> +{
> +	struct stack_depot_trie_side_root *root_vec;
> +
> +	root_vec = trie_side_table_root;
> +	if (!root_vec || root >= root_vec->dir_capacity)
> +		return NULL;
> +	/* Pairs with side-table directory rcu_assign_pointer(). */
> +	return rcu_dereference_check(root_vec->dirs[root],
> +				     lockdep_is_held(&stack_depot_trie_writer_lock) ||
> +				     rcu_read_lock_sched_held());
> +}
> +
> +static inline const struct stack_depot_trie_node __rcu **
> +trie_side_table_dir_load_chunk(struct stack_depot_trie_side_dir *dir,
> +			       unsigned int idx)
> +{
> +	/* Pairs with the chunk rcu_assign_pointer() in stack ID preparation. */
> +	return rcu_dereference_check(dir->chunks[idx],
> +				     lockdep_is_held(&stack_depot_trie_writer_lock) ||
> +				     rcu_read_lock_sched_held());
> +}
> +
> +/* Published capacity remains useful if insertion fails and needs no rollback. */
> +static bool
> +trie_side_table_try_take_cache(struct stack_depot_trie_side_prealloc *prealloc,
> +			       bool need_dir)
> +{
> +	bool taken = false;
> +
> +	lockdep_assert_held(&stack_depot_trie_writer_lock);
> +	lockdep_assert_held(&pool_lock);
> +
> +	if (!raw_spin_trylock(&trie_side_table_cache_lock))
> +		return false;
> +	if ((!prealloc->chunk && !trie_side_table_cache.chunk) ||
> +	    (need_dir && !prealloc->dir && !trie_side_table_cache.dir))
> +		goto out_unlock;
> +
> +	if (need_dir && !prealloc->dir) {
> +		prealloc->dir = trie_side_table_cache.dir;
> +		trie_side_table_cache.dir = NULL;
> +	}
> +	if (!prealloc->chunk) {
> +		prealloc->chunk = trie_side_table_cache.chunk;
> +		trie_side_table_cache.chunk = NULL;
> +	}
> +	taken = true;
> +
> +out_unlock:
> +	raw_spin_unlock(&trie_side_table_cache_lock);
> +	return taken;
> +}
> +
> +static u32
> +trie_side_table_prepare_stack_slot(struct stack_depot_trie_side_prealloc *prealloc)
> +{
> +	const struct stack_depot_trie_node __rcu **chunk;
> +	struct stack_depot_trie_side_dir *dir;
> +	struct stack_depot_trie_side_root *root_vec;
> +	unsigned int root;
> +	unsigned int idx;
> +	u32 id;
> +
> +	lockdep_assert_held(&stack_depot_trie_writer_lock);
> +	lockdep_assert_held(&pool_lock);
> +
> +	id = trie_side_table_last_stack_id + 1;
> +	if (id > trie_max_stack_id())
> +		return 0;
> +
> +	root_vec = trie_side_table_root;
> +	root = trie_side_table_root_index(id);
> +	dir = trie_side_table_load_dir(root);
> +	if (!dir) {
> +		if ((!prealloc->dir || !prealloc->chunk) &&
> +		    !trie_side_table_try_take_cache(prealloc, true))
> +			return 0;
> +		dir = prealloc->dir;
> +		prealloc->dir = NULL;
> +		/* Publish the zeroed directory before readers can load it locklessly. */
> +		rcu_assign_pointer(root_vec->dirs[root], dir);
> +	}
> +
> +	idx = trie_side_table_dir_index(id);
> +	chunk = trie_side_table_dir_load_chunk(dir, idx);
> +	if (!chunk) {
> +		if (!prealloc->chunk &&
> +		    !trie_side_table_try_take_cache(prealloc, false))
> +			return 0;
> +		chunk = prealloc->chunk;
> +		prealloc->chunk = NULL;
> +		rcu_assign_pointer(dir->chunks[idx], chunk);
> +	}
> +
> +	return id;
> +}
> +
> +static inline unsigned int trie_side_table_root_size_for_max_id(u32 max_stack_id)
> +{
> +	unsigned int top_size;
> +
> +	top_size = DIV_ROUND_UP(max_stack_id, STACK_DEPOT_TRIE_SIDE_TABLE_CHUNK_SIZE);
> +	return DIV_ROUND_UP(top_size, STACK_DEPOT_TRIE_SIDE_TABLE_DIR_SIZE);
> +}
> +
> +static int __init stack_depot_trie_init_memblock(void)
> +{
> +	struct stack_depot_trie_side_root *root_vec;
> +	struct stack_depot_trie_side_dir *first_dir;
> +	const struct stack_depot_trie_node __rcu **first_chunk;
> +	size_t root_bytes;
> +	u32 max_stack_id;
> +	unsigned int root_size;
> +
> +	max_stack_id = trie_max_stack_id();
> +	if (!max_stack_id)
> +		return -EINVAL;
> +	root_size = trie_side_table_root_size_for_max_id(max_stack_id);
> +	root_bytes = struct_size_t(struct stack_depot_trie_side_root, dirs, root_size);
> +
> +	root_vec = memblock_alloc(root_bytes, __alignof__(*root_vec));
> +	if (!root_vec)
> +		return -ENOMEM;
> +	first_dir = memblock_alloc(PAGE_SIZE, PAGE_SIZE);
> +	if (!first_dir) {
> +		memblock_free(root_vec, root_bytes);
> +		return -ENOMEM;
> +	}
> +	first_chunk = memblock_alloc(PAGE_SIZE, PAGE_SIZE);
> +	if (!first_chunk) {
> +		memblock_free(first_dir, PAGE_SIZE);
> +		memblock_free(root_vec, root_bytes);
> +		return -ENOMEM;
> +	}
> +
> +	root_vec->dir_capacity = root_size;
> +	RCU_INIT_POINTER(root_vec->dirs[0], first_dir);
> +	RCU_INIT_POINTER(first_dir->chunks[0], first_chunk);
> +	trie_side_table_root = root_vec;
> +	static_branch_enable(&stack_depot_trie_enabled);
> +	return 0;
> +}
> +
> +static int stack_depot_trie_init(void)
> +{
> +	struct stack_depot_trie_side_root *root_vec;
> +	unsigned int root_size;
> +	size_t root_bytes;
> +	u32 max_stack_id;
> +
> +	max_stack_id = trie_max_stack_id();
> +	if (!max_stack_id)
> +		return -EINVAL;
> +
> +	root_size = trie_side_table_root_size_for_max_id(max_stack_id);
> +	root_bytes = struct_size_t(struct stack_depot_trie_side_root, dirs, root_size);
> +	root_vec = kvzalloc(root_bytes, GFP_KERNEL);
> +	if (!root_vec)
> +		return -ENOMEM;
> +
> +	root_vec->dir_capacity = root_size;
> +	trie_side_table_root = root_vec;
> +	static_branch_enable(&stack_depot_trie_enabled);
> +	return 0;
> +}
> +
> +static int trie_side_table_get_prealloc(gfp_t gfp_flags,
> +					struct stack_depot_trie_side_prealloc *prealloc)
> +{
> +	unsigned long flags;
> +
> +	gfp_flags = gfp_nested_mask(gfp_flags);
> +	raw_spin_lock_irqsave(&trie_side_table_cache_lock, flags);
> +	prealloc->dir = trie_side_table_cache.dir;
> +	prealloc->chunk = trie_side_table_cache.chunk;
> +	trie_side_table_cache.dir = NULL;
> +	trie_side_table_cache.chunk = NULL;
> +	raw_spin_unlock_irqrestore(&trie_side_table_cache_lock, flags);

stack_depot_trie_save() pops both cached pages into thread-local
storage *before* taking stack_depot_trie_writer_lock. Under
concurrency, every other CPU that misses in trie_find_handle() sees an
empty cache, calls get_zeroed_page() twice, and then frees both pages
in trie_side_table_put_prealloc() because the existing dir (262k IDs)
and chunk (512 IDs) weren't full.

Worse: if get_zeroed_page() fails (e.g. GFP_NOWAIT),
stack_depot_trie_save() immediately returns 0 even when the current
dir, chunk, and trie pool already have plenty of free space. And while
a can_alloc caller temporarily holds the popped cache pages, a
concurrent !can_alloc caller crossing a chunk boundary fails because
the cached chunk was stolen.

> +	if (!prealloc->dir) {
> +		prealloc->dir = (void *)get_zeroed_page(gfp_flags);
> +		if (!prealloc->dir)
> +			return -ENOMEM;
> +	}
> +	if (!prealloc->chunk) {
> +		prealloc->chunk = (void *)get_zeroed_page(gfp_flags);
> +		if (!prealloc->chunk)
> +			return -ENOMEM;
> +	}
> +
> +	return 0;
> +}
> +
> +static void trie_side_table_put_prealloc(struct stack_depot_trie_side_prealloc *prealloc)
> +{
> +	unsigned long flags;
> +
> +	raw_spin_lock_irqsave(&trie_side_table_cache_lock, flags);
> +	if (!trie_side_table_cache.dir) {
> +		trie_side_table_cache.dir = prealloc->dir;
> +		prealloc->dir = NULL;
> +	}
> +	if (!trie_side_table_cache.chunk) {
> +		trie_side_table_cache.chunk = prealloc->chunk;
> +		prealloc->chunk = NULL;
> +	}
> +	raw_spin_unlock_irqrestore(&trie_side_table_cache_lock, flags);
> +
> +	if (prealloc->dir)
> +		free_page((unsigned long)prealloc->dir);
> +	if (prealloc->chunk)
> +		free_page((unsigned long)prealloc->chunk);
> +}
> +
> +static const struct stack_depot_trie_node *trie_side_table_lookup(u32 id)
> +{
> +	const struct stack_depot_trie_node __rcu **chunk;
> +	struct stack_depot_trie_side_dir *dir;
> +	unsigned int root;
> +
> +	root = trie_side_table_root_index(id);
> +	dir = trie_side_table_load_dir(root);
> +	if (!dir)
> +		return NULL;
> +	chunk = trie_side_table_dir_load_chunk(dir, trie_side_table_dir_index(id));
> +	if (!chunk)
> +		return NULL;
> +
> +	/* Pairs with side-table node publication. */
> +	return rcu_dereference_check(chunk[trie_side_table_slot_index(id)],
> +				     lockdep_is_held(&stack_depot_trie_writer_lock) ||
> +				     rcu_read_lock_sched_held());
> +}
> +
> +static inline struct stack_depot_trie_retired_children *
> +trie_retired_children(const void *ptr)
> +{
> +	return container_of(ptr, struct stack_depot_trie_retired_children, data);
> +}
> +
> +static bool depot_init_pool(void **prealloc);
> +
> +static unsigned int trie_pool_reserve_slots(struct stack_depot_trie_pool *pool,
> +					    unsigned int nr_slots)
> +{
> +	unsigned int start = pool->next_slot;
> +	unsigned int run = 0;
> +	unsigned int longest_run = 0;
> +	unsigned int i;
> +	unsigned int slot;
> +
> +scan:
> +	run = 0;
> +	longest_run = 0;
> +	for (slot = start; slot < STACK_DEPOT_TRIE_POOL_SLOTS; slot++) {
> +		if (pool->used[slot / BITS_PER_LONG] &
> +		    BIT(slot % BITS_PER_LONG)) {
> +			run = 0;
> +			continue;
> +		}
> +		run++;
> +		longest_run = max(longest_run, run);
> +		if (run != nr_slots)
> +			continue;
> +
> +		for (i = slot + 1 - nr_slots; i <= slot; i++)
> +			pool->used[i / BITS_PER_LONG] |= BIT(i % BITS_PER_LONG);
> +		pool->free_slots -= nr_slots;
> +		if (slot + 1 == STACK_DEPOT_TRIE_POOL_SLOTS)
> +			pool->next_slot = STACK_DEPOT_TRIE_POOL_FIRST_SLOT;
> +		else
> +			pool->next_slot = slot + 1;
> +		return slot + 1 - nr_slots;
> +	}
> +
> +	if (start != STACK_DEPOT_TRIE_POOL_FIRST_SLOT) {
> +		/* Keep holes and runs crossing the cursor visible. */
> +		start = STACK_DEPOT_TRIE_POOL_FIRST_SLOT;
> +		goto scan;
> +	}
> +
> +	pool->free_run_upper_bound = longest_run;
> +	return STACK_DEPOT_TRIE_POOL_SLOTS;
> +}
> +
> +/* Allocate at least @size bytes from one contiguous trie-pool slot run. */
> +static void *trie_pool_alloc(size_t size, void **prealloc)
> +{
> +	struct stack_depot_trie_pool *pool;
> +	unsigned int nr_slots;
> +	unsigned int slot;
> +
> +	lockdep_assert_held(&pool_lock);
> +
> +	if (size > STACK_DEPOT_TRIE_POOL_USABLE_SIZE)
> +		return NULL;
> +	nr_slots = DIV_ROUND_UP(size, STACK_DEPOT_TRIE_SLOT_SIZE);
> +	list_for_each_entry_reverse(pool, &stack_depot_trie_pools, list) {
> +		if (pool->free_slots < nr_slots ||
> +		    pool->free_run_upper_bound < nr_slots)
> +			continue;
> +		slot = trie_pool_reserve_slots(pool, nr_slots);
> +		if (slot != STACK_DEPOT_TRIE_POOL_SLOTS)
> +			return (char *)pool + slot * STACK_DEPOT_TRIE_SLOT_SIZE;
> +	}
> +
> +	if (!depot_init_pool(prealloc))
> +		return NULL;
> +	pool = stack_pools[pools_num - 1];
> +	/* Keep hash records out of this bitmap-owned pool. */
> +	pool_offset = DEPOT_POOL_SIZE;

depot_pop_free_pool() always allocates from stack_pools[pools_num - 1]
at pool_offset. When hash saves (page_owner COUNTABLE, KASAN GET) and
trie saves interleave, allocating a trie pool increments pools_num and
sets pool_offset = DEPOT_POOL_SIZE, permanently abandoning all unused
space in the previously active hash pool. Trie pools shouldn't be
placed in stack_pools[] at all (they also unnecessarily consume
stack_max_pools slots).

> +	memset(pool, 0, sizeof(*pool));
> +	pool->free_slots = STACK_DEPOT_TRIE_POOL_SLOTS -
> +			   STACK_DEPOT_TRIE_POOL_FIRST_SLOT;
> +	pool->free_run_upper_bound = pool->free_slots;
> +	pool->next_slot = STACK_DEPOT_TRIE_POOL_FIRST_SLOT;
> +	list_add_tail(&pool->list, &stack_depot_trie_pools);
> +
> +	slot = trie_pool_reserve_slots(pool, nr_slots);
> +	return (char *)pool + slot * STACK_DEPOT_TRIE_SLOT_SIZE;
> +}
> +
> +/* Release the slots for the byte count originally passed to allocation. */
> +static void trie_pool_release(const void *ptr, size_t size)
> +{
> +	struct stack_depot_trie_pool *pool;
> +	unsigned long pfn;
> +	unsigned int nr_slots;
> +	unsigned int slot;
> +	unsigned int i;
> +
> +	lockdep_assert_held(&pool_lock);
> +
> +	pfn = page_to_pfn(virt_to_page(ptr));
> +	pfn &= ~(BIT(DEPOT_POOL_ORDER) - 1);
> +	pool = page_address(pfn_to_page(pfn));
> +	slot = ((unsigned long)ptr - (unsigned long)pool) >> DEPOT_STACK_ALIGN;
> +	nr_slots = DIV_ROUND_UP(size, STACK_DEPOT_TRIE_SLOT_SIZE);
> +	for (i = slot; i < slot + nr_slots; i++)
> +		pool->used[i / BITS_PER_LONG] &= ~BIT(i % BITS_PER_LONG);
> +	pool->free_slots += nr_slots;
> +	/* A release can join at most two runs bounded by the old value. */
> +	pool->free_run_upper_bound = min(pool->free_slots,
> +					 2 * pool->free_run_upper_bound + nr_slots);
> +}
> +
> +static struct stack_depot_trie_children *
> +trie_pool_alloc_children(unsigned int capacity, void **prealloc)
> +{
> +	struct stack_depot_trie_retired_children *retired;
> +	struct stack_depot_trie_children *children;
> +
> +	/* Capacity counts child-pointer entries; allocation includes RCU metadata. */
> +	retired = trie_pool_alloc(trie_children_alloc_size(capacity), prealloc);
> +	if (!retired)
> +		return NULL;
> +
> +	children = (void *)retired->data;
> +	children->nr_children = 0;
> +	children->capacity = capacity;
> +	return children;
> +}
> +
> +static void
> +trie_pool_release_children(const struct stack_depot_trie_children *children)
> +{
> +	/* Capacity is immutable and therefore recovers the allocation byte size. */
> +	trie_pool_release(trie_retired_children(children),
> +			  trie_children_alloc_size(children->capacity));
> +}
> +
> +/*
> + * Return RCU-ready objects before allocating. Pending children are FIFO, so
> + * stop at the first incomplete grace period. A replaced node shares the same
> + * retirement cookie and is released with its former children container.
> + */
> +static void trie_drain_pending_children(void)
> +{
> +	struct stack_depot_trie_retired_children *retired;
> +	struct stack_depot_trie_retired_children *tmp;
> +	struct stack_depot_trie_children *children;
> +
> +	lockdep_assert_held(&pool_lock);
> +
> +	list_for_each_entry_safe(retired, tmp, &pending_trie_children, list) {
> +		if (!poll_state_synchronize_rcu(retired->rcu_state))
> +			break;
> +		children = (void *)retired->data;
> +		list_del(&retired->list);
> +		if (retired->pending_node)
> +			trie_pool_release(retired->pending_node,
> +					  trie_node_bytes(&retired->pending_node->run));
> +		trie_pool_release_children(children);
> +	}
> +}
> +
> +static void trie_retire_children(const struct stack_depot_trie_children *children)
> +{
> +	struct stack_depot_trie_retired_children *retired;
> +
> +	lockdep_assert_held(&pool_lock);
> +
> +	retired = trie_retired_children(children);
> +	retired->pending_node = NULL;
> +	retired->rcu_state = get_state_synchronize_rcu();
> +	list_add_tail(&retired->list, &pending_trie_children);
> +}

Every interior child insertion (pos < nr_children) allocates a new
power-of-two array and retires the old one via
get_state_synchronize_rcu(). Once a hot node has 512..1023 children
(8,232 bytes), two copies cannot fit in one 16,208-byte pool, so a
burst of N new call sites in one RCU grace period forces N separate
16 KiB trie pools to be allocated. Also,
stack_depot_trie_save_constrained() retires child arrays onto
pending_trie_children without ever calling
trie_drain_pending_children().

> +
> +static void
> +trie_retire_children_with_node(const struct stack_depot_trie_children *children,
> +			       const struct stack_depot_trie_node *node)
> +{
> +	struct stack_depot_trie_retired_children *retired;
> +
> +	lockdep_assert_held(&stack_depot_trie_writer_lock);
> +	lockdep_assert_held(&pool_lock);
> +	trie_retire_children(children);
> +	retired = trie_retired_children(children);
> +	retired->pending_node = node;
> +}
> +
> +static const struct stack_depot_trie_node *
> +stack_depot_trie_lookup(const unsigned long *entries, unsigned int nr_entries);
> +
> +static depot_stack_handle_t
> +trie_find_handle(const unsigned long *entries, unsigned int nr_entries)
> +{
> +	depot_stack_handle_t handle = 0;
> +	const struct stack_depot_trie_node *node;
> +
> +	rcu_read_lock_sched_notrace();
> +	node = stack_depot_trie_lookup(entries, nr_entries);
> +	if (node)
> +		handle = trie_handle(node->stack_id);
> +	rcu_read_unlock_sched_notrace();
> +
> +	return handle;
> +}
> +
> +/*
> + * Publish only after the node and its path are fully initialized and all
> + * fallible allocation is complete. Publication commits the path, so it cannot
> + * then be rolled back. Side-table mappings must precede trie topology
> + * publication that makes new or remapped nodes reachable from lookup.
> + * Published storage remains valid until RCU retirement; only descendant parent
> + * links may change meanwhile.
> + */
> +static void trie_side_table_publish(const struct stack_depot_trie_node *node)
> +{
> +	const struct stack_depot_trie_node __rcu **chunk;
> +	struct stack_depot_trie_side_dir *dir;
> +	u32 stack_id = node->stack_id;
> +
> +	lockdep_assert_held(&stack_depot_trie_writer_lock);
> +
> +	dir = trie_side_table_load_dir(trie_side_table_root_index(stack_id));
> +	chunk = trie_side_table_dir_load_chunk(dir,
> +					       trie_side_table_dir_index(stack_id));
> +	/* Pairs with trie_side_table_lookup(). */
> +	rcu_assign_pointer(chunk[trie_side_table_slot_index(stack_id)], node);
> +}
> +
>  static int __init disable_stack_depot(char *str)
>  {
>  	return kstrtobool(str, &stack_depot_disabled);
> @@ -147,7 +844,7 @@ static void init_stack_table(unsigned long entries)
>  		INIT_LIST_HEAD(&stack_table[i]);
>  }
>  
> -/* Allocates a hash table via memblock. Can only be used during early boot. */
> +/* Initializes hash and optional trie storage during early boot. */
>  int __init stack_depot_early_init(void)
>  {
>  	unsigned long entries = 0;
> @@ -221,11 +918,15 @@ int __init stack_depot_early_init(void)
>  		stack_depot_disabled = true;
>  		return -ENOMEM;
>  	}
> +	if (stack_depot_trie_requested && stack_depot_trie_init_memblock()) {
> +		pr_warn("trie storage initialization failed, disabling trie storage\n");
> +		stack_depot_trie_requested = false;
> +	}
>  
>  	return 0;
>  }
>  
> -/* Allocates a hash table via kvcalloc. Can be used after boot. */
> +/* Initializes hash and optional trie storage after boot. */
>  int stack_depot_init(void)
>  {
>  	static DEFINE_MUTEX(stack_depot_init_mutex);
> @@ -279,6 +980,15 @@ int stack_depot_init(void)
>  		kvfree(stack_table);
>  		stack_depot_disabled = true;
>  		ret = -ENOMEM;
> +		goto out_unlock;
> +	}
> +	if (stack_depot_trie_requested) {
> +		ret = stack_depot_trie_init();
> +		if (ret) {
> +			pr_warn("trie storage initialization failed, disabling trie storage\n");
> +			stack_depot_trie_requested = false;
> +			ret = 0;
> +		}
>  	}
>  
>  out_unlock:
> @@ -643,6 +1353,101 @@ static inline struct stack_record *find_stack(struct list_head *bucket,
>  	return ret;
>  }
>  
> +static u32
> +stack_depot_trie_insert(const unsigned long *entries,
> +			unsigned int nr_entries, void **pool_prealloc,
> +			struct stack_depot_trie_side_prealloc *side_prealloc);
> +
> +static depot_stack_handle_t
> +stack_depot_trie_save(unsigned long *entries, unsigned int nr_entries,
> +		      gfp_t alloc_flags)
> +{
> +	unsigned int attempt;
> +
> +	/* Allow one stale pool hint before the two pools a largest insert needs. */
> +	for (attempt = 0; attempt < 3; attempt++) {
> +		struct stack_depot_trie_side_prealloc side_prealloc = {};
> +		void *pool_prealloc = NULL;
> +		depot_stack_handle_t handle;
> +		unsigned long flags;
> +		struct page *page;
> +		u32 stack_id = 0;
> +
> +		handle = trie_find_handle(entries, nr_entries);
> +		if (handle)
> +			return handle;
> +
> +		if (trie_side_table_get_prealloc(alloc_flags, &side_prealloc)) {
> +			trie_side_table_put_prealloc(&side_prealloc);
> +			return 0;
> +		}
> +
> +		/* The hint may race; a missing page is recovered by the retry. */
> +		if (!READ_ONCE(new_pool)) {
> +			page = alloc_pages(gfp_nested_mask(alloc_flags),
> +					   DEPOT_POOL_ORDER);
> +			if (page)
> +				pool_prealloc = page_address(page);
> +		}
> +
> +		raw_spin_lock_irqsave(&stack_depot_trie_writer_lock, flags);
> +		raw_spin_lock(&pool_lock);
> +		printk_deferred_enter();
> +		trie_drain_pending_children();
> +		stack_id = stack_depot_trie_insert(entries, nr_entries,
> +						   &pool_prealloc, &side_prealloc);
> +		if (pool_prealloc)
> +			depot_keep_new_pool(&pool_prealloc);
> +		printk_deferred_exit();
> +		raw_spin_unlock(&pool_lock);
> +		raw_spin_unlock_irqrestore(&stack_depot_trie_writer_lock, flags);
> +
> +		if (pool_prealloc)
> +			free_pages((unsigned long)pool_prealloc, DEPOT_POOL_ORDER);
> +		trie_side_table_put_prealloc(&side_prealloc);
> +		if (stack_id)
> +			return trie_handle(stack_id);
> +	}
> +
> +	return 0;
> +}
> +
> +static depot_stack_handle_t
> +stack_depot_trie_save_constrained(unsigned long *entries,
> +				  unsigned int nr_entries, bool trylock)
> +{
> +	struct stack_depot_trie_side_prealloc side_prealloc = {};
> +	void *pool_prealloc = NULL;
> +	depot_stack_handle_t handle;
> +	unsigned long flags;
> +	u32 stack_id;
> +
> +	handle = trie_find_handle(entries, nr_entries);
> +	if (handle)
> +		return handle;
> +
> +	if (trylock) {
> +		if (!raw_spin_trylock_irqsave(&stack_depot_trie_writer_lock, flags))
> +			return 0;
> +		if (!raw_spin_trylock(&pool_lock)) {
> +			raw_spin_unlock_irqrestore(&stack_depot_trie_writer_lock, flags);
> +			return 0;
> +		}
> +	} else {
> +		raw_spin_lock_irqsave(&stack_depot_trie_writer_lock, flags);
> +		raw_spin_lock(&pool_lock);
> +	}
> +
> +	printk_deferred_enter();
> +	stack_id = stack_depot_trie_insert(entries, nr_entries, &pool_prealloc,
> +					   &side_prealloc);
> +	printk_deferred_exit();
> +	raw_spin_unlock(&pool_lock);
> +	raw_spin_unlock_irqrestore(&stack_depot_trie_writer_lock, flags);
> +
> +	return stack_id ? trie_handle(stack_id) : 0;
> +}
> +
>  depot_stack_handle_t stack_depot_save_flags(unsigned long *entries,
>  					    unsigned int nr_entries,
>  					    gfp_t alloc_flags,
> @@ -677,6 +1482,20 @@ depot_stack_handle_t stack_depot_save_flags(unsigned long *entries,
>  	if (unlikely(nr_entries == 0) || stack_depot_disabled)
>  		return 0;
>  
> +	if (!(depot_flags & (STACK_DEPOT_FLAG_GET | STACK_DEPOT_FLAG_COUNTABLE)) &&
> +	    static_branch_unlikely(&stack_depot_trie_enabled)) {
> +		if (nr_entries > CONFIG_STACKDEPOT_MAX_FRAMES)
> +			nr_entries = CONFIG_STACKDEPOT_MAX_FRAMES;
> +		if (in_nmi()) {
> +			WARN_ON_ONCE(can_alloc);
> +			return trie_find_handle(entries, nr_entries);
> +		}
> +		if (!can_alloc)
> +			return stack_depot_trie_save_constrained(entries, nr_entries,
> +							 !allow_spin);
> +		return stack_depot_trie_save(entries, nr_entries, alloc_flags);
> +	}
> +
>  	hash = hash_stack(entries, nr_entries);
>  	bucket = &stack_table[hash & stack_hash_mask];
>  
> @@ -763,6 +1582,8 @@ struct stack_record *__stack_depot_get_stack_record(depot_stack_handle_t handle)
>  
>  	if (!handle)
>  		return NULL;
> +	if (WARN_ON_ONCE(stack_depot_handle_is_trie(handle)))
> +		return NULL;
>  
>  	stack = depot_fetch_stack(handle);
>  	if (!stack)
> @@ -773,6 +1594,714 @@ struct stack_record *__stack_depot_get_stack_record(depot_stack_handle_t handle)
>  	return stack;
>  }
>  
> +static void frame_run_init(const unsigned long *entries,
> +			   unsigned int nr_entries,
> +			   struct stack_depot_frame_run *run)
> +{
> +	u32 payload;
> +	unsigned int i;
> +	bool compressed;
> +
> +	compressed = arch_stack_depot_frame_try_compress(entries[0], &payload);
> +	for (i = 1; i < nr_entries; i++) {
> +		bool next;
> +
> +		next = arch_stack_depot_frame_try_compress(entries[i], &payload);
> +		if (next != compressed)
> +			break;
> +	}
> +
> +	/* @i is the first non-matching frame, or @nr_entries if all matched. */
> +	run->mode = compressed ? STACK_DEPOT_FRAME_COMPRESSED : STACK_DEPOT_FRAME_RAW;
> +	run->nr_entries = i;
> +}
> +
> +static void
> +stack_depot_trie_node_frame(const struct stack_depot_trie_node *node,
> +			    unsigned int index, unsigned long *frame)
> +{
> +	u32 payload;
> +
> +	if (node->run.mode == STACK_DEPOT_FRAME_RAW) {
> +		memcpy(frame, node->data + index * sizeof(*frame),
> +		       sizeof(*frame));
> +		return;
> +	}
> +
> +	memcpy(&payload, node->data + index * sizeof(payload), sizeof(payload));
> +	arch_stack_depot_frame_decompress(payload, frame);
> +}
> +
> +static void trie_node_init(struct stack_depot_trie_node *node,
> +			   const struct stack_depot_trie_node *parent, u32 stack_id,
> +			   const unsigned long *entries,
> +			   const struct stack_depot_frame_run *run)
> +{
> +	if (run->mode == STACK_DEPOT_FRAME_COMPRESSED) {
> +		unsigned int i;
> +
> +		for (i = 0; i < run->nr_entries; i++) {
> +			u32 payload;
> +
> +			arch_stack_depot_frame_try_compress(entries[i], &payload);
> +			memcpy(node->data + i * sizeof(payload), &payload,
> +			       sizeof(payload));
> +		}
> +	} else {
> +		memcpy(node->data, entries, stack_depot_frame_run_bytes(run));
> +	}
> +
> +	RCU_INIT_POINTER(node->parent, parent);
> +	RCU_INIT_POINTER(node->children, NULL);
> +	node->stack_id = stack_id;
> +	node->run = *run;
> +}
> +
> +static void trie_node_init_slice(struct stack_depot_trie_node *node,
> +				 const struct stack_depot_trie_node *parent, u32 stack_id,
> +				 const struct stack_depot_trie_node *src_node,
> +				 unsigned int start, unsigned int nr_entries)
> +{
> +	struct stack_depot_frame_run run;
> +	size_t entry_bytes;
> +
> +	run = src_node->run;
> +	run.nr_entries = nr_entries;
> +
> +	entry_bytes = stack_depot_frame_run_entry_bytes(src_node->run.mode);
> +	memcpy(node->data, src_node->data + start * entry_bytes,
> +	       stack_depot_frame_run_bytes(&run));
> +	RCU_INIT_POINTER(node->parent, parent);
> +	RCU_INIT_POINTER(node->children, NULL);
> +	node->stack_id = stack_id;
> +	node->run = run;
> +}
> +
> +static unsigned int trie_node_match(const struct stack_depot_trie_node *node,
> +				    const unsigned long *entries,
> +				    unsigned int nr_entries)
> +{
> +	unsigned int limit;
> +	unsigned int i;
> +
> +	limit = min(node->run.nr_entries, nr_entries);
> +	if (node->run.mode == STACK_DEPOT_FRAME_RAW) {
> +		for (i = 0; i < limit; i++) {
> +			unsigned long frame;
> +
> +			memcpy(&frame, node->data + i * sizeof(frame), sizeof(frame));
> +			if (frame != entries[i])
> +				break;
> +		}
> +
> +		return i;
> +	}
> +
> +	for (i = 0; i < limit; i++) {
> +		unsigned long frame;
> +
> +		stack_depot_trie_node_frame(node, i, &frame);
> +		if (frame != entries[i])
> +			break;
> +	}
> +
> +	return i;
> +}
> +
> +static inline const struct stack_depot_trie_node *
> +trie_load_parent(const struct stack_depot_trie_node *node)
> +{
> +	return rcu_dereference_check(node->parent,
> +				     lockdep_is_held(&stack_depot_trie_writer_lock) ||
> +				     rcu_read_lock_sched_held());
> +}
> +
> +static inline const struct stack_depot_trie_children *
> +trie_load_children(const struct stack_depot_trie_children __rcu * const *slot)
> +{
> +	return rcu_dereference_check(*slot,
> +				     lockdep_is_held(&stack_depot_trie_writer_lock) ||
> +				     rcu_read_lock_sched_held());
> +}
> +
> +static inline const struct stack_depot_trie_node *
> +trie_children_load_child(const struct stack_depot_trie_children *children,
> +			 unsigned int pos)
> +{
> +	return rcu_dereference_check(children->nodes[pos],
> +				     lockdep_is_held(&stack_depot_trie_writer_lock) ||
> +				     rcu_read_lock_sched_held());
> +}
> +
> +static bool
> +trie_children_find_position(const struct stack_depot_trie_children *children,
> +			    unsigned long frame, unsigned int *pos)
> +{
> +	unsigned int left = 0;
> +	unsigned int right;
> +
> +	right = READ_ONCE(children->nr_children);
> +	while (left < right) {
> +		unsigned int mid = left + (right - left) / 2;
> +		const struct stack_depot_trie_node *node;
> +		unsigned long mid_frame;
> +
> +		node = trie_children_load_child(children, mid);
> +		if (!node) {
> +			/* Tail append may produce a transient lockless lookup miss. */
> +			right = mid;
> +			continue;
> +		}
> +		stack_depot_trie_node_frame(node, 0, &mid_frame);
> +		if (mid_frame < frame) {
> +			left = mid + 1;
> +		} else if (mid_frame > frame) {
> +			right = mid;
> +		} else {
> +			*pos = mid;
> +			return true;
> +		}
> +	}
> +
> +	*pos = left;
> +	return false;
> +}
> +
> +/* Initialize an unpublished container from a stable published prefix. */
> +static void trie_children_init(const struct stack_depot_trie_children *old,
> +			       struct stack_depot_trie_children *new)
> +{
> +	unsigned int nr_old = old->nr_children;
> +	unsigned int i;
> +
> +	new->nr_children = nr_old;
> +	for (i = 0; i < nr_old; i++)
> +		RCU_INIT_POINTER(new->nodes[i], trie_children_load_child(old, i));
> +	for (i = nr_old; i < new->capacity; i++)
> +		RCU_INIT_POINTER(new->nodes[i], NULL);
> +}
> +
> +static void trie_children_insert(struct stack_depot_trie_children *children,
> +				 const struct stack_depot_trie_node *node,
> +				 unsigned int pos)
> +{
> +	unsigned int i;
> +
> +	for (i = children->nr_children; i > pos; i--)
> +		RCU_INIT_POINTER(children->nodes[i],
> +				 trie_children_load_child(children, i - 1));
> +	RCU_INIT_POINTER(children->nodes[pos], node);
> +	children->nr_children++;
> +}
> +
> +static void trie_reparent_children(struct stack_depot_trie_node *parent)
> +{
> +	const struct stack_depot_trie_children *children;
> +	unsigned int i;
> +
> +	lockdep_assert_held(&stack_depot_trie_writer_lock);
> +
> +	children = trie_load_children(&parent->children);
> +	if (!children)
> +		return;
> +	/*
> +	 * Replacement nodes reuse unchanged descendant subtrees. Repoint their
> +	 * parent links before retiring the old parent so fetch never follows a freed
> +	 * node. Lockless fetches may see the new parent before publication, but the
> +	 * old and new parent chains contain the same frames and remain RCU-live.
> +	 */
> +	for (i = 0; i < children->nr_children; i++) {
> +		struct stack_depot_trie_node *child;
> +
> +		child = (struct stack_depot_trie_node *)trie_children_load_child(children, i);
> +		rcu_assign_pointer(child->parent, parent);
> +	}
> +}
> +
> +/*
> + * Split entries into runs, allocate and initialize each node once, and link
> + * adjacent nodes through singleton children. Both trie locks must be held.
> + * Failure walks the unpublished parent chain and releases local ownership.
> + */
> +static const struct stack_depot_trie_node *
> +trie_path_alloc(const struct stack_depot_trie_node *parent, u32 stack_id,
> +		const unsigned long *entries, unsigned int nr_entries,
> +		void **pool_prealloc,
> +		const struct stack_depot_trie_node **node_out)
> +{
> +	struct stack_depot_trie_children *path_children = NULL;
> +	const struct stack_depot_trie_node *path_root = NULL;
> +	const struct stack_depot_trie_node *last_node = parent;
> +	unsigned int entry = 0;
> +
> +	lockdep_assert_held(&pool_lock);
> +	lockdep_assert_held(&stack_depot_trie_writer_lock);
> +
> +	while (entry < nr_entries) {
> +		struct stack_depot_frame_run run;
> +		struct stack_depot_trie_node *node;
> +
> +		frame_run_init(&entries[entry], nr_entries - entry, &run);
> +		node = trie_pool_alloc(trie_node_bytes(&run), pool_prealloc);
> +		if (!node)
> +			goto err_release;
> +
> +		trie_node_init(node, last_node,
> +			       entry + run.nr_entries == nr_entries ? stack_id : 0,
> +			       &entries[entry], &run);
> +		entry += run.nr_entries;
> +		last_node = node;
> +		if (!path_root)
> +			path_root = node;
> +
> +		if (path_children)
> +			trie_children_insert(path_children, last_node, 0);
> +		if (entry < nr_entries) {
> +			path_children = trie_pool_alloc_children(1, pool_prealloc);
> +			if (!path_children)
> +				goto err_release;
> +			RCU_INIT_POINTER(node->children, path_children);
> +		}
> +	}
> +
> +	*node_out = last_node;
> +	return path_root;
> +
> +err_release:
> +	while (last_node != parent) {
> +		const struct stack_depot_trie_children *node_children;
> +		const struct stack_depot_trie_node *node = last_node;
> +
> +		last_node = trie_load_parent(node);
> +		node_children = trie_load_children(&node->children);
> +		if (node_children)
> +			trie_pool_release_children(node_children);
> +		trie_pool_release(node, trie_node_bytes(&node->run));
> +	}
> +	return NULL;
> +}
> +
> +static const struct stack_depot_trie_node *
> +stack_depot_trie_lookup(const unsigned long *entries, unsigned int nr_entries)
> +{
> +	const struct stack_depot_trie_children *children;
> +	unsigned int entry = 0;
> +
> +	children = trie_load_children(&stack_depot_trie_root);
> +
> +	while (entry < nr_entries) {
> +		const struct stack_depot_trie_node *node;
> +		unsigned int remaining = nr_entries - entry;
> +		unsigned int matched;
> +		unsigned int pos;
> +
> +		if (!children)
> +			return NULL;
> +		if (!trie_children_find_position(children, entries[entry], &pos))
> +			return NULL;
> +
> +		node = trie_children_load_child(children, pos);
> +		matched = trie_node_match(node, &entries[entry], remaining);
> +		if (matched < node->run.nr_entries)
> +			return NULL;
> +		entry += matched;
> +		if (entry == nr_entries)
> +			return node->stack_id ? node : NULL;
> +
> +		children = trie_load_children(&node->children);
> +	}
> +
> +	return NULL;
> +}
> +
> +static u32
> +trie_insert_path(const struct stack_depot_trie_children __rcu **slot,
> +		 struct stack_depot_trie_node *parent,
> +		 const struct stack_depot_trie_children *children,
> +		 unsigned int pos, const unsigned long *entries,
> +		 unsigned int nr_entries, void **pool_prealloc,
> +		 struct stack_depot_trie_side_prealloc *side_prealloc)
> +{
> +	struct stack_depot_trie_children *new_children = NULL;
> +	const struct stack_depot_trie_node *path_root;
> +	const struct stack_depot_trie_node *node;
> +	unsigned int capacity = 1;
> +	u32 new_stack_id;
> +	bool tail_append = false;
> +
> +	/*
> +	 * Reuse spare capacity only for a sorted tail append. Other insertions
> +	 * replace the children container without modifying visible pointers.
> +	 */
> +	if (children) {
> +		capacity = roundup_pow_of_two(children->nr_children + 1);
> +		tail_append = pos == children->nr_children &&
> +			children->nr_children < children->capacity;
> +	}
> +	if (!tail_append && trie_children_alloc_size(capacity) >
> +	    STACK_DEPOT_TRIE_POOL_USABLE_SIZE)
> +		return 0;

On 4K pages, STACK_DEPOT_TRIE_POOL_USABLE_SIZE is 16,208 bytes. When a
node reaches 1,024 children, inserting the 1,025th child requires
capacity = 2048 -> 40 + 8 * 2048 = 16,424 bytes, so every subsequent
insert under that node permanently fails and returns 0.

Because the trie is keyed from entries[0] (innermost callee) to
entries[nr_entries - 1] (outermost caller), all traces through common
leaf entry points funnel into single chokepoint nodes:
- All KASAN kfree/kmalloc tracks share [kasan_save_stack,
  kasan_save_track, __kasan_slab_free, kfree]; entries[4] is the
  kfree() call site. Once >1,024 call sites invoke kfree() or
  kmalloc(), all new call sites fail to record stacks forever (your
  commit message already saw 835 children on one node in a short test).
- As noted on patch 5, KMSAN's [KMSAN_ALLOCA_MAGIC_ORIGIN] and
  [KMSAN_CHAIN_MAGIC_ORIGIN] nodes hit 1,024 children and break KMSAN.
- Keying from entries[0] also branches at the leaf call site and
  duplicates the entire 20+ frame outer syscall/workqueue chain under
  every distinct call site. A call-stack prefix tree has to be rooted
  at entries[nr_entries - 1] (outermost caller).

> +
> +	new_stack_id = trie_side_table_prepare_stack_slot(side_prealloc);
> +	if (!new_stack_id)
> +		return 0;
> +
> +	/* Reserve replacement topology before the path, the final fallible step. */
> +	if (!tail_append) {
> +		new_children = trie_pool_alloc_children(capacity, pool_prealloc);
> +		if (!new_children)
> +			goto err_release;
> +	}
> +	path_root = trie_path_alloc(parent, new_stack_id, entries, nr_entries,
> +				    pool_prealloc, &node);
> +	if (!path_root)
> +		goto err_release;
> +
> +	/* Commit the stack ID before making the path reachable from the trie. */
> +	trie_side_table_publish(node);
> +	if (tail_append) {
> +		struct stack_depot_trie_children *tail_children =
> +			(struct stack_depot_trie_children *)children;
> +
> +		/*
> +		 * Publish the node before the visible count. Readers may transiently
> +		 * see NULL and miss; the writer-lock recheck prevents duplicates.
> +		 */
> +		rcu_assign_pointer(tail_children->nodes[pos], path_root);
> +		WRITE_ONCE(tail_children->nr_children, pos + 1);
> +	} else {
> +		if (children)
> +			trie_children_init(children, new_children);
> +		trie_children_insert(new_children, path_root, pos);
> +		rcu_assign_pointer(*slot, new_children);
> +		if (children)
> +			trie_retire_children(children);
> +	}
> +
> +	return new_stack_id;
> +
> +err_release:
> +	if (new_children)
> +		trie_pool_release_children(new_children);
> +	return 0;
> +}
> +
> +static u32
> +trie_split_child(const struct stack_depot_trie_children __rcu **slot,
> +		 const struct stack_depot_trie_children *children,
> +		 const struct stack_depot_trie_node *child,
> +		 unsigned int pos, unsigned int matched,
> +		 const unsigned long *entries, unsigned int nr_entries,
> +		 void **pool_prealloc,
> +		 struct stack_depot_trie_side_prealloc *side_prealloc)
> +{
> +	struct stack_depot_trie_children *prefix_children = NULL;
> +	struct stack_depot_trie_children *new_children = NULL;
> +	const struct stack_depot_trie_node *new_node;
> +	const struct stack_depot_trie_node *suffix_roots[2];
> +	struct stack_depot_frame_run run;
> +	struct stack_depot_trie_node *split_prefix = NULL;
> +	struct stack_depot_trie_node *old_suffix = NULL;
> +	unsigned int nr_suffix_roots;
> +	unsigned int old_suffix_len;
> +	unsigned int i;
> +	size_t split_prefix_size;
> +	size_t old_suffix_size;
> +	u32 new_stack_id;
> +	bool has_new_suffix;
> +
> +	new_stack_id = trie_side_table_prepare_stack_slot(side_prealloc);
> +	if (!new_stack_id)
> +		return 0;
> +
> +	/* Rebuild the child's run as newly allocated prefix and old suffix nodes. */
> +	run = child->run;
> +	run.nr_entries = matched;
> +	split_prefix_size = trie_node_bytes(&run);
> +	old_suffix_len = child->run.nr_entries - matched;
> +	run.nr_entries = old_suffix_len;
> +	old_suffix_size = trie_node_bytes(&run);
> +	has_new_suffix = matched < nr_entries;
> +	nr_suffix_roots = has_new_suffix ? 2 : 1;
> +
> +	/* Reserve fixed split topology before the optional new suffix path. */
> +	split_prefix = trie_pool_alloc(split_prefix_size, pool_prealloc);
> +	if (!split_prefix)
> +		goto err_release;
> +	old_suffix = trie_pool_alloc(old_suffix_size, pool_prealloc);
> +	if (!old_suffix)
> +		goto err_release;
> +	new_children = trie_pool_alloc_children(children->capacity, pool_prealloc);
> +	if (!new_children)
> +		goto err_release;
> +	prefix_children = trie_pool_alloc_children(nr_suffix_roots, pool_prealloc);
> +	if (!prefix_children)
> +		goto err_release;
> +
> +	if (has_new_suffix) {
> +		const struct stack_depot_trie_node *new_suffix;
> +		unsigned long old_suffix_frame;
> +
> +		new_suffix = trie_path_alloc(split_prefix, new_stack_id,
> +					     &entries[matched], nr_entries - matched,
> +					     pool_prealloc, &new_node);
> +		if (!new_suffix)
> +			goto err_release;
> +		stack_depot_trie_node_frame(child, matched, &old_suffix_frame);
> +		/* Children remain sorted by the first frame of each suffix. */
> +		if (old_suffix_frame < entries[matched]) {
> +			suffix_roots[0] = old_suffix;
> +			suffix_roots[1] = new_suffix;
> +		} else {
> +			suffix_roots[0] = new_suffix;
> +			suffix_roots[1] = old_suffix;
> +		}
> +	} else {
> +		new_node = split_prefix;
> +		suffix_roots[0] = old_suffix;
> +	}
> +
> +	/* Rebuild the old path as prefix -> old suffix and attach suffix roots. */
> +	trie_node_init_slice(split_prefix, trie_load_parent(child),
> +			     has_new_suffix ? 0 : new_stack_id, child, 0, matched);
> +	trie_node_init_slice(old_suffix, split_prefix, child->stack_id, child,
> +			     matched, old_suffix_len);
> +	for (i = 0; i < nr_suffix_roots; i++)
> +		trie_children_insert(prefix_children, suffix_roots[i], i);
> +	RCU_INIT_POINTER(old_suffix->children,
> +			 trie_load_children(&child->children));
> +	RCU_INIT_POINTER(split_prefix->children, prefix_children);
> +
> +	/* Publish IDs, reparent descendants, then replace and retire topology. */
> +	if (child->stack_id)
> +		trie_side_table_publish(old_suffix);
> +	trie_side_table_publish(new_node);
> +	/* Old and replacement chains contain identical frames during transition. */
> +	trie_children_init(children, new_children);
> +	RCU_INIT_POINTER(new_children->nodes[pos], split_prefix);
> +	trie_reparent_children(old_suffix);
> +	rcu_assign_pointer(*slot, new_children);
> +	trie_retire_children_with_node(children, child);
> +
> +	return new_stack_id;
> +
> +err_release:
> +	if (split_prefix)
> +		trie_pool_release(split_prefix, split_prefix_size);
> +	if (old_suffix)
> +		trie_pool_release(old_suffix, old_suffix_size);
> +	if (prefix_children)
> +		trie_pool_release_children(prefix_children);
> +	if (new_children)
> +		trie_pool_release_children(new_children);
> +	return 0;
> +}
> +
> +static u32
> +trie_promote_child(const struct stack_depot_trie_children __rcu **slot,
> +		   const struct stack_depot_trie_children *children,
> +		   const struct stack_depot_trie_node *child,
> +		   unsigned int pos, void **pool_prealloc,
> +		   struct stack_depot_trie_side_prealloc *side_prealloc)
> +{
> +	struct stack_depot_trie_children *new_children;
> +	struct stack_depot_trie_node *promoted_node;
> +	size_t node_size;
> +	u32 new_stack_id;
> +
> +	new_stack_id = trie_side_table_prepare_stack_slot(side_prealloc);
> +	if (!new_stack_id)
> +		return 0;
> +	node_size = trie_node_bytes(&child->run);
> +
> +	/* Reserve a clone and replacement children container before publication. */
> +	promoted_node = trie_pool_alloc(node_size, pool_prealloc);
> +	if (!promoted_node)
> +		return 0;
> +	new_children = trie_pool_alloc_children(children->capacity, pool_prealloc);
> +	if (!new_children)
> +		goto out_release_node;
> +
> +	/* Add the stack ID through a clone, then reparent before retirement. */
> +	memcpy(promoted_node, child, node_size);
> +	promoted_node->stack_id = new_stack_id;
> +	trie_side_table_publish(promoted_node);
> +	trie_children_init(children, new_children);
> +	RCU_INIT_POINTER(new_children->nodes[pos], promoted_node);
> +	trie_reparent_children(promoted_node);
> +	rcu_assign_pointer(*slot, new_children);
> +	trie_retire_children_with_node(children, child);
> +
> +	return new_stack_id;
> +
> +out_release_node:
> +	trie_pool_release(promoted_node, node_size);
> +	return 0;
> +}
> +
> +static u32
> +stack_depot_trie_insert(const unsigned long *entries,
> +			unsigned int nr_entries, void **pool_prealloc,
> +			struct stack_depot_trie_side_prealloc *side_prealloc)
> +{
> +	const struct stack_depot_trie_children *children;
> +	const struct stack_depot_trie_children __rcu **slot =
> +		&stack_depot_trie_root;
> +	const struct stack_depot_trie_node *child;
> +	struct stack_depot_trie_node *parent = NULL;
> +	unsigned int matched;
> +	unsigned int pos;
> +	u32 stack_id;
> +
> +	lockdep_assert_held(&stack_depot_trie_writer_lock);
> +	lockdep_assert_held(&pool_lock);
> +
> +	for (;;) {
> +		pos = 0;
> +		children = trie_load_children(slot);
> +		/* No matching child: attach the remaining path. */
> +		if (!children ||
> +		    !trie_children_find_position(children, entries[0], &pos)) {
> +			stack_id = trie_insert_path(slot, parent, children, pos,
> +						    entries, nr_entries, pool_prealloc,
> +						    side_prealloc);
> +			break;
> +		}
> +
> +		child = trie_children_load_child(children, pos);
> +		matched = trie_node_match(child, entries, nr_entries);
> +		/* A partial child match requires a prefix/suffix split. */
> +		if (matched < child->run.nr_entries) {
> +			stack_id = trie_split_child(slot, children, child, pos,
> +						    matched, entries, nr_entries,
> +						    pool_prealloc, side_prealloc);
> +			break;
> +		}
> +
> +		/* The input ends here: reuse a stack node or promote an internal one. */
> +		if (matched == nr_entries) {
> +			if (child->stack_id)
> +				return child->stack_id;
> +			stack_id = trie_promote_child(slot, children, child, pos,
> +						      pool_prealloc, side_prealloc);
> +			break;
> +		}
> +
> +		/* The child matched completely; continue with the remaining frames. */
> +		parent = (struct stack_depot_trie_node *)child;
> +		slot = &parent->children;
> +		entries += matched;
> +		nr_entries -= matched;
> +	}
> +
> +	if (stack_id)
> +		trie_side_table_last_stack_id = stack_id;
> +	return stack_id;
> +}
> +
> +static unsigned int trie_fetch_into(const struct stack_depot_trie_node *node,
> +				    unsigned long *entries,
> +				    unsigned int max_entries)
> +{
> +	const struct stack_depot_trie_node *cur;
> +	unsigned int total;
> +	unsigned int pos;
> +	unsigned int i;
> +
> +	total = 0;
> +	for (cur = node; cur; cur = trie_load_parent(cur))
> +		total += cur->run.nr_entries;
> +	if (max_entries < total)
> +		return 0;
> +
> +	pos = total;
> +	for (cur = node; cur; cur = trie_load_parent(cur)) {
> +		pos -= cur->run.nr_entries;
> +		for (i = 0; i < cur->run.nr_entries; i++)
> +			stack_depot_trie_node_frame(cur, i, &entries[pos + i]);
> +	}
> +
> +	return total;
> +}
> +
> +static unsigned int trie_fetch_range(const struct stack_depot_trie_node *node,
> +				     unsigned int offset,
> +				     unsigned long *entries,
> +				     unsigned int max_entries)
> +{
> +	const struct stack_depot_trie_node *cur;
> +	unsigned int end;
> +	unsigned int start;
> +	unsigned int total;
> +	unsigned int pos;
> +	unsigned int i;
> +
> +	total = 0;
> +	for (cur = node; cur; cur = trie_load_parent(cur))
> +		total += cur->run.nr_entries;
> +	if (offset >= total)
> +		return 0;
> +
> +	max_entries = min(max_entries, total - offset);
> +	end = offset + max_entries;
> +	pos = total;
> +	for (cur = node; cur; cur = trie_load_parent(cur)) {
> +		pos -= cur->run.nr_entries;
> +		start = max(pos, offset);
> +		for (i = start; i < min(pos + cur->run.nr_entries, end); i++)
> +			stack_depot_trie_node_frame(cur, i - pos, &entries[i - offset]);
> +	}
> +
> +	return max_entries;
> +}
> +
> +static unsigned int trie_fetch_handle_into(depot_stack_handle_t handle,
> +					   unsigned long *entries,
> +					   unsigned int max_entries)
> +{
> +	const struct stack_depot_trie_node *node;
> +	u32 stack_id;
> +	unsigned int nr_entries;
> +
> +	stack_id = trie_stack_id(handle);
> +	rcu_read_lock_sched_notrace();
> +	node = trie_side_table_lookup(stack_id);
> +	if (WARN_ONCE(!node, "corrupt trie handle %08x\n", handle)) {
> +		rcu_read_unlock_sched_notrace();
> +		return 0;
> +	}
> +	nr_entries = trie_fetch_into(node, entries, max_entries);
> +	rcu_read_unlock_sched_notrace();
> +	if (nr_entries)
> +		kmsan_unpoison_memory(entries, nr_entries * sizeof(*entries));
> +
> +	return nr_entries;
> +}
> +
> +static unsigned int trie_fetch_handle_range(depot_stack_handle_t handle,
> +					    unsigned int offset,
> +					    unsigned long *entries,
> +					    unsigned int max_entries)
> +{
> +	const struct stack_depot_trie_node *node;
> +	u32 stack_id;
> +	unsigned int nr_entries;
> +
> +	stack_id = trie_stack_id(handle);
> +	rcu_read_lock_sched_notrace();
> +	node = trie_side_table_lookup(stack_id);
> +	if (WARN_ONCE(!node, "corrupt trie handle %08x\n", handle)) {
> +		rcu_read_unlock_sched_notrace();
> +		return 0;
> +	}
> +	nr_entries = trie_fetch_range(node, offset, entries, max_entries);
> +	rcu_read_unlock_sched_notrace();
> +	if (nr_entries)
> +		kmsan_unpoison_memory(entries, nr_entries * sizeof(*entries));
> +
> +	return nr_entries;
> +}
> +
>  unsigned int stack_depot_fetch(depot_stack_handle_t handle,
>  			       unsigned long **entries)
>  {
> @@ -787,6 +2316,8 @@ unsigned int stack_depot_fetch(depot_stack_handle_t handle,
>  
>  	if (!handle || stack_depot_disabled)
>  		return 0;
> +	if (WARN_ON_ONCE(stack_depot_handle_is_trie(handle)))
> +		return 0;
>  
>  	stack = depot_fetch_stack(handle);
>  	/*
> @@ -813,6 +2344,8 @@ unsigned int stack_depot_fetch_into(depot_stack_handle_t handle,
>  	if (stack_depot_disabled)
>  		return 0;
>  	WARN_ON_ONCE(!entries || !max_entries);
> +	if (stack_depot_handle_is_trie(handle))
> +		return trie_fetch_handle_into(handle, entries, max_entries);
>  
>  	stack = depot_fetch_stack(handle);
>  	if (!stack)
> @@ -835,6 +2368,8 @@ void stack_depot_put(depot_stack_handle_t handle)
>  
>  	if (!handle || stack_depot_disabled)
>  		return;
> +	if (WARN_ON_ONCE(stack_depot_handle_is_trie(handle)))
> +		return;
>  
>  	stack = depot_fetch_stack(handle);
>  	/*
> @@ -851,11 +2386,53 @@ void stack_depot_put(depot_stack_handle_t handle)
>  }
>  EXPORT_SYMBOL_GPL(stack_depot_put);
>  
> +static void trie_print(depot_stack_handle_t handle)
> +{
> +	unsigned long entries[STACK_DEPOT_PRINT_CHUNK_FRAMES];
> +	unsigned int nr_entries;
> +	unsigned int offset = 0;
> +
> +	while ((nr_entries = trie_fetch_handle_range(handle, offset, entries,
> +						     ARRAY_SIZE(entries)))) {
> +		stack_trace_print(entries, nr_entries, 0);
> +		offset += nr_entries;
> +	}
> +}
> +
> +static int trie_snprint(depot_stack_handle_t handle, char *buf, size_t size,
> +			int spaces)
> +{
> +	unsigned long entries[STACK_DEPOT_PRINT_CHUNK_FRAMES];
> +	unsigned int generated;
> +	unsigned int nr_entries;
> +	unsigned int offset = 0;
> +	unsigned int total = 0;
> +
> +	while (size &&
> +	       (nr_entries = trie_fetch_handle_range(handle, offset, entries,
> +						    ARRAY_SIZE(entries)))) {
> +		generated = stack_trace_snprint(buf, size, entries, nr_entries, spaces);
> +		total += generated;
> +		if (generated >= size)
> +			break;
> +		buf += generated;
> +		size -= generated;
> +		offset += nr_entries;
> +	}
> +
> +	return total;
> +}
> +
>  void stack_depot_print(depot_stack_handle_t stack)
>  {
>  	unsigned long *entries;
>  	unsigned int nr_entries;
>  
> +	if (stack_depot_handle_is_trie(stack)) {
> +		trie_print(stack);
> +		return;
> +	}
> +
>  	nr_entries = stack_depot_fetch(stack, &entries);
>  	if (nr_entries > 0)
>  		stack_trace_print(entries, nr_entries, 0);
> @@ -868,6 +2445,9 @@ int stack_depot_snprint(depot_stack_handle_t handle, char *buf, size_t size,
>  	unsigned long *entries;
>  	unsigned int nr_entries;
>  
> +	if (stack_depot_handle_is_trie(handle))
> +		return trie_snprint(handle, buf, size, spaces);
> +
>  	nr_entries = stack_depot_fetch(handle, &entries);
>  	return nr_entries ? stack_trace_snprint(buf, size, entries, nr_entries,
>  						spaces) : 0;
> 
> -- 
> Git-155)

  reply	other threads:[~2026-10-20 15:05 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-08 13:13 [PATCH RFC v2 00/11] stackdepot: reduce memory use for persistent stack records with a trie Caleb Kan
2026-09-08 13:13 ` [PATCH RFC v2 01/11] stackdepot: stop preallocating after the final pool Caleb Kan
2026-10-16 16:53   ` Marco Elver
2026-09-08 13:13 ` [PATCH RFC v2 02/11] stackdepot: add caller-owned stack trace fetching Caleb Kan
2026-10-20 11:00   ` Marco Elver
2026-09-08 13:13 ` [PATCH RFC v2 03/11] mm/page_owner: preserve accounting with countable stack depot records Caleb Kan
2026-10-20 11:11   ` Marco Elver
2026-09-08 13:13 ` [PATCH RFC v2 04/11] mm/kmemleak: print trie-backed stack depot traces Caleb Kan
2026-09-08 13:13 ` [PATCH RFC v2 05/11] kmsan: report " Caleb Kan
2026-10-20 14:53   ` Marco Elver
2026-09-08 13:13 ` [PATCH RFC v2 06/11] mm/slub: materialize " Caleb Kan
2026-09-08 13:13 ` [PATCH RFC v2 07/11] drm/locking: preserve deadlock diagnostics for trie-backed stacks Caleb Kan
2026-09-08 13:13 ` [PATCH RFC v2 08/11] scripts/gdb: reject trie-backed stack depot handles Caleb Kan
2026-09-08 13:13 ` [PATCH RFC v2 09/11] stackdepot: add architecture hooks for compact frame storage Caleb Kan
2026-10-20 14:56   ` Marco Elver
2026-09-08 13:13 ` [PATCH RFC v2 10/11] stackdepot: share persistent stack prefixes with trie storage Caleb Kan
2026-10-20 15:04   ` Marco Elver [this message]
2026-09-08 13:13 ` [PATCH RFC v2 11/11] stackdepot: add KUnit tests for " Caleb Kan
2026-10-07 10:34 ` [PATCH RFC v2 00/11] stackdepot: reduce memory use for persistent stack records with a trie Matt Fleming
2026-10-20 15:07 ` Marco Elver

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=ateDFtLVwMByzZPG@elver.google.com \
    --to=elver@google.com \
    --cc=akpm@linux-foundation.org \
    --cc=andreyknvl@gmail.com \
    --cc=calebkan1106@gmail.com \
    --cc=ckan@cloudflare.com \
    --cc=dvyukov@google.com \
    --cc=glider@google.com \
    --cc=kasan-dev@googlegroups.com \
    --cc=kernel-team@cloudflare.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=osalvador@suse.de \
    --cc=vbabka@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®