前回までのあらすじ
Sinon.jsのspyをぱくったSub::Spyとかいうモジュールを作る
↓
blessせずに特定のcoderefにひもづいたデータを保存したい
↓
@__gfx__「それHash::FieldHashで出来るよ!」(参照: http://d.hatena.ne.jp/__gfx__/20111015/1318640600)
↓
この時点ではFieldHashの意味がよくわからなかったので、適当にInsideOutっぽいものを実装
↓
ブコメで@__gfx__「それメモリリークしてるよ!」
↓
会社で@xaicron「Sub::Spyはどううれしいのかよくわからない」俺「俺もよくわからない」
↓
会社のIRCで色々聞いたりして直すも、なんかleakしてないことを保証するためのテストが通らない
↓
循環参照してましたァー(ドーン
循環参照にサヨナラ
途中経過はいいとして、Sub::Spyのspyという関数は渡されたcoderefをさらにラップしたcoderefを返す(ラップするのは引数、返り値などの情報を保存しておくため)。こういう構造になっていた。
fieldhash our %f_store; sub spy { my $subref = shift; my $spy = sub { my @args = @_; my ($result, @array_result, $e); if ( wantarray ) { @array_result = eval { $subref->(@args); }; } else { $result = eval { $subref->(@args); }; } if ( $@ ) { $e = $@; } push @{$f_store{$spy}->{calls}}, Sub::Spy::Call->new({ args => \@args, exception => $e, return_value => wantarray ? \@array_result : $result, }); return wantarray ? @array_result : $result; }; return $spy; }
fieldhash化されたhashは、hashのkeyにあたる何か(ここでは$spy)の寿命が切れた(いわゆる参照カウントが0になった)時点で対応するvalueも解放してくれるという話だったので、こういうテストを書いた。
subtest("no leak thanks to Hash::FieldHash", sub { { my $subref = sub { return shift; }; my $spy = spy($subref); $spy->(); is ( scalar (keys %Sub::Spy::f_store), 1, "information stored in fieldhash" ); # ここで$spyの寿命が切れる } is ( scalar (keys %Sub::Spy::f_store), 0, "information removed from fieldhash" ); });
が、上記のテストは通らなかった。なんでかというと、一番初めのコードのここで$spyが循環参照しているから。(これ自分では気づけなかった...)
push @{$f_store{$spy}->{calls}}, Sub::Spy::Call->new({ args => \@args, exception => $e, return_value => wantarray ? \@array_result : $result, });
よくよく眺めてみれば$spyというcoderefの中にまた$spyの参照が入り込んでいるので、循環参照しているのは当たり前といえば当たり前なんだけど、自分で書いてると中々気づかないんだな、これが。
で、@__gfx__にヒントもらってこのように直した。こうすると、$spyの中に$spyの参照がないので、fieldhashによって$spyに対応するhashのvalueが解放され、上記テストが通るようになる。
fieldhash our %f_store; sub spy { my $subref = shift; my $store = +{}; my $spy = sub { my @args = @_; my ($result, @array_result, $e); if ( wantarray ) { @array_result = eval { $subref->(@args); }; } else { $result = eval { $subref->(@args); }; } if ( $@ ) { $e = $@; } push @{$store->{calls}}, Sub::Spy::Call->new({ args => \@args, exception => $e, return_value => wantarray ? \@array_result : $result, }); return wantarray ? @array_result : $result; }; $f_store{$spy} = $store; return $spy; }
めでたしめでたし。
結論
そこまで役に立たないモジュールでも、書くこと自体は自分の役に立つ