Skip to content

Commit b7f0c8f

Browse files
committed
[PyROOT] Set memory policy to "strict"
PyROOT has many memory leaks, which is a major pain point for people using it for long-running scripts in batch jobs. One source of memory leaks was indentified to be the "heuristic memory policy" of PyROOT. This means that PyROOT assumes that every non-const pointer member function argument was interpreted as the object taking ownership if the argument. For examle, take the non-owning RooLinkedList container. It has a `RooLinkedList::Add(RooAbsArg *arg)` method. PyROOT wrongly assumes that this means the RooLinkedList takes ownership of arg, and it drops the PyROOT overship. Nobody feels responsible for deleting the object anymore, and there is a memory leak or `arg`. That particular leak was reported in this forum post: https://root-forum.cern.ch/t/memory-leak-in-fits/56249 Function parameters of type `T *` are very common in ROOT, and only rarely do they imply ownership transfer. So changing the memory policy to "strict" would surely fix also many other memory leaks that are not reported so far. In fact, upstream cppyy doesn't even have this heuristic memory policy anymore! So moving PyROOT also to the strict memory policy closes the gap between PyROOT and cppyy. The potential drawback of this change are crashes in usercode if memory is not properly managed. But these problems should either be fixed by: * the user * dedicated pythonizations for these methods to manage shared ownership via Python reference counters (i.e., setting the parameter as an attribute of the object that the member function was called on) This follows up on PR #4294, in particular it reverts 3a12063.
1 parent d016feb commit b7f0c8f

File tree

8 files changed

+94
-17
lines changed

8 files changed

+94
-17
lines changed

README/ReleaseNotes/v630/index.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,68 @@ Some of these classes are now removed from the public interface:
283283
284284
## PROOF Libraries
285285
286+
## PyROOT
287+
288+
### Changed ownership policy for non-`const` pointer member function parameters
289+
290+
If you have a member function taking a raw pointer, like `MyClass::foo(T
291+
*obj)`, PyROOT was so far assuming that calling this method on `my_instance`
292+
transfers the ownership of `obj` to `my_instance`.
293+
294+
However, this resulted in many memory leaks, since many functions with such a
295+
signature actually don't take ownership of the object.
296+
297+
To avoid such memory leaks, PyROOT now doesn't make this guess anymore as of
298+
ROOT 6.30. Because of this change, some double-deletes or dangling references
299+
might creep up in your scripts. These need to be fixedby properly managing
300+
object lifetime with Python references.
301+
302+
You can fix the dangling references problem for example via:
303+
304+
1. Assigning the object to a python variable
305+
2. Creating an owning collection that keeps the objects alive
306+
3. Writing a pythonization for the member function that does the ownership
307+
transfer if needed
308+
309+
The double-delete problems can be fixed via:
310+
311+
1. Drop the ownership on the Python side with `ROOT.SetOvnership(obj, False)`
312+
3. Writing a pythonization for the member function that drops the ownership on the Python side as above
313+
314+
If there is a problem that you think might also affect also other users and can
315+
be fixed by a pythonization inside ROOT, please don't hesitate to open a GitHub
316+
issue about it!
317+
318+
This affects for example the `TList::Add(TObject *obj)` member function, which
319+
will not transfer ownership from PyROOT to the TList anymore. The new policy
320+
fixes a memory leak, but at the same time it is not possible anymore to create
321+
the contained elements in place:
322+
323+
```python
324+
# A TList is by default a non-owning containter
325+
my_list = ROOT.TList()
326+
327+
328+
# This is working, but resulted in memory leak prior to ROOT 6.30:
329+
obj_1 = ROOT.TObjString('obj_1')
330+
my_list.Add(obj_1)
331+
332+
333+
# This is not allowed anymore:
334+
my_list.Add(ROOT.TObjString('obj_2'))
335+
336+
# Python reference count to contained object is now zero,
337+
# TList contains dangling pointer!
338+
```
339+
340+
The new "strict" policy is also equivalent to the behavior of cppyy, which
341+
brings PyROOT and cppyy again closer together and avoiding surprises to the
342+
user.
343+
344+
**Note:** You can change back to the old policy by calling
345+
`ROOT.SetMemoryPolicy(ROOT.kMemoryHeuristics)` after importing ROOT, but this
346+
should be only used for debugging purposes and this function might be removed
347+
in the future!
286348

287349
## Language Bindings
288350

bindings/pyroot/pythonizations/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ if(roofit)
4646
ROOT/_pythonization/_roofit/_roomcstudy.py
4747
ROOT/_pythonization/_roofit/_roomsgservice.py
4848
ROOT/_pythonization/_roofit/_roonllvar.py
49+
ROOT/_pythonization/_roofit/_rooplot.py
4950
ROOT/_pythonization/_roofit/_rooprodpdf.py
5051
ROOT/_pythonization/_roofit/_roorealvar.py
5152
ROOT/_pythonization/_roofit/_roosimultaneous.py

bindings/pyroot/pythonizations/python/ROOT/_facade.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,6 @@ def _finalSetup(self):
204204
if not self.gROOT.IsBatch() and self.PyConfig.StartGUIThread:
205205
self.app.init_graphics()
206206

207-
# Set memory policy to kUseHeuristics.
208-
# This restores the default in PyROOT which was changed
209-
# by new Cppyy
210-
self.SetMemoryPolicy(self.kMemoryHeuristics)
211-
212207
# Redirect lookups to cppyy's global namespace
213208
self.__class__.__getattr__ = self._fallback_getattr
214209
self.__class__.__setattr__ = lambda self, name, val: setattr(gbl_namespace, name, val)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from ._roomcstudy import RooMCStudy
5252
from ._roomsgservice import RooMsgService
5353
from ._roonllvar import RooNLLVar
54+
from ._rooplot import RooPlot
5455
from ._rooprodpdf import RooProdPdf
5556
from ._roorealvar import RooRealVar
5657
from ._roosimultaneous import RooSimultaneous
@@ -82,6 +83,7 @@
8283
RooMCStudy,
8384
RooMsgService,
8485
RooNLLVar,
86+
RooPlot,
8587
RooProdPdf,
8688
RooRealVar,
8789
RooSimultaneous,

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooabspdf.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ def _pack_cmd_args(*args, **kwargs):
3131
assert len(kwargs) == 0
3232

3333
# Put RooCmdArgs in a RooLinkedList
34-
cmdList = ROOT.RooLinkedList()
34+
cmd_list = ROOT.RooLinkedList()
3535
for cmd in args:
3636
if not isinstance(cmd, ROOT.RooCmdArg):
3737
raise TypeError("This function only takes RooFit command arguments.")
38-
cmdList.Add(cmd)
38+
cmd_list.Add(cmd)
3939

40-
return cmdList
40+
# The RooLinkedList passed to functions like fitTo() is expected to be
41+
# non-owning. To make sure that the RooCmdArgs live long enough, we attach
42+
# then as an attribute of the output list, such that the Python reference
43+
# counter doesn't hit zero.
44+
cmd_list.owning_pylist = args
45+
46+
return cmd_list
4147

4248

4349
class RooAbsPdf(RooAbsReal):

bindings/pyroot/pythonizations/test/tseqcollection_listmethods.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@ class TSeqCollectionListMethods(unittest.TestCase):
1515
# Helpers
1616
def create_tseqcollection(self):
1717
sc = ROOT.TList()
18+
sc_pylist = []
1819
for i in reversed(range(self.num_elems)):
19-
sc.Add(ROOT.TObjString(str(i)))
20+
sc_pylist.append(ROOT.TObjString(str(i)))
21+
sc.Add(sc_pylist[-1])
2022

23+
# A TList is by default non-owning. To make sure that the objects live
24+
# long enough, we attach then as an attribute of the output list, such
25+
# that the Python reference counter doesn't hit zero.
26+
sc.owning_pylist = sc_pylist
2127
return sc
2228

2329
# Tests
2430
def test_insert(self):
2531
sc = self.create_tseqcollection()
2632

2733
# Insert with positive index
28-
o = ROOT.TObject()
29-
sc.insert(1, o)
34+
o1 = ROOT.TObject()
35+
sc.insert(1, o1)
3036
self.assertEqual(sc.GetEntries(), self.num_elems + 1)
31-
self.assertEqual(sc.At(1), o)
37+
self.assertEqual(sc.At(1), o1)
3238

3339
# Insert with negative index (starts from end)
3440
o2 = ROOT.TObject()
@@ -84,7 +90,8 @@ def test_pop(self):
8490
sc2.pop(1.0)
8591

8692
# Pop a repeated element
87-
sc2.append(ROOT.TObjString('2'))
93+
o1 = ROOT.TObjString('2')
94+
sc2.append(o1)
8895
elem = sc2.pop()
8996
self.assertEqual(sc2.At(0), elem)
9097

@@ -145,9 +152,9 @@ def test_index(self):
145152
self.assertEqual(sc.index(elem), i)
146153

147154
# Check element not in collection
148-
o = ROOT.TObjString(str(self.num_elems))
155+
o1 = ROOT.TObjString(str(self.num_elems))
149156
with self.assertRaises(ValueError):
150-
sc.index(o)
157+
sc.index(o1)
151158

152159

153160
if __name__ == '__main__':

tutorials/fit/combinedFit.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,16 @@ def __call__(self, par):
122122
fB.SetFitResult(result, iparB)
123123
fB.SetRange(rangeB().first, rangeB().second)
124124
fB.SetLineColor(ROOT.kBlue)
125-
hB.GetListOfFunctions().Add(fB)
125+
# The function list takes ownership of the added elements, so we are cloning
126+
# the function to a double delete.
127+
hB.GetListOfFunctions().Add(fB.Clone())
126128
hB.Draw()
127129

128130
c1.cd(2)
129131
fSB.SetFitResult(result, iparSB)
130132
fSB.SetRange(rangeSB().first, rangeSB().second)
131133
fSB.SetLineColor(ROOT.kRed)
132-
hSB.GetListOfFunctions().Add(fSB)
134+
hSB.GetListOfFunctions().Add(fSB.Clone())
133135
hSB.Draw()
134136

135137
c1.SaveAs("combinedFit.png")

tutorials/roofit/rf106_plotdecoration.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
import ROOT
1313

14+
ROOT.gROOT.SetBatch(True)
15+
1416
# Set up model
1517
# ---------------------
1618

0 commit comments

Comments
 (0)