Test::LeakTrace::JA(3) User Contributed Perl Documentation Test::LeakTrace::JA(3)

Test::LeakTrace::JA - メモリリークを追跡する

This document describes Test::LeakTrace version 0.17.

use Test::LeakTrace;

# simple report
leaktrace{
        # ...
};

# verbose output
leaktrace{
        # ...
} -verbose;

# with callback
leaktrace{
        # ...
} sub {
        my($ref, $file, $line) = @_;
        warn "leaked $ref from $file line\n";
};

my @refs = leaked_refs{
        # ...
};
my @info = leaked_info{
        # ...
};

my $count = leaked_count{
        # ...
};

# standard test interface
use Test::LeakTrace;

no_leaks_ok{
        # ...
} "description";

leaks_cmp_ok{
        # ...
} '<', 10;

PerlのGCはリファレンスカウンタを用いたものなので,オブジェクトが開放されるタイミングが明確であることや体感速度が高速であることなど数々の利点があります。 その一方で,循環参照を開放できないこと,Cレベルでの操作でミスしやすいなど,問題点がいくつかあります。それらの問題点のほとんどはメモリリークに関することですから,メモリリークを追跡することは非常に重要な課題です。

"Test::LeakTrce"はメモリリークを追跡するためのいくつかのユーティリティと"Test::Builder"ベースのテスト関数を提供します。このモジュールはPerlのメモリアロケーションシステムであるアリーナを走査するため,SVに関することであれば与えられたコードのどんなメモリリークでも検出できます。つまり,Perlレベルでの循環参照を始めとして,XSモジュールやPerl自身のバグによるメモリリークを追跡することができます。

ここでリークとは,特定のスコープ内で新たに作成されて,そのスコープ終了後にも残っている値を意味します。これは,新たに作成されたグローバルな値やPerlが暗黙のうちに作成するキャッシュの値も含みます。たとえば,リーク追跡を行っている最中に新たに名前つきサブルーチンを定義すれば,それはリークとみなされます。また,継承したメソッドを呼び出したり,オブジェクトを作成したりするだけで様々なキャッシュが生成され,リークが報告される可能性があります。

"leaked_info { BLOCK }"

BLOCKを実行し,追跡結果をリストで返します。 結果はリークした値のリファレンス,ファイル名,行番号の三要素を持つ配列,つまり"[$ref, $file, $line]"のリストとなっています。

なお,この関数はPerl内部で使用する値を返す可能性があります。そのような内部用の値を変更するとPerl実行環境に致命的な影響を与える可能性があるので注意してください。また,配列やハッシュの要素として,リファレンスではない配列やハッシュそれ自体が含まれる可能性があります。そのような値は通常Perlレベルで操作することができません。たとえば"Data::Dumper"などで出力することはできません。

"leaked_refs { BLOCK }"

BLOCKを実行し,リークしたSVのリファレンスのリストを返します。

"map{ $_->[0] } leaked_info{ BLOCK }"と同じですが,より高速です。

"leaked_count { BLOCK }"

BLOCKを実行し,リークしたSVのリファレンスの個数を返します。

"leaked_info()""leaked_refs()"もスカラコンテキストでは個数を返しますが, "leaked_count()"はコンテキストに依存しません。

"leaktrace { BLOCK } ?($mode | \&callback)"

BLOCKを実行し,その中で起きたメモリリークを*STDERRに報告します。

メモリリークの報告は$modeで指定したモードに従います。 受け付ける$modeは以下の通りです:

-simple
デフォルトのモードです。リークしたSVの型とアドレス,ファイル名,行番号を報告します。
-sv_dump
-simpleに加えて,"sv_dump()"でSVの中身をダンプします。 これは,"Devel::Peek::Dump()"の出力とほぼ同じです。
-lines
-simpleに加えて,リークしていると見られる行の周辺を出力します。
-verbose
-simple-sv_dump-linesの全てを出力します。

より細かな制御のためにコールバックを指定することもできます。 \&callbackはリークしたSV毎に呼び出され,その引数はリークしたSVのリファレンス,ファイル名,行番号の3つです。

"no_leaks_ok { BLOCK } ?$description"

BLOCKにメモリリークがないことテストします。 これは"Test::Builder"ベースのテスト関数です。

なお,BLOCKは複数回実行されます。これは,初回の実行でキャッシュを用意する可能性を考慮するためです。

"leaks_cmp_ok { BLOCK } $cmp_op, $count, ?$description"

BLOCKのメモリリーク数と特定の数値を比較するテストを行います。 これは"Test::Builder"ベースのテスト関数です。

なお,BLOCKは複数回実行されます。これは,初回の実行でキャッシュを用意する可能性を考慮するためです。

"Devel::LeakTrace"と同様に,スクリプトのリーク追跡のために"Test::LeakTrace::Script"が提供されます。"use Test::LeakTrace::Script"宣言の引数は"leaktrace()"と同じです。

$ TEST_LEAKTRACE=-sv_dump perl -MTest::LeakTrace::Script script.pl
$ perl -MTest::LeakTrace::Script=-verbose script.pl

#!perl
# ...

use Test::LeakTrace::Script sub{
        my($ref, $file, $line) = @_;
        # ...
};

# ...

以下はモジュールのメモリリークをチェックするテストスクリプトのテンプレートです。

#!perl -w
use strict;
use constant HAS_LEAKTRACE => eval{ require Test::LeakTrace };
use Test::More HAS_LEAKTRACE ? (tests => 1) : (skip_all => 'require Test::LeakTrace');
use Test::LeakTrace;

use Some::Module;

leaks_cmp_ok{
        my $o = Some::Module->new();
        $o->something();
        $o->something_else();
} '<', 1;

"Test::LeakTrace"はアリーナを走査します。アリーナとは,Perlが作成するSVのためのメモリアロケーションシステムであり,sv.cで実装されています。 アリーナの走査にはsv.cにある"S_visit()"のコードを元にしたマクロを用いています。

さて,アリーナを走査すれば,メモリリークの検出そのものは簡単にできるように思えます。まず,コードブロックを実行する前に一度アリーナを走査し,全てのSVに「使用済み」の印を付けておきます。次に,コードブロック実行後にもう一度アリーナを走査し,使用済みの印がついていないSVがあれば,それはコードブロック内で作成され,開放されなかったSVだと考えます。あとはそれを報告するだけです。実際には,SVに対して使用済みの印を付けるスペースがないため,インサイドアウト法を応用して外部のコンテナに使用済みの印を保存します。 これを仮にPerlコードで書くと以下のようになります。

my %used_sv;
foreach my $sv(@ARENA){
        $used_sv{$sv}++;
}
$block->();

my @leaked
foreach my $sv(@ARENA){
        if(not exists $used_sv{$sv}){
                push @leaked, $sv;
        }
}
say 'leaked count: ', scalar @leaked;

リークしたSVを得るだけならこの方法で十分です。実際,"leaked_refs()""leaked_count()"はこのような方法でリークしたSVやその個数を調べています。

しかし,リークしたSVのステートメントの情報,つまりファイル名や行番号を得るためにはこれだけでは不十分です。Perl 5.10以降にはSVが作成されたときのステートメント情報を追跡する機能があるのですが,この機能を利用するためには,コンパイラオプションとしてに"-DDEBUG_LEAKING_SCALARS"を与えてPerlをビルドしなければなりません。

そこで,"Test::LeakTrace"では拡張可能な"PL_runops"を利用して,Perl VMがOPコードを実行する1ステートメント毎にアリーナを走査し,ステートメント情報を記録します。これは,1ステートメント毎にマーク&スイープのような処理を行うのに等しく,非常に時間が掛かります。しかし,Perlを特殊な条件の下でビルドする必要もなく,バージョンに依存した機能もほとんど使用しないため,多くの環境で動かすことができます。

また,"no_leaks_ok()"のようなテスト関数はまず"leaked_count()"でリークしたSVの個数を得てから,必要に応じてリークした位置を特定するために"leaktrace()"を実行するため,テストが成功する限りは時間の掛かる追跡処理はしません。

Perl 5.8.1 or later, and a C compiler.

"Test::LeakTrace""Devel::Cover"と一緒に動かすことはできません。 したがって,"Devel::Cover"の元で動いていることが検出されると,テスト関数は何も行わずにテストをパスさせます。

No bugs have been reported.

Please report any bugs or feature requests to the author.

Devel::LeakTrace.

Devel::LeakTrace::Fast.

Test::TraceObject.

Test::Weak.

For guts:

perlguts.

perlhack.

sv.c.

Goro Fuji <gfuji(at)cpan.org>.

Copyright (c) 2009, Goro Fuji. Some rights reserved.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

2021-05-24 perl v5.34.0