Catalystのオーバーヘッド~Catalyst::Engine::FastCGIのprepare系

| |
Catalystのオーバーヘッド~Catalyst::Engine::FastCGIの続き。 handlerの準備段階であるprepare_*の詳細を見てみよう。 %Time ExclSec CumulS #Calls sec/call Csec/c  Name  86.2   0.950 13.359  10002   0.0001 0.0013  Catalyst::Engine::prepare  25.3   0.450  3.930  10002   0.0000 0.0004  Catalyst::Engine::CGI::Base::prepare_path  18.9   0.290  2.930  10002   0.0000 0.0003  Catalyst::Engine::FastCGI::prepare_request  14.8   0.040  2.300  10002   0.0000 0.0002  Catalyst::Engine::CGI::prepare_request  14.5   0.950  2.250  10002   0.0001 0.0002  Catalyst::Engine::CGI::Base::prepare_headers   13.359秒のうち,遅いのはprepare_path, prepare_request(FastCGIとCGIの2箇所), prepare_headersの3つ。この3つで9秒ほどかかっている,と。まずはC::E::CGI::Base::prepare_pathから。 sub prepare_path {     my $c = shift;     my $base;     {         my $scheme = $c->request->secure ? 'https' : 'http';         my $host   = $ENV{HTTP_HOST}   || $ENV{SERVER_NAME};         my $port   = $ENV{SERVER_PORT} || 80;         my $path   = $ENV{SCRIPT_NAME} || '/';         unless ( $path =~ /\/$/ ) {             $path .= '/';         }         $base = URI->new;         $base->scheme($scheme);         $base->host($host);         $base->port($port);         $base->path($path);         $base = $base->canonical->as_string;     }     my $location = $ENV{SCRIPT_NAME} || '/';     my $path = $ENV{PATH_INFO} || '/';     $path =~ s/^($location)?\///;     $path =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;     $path =~ s/^\///;     $c->req->base($base);     $c->req->path($path); }   prepare_pathはExclSecが0.45秒,CumulSが3.930秒かかっている。ExclSecは後半の正規表現連打がもったいない気がする。CumulSはURI::schemeなどが積もり積もって,だろう。new, scheme, host, port, pathと呼んでcanonicalしている。URI::*を抜き出してみると, %Time ExclSec CumulS #Calls sec/call Csec/c  Name  5.49   0.110  0.850  10002   0.0000 0.0001  URI::http::canonical  5.23   0.300  0.810  20004   0.0000 0.0000  URI::_server::host  5.17   0.620  0.800  70014   0.0000 0.0000  URI::_generic::authority  4.26   0.420  0.660  20004   0.0000 0.0000  URI::_scheme  4.07   0.230  0.630  10002   0.0000 0.0001  URI::_server::canonical  3.94   0.020  0.610  10002   0.0000 0.0001  URI::scheme  3.42   0.240  0.530  20004   0.0000 0.0000  URI::new  3.23   0.240  0.500  20004   0.0000 0.0000  URI::_server::_port  3.04   0.060  0.470  10002   0.0000 0.0000  URI::_server::port  1.81   0.220  0.280  20004   0.0000 0.0000  URI::_generic::path   となっている。それぞれの詳細にまでたどるのは面倒くさいのであまり考えない。要するに環境変数から$c->req->baseと$c->req->pathを作っているだけなんだが,そこにURI::*を使うのはコストが高いんじゃないだろうか。 しかも,FastCGIなら,そもそも環境変数にセットして渡されるんじゃなく,FastCGIプロトコルで渡ってきたFCGI_PARAMSをFCGI.xsのpopulate_envでハッシュにコピーした結果だもんなぁ(コピー先のデフォルトが%ENVになっている)。 ところでlighttpdだとSERVER_NAMEにもHTTP_HOSTにもポート番号入っているな。環境変数取得した結果を見ると。いや,wgetか? まあいいや。 # wget -q -O - http://localhost:8080/fcgi/ctest.fcgi/hoge//fuga?foo=bar DOCUMENT_ROOT:/var/www/localhost/htdocs FCGI_ROLE:RESPONDER GATEWAY_INTERFACE:CGI/1.1 HTTP_ACCEPT:*/* HTTP_CONNECTION:Keep-Alive HTTP_HOST:localhost:8080 HTTP_USER_AGENT:Wget/1.10.2 PATH_INFO:/hoge/fuga PATH_TRANSLATED:/var/www/localhost/htdocs/hoge/fuga QUERY_STRING:foo=bar REDIRECT_STATUS:200 REMOTE_ADDR:127.0.0.1 REMOTE_PORT:41597 REQUEST_METHOD:GET REQUEST_URI:/fcgi/ctest.fcgi/hoge//fuga?foo=bar SCRIPT_FILENAME:/var/www/localhost/htdocs/fcgi/ctest.fcgi SCRIPT_NAME:/fcgi/ctest.fcgi SERVER_ADDR:127.0.0.1 SERVER_NAME:localhost:8080 SERVER_PORT:8080 SERVER_PROTOCOL:HTTP/1.0 SERVER_SOFTWARE:lighttpd/1.4.7   さて,この3.9秒が省略できたとしたら,10000 / (43.469302 - 3.9) = 252.7req/sになるはず。試しにprepare_pathを sub prepare_path {     my $c = shift;     $c->req->base('http://localhost/fcgi/ctest.fcgi/');     $c->req->path( $ENV{PATH_INFO} || '/' ); }   としたC::E::FastCGI2.pmを作ってみよう。 ・・・と,その前にab2をDProf無しで試しておこう。 Server Software:        lighttpd/1.4.7 Server Hostname:        localhost Server Port:            8080 Document Path:          /fcgi/ctest.fcgi Document Length:        48 bytes Concurrency Level:      1 Time taken for tests:   21.617732 seconds Complete requests:      10000 Failed requests:        0 Write errors:           0 Keep-Alive requests:    9412 Total transferred:      1717060 bytes HTML transferred:       480000 bytes Requests per second:    462.58 [#/sec] (mean) Time per request:       2.162 [ms] (mean) Time per request:       2.162 [ms] (mean, across all concurrent requests) Transfer rate:          77.53 [Kbytes/sec] received   あ,結構速いか。まあ試してみよう。 Concurrency Level:      1 Time taken for tests:   17.791063 seconds Complete requests:      10000 Failed requests:        0 Write errors:           0 Keep-Alive requests:    9412 Total transferred:      1717060 bytes HTML transferred:       480000 bytes Requests per second:    562.08 [#/sec] (mean) Time per request:       1.779 [ms] (mean) Time per request:       1.779 [ms] (mean, across all concurrent requests) Transfer rate:          94.20 [Kbytes/sec] received   うん。100req/sのスピードアップ。 続いてprepare_headers。 sub prepare_headers {     my $c = shift;     while ( my ( $header, $value ) = each %ENV ) {         next unless $header =~ /^(HTTP|CONTENT)/i;         ( my $field = $header ) =~ s/^HTTPS?_//;         $c->req->headers->header( $field => $value );     }     $c->req->method( $ENV{REQUEST_METHOD} || 'GET' ); }   prepare_pathと同じく,FCGI_PARAMS→%ENV→$c->req->headersとコピーの無駄があるのと,正規表現のボトルネック。substrとeqで良いんじゃないだろうか。中で約1秒の消費。残り1秒は$c->req->headers->headerと辿るとこかな・・・とりあえずヘッダは大文字前提,CONTならCONTENTと仮定した決め打ちでやってみよう。 sub prepare_headers {     my $c = shift;     while ( my ( $header, $value ) = each %ENV ) {         my $h4 = substr($header,0,4);         if( $h4 eq 'HTTP' ){            if( substr($header,4,1) eq 'S' ){              $c->req->headers->header(substr($header,6) => $value);            }else{              $c->req->headers->header(substr($header,5) => $value);            }         }elsif( $h4 eq 'CONT' ){            $c->req->headers->header($header => $value);         }     }     $c->req->method( $ENV{REQUEST_METHOD} || 'GET' ); }   Concurrency Level:      1 Time taken for tests:   17.787568 seconds Complete requests:      10000 Failed requests:        0 Write errors:           0 Keep-Alive requests:    9412 Total transferred:      1717060 bytes HTML transferred:       480000 bytes Requests per second:    562.19 [#/sec] (mean) Time per request:       1.779 [ms] (mean) Time per request:       1.779 [ms] (mean, across all concurrent requests) Transfer rate:          94.22 [Kbytes/sec] received   うーん。変わらん。ずるしまくってみよう。 sub prepare_headers {     my $c = shift;     my $hdrs = $c->req->headers;     while ( my ( $header, $value ) = each %ENV ) {         my $h = substr($header,0,1);         if( $h eq 'H' ){            if( substr($header,4,1) eq 'S' ){              $hdrs->{substr($header,6)} = $value;            }else{              $hdrs->{substr($header,5)} = $value;            }         }elsif( $h eq 'C' ){            $hdrs->{$header} = $value;         }     }     $c->req->method( $ENV{REQUEST_METHOD} || 'GET' ); }   ここまでやって Concurrency Level:      1 Time taken for tests:   16.736236 seconds Complete requests:      10000 Failed requests:        0 Write errors:           0 Keep-Alive requests:    9412 Total transferred:      1717060 bytes HTML transferred:       480000 bytes Requests per second:    597.51 [#/sec] (mean) Time per request:       1.674 [ms] (mean) Time per request:       1.674 [ms] (mean, across all concurrent requests) Transfer rate:          100.14 [Kbytes/sec] received   こんなもん。まぁ,prepare_headers側でいじるより,FCGI.pmを使わないで直接準備する方が正解ではなかろうか。 続いてC::E::FastCGI::prepare_request。CGI::_reset_globals()してC::E::FastCGI::Base::prepare_requestしてC::E::CGI::prepare_requestしている。C::E::FastCGI::Baseでは$c->fastcgiに$requestを入れている。 sub prepare_request {     my ( $c, $request, @arguments ) = @_;     CGI::_reset_globals();     $c->SUPER::prepare_request($request);     $c->Catalyst::Engine::CGI::prepare_request(@arguments); }   C::E::CGI::prepare_request。 sub prepare_request {     my ( $c, $object ) = @_;     my $cgi;     if ( defined($object) && ref($object) ) {         if ( $object->isa('Apache') ) {                   # MP 1.3             $cgi = CGI->new($object);         }         elsif ( $object->isa('Apache::RequestRec') ) {    # MP 1.99             $cgi = CGI->new($object);         }         elsif ( $object->isa('Apache2::RequestRec') ) {   # MP 2.00             $cgi = CGI->new($object);         }         elsif ( $object->isa('CGI') ) {             $cgi = $object;         }         else {             my $class = ref($object);             Catalyst::Exception->throw(                 message => qq/Unknown object '$object'/             );         }     }     $c->cgi( $cgi || CGI->new ); }   むむ。どうなっているのだろう・・・C::E::FastCGI::prepare_requestで\@argumentsをdumperしてみよう。 ・・・空だった。つまり最後の$c->cgi(CGI->new)されているだけ。しかもそれが重い。 ってか,Engineの実装によるんだったらEngine側で入れておけよな,って感じ。えーい,どうせCatalyst::*では使っていないから$c->cgiを外しちまえ。そうすりゃ_reset_globalsもしなくていいじゃん。 Concurrency Level:      1 Time taken for tests:   9.104013 seconds Complete requests:      10000 Failed requests:        0 Write errors:           0 Keep-Alive requests:    9412 Total transferred:      1227060 bytes HTML transferred:       0 bytes Requests per second:    1098.42 [#/sec] (mean) Time per request:       0.910 [ms] (mean) Time per request:       0.910 [ms] (mean, across all concurrent requests) Transfer rate:          131.59 [Kbytes/sec] received   ほら。倍速くなった。・・・あれ? 速すぎだろ?何か別の(メモリ管理とか)要素が働いたのかな。 さて,cgiを外して問題あるかと見れば・・・ありゃ。 sub prepare_body {     my $c = shift;     # XXX this is undocumented in CGI.pm. If Content-Type is not     # application/x-www-form-urlencoded or multipart/form-data     # CGI.pm will read STDIN into a param, POSTDATA.     $c->request->body( $c->cgi->param('POSTDATA') ); } sub prepare_parameters {     my $c = shift;     my ( @params );     if ( $c->request->method eq 'POST' ) {         for my $param ( $c->cgi->url_param ) {             for my $value (  $c->cgi->url_param($param) ) {                 push ( @params, $param, $value );             }         }     }     for my $param ( $c->cgi->param ) {         for my $value (  $c->cgi->param($param) ) {             push ( @params, $param, $value );         }     }     $c->request->param(@params); }   C::E::CGIのこんなところに。C::E::FastCGIはこっちを呼んでるなぁ。しかし,GETリクエストの時には不要だろう。 --2005/11/08 04:34追記 間違い。その後prepare_parameters呼んでいてエラー出していることが発覚。 とりあえず手抜きで,prepare_parameters, prepare_bodyで何もしないようにする(つまりPOSTを受け取ったりできな)と Concurrency Level:      1 Time taken for tests:   13.821929 seconds Complete requests:      10000 Failed requests:        0 Write errors:           0 Keep-Alive requests:    9412 Total transferred:      1717060 bytes HTML transferred:       480000 bytes Requests per second:    723.49 [#/sec] (mean) Time per request:       1.382 [ms] (mean) Time per request:       1.382 [ms] (mean, across all concurrent requests) Transfer rate:          121.26 [Kbytes/sec] received   こんなもんだった。723.49 req/sと高速化はしているけど,倍は嘘でした。 これはprepare_parameters, prepare_bodyをさぼっているのでC::E::FastCGIで(CGI.pmを使わずに)用意してやる必要がある。

Trackback URL for this post:

http://old.typemiss.net/trackback/3
from Typemiss.net on 木, 2006-01-12 13:50
続いてCatalyst::Dispatcher::dispatchのオーバーヘッドについて。 dispatchではCatalystの面白さでもあり,複雑さにもつながるURL⇔クラス・メソッドのマッピングを処理しているので,それなりのオー