Repository navigation
Conversation
InterpreterCallbacks wraps the ASTContext's existing external source, the ASTReader, into a multiplexer together with its own source. Before doing so it released the ASTContext's reference with resetWithoutRelease(), so the reference count of the ASTReader never dropped to zero and the ASTReader, with everything it had deserialized, leaked with every interpreter. ExternalASTSourceWrapper now holds the wrapped source by IntrusiveRefCntPtr, and the ASTContext's reference is released normally when the multiplexer replaces it. The ASTReader is deleted when the last of the wrapper and the CompilerInstance lets it go. Assisted-by: Claude:Opus 5.5
An interpreter keeps its AST and its compiled code until the process ends, even when it is destroyed. CIFactory sets DisableFree for every interpreter, so Interpreter::ShutDown() hands the Sema and the ASTContext to llvm::BuryPointer. The JIT memory managers never free their sections, because unloading in ROOT's interpreter freed code that TClingCallFunc wrappers still pointed to (root-project#10898). Added the option --disposable. An interpreter created with it turns DisableFree off, and its memory managers release the JIT's memory. A program can then do work in an interpreter of its own and get all the memory back by destroying it. Interpreters created without the option behave as before. Assisted-by: Claude:Opus 5.5
The interpreter never frees the code it compiles: every line processed stays in memory until the process ends. Code that is not needed after it has run can instead be compiled in an interpreter of its own, which gives all its memory back when it is destroyed. TInterpreter::Disposable is the interface of such an interpreter, with Declare() and ProcessLine(). TCling keeps the arguments it creates its cling interpreter with, and CreateDisposable() creates a TClingDisposable: a cling interpreter with the same arguments and cling's --disposable. It finds the same headers and C++ modules, does not see what was declared in the main interpreter, and runs without cling's runtime. ROOT's LLVM is built without thread support, so its creation, its calls and its destruction take gInterpreterMutex. Added tests to TClingTests: a disposable interpreter finds ROOT's headers and runs code, and on Linux three of them with 200 lines each keep less than 1 MB of heap. Assisted-by: Claude:Opus 5.5
MIRs go through TMethodCall, which cannot pass every argument type: a
MIR calling REveElement::SetTitle("...") crashes the server, because the
string literal does not become the std::string the method takes. An
alternative through gROOT->ProcessLine() was commented out, because every
compiled MIR stays in the interpreter's memory.
With WebEve.MIRInterpreter set to Disposable, a MIR is compiled and run
in a disposable interpreter instead, which declares the header of each
class it is used for. After WebEve.MIRsPerInterpreter MIRs, 1000 by
default, the interpreter is replaced, and destroying it frees the memory
of the code it compiled. TMethodCall stays the default: it is much
faster for the MIRs it can handle.
Assisted-by: Claude:Opus 5.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds disposable interpreters: cling interpreters that give back all the memory of the code they
compiled when they are destroyed.
Motivation
cling never frees the code it compiles. Every line
gInterpreterprocesses stays in memory untilthe process ends, about 32 kB per line for REve's method calls. Code that is needed only once, such
as REve's calls from the browser or jitted expressions, can instead be compiled in an interpreter of
its own, which is destroyed afterwards.
Usage
A disposable interpreter is created with the same arguments as
gInterpreter, so it finds the sameheaders and C++ modules. Creating one takes about 60 ms, and loading REve's declarations into it
another 35 ms (Debug build). It does not see what was declared in
gInterpreter. Nothing may pointinto its code once it is destroyed.
Changes
Three things in cling kept the memory of a destroyed interpreter. The first two commits deal with
them.
[cling] Let the ASTReader be freed with its interpreter. TheASTReaderleaked with everyinterpreter,
gInterpreterincluded. This bug fix stands on its own.[cling] Add --disposable, for interpreters that free all their memory. cling setsDisableFreefor every interpreter, so the AST is never deleted, and it never releases JITmemory. An interpreter created with the new option does both.
The JIT release was disabled in TClingCallFunc needs to support unloading root-project/root#10898, because unloading with
.Ufreed code thatTClingCallFuncwrappers still pointed to. It stays disabled forgInterpreter, andTClingCallFuncnever uses disposable interpreters.[core] Add TInterpreter::CreateDisposable(). The interfaceTInterpreter::Disposable, withDeclare()andProcessLine(), its implementation incore/metacling, and tests. ROOT's LLVMis built without thread support, so a disposable interpreter takes
gInterpreterMutexforeverything it does.
[eve7] Optionally execute MIRs in a disposable interpreter. An example of use, which can bedropped.
Results
17 different REve setter calls on four elements, 6000 calls in all (Linux, Debug build):
gInterpreter->ProcessLine()What the disposable interpreters kept stayed from the first one. From the second on, each kept
nothing.
With the first three commits, ROOT's tests and roottest pass, apart from three that fail the same
way without these changes. REve's tests pass with the fourth.
The REve example
REve executes method calls from the browser, such as
SetMainColor(3)on element 17, throughTMethodCall. WithWebEve.MIRInterpreter: Disposablein.rootrc, it compiles them in adisposable interpreter instead, replaced every 1000 calls.
TMethodCallis about 40 times faster for the calls it can handle. The disposable interpreter isfor the calls it cannot: a call with a string argument, such as
SetTitle("box"), crashes REve'sserver through
TMethodCall. Compiled, it works, and the memory stays bounded by one liveinterpreter:
TMethodCallREve also compiles lambdas for collection filters and table columns. These are kept in
std::functions, which would have to be reassigned before their interpreter is replaced, so theyare not part of this.
Open questions
ASTReaderfix go in on its own first?TInterpreter::CreateDisposable()the right shape and place for the API?LLVM_ENABLE_THREADS=ON, disposable interpreters ranconcurrently on 4 threads, and helgrind found two races in cling, both with small fixes.
🤖 Generated with Claude Code