Skip to content

fix(textprop): reuse detached interval slots - #479

Open
thanosapollo wants to merge 1 commit into
eval-exec:mainfrom
thanosapollo:fix/text-prop-interval-recycling
Open

thanosapollo wants to merge 1 commit into
eval-exec:mainfrom
thanosapollo:fix/text-prop-interval-recycling

Conversation

@thanosapollo

@thanosapollo thanosapollo commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Text-property intervals live in an index arena (IntervalTree::nodes). When an interval is merged, deleted or pruned, its node is unlinked but the slot stays in the arena, and push_node always appends. So any workload that keeps splitting and re-merging the same text grows the arena for as long as the table lives, even though the number of live intervals stays constant. Examples are a set-text-properties call followed by clearing the same range, or deleting text that spans interval boundaries. Each node is about 80 bytes on 64-bit targets.

Reproducer on current main: in a 32-character buffer with undo disabled, 2,000 iterations of (set-text-properties 9 25 '(p 1)) followed by (set-text-properties 1 33 nil) leave 4,001 arena slots (capacity 4,096) for one live interval. Doing the same thing directly on the table 20,000 times leaves 40,001 slots.

Change

  • Unlinked slots go onto an intrusive free list, chained through their now-unused right link. push_node takes a slot from that list before it grows nodes. Live ids never move.
  • The free list gets slots from both retirement points: delete_zero_length_interval (merge/delete), which already reset the node, and remove_rightmost_interval (trailing prune), which used to leave its node and plist behind.
  • A whole-range delete_range now clears the arena (keeping its capacity) instead of only dropping the root.
  • Both recycling and clearing bump the tree version, so the position, finger and rightmost memos cannot return a reused id. A copy-on-write clone copies the arena together with its free list, and each side then recycles on its own.
  • assert_invariants_for_test now checks that every slot is either linked exactly once or on the free list, and that free slots are nil and zero-length.

The arena's size is now bounded by the peak number of intervals that exist at the same time. Capacity reached in the past is not shrunk.

Tests

New regressions are in buffer/text_props/tests/retention.rs and buffer/buffer_text/tests/interval_retention.rs:

  • fixed-size table churn (20,000 cycles) stays at ≤3 slots/≤4 capacity
  • the Lisp set-text-properties churn above stays at ≤3 slots/≤4 capacity
  • copy-on-write snapshots stay independent while both sides recycle
  • trailing prune and whole-range delete reuse or clear every retired slot, with memos seeded right before retirement
  • 5,000 randomized set/put/remove/insert/delete/clear steps, compared with a per-character model, checking the cached against the uncached lookup and the syntax cache consistency after every step

All five fail on 7764ef71d2 (for example, fixed-size churn retained 40001 slots, set-text-properties churn retained 4001 slots, capacity 4096) and pass with this change.

cargo nextest run -p neovm-core --lib -E 'test(/interval_retention::|text_props::tests::retention::/)'

With Nextest 0.9.146 (neovm-core --lib, 2 test threads):

  • 417/417 pass for test(/interval_retention::|text_props::|textprop|text_prop|buffer_text::|indirect|interval/).
  • 1,740/1,748 pass for the broader test(/^buffer::|pdump|syntax|undo|fontif|font_lock|gc_|textprop|text_prop|propert/). The 8 failures fail identically on unmodified 7764ef71d2 in the same checkout: 2 chartable tests can't find the generated Unicode tables, and 6 doc/load/vm tests fail loading emacs-lisp/pcase from source.
  • cargo clippy -p neovm-core --lib --tests reports nothing in the changed files.

No full-workspace or GUI run is claimed.

Found while tracking down memory growth in a long-running session: repeated text-property updates kept growing the interval arena.

This PR is agent-assisted.

The interval arena only ever grew: merging, deleting or pruning an
interval unlinked its node but left the slot behind, and push_node
always appended.  Repeatedly re-propertizing the same text therefore
grew the arena without bound while the number of live intervals stayed
constant -- 2,000 set-text-properties/clear cycles over a 32-character
buffer left 4,001 slots for one live interval.

Chain unlinked slots into an intrusive free list through their unused
right link and let push_node take from it before growing.  Both
retirement points feed it: delete_zero_length_interval and the
trailing-prune remove_rightmost_interval, which previously left its
node and plist in place.  A whole-range delete_range now clears the
arena, keeping its capacity.  Recycling and clearing bump the tree
version, so no memo can return a reused id, and live ids never move.
A copy-on-write clone copies the free list with the arena.

The arena is now bounded by the peak number of simultaneous intervals.
The test invariant check also verifies that every slot is linked
exactly once or free.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a75e96e6-cc80-437a-9823-fcf50fad512d
📥 Commits

Reviewing files that changed from the base of the PR and between 7764ef7 and 38b1e0a.

📒 Files selected for processing (5)
  • crates/neovm-core/src/buffer/buffer_text/tests/interval_retention.rs
  • crates/neovm-core/src/buffer/buffer_text/tests/mod.rs
  • crates/neovm-core/src/buffer/text_props.rs
  • crates/neovm-core/src/buffer/text_props/tests/mod.rs
  • crates/neovm-core/src/buffer/text_props/tests/retention.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The interval tree now recycles detached node slots instead of continually growing its arena during text-property edits. New tests check slot and capacity bounds, copy-on-write independence, deletion behavior, and property lookups after mixed edits.

Changes

Text property interval retention

Layer / File(s) Summary
Interval arena recycling
crates/neovm-core/src/buffer/text_props.rs
The interval tree reuses free-list slots when adding nodes and recycles slots during deletion. Whole-tree clearing resets arena state while retaining vector capacity. Invariant checks account for linked and free slots.
Retention and behavior tests
crates/neovm-core/src/buffer/text_props/tests/*, crates/neovm-core/src/buffer/buffer_text/tests/*
Tests check slot and capacity bounds during repeated churn, copy-on-write snapshot independence, deletion and pruning, and property lookups after deterministic mixed edits.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: eval-exec

Merge Risk: ⚪ Minimal · up to 38b1e

No concrete issue currently blocks merging the interval-slot reuse change.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 38b1e

The inspected change introduces no material security risk. Retired slots are cleared before reuse, cached lookups are invalidated, and cloned snapshots retain independent storage. Existing text-property entrypoints and their controls are unchanged.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A caller already able to execute text-property mutations can drive recycling in accessible buffer or string property tables. The inspected change affects that existing process-local storage path without adding an entrypoint, target-selection capability, or authority-bearing operation.

Trust Boundaries and Controls

  • inferred — The principal reuse hazards are stale cached identity and cross-snapshot state contamination. Generation invalidation, clearing retired property values, and independently cloned arenas provide concrete counterevidence against those paths in the inspected implementation.

Resilience and Maintainability Implications

  • inferred — Reusing detached slots removes the append-only accumulation mechanism exercised by fixed-size property churn, improving containment of memory retention from repeated edits. This is not a general memory quota: vector capacity remains retained, and workloads with many simultaneously live intervals still require storage.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: reusing detached text-property interval slots.
Description check ✅ Passed The description explains the arena-growth problem, the slot-reuse implementation, and the related regression tests.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from eval-exec October 5, 2026 09:56
thanosapollo added a commit to thanosapollo/neomacs that referenced this pull request Oct 6, 2026
thanos already carries the content of these heads, verbatim or as the
personal variant it ships; this merge keeps the tree and records the
heads so later upstream merges of them need no re-resolution.

- contrib/repeat-backpressure 551220e (eval-exec#459): carried; the rebase only
  moved to FrontendKey, as the main merge did.
- contrib/nested-reader-recovery 91a4022 (eval-exec#460): carried, combined with
  the command-error reporting below.
- fix/command-error-literal-message 271a202 (eval-exec#489): carried as
  56339fa, 98b7742, 0ebd026.
- fix/non-ascii-face-width 9a52a76 (eval-exec#481): carried as 13ad539 and
  7eee365.
- feature/builtin-mcp c9542d3 (eval-exec#480): personal endpoint variant with the
  same reply bound (677591e) and documentation (e0ee6c3).
- fix/text-prop-interval-recycling 38b1e0a (eval-exec#479): personal interval
  recycling passes the same retention tests.
- feature/gui-daemon-publication 1cd96f8 (eval-exec#454): personal deferred GUI
  daemon, a superset.  Kept deliberately: terminal-live-p classifies every
  terminal by its output method (GNU Fterminal_live_p), an unregistered
  frame's native-window wait fails closed, and neomacs-set-frame-opacity
  passes integer percentages through unchanged, since alpha-background
  already reads a fixnum as a percentage (GNU gui_set_alpha_background).
@eval-exec
eval-exec requested a balanced review from Copilot October 7, 2026 13:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It modifies a core, concurrency-sensitive interval arena with subtle free-list/memo-invalidation and copy-on-write invariants whose correctness warrants human verification despite comprehensive tests.

Review effort: Balanced
Findings: None

What changed in this PR

This PR fixes unbounded memory growth in the text-property interval arena (IntervalTree::nodes). Previously, when intervals were merged, deleted, or pruned, their arena slots were unlinked but never reused — push_node always appended — so churn-heavy workloads (e.g. repeated set-text-properties followed by clearing) grew the arena indefinitely even though the live interval count stayed constant. The fix introduces an intrusive free list so retired slots are reused, bounding the arena by the peak simultaneous interval count.

Changes:

  • Added a free_head free list chained through each node's now-unused right link; push_node reuses a free slot before growing nodes, and both retirement points (delete_zero_length_interval, remove_rightmost_interval) recycle via a new recycle_node.
  • A whole-range delete_range now calls a new clear() that truncates nodes (keeping capacity); recycling and clearing bump version so position/finger/rightmost memos cannot return a reused id, and COW clones copy the arena with its free list.
  • assert_invariants_for_test now verifies every slot is linked exactly once or on the free list, plus new regression tests at the table and Lisp level.
File Description
crates/​neovm-core/​src/​buffer/​text_props.rs Core change: free_head, push_node reuse, recycle_node, clear, updated invariants/docs, COW clone, and test accessor.
crates/​neovm-core/​src/​buffer/​text_props/​tests/​retention.rs New unit tests for slot reuse, COW independence, prune/whole-delete, and a randomized differential model.
crates/​neovm-core/​src/​buffer/​text_props/​tests/​mod.rs Registers the new retention test module.
crates/​neovm-core/​src/​buffer/​buffer_text/​tests/​interval_retention.rs New Lisp-level set-text-properties churn regression test.
crates/​neovm-core/​src/​buffer/​buffer_text/​tests/​mod.rs Registers the new interval_retention test module.

I verified the free-list invariants (slots recycled only after full unlinking, no double-free given the cycle check), memo correctness (all three mutation sites bump version, invalidating version-tagged memos), COW clone semantics, build_balanced/from_normalized_runs only running on empty arenas, and that no full-arena traversal ever reads free slots. I found no objective defects to comment on.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@eval-exec eval-exec added the merge-conflict Pull request has merge conflicts; removed automatically once resolved label Oct 8, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-conflict Pull request has merge conflicts; removed automatically once resolved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants