愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

新・循環参照によろしく

前回までのあらすじ

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;
}


めでたしめでたし。

結論

そこまで役に立たないモジュールでも、書くこと自体は自分の役に立つ