Ruby拡張を書くことによる高速化を試みた一例

Ruby拡張を書くことによって、高速化を試みたのでその顛末を書く。尚、Ruby拡張と話の本筋はあまり関係がない。

モチベーション

パケットキャプチャの結果からヘッダ情報を取り出すためのプログラムを、pcaprubライブラリを用いて今まで書いていた。

このpcaprubは、キャプチャのためのライブラリであるlibpcapのための薄いラッパーであり、具体的にはキャプチャ結果であるpcapファイルから1パケットずつを切り離した状態で取り出すことが出来るのみである。このパケット中から各プロトコルのヘッダ情報(例えばIPヘッダやTCPヘッダ)を取り出すためには、Ruby上でバイナリを解析して情報を切り分けて行く必要がある。Ruby上でバイナリからヘッダ情報を取り出すライブラリの例としてpacketfuがある。

packetfuでは十分ではなかったので、Ruby上でバイナリからヘッダ情報を取り出すライブラリを自作していた。やがて、キャプチャ時間の増大に従って、このヘッダ情報の取り出しに遅さが感じられるようになってきた。処理するたびにCPUは100%に張り付く。不満が大きくなってきた。

そこで、pcaprubライブラリの置き換えとして、直接libpcapを叩き、ヘッダ情報の解析を行うプログラムを自作した。1日半かかった。

速度が出ない

Ruby上で行なっていたバイナリからヘッダ情報を取り出すコードをCに書き換えたRuby拡張を作成した。結果としてはrubypcapというライブラリが行なっていることをそのまま行い、必要なプロトコルのための処理を追加した形になっている。

速度改善の効果を大きくするために、不必要なペイロード部分のデータをRubyクラスにコピーしないようにもした。

常にtimeコマンドを使い、速度が出ているかどうかのチェックを行うようにした。結果としては期待するよりは下回ったが、2倍程度の速度が出るようだった。

改善後
real    0m5.902s
user    0m5.692s
sys     0m0.196s

改善前
real    0m13.363s
user    0m13.041s
sys     0m0.280s

しかし、測定のタイミングによっては遅くなる時がある。おそらくdisk cacheのせいだろうとは思っていたが、気には留めていなかった。最後にbenchmark用のスクリプトを書き、disk cache無しの状態の測定を行うことにした。

disk cacheのクリアには、sysctl -w vm.drop_caches=1を用いた。

改善後
vm.drop_caches = 1
real    0m15.106s
user    0m6.768s
sys     0m0.344s

改善前
vm.drop_caches = 1
real    0m16.626s
user    0m13.497s
sys     0m0.436s

disk cacheが効いている状態でなければ速度が出ない、という結果に終わった。(なんだ、俺は1秒を縮めるために1日半も時間をかけてしまったのか。ショックだ。)

この速度が出ない状態のCPU使用率を見ても50%程度であり、wait等に時間が取られている。前述の結果のrealとuserの値を比べても分かる。

disk cacheが効いている状態では2倍早いのは救いだが、このプログラムの利用上、disk cacheが効く状態、すなわち、一度pcapを読み込んだ状態で使うことは頻度としては少ない。少ないので苦労は報われない。

圧縮・伸張を使う

さて、どうにかできないものかと考えた。

先の結果のrealとuserを比較しても半分程はディスクアクセスにかけていそうな時間である。仮にメモリ上から直接読み込んで処理できたら早くなるのではないか。

このpcapファイルの中身、特にペイロード(データ)部分はパターン化されたものであり、特に圧縮が効く。gzip圧縮した場合、1/50になる。元々pcap生成時にgzip圧縮後に伸張して、このプログラムを使用している。このことから、ディスク速度がネックになるのであれば、1/50の状態のファイルから直接メモリ上に展開して処理できないものか、と考えた。

残念なことに、libpcapライブラリのAPIを用いてpcapファイル内データのメモリ上からの読み込みを行うことは簡単にはできない。

しばらく考え、メモリ上だがファイルシステムとして見えるRAMディスクを用いることを考えた。使い勝手がよさそうだったのは、特に何もしなくてもマウントされている、/dev/shmを使うことだった。RAMディスクを使った流れを示す。

  1. shm上にgz内のファイルを展開する
    • gzip -dc file.pcap.gz > /dev/shm/file.pcap
  2. shmに展開したファイルを読み込む
    • ruby_program /dev/shm/file.pcap

shmを使った状態の、改善後と改善前の双方のプログラムを用いた結果を示す。

双方ともにshm上のファイルにアクセス

改善後
vm.drop_caches = 1
real    0m6.194s
user    0m5.580s
sys     0m0.240s

改善前
vm.drop_caches = 1
real    0m14.173s
user    0m13.185s
sys     0m0.244s

2倍程度は早くなった。改善前のプログラムでは2秒程度しか速度が早くなっていないが、改善後のプログラムでは1/2程度に早くなっている。realとuserのバランスを見ても、問題なさそうだ。

gzip伸張する時間分は必要だが、これはどちらにせよ行う作業であり、disk上に伸長するのか、ram上に伸長するのかの違いしかないため、問題はない。ギリギリで速度改善に成功した例だった。

思う所

このような速度改善をおこなう場合、ディスクアクセスがネックになると早期に気づくためには何をすればよかっただろうか、と思うに至る。少なくとも、sysctl -w vm.drop_caches=1をtimeコマンドと共に利用すべきだった。

今回はHDD上だったので速度の問題が出たが、仮にSSD上であれば、問題は出なかったかもしれない。プロファイラーを使って詳細に解析していれば、分かった問題だったのだろうか。

ともかく、俺の1日が無駄にならなくて良かった。これに尽きる。

追記

別の種類ファイルで実験した所、改善前後で大きく性能が違った。

改善後(RAMディスク未使用)
vm.drop_caches = 1
real    0m16.852s
user    0m9.185s
sys     0m0.496s

改善後(RAMディスク使用)
vm.drop_caches = 1
real    0m8.845s
user    0m8.061s
sys     0m0.240s

改善前(RAMディスク未使用)
vm.drop_caches = 1
real    0m38.786s
user    0m36.110s
sys     0m0.556s

改善前(RAMディスク使用)
vm.drop_caches = 1
real    0m37.264s
user    0m35.850s
sys     0m0.388s

38.8秒程度かかっていたのが、8.8秒で終わっている。これは早い。

2種類のファイルともプログラムでの処理を毎回行うので、改善前(RAMディスク未使用)16.6+38.7=55.3秒かかっていたのが、改善後(RAMディスク使用)6.2+8.8=15.0秒 程度で済むことになる。3,4倍は早くなっているので、これでいいか、という気持ちになる。

infinite scroll採用のリンク遷移の問題点と対応

TwitterPinterestにて、画面最下部までスクロールすると、自動的に次のページを読み込む仕組みがある。これのことをinfinite scroll、無限スクロールと呼んだりする。これをなぜ多くのWebサイトが採用しないのか、疑問に思っていた。

よく考えてみると、このinfinite scrollを使うためにはサイト側で対応する必要がある。その理由はリンクによって次のページに移動してはならない、という制限があるからだ。ブラウザの「戻る」ボタンで戻った際に、以前の押したリンクの場所まで戻ることができない。

無限スクロールを必要とする程の長いコンテンツ一覧からコンテンツに飛ぶということは、戻った場合には長いコンテンツ一覧にて先ほど飛んだ場所に戻って欲しいものだ。これを実現できない場合、不便になり、無限スクロール採用するメリットが消えてしまう。

これに対して、各社は対応を行なっている。

例えばTwitterは140文字制限であり、コンテンツ一覧に入れ込むことができる。外部のリンクは、新しいウィンドウを開いて表示するようにしている。

Pinterestはコンテンツをクリックすると、ページ遷移をせずに、ページ上にオーバーレイ表示(pop)するようにしている。

よって、既存のコンテンツに対して無限スクロールを採用する場合は、

  • ページ遷移をする場合は新しいウィンドウを開くように強制する
  • ページ上にオーバーレイ表示するようにする

などの対応が必要になるはず。

AngularJSなどJavascriptのMVCフレームワークを試してみる環境について作ってみる

Ruby, RailsRubygemsの仕組みがある程度、わかっている人向けに書く。
あと、作ってみただけであって、実際に使ってはいないので、色々と問題あるかもしれない。

「HTMLやCSSJavascriptを1〜2年前くらいの知識ではどう書くのかな」の参考程度に見ても良いかもしれない。

前説

Backbone.jsやAngularJSなどJavascriptでもMVCフレームワークが出てきている。特にAngularJSはDIの仕組みを持つなど、ActionScript3のMVCフレームワークであるRobotlegsに感動した身としては興味深いライブラリである。

そのようなJavascirptライブラリを試す場合、色々な仕組みが欲しくなる。例えば以下のようなものだ。

  • HTMLのテンプレートエンジン
  • coffee scriptのコンパイラ
  • assetsエンジン

これを実現するにはRailsの新規プロジェクトを作成するのが手っ取り早い。早いのだが、Railsフルスタックなので色々と重い。軽量なシステムで試したい場合もある。

そんな時にはSinatraを試してみると良いと思う。

Sinatra公式サイトに書いてある通り、始めるには分かりやすい。

require 'sinatra'

get '/hi' do
  "Hello World!"
end

単純でいいが、しかしながらテンプレート(サイトを動作させるために最小限必要なディレクトリやファイル)を自動で作成してくれるRailsのような環境に慣れてしまうと、テンプレートを作成してくれないSinatraに不満を持つようになる。

そのような人のために、テンプレートや面倒な作業を肩代わりしてくれるPadrinoというライブラリがある。

$ padrino g project angular-test -e slim -c sass -s jquery -b
  • eの引数はHTMLテンプレートエンジンにslimを使い、-eの引数はcssのエンジンにsassを使う、という意味であり、これらのエンジンを使えるように準備してくれる。

HTMLテンプレートエンジン

slimは書く文字数を減らしてくれるエンジンであり、修正も楽だ。

doctype html
html
  head
    title Slim Examples
    meta name="keywords" content="template language"
    meta name="author" content=author
    javascript:
      alert('Slim supports embedded javascript!')

  body
    h1 Markup examples

    #content
      p This example shows you how a basic Slim file looks like.

このJavascriptMVCフレームワークを試す前はHamlをよく使っていた。

#profile
  .left.column
    #date= print_date
    #address= current_user.address
  .right.column
    #email= current_user.email
    #bio= current_user.bio

SlimはHamlからの派生であり、実際にHTMLを書くだけならSlimが必要というものでもない。

だが、AngularJSのようにHTML内に挙動を書き込むタイプと絡む場合はそうはいかない。

例えば、このようなコードを書くことになる。

<!doctype html>
<html ng-app>
  <head>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
  </head>
  <body>
    <div>
      <label>Name:</label>
      <input type="text" ng-model="yourName" placeholder="Enter a name here">
      <hr>
      <h1>Hello {{yourName}}!</h1>
    </div>
  </body>
</html>

これをHamlで表現するとこうなる。html2hamlで変換すれば結果が得られる。

!!!
%html{"ng-app" => ""}
  %head
    %script{:src => "//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"}
  %body
    %div
      %label Name:
      %input{"ng-model" => "yourName", :placeholder => "Enter a name here", :type => "text"}/
      %hr/
      %h1 Hello {{yourName}}!

さらにSlimの結果を載せる。http://html2slim.herokuapp.com/ の結果の一部を修正した。

doctype
html ng-app=""
  head
    script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"
  body
    div
      label Name:
      input ng-model="yourName" placeholder="Enter a name here" type="text"
      hr
      h1 Hello {{yourName}}!

この結果を見ると必要なタイプ数は減っていることが分かると思う。ともかくHamlを書いているとHTMLタグの属性を書くために{}が必要で、それを書くのが煩わしい。プラグイン書けばどうにかなるのだが、それを使うなら、もっとシンプルなエンジンを使ったほうが良いかと考えた。

CSSエンジン

CSSエンジンもHamlと同じく、改行によってクロージャーを表現する。

$blue: #3bbfce
$margin: 16px

.content-navigation
  border-color: $blue
  color: darken($blue, 9%)

.border
  padding: $margin / 2
  margin: $margin / 2
  border-color: $blue

とか

table.hl
  margin: 2em 0
  td.ln
    text-align: right

li
  font:
    family: serif
    weight: bold
    size: 1.2em

のような書き方ができる。sassの他にscssという書き方もある。

もっと先鋭な人はlessとか使っていると思う。

自分はクロージャーのために{}を書く思想が受け入れられないので使えない。(だったらpython使えよ、というツッコミはなしの方向で...なにとぞ...)

Javascriptのための言語

言語と呼べばいいのか何なのかわからないが、Javascriptを書く手数を減らしてくれる。

square = (x) -> x * x

cubes = (math.cube num for num in list)

square = function(x) {
  return x * x;
};

cubes = (function() {
  var _i, _len, _results;
  _results = [];
  for (_i = 0, _len = list.length; _i < _len; _i++) {
    num = list[_i];
    _results.push(math.cube(num));
  }
  return _results;
})();

になったりする。

例えばAngularJSでは、

function TodoCtrl($scope) {
  $scope.todos = [
    {text:'learn angular', done:true},
    {text:'build an angular app', done:false}];
 
  $scope.addTodo = function() {
    $scope.todos.push({text:$scope.todoText, done:false});
    $scope.todoText = '';
  };
 
  $scope.remaining = function() {
    var count = 0;
    angular.forEach($scope.todos, function(todo) {
      count += todo.done ? 0 : 1;
    });
    return count;
  };
 
  $scope.archive = function() {
    var oldTodos = $scope.todos;
    $scope.todos = [];
    angular.forEach(oldTodos, function(todo) {
      if (!todo.done) $scope.todos.push(todo);
    });
  };
}

のようなコードを書くが、これがcoffeeだと、

TodoCtrl = ($scope) ->
  $scope.todos = [
    text: "learn angular"
    done: true
  ,
    text: "build an angular app"
    done: false
  ]
  $scope.addTodo = ->
    $scope.todos.push
      text: $scope.todoText
      done: false

    $scope.todoText = ""

  $scope.remaining = ->
    count = 0
    angular.forEach $scope.todos, (todo) ->
      count += (if todo.done then 0 else 1)

    count

  $scope.archive = ->
    oldTodos = $scope.todos
    $scope.todos = []
    angular.forEach oldTodos, (todo) ->
      $scope.todos.push todo  unless todo.done

と表現できる。http://js2coffee.org/ を使わせてもらった。

assetsエンジン

JavascriptCSSは大体の場合、最終的には1つのファイルとして扱ったほうが良い。複数のファイルに分散されていると、それごとに取得しに行かなければならない。またminifyという縮小化の名を借りた難読化を行うことも多い。

そういった処理を行うためのエンジンとしてsprocketsというライブラリがある。

このsprocketsの導入に前もって、 app/assets/javascripts, app/assets/stylesheets, app/assets/images などのディレクトリを作成する。jsとcssは、application.cssとapplication.jsというファイルを作成し、それぞれ、

//= require common
//= require_tree .

などと書いておく。require_treeが書かれていれば、そのディレクトリ内の全てのファイルがcombineされるし、読み込む順番を指定したいのであれば、requireによって個別に指定できる。前述の例では、common.js(common.css)を呼び出す。

そしてHTML上でlinkタグやscriptタグで、assets/application.css、もしくはjsにアクセスするようにすれば、アクセスごとにcombineされる。

sinatraへのsprockets導入はpadrino-sprocketsの0.0.2がオススメである。0.0.1はバグる。

ちなみにexecjsによってruby上でJavascriptを動作させようとするので、therubyracer(Google V8)などのgemを入れておく必要がある。

結果としてのGemfile

こうなる。

source :rubygems

# Project requirements
gem 'rake'
gem 'sinatra-flash', :require => 'sinatra/flash'

# Component requirements
gem 'rack-coffee', :require => "rack/coffee"
gem 'therubyracer'
gem 'coffee-script'
gem 'sass'
gem 'slim'

# Test requirements

# Padrino Stable Gem
gem 'padrino', '0.10.7'

gem 'padrino-sprockets', :require => "padrino/sprockets", :git => "https://github.com/nightsailer/padrino-sprockets.git"

つまり

生でHTML書いたり、生でCSS書いたり、生でJavascript書いていることはもう無いですよ、ということです。

言いたいこと

Padrino(Sinatra) + slim(html) + sass(css) + coffee-script(js) 上でAngularJSなどのMVCフレームワーク使って遊ぶと楽しいかもよ、と。

で、理想としては、バックエンドのAPIサービスとしてRailsを使い、Rails側はHTMLを吐かずにJSONの出し入れのみを行うこと。

JS-MVC(Sinatra) <--JSON--> JSON-API(Rails) <--SQL(ActiveRecord)--> DB

こうすれば、JS-MVC部分はiPhone/AndroidアプリだろうとFlash(Adobe Air)だろうと、同じサービスを同じAPI(ロジック層)で実現できるはず。

JSアプリケーション部分とAPI部分のインターフェースの策定方法や自動化に良いアイディアが生まれるとすれば、今後は、GmailGoogle Readerのようなアプリケーションがぽっこぽこ生まれるような世の中になると予想する。

そういう世の中になるんかな、を実感するために、MVCフレームワークを試してみる環境を作る、という企画でした。

AirVideoのためにUbuntu 12.04 LTSにてmp4creator(mpeg4ip)をビルドする

AirVideo For Linuxを動作させるためにmp4creatorが必要になるのだが、Ubuntu 12.04 LTSではmp4creatorが入っているパッケージが存在しない。よって、何処からかパッケージを持ってくるか、自分でビルドする必要がある。ビルドしようとすると色々ハマる。

installは行わずに、AirVideoの設定(properties)にて直接パスを指定して、利用する。

buildに必要なパッケージは入っている事とする。
mp4liveやv4l2はいらない。

$ sudo aptitude install bzr
$ bzr branch lp:ubuntu/precise/mpeg4ip
$ cd mpeg4ip
$ cat debian/patches/* | patch -p1
$ ./configure --prefix=/usr --disable-alsatest --disable-player
$ make

途中で止まる。

make[3]: ディレクトリ `/tmp/mpeg4ip/lib/rtp' に入ります
if /bin/bash ../../libtool --mode=compile --tag=CC gcc -DHAVE_CONFIG_H -I. -I. -I../.. -I../../include -I../../lib/utils   -DDEBUG -Wall -Werror -g -O2 -DMPEG4IP -I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT -MT net_udp.lo -MD -MP -MF ".deps/net_udp.Tpo" -c -o net_udp.lo net_udp.c; \
        then mv -f ".deps/net_udp.Tpo" ".deps/net_udp.Plo"; else rm -f ".deps/net_udp.Tpo"; exit 1; fi
 gcc -DHAVE_CONFIG_H -I. -I. -I../.. -I../../include -I../../lib/utils -DDEBUG -Wall -Werror -g -O2 -DMPEG4IP -I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT -MT net_udp.lo -MD -MP -MF .deps/net_udp.Tpo -c net_udp.c  -fPIC -DPIC -o .libs/net_udp.o
net_udp.c: In function 'socket_error':
net_udp.c:166:11: error: variable 'retlen' set but not used [-Werror=unused-but-set-variable]
cc1: all warnings being treated as errors
  • Werrorによってwarningが出たらエラーになり止る設定になっている。

lib/rtp/Makefileの328行目のWerrorを消して再度、make。

今度はmpeg2psで止まる。

g++ -D_REENTRANT -Wall -Wno-char-subscripts -Woverloaded-virtual -Wno-unknown-pragmas -Wno-deprecated -Wformat=2 -Wpointer-arith -Wsign-compare -fno-strict-aliasing -g -O2 -DMPEG4IP -I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT -o .libs/mpeg_ps_info ps_info.o  ../../lib/gnu/.libs/libmpeg4ip_gnu.so ../../lib/mp4av/.libs/libmp4av.so ../../lib/mp4v2/.libs/libmp4v2.so ./.libs/libmpeg2_program.a -ldl -Wl,--rpath -Wl,/usr/lib
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `get_info_from_frame':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1014: undefined reference to `h264_nal_unit_type'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1024: undefined reference to `h264_find_next_start_code'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1014: undefined reference to `h264_nal_unit_type'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1017: undefined reference to `h264_read_seq_info'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1060: undefined reference to `MP4AV_Mp3HeaderFromBytes'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1061: undefined reference to `MP4AV_Mp3GetChannels'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1062: undefined reference to `MP4AV_Mp3GetHdrSamplingRate'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1063: undefined reference to `MP4AV_Mp3GetHdrSamplingWindow'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1064: undefined reference to `MP4AV_Mp3GetBitRate'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1065: undefined reference to `MP4AV_Mp3GetHdrLayer'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1066: undefined reference to `MP4AV_Mp3GetHdrVersion'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1036: undefined reference to `MP4AV_Mpeg3ParseSeqHdr'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1075: undefined reference to `MP4AV_Ac3ParseHeader'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `find_pack_start':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:179: undefined reference to `MP4AV_Mpeg3FindNextStart'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_stream_find_mpeg_video_frame':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:549: undefined reference to `MP4AV_Mpeg3FindNextStart'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:585: undefined reference to `MP4AV_Mpeg3FindNextStart'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_stream_find_h264_video_frame':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:643: undefined reference to `h264_is_start_code'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:645: undefined reference to `h264_find_next_start_code'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:643: undefined reference to `h264_is_start_code'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:680: undefined reference to `h264_find_next_start_code'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:697: undefined reference to `h264_nal_unit_type'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:699: undefined reference to `h264_nal_unit_type_is_slice'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_stream_find_mp3_frame':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:883: undefined reference to `MP4AV_Mp3GetNextFrame'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_stream_figure_out_video_type':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:735: undefined reference to `h264_is_start_code'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:736: undefined reference to `h264_nal_unit_type'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_stream_find_ac3_frame':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:824: undefined reference to `MP4AV_Ac3ParseHeader'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_get_video_stream_name':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1575: undefined reference to `mpeg2_type'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_get_video_frame':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1854: undefined reference to `MP4AV_Mpeg3PictHdrType'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_seek_video_frame':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:2118: undefined reference to `h264_access_unit_is_sync'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:2129: undefined reference to `MP4AV_Mpeg3PictHdrType'
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:2135: undefined reference to `MP4AV_Mpeg3PictHdrType'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_get_video_stream_name':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1571: undefined reference to `h264_get_profile_level_string'
./.libs/libmpeg2_program.a(mpeg2ps.o): In function `mpeg2ps_get_video_stream_mp4_type':
/tmp/mpeg4ip/lib/mpeg2ps/mpeg2ps.c:1633: undefined reference to `mpeg2_profile_to_mp4_track_type'
collect2: ld returned 1 exit status
make[3]: *** [mpeg_ps_info] エラー 1

これはライブラリのlibmp4av.soなどの読み込む順番が悪くて、関数が引けなくなっている。
lib/mpeg2ps/Makefileの286行目から始まる

prog_libadd = \
  $(top_builddir)/lib/gnu/libmpeg4ip_gnu.la \
  $(top_builddir)/lib/mp4av/libmp4av.la \
  $(top_builddir)/lib/mp4v2/libmp4v2.la \
  libmpeg2_program.la

prog_libadd = \
  libmpeg2_program.la \
  $(top_builddir)/lib/gnu/libmpeg4ip_gnu.la \
  $(top_builddir)/lib/mp4av/libmp4av.la \
  $(top_builddir)/lib/mp4v2/libmp4v2.la

にして、先にライブラリを読ませるようにする。再度、make。

今度は最後まで通る。

server/mp4creatorに出来ているはず。

タブで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:コードについて書く方がコードを書くより読まれる現実

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

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

おわり。

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がフルセットのHTMLをRackに応答する。
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はそれなりに気に入っているのです。

  • 2つのプラグイン
  • pjax_rails
    • Railsプラグイン
    • レンダリングは必要な部分のみ(layoutは除く)
      • なのでサーバ側は余計な処理(layoutのview処理)をしないから、早くなるはず
      • 出力結果も代わるので注意が必要
    • dhh
  • rack-pjax
    • Rackプラグイン
    • レンダリングはフルセット(layoutも含む)
      • なのでサーバ側は特に変わらない
      • 出力結果も変わらないので処理しやすい
      • Rackでパースして必要な部分だけ送ってくれるので送信サイズは小さい
  • 番外,jQuery Mobile
    • Javascript。普通のHTMLでもscriptタグ書けば対応できる。
    • レンダリングはフルセット(layoutも含む)
      • なのでサーバ側は特に変わらない
      • 出力結果も変わらないので処理しやすい
      • ブラウザでパースして必要な部分だけ使う
プラグイン サーバ処理 データ送信量 ブラウザ側処理 手間
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部分のみを変更していく、という話ならば、そこまで難しくはない。

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が表示される。またレポジトリブラウザの遷移にはアニメーションが用いられるため、フォルダの階層を移動していることが分かりやすい。