実際、厳密に言えば、テストは最初の実行時にメモリ リークを起こしています。
FastMM、DUnit、または Delphiのバグではありません。バグはテストにあります。
誤解を解いて、内部の仕組みを説明することから始めましょう。
誤解: FastMM は、アプリにリークがないことを証明します
ここでの問題は、FastMM がリークを検出しない場合、誤った安心感を与えてしまう可能性があることです。その理由は、どのような種類のリーク検出でも、チェックポイントからのリークを探す必要があるためです。開始チェックポイントの後に行われたすべての割り当てが終了チェックポイントによって回復される場合、すべてがクールです。
したがって、グローバル オブジェクト Bin を作成し、すべてのオブジェクトを破棄せずに Bin に送信すると、メモリ リークが発生します。同様に実行し続けると、アプリケーションはメモリ不足になります。ただし、Bin が FastMM End チェックポイントの前にすべてのオブジェクトを破棄した場合、FastMM は不都合なことに気付きません。
テストで起こっていることは、FastMM のチェックポイントの範囲が DUnit リーク検出よりも広いことです。テストでメモリ リークが発生しましたが、そのメモリは後で FastMM がチェックを行うまでに回復されます。
各 DUnit テストは、複数の実行のために独自のインスタンスを取得します
DUnit は、テスト ケースごとにテスト クラスの個別のインスタンスを作成します。ただし、これらのインスタンスはテストの実行ごとに再利用されます。イベントの簡略化されたシーケンスは次のとおりです。
- チェックポイントを開始
- 通話設定
- テストメソッドを呼び出す
- ティアダウンを呼び出す
- 終了チェックポイント
したがって、これら 3 つのメソッド間でリークが発生した場合 (リークがインスタンスのみに発生し、オブジェクトが破棄されるとすぐに回復される場合でも)、リークが報告されます。あなたの場合、オブジェクトが破棄されるとリークが回復します。そのため、代わりに DUnit が実行ごとにテスト クラスを作成および破棄した場合、リークは報告されません。
注 これは仕様によるものであり、実際にバグと呼ぶことはできません。
基本的に、DUnit は、テストが 100% 自己完結型でなければならないという原則について非常に厳格です。SetUp から TearDown まで、割り当てたメモリは (直接的または間接的に) 回復する必要があります。
定数文字列は、変数に割り当てられるたびにコピーされます
StringVar := 'SomeLiteralString'
コードまたは定数の値がコピーされるたびに (StringVar := SomeConstString
はい、コピーされます- 参照はカウントされません)StringVar := SomeResourceString
繰り返しますが、これは仕様によるものです。その意図は、文字列がライブラリから取得された場合、ライブラリがアンロードされた場合にその文字列が破棄されないようにすることです。したがって、これは実際にはバグではなく、単なる「不便な」設計です。
したがって、最初の実行時にテスト コードがメモリ リークする理由A := 'test'
は、「test」のコピーにメモリを割り当てているためです。後続の実行では、「test」の別のコピーが作成され、前のコピーは破棄されますが、正味のメモリ割り当ては同じです。
解決
この特定の場合の解決策は簡単です。
procedure TTest.TearDown;
begin
A := ''; //Remove the last reference to the copy of "test" and presto leak is gone :)
end;
一般に、それ以上のことをする必要はありません。テストが定数文字列のコピーを参照する子オブジェクトを作成する場合、それらのコピーは、子オブジェクトが破棄されるときに破棄されます。
ただし、テストのいずれかが文字列への参照をグローバルオブジェクト/シングルトンに渡す場合(いたずら、いたずら、それを行うべきではないことを知っています)、参照がリークされ、メモリがリークしたことになります-たとえそれが後に回復。
いくつかのさらなる観察
DUnit がテストを実行する方法についての議論に戻ります。同じテストを別々に実行すると、互いに干渉する可能性があります。例えば
procedure TTestLeaks.SetUp;
begin
FSwitch := not FSwitch;
if FSwitch then Fail('This test fails every second run.');
end;
アイデアを拡張すると、最初と毎秒(偶数)の実行でテストを「リーク」させることができます。
procedure TTestLeaks.SetUp;
begin
FSwitch := not FSwitch;
case FSwitch of
True : FString := 'Short';
False : FString := 'This is a long string';
end;
end;
procedure TTestLeaks.TearDown;
begin
// nothing here :( <-- note the **correct** form for the smiley
end;
これは実際にはメモリの全体的な消費量を増加させるという結果にはなりません。これは、代替実行ごとに、2 回目の実行ごとにリークされたのと同じ量のメモリが回復されるためです。
文字列をコピーすると、興味深い (そしておそらく予期しない) 動作が発生します。
var
S1, S2: string;
begin
S1 := 'Some very very long string literal';
S2 := S1; { A pointer copy and increased ref count }
if (S1 = S2) then { Very quick comparison because both vars point to the same address, therefore they're obviously equal. }
end;
でも....
const
CLongStr = 'Some very very long string literal';
var
S1, S2: string;
begin
S1 := CLongStr;
S2 := CLongStr; { A second **copy** of the same constant is allocated }
if (S1 = S2) then { A full comparison has to be done because there is no shortcut to guarantee they're the same. }
end;
これは、アプローチのまったくのばかげさのために、興味深い、極端でおそらく不適切な回避策を示唆しています。
const
CLongStr = 'Some very very long string literal';
var
GlobalLongStr: string;
initialization
GlobalLongStr := CLongStr; { Creates a copy that is safely on the heap so it will be allowed to be reference counted }
//Elsewhere in a test
procedure TTest.SetUp;
begin
FString1 := GlobalLongStr; { A pointer copy and increased ref count }
FString2 := GlobalLongStr; { A pointer copy and increased ref count }
if (FString1 = FString2) then { Very efficient compare }
end;
procedure TTest.TearDown;
begin
{... and no memory leak even though we aren't clearing the strings. }
end;
最後に / まとめ
はい、どうやらこの長い投稿は終了する予定です。
ご質問ありがとうございます。
それは、私がしばらく前に経験したことを覚えている関連する問題についての手がかりを与えてくれました。私の理論を確認する機会があった後、Q & A を投稿します。他の人も役に立つと思うかもしれません。