Skip to content

Fix memory leak in table setters#220

Open
joaopaulosr95 wants to merge 1 commit into
NatLabRockies:mainfrom
joaopaulosr95:fix/table-setter-memory-leak
Open

Fix memory leak in table setters#220
joaopaulosr95 wants to merge 1 commit into
NatLabRockies:mainfrom
joaopaulosr95:fix/table-setter-memory-leak

Conversation

@joaopaulosr95

@joaopaulosr95 joaopaulosr95 commented Jul 9, 2026

Copy link
Copy Markdown

Problem

PySAM_table_setter converts the incoming Python dict to a SAM_table and passes it to the generated *_tset function, which copies the table into the module's data. The intermediate table is only destructed on the error path, so every successful dict assignment to a table variable leaks the full converted table.

For Pvwattsv8.SolarResource.solar_resource_data with an 8760-row weather file this is ~1.2MB leaked per assignment. Long-running services that build a model per request (e.g. a REST API wrapping PVWatts) leak until OOM. The leak is unreachable from Python.

PySAM_assign_from_dict (the Model.assign() path) has the same leak on both its success and error paths.

Evidence

Repeat-loop RSS measurement (macOS arm64, Python 3.13, PySAM 7.1.0), assigning the same prebuilt dict to fresh Pvwattsv8 models, GC forced, live Python object counts flat:

Operation ×50 RSS slope (before) RSS slope (after fix)
m.SolarResource.solar_resource_data = d +1.18 MB/iter, linear +0.001 MB/iter
m.assign({"SolarResource": {"solar_resource_data": d}}) +1.18 MB/iter +0.001 MB/iter
pvwatts.new() alone flat flat

That the *_tset functions copy (rather than take ownership) is confirmed by the nested-table branch of PySAM_dict_to_table itself, which already destructs its intermediate right after a successful SAM_table_set_table.

Verification

  • ac_annual and all exported outputs bit-identical before/after the fix.
  • New regression test tests/test_memory_leak.py (peak-RSS bound over 100 assignments, POSIX-only). It runs its measurement loop in a python -I subprocess so the peak-RSS reading is immune to test ordering and to the repo-root PySAM/ dir shadowing the installed package. Fails on stock wheels, passes on patched builds.
  • Verified on both CI platforms by replicating the workflow steps (published wheel + version-matched SAM checkout + pytest tests/test_pysam_all.py):
    • macOS arm64 (Python 3.13): 15/15 functional tests pass stock and patched; leak test fails stock (+82.7MB over 100 assignments), passes patched.
    • Linux aarch64, python:3.13 container (manylinux2014 wheel): 15/15 functional tests pass stock and patched; leak test fails stock (+62.4MB), passes patched.
  • The leak reproduces in the latest published wheel (nrel-pysam==7.1.1.post1) on both platforms; it is not specific to an OS or an older release.

Additional notes

test_pkg.yml installs the published wheel rather than building from source, so the new regression test will fail on the macOS jobs until a wheel containing this fix is published. It passes against any wheel built from a patched source tree.

@brtietz brtietz requested a review from sjanzou July 9, 2026 17:46
PySAM_table_setter and PySAM_assign_from_dict convert the incoming dict
to a SAM_table and pass it to the setter function, which copies the
table into the module's data. The intermediate table was only destructed
on the error path, leaking the full table on the successful path, thus
causing services that build a model per request leak until OOM.

Adds a peak-RSS regression test (POSIX only).
@joaopaulosr95 joaopaulosr95 force-pushed the fix/table-setter-memory-leak branch from a7c0fa0 to b4af536 Compare July 9, 2026 18:17
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.

1 participant