Skip to content

Commit

Permalink
[pydrake] Add example of memory leaks (RobotLocomotion#21951)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwnimmer-tri authored and RussTedrake committed Dec 15, 2024
1 parent ecc035c commit 4249b7d
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 0 deletions.
10 changes: 10 additions & 0 deletions bindings/pydrake/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,16 @@ drake_py_unittest(
tags = ["lint"],
)

# TODO(jwnimmer-tri) Once this a real test, switch it to drake_py_unittest.
drake_py_binary(
name = "memory_leak_test",
srcs = ["test/memory_leak_test.py"],
add_test_rule = True,
deps = [
":all_py",
],
)

drake_py_binary(
name = "stubgen",
srcs = ["stubgen.py"],
Expand Down
90 changes: 90 additions & 0 deletions bindings/pydrake/test/memory_leak_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Eventually this program might grow up to be an actual regression test for
memory leaks, but for now it merely serves to demonstrate such leaks.
Currently, it neither asserts the absence of leaks (i.e., a real test) nor the
presence of leaks (i.e., an expect-fail test) -- instead, it's a demonstration
that we can instrument and observe by hand, to gain traction on the problem.
"""

import argparse
import dataclasses
import gc
import sys

from pydrake.systems.analysis import Simulator
from pydrake.systems.framework import DiagramBuilder
from pydrake.systems.primitives import ConstantVectorSource


@dataclasses.dataclass
class RepetitionDetail:
"""Captures some details of an instrumented run: an iteration counter, and
the count of allocated memory blocks."""
i: int
blocks: int | None = None


def _dut_simple_source():
"""A device under test that creates and destroys a leaf system."""
source = ConstantVectorSource([1.0])


def _dut_trivial_simulator():
"""A device under test that creates and destroys a simulator that contains
only a single, simple subsystem."""
builder = DiagramBuilder()
builder.AddSystem(ConstantVectorSource([1.0]))
diagram = builder.Build()
simulator = Simulator(system=diagram)
simulator.AdvanceTo(1.0)


def _repeat(*, dut: callable, count: int) -> list[RepetitionDetail]:
"""Returns the details of calling dut() for count times in a row."""
# Pre-allocate all of our return values.
details = [RepetitionDetail(i=i) for i in range(count)]
gc.collect()
tare_blocks = sys.getallocatedblocks()
# Call the dut repeatedly, keeping stats as we go.
for i in range(count):
dut()
gc.collect()
details[i].blocks = sys.getallocatedblocks() - tare_blocks
return details


def _main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--count",
metavar="N",
type=int,
default=25,
help="Number of iterations to run",
)
parser.add_argument(
"--dut",
metavar="NAME",
help="Chooses a device under test; when not given, all DUTs are run.",
)
args = parser.parse_args()
all_duts = dict([
(dut.__name__[5:], dut)
for dut in [
_dut_simple_source,
_dut_trivial_simulator,
]
])
if args.dut:
run_duts = {args.dut: all_duts[args.dut]}
else:
run_duts = all_duts
for name, dut in run_duts.items():
details = _repeat(dut=dut, count=args.count)
print(f"RUNNING: {name}")
for x in details:
print(x)


assert __name__ == "__main__", __name__
sys.exit(_main())

0 comments on commit 4249b7d

Please sign in to comment.