Skip to content

Own generated native closures and retain their method definitions - #8962

Open
1ndahous3 wants to merge 2 commits into
RustPython:mainfrom
1ndahous3:native_function_owner
Open

1ndahous3 wants to merge 2 commits into
RustPython:mainfrom
1ndahous3:native_function_owner

Conversation

@1ndahous3

@1ndahous3 1ndahous3 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Release dynamically generated native closures with their method definitions instead of leaking each allocation.
  • Keep method definitions alive through descriptor binding and GC clearing, so bound methods retain their callable storage.
  • Tie callable borrows to the function's lifetime and prevent descriptors from exposing an unowned static method definition.

Extracted from #8944 as an independent fix.

AI assistance

Written with Codex (GPT-6), reviewed by a human before submission.

Summary by CodeRabbit

  • Bug Fixes
    • Native methods and descriptors now retain captured state for as long as it is needed, including after the original definition is dropped.
    • Captured state is released when no longer referenced, improving memory cleanup and preventing premature loss of state.
    • Bound native methods preserve their owning definition, improving reliability when methods are accessed through descriptors and helping ensure they remain callable.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: RustPython/RustPython/.coderabbit.yml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: effbc8ea-2428-4be4-978c-2dffb8ea8df9

📥 Commits

Reviewing files that changed from the base of the PR and between a997f2b and b0c4ae4.


📒 Files selected for processing (4)
  • crates/vm/src/builtins/builtin_func.rs
  • crates/vm/src/builtins/descriptor.rs
  • crates/vm/src/function/method.rs
  • crates/vm/src/vm/context.rs

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



📝 Walkthrough

Walkthrough

Native method definitions now retain boxed native functions. Native functions and descriptors use manual GC traversal. Descriptor binding passes the method-definition owner to bound native methods.

Changes

Native method lifetime

Layer / File(s) Summary
Owned method definitions
crates/vm/src/function/method.rs, crates/vm/src/vm/context.rs
HeapMethodDef can own a boxed native function. Context::new_method_def uses the owning constructor. A test checks that generated functions and bound methods remain callable after intermediate objects are dropped, and that captured state is released after the final method is dropped.
Native function traversal
crates/vm/src/builtins/builtin_func.rs
PyNativeFunction and PyNativeMethod manually traverse references. as_func returns a reference tied to self.
Descriptor ownership and binding
crates/vm/src/builtins/descriptor.rs, crates/vm/src/function/method.rs
Descriptors manually traverse the method-definition owner and pass it to bound native methods.

Priority: ⬇️ Low

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

Change: Bug fix

Suggested reviewers: youknowone


Merge Risk

Merge Risk: ⚪ Minimal · up to b0c4a

The generated native-method ownership change has no identified issue that needs resolution before merging.

Security Architecture Review

Security architecture risk: 🔵 Low · up to a997f

This change replaces leaked resources with explicit lifetime management while preserving existing access checks. No introduced security defect was established, but cleanup under concurrent or reentrant execution has not been validated end to end.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The security-relevant scope is in-process native-callable memory safety and resource lifetime. Python callers can invoke existing registered callbacks, but the changed paths do not establish a new capability to select arbitrary native pointers or acquire additional callback authority.

Trust Boundaries and Controls

  • observed — Existing receiver checks before direct and vectorcall dispatch, and type/subtype checks before classmethod binding, remain unchanged. Existing static/class-method exemptions predate this PR; owner propagation changes storage lifetime, not those validation rules.

Resilience and Maintainability Implications

  • observed — The collector subtracts only references into its active candidate set. Before clearing, it untracks dead objects and preserves late-resurrected objects and their reachable dependents. Clearing and final drops run within deferred-drop handling, while callable clearing retains the borrowed storage's owner.

Hardening Proposals

  • proposed — Extend lifetime regression coverage with forced GC, repeated binding, reentrant capture destruction, and concurrent resurrection where threading is enabled. Assert that callable storage remains valid through clearing and is released only after the final owner disappears.



🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: owning generated native closures and retaining their method definitions.
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.


  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • 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.

@youknowone youknowone mentioned this pull request Oct 8, 2026
1 of 3 tasks
@youknowone

Copy link
Copy Markdown
Member

Thank you for contributing. about this patch, I investigated the topic and then I found #9004 approach looks better at the moment.

@1ndahous3

Copy link
Copy Markdown
Contributor Author

@youknowone following #9004, I propose keeping the new type model and narrowing this change to dynamic callable lifetimes. Context::new_method_def still uses Box::leak, so captured state is never released. HeapMethodDef could own the closure, with bound callables retaining the definition’s owner so cleanup cannot leave dangling references.

If you support this focused ownership fix, I can prepare a new PR or remake the current.

@codspeed

codspeed Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 76.92%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 61 untouched benchmarks
⏩ 4 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ rustpython[10000] 14 ms 7.9 ms +76.92%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing 1ndahous3:native_function_owner (b0c4ae4) with main (bd8fdb7)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@fanninpm

fanninpm commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@codspeedbot explain why performance improved

@codspeed

codspeed Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

@codspeedbot explain why performance improved

Why pystone::rustpython[10000] "improved" by 77%

This is not a real speedup of pystone. The PR changes when the automatic cyclic GC runs. On main, a full GC collection happened inside the measured iteration of rustpython[10000]. With this PR, it no longer does.

What the flamegraphs show

main (bd8fdb7) PR (b0c4ae4)
Total 14 ms 7.9 ms
GcState::collect_inner (from eval_breaker_work) 6.1 ms (43%) not present
Compile (compile_with_syntax_warning_handler) 5.7 ms 5.7 ms
Parse ~1.0 ms ~1.0 ms

Everything except the GC pass costs the same. The ~6 ms difference is one collect_inner call: list/dict/tuple traversal via gc_extend_referent_ptrs, plus dropping the garbage it found. This also explains the odd baseline on main. There, rustpython[10000] (14 ms) is slower than rustpython[20000] and rustpython[30000] (7.9 / 7.8 ms), even though it runs fewer loops. The first pystone case was the one paying for the collection.

Why the PR moves the collection

The PR switches PyMethodDescriptor and PyClassMethodDescriptor to traverse = "manual". A payload with HAS_TRAVERSE gets GC-tracked when it's allocated (crates/vm/src/object/core.rs), so these descriptors are now tracked. Before, they weren't. I built main and this PR and checked a fresh interpreter:

main PR
gc.get_count() at startup (818, 3920, 0) (1130, 4618, 0)
Tracked method_descriptor 0 1025
Tracked classmethod_descriptor 0 40

Each interpreter in benches/execution.rs now starts with ~1,000 more tracked objects. Generation counts are shared across the whole process, and the gen0 threshold is 2000, so the point where collections trigger moves. The collection that used to land in pystone's single measured iteration now runs outside the measured window (for example during interpreter setup). None of the other 61 benchmarks changed.

Takeaways

  • Treat the +77% as a side effect of the benchmark setup, not a win from this PR. Any change in how many objects get tracked can move this result either way.
  • The descriptors are now tracked on purpose: they hold _method_def_owner and need to be traversed. The real cost is small: each collection has ~1k more objects to scan.
  • To stop this benchmark from flipping on unrelated changes, the harness could call gc.collect() (or disable automatic GC) before the measured loop in bench_rustpython_code, so no collection falls inside the timed section.

Flamegraphs: main · PR · comparison

Summary
  • Warming up the performance wizard...
  • Investigate pystone rustpython[10000] improvement — Cause: a GC collection no longer falls inside the measured iteration. Not a real speedup.
  • Reply with explanation — Reply with explanation
  • Handling comment — Comment handling finished successfully

Learnings updated. See what CodSpeed remembers about this repository.

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

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.

3 participants