2011/03/31

chrome.tabsイベントの動きを追って、graphvizで図にしてみた

Open PinnedTab LinkというGoogle chromeの機能拡張を作成したので、 Google Chrome Extensionsのデベロッパーガイドを読む必要がありました。

特に Browser Interaction/Tabs (chrome.tabs package)にあるイベントハンドラーの動きがちょっと分かりづらかったので、備忘録的にどういう風に呼ばれるのか図にしてみました。

各ハンドラーの先頭にconsole.log()を入れるローレベルな方法で動きを追ったので、事前条件やテストの方法がまずかったりして違う動きになるかもしれません。

まずは、Graphviz(dot)で図にしてみた

タブを開くたびに上からNormal stateまでのevent handlerメソッド(chrome.tabs.on*())が呼ばれます。

"Normal state"は通常のWebブラウジングをしている状態です。

Statechart diagram of chrome.tabs event handlers

遷移するアクションの説明

説明のない矢印は自動で次の状態に遷移する事を示していて、Chromeのイベントは楕円で示しています。

  • create_tab(): Chrome上で新しいタブを開いたり、"Open Link in New Tab"を選択した場合のアクション
  • (un)pin_tab(): Pinを固定したり、外したりを選択した場合のアクション
  • reloase(): C-rや"Reload"などで明示的にページを更新した場合のアクション
  • close_tab(): C-wや"Close Tab"を選択した場合のアクション
  • (un)dock_tab(): タブをドラッグして、別ウィンドウに開いたり、別ウィンドウに統合した場合のアクション
  • change_focus(): 単純にタブを選択したり、別のタブを閉じたりした事でフォーカスが当った場合のアクション

chrome.tabs.OnRemovedが呼ばれた時には、そのタブは閉じますが、別のタブにフォーカスが当たるので、矢印が2つ出ている事になります。

明示的にTabIDやガード条件のようなものは書いていませんが、適当に読み取れると思います。

楕円で示したところは Events を示していますが、念のため対応する完全修飾のイベント名を列挙しておきます。

  • onCreated(): chrome.tabs.onCreated イベント
  • onUpdated(): chrome.tabs.onUpdated イベント
  • onSelectionChanged(): chrome.tabs.onSelectionChanged イベント
  • onDetached(): chrome.tabs.onDetached イベント
  • onAttached(): chrome.tabs.onAttached イベント
  • onMoved(): chrome.tabs.onMoved イベント
  • onRemoved(): chrome.tabs.onRemoved イベント

図を生成する元のdotファイル

図を生成するためのa.dotファイル



// Statechart diagram of chrome.tabs.on*()

digraph G {

  start [shape=circle, label="", style=filled];
  normal [shape=rect label="Normal state"];
  closed [shape=doublecircle, label="", style=filled];

  onCreated [label="onCreated()"];
  onSelectionChanged [label="onSelectionChanged()"];
  onSelectionChanged_ [label="onSelectionChanged()"];
  onUpdated [label="onUpdated()"];
  onRemoved [label="onRemoved()"];
  onMoved [label="onMoved()"];
  onDetached [label="onDetached()"];
  onAttached [label="onAttached()"];

  start -> onCreated [label="create_tab()"];
  onCreated -> onSelectionChanged_;
  onSelectionChanged_ -> onUpdated;
  onUpdated -> normal;

  // normal state
  normal -> normal;
  normal -> onSelectionChanged [label="change_focus()"];
  onSelectionChanged -> normal;

  // change tab position
  normal -> onMoved;
  onMoved -> normal;

  // pin or unpin tab
  normal -> onUpdated [label="(un)pin_tab() /\nreload()"];

  // dock or undock tab
  normal -> onDetached [label="(un)dock_tab()"];
  onDetached -> onAttached;
  onAttached -> onSelectionChanged [label="change_focus()"];

  // close tab
  normal -> onRemoved [label="close_tab()"];
  onRemoved -> onSelectionChanged [label="change_focus()"];
  onRemoved -> closed;
}

これを図にするには、dotコマンドを使って次のようなコマンドラインを使っています。

$ dot -Tpng -o a.png a.dot

困ったこと

複数のタブを保存して開いた時に、chrome.tabs.onUpdated が呼ばれずに、いきなり chrome.tabs.onSelectionChanged が呼ばれる場合がありました。

さらに悪いことに、この場合には Pin Tab かどうか確実に判別することができませんでした。

そのためタブを保存した状態のChromeを起動して、偶然この問題に遭遇するとPin/Unpinを判別するような機能拡張がうまく動作しない場合があります。

Open PinnedTab Linkでは、この他にもUnpinの場合にContext Menuを無効にするようにしたかったのですが、いまのところ良い手がなく全てのタブの中にメニューを表示しています。

問題はいろいろありますが、タブをブックマーク的に使う初期の目標は達成できたので良しとしましょう。 でも当然ユーザーは不満に思いますよね、「無駄なら消してよ」って。うーん、困ったなぁ。

2011/03/29

LinuxのVMWare Workstationで録画したスクリーンキャプチャにMacのLogic 8でBGMをつけてみる

手元のLinux版のVMWare Workstation 7.1.3には画面操作を録画する機能があります。

形式はAVI形式なのですが、中身のコーデックがWindowsベッタリなのかMac側のLogic 8やCompressorには直接取り込む事ができません。

全般的な流れはOgg Theoraを経由し、AVI → MOVファイル変換を行なう事になっています。

簡単ですが、備忘録的にログを残しておきます。

作業の手順

Linux上でVMWareが作成したAVIファイルをエンコーディングをOgg Theoraに変更する。

$ oggconvert

変換したファイルはMacに転送する。

$ scp vmware_movie.ogv macmini:

Mac(macmini)側のデスクトップに移動し、作業を継続する。

MacのQuickTimeにxiph.org/quicktimeからXiphQT 0.1.9をダウンロードし、Library/Components/XiphQT.componentに配置する。

成功すると、転送したvmware_movie.ogvファイルをQuickTime 7で再生することができるようになる。

そのQuickTime 7でvmware_movie.ogvを開いたまま、 ファイルメニューから「書き出し」を選択し、QuitkTime(mov)形式に変換する。

作成したファイルをQuickTimeのメニューにある「ムービー」から読み込み、必要なオーディオトラックの編集をする。

MOVファイルをYouTubeへアップロードする場合に、うまく行かないと感じた場合はComposerを使って、mpeg-4やh264形式に変換する。

以上、ここまで。

Google Chrome用の機能拡張 "Open PinnedTab Link" を作ってみた

結論からいうと、この記事ではGoogle Chrome用に作成したOpen PinnedTab Linkという拡張機能を紹介しています。

ブックマーク代りのピン化したタブ

いまのところLinux 64bit版のGoogle Chrome 10.0.648.204をメインに使っています。

Ubuntu 10.04ではmozilla-daily-ppaを経由してFirefox 4を導入していますが、ピンにしたタブから開くリンクは新しいタブで開くところがとても便利だと思っています。

Google Chromeでもタブをピンにすることはできますが、標準ではピンになったタブの中でリンクが開いてしまい、ブックマーク代りに使うには適していません。

TabLinkは全部のリンクを書き換えてしまう…

似たようなことを考える人はいて、Pin Tab Links should open new windowというHelp forumのトピックからTabLinkという機能拡張を試してみました。

TabLinkは問題なく動いたのですが、全部のリンクが書き変わってしまうので、自分の目的には合いませんでした。 そのため今回はピン化したタブからのリンクだけは新しいタブで開くというOpen PinnedTab Linkを作りました。

特別なことはしていません。内部的にはピン化したタブかどうかを判断する処理の後はTabLinkと同じことをしています。

ただし、ピンを外した後は全リンクの"target"属性を空にすることをしているので、元々新しいタブを開くリンクは違う動きをするかもしれません。

さいごに

ピンタブを使うと、アイコンでその機能を見分ける必要がでてくるので、サイトがfavicon.icoを設定していないと微妙に不便になるでしょう。

2011/03/24

RubyでFastCGIとGetTextモジュールを組み合せる

Rubyで多言語化を行なうにはMutohさんのRuby-GetText-Packageが便利そうです。

通常の po → mo の変換を行なえばメッセージを m17n できるところが良いので、作業の内容は一般的なものです。

単純な置換なら手でも作れますが、将来的に複数の言語に対応するなら、自分でロジックを組む手間が削減できます。

処理速度が速くなるかどうかは微妙で、ステータスをThread毎に格納して、全体のメッセージ文はRuntimeでキャッシュされるので手作業で置換ロジックを書くよりも多少はスピードアップが期待できそうです。ただメモリを消費してもよいならシンプルに正規表現のリストと置換する文字列の対を作った方が早いんじゃなかろうかと想像しています。

ERBだと<%= _("message") %>のように埋め込む手間がかかり、可読性は多少低下するデメリットもありそうです。

それでもGetTextモジュールを使うことで作業手順が標準化できるので、置換対象の文字列を管理する後々のメンテナンスを考えると総合的に利点が上回ると思います。

GetText自体はcgiクラスとの組み合せを想定しているので、FastCGIについてはドキュメントがなさそうでした。 そのためGetTextとFastCGIについてまとめておきます。

開発環境について

いつものようにUbuntu 10.04 LTS x86_64版を使用しています。

  • OS: Ubuntu 10.04 LTS x86_64
  • Ruby 1.9.2-p136 (/usr/local/bin/ruby) + gettext + fcgi
  • Apache 2.2.14 + libapache2-mod-fcgid

FastCGIとCGIの違い

FastCGIは標準添付ライブラリにはないため、fcgiモジュールをgemなどでインストールする必要があります。

CGIとの違いは起動済みのプロセスにWebブラウザからアクセスする仕組みなので、プロセスの起動にかかる時間が短縮される分、メモリを常に使いますが処理スピードは速いです。

またプロセス自体は終了せずに(Rubyの場合)スレッドが各ブラウザからのリクエストを処理するため、インスタンス変数やクラス変数を適切に使うことで、キャッシュの効果により全体的なレスポンスを向上させる事もできます。

その反面、処理する単位はオブジェクト単位にしてシンプルに切り分けないと、キャッシュしたくない内容が残ったりしてプライバシー上の問題を引き起す可能性もあります。

一般的なWebコンテンツのホスティングサービスでは、FastCGIのようにプロセスが常駐するとメモリを消費するため、必要に応じてリソースを消費するCGIやPHPが一般的です。

FastCGIもCGIもWebの初期からある仕組みなので古典的に扱われますが、Web以前から起動済みのプロセスにオンライントランザクションを処理させる仕組みは、決済やら問い合せ処理やらを高速に処理する仕組みとしてよく知られていました。

起動時間とその起動処理を無視できることは、アクセスが増えていく中で安定的なレスポンスを得るための重要な要件の一つです。

PHPはApacheのモジュールとしてサーバのプロセスイメージの中で処理が完結するため速いはずですが、基本的にはあらかじめ処理内容をキャッシュするわけではないので、時間は短縮できますが、接続毎にいろいろな初期化処理が必要となる点が独特です。

さて「FastCGIは古くない!」という主張は十分したので、GetTextとの組み合せについてまとめます。

FastCGIとGetTextの組み合せ

GetText付属のサンプルをみると、CGIモジュールに対応したスクリプトがありますが、そのままではFastCGIに適用できません。

その理由はFastCGIのスレッドをプールして使い回すという性質にあります。

違いを説明する前に雛型になりそうなスクリプトは次のようになります。

完全に動作するFastCGIスクリプト全体

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-

$:.unshift "/app/lib"
ENV['GEM_HOME'] = "/app/gems"

require 'rubygems'
require 'fcgi'
require 'cgi'
require 'erb'
require 'gettext/cgi'
require 'gettext/tools/parser/erb'

## Render
class SimpleRender

  def initialize(query, env)
    @query = query
    @env = env
    Locale::clear
    Locale::set_request([query["lang"]], [], env["HTTP_ACCEPT_LANGUAGE"], env["HTTP_ACCEPT_CHARSET"])
    GetText::set_output_charset("UTF-8")
    GetText::bindtextdomain("sample", "/app/data/locale")
  end
  
  def render
    ret = ""
    content = "/app/sample.erb"

    ret += ERB.new(open(content, "r:utf-8").read).result(binding)
    return ret
  end

  def _(msgid)
    GetText::_(msgid)
  end
end

## Controller
class Main
  def initialize(request)
    @env = request.env
    @query = check_query_string(CGI.parse(@env['QUERY_STRING']))
  end
  
  def run
    ret = "Content-Type: text/html; charset=UTF-8\r\n\r\n"
    case @query["mode"]
    when "search"
      sr = SimpleRender.new(@query, @env)
      ret += sr.render()
    end
    return ret
  end

  private
  def check_query_string(q)
    ret = {}
    label = "mode"
    ret[label] = (q.has_key?(label) and not q[label][0].empty?) ? q[label][0] : "search"
    label = "lang"
    ret[label] = (q.has_key?(label) and not q[label][0].empty?) ? q[label][0] : "ja"
    return ret
  end
end

###############
## main loop ##
###############

FCGI.each {|request|
  main = Main.new(request)
  request.out.print main.run
  request.finish
}

スクリプトが参照している外部ファイルについて

gemsを使うかどうかは問題ではないのですが、requireしているモジュール・ライブラリにアクセスできるように、先頭の6行は適切なパスに書き直す必要があります。

/app/sample.erbはコンテンツ本体で、次のような内容になっています。

sample.erb本体

<html>
<body>
<h1><%= _("Hello World") %></h1>
</body>
</html>

GetTextモジュールの処理の中心となる翻訳データを保存するためには /app/data/localeディレクトリを作成しています。 作成方法はmoファイルを生成するところで行なっています。

結果として、ライブラリを除いた/appディレクトリの構造は次のようになっています。

$ find . -type f
./po/en/sample.po
./po/ja/sample.po
./po/sample.pot
./Rakefile
./data/locale/en/LC_MESSAGES/sample.mo
./data/locale/ja/LC_MESSAGES/sample.mo
./sample.erb
解説:Thread毎のキャッシュの破棄

LocaleはThread.current[:current_request]に必要なデータ構造をキャッシュします。

スレッドが消滅しないかもしれないので、接続毎に処理が終った段階でデータを破棄する必要があります。

処理が終った段階」を徹底するために、処理を始める前にデータを破棄しています。 このLocale::clearはnilを代入するだけの処理なので、スレッドが立ち上がった最初に実行しても問題ない内容になっています。

処理開始時にLocale::clearを呼び出しているところ

    Locale::clear
    Locale::set_request([query["lang"]], [], env["HTTP_ACCEPT_LANGUAGE"], env["HTTP_ACCEPT_CHARSET"])
    GetText::set_output_charset("UTF-8")
    GetText::bindtextdomain("sample", "/app/data/locale")
解説:クライアントの言語情報の登録

前項にあるコードの2行目 Locale::set_request()メソッド が、クライアントが利用する言語を登録しているところです。

CGIモジュールを使うサンプルでは、Locale::set_cgi()を使用していますが、今回はCGIオブジェクトは使えないため直接その内部で使用している Locale::set_requestメソッド を呼び出しています。

第一引数に指定した言語から優先度が高く、第二引数にはCookieに保存した言語情報を格納する想定ですが、cookieを使っていないので空にしています。

GetText関連ファイル(po,moファイル)の作成

今回の例では、moファイルにアクセスするために、/app/data/localeを指定しています。

必要なファイルを作成するために、まず次のような内容の/app/Rakefileファイルを作成しました。 これはMutohさんのページの説明ほぼそのままです。

/app/Rakefileの全体

$:.unshift "/app/lib"

desc "Update pot/po files."
task :updatepo do
  require 'gettext/tools'
  GetText.update_pofiles("sample", Dir.glob("*.erb"), "sample 1.0.0")
end

desc "Create mo-files"
task :makemo do
  require 'gettext/tools'
  GetText.create_mofiles
end

先頭の $:.unshift は、FastCGIスクリプトと同じで、GetTextモジュール(gettext.rb)へのパスです。

デフォルトのパスにインストールしていれば不要ですが、今回は特別な場所にモジュールを配置しているので追加しています。

ファイルを配置した後は、poファイルを作成します。

$ cd /app
$ rake updatepo

"/app/po/sample.pot"が作成されるので、それを言語毎のディレクトリにコピーをして翻訳を行ないます。

$ cd /app
$ mkdir po/ja po/en
$ cp po/sample.pot po/ja/sample.po
$ cp po/sample.pot po/en/sample.po

2つのsample.poの翻訳が終ったら moファイル を作成します。

$ rake makemo

これが無事に終ると /app/data/locale/ja/LC_MESSAGES/sample.mo/app/data/locale/en/LC_MESSAGES/sample.mo のファイルが作成されているはずです。

po/ja/sample.poファイルの内容

# Copyright (C) 2011 Yasuhiro ABE yasu@yasundial.org
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: sample 1.0.0\n"
"POT-Creation-Date: 2011-03-24 09:25+0900\n"
"PO-Revision-Date: 2011-03-24 09:25+0900\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: sample.erb:3
msgid "Hello World"
msgstr "こんにちは"

FastCGIスクリプトの実行

Apacheを適切に設定して、Options ExecCGIが設定された場所に配置してWebブラウザから呼び出します。

URLの最後に ?lang=ja?lang=enをつけて呼び出して内容が変化することを確認します。

GetTextモジュールを使っていて気がついたこと

rmsgmergeコマンドが使えない

たぶん一斉置換か何かの影響か、gettext/tools/rmsgmerge.rb の中がおかしくなっています。

rmsgmergeの修正個所

#      refpot = parser.parse_file(config.refstrrefstr, PoData.new, false)
      refpot = parser.parse_file(config.refpot, PoData.new, false)

これで終りかと思いきや、-hで表示される第一引数と第二引数は逆にしないといけない模様です。

いまのところRakefileの方が便利そうなので困ってはいませんが、コマンドを使って更新をpoファイルに反映したい場合には注意が必要です。

困った事にdebパッケージでruby 1.8.x用に入っているrmsgmergeも動きません。

rgettextコマンド内部で参照しているrgettext.rbへのパスがおかしい

require 'gettext/rgettext'require 'gettext/tools/rgettext'に変更しました。

rgettextコマンド内部にあるrubyコマンドへのパスを変更する

rmsgmergeでも同じでしたが、コマンドの先頭にある#! /usr/bin/rubyのパスを#!/usr/local/bin/rubyに変更しました。

$:変数にライブラリディレクトリを加える

またRuby 1.9.2では$:はカレントディレクトリを指さないので、gettext.rbのあるディレクトリへのパスを$:に登録しています。

いろいろ修正したrgettextスクリプト全体

#! /usr/local/bin/ruby
# -*- coding: utf-8 -*-
=begin
  rgettext - ruby version of xgettext

  Copyright (C) 2005-2009  Masao Mutoh
  
  You may redistribute it and/or modify it under the same
  license terms as Ruby.

=end

$:.unshift File::join([File::dirname($0),"..","lib"])

begin
  require 'gettext/tools/rgettext'
rescue LoadError
  begin
    require 'rubygems'
    require 'gettext/tools/rgettext'
  rescue LoadError
    raise 'Ruby-GetText-Package are not installed.'
  end
end

GetText.rgettext

ドキュメントが微妙に古くて通用しない記述もいろいろありましたが、ちゃんと動いています。

m17n のためには、gettextの標準的な作法が使えるというのは強力だと思います。

2011/03/17

「不謹慎」を狩るのが流行っているみたい

プロがコンサートを開くといって「不謹慎」だと叩かれているような状況が報道されています。

そもそも「つつしみ」がない、不足している状態をみて、「それは不謹慎だ」などというわけですが、 「つつしみ」を内に秘めて行動した場合、あるいは言葉が足りていない場合には、それが相手に伝わらずに反発を招くことは十分に予想されます。

そういった場合には、返す刀で斬るのではなく、「誤解を与えたようですが、〜です。」などと冷静に対応したいものです。

インターネットや書籍など主に文字を使った表現では、相手の息遣いや表情、言葉の抑揚や体の動きといったコミュニケーションに必要な要素がかなり省かれた状態であることに常に留意するべきで、それがリテラシーを備えている状態といえるでしょう。

そもそも不謹慎な言葉が狩られている様をみて、厳しい生活を送っている人達は喜びもしないし、おもしろくもないと思います。

「不謹慎じゃないか」そういう指摘をする事は、自由に行なわれるべきです。 しかし、それを大義名分に刀を振るうのは間違いだと思います。

関西ウォーカーTVにみる「不謹慎」への反応

昨日、関西ウォーカーが震災時のボランティア活動について、専門家を招いて、この時期に何をするべきか、いつボランティアが必要になるのか、という話をしていました。

その放送の最後に「不謹慎」について言及し、関西方面のメディアが、かなり神経を使っている様子がみてとれます。

「買い占め」や関東方面への個人的な物資輸送は、物流を混乱させる要素になるので、被災地への一定の影響があるかもしれません。そういう行動は「つつしみ」を持てばいいのだと思います。

極端な例として、あまりに有名なアーティスト達がウッドストックみたいなコンサートを企画すれば、関東圏含めて大勢が移動することで輸送機関に影響があるかもしれません。

しかし「不謹慎」という理由は十分ではないように思えます。その機材の移動に使うバスを流用する事はできるかもしれませんが、多くの人が日常を取り戻して買い占めみたいな行動を抑制できるかもしれません。 これはバランスの問題です。

そんな風に個別に与える影響の可能性を考えつつ「つつしみ」を持って行動し、またその様子や経緯が読み聞きする相手に伝わることが必要なのでしょう。

けっきょく、みんな不安なだけ

そもそも文字ベースのコミュニケーションでは、思いが全て伝わらない事は、気がつくかどうか別にして、常に起っていることです。

「考える」という事が苦手な人達や、教条的な生活を良とする人達は、一律に派手な活動や言動を「不謹慎」という言葉で狩るという行動にでても、不思議ではありません。

そういった行動の背景に思いを馳せると、何かをしなきゃと不謹慎という印象を与えかねない発言をする人も、不謹慎を気にするアナウンサーも、不謹慎と言葉を狩る人達も、漠然とした不安感を抱えている様子が想像できます。

そして、その心の奥底にある気持ち、行動を引き起す衝動の源である不安感は、いま被災地にいる人達と共通の感情なのではないでしょうか。

非生産的な漠然とした不安感は解消するべき

もし自分が被災地周辺のお店に並んで、自分が買う事で後に続く人達が手にする物資が減っていく、そういった事を想像するのは被災地から遠く離れていても多くの人達が共有可能な感情なはずです。

ある側面からみれば良い/悪いと判断がつくかもしれませんが、何をするときも、感情を爆発させるのではなくて、抑制を効かせて「つつしみ」を持って行動する、それがいま求められていることだと思います。

「不謹慎」だという事象はきっと確かに存在するのでしょう。

けれどそれを指摘する人たち自身が「つつしみ」を持っているか、立ち止まって考えて欲しいと思います。

そういう抑制の効いた様をみて、あるいは後からその事実を知って、被災者の方々はきっとポジティブなメッセージを受け取るでしょう。

そして「不謹慎」にみえても日常を取り戻す事は不安感を解消するために必要なことなのです。 それが出来るところにいるのであれば、ぜひそうしてください。

2011/03/11

DB2 Express-C 9.7とRuby(IBM_DB Driver)を使ってみる

最近はNonSQL DBばかりに注力していたので、ひさしぶりにRDBMSに回帰してみました。

データは最近扱っているiptablesログか郵便番号か、どちらにしようかと思ったのですが、MongoDBでは郵便番号情報を扱っていたので、今回はRubyを使って郵便番号DBを作成しています。

さいしょに感想らしきものを一言

MongoDBとCouchDBの比較はいろいろありますが、DB2を使ってみて改めて感じるのはチューニングポイントが沢山あって使いこなすマニアックな喜びはありそうだという点です。

けれど、ACIDがなくてもOptimisticな処理で十分対応できる用途に対しては、RDBMSを導入する利点よりもメンテナンスコストが上回ってしまう気がします。

あと、DB2と関係ないですが、MongoDBが物理メモリをかなり消費する点も気になっています。 本体の消費メモリは少ないはずですが、memmapを使ってファイルにアクセスしているようです。

CouchDBが動いているErlangのbeamプロセスは負荷をかけてもだいたい30MB前後、MongoDBは物理メモリの搭載量にも依存するようですが、できるだけ空き領域をキャッシュとして使うようにみえます。

DB2は比較にならないほどのプロセス数とスレッド数とメモリを消費してくれますが、NODEディレクトリにある物理ファイルのサイズはMongoDBよりも小さいです。

もっともMongoDBは使うファイルを最初に領域を確保してしまいますから、db.stats()で表示されるdataSizeをみると純粋なデータサイズはかなり小さくですけどね。

今回は ibm_db ライブラリを使って、Ruby から DB2 CLI Driver を呼び出しています。

オフィシャルのibm_db API documentだけでは役に立たない模様で、DeveloperWorksの参考書を読まないと何がなんだかさっぱりでした。

環境の説明

今回は次のような環境で作業を行ないました。

  • CPU: PhenomII 940 X4
  • Memory: 8GB (FileCache: 約4GB)
  • Disks: 500GBx2 (Software RAID-1)
  • DB2 Express-C 9.7 (db2leve: DB2 v9.7.0.2)
  • Ruby: 1.9.2-p136
  • Ruby CLI Driver: IBM_DB 2.5.6

Ruby CLI Driverの導入

gemを使ってダウンロードしますが、gemsの仕組みは使わないので、ibm_dbのlibディレクトリだけコピーしてきます。

$ export IBM_DB_LIB=~/sqllib/lib
$ export IBM_DB_INCLUDE=~/sqllib/include
$ gem install --install-dir tmp_rubylib ibm_db
$ cp -r tmp_rubylib/gems/ibm_db-2.5.6/lib .

ここから先はコピーしたlibディレクトリのあるディレクトリで作業を行ないます。

DBの作成とテーブルの定義

Databaseの作成は最初にちょっとするだけなので、手動でやっておきます。

$ db2 create db postaldb using codeset UTF-8 territory en

しばらくはリモート接続をしないので、nodeの作成やらDB2COMMの設定などはしないでおきます。

次はテーブルを作成します。 分割はせずに一つの巨大なテーブルを作成しています。

POSTALテーブル作成スクリプト

#!/bin/bash

db2 'connect to postaldb'
db2 'DROP TABLE POSTAL'
db2 'CREATE TABLE POSTAL ( SERNUM INTEGER PRIMARY KEY NOT NULL, CITYID  INTEGER, PCODEOLD  CHAR(6), PCODE CHAR(8), PREFKANA  GRAPHIC(7), CITYKANA  GRAPHIC(25), STREETKANA  GRAPHIC(70), PREF  GRAPHIC(7), CITY  GRAPHIC(25), STREET  GRAPHIC(70), OP0  INTEGER, OP1  INTEGER, OP2  INTEGER, OP3  INTEGER, OP4  INTEGER, OP5  INTEGER )' 
db2 'terminate'

preparedステートメントを使ったデータのINSERT

Rubyを使ったINSERT文の使い方はドキュメントになくて、executeUpdateに相当するメソッドもないようなので、普通にexecuteメソッドを使いました。

ken_all.utf8.csvファイルをPOSTALテーブルにINSERTするRubyスクリプト (insert_csv_prepare.rb)

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-

require 'csv'

$:.unshift "lib"
require 'ibm_db'

conn = IBM_DB::connect("postaldb","","")
IBM_DB::autocommit(conn, IBM_DB::SQL_AUTOCOMMIT_ON)

sql = "INSERT INTO POSTAL (SERNUM,CITYID,PCODEOLD,PCODE,PREFKANA,CITYKANA,STREETKANA,PREF,CITY,STREET,OP0,OP1,OP2,OP3,OP4,OP5) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
pstmt = IBM_DB::prepare(conn,sql)
sernum = 1
opts={}
opts[:headers] = [:i,:pcode_old,:pcode,:pref_kana,:city_kana,:street_kana,:pref,:city,:street,:op0,:op1,:op2,:op3,:op4,:op5]
CSV.new(open("ken_all.utf8.csv"), opts).each do |row|
  IBM_DB::execute(pstmt, [sernum.to_i, row[:i].to_i,
                          row[:pcode_old], row[:pcode], row[:pref_kana],row[:city_kana],row[:street_kana],row[:pref],row[:city],row[:street],row[:op0].to_i,row[:op1].to_i,row[:op2].to_i,row[:op3].to_i,row[:op4].to_i,row[:op5].to_i])
  sernum += 1
end

IBM_DB::close(conn)

このスクリプトを準備するために、郵便局のWebサイトから郵便番号データをダウンロードしておきます。

$ unzip ken_all.zip
$ nkf -w ken_all.csv > ken_all.utf8.csv
$ ruby insert_csv_prepare.rb

約12万件(元データ約17MB)分のテーブルを作成するのに、2分30秒ほどかかりました。 Prepared statement(プリペアード・ステートメント)を使わずにSQLを毎回生成すると、7分30秒ほどでしたから、だいたい処理効率は3倍くらい違いがあります。

パフォーマンスについて

MongoDBでもDISTINCT(PREF),PREFKANAの結果を47行出力させてみましたが、MongoDBではPREFに対するINDEXがかなりパフォーマンスに寄与しました。

DB2でINDEXを作成せずにSQLを投げると、だいたいdb2start直後で接続時間を省いて3秒前後くらいです。 2回目以降はキャッシュが効くのか、0.2秒くらいになりました。

DB2のINDEXを使ってどうなるのか。 いろいろ謎なパラメータが沢山あるので、使い方によってはマッチしないんじゃないかなという心配がありました。

そんな理由で調査にはdb2advisが便利そうだったので、EXPLAIN表を作ってからSQLを実行して、DB2に最適なINDEXを考えさせました。

$ db2 connect to postaldb
$ db2 -tvf /opt/ibm/db2/V9.7.2/misc/EXPLAIN.DDL
$ db2advis -d postaldb -s "select distinct(pref),prefkana from postal"
-- LIST OF RECOMMENDED INDEXES
-- ===========================
-- index[1],    0.771MB
   CREATE INDEX "YASU    "."IDX1103110446310" ON "YASU    "."POSTAL"
   ("PREFKANA" ASC, "PREF" ASC) ALLOW REVERSE SCANS COLLECT SAMPLED DETAILED STATISTICS;
   COMMIT WORK ;

おなじことをしてみた結果は、予想どおり、この程度のデータ量ではあまり変化はなく、むしろDB2の接続にかかる時間が全体のパフォーマンスを低下させています。

RubyスクリプトでMongDBとDB2で同じような結果になるようにして時間を計測しましたが、MongoDBはdistinctしたPREFに対応するPREFKANA列を別に検索するカーソルの1ページ分の結果をprefetchするロジック的には効率は低いはずです。 また、結果はそれぞれ数回実行した後のものを載せています。

MongoDB | 2.182[s] (index無。接続時間含む)
MongoDB | 0.282[s] (index有。接続時間含む)
DB2     | 3.225[s] (index無。connect時間含)
DB2     | 1.751[s] (index有。connect時間含)
DB2     | 0.164[s] (index無。connect済)
DB2     | 0.121[s] (index有。connect済)

当たり前の結果ですが、DB2を使うならDBPoolingの仕組みは大切だということになりそうです。

SELECT文を発行するRubyスクリプトはprepared statementを利用する必要はありませんが、参考までにprepared state版を載せておきます。

SELECTを行なうSQL文 ibm_db driver/prepared statement版 (select_distinct_pref_pstmt.rb)

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-

$:.unshift "lib"
require 'ibm_db'

conn = IBM_DB::connect("postaldb","","")
IBM_DB::autocommit(conn, IBM_DB::SQL_AUTOCOMMIT_ON)

sql = "SELECT DISTINCT(PREF),PREFKANA FROM POSTAL"
pstmt = IBM_DB::prepare(conn,sql)
if IBM_DB::execute(pstmt, [])
  while row = IBM_DB::fetch_array(pstmt)
    puts "#{row[0].strip},#{row[1].strip}."
  end
end

IBM_DB::close(conn)

2011/03/04

YALTools: 手作業によるデータベースコピーの効率

CouchDBをメンテナンスするために作成したlscouchdbのパフォーマンスを考えてみました。

対象はDTI VPSにホスティングしているサーバー上のCouchDB 1.0.2です。

lscouchdbをコピーしてloopbackのCouchDBのポートに直接接続しているので、計測したデータの大部分はサーバのCPUとDisk I/Oのパフォーマンスに依存しています。

テストの概要

次のようなイディオムを使って、郵便番号DBに入っているView定義によるインデックス分を除く全データ(約220MB、約12万件)のコピーを作成しています。

作業手順

$ sbin/mkdb testp
$ time bin/lsdocs postal -u 15 | grep -v "_design/" | bin/postdocs -u 15 testp
$ sbin/rmdb testp

何回か-u 15の数字を増やして様子をみてみます。

PhenomII X4 940 (Mem: 8GB)での結果

物理メモリの半分ほどはファイルキャッシュに使われていて、今回のデータは十分この範囲に収まるようになっています。手元のPCを使った場合の結果は次のようになりました。

PhenomII X4 940での挙動: time bin/lsdocs postal -u 200 | grep -v _design | bin/postdocs testp -u 200


real	18m13.589s
user	1m3.160s
sys	0m8.400s

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 200 | grep -v _design | bin/postdocs testp -u 50


real	17m43.949s
user	1m7.320s
sys	0m8.520s

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 50 | grep -v _design | bin/postdocs testp -u 200


real	61m45.391s
user	1m12.870s
sys	0m12.770s

読み出し単位を4倍にして時間が1/4になっているので、読み出し回数は低く抑える方が良さそうです。

ちなみにDTI@VPSのエントリーレベル(256MB)で実行すると次のようになります。 Muninで観察している範囲では使用メモリは、この時間帯は180-190MBの範囲で遷移していました。

DTI@VPSでの挙動: time bin/lsdocs postal -u 220 | grep -v _design | bin/postdocs testp -u 100


real	22m51.496s
user	0m46.429s
sys	0m4.081s

ポイントは読み込み処理の効率化らしい

結局のところはデータを保存するパフォーマンスよりも、読み出しの単位を効率的なところにおさめるのが必要そうです。PhenomII X4 940 8GBで、データを取ってみました。

50〜3000件を一度に取得するようなコマンドラインの実行時間をtimeコマンドで取って、real時間をグラフにしてみました。

データ取得用のコマンドライン

$ i=50; time bin/lsdocs postal -u $i -p 1 > /dev/null

横軸がunitで、縦軸に処理時間をとったグラフ

/_all_docsの効率的な数字は環境によるはずですが、1秒辺りの処理件数は2000件で最高の836件になったので、2000前後にしておくのが良さそうです。

ここら辺を踏まえつつ、どうせ読み込み時間が線形に伸びるなら大きな数字にしてしまえと、やった結果が次のようになりました。

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 2000 | grep -v _design | bin/postdocs testp -u 200


real	5m4.372s
user	0m59.750s
sys	0m6.710s

書き込みの単位を増やしても、ほとんど影響がみられませんでした。

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 2000 | grep -v _design | bin/postdocs testp -u 2000


real	4m57.758s
user	0m54.100s
sys	0m6.500s

Proxy的な中間層がある場合の処理

注意しなければならないのは、今回の作業はバックエンドに直接接続しているということです。

これがstunnelなどを介している場合には、中間層のオーバーヘッドが問題になって時には処理が滞留することもあります。

Stunnelを使用した時の読み込み速度

読み出し側はだいたい10%程度のパフォーマンスダウンですが、書き込み時の単位を50件にすると処理は進みません。

手元の環境では25程度の書き込みにしないとCouchDBへの書き込みが発生しませんでした。

PhenomII X4 940での挙動:$ time bin/lsdocs postal -u 2000 -x stunnel.admin | grep -v _design | bin/postdocs testp -u 25 -x stunnel.admin


real	12m5.389s
user	1m36.620s
sys	0m7.260s