Skip to content

KRN-1117: simplify stackdepot trie insertion - #5

Open
mfleming wants to merge 13 commits into
caleb-kan:caleb/KRN-1117-kernel-portfrom
mfleming:mfleming/KRN-1117-simplify-trie-insert
Open

KRN-1117: simplify stackdepot trie insertion#5
mfleming wants to merge 13 commits into
caleb-kan:caleb/KRN-1117-kernel-portfrom
mfleming:mfleming/KRN-1117-simplify-trie-insert

Conversation

@mfleming

Copy link
Copy Markdown

Summary

This is a review-helper series for the stackdepot trie port. It keeps the trie behavior intact while simplifying the insertion/preallocation control flow:

  • split stack_depot_trie_insert() into smaller case helpers
  • collapse the missing-child/no-matching-child insert paths
  • make trie_child_array_find_slot() handle empty child arrays
  • drop redundant _locked suffixes from private helpers
  • inline trie preallocation into stack_depot_trie_save() and remove the retry path
  • simplify trie pool preallocation to always carry a spare pool page and keep/free it using existing stackdepot semantics
  • skip the x86_64 frame-compression KUnit case under UML

Validation

  • git diff --check
  • remote kbuild object build for lib/stackdepot.o lib/tests/stackdepot_kunit.o
  • KUnit via kbuild pod/UML: stackdepot 8/8 passed with stackdepot.trie_enabled=1 stackdepot_kunit.trie_pool_limit=8192

mfleming added 8 commits July 28, 2026 14:10
The main loop mixed child insertion, splitting existing children on a
diverging nibble, and promoting an internal child to a leaf, which made
the ownership of the walk state hard to follow. Split those three
transitions into dedicated helpers so the loop reads as a decision tree
over the child slot's current state.

Also skip the x86_64 frame-compression KUnit case under CONFIG_UML,
since the UML build pulls in the x86_64 arch bits but can't exercise
the direct-map addresses the test asserts against.
Follow-up to 812b3f5 ("Simplify stack_depot_trie_insert_locked()
control flow"). The missing-children and no-matching-child cases both
end up calling the same helper to append the remaining path, so fold
them into one branch and rename trie_insert_child_locked() to
trie_insert_path_locked() to match what it actually does.

Teach trie_child_array_find_slot() to accept a NULL array (treated as
empty, returning pos = 0) so the caller doesn't need a separate NULL
check or a conditional pos argument. Document the helper's return
contract while we're there.

While here, funnel every insertion case through a single
trie_finish_insert() return via break, and drop a one-line comment on
each of the four cases in the loop so the shape is obvious at a glance.
These helpers are all file-static and only ever run under the trie
writer lock or pool_lock, so the _locked tag doesn't tell the reader
anything the surrounding code doesn't already make obvious. Rename
them to the shorter forms; no behaviour change.
The retry path in stack_depot_trie_save() only re-ran the same
preallocate-then-insert sequence after releasing the leftover pool page
and side-table prealloc, which the out_free path already handles. Drop
it and inline __stack_depot_trie_alloc_prealloc() so the allocation and
locking sit in one place.
Taking pool_lock just to read new_pool is overkill. new_pool can change
between this check and the actual insertion anyway, so the test is only
an opportunistic hint about whether we need to preallocate a page. Drop
the lock and READ_ONCE() the pointer, matching what the hash stackdepot
save path already does.
The new_pool hint was racy. Another writer could publish a pool
between our READ_ONCE() and the trie insert, leaving us without a
preallocated page when we needed one. Drop the hint and always try
the pool-page prealloc once the side-table prealloc has succeeded.

If the insert doesn't consume the page we still hand it to
depot_keep_new_pool() or free it, so keep/free semantics are
unchanged. The out_free label goes away too now that side-table
prealloc failure has nothing to clean up.
The old code mixed child/node/children/array in various ways that made
following it a bit confusing.

Rename the container to struct stack_depot_trie_children with a nodes[]
flexible member, and update the helpers, locals, and pending list to
match. A trie node now has a parent and children, and a children
container owns nodes; each individual child is a stack_depot_trie_node.
No behaviour change.
The generic trie_pool_alloc_insert() and trie_build_split() helpers grew
several unrelated parameters and workspace slots to cover all three
insertion cases at once, which made each caller harder to follow than
the work it was actually doing.

Inline the pool reservation and rollback into trie_insert_path(),
trie_split_child(), and trie_promote_child() so each path allocates only
what it needs and releases the exact set it took on failure. Make
trie_pool_alloc_children() capacity-based and self-sizing, and let
trie_pool_release_children() derive the size from the object it frees.
Zero the insertion workspace once up front and drop the size-carrying
release variant.

Split handling now has a common setup, a single divergence branch that
builds the new tail and orders the two suffixes, and shared
finalisation. Node sizing is expressed via trie_node_bytes_for(mode,
nr_entries) and local names spell out prefix/tail intent.

No behaviour change intended. Validated with git diff --check, strict
checkpatch on the diff, kbuild of lib/stackdepot.o and
lib/tests/stackdepot_kunit.o, and stackdepot KUnit under UML.
@mfleming
mfleming force-pushed the mfleming/KRN-1117-simplify-trie-insert branch from 3852423 to 8d43f65 Compare July 29, 2026 14:03
mfleming added 5 commits July 30, 2026 09:02
Drop the in-place tail append so published children are immutable and
readers no longer need to tolerate transient NULL slots during a
lockless search. trie_children_copy() now accepts a NULL source so
the first-child case shares the copy-and-publish path.

Build the new node chain incrementally with local pointers instead
of the stack_depot_trie_alloc scratch arrays, and roll back on
allocation failure by walking parent links from the leaf.
The trie insert and split paths were hard to follow. Insertion sized
the path in one pass, allocated in another via a global
stack_depot_trie_insert_alloc workspace whose arrays were sized to
CONFIG_STACKDEPOT_MAX_FRAMES, then built the chain in a third pass.
Split reused the same workspace and its bulk rollback loop. The
workspace also demanded init-time allocation from memblock or kvzalloc.

Fold all of that into a single trie_path_alloc() shared by insert and
split. It walks @entries once, allocates each node and its path
children as it goes, and rolls back only what it allocated on failure.
The workspace, its init-time allocation, the sizing pass, and the bulk
rollback all go away.

trie_children_copy() is split into trie_children_init(), which
initialises an unpublished container from an immutable source, and
trie_children_insert(), which shifts and inserts into an unpublished
container. Callers now express "copy then insert" directly, and the
immutable/unpublished invariants are visible at each step.

Validation: git diff --check, strict checkpatch, kernel-doc, kbuild of
lib/stackdepot.o and lib/tests/stackdepot_kunit.o, and the UML
stackdepot KUnit suite.
The trie code carried several overlapping vocabularies -- leaf,
COW/generation, tail -- for the same underlying concepts. Reading the
insert and split paths meant translating between them. Settle on one
glossary so the algorithm reads directly.

  node       one frame run in the trie
  children   immutable container of child-node pointers
  child      one node inside a children container
  path       linear sequence of nodes for a stack
  path_root  first node of that sequence
  stack_id   optional non-zero ID set only where a stored stack ends
  prefix     shared portion of a split child
  suffix     divergent portion(s) of a split child
  pos        index within a children container
  slot       root-or-child pointer location the writer updates

leaf_id and the "leaf" node concept go away; ordinary nodes carry an
optional stack_id and there is no distinct leaf type. The RCU/COW and
"generation" comments on children go away in favour of describing the
replacement children container as immutable once published. Tail
becomes suffix in the split path.

While here, drop the __stack_depot_ prefix from private static trie
helpers. We were using __ merely to mean "private", which conflicts
with the kernel convention of reserving __ for genuine lower-level or
public-interface pairings. These helpers are all file-local, so name
them consistently with the trie_ prefix instead.

No behaviour change. Validated with git diff --check, strict
checkpatch, kernel-doc, kbuild objects, and UML stackdepot KUnit
(8/8).
trie_finish_insert() is three lines and has a single caller. Folding
it into stack_depot_trie_insert() makes the successful stack-ID
update and return visible at the call site.
Rename trie_side_table_publish_new_node() to trie_side_table_publish()
and reorder its arguments to (node, stack_id). Document that the path
must be fully initialised before the call, that publication commits
the path with no rollback, and that the side-table mapping must be
installed before the trie slot that makes @node reachable.

The two-node split variant had a single caller, so inline it into
trie_split_child() and drop trie_side_table_publish_split_nodes().
Similarly drop trie_children_replace_at() and express the init plus
pointer store at its callers, where the intent reads directly.

Add kernel-doc for trie_pool_alloc()/trie_pool_release() clarifying
that @SiZe is a byte count, and for trie_pool_alloc_children()/
trie_pool_release_children() clarifying that @capacity counts
child-pointer entries rather than bytes.

Document trie_drain_pending_children() -- when it must run relative
to pool_lock and allocation, the FIFO stop condition, and the paired
node+children reclaim -- and document trie_reparent_children() as a
reparent-before-retirement step, adding a lockdep assertion for the
writer lock.

Normalize trie function prototypes to keep the return type on the
same line as the name where the signature fits in 100 columns.

Validation: diff review, strict checkpatch, and kernel-doc all clean.
UML stackdepot KUnit reports 8/8 passing on the behaviour-changing
revision; the drain-comment refinement afterwards is comment-only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant