93565


# 現象

tkinterを使用したGUIアプリで、該当箇所をクリックすると、外部と通信して、その情報に基づき、画面更新を行う処理をしています。  
自動実行アプリで、繰り返しクリックさせていると、徐々にメモリ量が上昇していくことが分かりました。（自動実行アプリは、MacroRecorderというものと使用しており、自作ではありません。）  
※手動でクリックしても、同様にメモリ上昇は発生します。

Gitでバージョン管理をしているため、どの時点から現象が発生したかを追跡したのですが、現象が発生するバージョンと発生しないバージョンでのコードの違いはわずかでした。  
現象が発生し始めたバージョンを直前の変更点を書き直して（現象が発生しないバージョンとソースコードを同じに直す）実行してもメモリ上昇が止まりません。  
クリックせず、状態が変化しない場合は、とくにメモリ上昇はありません。

git reset --hardコマンドでバージョンを戻すと、メモリ上昇は起きなくなります。  
git Historyでコードの違いは一目瞭然なので、手動で書き戻しが間違っている、見落としているということはありません。

# ソースコードの説明

全体として大きなコードなので、記載ができません。  
いくつかのマルチプロセス処理で動作しており、Windowsパフォーマンスモニタで、プロセスごとのメモリ上昇を追うと、GUIプロセスでメモリ上昇が発生していることが分かりました。

# 困っていること。

ソースコード戻しても現象が止まらず、コードに原因があるとは思えません。  
そのため、コードを修正しようがないため、大きなアプリケーションでは、何か他にメモリ確保など必要なことがあるのでしょうか？


CC BY-SA 4.0
2023-01-24T02:19:00.413
93535
2023-01-26T06:54:29.860
32891
question
python tkinter memory-leaks
tkinter メモリリーク


ひとまず解決したと思うので、回答に記述します。

# 分析方法

tracemallocを用いて以下のクラスを作成しました。

## ソースコード

[code]

    import datetime
    import os
    import time
    import tracemalloc
    
    
    class trace_memory():
        def __init__(self, interval_time: int = 1*60, top: int = 10):
            self.b_snap_malloc = None
            self.a_snap_malloc = None
    
            self.timer = None
            self.trace_interval = interval_time  # sec
            self.top_slice = top
    
            file_dir = os.getcwd() + '/logs'
            self.file_path = file_dir + '/trace_memory.log'
    
            os.makedirs(file_dir, exist_ok=True)
    
            if os.path.exists(self.file_path):
                with open(self.file_path, 'w') as f:
                    pass
    
        def start(self):
            tracemalloc.start()
    
        def trace(self):
            if self.timer is None:
                self.timer = time.time()
                if self.b_snap_malloc is None:
                    self.b_snap_malloc = tracemalloc.take_snapshot()
            else:
                if (time.time() - self.timer) >= self.trace_interval:
                    self.timer = time.time()
    
                    timestamp = datetime.datetime.now()
    
                    time_format = '%Y-%m-%d %H:%M:%S.%f'
                    timestamp = timestamp.strftime(time_format)
    
                    self.a_snap_malloc = tracemalloc.take_snapshot()
                    top_stats = self.a_snap_malloc.compare_to(
                        self.b_snap_malloc, 'lineno')
    
                    with open(self.file_path, "a") as f:
                        f.write(f'{timestamp}:[memory trace differences]\n')
                        for stat in top_stats[:self.top_slice]:
                            f.write(f'{timestamp}:{stat}\n')
    
                    self.b_snap_malloc = tracemalloc.take_snapshot()
    
    
    if __name__ == '__main__':
        inst = trace_memory()
        inst.start()
        inst.trace()
    
    
[/code]

## 使用方法

デフォルトでは、1分毎にメモリ使用量の差異が大きいTOP10をログに出力します。  
初期化の引数でインターバル時間とTOPいくつまで出力するかを指定できます。  
ループ処理を行うクラスあるいは関数において、  
・ループ前にtrace_memoryのインスタンスを作成  
・ループ前にtrace_memory.start()メソッドを呼び出す。  
ループ処理の内部に  
・trace_memory.trace()メソッドを呼び出すように記述

# 原因

今回のケースでは、Canvasウィジットに対するMouseWheelイベントのbindを画面更新処理ごとに実施していたことが原因でした。これを画面変化で更新が必要な場合にのみbindさせるように変更しました。

## 分かったこと

tkinterはイベントをウィジットに動的にbindさせようとすると、同じイベントにbindしていた過去のメソッドを内部的に破棄しない場合があるようです。現象にも記述した通り、確実にリークが発生するわけではなく、関係のない変更がきっかけで発生するようです。  
・bindの前にunbindメソッドを入れても解決しません。  
・ガーベージコレクション解放しても解決しません。

本件とは別の処理で、ハンドルカウントが上昇するリークも確認しており、これはポップアップ表示させるダイアログ中に使用する画像ファイルに対して、ダイアログの表示/非表示毎にハンドルが確保され解放されないことが分かってきました。

tkinterを使用する際には、何らかのユーザーアクションが伴う繰り返しパターンで、パフォーマンスモニタ等でメモリ、ハンドルの推移を確認しておくことをお勧めします。  
ProcessExplorerというツールで、具体的なハンドル対象を観察することができます。


CC BY-SA 4.0
2023-01-26T06:54:29.860
93565
2023-01-26T06:54:29.860
32891
93535
answer