Skip to content

Memory leak when attaching event handlers from Python #1972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sean-marten opened this issue Oct 13, 2022 · 6 comments · Fixed by #1973
Closed

Memory leak when attaching event handlers from Python #1972

sean-marten opened this issue Oct 13, 2022 · 6 comments · Fixed by #1973

Comments

@sean-marten
Copy link

sean-marten commented Oct 13, 2022

Environment

  • Pythonnet version: 3.0.0
  • Python version: 3.7.4
  • Operating System: Windows 10
  • .NET Framework 4.8.4515.0

Details

  • There appears to be a memory leak when attaching a method to an event handler from Python. I was able to break this down into a very simple example. Below is my C# Library
using System;

namespace MemoryLeak
{
    public Class MemoryLeak : IDisposable
    {
        public event EventHandler LeakEvent;
        public MemoryLeak()
        {
            GC.Collect();
            Console.WriteLine(GC.GetTotalMemory(true));
        }
        public void Dispose()
        {
            this.LeakEvent = null;
        }
    }
}

(This was the file in my C# class library, just built and took the DLL to use in the below python script)

and the python script that utilizes this library:

import gc
import sys

import clr

def main():
    sys.path.append("./dlls") # the dll from the above class library is stored here
    clr.AddReference("MemoryLeak")

    import System

    from MemoryLeak import MemoryLeak

    def event_handler():
        pass

    for _ in range(200):
        example = MemoryLeak()
        example.LeakEvent += event_handler
        example.LeakEvent -= event_handler
        example.Dispose()
        del example
        gc.collect()
        System.GC.Collect()

if __name__ == "__main__":
    main()

If you run this python script successfully, you will notice that the total memory steadily increases. If you simply remove the following two lines from the python script regarding the addition of the event handler into the C# object, you will notice no memory increase:

        example.LeakEvent += event_handler
        example.LeakEvent -= event_handler

I would expect that between the cleanup of the attached handler, disposal and manual collections from both python and C#, that the event handler + memory leak class would get garbage collected properly, but they do not appear to be removed entirely based on memory tracking.

To note, I did attempt to make a C# console application that performs the same steps as the python script and the memory did not leak in that example.

I am more than happy to help contribute if this is deemed to be an issue with pythonnet, but would need a little guidance on where to start.

@sean-marten
Copy link
Author

sean-marten commented Oct 13, 2022

Output from full python script (in bytes):

502260
525832
526020
526160
526316
526392
526820
527056
527332
527408
527484
527912
527988
525456
525532
525608
526036
526112
526588
526664
527092
527168
527244
527320
527396
527824
527900
527976
536244
528800
528876
528952
529028
537296
529544
529620
529696
529772
531240
531316
531392
529020
529096
529524
529600
529676
529752
529828
530256
530332
530408
538676
530912
530988
531064
531140
531216
531644
531720
531796
531872
532300
532376
532452
532528
532604
533032
533108
533184
541452
530876
530952
531028
531104
531180
531608
531684
531760
531836
531912
532340
532416
532492
532568
532996
533072
533148
533224
533300
533728
535964
536040
536116
536544
536620
536696
536772
536848
537276
534892
534968
535044
535472
535548
535624
535700
535776
536204
536280
536356
536432
536508
536936
537012
537088
537164
537592
537668
537744
537820
537896
538324
538400
538476
538552
538980
539056
536684
536760
536836
537264
537340
537416
537492
537920
537996
538072
538148
546416
538652
538728
538804
538880
547148
539384
539460
539536
539612
540040
540116
540192
540268
540344
540772
540848
540924
538540
538968
539044
539120
539196
539272
539700
539776
539852
539928
540356
540432
540508
540584
540660
541088
541164
541240
541316
549584
541820
541896
541972
542048
542476
542552
542628
542704
540332
540760
540836
540912
549180
541416
541492
541568
541644
541720
542148
542224
542300
542376
547484
547560

Output when event handler lines are removed (in bytes):

502260
504568
504944
505032
505056
505080
505104
505288
505312
505336
505712
505736
505760
505784
505808
505832
505856
506200
506576
506600
506624
506648
506672
503704
503728
503752
504128
504152
504176
504200
504224
504248
504272
504296
504672
504696
504720
504744
504768
504792
504816
504840
505216
505240
505264
505288
505312
505336
505360
513576
505760
505784
505808
505832
505856
505880
505904
505928
506316
507172
507196
507220
507244
507268
507292
515508
507692
507716
507740
507764
507788
507812
507836
512740
504924
504948
504972
504996
505020
505044
505068
513284
505468
505492
505516
505540
505564
505588
505612
505636
506012
506036
506060
506084
506108
506132
506156
506180
506556
506580
506604
506628
506652
506676
506700
506724
507100
507124
507148
507172
507196
507220
507244
507268
507644
507668
507692
507716
507740
507764
507788
507812
508188
504548
504572
504596
504620
504644
504668
504692
505068
505092
505116
505140
505164
505188
505212
505236
505612
505636
505660
505684
505708
505732
505756
513972
506156
506180
506204
506228
506252
506276
506300
506324
506700
506724
506748
506772
506796
506820
506844
515060
507244
507268
507292
507316
507340
507364
507388
507412
507788
507812
507836
504548
504572
504596
504620
504644
505020
505044
505068
505092
505116
505140
505164
505188
505564
505588
505612
505636
505660
505684
505708
505732
506108
506132
506156
506180
506204
506228

If you run even more cycles, the problem will exacerbate as well.

@sean-marten sean-marten changed the title Memory link when attaching event handlers from Python Memory leak when attaching event handlers from Python Oct 13, 2022
@lostmsu
Copy link
Member

lostmsu commented Oct 13, 2022

I don't see you calling .NET GC anywhere.

@lostmsu lostmsu closed this as not planned Won't fix, can't repro, duplicate, stale Oct 13, 2022
@sean-marten
Copy link
Author

I don't see you calling .NET GC anywhere.

Do you mean I am not manually calling collect?

@sean-marten
Copy link
Author

sean-marten commented Oct 13, 2022

@lostmsu Sorry I was typing over from a different computer, I was actually manually calling GC.Collect(); (and results are the same). If that is not what you meant, could you please clarify?

EDIT: I saw a different issue where you also mentioned someone was not calling .NET GC, and by those comments I assume that you mean that I am not doing some sort of:

import System

# run some code...

System.GC.Collect()

...in my python script. I have tried this out and edited my original python script and the results are still the same.

@lostmsu
Copy link
Member

lostmsu commented Oct 13, 2022

@sean-marten please, doublecheck the output with all the GC calls, then if it reproduces, I can take a look at it.

@sean-marten
Copy link
Author

Okay @lostmsu , so I re-ran with the .NET GC collection calls both at the end of the python loop and in the dispose method of the C# library. Also for posterity I still am running the python gc collection at the end of the python loop. I also added a dummy array object to the MemoryLeak constructor class which exacerbates the memory leak as well. I will paste in the updated scripts and a summary of the results:

import gc
import sys

import clr

def main():
    sys.path.append("./dlls") # the dll from the above class library is stored here
    clr.AddReference("MemoryLeak")

    import System

    from MemoryLeak import MemoryLeak

    def event_handler():
        pass

    for _ in range(200):
        example = MemoryLeak()
        example.LeakEvent += event_handler
        example.LeakEvent -= event_handler
        example.Dispose()
        del example
        gc.collect()
        System.GC.Collect()

if __name__ == "__main__":
    main()
using System;
using System.Linq;

namespace MemoryLeak
{
    public Class MemoryLeak : IDisposable
    {
        public event EventHandler LeakEvent;
        private Array arr;  // dummy array to exacerbate memory leak

        public MemoryLeak()
        {
            this.arr = Enumerable.Range(0, 10000).ToArray();
        }

        public void Dispose()
        {
            this.LeakEvent = null;
            GC.Collect();
            Console.WriteLine(GC.GetTotalMemory(true));
        }
    }
}

When run normally, we see the min and max of the GC.GetTotalMemory method return ~0.6 MB and ~8.6 MB, respectively. If you increase the number of loops in the python script from 200 to let's say 500, that becomes a min and max of ~0.6 MB and ~20.6 MB, respectively. And increasing the loop number only makes it worse, as expected.

If you remove the python lines regarding registering the event handler from python, the output of GC.GetTotalMemory teeters between ~0.6 MB and ~2.1 MB, but never exceeds the larger value.

Please let me know if you would like me to do any further diagnosis!

@filmor filmor reopened this Oct 13, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Oct 13, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Oct 13, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Oct 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants