それでもやっぱりTemplateToolkitがすき

TemplateToolkitが大好き。TemplateToolkit無しでは生きられない。TemplateToolkitは僕の人生に楽しみと時間を与えてくれました。

そんなTemplateToolkitに日々感謝の念を忘れない僕ですが、今回ばっかりはちょっと我が目を疑う出来事に遭遇。

Template::Plugin::Filterに潜む困ったちゃん。

結果から言うとFilterの扱いで油断するとメモリリークします。くれぐれも油断めされるな。

まぁこういうFilterを作ってみた訳です

package Matsumo::TTPlugin::MatsumoFilter;

use base qw(Template::Plugin::Filter);

sub init {
    my $self = shift;
    $self->install_filter('matsumo');
    return $self;
}

sub filter {
    my ($self, $text, $args, $config) = @_;
    $text =~ s/hoge/fuga/g;
    return $text;
}

1;

何に使うんだとかはさておきまして。

このFilterを使ってみます

use strict;
use Template;

local $/ = undef;
my $template = <DATA>;
Template->new({PLUGIN_BASE => 'Matsumo::TTPlugin'})->process(\$template, {}, \my $out) or die;
print $out;

__END__
[% USE MatsumoFilter %]
filtering="[% 'hogehogefugafuga' | matsumo %]"

結果

filtering="fugafugafugafuga"

でましたフィルタリングされてますね。フガフガフガフガ。

さぁ稼動開始。おっとメモリ急騰!!なんだなんだなんだ!!なんだってーーーー!!
という事態に見舞われました。


という訳で原因究明
以下のように手を加えてループするようにしました。Apacheのプロセスに近いっちゃ近い感じ。
psの結果でメモリの消費量を見てみようという感じです。
(他に良いメモリの見方あったら誰か教えて下さい。)

id:usuihiro1978さんに教えてもらったDevel::Leak::ObjectでLeakをチェックと万全の体制。

use strict;
use Devel::Leak::Object qw(GLOBAL_bless);
use Template;

local $/ = undef;
print STDERR `ps aux | head -n1`;
my $template = <DATA>;
for (0..$ARGV[0]) {
    Template->new({PLUGIN_BASE => 'Matsumo::TTPlugin'})->process(\$template, {}, \my $out) or die;
    print $out;
    print STDERR `ps aux | grep "perl leak.pl"` if $_ == 0 || $_ == $ARGV[0];
}

__END__
[% USE MatsumoFilter %]
filtering="[% 'hogehogefugafuga' | matsumo %]"

とりあえずpsの結果を見てみよう

$ perl leak.pl 500 > /dev/null
USER        PID %CPU %MEM   VSZ  RSS  TT  STAT STARTED      TIME COMMAND
yumatsumo 43690  2.0  1.1  5704 5184  p0  S+   12:54AM   0:00.16 /usr/local/bin/perl leak.pl 500
yumatsumo 43690 50.7  2.1  9944 9428  p0  S+   12:54AM   0:01.88 /usr/local/bin/perl leak.pl 500

grep "perl leak.pl"自体のプロセスも引っかかっちゃってるけど割愛してます。

10回まわして結果チェック

$ perl leak.pl 10
Tracked objects by class:
Config                                   1
Matsumo::TTPlugin::MatsumoFilter         11
Template::Context                        11
Template::Filters                        11
Template::Grammar                        11
Template::Parser                         11
Template::Plugins                        11
Template::Provider                       11
Template::Stash::XS                      11

来てますねぇ。リークしてますねぇ。


さぁ犯人を捜せ
勿体ぶってもしょうがないので結論はTemplate::Plugin::Filterの中にいました。

先に出てきたフィルター内のメソッドinstall_filterは

sub install_filter {
    my ($self, $name) = @_;
    $self->{ _CONTEXT }->define_filter( $name => $self->factory() );
    return $self;
}

こういうメソッドでfactoryに処理が投げられてます。で、factoryメソッドの中に↓のような箇所。

return $self->{ _STATIC_FILTER } ||= sub {
    $self->filter(shift);
};

さぁ違和感感じませんか。どうですか?
そう$self->{_STATIC_FILTER}の中で$selfが参照されてると。

これはまずいという事で解決策を模索。
答えはid:miyagawaさんのTemplate::Plugin::CommaのChangesの中に有りました。(他にもあったけど)

0.03 Thu Nov 14 13:47:51 JST 2002
* Fixed memory leak due to Template::Plugin::Filter

というわけで2002年に既にソリューションされてたみたいですorz
という事でdiff見てみた。
http://search.cpan.org/diff?from=Template-Plugin-Comma-0.02&to=Template-Plugin-Comma-0.03#lib/Template/Plugin/Comma.pm
なるほど。

  1. 親クラスをTemplate::Plugin::FilterからTemplate::Pluginに
  2. initをnewに
  3. $self->install_filterを$context->define_filterに

とか言う感じで対応。

という訳で出来ました
変更点は上のリスト通り。

package Matsumo::TTPlugin::MatsumoFilter;

use base qw(Template::Plugin);

sub new {
    my ($self, $context) = @_;
    $context->define_filter('matsumo', \&matsumo);
    return $self;
}

sub matsumo {
    my $text = shift;
    $text =~ s/hoge/fuga/g;
    return $text;
}

1;

さぁチェック!
この瞬間が一番好きだったりします。チェック!チェック!チェック!

$ perl leak.pl 500 > /dev/null
USER        PID %CPU %MEM   VSZ  RSS  TT  STAT STARTED      TIME COMMAND
yumatsumo 49501  0.0  1.1  5684 5164  p0  S+    1:26AM   0:00.16 /usr/local/bin/perl leak.pl 500
yumatsumo 49501 85.6  1.1  5688 5172  p0  S+    1:26AM   0:01.90 /usr/local/bin/perl leak.pl 500

大丈夫そう!

$ perl leak.pl 10
Tracked objects by class:
Config                                   1

おっけ〜。バッチコ〜イ。

という訳で無事解決しました。
みなさまもお気をつけあれ。(って常識だったりするのかしら)

まとめ

それでもやっぱりTemplateToolkitがすき!!