GDIリーク(または、単にGDIオブジェクトが多すぎる)は、最も一般的な問題の1つです。最終的には、レンダリングの問題、エラー、および/またはパフォーマンスの問題が発生します。この記事では、この問題をデバッグする方法について説明しています。
2016年、ほとんどのプログラムがサンドボックスで実行され、最も能力のない開発者でもシステムに害を及ぼすことができない場合、この記事で説明する問題に直面することに驚いています。率直に言って、私はこの問題がWin32Apiと一緒に永遠に続くことを望みました。それにもかかわらず、私はそれに直面しました。その前に、私はそれについてのホラーストーリーを古い経験豊富な開発者から聞いたばかりです。
問題
膨大な量のGDIオブジェクトのリークまたは使用。
症状
- タスクマネージャの[詳細]タブの[GDIオブジェクト]列に重要な10000が表示されます(この列がない場合は、テーブルヘッダーを右クリックし、[列の選択]を選択して追加できます)。
- C#またはCLRによって実行される他の言語で開発する場合、次の情報不足のエラーが発生します。
メッセージ:GDI+で一般的なエラーが発生しました。
出典:System.Drawing
TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
タイプ:System.Runtime.InteropServices.ExternalException
特定の設定または特定のシステムバージョンではエラーが発生しない場合がありますが、アプリケーションは単一のオブジェクトをレンダリングできません: - С/С++での開発中に、Create%SOME_GDI_OBJECT%などのすべてのGDIメソッドがNULLを返し始めました。
なぜですか?
Windowsシステムでは、 65535を超えるファイルを作成することはできません。 GDIオブジェクト。実際、この数は印象的であり、このような大量のオブジェクトを必要とする通常のシナリオを想像することはほとんどできません。プロセスには制限があります–変更できるプロセスごとに10000( HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows NT \ CurrentVersion \ Windows \ GDIProcessHandleQuota を変更することにより) 値は256〜65535の範囲です)が、Microsoftはこの制限を増やすことをお勧めしません。それでも実行すると、1つのプロセスでシステムをフリーズできるため、エラーメッセージも表示できなくなります。この場合、システムは再起動後にのみ復活できます。
修正方法
快適で管理されたCLRの世界に住んでいる場合は、アプリケーションで通常のメモリリークが発生している可能性が高くなります。問題は不快ですが、それはごく普通のケースです。これを検出するための優れたツールが少なくとも12個あります。プロファイラーを使用して、GDIリソース(Sytem.Drawing.Brush、Bitmap、Pen、Region、Graphics)をラップするオブジェクトの数が増えるかどうかを確認する必要があります。その場合は、この記事を読むのをやめることができます。ラッパーオブジェクトのリークが検出されなかった場合、コードはGDI APIを直接使用し、それらが削除されないシナリオがあります
他の人は何をお勧めしますか?
このテーマに関するMicrosoftの公式ガイダンスまたはその他の記事では、次のようなものを推奨しています。
すべての作成を検索 %SOME_GDI_OBJECT% 対応するDeleteObjectかどうかを検出します (またはHDCオブジェクトの場合はReleaseDC)が存在します。そのような場合DeleteObject 存在する場合、それを呼び出さないシナリオが存在する可能性があります。
このメソッドには、追加の手順を含むわずかに改善されたバージョンがあります:
GDIViewユーティリティをダウンロードします。タイプごとにGDIオブジェクトの正確な数を表示できます。オブジェクトの総数は、最後の列の値に対応していないことに注意してください。ただし、検索分野を絞り込むのに役立つ場合は、これに目をつぶることができます。
私が取り組んでいるプロジェクトのコードベースは900万レコードで、ほぼ同じ量のレコードがサードパーティライブラリにあり、数十のファイルにまたがるGDI関数の数百の呼び出しがあります。障害のない手動分析が不可能であると理解する前に、私は多くの時間とエネルギーを浪費していました。
何を提供できますか?
この方法が長すぎて面倒だと思われる場合は、前の方法で絶望のすべての段階を通過していません。前の手順に従ってみてもかまいませんが、それでも解決しない場合は、この解決策を忘れないでください。
リークを追跡するために、私は自分自身に疑問を投げかけました。リークしているオブジェクトはどこで作成されますか? API関数が呼び出されるすべての場所にブレークポイントを設定することは不可能でした。その上、それが.NETFrameworkまたは私たちが使用しているサードパーティライブラリの1つで発生しないかどうかはわかりませんでした。数分のグーグルで、すべてのシステム機能への呼び出しをログに記録して追跡できるAPIMonitorユーティリティにたどり着きました。 GDIオブジェクトを生成するすべての関数のリストを簡単に見つけ、APIMonitorでそれらを見つけて選択しました。次に、ブレークポイントを設定します。
その後、デバッグプロセスを実行しました Visual Studioを使用して、プロセスツリーで選択します。 5番目のブレークポイントはすぐに解決しました:
私はこの急流に溺れること、そして何か他のものが必要であることに気づきました。関数からブレークポイントを削除し、ログを表示することにしました。それは何千もの呼び出しを示しました。手動で分析できないことが明らかになりました。
タスクは、削除の原因とならないGDI関数の呼び出しを見つけることです。 。ログには、必要なものがすべて含まれていました。時系列の関数呼び出しのリスト、それらの戻り値、およびパラメーターです。したがって、Create%SOME_GDI_OBJECT%関数の戻り値を取得し、この値を引数としてDeleteObjectの呼び出しを見つける必要がありました。 API Monitorですべてのレコードを選択し、それらをテキストファイルに挿入して、TAB区切り文字でCSVのようなものを取得しました。解析用の小さなプログラムを作成するつもりだったVSを実行しましたが、ロードする前に、データをデータベースにエクスポートし、必要なものを見つけるためのクエリを作成するという、より良いアイデアが思い浮かびました。すぐに質問して回答を得ることができたので、これは正しい選択でした。
CSVからデータベースにデータをインポートするためのツールはたくさんあるので、このテーマについては詳しく説明しません(mysql、mssql、sqlite)。
次の表があります:
CREATE TABLE apicalls ( id int(11) DEFAULT NULL, `Time of Day` datetime DEFAULT NULL, Thread int(11) DEFAULT NULL, Module varchar(50) DEFAULT NULL, API varchar(200) DEFAULT NULL, `Return Value` varchar(50) DEFAULT NULL, Error varchar(100) DEFAULT NULL, Duration varchar(50) DEFAULT NULL )
API呼び出しから削除されたオブジェクトの記述子を取得するために、次のMySQL関数を作成しました。
CREATE FUNCTION getHandle(api varchar(1000)) RETURNS varchar(100) CHARSET utf8 BEGIN DECLARE start int(11); DECLARE result varchar(100); SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )' IF start = 0 THEN SET start := INSTR(api, '('); END IF; SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1); RETURN TRIM(result); END
そして最後に、現在のすべてのオブジェクトを見つけるためのクエリを作成しました:
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates LEFT JOIN (SELECT d.id, d.API, getHandle(d.API) handle FROM apicalls d WHERE API LIKE 'DeleteObject%' OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels ON dels.handle = creates.handle WHERE creates.API LIKE 'Create%';
(基本的には、すべてのCreate呼び出しのすべてのDelete呼び出しを検索するだけです。)
上の画像からわかるように、削除が1つもないすべての呼び出しが一度に見つかりました。
それで、最後の質問が残されました:これらのメソッドが私のコードのコンテキストでどこから呼び出されているかを判断する方法は?そして、ここで1つの凝ったトリックが私を助けました:
- デバッグのためにアプリケーションをVSで実行します
- Api Monitorで見つけて、選択します。
- APIで必要な関数を選択し、ブレークポイントを設定します。
- 問題のパラメータで呼び出されるまで[次へ]をクリックし続けます(VSからの条件付きブレークポイントを本当に見逃しました)
- 必要な電話がかかってきたら、CSに切り替えて、すべてを解除をクリックします。 。
- VSデバッガーは、リークしているオブジェクトが作成された場所で停止します。必要なのは、オブジェクトが削除されない理由を見つけることだけです。
注:コードは説明のために書かれています。
概要:
説明されているアルゴリズムは複雑で多くのツールを必要としますが、巨大なコードベースを介したダム検索と比較してはるかに高速な結果が得られました。
すべての手順の概要は次のとおりです。
- GDIラッパーオブジェクトのメモリリークを検索します。
- 存在する場合は、それらを削除して手順1を繰り返します。
- リークがない場合は、API関数の呼び出しを明示的に検索します。
- 数量が多くない場合は、オブジェクトが削除されていないスクリプトを検索してください。
- 数量が多い場合や追跡が難しい場合は、API Monitorをダウンロードして、GDI関数の呼び出しをログに記録できるように設定してください。
- VSでデバッグするためにアプリケーションを実行します。
- リークを再現します(キャッシュされたオブジェクトを非表示にするためにプログラムが初期化されます)。
- APIMonitorに接続します。
- リークを再現します。
- ログをテキストファイルにコピーし、手元にある任意のデータベースにインポートします(この記事で取り上げるスクリプトは、MySQL用ですが、リレーショナルデータベース管理システムに簡単に採用できます)。
- CreateメソッドとDeleteメソッドを比較し(上記のこの記事でSQLスクリプトを見つけることができます)、Delete呼び出しのないメソッドを見つけます。
- 必要なメソッドの呼び出し時にAPIMonitorにブレークポイントを設定します。
- 再取得したパラメーターを使用してメソッドが呼び出されるまで、[続行]をクリックし続けます。
- 必要なパラメータを使用してメソッドが呼び出されたら、[VSですべてブレーク]をクリックします。
- このオブジェクトが削除されない理由を確認してください。
この記事がお役に立てば幸いです。