Skip to content

Commit 2faf8ee

Browse files
Fix memory leak in pickle creation (#213)
* fix a memory leak related to pickling Atom subclasses The reason for the changes around __slotnames__ are not clear to me but are necessary * cis: fix ruff invocation * tests: update test so that it does show an improvement over main
1 parent 14eb253 commit 2faf8ee

File tree

6 files changed

+93
-44
lines changed

6 files changed

+93
-44
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
- name: Linting
4848
if: always()
4949
run: |
50-
ruff atom examples tests
50+
ruff check atom examples tests
5151
- name: Typing
5252
if: always()
5353
run: |

atom/src/catom.cpp

+21-13
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,9 @@ PyObject*
375375
CAtom_getstate( CAtom* self )
376376
{
377377
cppy::ptr stateptr = PyDict_New();
378-
if ( !stateptr )
378+
if ( !stateptr ) {
379379
return PyErr_NoMemory(); // LCOV_EXCL_LINE
380+
}
380381

381382
cppy::ptr selfptr(pyobject_cast(self), true);
382383

@@ -391,26 +392,33 @@ CAtom_getstate( CAtom* self )
391392
// Copy __slots__ if present. This assumes copyreg._slotnames was called
392393
// during AtomMeta's initialization
393394
{
394-
cppy::ptr typeptr = PyObject_Type(selfptr.get());
395-
if (!typeptr)
396-
return 0;
397-
cppy::ptr slotnamesptr = typeptr.getattr("__slotnames__");
398-
if (!slotnamesptr.get())
395+
PyObject* typedict = Py_TYPE(selfptr.get())->tp_dict;
396+
cppy::ptr slotnamesptr(PyDict_GetItemString(typedict, "__slotnames__"), true);
397+
if ( !slotnamesptr ) {
399398
return 0;
400-
if (!PyList_CheckExact(slotnamesptr.get()))
399+
}
400+
if ( !PyList_CheckExact(slotnamesptr.get()) ) {
401401
return cppy::system_error( "slot names" );
402+
}
402403
for ( Py_ssize_t i=0; i < PyList_GET_SIZE(slotnamesptr.get()); i++ )
403404
{
404405
PyObject *name = PyList_GET_ITEM(slotnamesptr.get(), i);
405406
cppy::ptr value = selfptr.getattr(name);
406-
if (!value || PyDict_SetItem(stateptr.get(), name, value.get()) )
407+
if (!value ) {
408+
// Following CPython impl it is not an error if the attribute is
409+
// not present.
410+
continue;
411+
}
412+
else if ( PyDict_SetItem(stateptr.get(), name, value.get()) ) {
407413
return 0;
414+
}
408415
}
409416
}
410417

411418
cppy::ptr membersptr = selfptr.getattr(atom_members);
412-
if ( !membersptr || !PyDict_CheckExact( membersptr.get() ) )
419+
if ( !membersptr || !PyDict_CheckExact( membersptr.get() ) ) {
413420
return cppy::system_error( "atom members" );
421+
}
414422

415423
PyObject *name, *member;
416424
Py_ssize_t pos = 0;
@@ -421,9 +429,8 @@ CAtom_getstate( CAtom* self )
421429
}
422430
int test = PyObject_IsTrue( should_gs.get() );
423431
if ( test == 1) {
424-
PyObject *value = member_cast( member )->getattr( self );
425-
if (!value || PyDict_SetItem( stateptr.get(), name, value ) ) {
426-
Py_XDECREF( value );
432+
cppy::ptr value = member_cast( member )->getattr( self );
433+
if (!value || PyDict_SetItem( stateptr.get(), name, value.get() ) ) {
427434
return 0;
428435
}
429436
}
@@ -433,8 +440,9 @@ CAtom_getstate( CAtom* self )
433440
}
434441

435442
// Frozen state
436-
if ( self->is_frozen() && PyDict_SetItem(stateptr.get(), atom_flags, Py_None) )
443+
if ( self->is_frozen() && PyDict_SetItem(stateptr.get(), atom_flags, Py_None) ) {
437444
return 0;
445+
}
438446

439447
return stateptr.release();
440448
}

docs/source/examples/example_doc_generator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def generate_example_doc(docs_path, script_path):
8787

8888
# Add the script to the Python Path
8989
old_python_path = sys.path
90-
sys.path = sys.path + [os.path.dirname(script_path)]
90+
sys.path = [*sys.path, os.path.dirname(script_path)]
9191

9292
# Restore Python path.
9393
sys.path = old_python_path

tests/test_atom_from_annotations.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class A(Atom, use_annotations=True):
168168
assert A.a.validate_mode[1] == (annotation.__origin__,)
169169
elif member is Subclass:
170170
if isinstance(annotation.__args__[0], TypeVar):
171-
assert A.a.validate_mode[1] == object
171+
assert A.a.validate_mode[1] is object
172172
else:
173173
assert A.a.validate_mode[1] == annotation.__args__[0]
174174
else:
@@ -254,9 +254,9 @@ class A(Atom, use_annotations=True, type_containers=depth):
254254
assert type(v) is type(mv)
255255
assert f(A()) == mf(A())
256256
else:
257-
assert type(A.a.item) is type(member.item) # noqa: E721
257+
assert type(A.a.item) is type(member.item)
258258
if isinstance(member.item, List):
259-
assert type(A.a.item.item) is type(member.item.item) # noqa: E721
259+
assert type(A.a.item.item) is type(member.item.item)
260260

261261

262262
@pytest.mark.parametrize(

tests/test_mem.py

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
# --------------------------------------------------------------------------------------
2-
# Copyright (c) 2023, Nucleic Development Team.
2+
# Copyright (c) 2023-2024, Nucleic Development Team.
33
#
44
# Distributed under the terms of the Modified BSD License.
55
#
66
# The full license is in the file LICENSE, distributed with this software.
77
# --------------------------------------------------------------------------------------
88
import gc
99
import os
10+
import pickle
1011
import sys
1112
import time
13+
import tracemalloc
1214
from multiprocessing import Process
1315

1416
import pytest
@@ -53,6 +55,13 @@ class RefObj(Atom):
5355
"atomref": RefObj,
5456
}
5557

58+
PICKLE_MEM_TESTS = {
59+
"dict": DictObj,
60+
"defaultdict": DefaultDictObj,
61+
"list": ListObj,
62+
"set": SetObj,
63+
}
64+
5665

5766
def memtest(cls):
5867
# Create object in a loop
@@ -105,3 +114,34 @@ def test_mem_usage(label):
105114
finally:
106115
p.kill()
107116
p.join()
117+
118+
119+
@pytest.mark.parametrize("label", PICKLE_MEM_TESTS.keys())
120+
def test_pickle_mem_usage(label):
121+
TestClass = PICKLE_MEM_TESTS[label]
122+
123+
obj = TestClass()
124+
125+
for _ in range(100):
126+
pickle.loads(pickle.dumps(obj))
127+
128+
gc.collect()
129+
tracemalloc.start()
130+
for i in range(10000):
131+
pck = pickle.dumps(obj)
132+
pickle.loads(pck)
133+
del pck
134+
gc.collect()
135+
for stat in (
136+
tracemalloc.take_snapshot()
137+
.filter_traces(
138+
[
139+
tracemalloc.Filter(True, "*/atom/*"),
140+
tracemalloc.Filter(False, "*/tests/*"),
141+
]
142+
)
143+
.statistics("lineno")
144+
):
145+
# not sure why I sometimes see a 2 here but the original buggy version
146+
# reported values > 50
147+
assert stat.count < 5

tests/type_checking/test_subclass.yml

+26-25
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,61 @@
11
#------------------------------------------------------------------------------
2-
# Copyright (c) 2021, Nucleic Development Team.
2+
# Copyright (c) 2021-2024, Nucleic Development Team.
33
#
44
# Distributed under the terms of the Modified BSD License.
55
#
66
# The full license is in the file LICENSE, distributed with this software.
77
#------------------------------------------------------------------------------
88
- case: subclass
9+
skip: sys.version_info < (3, 9) # 3.8 uses Type[] while 3.9+ uses type[]
910
parametrized:
1011
- member: Subclass
1112
member_instance: Subclass(A)
12-
member_type: atom.subclass.Subclass[Type[main.A]]
13-
member_value_type: Type[main.A]
13+
member_type: atom.subclass.Subclass[type[main.A]]
14+
member_value_type: type[main.A]
1415
- member: Subclass
1516
member_instance: Subclass(A, B)
16-
member_type: atom.subclass.Subclass[Type[main.A]]
17-
member_value_type: Type[main.A]
17+
member_type: atom.subclass.Subclass[type[main.A]]
18+
member_value_type: type[main.A]
1819
- member: Subclass
1920
member_instance: Subclass((A,))
20-
member_type: atom.subclass.Subclass[Type[main.A]]
21-
member_value_type: Type[main.A]
21+
member_type: atom.subclass.Subclass[type[main.A]]
22+
member_value_type: type[main.A]
2223
- member: Subclass
2324
member_instance: Subclass((A,), B)
24-
member_type: atom.subclass.Subclass[Type[main.A]]
25-
member_value_type: Type[main.A]
25+
member_type: atom.subclass.Subclass[type[main.A]]
26+
member_value_type: type[main.A]
2627
- member: Subclass
2728
member_instance: Subclass((int, A))
28-
member_type: atom.subclass.Subclass[Union[Type[builtins.int], Type[main.A]]]
29-
member_value_type: Union[Type[builtins.int], Type[main.A]]
29+
member_type: atom.subclass.Subclass[Union[type[builtins.int], type[main.A]]]
30+
member_value_type: Union[type[builtins.int], type[main.A]]
3031
- member: Subclass
3132
member_instance: Subclass((int, A), B)
32-
member_type: atom.subclass.Subclass[Union[Type[builtins.int], Type[main.A]]]
33-
member_value_type: Union[Type[builtins.int], Type[main.A]]
33+
member_type: atom.subclass.Subclass[Union[type[builtins.int], type[main.A]]]
34+
member_value_type: Union[type[builtins.int], type[main.A]]
3435
- member: Subclass
3536
member_instance: Subclass((int, A, str))
36-
member_type: atom.subclass.Subclass[Union[Type[builtins.int], Type[main.A], Type[builtins.str]]]
37-
member_value_type: Union[Type[builtins.int], Type[main.A], Type[builtins.str]]
37+
member_type: atom.subclass.Subclass[Union[type[builtins.int], type[main.A], type[builtins.str]]]
38+
member_value_type: Union[type[builtins.int], type[main.A], type[builtins.str]]
3839
- member: Subclass
3940
member_instance: Subclass((int, A, str), B)
40-
member_type: atom.subclass.Subclass[Union[Type[builtins.int], Type[main.A], Type[builtins.str]]]
41-
member_value_type: Union[Type[builtins.int], Type[main.A], Type[builtins.str]]
41+
member_type: atom.subclass.Subclass[Union[type[builtins.int], type[main.A], type[builtins.str]]]
42+
member_value_type: Union[type[builtins.int], type[main.A], type[builtins.str]]
4243
- member: ForwardSubclass
4344
member_instance: ForwardSubclass(resolve1)
44-
member_type: atom.subclass.ForwardSubclass[Type[main.A]]
45-
member_value_type: Type[main.A]
45+
member_type: atom.subclass.ForwardSubclass[type[main.A]]
46+
member_value_type: type[main.A]
4647
- member: ForwardSubclass
4748
member_instance: ForwardSubclass(resolve2)
48-
member_type: atom.subclass.ForwardSubclass[Type[main.A]]
49-
member_value_type: Type[main.A]
49+
member_type: atom.subclass.ForwardSubclass[type[main.A]]
50+
member_value_type: type[main.A]
5051
- member: ForwardSubclass
5152
member_instance: ForwardSubclass(resolve3)
52-
member_type: atom.subclass.ForwardSubclass[Union[Type[builtins.int], Type[main.A]]]
53-
member_value_type: Union[Type[builtins.int], Type[main.A]]
53+
member_type: atom.subclass.ForwardSubclass[Union[type[builtins.int], type[main.A]]]
54+
member_value_type: Union[type[builtins.int], type[main.A]]
5455
- member: ForwardSubclass
5556
member_instance: ForwardSubclass(resolve4)
56-
member_type: atom.subclass.ForwardSubclass[Union[Type[builtins.int], Type[main.A], Type[builtins.str]]]
57-
member_value_type: Union[Type[builtins.int], Type[main.A], Type[builtins.str]]
57+
member_type: atom.subclass.ForwardSubclass[Union[type[builtins.int], type[main.A], type[builtins.str]]]
58+
member_value_type: Union[type[builtins.int], type[main.A], type[builtins.str]]
5859
main: |
5960
import io
6061
from typing import Tuple, Type

0 commit comments

Comments
 (0)