Repository: python-trio/trio

Issue:
Potential memory leak on windows
I was recently running some scripts overnight that failed due to a MemoryError, but was unable to find anything obvious that would be leaking memory in my scripts.

During my investigation process, I have found that the following script increases memory usage by ~1 MB/s while running. If the `await trio.sleep(0)` is replaced by `pass` the process runs at a constant 9.7 MB of memory usage.

```python
import trio

async def main():
    while True:
        await trio.sleep(0)

if __name__ == '__main__':
    trio.run(main)
```

## Version Info

Python 3.8.1
Trio 0.13.0
Windows 10



Discussion:
Huh, that's plausible; on Windows we have to do some tricky low-level stuff with raw memory allocations and we could easily have messed something up there. Nice catch and thanks for the very clean reproducer.
Just tried this on ubuntu since I hadn't before and the memory usage was constant 😄 
Thanks for checking! That's consistent with it being something in the Windows-only cffi code. I did a quick read of `_io_windows.py` and nothing jumped out at me. Maybe I don't understand how cffi's memory management works? Maybe it's something else and cffi is a red herring? Since we have a clean reproducer there's probably some memory debugging tool that could easily give a definitive answer, but unfortunately I'm not very familiar with Windows dev tooling.
@njsmith 

Using `objgraph` I am able to see that the number of function types seems to be growing by 4 each time the `await trio.sleep(0)` occurs.

The printed sets are the new function types created between each iteration so maybe those function names are helpful.

```
{<function Task._attempt_delivery_of_any_pending_cancel.<locals>.raise_cancel at 0x03969A98>, <function checkpoint.<locals>.<lambda> at 0x03969B28>, <function _filter_impl.<locals>.filter_tree at 0x03969ED0>, <function _filter_impl.<locals>.push_tb_down at 0x03969AE0>}
-----------------------
function                   5652
tuple                      2920
dict                       2914
weakref                    1307
wrapper_descriptor         1208
getset_descriptor          954
builtin_function_or_method 942
method_descriptor          932
cell                       765
type                       725
None
{<function checkpoint.<locals>.<lambda> at 0x03969A08>, <function _filter_impl.<locals>.filter_tree at 0x03969D20>, <function Task._attempt_delivery_of_any_pending_cancel.<locals>.raise_cancel at 0x03969030>, <function _filter_impl.<locals>.push_tb_down at 0x03969348>}
-----------------------
function                   5656
tuple                      2922
dict                       2914
weakref                    1307
wrapper_descriptor         1208
getset_descriptor          954
builtin_function_or_method 942
method_descriptor          932
cell                       768
type                       725
None
{<function _filter_impl.<locals>.push_tb_down at 0x03969618>, <function _filter_impl.<locals>.filter_tree at 0x039696A8>, <function Task._attempt_delivery_of_any_pending_cancel.<locals>.raise_cancel at 0x039695D0>, <function checkpoint.<locals>.<lambda> at 0x039698E8>}
-----------------------
function                   5660
tuple                      2924
dict                       2914
weakref                    1307
wrapper_descriptor         1208
getset_descriptor          954
builtin_function_or_method 942
method_descriptor          932
cell                       771
type                       725
None
{<function _filter_impl.<locals>.push_tb_down at 0x039CF300>, <function checkpoint.<locals>.<lambda> at 0x03969588>, <function _filter_impl.<locals>.filter_tree at 0x039697C8>, <function Task._attempt_delivery_of_any_pending_cancel.<locals>.raise_cancel at 0x03969078>}
```
I think it might be related to `ffi.getwinerror()`

Calling

```python
import tracemalloc
tracemalloc.start()

snapshot1 = tracemalloc.take_snapshot()
trio.run(main)
snapshot2 = tracemalloc.take_snapshot()

top_stats = snapshot2.compare_to(snapshot1, 'lineno')

print("[ Top 10 differences ]")
for stat in top_stats[:10]:
	print(stat)

```

gives me
```
Python37\lib\site-packages\cffi\api.py:479: size=7520 KiB (+7520 KiB), count=100004 (+100004), average=77 B
```
 as largest difference.

And `api.py:479` is
```
    def getwinerror(self, code=-1):
        return self._backend.getwinerror(code) <---
```
Called by `raise_winerror()` in `trio/_core/_windows_cffi.py`.

EDIT:
It looks like a cffi bug.
Calling
```python
import cffi
ffi = cffi.FFI()
while True:
    a = ffi.getwinerror()
```
makes the memory grow rapidly.
Awesome, thanks for tracking that down!

I've filed a bug upstream here: https://bitbucket.org/cffi/cffi/issues/442/getwinerror-leaks-memory
Armin just commented there that he'd committed a fix to the cffi dev branch and is wondering if someone could check and confirm it works. I don't have a Windows C compiler handy, but if someone does and can install that and then try running the original test script again that would be cool.
It's fixed with the changes that Armin committed, I don't have a bitbucket account or else I would have posted there.

Thanks everyone for helping track this one down!
Running the initial snippet with the fixed cffi, memory usage does not increase anymore. I haven't looked into growing number of function types reported by @gesslerpd but they are probably getting cleaned up at some point.
I let the loop run for a few minutes and didn't see any increase (but I did see some slight fluctuations, probably caused by the gc).

So I think this can be closed. The requirements probably have to be updated to reference the latest cffi version as soon as it's released.
Yeah agreed probably taken care of by the garbage collector at some point I just didn't notice it in the short run that I did.

Wasn't aware of `tracemalloc`, will have to remember that it's good for debugging these types of issues.


cffi 1.14 is out with the fix: https://cffi.readthedocs.io/en/latest/whatsnew.html#v1-14

Should we update setup.py to specify 1.14+ instead of 1.12+?

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -86,7 +86,8 @@
         "outcome",
         "sniffio",
         # cffi 1.12 adds from_buffer(require_writable=True) and ffi.release()
-        "cffi>=1.12; os_name == 'nt'",  # "cffi is required on windows"
+        # cffi 1.14 fixes memory leak inside ffi.getwinerror()
+        "cffi>=1.14; os_name == 'nt'",  # "cffi is required on windows"
         "contextvars>=2.1; python_version < '3.7'"
     ],
     # This means, just install *everything* you see under trio/, even if it


Test patch: