Hatena::ブログ(Diary)

Life on Rails

2012-01-29

タブでapacheのログを区切る件について

apacheのログをタブで区切ると良い、という話を、nginx + unicornでrails3.1が動作する環境を作る - A Peak Never Ending !のnginxの設定ファイルのログ出力周りを見ていて思い出した。

/etc/nginx/nginx.conf:
(中略)
    log_format  main '$msec\t'
        '$status\t'
        '$request_time\t'
        '$remote_addr\t'
        '$upstream_addr\t'
        '$upstream_response_time\t'
        '$request\t'
        '$http_referer\t'
        '$http_user_agent';

その話の記事は以下になります。

404 Blog Not Found:tips - ApacheのLogフォーマットの方を変えて高速化

Benchmark: running rx_naive, rx_optim, tsv for at least 3 CPU seconds...
  rx_naive:  3 wallclock secs ( 3.15 usr +  0.00 sys =  3.15 CPU) @ 2417.71/s (n=7612)
  rx_optim:  3 wallclock secs ( 3.78 usr +  0.00 sys =  3.78 CPU) @ 78526.94/s (n=296930)
       tsv:  4 wallclock secs ( 3.02 usr +  0.00 sys =  3.02 CPU) @ 646447.96/s (n=1954495)
             Rate rx_naive rx_optim      tsv
rx_naive   2418/s       --     -97%    -100%
rx_optim  78527/s    3148%       --     -88%
tsv      646448/s   26638%     723%       --

タブ区切りをparseすると、実に10倍程度、早い。

#!/usr/local/bin/perl
use strict;
use warnings;
use Benchmark qw/timethese cmpthese/;

my $logline =
q{localhost.local - - [04/Oct/2007:12:34:56 +0900] "GET /apache_pb.gif HTTP/1.1" 200 2326 "http://www.dan.co.jp/" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7"};

my (@logline) =
  ( $logline =~
      m!^(.*) (.*) (.*) \[(.*)\] "(.*)" (.*) (.*) "(.*)" "(.*)"! );

my $tsv = join "\t", @logline;

cmpthese(
    timethese(
        0,
        {
            rx_naive => sub {
                my (@l) =
                  ( $logline =~
m!^(.*) (.*) (.*) \[(.*)\] "(.*)" (.*) (.*) "(.*)" "(.*)"!
                  );
            },
            rx_optim => sub {
                my (@l) =
                  ( $logline =~
m!^([^\s]*) [^\s]* [^\s]* \[([^]]*)\] "([^"]*)" ([^\s]*) [^\s]* "([^"]*)" "([^"]*)"!
                  );
            },
            tsv => sub { my (@l) = split /\t/, $logline },
        }
    )
);

かなり前にapacheのログを自分で解析するプログラムを書いたときに、とてもお世話になったコードです。

で、実はこのコードには問題があって、

31c31
<             tsv => sub { my (@l) = split /\t/, $logline },
---
>             tsv => sub { my (@l) = split /\t/, $tsv },

というpatchを当てないと正確な値が出ないのだと思うのです。perlを書いたことがないので正しいのかどうか分からんけれども。

結果はこうなります。

$ ./ben2.pl
Benchmark: running rx_naive, rx_optim, tsv for at least 3 CPU seconds...
  rx_naive:  3 wallclock secs ( 3.13 usr +  0.00 sys =  3.13 CPU) @ 4771.25/s (n=14934)
  rx_optim:  3 wallclock secs ( 3.12 usr +  0.00 sys =  3.12 CPU) @ 168081.41/s (n=524414)
       tsv:  2 wallclock secs ( 3.32 usr +  0.00 sys =  3.32 CPU) @ 278219.28/s (n=923688)
             Rate rx_naive rx_optim      tsv
rx_naive   4771/s       --     -97%     -98%
rx_optim 168081/s    3423%       --     -40%
tsv      278219/s    5731%      66%       --

それでも66%増なので、タブ区切りの方が良いのです。前述のnginxの設定ファイルのように、タブ区切りを採用しています。

この件について、twitterでもはてブでも指摘している人は居なかったので、

404 Blog Not Found:コードについて書く方がコードを書くより読まれる現実

読者のほとんどは、コードを読みたくないのだ、ということ。
ざっくり、記事中にコードが一行あるだけで読者は半減する。

ということは、そのとおりなんだろうなぁ、と思いました。

おわり。

2012-01-28

pjaxというもの

pjaxというものの話題を目にしたのは昨年だった。

javascriptを用いて動的なページ変化を作ろうと流行ったのはajaxだった。そこにpushStateを用いた履歴管理を含むpjax(pushState + Ajax)なるものが登場した。

Ajaxによるページ遷移はそうではないものに比べて、jsファイルの再ロードや冗長なレンダリングにかかる時間を削減することができる。加えてサーバ側では冗長なHTMLを吐くことを削減できるため、処理時間が短く済む。

通常、Ajaxによる画面遷移では履歴が残らないという問題を持つ。それをpushState関数を使うことで追加できるという。それだけでなく、Ajaxを利用したページ遷移に関連するいくつかの問題も解決している。

Ajaxを用いたページ遷移を行う場合、専用のjsもしくはhtmlの断片を送信することになる。このURLアクセスした場合、意味を成さない断片をユーザーは手にする。履歴管理をしていても無駄である。この問題の解決にpjaxではHTTPヘッダを用いる。pjaxを利用した画面遷移のURLリクエストに関してはHTTPヘッダにPJAX(HTTP_X_PJAX)を追加する。WebアプリケーションはPJAXヘッダの有無を見ることで、それがpjaxリクエストかどうかを判断し、PJAXリクエストであれば断片となるHTMLを、そうでなければフルセットのHTMLを返信する。

pjaxの最たる例はgithubレポジトリブラウザとされている。レポジトリブラウザにて何回かの遷移を行った後、アドレスバーに表示されるURLをコピー&ペーストしても、断片ではないフルセットのHTMLが表示される。またレポジトリブラウザの遷移にはアニメーションが用いられるため、フォルダの階層を移動していることが分かりやすい。

Railsにおけるpjax

このPjaxを実現するためのRailsプラグインとしてpjax_railsが作成されている。

フレームワークにとってこのプラグインは、PJAXヘッダの確認、jquery-pjaxのロード周りの整備などを行う。PJAXヘッダは、HTTPヘッダにおけるPJAXヘッダの有無を確認して、一部のHTMLを返すのか、フルセットのHTMLを返すのか、の判断を行う。

pjax特有のものとして、ページ読み込み時のjavascript読み込み(例えばhead内のscriptタグによる)が行われないため、初期化すべき箇所は自分でハンドリングしならない。

また、このプラグインでは、フルセット部分のレイアウトが行われなくなるため、ページに合わせてtitleタグを変更することも自分で行う必要がある。

最近ではRailsよりもWebサーバ側に近いRack側でpjaxに対応するプラグインが登場している。

https://github.com/eval/rack-pjax/

このプラグインは既存のpjax_railsにおいて、タイトルの変更が難しい点を解決している。短いコードでpjaxを実装している。

https://github.com/eval/rack-pjax/blob/master/lib/rack/pjax.rb

HTTPリクエストがあればRailsがフルセットのHTMLRackに応答する。

PJAXヘッダがHTTPリクエストに存在していたかを確認し、存在していればpjaxとして扱う。その応答をHTMLパーサ(Hpricot)にてパースし、data-pjax-container属性のあるタグとtitleタグを抜き出して、ブラウザに応答する。抜き出して送信するため、送信サイズは小さくなる。PJAXヘッダが存在していなければ、そのまま返す。

Hpricotによるパーサの影響のためか、aタグの下にdivタグなどを置くと、変なことになる(そもそも置いてはいけない)。

サーバ側の処理を少なくするという利点を殺しているが、レンダリングはpjax_railsよりも意図したとおりに行われるので、手間はこちらの方が少ない。

斜め上、jQuery Mobile

また、別のものとして、jQuery Mobileが存在する。

jQuery MobileもAjaxによるページ遷移を行うが、これはHTMLの抜き出しを必要とせずに、常にフルセットのHTMLを読み込む。フルセットのHTMLの中から、jQuery Mobileが必要とする部分をクライアントブラウザ上のjsにて判断し、抜き出して表示する。

jQuery Mobileのための独特な属性をHTML上に書く必要がある。

フルセットのHTMLを受信するため、サーバ側の送信データサイズは小さくならないが、画面遷移のための対応をサーバアプリケーションでする必要がない、という利点がある。ajaxによるページ遷移の利点のうち、js再ロードとレンダリングが入らないので、これもそれなりに使える。

が、ページ遷移時のscript問題はそのままなので、例えばgoogle analysisなどは独自に対応する必要がある。

ちなみにjQuery Mobileはαの時代に触ったきりなので、リリース後のことはしらない。

rack-pjaxとjQuery Mobile

rack-pjaxはjQuery Mobile的なフルセットな生成を必要としながらも、サーバ上でパーサーを働かせて抜き出してサイズを小さくしてくれる的なもの、に見える。つまり、サーバ側でパースするのか、ブラウザ側でパースするのか、の違い、ということだ。

まとめ

で、いろいろ、あれだけれども、rack-pjaxはそれなりに気に入っているのです。

プラグインサーバ処理データ送信量ブラウザ側処理手間
pjax_rails-layout部分view大(layoutしない部分の対応)
rack-pjax+HTMLパース小(pjax独特の初期化
jQuery Mobile普通普通中(独自タグ習得)

実際にこの手のサイトを作ったことがないので、どうとは言えないのだけれども、pjaxやり始めならrack-pjaxあたりで良いと思う。ソースを見ても分かるとおり、とても単純なので。慣れてきたらpjax_railsあたりを使うと良いかと。

jQuery Mobileは昔はもっさりしている気がしたのだが、今は知らない。

あれ

  • jsロード時間とレンダリングって、そんなに時間かかったっけ?
  • データ送信量によってデータ受信時間ってそんなに変わるっけ?
  • layout部分の処理ってcacheで救えるんじゃね?

という疑問も出てくるので測定は大事ですね。。

pjaxプラグインの問題点

例えば、この部分をpjaxで変える、と決めたら、そのdivタグにdata-pjax-containerという属性を加える。

 <div data-pjax-container></div>

そこに決めうちされているので、例えば、左右2ペインのページで左右それぞれをpjaxで更新したい場合は独自に作りこむ必要がある。後は変更したい箇所が入れ子になっている場合も。

なので複雑なことをしようとすると、フレームワークのlayoutの部分と密接に関わるプラグインを作っていかないと、面倒なことになりそうな予感がする。サイトの1部分のみを変更していく、という話ならば、そこまで難しくはない。

2011-11-18

Vimのプラグイン管理Vundleにてrails.vimを入れるとautoload周りでエラーが出る件について

vimプラグイン管理

vimプラグイン管理に関して、以前はvim-pathogenを使ってbundleディレクトリ以下にプラグインファイルをまとめる管理をしていた。標準のvimのやり方だと、プラグインのファイルが混ざってしまい、特定のプラグインを削除したり、更新したりをするためにファイルを1つ1つ確認せねばならず面倒になるからだ。

最近はvim-pathogenを一歩進めたvundleというプラグインを使っている。これは設定ファイルを書いておけば、vim上からプラグインを自動でダウンロード、更新してbundleディレクトリ以下で管理してくれる。vundleはRailsgem管理に使われているBundleを参考にしているらしい。

ということでvundleを使っている。

rails.vimでautoload周りのエラーが出る

ところが最近、rails.vimの読み込みで失敗してしまう。調べてみたところ、以下の記事が見つかった。

" Bundle 'rails.vim'

" Fetch 'rails.vim' plugin from github.

Bundle 'tpope/vim-rails'

としたところ、ちゃんと使えるようになりました。

http://d.hatena.ne.jp/Umeyashiki/20111001/1317488217

上記のとおり、rails.vimからtpope/vim-railsに変更すると上手く動作する。

これはどういうことなのか。そもそもvundleの動作原理を知らなかったのでソースを参照することにした。

vundleの仕組みを調べる

https://github.com/gmarik/vundle/blob/master/autoload/vundle/config.vim

これを見ると、parse_nameで何処にレポジトリを見に行くのか決定している。

例えばレポジトリの設定を

Bundle 'rails.vim'

のように書いていくのだが、この書き方によって変わる。

よって、

Bundle 'tpope/vim-fugitive'

Bundle 'git://git.wincent.com/command-t.git'

Bundle 'rails.vim'

のような指定があった場合、tpope/vim-fugitiveは[文字列]/[文字列]なので、https://github.com/tpope/vim-fugitive.gitを、git://git.wincent.com/command-t.gitはそのまま、rails.vimhttps://github.com/vim-scripts/rails.vimを見に行く。

https://github.com/vim-scripts/rails.vimレポジトリの中を見ると、autoloadディレクトリはなく、2011/8/31の更新のままであり、かつミラーであると記されている。本家のhttps://github.com/tpope/vim-railsは5日前に変更されたばかりのファイルがあり、前述のミラーは多少古いようだ。

結論

rails.vimのように、単純に書いただけだと割と古い https://github.com/vim-scripts/スクリプトを掴んでしまうので、多少面倒でもgithubプラグインの執筆者を調べてtpope/vim-railsのように書いたほうが最新のプラグインを使えるので楽しいかと思う。

2011-07-17

Ruby1.9のCGI.unescapeHTMLにて実体参照の戻しを行う場合には変換元の文字列のエンコーディングを考慮する必要がある

Ruby1.9で遊び始めた。WebのAPI叩いていたら、実体参照が含まれる文字列をunescapeHTMLしようとしても上手くいかない。例えば、"&#xffff"のような形式の実体参照

Ruby1.8の場合には問題とならなかったケースだった。cgi.rbソースコードを読んで、ようやく原因が分かった。

cgi/util.rb@1.9.2p180に含まれる、一節を抜粋する。

  def CGI::unescapeHTML(string)
    enc = string.encoding
#(中略)
    string.gsub(/&(amp|quot|gt|lt|\#[0-9]+|\#x[0-9A-Fa-f]+);/) do
      match = $1.dup
      case match
#(中略)
      when /\A#x([0-9a-f]+)\z/i
        n = $1.hex
        if enc == Encoding::UTF_8 or
          enc == Encoding::ISO_8859_1 && n < 256 or
          asciicompat && n < 128
          n.chr(enc)
        else
          "&#x#{$1};"
        end
      else

上記のwhen節は正規表現で"&#x0000;"形式を抜き出したあとの処理になる。encとは、引数stringのencodingを示し、この入ってくるstringのエンコーディングUTF-8か、それ以外で処理が異なる。

Ruby1.9ではそれぞれの文字列にそれぞれのエンコーディングが関連付けられる。よって引数としてのstringにASCIIエンコディングとなっている文字列が入っているのではないか、と考えられる。

#!/usr/bin/env ruby
require 'cgi'

print "RUBY_VERSION ", RUBY_VERSION,"\n\n"
word = "&#x901A;&#x5E38;"
print "encoding: ", word.encoding, "\n"
print "word: ", CGI.unescapeHTML(word), "\n\n"

word_utf8 = word.encode("UTF-8")
print "encoding: ", word_utf8.encoding, "\n"
print "word: ", CGI.unescapeHTML(word_utf8), "\n\n"

上記に検証コードを示す。下記に実行結果を示す。

$ ruby unescape.rb
RUBY_VERSION 1.9.2

encoding: US-ASCII
word: &#x901A;&#x5E38;

encoding: UTF-8
word: 通常

推測のとおり、US-ASCIIを入れてしまっていたようだ。文字コードを利用して整数から文字に変換するためには、当然、変換先のエンコーディングが必要になる。US-ASCIIのようなエンコーディング指定では、無理に変換しようとせずに、無視される。

Ruby1.9ではプログラムの冒頭にてリテラル文字列エンコーディングに何を使うかの指定が行える。そのため通常は問題にならなそうだ。

# -*- encoding: utf-8 -*-

今回のケースでは、WebのAPIを叩く過程のどこかでASCIIエンコーディング文字列が返ってきたようだ。どこで入っていたのかについては確認をしていない。

あまりにもアホなミスなのか、ネット上にunescapeHTMLについての事例の情報がなかった。恥ずかしいが、書いておこうと思う。

2010-07-02

vimでruby,railsのインストールをUbuntu 10.4で

結論としては素直にvim-railsを入れましょう

Ubuntu 10.4が出たのでvimrailsのコードを編集しやすい環境を構築中。オムニ補完が欲しいので、rubyライブラリにリンクしたvimインストールする。

sudo aptitude install vim-nox

当初は vim-basic あたりで十分かと思っていた。が、

vim --version | grep ruby

コマンドでrubyがリンクされているかどうか確認してみたところ、 +ruby ではなく -ruby なんぞが出てきた。面倒なので vim の再コンパイルをしようとしたが、尋常ではない依存関係のパッケージの多さに諦めた。debian/rulesを読むと、vim-gnome や vim-nox なんかは ruby ライブラリを有効にしてある設定だったので、それを入れることにした。

が、次は rubygems がない。

sudo aptitude install rubygems

ruby-enterprise を入れていたので、Ubunturuby は入れていなかった。混在するのは得策ではないと考えたからだ。諦めた。素直に rubygems を入れよう。

次は rails が入っていないと...

sudo /usr/bin/gem install rails

うん。もう既に vim-rails を入れない理由はない。素直に vim-rails を入れよう。

sudo aptitude install vim-rails
vim-ruby-install.rb
vim-rails-setup