3 キャリア対応の HTML::MobileJpCSS 0.02 をチューニング

3 キャリアの CSS の実装の差異を吸収してインライン展開してくれるという、HTML::MobileJpCSS ですが、CSS ファイルが大きくなってくるにつれ、処理時間が気になってきたので、内部処理を見直してチューニングしてみました。

チューニング作業は Devel::NYTProf を使いながら行ったのですが、Devel::NYTProf の使い方はまた後日紹介します。

主に行った修正は、ループ処理の見直しと、正規表現の見直しです。複数回呼び出されてるパターンマッチに o を付けたり、大きめの引数は参照渡しにしたりなど、微々たる修正がほとんどですが、チリも積もれば山となるで、結果12倍近くパフォーマンスアップしました。

以下の修正箇所の diff で、問題が無ければ、CodeRepos の方にコミットしたいと思いますので、id:komoriya さんご意見いただければと思います。

ベンチマーク結果

Benchmark: timing 10000 iterations of 0.02, after...
   0.02: 354 wallclock secs (327.05 usr + 27.54 sys = 354.59 CPU) @  28.20/s (n=10000)
  after:  27 wallclock secs (  0.06 usr + 26.61 sys =  26.67 CPU) @ 374.95/s (n=10000)

           Rate     0.02    after
 0.02    28.2/s       --     -92%
after     375/s    1230%       --

TODO

現状では、置換を行う apply() で CSS::Tiny よる CSS のパースも行っていますので、WAF から使う場合、リクエストの度にパースすることになります。ですので、CSS のパース自体は apply() とは別の場所で行い、結果をインスタンスに持つなどして、CSS ファイルに更新があった時だけ再びパースできるように、リロード用のメソッドを追加すると良いかなと思ってます。

CSS::Tiny->read() が結構な負担になっているので、上記の変更で更に倍くらいの高速化が望めると思います。

修正箇所

--- lib/HTML/MobileJpCSS.pm.orig	2009-03-01 02:18:11.000000000 +0900
+++ lib/HTML/MobileJpCSS.pm	2009-03-01 04:05:33.000000000 +0900
@@ -57,17 +59,18 @@
     return $content if $self->{agent}->is_non_mobile;
     return $content if $self->{agent}->is_ezweb && !(map { $self->{$_} } qw/inliner_ezweb css_file css/);
 
-    $content =~ s/(?:\r\n|\n)/\n/g;
+    $content =~ s/\x0D\x0A/\n/g;
+    $content =~ tr/\x0D/\n/;
 
     my @css;
-    my @link = $content =~ /<link\s.*?rel="stylesheet".*?>/isg;
+    my @link = $content =~ /<link\s.*?rel="stylesheet".*?>/isgo;
     for (@link) {
-        if (/href="(.+?)"/) {
+        if (/href="(.+?)"/o) {
             my $css = $self->_read_href($1);
             push @css, $css; 
         }
     }
-    $content =~ s/<link\s.*?rel="stylesheet".*?>\s*//isg if @link;
+    $content =~ s/<link\s.*?rel="stylesheet".*?>\s*//isgo if @link;
 
     push @css, $self->_read($self->{css_file}) if $self->{css_file};
     push @css, bless $self->{css}, 'CSS::Tiny' if $self->{css};
@@ -75,17 +78,17 @@
     my $style = {};
     for my $css (@css) {
         for (keys %$css) {
-            if (/^a:(?:link|focus|visited)$/) {
+            if (/^a:(?:link|focus|visited)$/o) {
                 $style->{pseudo}->{$_} = $style->{pseudo}->{$_}
                     ? { %{$style->{pseudo}->{$_}}, %{$css->{$_}} }
                     : $css->{$_};
             }
-            elsif (/^(\#(.+))/) {
+            elsif (/^(\#(.+))/o) {
                 $style->{id}->{$2} = $style->{id}->{$2}
                     ? { %{$style->{id}->{$2}}, %{$css->{$1}} }
                     : $css->{$1};
             }
-            elsif (/^(\*?\.(.+))/) {
+            elsif (/^(\*?\.(.+))/o) {
                 $style->{class}->{$2} = $style->{class}->{$2}
                     ? { %{$style->{class}->{$2}}, %{$css->{$1}} }
                     : $css->{$1};
@@ -105,38 +108,35 @@
         $content =~ s{<head>(.*)</head>}{<head>$1<style type="text/css">\n$pseudo</style></head>}is;
     }
 
-    # tag
-    for my $tag (keys %{$style->{tag}}) {
-        my $props = $style->{tag}->{$tag};
-        my @node = $content =~ /<$tag[^<>]*?>/isg;
-        for my $node (@node) {
-            $content = $self->_replace_style($content, $node, $props);
-        }
-    }
+    $self->{carrier} = $self->{agent}->carrier;
+    $self->{carrier} =~ tr/VH/SW/;
 
-    # id
-    for my $id (keys %{$style->{id}}) {
-        my $props = $style->{id}->{$id};
-        my @node = $content =~ /<[^<>]+?id="$id"[^<>]*?>/isg;
-        for my $node (@node) {
-            $content = $self->_replace_style($content, $node, $props);            
+    my %nodes = $content =~ /(<(\w+)[^>]*>)/sgo;
+    while (my ($node, $tag) = each %nodes) {
+        my %props;
+        if (my $props = $style->{tag}{$tag}) {
+            %props = %$props;
         }
-    }
-
-    # class
-    for my $class (keys %{$style->{class}}) {
-        my $props = $style->{class}->{$class};
-        my @node = $content =~ /<[^<>]+?\sclass="$class"[^<>]*?>/isg;
-        for my $node (@node) {
-            $content = $self->_replace_style($content, $node, $props);
+        if (my ($id) = $node =~ /id\s*=\s*["']([^"']+)/io) {
+            if (my $props = $style->{id}{$id}) {
+                %props = (%props, %$props);
+            }
+        }
+        if (my ($class) = $node =~ /class\s*=\s*["']([^"']+)/io) {
+            if (my $props = $style->{class}{$class}) {
+                %props = (%props, %$props);
+            }
+        }
+        if (%props) {
+            $self->_replace_style(\$content, $node, $tag, \%props);
         }
-        $content =~ s/<([^<>]+?)\sclass="$class"([^<>]*?)>/<$1$2>/isg;
     }
+    $content =~ s/\s*class\s*=\s*["'][^"']*["']//ig;
 
     # istyle for DoCoMo
     if ($self->{agent}->is_docomo) {
-        $content =~ s/(<input[^>]*?)(istyle="(\d)")([^>]*?>)/$1style="$IstyleDoCoMo{$3}"$4/isg;
-        $content =~ s/(<textarea[^>]*?)(istyle="(\d)")([^>]*?>)/$1style="$IstyleDoCoMo{$3}"$4/isg;
+        $content =~ s/(<input[^>]*?)(istyle="(\d)")([^>]*?>)/$1style="$IstyleDoCoMo{$3}"$4/isgo;
+        $content =~ s/(<textarea[^>]*?)(istyle="(\d)")([^>]*?>)/$1style="$IstyleDoCoMo{$3}"$4/isgo;
     }
     return $content;
 }
@@ -154,8 +154,8 @@
 my %CSS;
 sub _read_href {
     my ($self, $href) = @_;
-    if ($href !~ m{^https?://}) {
-        $href =~ s/\?.+$//;
+    if ($href !~ m{^https?://}o) {
+        $href =~ s/\?.+$//o;
         $href = File::Spec->catdir($self->{base_dir}, $href);
         return $self->_read($href);
     }
@@ -196,22 +196,20 @@
 }
 
 sub _replace_style {
-    my ($self, $content, $node, $props) = @_;
-    my ($tag) = $node =~ /^<([^\s]+).*?>/is;
+    my ($self, $content, $node, $tag, $props) = @_;
     my $replace = $node;
     my $style;
     for (keys %$props) {
         $style .= $self->_filter($tag, $_, $props->{$_});
     }
-    if ($node =~ /style=".+?"/) {
+    if ($node =~ /style=".+?"/o) {
         $replace =~ s/(style=".+?)"/$1$style"/is;
     }
     else {
         $replace =~ s/<$tag/<$tag style="$style"/is;
     }
-    $replace =~ s/[\n\s]+/ /g;
-    $content =~ s/$node/$replace/is;
-    return $content;
+    $replace =~ s/[\n\s]+/ /go;
+    $$content =~ s/\Q$node\E/$replace/igs;
 }
 
 sub _filter {
@@ -219,15 +217,12 @@
     return "$property:$value;" unless $StyleMap->{$tagname};
     return "$property:$value;" unless $StyleMap->{$tagname}->{$property};
     my $style = $StyleMap->{$tagname}->{$property};
-    my $carrier = $self->{agent}->carrier;
-    $carrier =~ s/^V$/S/;
-    $carrier =~ s/^H$/W/;    
     if (ref $style eq 'ARRAY') {
-        my $prop = $style->[0]->{$value}->{$carrier};
+        my $prop = $style->[0]->{$value}->{$self->{carrier}};
         return "$prop;" if $prop;
     }
     elsif ($style) {
-        my $prop = $style->{$carrier};
+        my $prop = $style->{$self->{carrier}};
         return "$prop:$value;" if $prop;
     }
     return "$property:$value;";
追記 [01:25]

先ほど公開したパッチにはバグがありましたので、修正版が完成しだい UP しなおします。

追記 [04:12]

修正版を UP しました。