As Sloth As Possible

可能な限りナマケモノでありたい

カテゴリ: プログラミング

Rustで遊んでみよう第二弾、本当は並列処理を試してみたにする予定だったんだけど何故かHTTPパーサが出来てた。

経緯はこう。

「並列実行できるクローラ作ってください」「作るってどのレベルで?HTTPパーサから?」みたいなのがRust村みたいです。まぁ言うてまだアプリケーション層だ。TCP/IPからなんとかしましょうとか言われなくてよかった。一応C国からの輸入は許されてるけど、TLSとかならともかくHTTPくらいなら自力でなんとかできるでしょ、普段そんなの車輪の再発明だしやんないからいい機会だ、ってことでご用意しましたのがこちらになります

一応それらしいものにはなって、ステータスラインやリクエストラインの処理、ヘッダをフィールド名と値に分解、keep-aliveやupgradeの検出、chunkedなボディのデコード、くらいはできる。

ソース見ると一瞬うってなるけど、やってることはといえば、愚直に与えられたバイト列を1バイトずつチェックしてって適宜コールバックを呼ぶ、これだけ。TcpStreamには直接は触らないし、正規表現や文字列のパターンマッチはおろかcharへのキャストすらしてない。パースした結果を構造体に詰め直すとかはコールバック側で勝手にやってくださいのぶん投げ。

もっと綺麗に書けるのになんでこんなことになってるかというと、素直に書いてもつまらないので、crazyな速度と評判のCで書かれたhttp-parserを参考に速度最優先で作ったから。その甲斐あって400バイト強の普通のブラウザのGETリクエストをただパースするだけなら、手元の環境だと3~5μsくらいで 処理できるようになった(--opt-level=3でコンパイルしたとき)。

(試しに10万回ぶんまわすサンプルコード書いてみて、元のCのやつと比較してみたら、Cの方が550ms程度なのに対して俺が書いた方が330msで終わった。…なんで勝ってんだろ。多分なんかの間違いだろうなー。実装漏れしてるとか最適化でほぼ何もしなくなるコードになってるとかそんなとこだとは思う…。)

そんな感じで何もない荒野を耕しては雑草植えてますけどこれ結構楽しいです。んでよーやくHTTPクライアントに手を付け始めたけど、こっちの方は今任意のリクエスト投げて結果を表示するとこまではできた。keep-alive、リダイレクトも対応、Content-Typeにcharsetが付いてたらその文字コードでボディをデコードもできる。あとやりたいのは、POSTでフォームデータとかファイルとか送れるようにするのと、gzip/deflate対応と、TLSサポート(LibreSSLとSecure Transport選べるようにとかしたい)あたりか。まだ先長そうだなー。

なんでなのかと聞かれても「なんか暇だったから…」としか。

いや元々はこう周期的に来る「なんか仕事と全然関係ないコード書きたい」期だっただけなんだけど、Goとかで遊んでる最中にひょんなことからRustを見付けてしまってへー面白そうとか手を出してみることになり、「慣れない言語を使うときはとりあえずWhitespaceの処理系を作ってみるとなんか慣れたような気分になる」という癖でですね。なんか出来ちゃった。

記事書いてる時点の最新のnightly(v0.12.0-pre)ではコンパイル通るのを確認済み(というか多分0.12.0でしかビルドできない。流石いつβが出るとも分からないα言語、0.10.0と0.11.0と0.12.0で互換性無いし、ドキュメント見に行ったら標準ライブラリを含むほとんどのAPIがexperimentalだった)。rust-albinoの方をcloneしたら最新のrustcと対応するcargoをrustupかなんかで入れて、

cargo build
するとビルドできる。homebrewでも入れられるけどバイナリだと0.11.0、ソースからのビルドだとかなり時間かかるので大人しくnightlyのバイナリ使うがよろし。

Albinoがコマンドラインツールで、こいつがWhitespaceを実行したり逆アセンブルしたりしてくれる。最初は普通にインタプリタ作ってたんだけど、例のごとくWhitespace読めないし書けないからアセンブリ言語でもでっちあげよう、あとその昔書いたDTとかも動かしたいよねー、とかやってたらいつの間にかVMとパーサが分離して副産物として何故かライブラリになっちゃったのがWhitebase

厳密に言うと本体はWhitebaceの方でAlbinoはただのインターフェースだったりするけど細かいことは気にしない。先に名前が決まったのはWhitebaseの方で、Whitespaceの命令セットを持ったVMとパーサのライブラリなので「白地」というのが由来であって決して某19歳に見えない艦長率いる連邦の戦艦ではない。合わせてツールの方はWhitedevilにしようかなーとか一瞬考えたけどコマンド名としては長すぎるしrx78とかエイリアスするはめになるのでアルビノにした。VMとパーサが分離した結果どういうわけかBrainf*ck派生の言語をWhitespaceの命令セットに変換できるようになったので、「本来白くないものまで白くなっている」からアルビノなんです。

得られたもの。Rustがなかなか触っていて楽しいということが分かった。いまどきっぽいサブコマンド型のコマンドどうやって作ってるのかなーってのが分かった。WhitespaceもしくはBrainf*ckのトークンの内部表現を返すイテレータさえ実装できれば簡単にオレオレ言語が作れるライブラリが出来たので、今後さっと言語を作ってドヤリングしたいときは一瞬で出来るようになった。Brainf*ckの任意のプログラムからWhitespaceのコードが生成できるようになった。

失なったもの。時間。結構な無駄な時間。

珍しく趣味で書き始めたやつがそれなりにそれっぽいものになったので公開してみる。

パートカラーってのは、モノクロ写真なんだけど一部分だけ色がついてるやつ。和製英語っぽい気はする。英語だとselective colorで検索するとそれっぽい画像が出てくるから多分これなのかな。あれを例えば赤だけ残してとか指定したら自動で作ってくれるようなのが、デジカメのフィルタとかには入ってたりするんだけど、あれどうやってやってんだろと思って試しに作ってみた。

こういう元画像があったりして、

かくれんぼ

これに対して「色相0度を基準に60度の範囲の色を残してあとは彩度0にする(パートカラー赤)」ってのをやってあげると、

lory-out

こんな感じになる。実用性があるかどうかはともかく意外とそれっぽくなった。

とりあえずライブラリとツールっぽい何かが出来たのでGitHubに置いてある。

faultier/lory

できることは、

  1. alpha, red, green, blueからなる色構造体と基準になる色相を渡してやると、色相を判別して残すか彩度0にするかする
  2. jpeglibを使うのを想定して、RGBRGB...の順に並んでるuint8_tの配列を渡してやると、それ全体に対して1の処理をする
  3. Android NDKで使うのを想定して、uint32_tの配列と幅、高さ、ストライドを渡してやると、それ全体に対して1の処理をする

みたいな感じ。一応Android用のstatic library作るとこまではやったのでせっかくだしアプリ作ってみるかなーとか思ってるんだけどいつになるやら。あとはまぁjpegだけじゃなくて色んなフォーマットで試してみるかーとか高速化とか考えたらどうなるかなーとかでしばらくは遊べそう。

これ作ってるときにサンプル画像にインコの画像(あいつら体色がカラフルなのでこういう時に便利)を使ってたので、なんか良い名前ないかなー、あ、こういう小さいインコはLoryとLorikeetっていうのか、じゃあそれで、ってプロジェクト名にしたら、ツールの名前がloryconvに、loryconvによって出力される画像のデフォルト名がlory-out.jpgになってなんかこう、あれだ、おまわりさんこの人です的な語感になってしまったんだけど全然そんなつもりはなかったんです。ほんとなんです。

カメラを買って以来すっかり写真を撮るのに夢中になってて、その上最近はリスなんか飼っちゃったりしてて、んまーめっきり週末プログラマーしてなかったもんで、しまったこっちのブログ全然書いてないやーと思って久々に見たら最後の記事が去年の8月26日とかになってた。これはひどい。少しは書かないと。

久々の更新なのに大分周回遅れ感のある話題だけど、Androidアプリ内でFlashを表示するときに面倒な感じだったのでメモ。Flash PlayerがPlay Storeから消えてから大分経ち、時代はAndroid 4系だと言うのに今更Flashかよーって自分でも思ってるしなんならこのメモ二度と見返すことが無ければいいなーと心から願いながら書く。あれなんです。世の中には大人の事情が一杯あるんです。2系とか爆発しろって思ってるけど俺の端末はGalaxy SII LTE 2.3.6です。俺も爆発しろ。

Androidアプリ内でFlashを表示するには単純にWebViewを開いてswfなりswfをロードするタグが書いてあるhtmlなりをloadUrlすればいい。ただしデフォルトだとプラグインが無効になっているので、WebSettingsを弄っておいてやる。

package jp.faultier.sample;

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;

public class FlashActivity extends Activity {
    private WebView webView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.flash);

        String url ="file:///android_asset/hoge.html"; // Flashを表示するタグが書いてあるHTML

        webView = (WebView) findViewById(R.id.webView);
        webView.getSettings().setPluginsEnabled(true);
        webView.loadUrl(url);
    }
}

とりあえずこれで望み通りassetsに放り込んだhoge.htmlが表示され、その中で指定されたswfが読み込まれる。ところがここで罠にハマる。このままだとFlashは止まらなくなる。

例えばあなたがFlash黄金時代のFlashを蒐集し、密かに通勤中に眺めるのが趣味だったとしよう。もしそれが「さくらんぼキッス 〜爆発だも〜ん〜」をBGMに1さんと8頭身が舞い踊るやつだったらこっそり俺に送って欲しい。たぶん違うと思うけど。

で、にやにやしながら心の中で「あれれ?おかしいなこのドーキードキーはー 君の腕の中であふーれだすー」と歌っていたあなたは、たまたま電車を降りたところで上司にばったり出くわすかもしれない。慣れた手付きで電源ボタンを押してポケットにAndroidをしまうだろう。そして上司にあいさつをするだろう。でもその最中もイヤホンからは「すきすきすき すーきすきすき すきすきすき すーきすきすき(キュンキュン!)」とか流れっぱなしなのだ。ついでにいい加減暑くなってきてるというのに、ポケットの中には今にも発火せん勢いで発熱しているホッカイロが出来上がっている。なんなんだその罰ゲームは。

これを避けるには、onPauseのタイミングでWebViewの動作も止めておいてやらないといけない。そんくらいなんでやっといてくんないのと思うんだけど。

public class FlashActivity extends Activity {
    private WebView webView;

    ...

    protected void onResume() {
        super.onResume();
        try {
            WebView.class.getMethod("onResume").invoke(webView);
        } catch (...) {
            ...
        }
    }

    @Override
    protected void onPause() {
        try {
            WebView.class.getMethod("onPause").invoke(webView);
        } catch (...) {
            ...
        }
        super.onPause();
    }
}

やっと止まった。これなら上司とばったり出会っても、上司が昔Fla板に常駐してたかどうかを気にしないで済む。ちなみにリフレクションで呼んでるのはこのAPIが公式に実装されてるのはAPI Level11、つまり3.0以降なので、2.2や2.3では必ずしも実行できないから。とは言えGalaxy SIIでは一応これで止まった。めでたしめでたしだ。

さて安心して上司と雑談してるあなただが、途中で困ったことに「さっきスマホで何見てたの?」という話になる。また面倒な、と思いながらも、光速の異名を持ち重力を自在に操る高貴なるAndroid騎士のあなたはロック解除からコンマ数秒でアプリを終了させれば良いだろうとポケットから出し、電源ボタンを押す。するとどうだろう、ロック画面が表示されているというのに、端末からは「(ほんとはね…ずっと好きだったの…ナイショだよ…)」とか囁きが聞こえてくる。おかしいなー、ビルの中は空調効いてるのになんかすっごい汗出てきたなー。うふふそうかー、これ冷や汗って言うんだー。

はい。どうしてそんなふざけた実装になってるのかは知らないけども、Androidはロック画面が表示された時点で既に最前面のActivityのonResumeが呼ばれてしまっているので、上記のWebViewへのonResumeも走り、Flashは再び走り出してしまっている。落ち着いて聞いて欲しいのだけど、この挙動はAPI Level16、4.1になるまで直ってない。Androidさん的にはつい最近まで「スリープ状態からのアプリの再開=ロック画面の表示」だった。うん、本当に意味が分からない。

これを避けるには、onResumeじゃなくてonWindowFocusChangesを使うように変える。ロック画面表示した時点ではActivityはresumeしているが、Activityのwindowは表示されていないので、フォーカスしていない。ついでに、逆にフォーカスが外れたら画面が見えてないってことだよね、と考えれば、このメソッドだけで完結しそうだ、ということでonPauseの分もこっちに持ってってみよう。

public class FlashActivity extends Activity {
    private WebView webView;

    ... // さっき追加したonResumeとonPauseの処理は削除

    @Override
    public void onWindowFocusChanges(boolean hasFocus) {
        if (hasFocus) {
            try {
                WebView.class.getMethod("onResume").invoke(webView);
            } catch (...) {
                ...
            }
        } else {
            try {
                WebView.class.getMethod("onPause").invoke(webView);
            } catch (...) {
                ...
            }
        }
        super.onWindowFocusChanges(hasFocus);
    }
}

どうやら今度こそうまく行ったようだ。電源ボタンを押せば音声は止まるし、ロック画面表示中も止まっているようだけど、ロック解除すればちゃんとまた動き出す。完璧だ。

そう思いながらの帰り道、懲りずに蒐集したFlashたちを眺めながら乗る電車は満員の埼京線、屠畜場に行く肉牛の方がもう少し人道的な扱いを受けるんじゃないかなー?うん?人道じゃなくて牛道?とか考えてる最中、さっきまで狂ったように連呼されていた「みこみこナース!みこみこナース!」の声が聞こえないことに気付く。見ると、画面ではフルスクリーン表示されているFlash。そこまではいい。多分、車内で圧縮されてるときにうっかり画面を長押ししてしまったのだろう。AndroidではFlashを長押しすることでフルスクリーンモードに移行することができる。のだけど、明らかに再生は止まっているし、押してもつまんでも舐めても全く反応してくれない。一体何が起きたんだ。

これは一個前の変更で、onWindowFocusChangesでwebViewのonPauseを呼ぶようにしてしまったせいだ。残念なことに「通常表示からフルスクリーンモードへの移行」も「WebViewを表示しているActivityのWindowのフォーカスが外れた」ことになるので、onPauseが走り、一切の動きが停止する。ちなみにバックボタンで戻るとまたフォーカスするのでonResumeが走り、何事も無かったかのように再生が再開する。

折角フルスクリーンにできるというのにこれは大変がっかりな事態である。結局、一部を元に戻して、最終的には次のようにする。

  • onPauseでwebViewのonPauseを呼ぶ
  • onWindowFocusChangesで、hasFocusがtrueの場合のみ、webViewのonResumeを呼ぶ

これで一応、電源ボタン押したりホームボタン押したりすると再生が停まるし、ロック画面を表示しても勝手に走り出さず、ロックを解除すると再開し、フルスクリーンモードもちゃんと動く、という動きになる。なんかえらく回り道した。途中の懐しFlashのくだりとか全く要らんかった。懐し過ぎてなつみSTEPとかnum1000とか観てしまった。全然関係ない話である。

とりあえず、Androidの2系速やかにシェアが下がるといいなぁ。なんなら今2系の端末はボタンひとつで一斉に爆発するといいなぁ。俺のも爆発四散するけどこの際それでいいから消えてなくなってくれないかなぁ。

これで万事解決や、と思ったんだけど、フルスクリーンモードだと一旦閉じて復帰してもonWindowFocusChangesが呼ばれない(だってActivityの方は裏に隠れたままだから)ので、Flashの再生が再開しない。結局「ロック画面出てるのに再生が再開しちゃう」のと「画面に戻ってきてもフルスクリーンを解除するまで一切反応しない」のだと後者のが深刻なので最初のonResumeのとこまで戻した。はー。

完全に余談だけどブラウザもこの問題解決できてないようで、デフォルトブラウザとBoat BrowserはonResumeで、Opera Mobile ClassicはonWindowFocusChangesでやってるらしい挙動だなってのが分かった。Operaに親近感を感じる。

ゴールデンウィークを利用して久々にろくでもないものができたので晒しておく。

「ごっこじゃないよ、兄ちゃん!」「才能の無駄遣いじゃなくて、無駄そのものだよ、お兄ちゃん。」

前のと何が違うの?

そもそもの発端についてはプログラミング言語「DT」という記事を、実際の言語仕様についてはREADMEでも読んでもらうとして、何で再実装したのかという話。

3年前に作ったのは「Whitespaceのトークンを置換した処理系をインタプリタとしてRubyで実装」したものなんだけど、何を思ったかコンパイルしてみたいと思い立ってしまい、んでそれ自体はLLVMが良く分かんなかったとか、llvmruby自体がプロジェクトとして大分アレな感じになってしまっていたとか、そういう事情で挫折してたんだけど、時は流れ、なんか見返してみたら普通にLLVMアセンブリ読み書きできるようになってたし、ruby-llvmがちゃんと動くようになってるみたいだし、ということで再実装してみることにした。

そんで作り直す過程で「Whitespaceの置換ってのもなんだか面白くないな」と思ってVMのアーキテクチャと言語仕様を見直すことにした(ぱっと見だと全然分かんないけど、旧DTと新DTの間に互換性は無い)。そのせいで実際にはチューリング完全じゃなくなってると思うし、そもそもちゃんと出来てるのかどうか怪しいものだけど、とりあえず「Hi!」って出力するのとフィボナッチ数を出すのは動いたので良しとする。

当初の目標だった「DTコードをネイティブのバイナリにまでコンパイルして高速に実行」ってのも実現して、DTインタプリタで250msくらいかかるfib.dtをコンパイルすると4msくらいで結果が出るようになった。似たようなRubyのコードだと大体100msくらいだったので素敵に速い。まぁ、コンパイル自体はLLVMとコンパイラに任せているので大して威張れるあれでもないのだけども。そして高速に動いたからなんだという話なのだけども。

ぶっちゃけDTよりLLVMアセンブリの方が遥かに記述力高いし書きやすい。当たり前のように構造体とか関数ポインタとか使えるのな。Cの関数呼べるし。最初Cで書いてたVMを途中でLLVMアセンブリ直接書くようにしたけど、大して変わらない。DTで書くくらいならLLVMアセンブリ書いた方がいいです。LLVMの無駄遣いタグを付けていただきたい。

DTの存在意義については疑問を差し挟む余地もなく皆無だと言っていいけど、その過程でレジスタマシンについて色々調べて、はー、こんぴゅーたーってこうやってうごいてるのかー、と勉強になったのでそれはそれで良かった。よくこんなの考えられるよなぁ。CPUとかコンパイラとか作ってる人達、本当に俺と同じ種類の生き物なのだろうか。凄い。

随分長いことブログ放置してしまったのだけどネタ見付けたので久々の記事。

UnicornはPassengerより遅かった?

なんかTwitterで「アクセス少ないときはPassengerよりUnicornのが速いのに、アクセス多くなってきたらその逆になった」って話をみかけたので、それ単にUnicornのworkerが足りないんじゃないの、と返したのだけど、どういうことかという話を少しまとめる。

まず、Unicornのworkerは1プロセスにつき1度に1リクエストしか処理しない。だから例えば、凄い大雑把な計算だけど、平均50msくらいでレスポンスを返すアプリだとすれば、1workerは20req/secくらいは返せるかなと見積もって、ピーク時に100req/secくらいアクセスがありそうだったらworkerを5個くらい立てとくかな、足んなかったらもうちょっとかな、みたいに考える。実際どんくらいのアクセスなのかは聞いてないので知らないけど、Nginxが10workerいるのにUnicornが2workerだって言ってたから、普通逆じゃない、とつっこんだ。Nginxは逆に1workerで複数リクエスト捌ける(数千くらい同時にアクセスされても耐えられるらしい)のでCPUのコア数と同じかちょっと多いくらいでよくて、Unicornのworkerの方をガンガン立ち上げるべき。

Passengerもそれ自体は同じなんだけど、Passengerの場合負荷上がってきたら空気読んで設定した上限までプロセス数を増やしてくれるので、「アクセス増えても強い」のではなくて「アクセス増えたらいつの間にか仲間呼んで倍の人数で戦ってた」とかそういうことじゃないかと思う。上記逆転が起こるのは「Unicornのworkerが少ない」「リクエスト数が増えても1個1個の処理時間は大して変わらない」「メモリやCPUには余裕がある」って条件のときだと思うので、多分Unicornのworker数を適切な数に調整すれば解決する。

workerの数増やしたらメモリ食うんじゃ、って言う話なら、Unicornはworkerプロセスを立ち上げる前にアプリをpreloadする仕組みがある。アプリ初期化時点で必要なライブラリを全部読み込んであれば、workerプロセスの起動は単なるforkなので、CopyOnWriteで親プロセスとメモリを共有して効率的に使ってくれそうな気配はある。それでも食うっちゃ食うけども、worker一杯立ち上げたらメモリ足んなくなるならPassengerでも同じ話。平常時には節約してくれるけども、裏を返せば急激なアクセス増大に際しては慌ててプロセスを沢山作るコストで負荷上がることもあるし、単体で比較するとUnicornのがまぁ普通に強い。大量アクセスがある程度定期的に来るの分かってるならUnicornで、滅多にないならお手軽なPassengerで、みたいな使い分け。

Rainbows!って何

ここまでの話はあくまで、1個1個のリクエストは大して重くないけど一度に沢山のリクエストが来ちゃったら遅くなった、みたいな場合。例えばさっきと違って、平均で見れば100msくらいなんだけど、20回に1回くらいの割合で1500msくらいかかるリクエストがくることがある、とする。それがたまたま5人同時にリクエストしてしまったら、5workerがそれぞれ1.5秒ずつ拘束されてその間一切レスポンスを返せなくなる。内部ではファイルサーバにデカいファイル書いてるとか、DBに重いクエリ投げてるとか、API叩いて外部からデータ取って来てるとかで、実際にはアプリ自体は待ってるだけでCPUもメモリもスッカスカ、みたいな状態であっても。それ以外のリクエストは30msくらいで即答できるのだとしても。

Rainbows!の場合はUnicornをベースにしているけれども、EventMachineとかRevacatorみたいなイベント駆動ライブラリを使って1workerで並列にリクエストを処理できるようにしている。さっきみたいに長時間待ちになるリクエストが来てても、待ってる間に他の数十ms程度で返せるリクエストを受け取って処理して返せる。1プロセス1リクエストの枷が外れるので素のUnicornよりはworker数減らせてコンパクトになるかもしれない。そしてイベントループの中でアプリが動くのでAsyncSinatraみたいなフレームワークを使ってアプリ内で非同期処理を書ける。なので重いリクエストがある場合はこっちを使うといい、って話になる。

なんだ良いことづくめじゃん、Unicornいらない子じゃん、ってなるかと思えばそうでもない。Unicornがそもそもpreforkで1プロセス1リクエストでみたいな、ひどく割り切った、見ようによっては割と古典的な仕組みになってるのは、先行していたイベント駆動型のThinみたいなアプリケーションサーバに対しての「シンプルな方が作るのも管理も楽だし、なんだかんだでパフォーマンスも出るじゃん」みたいなアンチテーゼ的なところだったりするので、わざわざ複雑さを戻してるRainbows!はそういうメリットとのトレードオフになる。使いこなすと素敵な感じにはなるけど、worker増やせば解決する程度の話なら素のUnicornでも大して困らない。

追記

「Unicornシグナル送ればプロセス増やしたり減らしたりできるから、netstatとか見て詰まってそうだったら自動で増えるようにしてるー。滅多に増えないけど」っていう情報を貰った。TTINでプロセス増やす、TTOUでプロセス減らすとかできるので、自動で増減して欲しければなんか適当に監視スクリプト書いて適宜シグナル送るようにしとけばいいかも。まぁ手で送ってもいいし。

どうも、「iOS Advent Calendar 2011」5日目担当のfaultierです。つい最近使ったのでNSURLProtocolネタで。

NSURLProtocolって何?

Foundationフレームワークで最初から扱えるプロトコルはhttp、https、ftp、fileの4つ。これ以外のプロトコルでの通信をNSURLConnectionやNSURLDownloadなどで扱う場合や、特定のリクエストに限って特別な処理をしたい場合などに、NSURLProtocolを継承して登録することで使えるようになる。ちなみに、他のアプリからopenURLしたときにアプリを起動させるカスタムURLスキームとはまた別なので注意。こちらはアプリ内でURL Loading Systemを使うときにだけ影響するもの。

使い方

最低限必要なのは、+canInitWithRequest:、+canonicalRequestForRequest:、-startLoading、-stopLoadingの4つ。

まずはこんな感じで、どんなリクエストのときにそのURLProtocolをが処理するのかを決める。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    return [[[request URL] scheme] isEqualToString:@"udon"];
}

ここでYESを返すとこのクラスがインスタンス化されて通信処理に進む。NOの場合は他に登録されているURLProtocolのこのメソッドが登録時の逆順に呼ばれて行く。この場合はudonというスキーム、例えばudon://marukame/bukkake/coolみたいなURLへのリクエストの時に処理をすることになる。別にこれは独自のスキームである必要ではなく、httpやfileなどをフックすることもできるし、特定のホストや特殊なヘッダが付いている時だけ処理するようなこともできる。

次は+canonicalRequestForRequest:。リクエストをcanonicalな形に変える必要がある場合はここで弄るのだけど、特に何もすることが無ければrequestをそのまま返してやればいい。

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

その後実際の通信が始まり、-startLoadingが呼ばれる。NSURLProtocolのオブジェクトは、id<NSURLProtocolClient>のclientと、NSURLRequestのrequestというプロパティを持っているので、requestからどんなリソースが必要なのかを判断して、clientに対してデータを返してやる、という形で通信の中継をする。NSURLProtocolClientのメソッドは大体NSURLConnectionのdelegateと対応しているので、NSURLConnectionでの通信を実装したことがあれば分かるはず。NSURLConnection側でキャンセルされたときはstopLoadingが呼ばれる。簡単な例としては以下のようになる。

- (void)startLoading
{
    // 本来非同期で通信するのだけど、
    // この例では単に文字列データを返すだけなので、
    // その場で返してしまう
    NSData *data = [@"うどんが食べたい" dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys:
                              @"text/plain", @"Content-Type",
                              [NSString stringWithFormat:@"%d", [data length]], @"Content-Length",
                              nil];
    NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[self.request URL]
                                                              statusCode:200
                                                             HTTPVersion:@"1.1"
                                                            headerFields:headers];
    // NSURLResponseのオブジェクトを返し、
    [self.client URLProtocol:self
          didReceiveResponse:response
          cacheStoragePolicy:NSURLCacheStorageAllowedInMemoryOnly];
    // データを渡し、
    [self.client URLProtocol:self didLoadData:data];
    // 通信を終了。
    // 通信失敗の場合は -URLProtocol:didFailWithError:を呼ぶ。
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)stopLoading
{
    // この例ではキャンセルしようが無い…
    // 内部で別なNSURLConnectionを使っていたり、
    // NSOperationQueueやGCDなどで非同期処理をしている場合、
    // ここでキャンセルの処理を実行する。
}

これだけだとudonスキームはまだURL Loading Systemに登録されていないので、適宜NSURLProtocolの+registerClass:、+unregisterClass:呼んであげることで使えるようになる。その独自スキームを使うUIViewControllerとか、あるいはもうクラスのloadメソッドで登録してしまうとか。

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSURLProtocol registerClass:[self class]];
    });
}

使いどころ

それどういうときに使うの?http以外のプロトコルとかそんな実装しないよ?と思うかもしれないけど、意外と使い道がある。UIWebView等での通信は裏側でNSURLConnectionを使っているので、WebView内での通信をフックしてアプリ側で弄ったりできるのだ。例えばCoreDataにあるデータをJSON形式にして返すようにしておくとか、JSからのトリガーでアプリ内のバックグラウンド処理を走らせるとか、画像をファイルキャッシュしておいてオフラインでも表示するとか、複数の異なる形式のWebAPIをプロキシして同じ形で扱えるようにしたり、ということも簡単にできる。

また、逆に「特定のリクエストに限って特別な処理をする」ではなく「特定のリクエストはスルーするけど他はブロックする」のような使い方もできる。最近(というには結構前からだけど)話題になったのは、iPhoneのWebViewにこういう脆弱性がある、という話。

何故デバイスのアドレス帳や着信履歴みたいなのの生のデータにJSからアクセスできる必要があるのか良く分からないし、おかしな仕様だと思うのでバージョンアップで塞がれるだろうとは思うけど、少なくともiOS5.0まではこの問題は残っているので、「アプリ内でUIWebViewにloadHTMLStringさせて、外部から取得したHTMLをfile://等のスキームで表示させている」ような場合には対処する必要がある。簡単な対策としてはきちんとbaseURLを設定しておけばいいのだけど、そうすると今度はCSS、JS、画像などのバンドル内部に持っているリソースが使えなくなる。「外部から取得したHTMLをアプリ内のUIWebViewで表示したいけど、アプリ内の安全なリソースにはアクセスさせたい」場合に、NSURLProtocolで通信をフックする仕組みが使える。

前者の記事だと、NSURLProtocolのサブクラスでバンドル内のファイルを開いて、そのデータを返してしまう、というやり方をしている。また、後者の記事やPhoneGapの実装では、NSURLProtocolの「登録時の逆順に対応できるかどうかチェックする」「YESを返せばそこで止まり、NOを返すと次のNSURLProtocolのチェックに進む」という性質を利用して、「ホワイトリストに載ってないリクエストの場合は、canInitWithRequest:でYESを返した上でエラーなり空レスポンスなりを返し、ホワイトリストに載っている場合はNOを返して通常のhttpやfileプロトコルとして処理させる」という方法でサンドボックス外のリソースにアクセスさせないようにしている。

まとめ

  • NSURLProtocolを使えばさくっと独自スキームを定義できるよ
  • WebViewみたいに細かく制御しづらいものの内部の通信もフックできるよ
  • セキュリティ面でも意味があるよ

という感じなので、是非試してみてください。

ふと思い立ってA successful Git branching modelとかを今頃読んでみたり。

最近のお仕事、基本的に一人でiPhone/Androidアプリを作ってて同じコードを複数人で弄ることがなくて、しかも大体の案件が一月くらいかけて一から作るとか丸ごと作り直すとかそういう感じなので、割と開発プロセスとか考えずにフリーダムにやっちゃってるので、まぁあまりよろしくないなと反省をしている。かといってgitの機能を隅から隅まで使いこなしてるとか自分にとって最高にやりやすい状況になってるわけでもなく、むしろあんまり開発サイクルが上手く回ってなくて、でも人にすぐ渡せる状態かというとそうでもなく、というわけで意識的になんかの約束事に乗ってみることにした。

で。ついでにgitconfigいじってたりああそう言えばと思い出してzshrcいじったりしてたら日が暮れてた。開発環境整備すんのなんであんな楽しいんだろな。やりすぎ良くない。

なんか金縛りにあって眠れなくなったのでTwitter見てたら面白いやりとりをみかけた。すげー大雑把に要約すると、こんな感じ。

  1. Railsの勉強をしてる人が「なんでHello worldしたいだけなのにDB必要なの!?DB使うにしたってMySQLなんか今必要じゃないしチュートリアルではSQLiteとかにしといてくれたっていいのに!プリプリ!」みたいな呟きをする
  2. 「いやRails使うんなら普通DB要るでしょ。なんでそこに文句言ってるの。」とツッコミが入る
  3. 「とりあえずチュートリアルは一番簡単なやり方書いといてくれた方が親切じゃないですか。」と反論
  4. 「いやだから、Railsを使うのに一番当たり前の構成を作るのにはどうしたらいいかがチュートリアルでしょ。それが必要ないんなら別なの、例えばSinatraとか使えばいいじゃん。フレームワークの特性も分からずに使っておいてチュートリアルがおかしいとか筋違いだろ。」と再度ツッコミ
  5. 「Railsがどんな特性のフレームワークとか知らないから勉強してるのに…最初っからそういうの全部考えてやれって言われても…僕はただRailsを手っ取り早く学ぶ手段があればいいのにって思っただけなのに…」みたいに凹んでしまう

ってな感じ。んで、それと隣接して外野で「最初は知らないことだらけなんだからおまじないだって言っておけばいいじゃん、フレームワークの特性がーとか大鑑巨砲主義がーとかどうでもいいだろ、やってみなきゃ分かんないことだってあんじゃん」「いや不要なとこに不要なもの使おうとしてるから適切なのはこっちだって教えてやっただけだろ、なんでわざわざ面倒なことやろうとしてるのにフレームワークに文句付けてんだよ」みたいな論争が若干生じてたりとか。

なんちゅーかね。この件に関して言えば、「それRailsに文句付けるとこじゃねーよ」って指摘は妥当だとは思う。Railsは「中規模のアプリケーションを、一般的な構成で」作るときに楽になるように設計されてるものなので、それを知らないまま進むと大概後で苦労する(レールに乗るから楽なんであって、レールから外れると荒野だ。今は大分良いけど、昔は黒魔術に手を染めないとレールから外れることすらできなかったんだよ!)。

そんなの知らないから勉強してるんだろ、それは後から考えさせろって?いや、WAFの選択って、「どんな目的に何を使うか」も重要なことなので、何かWAFを学ぶってときにはそういう選択の力も付ける必要はあると思うよ。WAF一般について知りたいんじゃないんだ、Railsについて勉強してるんだって?だったらとりあえずRailsの流儀に沿っておきなよ、何か意味があるからそういう構成になってるんだよ。分からないなら文句付けてないで、何でそうなってるか考えようよ。

少しまとめる。

  • 何かにとりかかろうとして、チュートリアルの時点で「なんでこんなことせにゃならんのだ?」って思うなら、「自分がやろうとしてることに適切じゃないものを使おうとしている」か、「今取り組んでいることを正しく理解できてない」可能性を先に疑った方がいい。大概の場合は「あー俺が間違えてたわー」ってなるか「あーこういうことだったのかー」って後で分かる。まぁ本当に提供側が不親切だったりいい加減だったりすることもままあるけど、それにつっこめるのはある程度使い熟せてからだ。
  • 教える側は「これはおまじないだから、そういうことだって思ってやりなさい」を安易に使うべきじゃない。何にか躓いてるときは、何が間違ってて何が理解できてないのか把握する・させるチャンスなので、そのタイミングで教えてあげればいいのにと思う。あんまり深入りさせてなかなか最初のステップに到達しないのはアレだけど、「おまじないだからとりあえず無視して」って言っちゃうとほんの少し潜ってみる足掛かりすら失なう。
  • でも、分からないことを責めるな、とは思う。「うぇぶかいはつしゃもすなるれいるずをしてみんとてするなり」みたいな人に「お前はゴキブリ退治に第七艦隊を呼ぶのか?」みたいなツッコミ入れたってそもそも分かってなかったからそんなことしてるんでしょと。単に教えてあげればいいじゃない。

どうでもいいけどRailsは「最初の一歩」には丸っきり向いてないと思うんだけど、なんでみんなRailsやりたがるし教えたがるんだろね。不思議。

あるいは、お遊びチーム2号は一体何をしていたのかについて

ISUCONという大変白熱した楽しいお祭を開催するにあたって、その前夜祭的な環境試験のためのチューニング祭が社内の有志数名で行われていて、そのときに色々学んだことをおまけとして書いておきます。

ISUCONて何?

下記参照。

要するに、「閲覧者視点での振る舞いさえ満たしてくれれば何をしようと構わんからWebアプリのレスポンスを改善しなさい」というお題で、誰が一番速くできるか競うイベント。

最初にある程度環境が整備されてるサーバ4台と、主催者側が用意した参考実装のアプリとテスト用データが渡される。このアプリってのはごくシンプルな個人ブログだと思ってもらえば良くて、最新記事10件が表示されるインデックスページと、記事全文が見られる詳細ページがあり、記事の投稿とコメントの投稿ができて即時反映される。全てのページには「最近コメントが付いた記事10件」が表示されるサイドバーがあり、ヘッダ画像、スタイルシート、JavaScriptが読み込まれる。テストデータにはそれなりに大量の記事データとコメントデータが入っている。フロントのWebサーバはApache、DBはMySQL、アプリはPerlとRubyとNode.jsのものが用意されている。

「お遊び組」って?

ISUCONは見学席が用意されていたのだけど、参加者以外はぶっちゃけ暇なので、空いたサーバ1チーム分を使って好きに遊んでいいことになっていた。で。そのお遊び用サーバで、事前に社内βで散々いじくり倒した俺とhidedenさんが空気読まずに自分のアプリで参加者達に対抗する、という大人気ないことをしていた。(すいません…)

一応ちゃんと言っておくと、中の人なので裏ルールや罠の存在についてある程度聞いていた上、6時間どころか2日3日費して試行錯誤していて、加えてhidedenさんに圧倒的な差を付けられた時は教えを乞うなどしていて、完全なるチートです。正直あの短時間でここまでやってくるって本当凄いな参加者のみなさん、と感動しっぱなしでした。勝手に熱くなって最後の方はムキになってたのは内緒です。

※ あ、でもでも、参加者の条件と対等じゃない「強くてニューゲーム」だった(会場に着くやいなや会社に行ってソース取って来た)という意味での「チート」で、コンテスト向けの実運用ではありえない実装にするとかはしてないです。なるべく突飛なことをせず普通にWAFを使い普通に配置するように心掛けてます。サーバのセットアップとかは手伝って貰ってるけど、アプリの実装は一人でやってるし、戦略そのものも自分で考えてます。

というわけで以下は俺のアプリで実際にやったこと。お遊び組ベストスコアを出した方とは別のアプリです。ちなみに使用言語はRuby。隣に座ったCTOと向かいに座った部長に「うちPerlの会社だからね?」とニヤニヤされながらも100%趣味全開のチョイス。「言語処理系の性能の違いが戦力の決定的差では無いということを教えてやる!」と赤くて速い人気取りで息巻いてみたものの、実際問題別な意味で全くその通りだった

糞クエリ対策とキャッシュ

殆どのチームがまずはクエリの見直しとDBのチューニングに手を付けてた様子。テーブル構造の見直しからMySQLのオプションやストレージエンジンの変更とかをやってたチームもあったみたいだけど、俺は必要なカラムにインデックスを貼る程度に留めて、データをガンガンキャッシュしてなるべくDBまで到達しないようにする戦略にすることにした。

まぁまずみんな真っ先に気付いてたところとしては、アプリ内に非常に残念なクエリがわざと仕込んであるということ。素晴しく分かりやすいお手本のような糞クエリだった。サイドバーのデータを取得するのが次のようなクエリ。

SELECT a.id, a.title
FROM comment c
LEFT JOIN article a ON c.article = a.id
GROUP BY a.id
ORDER BY MAX(c.created_at) DESC LIMIT 10;

記事が数千件、コメントが十数万件あるので、これは大分辛い。しかもサイドバーは全ページに表示されるので、全てのリクエストでこのクエリが発行されるという鬼畜さ。なので、次のように変更する。

  1. キャッシュからサイドバーに表示する記事IDのリスト取得を試みる
    1. なければ、commentテーブルから記事IDのみのリストを取得する
    2. キャッシュする
  2. 記事IDのリストを元に、キャッシュから記事データ10件のリスト取得を試みる
    1. それでも無かった数件をarticleテーブルからWHERE id INで取得する
    2. キャッシュする

あと、サイドバー・インデックスページ・記事詳細ページでそれぞれ必要なカラムだけを取って使ってたけど、これは逆にID・タイトル・本文・投稿日時全てを取得してIDをキーにキャッシュに突っ込むようにした。こうすることでサイドバーとインデックスと記事詳細でキャッシュを共有できるようになる上、コメントの投稿も記事の参照も新しい記事に偏っているので、サイドバーを読み込む時点では記事データはほぼ全てキャッシュに載っていることが期待できる。

commentテーブルから記事IDのリストを取得してるところはまだ重いと思うけど、当初のクエリよりは遥かに速いし、並行してコメントが投稿されたときの整合性の担保とかするのが地味に面倒だったので、とりあえず後回しにする。

コメントはコメントのIDではなく記事のIDでまとめてキャッシュするようにした。ページングや並べかえ、コメントの削除や編集などは仕様になく、コメントのパーマリンクなどもないので記事ページ以外では表示されないため、1回で全部取れるのがよかろうという判断。それらがあるようだったら、記事と同じようにコメントIDをキーにして「リストの順番や内容が変更されてもIDだけを取ってくるようにして、データ自体はキャッシュに載ってるものは使う」みたいにしたかもしれない。でもあんまりDBやmemcachedへのリクエストが増え過ぎるのはアレなので何らかのまとまりでキャッシュするかなぁ、とか色々考えてたけど、複雑になって余計悪化する、みたいなことにもなりかねないし、微妙に難しい。これもとりあえずこれでいいやってことにして後回し。

ノンブロッキングなフレームワーク

若干工夫してみたのは、フレームワークにはGoliathを選んだあたり。EventMachineベースのRubyのWebアプリケーションフレームワークおよびサーバで、これでI/Oを多重化して、同時接続数が増えてきても重いI/O処理でブロックしないで効率的に処理してくれることを期待する。まぁ、ぶっちゃけこれは実際にはそんなに効果なかった。

当初はPOSTのリクエストがばんばん飛んでくるとか、複数のテーブルに跨ってデータを取ってきて複雑な集約をするとか、画像アップロードみたいな長時間コネクション張り続けるようなリクエストがあるかなとか、そういう状況を想定してたんだけど、実際にはGETの比率のが圧倒的に高いしテーブル構造もシンプルで投稿はテキストのみ、みたいなパターンだったので「並列にI/O処理をする」ことがあまりなかった。

「遅延書き込みをするようにしてPOSTの際は即座にレスポンスを返してしまう」みたいなこともちょっとはやろうとしてたのだけど、「POSTリクエスト完了後1秒以内に表示に反映すること」ってルールにひっかかってテスト通らないことが頻繁にあったりして地味に嫌な感じになったので方針転換した。ここはむしろ同期処理にして「終わったら即書き込み即キャッシュ破棄」するようにして(実際には反映までにかかる時間に大差は無いはずなんだけど、こう振る舞った方がクライアント側からは速く反映されてるように見える)、POSTでは若干の時間をかけてしまってもいい、それよりもGETリクエストが来たときに既に準備が整ってるようにここで再取得再キャッシュまでやってしまう。

同時接続数の方はどうかというと、そもそもベンチマークスクリプトの並列数が最大10とかだったしコネクションも一瞬で切断されるし、じゃあワーカプロセスを10個立ち上げちゃえばいいじゃん、というオチが付いた。CPUもメモリもスッカスカでアプリサーバが遊んでたし、RubyやPythonみたいな言語を使う分には、1プロセス辺りの並列処理の効率化みたいなとこよりもUnicornの割り切りっぷりの方が現状理にかなってると思う。てことで言えばぶっちゃけ最初の素のSinatraで別に問題なかったような…どうしてもEventMachine使いたいならRainbows!Async Sinatraとかでも良かったような…まぁもう書いちゃったしいいか…。

余談だけどGoliath採用に併せて関連ライブラリも選び直すことになったので、MySQLクライアントにはMysql2を、mechachedクライアントにはremcachedを、ついでに趣味でテンプレートエンジンにSlimを使った。Mysql2やSlimは大分良かったので何か機会があれば使っていきたいところ。

Web「アプリ」って何?

とにかくもうDBに複雑なことさせたら負け、それ以前にDBに行ったら負け、と来たら次に来るのは「ていうかアプリに行ったら負け」。この段階ではフロントのWebサーバはアプリサーバ2台のロードバランサとしての仕事しかしていなくて、静的ファイルもアプリ側でファイル読んで返してたし、更新してないページも毎回アプリにリクエストが来ていた。バックエンドのアプリケーションサーバがどんなに速くなってもフロントエンドのWebサーバの処理速度とは文字通り桁が違うので、できればなるべくバックエンドに行って欲しくない。ならばということで、フロントエンドでページ丸ごとキャッシュしてGETリクエストは全部そっちで返してしまうことにする。

VCLの記述力やキャッシュ管理のしやすさ、ESI機能、あと名前のかっこよさなどが魅力的だったので、最初はVarnishを使ってみた。directorでアプリサーバ2台をまとめて、GET以外のリクエストは素通りするようにして、GETのレスポンスはページ単位でキャッシュするようにする。このままだと当然「投稿は1秒以内に反映されること」というルールが満たせないので、HTTPでパージできるように設定して、POSTのリクエストを処理したらアプリ側からVarnishにPURGEリクエストを送るようにした。

そうなると今度はキャッシュの破棄のタイミングが問題になってくる。記事の投稿はまぁいい。記事が投稿されて内容が更新されるのはインデックスページだけなので、1ページ破棄してやればいい。が、問題はコメントの方。コメントが投稿されるとまず記事ページが更新されて、「最近コメントが付いた記事10件」が表示されるサイドバーも更新される。が、このサイドバーは全てのページで表示されている。どこかの記事に1件コメントされる度に全ページのキャッシュが破棄されてたのでは殆どキャッシュの意味を為さない。なのでサイドバーは各ページのレンダリング時には生成せず、ESIでVarnishの段階でincludeさせるようにした。こうすれば、コメントが投稿されたときにパージするのは該当する記事ページとサイドバーだけになる。

そこまでやると最初の数回とPOST直後の数回以外は全部Varnishが返してくれるようになるので、アプリサーバの方は殆ど遊んでいる状態になる。前の段で「リソース余ってるからガンガンプロセス増やしちまおうぜ」って言えたのはこのおかげ。数万リクエスト処理しても数百とか数千くらいしか後ろに到達しなくて、さらにその後ろのDBまで行くのはもっと絞られてくる。この段階で初期状態から10倍くらいパフォーマンスが向上している。

ところがこの辺りで地味に嫌な罠を踏む。VarnishでESIを使うとContent-Lengthヘッダを返さなくなるので、Keep-Aliveで接続してるクライアントがいつレスポンスが終わったのかよくわからなくてタイムアウトするまで一部のクライアントできちんと扱えないのにKeep-Aliveのリクエストを送ってきたときにいつレスポンスが完了したのか判断できず、コネクションが切れるまで待ち続けてしまう。設定でどうにかできそうな気がするけど不慣れなVarnishに四苦八苦してなかなかうまくいかず、前段にもう一段Nginxを立ててリクエスト/レスポンスをいじってみたりするも今度は多段にしたのが祟ってそのオーバーヘッドで大分パフォーマンスが落ちてしまう。

結局Keep-Alive問題は真面目に対応するのをやめていかなる場合でも無効にしてしまおうかーとか考えてるあたりで、hidedenさんのNginx/SSI+SCGI構成にダブルスコアをつけられてしまって、NginxマジはえーほんとパネェつかVarnishってぶっちゃけNginxより遅いのに何で選んだの、みたいな声に負けてVarnishと戯れるのを放棄することにした。いや多分、俺のVarnish力の低さのせいで真の実力を発揮できてなかっただけでそこまでオワコンでは無いと信じたいのだけど、と一応擁護しておく。でもしばらくはNginx一択だけど。

ちなみに、ベンチマークスクリプトはそもそもKeep-Aliveに対応してないその「一部のクライアント」と同じ挙動をしていたわけではないので、この時点ではこの問題は割り切って無視するという選択肢もあった。が、本番当日の講評の際にkazeburoさんが恐しい罠を仕込んでたことを知らされる(っていうか社内では普通に話してたらしいけど聞いてなかった)。なんと、HTTP1.1じゃない持続的接続ができないのにそうできるかのように偽装してKeep-Aliveって付けた嫌がらせリクエストを3%程混ぜており、これに律儀に応えると、ベンチマークスクリプトはコネクション切れるまで待ち続けてしまって致命的に遅くなる、というもの。Nginxが素晴らしいのは周知の事実なので本番でも使ってくるチームが多いことを予想して、「Nginx(や、他の高速Webサーバやキャッシュサーバ)をチューニングせずにただ設置するとハマる罠」を仕掛けたんだとか。そうと知らずにそれを回避することに成功していて怪我の功名だった。本当運営の人達悪魔や…。

Nginxのチューニング

気を取り直してNginxの設定。まずNginxはWebサーバなので、餅は餅屋ということで静的なファイルはアプリサーバからフロントサーバに全部持ってきてNginxに返させてしまう。これでアプリから静的ファイルの配信機能を取っ払うのに成功して、本当に若干だけど無駄な処理を減らせる。リバースプロキシの設定は簡単なのでこれも普通に設定してしまう。それからVarinishのESI同様NginxでもSSIを有効にする。

それからキャッシュ。Nginxのキャッシュの方法はいくつかある。まず直感的なのはファイルキャッシュ。キャッシュファイルの置き場所を決めておいて、upstreamに飛ばすlocationのところでcacheを使うよって指定してあげれば、upstreamからのレスポンスを自動的にそこに貯めてってくれて、二回目以降は勝手にそのファイルから返すようにしてくれる。Varnishのときはキャッシュストレージにメモリキャッシュとファイルキャッシュが指定できて、ファイルの方を指定すると格段に遅くなるので、Nginxもそうなるんじゃないかと思ったけどこれが驚く程高速でびっくりする。むしろVarnishのメモリキャッシュの時より速いくらいだった気がする。ちゃんとは検証してない。ただ、このファイルキャッシュはuriをハッシュしてディレクトリに配置してしまうため、外からはどれが何のキャッシュなのか分からなくて、Varnishに比べるとキャッシュの管理が難しい、と思ってたら、ちゃんとこういうモジュール作ってる人がいた。これならVarnishのときに作ったHTTP越しのパージの仕組みがそのまま使える。

もう一つ、memcachedをまるでアプリサーバのように見立てて、pathをキーにしてmemcachedにあるデータをそのままレスポンスボディにして返してしまうという驚きのモジュールもある。こっちの利点は、アプリ側からもキャッシュが扱い易いということ。普通にアプリからmemcachedに繋いで、Nginxにレスポンスを返すときに同時にmemcachedにも書き込んでおくと、次はそっちから読んでくるようにしてくれる。破棄するのも普通にdeleteすればいい。HTTPリクエストを投げてパージするよりは分かりやすいし、memcachedプロトコルのがHTTPよりは速そうだ。が、問題は、前述のファイルキャッシュより遥かに遅いということ。これはmemcachedが遅いというよりmemcachedに毎回コネクションを張り直すコストがファイルキャッシュからの読み込み(これはおそらくかなり内部で最適化されているはず)のコストよりも高いせいらしい。ほぼ同じ状態でファイルキャッシュをmemcachedに切り換えたら、スコアが半分以下になってしまって絶望的な気分になった。仮にHTTP越しのパージよりもmemcachedのがアプリ側からは扱い易くてコストも低かったとしても、大半はNginxの段階で完結するキャッシュ済みGETリクエストなので、そっちのオーバーヘッドの方がもろに結果に影響する。ので最初はファイルキャッシュを採用した。

けど、hidedenさんの方はmemcachedを使っててそれでも俺のやつよりパフォーマンス出てるのでなんでだろうと聞いてみたら、upstreamとのコネクションを繋ぎっぱなしにしていたからだったらしい。試しにkeepaliveを設定してみたら、アプリ側何もせずに一気に3倍くらいのスコアになってhidedenさんのスコアに肉迫するレベルになった。同じことを当日もやらかした。hidedenさんが毎分11万リクエストというハイスコアを出した一方、前日まで大差はついてなかった俺の方は3万程度しか出てなくて焦ったのだけど、nginx.confを見直してkeepalive付け忘れに気付いて再起動したらちゃんと動いた。

で、結果はというと、ギリギリ100000req/minを越えるくらい。「お前はチートしてそれかよショボいな」と言われないくらいの結果は出せたと思うのでちょっとホッとした…。

この戦略の意味

POSTよりGETが圧倒的に多く、大半のリクエストが新しいデータに集中し、一度書き込まれた投稿はその後はあまり書き換えが起こらない、というのは、「大部分がほとんど書き換わらない動的コンテンツ」ではなくて「一部分が書き換わることがある静的コンテンツ」だと思っちゃえば少し話が簡単になる。

GETリクエストなんか静的なHTMLファイルを自動生成するためのトリガー、くらいに考えて、ただそれがファイル書き出しじゃなくてメモリ上のキャッシュに突っ込む方が扱い易いよねって発想で行けば、どんなミドルウェアが欲しいかとか、アプリは何をするべきなのかとか、どこで一番頑張るんだそうかフロントのWebサーバか、みたいなことでやることが決まる。てか、どっか1チームくらい本当にHTMLファイル書き出してデプロイしちゃうとこ無いかなーとか思いながら見てた。多分それはそれで速い。絶対面倒なのであまりやりたくは無いけど。

もっとぶっちゃけ話をすると、ライブドアブログの閲覧側チューニング戦略が(実現してる方法は違うけど)大体この形なので、ページのキャッシュと更新の局所化は最初からやる予定でいた。アプリエンジニアの性でついついアプリいじりに時間を割きたくなっちゃうとか、ミドルウェアやサーバ管理の知識経験が致命的に不足していたので時間ばっかり食ってしまったとか、ってのが「時間内には終わらなかったけど最終的には出来た」の実情だったりします。アプリエンジニアは常日頃からそういう知識をちゃんと収集しておけ、あとインフラチームと仲良くしておけ、色々捗るぞ、というお話でした。

上手く行かないケースと使い回せるテクニック

ブログ型の単純な表示系のリクエストが多いお題だったからWebサーバの性能が結果に直結してたけど、更新系のリクエストが多くて条件によって表示要素の個数や種類が大きく変わる、みたいな場合だと今度はアプリやDBの方に比重が移ってくる。例えばTwitterのサブセットみたいなのがお題だったらまた全然結果が違ったはず。

データのキャッシングやSSIみたいな仕組みはその場合でも有効だろうけど、「Nginx置いたらパフォーマンス20倍になったwwwwマジうめぇwwwwアプリ関係ないwwww」みたいなシンプルなことにはならないので、そっちの場合はアプリの実装力を鍛えてないと死ぬ。ISUCONに「部門別」とかあったら面白いかもねーとか思ったけど準備する側が死にそうなので軽々しく言うのはやめときます。僕お手伝いって名目なのに普通に遊んでただけでほんとごめんなさい。

反省点とか

DBロクに見てない。上位陣の方々見てるとMySQLバリバリチューニングしてるので、もっとちゃんといじればもう少し速度出るはず。一番効果が高いところを優先したと言えばそうだけど、あんま詳しくないところを放置しただけだったりもするので(アプリの全面書き直しとかマジで要らんかった)、ちゃんと勉強する。

みんなもやってみるといいよ

ISUCON運営チーム謹製のベンチマークツールと各言語の参考アプリは公開されてるので、是非触ってみてくだしあ。ゲーム感覚で楽しいし、各参考実装や意図的に仕込まれてる罠は、新人教育なんかにもうってつけだと思う。ええはい。自分の実力不足をガチで痛感した次第です。いや本当勉強しよう。ちゃんと。

前の記事で予告した通り、今度はNSRegularExpressionの話。

正規表現でマッチした部分文字列を取得する

まずNSRegularExpressionオブジェクトを作って、それのメソッドにNSStringのオブジェクトを渡す、という形で使う。まぁ説明するよりコード見た方が早い。

NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
NSError *error   = nil;
NSRegularExpression *regexp =
  [NSRegularExpression regularExpressionWithPattern:@"「そんな(.+)で大丈夫か?」「(.+)」"
                                            options:0
                                              error:&error];
if (error != nil) {
  NSLog(@"%@", error);
} else {
  NSTextCheckingResult *match =
    [regexp firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
  NSLog(@"%d", match.numberOfRanges); // 3のはず
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]); // マッチした文字列全部
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]); // "正規表現"
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]); // "大丈夫だ、問題ない"
}

地味にややこしい。Rubyで書いたらこんなんで済むのに。

# coding: utf-8
if "「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」" =~ /「そんな(.+)で大丈夫か?」「(.+)」/
  puts $&
  puts $1
  puts $2
end

まぁRubyやPerlと比べるのは(少なくとも文字列操作や正規表現に関して言えば)フェアじゃないですけど!とにかくこれで正規表現で部分文字列を探せるようになりました、と。

ちなみに、-firstMatchInString:options:range:というメソッド名で分かると思うけど、これは最初にマッチした箇所しか取ってこない。マッチした箇所全て欲しければ、-matchesInString:options:range:を使えば、NSTextCheckingResultが入ったNSArrayが返ってくる。別に返り値はずっと取っておく必要はなくて、単にマッチする毎になんか処理をしたいんだよ、ってときは、-enumerateMatchesInString:options:range:usingBlock:が使える。さっきの-firstMatchInString:options:range:を書き換えるとこんな感じになる。

NSRegularExpressionOptions options = 0;
NSRange range = NSMakeRange(0, string.length);
id block = ^(NSTextCheckingResult *match, NSMatchingFlags flag, BOOL *stop){
  NSLog(@"%d", match.numberOfRanges);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]);
};
[regexp enumerateMatchesInString:string options:options range:range usingBlock:block];

Blocksの使い方は以前書いた記事とか読んでもらえると分かるかもしれない。あの記事を書いた時点ではiOS4.0を想定してアプリ作れなかったので実質まともに使えるのがSnow Leopardだけだったのだけど、今ならiPhone/iPadともに4系前提で作れるし、そもそもNSRegularExpression自体がiOS4.0以降にしか無いのでNSRegularExpressionを使える環境ならBlockも使えるので問題ない。

置換する

正規表現が使えるなら一番やりたいのは置換だろう、ということでもちろん置換もできる。-stringByReplacingMatchesInString:options:range:withTemplate:というのがそれ。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"$0\n→($2砕け散る)\n→「神は言っている、ここで死ぬ運命ではないと」\n→「$1」「一番いいのを頼む」";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"「(そんな(.+)で大丈夫か?)」「.+」"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

最初は話を聞かなかったあいつもちゃんと一番いいのを頼んできたので、今度は大丈夫だろう。しれっと$0とか$1とか使ってるけど、もちろんちゃんと置換文字列の中でキャプチャした部分文字列を参照したりできてるはず。

ただ、-stringByReplacingMatchesInString:options:range:withTemplate:は文字列そのものを置換してるわけじゃなくて、引数のNSStringのオブジェクトをcopyして置換したものを返してくる。なので、元のstringは何も変わってないので変わったつもりで使おうとしたらアレ?ってなるし、毎回文字列のコピーをするので場合によっては無駄になる。その場合は-replaceMatchesInString:options:range:withTemplate:の方を使う。基本的には-stringByReplacingMatchesInString:options:range:withTemplate:と同じなんだけど、以下の点が違う。

  • 引数にNSStringでは無くNSMutableStringを取る
  • 引数のオブジェクトのコピーではなく引数のオブジェクト自体を置換する
  • 返り値は置換後の文字列ではなく整数値で、置換箇所の数を返す

というわけで、ある正規表現で置換した文字列をさらに別な正規表現で置換して、みたいなことをやる場合はこっちのメソッドを使うべき。

ちなみに、上記二つのメソッドはマッチした箇所を全部置換する。例えば下のようなコードだと「大丈夫か」と「大丈夫だ、」が両方置換される。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"チョ☆チョニッシーナ☆まっソコぶれっシュ☆エスボグリバンバーベーコンさんだね!";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"大丈夫(か|だ)、?"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

もしマッチした箇所の内特定の部分だけを置換したい場合は、-firstMatchInString:options:range:とか-matchesInString:options:range:でNSTextCheckingResultのオブジェクトを取得しておいてから、-replacementStringForResult:inString:offset:template:を使う、みたいな感じになるかしら。ちょっと面倒な気もするけど。

RegexKitLite or NSRegularExpression

両方書いてみた感想で言うと、個人的にはRegexKitLiteのNSStringにメソッド生やしてくアプローチのAPIのが使い易いと思った。CoreFoundation使ってごりごり書いてるのでパフォーマンスも悪くないし、割と早い段階からBlocksに対応してたりとアクティブに開発されてるし、その気になればソース読めるし(まぁ、チラ見しては見たもののあんまり読む気にはならないのだけども)…とか考えると、既にRegexKitLiteを使ってるなら別に無理にNSRegularExpressionに乗り換える必要は無い気がしてくる。iOS4.0以前のバージョンもターゲットにするなら他に選択肢はないし、あと何故かNSRegularExpressionクラスはiOSにしか無くてMacOSXでは使えないという面白いことになってるので、iOSでもMacでも動くようなコードを書く場合もやっぱりNSRegularExpressionは使えない。

とは言えNSRegularExpressionの方はFoundationの一部なので、数カ所正規表現での置換を使いたいが為に外部のコード落としてきてプロジェクトに組み込んでlibicucoreに忘れずにリンクして…ってしないで済むなぁとか、万が一iOSの内部の実装が変わったりなんかの規約が変わったりしてもおそらく書き換えないで済むだろうなぁという多少の安心感とかはある。ので、これから作るアプリで、4.0以降のみをターゲットにしてる場合は、NSRegularExpressionを使って書こうかなぁなんて思ったりしてたり。

ちょっと前に書こうと思ってて忘れてたネタ。iOSアプリ内で正規表現を使ってごにょごにょしようと思ったらRegexKitLiteを導入するのが一番てっとりばやいのだけど、iOS 3.2以降はFoundation Framework内でも地味に正規表現が使えるようになってきてるのでメモがてら記事にしておく。

NSRegularExpressionSearch

Cocoaで文字列中に別な文字列が含まれているかどうかを知りたいときは、NSStringの-rangeOfString:というメソッドを使う。RubyのString#indexみたいな感じで、見付かった文字列がどこにあるかの位置を返してくれる。こんな感じ。

NSString *string = @"I love Udon.";
NSRange match = [string rangeOfString:@"Udon"];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

これにもう少し細かく色々なオプションを指定できる-rangeOfString:options:というメソッドがあるのだけど、iOS3.2以上のバージョンだとこのオプションにNSRegularExpressionSearchというのが指定できるようになっている。実際に使うときはこう。

NSString *string = @"1日3食のうち4食はうどんを食べたいと思っている。";
NSRange match = [string rangeOfString:@"[0-9]+食" options:NSRegularExpressionSearch];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

rangeOfString:に正規表現(の文字列)を渡せるようになってちょっと便利。書式はICU-comaptibleだそうだけど、RegexKitLiteもlibicucoreを使ってるので、RegexKitLiteを使ってた人は得に気にすることなく使えると思う。

これだけでも大分マシにはなったんだけど、さっきのサンプルコード見て分かる通り最初にマッチした部分しか取ってこれないし、もしかして-stringByReplacingOccurrencesOfString:withString:options:range:とかにも正規表現使えるのかなとwktkしたのだけど、「You can use this option only with the rangeOfString:... methods.」だそうで。マッチした箇所を全部取ってくるとか置換するとかは別な方法でやるようだ。

NSRegularExpression

さっきのはNSStringの文字列検索のオプションだったけど、正規表現そのものを扱うNSRegularExpressionというクラスがある。NSRegularExpressionSearchオプションは3.2以降であれば使えるけど、NSRegularExpressionクラスは4.0以降。つまり今までiPadでは使えなかったので、RegexKitLiteを置き換えるには至らなかった。

が。そろそろiPad版を含むiOS4.2がリリースされるので、ようやくiPadでも4系の機能が使えるようになるのです。弟の仇をトルノデス。ということで次回はNSRegularExpressionを使った文字列検索を記事にします。予告。

こないだ、と言っても2週間くらい前の話なんだけど、社内でZeroMQMongrel2の勉強会をやった。Mongrelと言えば、俺がRails(たしか当時1.2くらいだったと思う)で仕事してた頃にアプリケーションサーバとして使ってたけど、最近だとThinとかPassengerとかUnicornとかの人気に押されてついぞ聞かなくなったアレだよなぁ、なんでPerlの会社の勉強会でMongrelなんだろう、と思ってたんだけど、Mongrel2はもはやRailsのアプリケーションサーバじゃなくて、通信にZeroMQなるものを使った汎用的なWebサーバになってたらしい。

大分野心的なプロジェクトではあるものの、今はZeroMQもMongrel2も「とりあえず出た」って感じらしく、今年一杯くらいは地雷原を突き進む気がある人だけ触るといいんじゃないかなという話だった。プロダクションで使うようなレベルになるにはもうしばらくかかりそうだけど、今なら各言語の実装も追い付いてないようなので、遊んでおくなら今のうち。いずれMongrel2が大流行したときに「faultierさん、是非本を書いてください!」ってお願いされることを夢見て色々ごにょごにょしてみたよ。

準備

何はともあれZeroMQとMongrel2をインストールする。とは言っても、Getting Startedの通りにインストールするだけ。例に書いてある奴は若干バージョンが古いので、それぞれの最新版を取ってきた方がいいと思う。ちなみにZeroMQはhomebrewにもFormulaがあった。pyzmqは無かったので自分で作るなどした。

今回はRubyで試すので、RubyのZeroMQバインディングも入れておく。これは普通にgem install zmqで入るはず。Rackのハンドラの例の方はffi-rzmqを使ってるんだけど、なんか手元の環境で上手くインストールできなかったのでそっちは試してない。まぁ今回やることにはどっちがどうとかあんまり関係ないのでzmqの方でいいか。

そこまでできたら今度はMongrel2の設定を用意する。なんでCで書かれてるはずのMongrel2がやたらとPythonのライブラリ入れまくるんだろうと思ったんだけど、Mongrel2の操作にはm2shというPythonで書かれたスクリプトを使うかららしい。設定の仕方が面白くて、まずはPythonで書かれた設定ファイルを用意して、それをm2shでsqliteのファイルに書き出し、それを使ってMongrelが起動する、というようになってる。だから多分m2sh相当のものをPerlなりRubyなりで用意してしまえば、別にPythonは必要ないはず。まぁ、面倒なので大人しくm2shを使う。設定ファイルはこんな感じにした。

# m2test.py
from mongrel2.config import *

main = Server(
    uuid ="2f62bd5-9e59-49cd-993c-3b6013c28f05",
    chroot="./",
    access_log="/logs/access.log",
    error_log="/logs/error.log",
    pid_file="/run/mongrel2.pid",
    default_host="localhost",
    name="main",
    port=6767,
    hosts=[
        Host(
            name="localhost",
            routes={
                r'/m2test': Handler(
                    send_spec="tcp://127.0.0.1:9997",
                    send_ident="70D107AB-19F5-44AE-A2D0-2326A167D8D7",
                    recv_spec="tcp://127.0.0.1:9996",
                    recv_ident=""
                )
            }
        )
    ]
)
settings = {"zeromq.threads": 1}
commit([main], settings=settings)

Mongrel2のexamplesに入ってたのを参考にした。なんとなくわかると思うけど、hostsの中のHandlerってやつが今から作るハンドラと通信する為の設定になる。これが出来たら、

$ mkdir run log tmp
$ m2sh init -db m2test.db
$ m2sh load -db m2test.db -config m2test.py
$ m2sh start -db m2test.db -host localhost

とかすると、Mongrel2が立ち上がる。http://localhost:6767/m2testでアクセスすると裏のハンドラに処理が渡るはずだけど、まだ作ってないのでこの時点ではレスポンスが返ってこず、延々待たされる。

プロトコルを調べる

Mongrel2は内部にアプリケーションサーバを持つわけではなく、基本的にやることはZeroMQを使った通信をするだけ。上に書いた設定だと、ローカルの9997ポートと9996ポートにZeroMQのソケットが用意されて、ハンドラは9997ポートからリクエストを受けとり、処理したら9996ポートにレスポンスを送ってやるようにする。ちなみにこのソケットは別にUnixソケットのことではなく、ファイルを経由したりプロセス内通信したりネットワーク越しに通信したり色々できるらしい。詳しくはZeroMQを調べてみるといいと思う。

リクエストのメッセージは、「センダのID コネクションのID パス ヘッダの長さ:ヘッダ,ボディの長さ:ボディ」の形式で飛んでくる。実際にはこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 2 /m2test 542:{"PATH":"/m2test","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/m2test","PATTERN":"/m2test","Accept":"application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5","Accept-Charset":"Shift_JIS,utf-8;q=0.7,*;q=0.3","Accept-Encoding":"gzip,deflate,sdch","Accept-Language":"ja,en-US;q=0.8,en;q=0.6","Cache-Control":"max-age=0","Connection":"keep-alive","Host":"localhost:6767","User-Agent":"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.62 Safari/534.3"},0:,

んで、レスポンスは「センダのID コネクションIDの長さ:コネクションのID HTTP/1.1 ステータスコード ステータスメッセージ ヘッダ ボディ」という形式で返す。例えばokと返すだけのレスポンスならこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 1:3, HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2

ok

つまりハンドラが何をすればいいかというと、9997ポートから来た上の形式のメッセージをパースしてリクエストを判断し、処理結果を下の形式に加工して9996ポートに送ってやる、とこういうことです。

ミニマムなハンドラを作る

そしてこちらが調理済みのハンドラになります(料理番組風)。

#!/usr/bin/env ruby
# coding: utf-8

require 'zmq'

sid   = "70D107AB-19F5-44AE-A2D0-2326A167D8D7"
con   = ZMQ::Context.new
rsock = con.socket(ZMQ::UPSTREAM)
ssock = con.socket(ZMQ::PUB)

rsock.connect('tcp://127.0.0.1:9997')
ssock.connect('tcp://127.0.0.1:9996')
ssock.setsockopt(ZMQ::IDENTITY, sid)

loop do
  str = rsock.recv
  sender, conn_id, path, str = str.split(' ', 4)
  ssock.send "#{sender} #{conn_id.size}:#{conn_id}, HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok", 0
end

こいつとMongrel2を起動させといて、http://localhost:6767/m2testにアクセスすると、okとそっけない返事が返ってくる、というだけのハンドラ。「今夜は帰したくない」とか「結婚しよう」とかいうリクエストを投げても「ok」って返してくれます。まぁ「別れよう」でも「ok」って返ってきますけど。

上のコードだとreceiveしたメッセージのセンダIDとコネクションIDしか見てないけど、ヘッダとボディをパースすればRackアプリに渡すENVを作ることができるし、Rackアプリが返すレスポンスの仕様は決まってるのでそれをMongrel用のレスポンスメッセージに変換してやるのも簡単にできるわけで、そこまですればMongrel2と連携できるRackハンドラが作れる、というわけ。あとは、このハンドラ自身でリクエストを処理しなくても、スレッドを一杯作ってその中でRackアプリの処理をさせて、ハンドラ自身はプロセス内通信でリクエスト/レスポンスの中継役になってやるとかすれば、ワーカをがんがん増やせるとか、そんな風にもできる。

あと、Mongrel2とハンドラ間はZeroMQで通信してるだけなので、お互いが生きてるか死んでるか、何個あるのか一個もないのか、などについて何も感知しない。ので、急に負荷が上がったらプロセスやサーバを増やして緊急投入してもMongrel側の設定は変更する必要なかったりとか、デプロイ時にはもう一個ハンドラプロセスを立ち上げて起動し終えたら古いプロセスを殺すとかしてやればダウンタイム無しでデプロイできたりとか、するんじゃないのかな。多分。

あ、ちなみに今回作ったものはGistに置いといた。

「RubyKaigiが終わったら真面目にやろう」とか言ってたくせに、中々やる暇無くて放置してたら大変に分かりやすい作り方講座が出てしまった上、弾さんまで乗ってきてしまって完全にタイミングを逃したfaultierですこんばんは。

悔しいので対抗してみる

うどんげが出たならてゐもいてもいいだろう、ということでてゐ。AAは上記の記事同様こちらを使わせてもらった。初春もいいなーと思ったんだけど表示してみたら大き過ぎて自分のターミナルで表示できなかったのでやめといた。コードはこんな感じ。

# tewi.rb
eval$s=%w't=true;e="eval($s=join("<<34<<34<<",qw{$t=1;$s=~/"<<92<<"[[0-9,]+"<<92<
<"]/;$n=eval($&);$e=          "<<39<<"eval$s=%w"<<39<<    ".chr(39)."<<39<<($s+(
($s.length>1756)?"   ":"#"<<$    s.gsub(/[^0-9a-zA-   Z]/,      "")[0,(1755-$s.l
ength)]));e[-312,  311]=""<<39<<   ";$e.=          ("<<39<<"#"<   <39<<".substr(
join("<<39<<39<<  ",split(/[^0-9a-z              A-Z]/,$s)),0,(200  9-length($s)
))).chr(39)."<<  39<<".join"<<39<<";@o   =       map{$t=!$t;split(//  ,((!$t)?su
bstr($e,0,$_,"< <39<<39<<"):chr(32)x$_)         )}@$n;for(1..34){spli  ce(@o,$_*
81,0,chr(10))}  ;print(join("<<39<<39<<         ",@o).chr(10      )    )}))";o=[
101,10,24,4,40 ,3,8,4,18,3,4,6,33,2,13           ,3,6,10,1            2,3,30,2,1
7,14,17,2,27,  2,21,3,1,7,19    ,2                     ,              25,1,23,9,
21,2,23,2,23,  9,12,6,1,                           4                   ,23,1,23,
11,9,12,23,2, 13,4,2,2                   1                             ,1,14,23,
2,9,27,1,19   ,22,1,8         ,                  1    9   ,             1,29,20,
3,7,9,1,      18,1,4      ,         1        ,          3           ,    1,13,16
,6,6,6,      1,9,1,8                          ,1           ,    1    0,    1,11,
1,4,14,      6,7,26,                   2,1     1,1,4   ,1              ,4,   2,4
,12,6,7     ,19,3,5,             5,  3,2,14,3  ,3,1   0,5               ,8,13  ,
2,2,8,    2,4,3,3,15,          5,2    ,7    ,  4,11 ,10,3,              4,2,4,1,
2,4,    1,6,14,12,4,1      3  ,   6,1,     2,1,3,4,5,18,3,1,3   ,   1   ,3,12,3,
12,1   3,5,1,2,4,15             ,1,1, 11    ,10,3,14,12,6,1 ,           2,5,13,2
,1   ,1,1,6,12,2,16            ,11,7, 8,     13,2,1,7,13,1  , 1      8,10,8,6,14
,  1,2,10,10,1,20,1           2,5,3,1        2,1,6,13,7,2,  1       9,13,26,14,7
, 2,16,16,11,1,13,14          ,8,2,14,      16,13,7,5,15,1 0,          3,8,10,1,
8 ,25,10,1,4,13,27,23,            12,4,   2,12,5,1,1,1 ,8,1,1             3,17,1
0  ,1,3,5,1,12,14,4,8,             4,10,2,13,25,2,3,9,4,2,1,3              ,15,1
3,  33,4,5,1,4,3,20,                8,182].map{ |i|t=!t;((!t)              ?e.sl
ice  !(0,i):32.chr*                i)}.join;1.up       to(35               ){|i|
o[(i*   81)-1,0]          =        10.chr};puts(o)#ttrueeeva          l    sjoin
3434qwt1                           s920992nevale39evalsw39            chr3  939s
slength1     7 5        6             sgsub09azAZ01755s          l   ength e3123
1139e39              39su        bstr          jo             #t1s09nevaleevalsw
chr39tt  rue         eeva  l   sjoin3434qwt1s9             20992nevale39evalsw39
chr3939sslen    gth17 56sg   sub09azAZ01755slengt        he31231139e3939substrjo
in3939split09azAZs02009lengthschr3939join39omapttsplittsubstre03939chr32xnfor134
spliceo810chr10printjoin3939ochr10o10110244403841834633213361012330217141'.join

Gistにも置いてあります。はい。

Quineじゃない件

上のコードをコピペしてRubyに実行させると何やらコードを吐くので、それをさらにRubyに流し込んで実行させてやると…なんということでしょう!エラーを吐くではありませんか!Quineになってねぇじゃねーか、このド低能が!

# tewi.pl(tewi.rbの出力結果)
eval($s=join("",qw{$t=1;$s=~/\[[0-9,]+\]/;$n=eval($&);$e='eval$s=%w'.chr(39).'t=
true;e="eval($s=join(          "<<34<<34<<",qw{$t=1;$s=    ~/"<<92<<"[[0-9,]+"<<
92<<"]/;$n=eval($&)   ;$e="<<3    9<<"eval$s=%w"<<39   <<".      chr(39)."<<39<<
($s+(($s.length>17  56)?"":"#"<<$   s.gsub          (/[^0-9a-zA-   Z]/,"")[0,(17
55-$s.length)]));  e[-312,311]=""<<3              9<<";$e.=("<<39<<  "#"<<39<<".
substr(join("<<3  9<<39<<",split(/[^0-9   a       -zA-Z]/,$s)),0,(200  9-length(
$s)))).chr(39)." <<39<<".join"<<39<<";@o         =map{$t=!$t;split(//,  ((!$t)?s
ubstr($e,0,$_,"  <<39<<39<<"):chr(32)x$_         ))}@$n;for(1      .    .34){spl
ice(@o,$_*81,0, chr(10))};print(join("<           <39<<39<<            ",@o).chr
(10))}))";o=[1  01,10,24,4,40    ,3                     ,              8,4,18,3,
4,6,33,2,13,3,  6,10,12,3                           ,                   30,2,17,
14,17,2,27,2,2 1,3,1,7,                   1                             9,2,25,1
,23,9,21,2,2   3,2,23,         9                  ,    1   2             ,6,1,4,
23,1,23,1      1,9,12      ,         2        3          ,           2    ,13,4,
2,21,1,1      4,23,2,                          9,           2    7    ,1    ,19,
22,1,8,1      9,1,29,                   20,     3,7,9   ,1              ,18   ,1
,4,1,3,1     ,13,16,6             ,6  ,6,1,9,1  ,8,1   ,10               ,1,11  
,1,4,14    ,6,7,26,2,1          1,1    ,4    ,  1,4, 2,4,12              ,6,7,19
,3,5,    5,3,2,14,3,3,      1  0   ,5,8     ,13,2,2,8,2,4,3,3,   1   5   ,5,2,7,
4,11,   10,3,4,2,4,1             ,2,4, 1,    6,14,12,4,13,6, 1           ,2,1,3,
4,5   ,18,3,1,3,1,3,            12,3,1 2,     13,5,1,2,4,15  , 1      ,1,11,10,3
,1  4,12,6,1,2,5,13,           2,1,1,1        ,6,12,2,16,11  ,       7,8,13,2,1,
7, 13,1,18,10,8,6,14,          1,2,10,1      0,1,20,12,5,3, 12          ,1,6,13,
7, 2,19,13,26,14,7,2,16            ,16,1   1,1,13,14,8, 2,14,1             6,13,
7,  5,15,10,3,8,10,1,8,             25,10,1,4,13,27,23,12,4,2,              12,5
,1,  1,1,8,1,13,17,10                ,1,3,5,1,12 ,14,4,8,4,10,              2,13
,25,  2,3,9,4,2,1,3,                15,13,33,4,5,       1,4,3               ,20,
8,182]   .map{|i|          t        =!t;((!t)?e.slice!(0,i):3          2    .chr
*i)}.join                           ;1.upto(35){|i|o[(i*81)            -1,0  ]=1
0.chr};pu     t s        (             o)#ttrueeevalsjoi          n   3434q wt1s
920992ne              vale        39ev          al             sw39chr3939ssleng
th1756sg  sub         09az  A   Z01755slengthe3             1231139e3939substrjo
';$e.=('#'.su    bstr( join   ('',split(/[^0-9a-zA        -Z]/,$s)),0,(2009-leng
th($s)))).chr(39).'.join';@o=map{$t=!$t;split(//,((!$t)?substr($e,0,$_,''):chr(3
2)x$_))}@$n;for(1..34){splice(@o,$_*81,0,chr(10))};print(join('',@o).chr(10))}))

ええはい。良く見てもらえると分かると思うけど、実はこのコードは自分自身を出力してない。何を出力してるかと言うと、Perlのコードを出力している。んで、そのPerlのコードは何をするかというと、最初のRubyのコードを出力する。つまり、最初のコードは「『このRubyのコードを出力するPerlのコード』を出力するRubyのコード」で、後のコードは「『このPerlのコードを出力するRubyのコード』を出力するPerl」のコードになっていて、お互いがお互いの自分自身を相手に出力させるという、「人は一人では生きて行けないんだ」というメッセージ性を持った難読コードなわけです。まぁ嘘です。RubyもPerlもやられちゃったので、そのまま真似してもつまらなかっただけです。こんな感じで遊んでみるといいよ!

$ cat tewi.rb | ruby | perl | ruby | perl | ruby

何をしたの?

実のところ別になんということはなくて、やってることは基本的にはうどんげQuineと一緒。配列リテラルとjoinとevalを使ったQuineはPerlでもRubyでも全く同じ要領でできるので、自分自身を吐く代わりに相手のコードを吐くように書き換えると簡単に行ったり来たりできるようになる。

# 任意に加工できるRubyコードを出力する、
# 任意に加工できるPerlコードを出力する、
# 任意に加工できるRubyコードの例
eval$s=%w'puts("eval(join("<<39<<39<<",qw{print("<
<39<<"eval$s=%w"<<
39<<".chr(39)."<<39<
<$s<<39<<".chr(39)."<<39<<".join"<<39<
<")}))")'.join

あとは、二言語分のコードが一つのAAの中に入ることになるし、PerlとRubyだとリテラルがそっくりなのでデータ部は共有したいので生のデータを書きたい、となると流石に元のAAデータだと文字数の制限がキツいから、反転して白抜きのAAにした。ちなみに、てゐの方はうどんげのとはAAデータ持ち方は換えてる(元記事のロジックをPerlで実装するのが面倒だっただけ)。

あとはまぁ、出力時に尻尾に適当なゴミを付けて文字数調整してるんだけど、文字列リテラルの入れ子が簡単に崩れるので面倒臭いとかそんな程度。chr(39)濫用しまくり。あとドットがRubyのドットなのかPerlのドットなのか良くわかんなくなったりします。

まとめ

途中で「俺は一体何をやってるんだ」と思ったら多分負けなんだと思います。一度ベースができちゃうと後はデバッグと文字数の調整が面倒なだけで、割と簡単にできます。あと段々哲学的な気分になります。

本当に遊んでみただけだけど、例えばこんなの。

訪問回数
0
メッセージ
なんか多分エラってる

リロードする度に訪問回数がインクリメントされて、3の倍数の時には「奴」が疼き出し、5の倍数の時には「奴」が目覚めてしまうという、訪問回数カウンタとFizzBuzzを組み合わせたしょーもない何かです。まぁこれだけだと「そうですね…で?」みたいな話で終わりなんだけど、一応無駄にlocalStorageを使ってみてます。ちなみに「貴様は既に漆黒の闇に囚われている…」云々て表示されてる場合はlocalStorageが使えないブラウザなので、違うのが入ってたらそれで試してみてください。Firefox、Safari、Chromeあたりの最新版だと動いてるはず。多分。Operaも動いてるっぽい。

ろーかるすとれーじ?

Web Storageって何なのって人はHTML5.JPにある資料でも読んでもらうとして、まぁ大雑把に言うとブラウザにKey-Valueストレージが付いててなってJSから簡単に扱えるようになってるよって話。使い方は簡単。単に、こんな風にすればいいだけ。

localStorage.devilCount = 1;

これでlocalStorageにdevilCountってKeyで'1'って値が保存されて、同じドメインのページからだったらlocalStorage.devilCountって形で参照できるようになって、ブラウザが終了しても永続化されてるという。かんたん。リロードするだけじゃなく、試しにブラウザ閉じてもう一度この記事を見てもっても、ちゃんとカウントアップされてるのが分かるはず。べんり。この例は流石にしょーもなさすぎるけど、まぁ上手く使えば色々できるよね。

こんな風にアクセスすることもできる。

localStorage['devilCount'] = 1;
localStorage.setItem('devilCount', 1);
localStorage.getItem('devilCount');

消すときはこんな感じ。

delete localStorage.devilCount; // or localStorage.removeItem('devilCount');
localStorage.clear();           // 全て消す

ちなみに、localStorageに保存されてる値は文字列として保存されるので、文字列以外の値を入れたければJSONとしてシリアライズ/デシリアライズするとかして使う必要がある。上の例ではparseIntして数値として扱ったりしてる。

あと、localStorageはorigin単位で別のストレージになることに注意。ドメインやポートが変わるとlocalStorageも変わるので、例えばhttp://image.blog.livedoor.jpとかhttps://blog.livedoor.jpとかからはアクセスできないし、一方で別のブログ(例えばhttp://blog.livedoor.jp/faulist-mobile/とか)からはアクセスできてしまうはず。

せっしょんすとれーじ

同じものをsessionStorageで実装してみる。インターフェースは全く同じなので、ソース見てもらえれば分かるけど基本的にはlocalStorageとしてたものをsessionStorageに書き換えるだけ。

訪問回数
0
メッセージ
なんか多分エラってる

何回かリロードしたあと、一旦ウインドウを閉じる、別のウインドウで同じページを開く、などしてみると、違いが分かると思う。上のlocalStorageを使ってる方はブラウザ終了させようが別のウインドウで開こうがその度にインクリメントされるけど、こっちは「このウインドウが開かれてから閉じられるまで」の間だけ値が保存されるので、ブラウザを終了させると当然リセットされるし、別のウインドウで開くとまた1からカウントされる。

インターフェースが全く同じなのは楽でいいな。次は何やってみよう。コミュニケーションAPIかな。

補足

セッションストレージはブラウザ閉じたら消えるけど、ローカルストレージの方は消えないので、気持ち悪い場合はこの記事中の「訪問回数をリセットする」ボタンを押すか、SafariやChrome、Operaだと開発者向けのツールから消せる。もう一度記事見たらまた保存されちゃうけど。Firefoxだとどっから消すんだろうな。

あと、上記のコードを自分で書いて試してみるときは、Firefoxの場合はローカルファイルを開いても駄目で、Webサーバを介して見る必要がある。ローカルファイルとして開いた場合、ローカルストレージはリロードする度にリセットされてしまって値が保存されないし、セッションストレージは無効になっててエラーが出る。Chrome、Safariでは特に問題ない。

どうも。「いらないって言ってたのに開発機をいじってたら欲しくなっちゃいましたの法則」が発動してiPadも先週結局買っちゃった僕です。

iPad買ったら当然JailBreakしてターミナルでコード書きまくってやる、と思ってたんだけど、いきなりmobileterminalが動かなくて躓く。ふぁっきん。とは言え自分で修正するとか新しく作るとかまでする気も起きず、あーそうだJSとかHTMLくらいだったらiPad上で書いたりデバッグしたりできるエディタあったよねーと思って、AppStoreで探してみたらちらほら見付かるも、コレ、というのはまだ無い様子。当たり前だけど、Syntax Highlightができるメモ帳程度なんだよなー。

エディタのViewを作る

ここで「タッチインターフェースを有効活用したコードエディタ」ってのを思い付いたら一時代築けそうなんだけど、別に今のところなんかアイディアがあるわけでもないので、誰かが作ればいいなー、もしくはアイディアくれるといいなーとか言うだけ言っておいて、とりあえずごく普通のテキスト入力画面の話をする。

例えば

  • 画面いっぱいにTextViewが表示されてて
  • Viewが表示されたらTextViewにフォーカスが当たるようにして
  • キーボードが表示されたらそれに合わせてViewをリサイズもしくはスライドさせて
  • キーボードが非表示になったらまたViewをリサイズもしくはスライドさせる

ようなごく普通のテキスト入力画面を作りたいとする。

TextViewにフォーカスを当てるのは簡単で、例えばViewControllerのviewDidAppearとかの中でTextViewのresignFirstResponderメソッドを呼んでやればいい。そうするとTextViewにフォーカスが当たってキーボードがせりあがってくる。キーボードのサイズは、UIKeyboard(Will|Did)(Show|Hide)Notificationていう名前でキーボードの表示、非表示の際に通知が飛ぶので、それをViewControllerで受けてViewをいじってやればいい(この記事とかが参考になった: 画面いっぱいのUITextViewがキーボードに隠れないようにする ? LANCARD.LAB|ランカードコムのスタッフブログ)。リンクの記事だとアニメーションさせてるけど、UIKeyboardDidShowNotificationを受けて単にself.view.frameの値を変えてやるだけでもそれっぽくなる。

あれ、Viewが消えた

iPhone/iPod Touchだとこれで上手く行くんだけど、実はiPadだとこのままだと上手く行かない。どうなるかというと、「日本語キーボードと英語キーボードを切り換えるとViewがどんどん小さくなる、消える」みたいなことになる。これ何でなのかなと思ったら、iPadだと日本語キーボードのときはキーボードの上に変換候補を表示するバーが出るのでその分だけ英語キーボードと表示領域のサイズが違うんだけど、表示領域のサイズが変わる度にUIKeyboard(Will|Did)ShowNotificationが通知される。前述のコードだとkeyboardWillShow:が呼ばれる度にキーボードの高さの分だけViewの高さを縮めてるので、連続でkeyboardWillShow:が呼ばれちゃうとどんどん小さくなってしまうという話。

これを防ぐには、単にself.view.frame.size.heightからキーボードの高さ分丸ごと引くんじゃなくて、UIKeyboardFrameBeginUserInfoKeyでキーボードの表示領域変更前の、UIKeyboardFrameEndUserInfoKeyで変更後のサイズが取れるので、その差分を取ってViewのサイズを調整してやる必要がある。例えばこんな感じ。

- (void)keyboardWillShow:(NSNotification *)notification {
  NSDictionary *userInfo = [notification userInfo];
  UIView *superview      = self.view.superview;
  CGRect beginRect       = [superview convertRect:[[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue] fromView:nil];
  CGRect endRect         = [superview convertRect:[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
  CGRect superviewFrame  = superview.frame;
  CGRect viewFrame       = self.view.frame;

  viewFrame.size.height -=
    (beginRect.origin.y > superviewFrame.size.height) // キーボードが表示されてないときは origin.y が全体の表示領域より下になってる
    ? endRect.size.height
    : endRect.size.height - beginRect.size.height;

  // 他にもやることがあればごにょごにょ

  self.view.frame = viewFrame;
}

hideの方は連続で呼ばれることはないのでそのままでも使えるけど、これも「既に非表示になってたら処理をスキップする」とかちゃんと入れといた方がいいです。あと、iPhoneSDK 3.2からUITextFieldやUITextViewにカスタムキーボードを付けてあげることができるようになったけど、iPhoneではまずやらないと思うけどiPadでは「それぞれの入力欄にそれぞれのカスタムキーボードを設定してあり、しかも全部形が違う」みたいなこともやろうと思えばできる。そうなるとコードのカオス度が格段に上がるし、上記のコードだとまた残念なことになるのでご注意を。

Objective-Cで、参照先のオブジェクトから参照元に通知を送る方法。」を読んてで思ったんですが、若干冗長な気がする。

プロトコルを使う

プロトコルを使うんだったら、最初からそのプロトコルに適合するオブジェクトしかdelegateになれないようにしちゃった方がいいと思う。こんな感じで。

@interface Foo : NSObject {
  id<SampleDelegate> delegate;
}
@property (assign) id<SampleDelegate> delegate;
@end

こうしちゃえば、そもそもSampleDelegateに適合しないオブジェクトはdelegateになれないので、conformsToProtocolのチェックは必要なくなる。UIKitでも(例えばUITableViewのdelegateとか)大体そうなってますね。あと、ObjCはレシーバがnilの場合はメッセージ送信を単に無視するので、respondToSelectorとか送ってもYESが返らないのでこの場合はnitチェックもしなくていいと思う。だから、notifyObjectChangedの定義はこれで十分。

- (void)notifyObjectChanged {
  if ([self.delegate respondsToSelector:@selector(objectChanged:)]) {
    [self.delegate objectChanged:self];
  }
}

非形式プロトコル(カテゴリ)を使う

プロトコルがoptionalなメソッドしか規定してなくて、あるメソッドを定義したデリゲートがある場合はそれを呼ぶけど、そうでない場合は無視するかデフォルトの動作をする、っていうような場合は、そもそもプロトコルで規定しないでカテゴリで実現しちゃう手もある。

@interface NSObject (SampleDelegate)
- (void)objectChanged:(id)object;
@end

@interface Foo : NSObject {
  id delegate;
}
@property (assign) id delegate;
@end

ちなみにnotifyObjectChangedの実装は同じでいい。このタイプの例はNSURLConnectionとか。

こっちの利点はプロトコルの宣言がいらないので、既存のクラスや外部のライブラリのクラスのインスタンスでもdelegateにできること。それが何であるかはどうだっていいんだ、ただ送ったメッセージに答えてくれる何かでありさえすれば、みたいなときにはこの方法でもいい。

欠点は、プロトコルと違ってコンパイル時にdelegateが期待してるオブジェクトかどうかを解決できないこと。元記事の例みたいにoptionalなメソッドしか規定してない場合だと、プロトコルに適合してるからと言って期待してるオブジェクトかどうかははっきりしないので大差ないんだけど、仮にそうであってもプロトコル宣言を強制することによってデリゲートになり得るクラスを限定するってこともあるので、この辺はケースバイケース。

Key-Value Observingを使う

あと、「あるオブジェクトのあるプロパティが変更されたことを知りたい」っていう用途に限って言えば、Key-Value Observingを使う手もある。

@implementation Bar

- (void)test {
  Foo *f = [[Foo alloc] init];
  [f addObserver:self
      forKeyPath:@"hoge"
         options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
         context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    // do something
}

@end

こういう風にしておくと、fのhogeプロパティの値が変更されたときに、BarのインスタンスのobserveValueForKeyPath:ofObject:change:context:メソッドが呼ばれる。その際、keyPathには@"hoge"が、objectにはfが、changeには{ old: /* 変更前の値 */, new: /* 変更後の値 */ }というDictionaryが、contextにはNULLが渡ってくる。

これの利点は、「Fooクラス側には全く手を加えなくていい」「あらゆるオブジェクトに対して、一貫したインターフェースで同じように設定できる」ということ。Cocoaのクラスだろうが外部のライブラリのクラスだろうが自分で作ったクラスだろうが同じように「あるプロパティの値が変更されたら教えてねー」っていう設定ができるし、参照先のオブジェクトが参照元のオブジェクトのことを気にする必要が無くなって関係性が緩くできる。Barの仕様を変更したくなってもFooをいじる必要はない。あと、delegateの場合と違って通知を受けとるオブジェクトが複数設定できるので、アプリケーションのあちこちが同時多発的に状態変化するみたいなことができる。

欠点は、「f.hogeに代入するか、[f setValue:obj forKey:@"hoge"]が呼ばれたときだけしか通知されない」ということ。なのでFooクラスの内部でインスタンス変数を直接弄って内部状態が変わったりしたときには通知されない。あとは、あらゆるオブジェクトに対して使えるので便利なんだけど、どのオブジェクトからの通知も必ずobserveValueForKeyPath:ofObject:change:context:を呼ぶので、沢山通知を設定するとobserveValueForKeyPath:ofObject:change:context:の中身が大分カオスなことになるので、ご利用は計画的に。

他には

NSNotificationを使う方法もあるけど、多用すると処理がどこでどうなるかわかりづらくなるので割と慎重に使った方がいいかも。まぁこれはKey-Value Observingでもそうなんだけど、NSNotificationの方がより汎用性が高いのでよりこんがらがり易くてよくハマる。

scrollView.canCancelContentTouches = NO;
[scrollView setCanCancelContentTouches:NO];

って書き方があってこれみんなどういう使い分けしてるんだろうなぁ。。って思ってます。

セッター - poohtarouの日記

セッターというか、ドット記法の話かな。

ドット記法と普通のメソッドの使い分け

まぁ、まずはこんなクラスがあったとします。

@interface Book : NSObject {
  NSObject *title;
}
@property (retain) NSString *title;
@end

このクラスのオブジェクトを作ってtitleを設定/参照するコードはこんな感じです。

Book *book = [[Book alloc] init];
// ドット記法
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);
// メソッド
[book setTitle:@"詳解 Objective-C 2.0"];
NSLog(@"%@", [book title]);

これは上と下どっちのコードも同義。ドット記法でアクセスすると、setTitle:メソッドやtitleメソッドが呼ばれる。getter/setterを自動生成させないで自分で実装した場合もちゃんとそのメソッドを呼んでくれる。じゃあこんな場合はどうか。

id book = [[Book alloc] init];
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);

これはコンパイルエラーになる。id型のオブジェクトのメンバにドット記法でアクセスしようとすると怒られる。いやid型じゃなくてちゃんとクラスを明示して変数宣言すればいいじゃんて?NSArrayのobjectAtIndex:やNSDictionaryのobjectForKey:は返り値の型がid型ですよね。キャストすればいい?だって中に入ってるのがBookクラスのオブジェクトかどうかわかんないじゃん。isKindOfClass:で調べてからキャストする?いやいや、そのArrayの中にはGameクラスのオブジェクトも一緒に入ってて、GameクラスもsetTitleできるからそこは区別なく扱えた方が便利なんだよ!…みたいなこともあるわけで。そんなときはこう書くかなぁ。

id obj = [array objectAtIndex:0]; // Bookのオブジェクトが入ってると期待できるとする
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj performSelector:@selector(setTitle:) withObject:@"化物語 上"];
}

はいできた。これでBookクラスのオブジェクトをid型の変数で受けてもちゃんとsetTitle:できました。ついでにsetTitle:メソッドを実装してるオブジェクトであれば、Bookクラスと継承関係になくてもsetTitle以外は全然無関係のインターフェースを実装してようとも同じように扱えます。アヒルのように鳴くものはアヒルではなく隣のおばあちゃんでした。じゃなかった、アヒルのように鳴くものはアヒルです。ちなみに、上のコードは実は

id obj = [array objectAtIndex:0];
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj setTitle:@"本当は怖いグリム童話"];
}

って書いてもコンパイル通るし実行時にエラーも出ない。まぁ警告出るけどね。んで、こういう場合はプロトコルを定義してあれば(そしてobjがそのプロトコルに適合してれば)警告が出ない。

// こんなプロトコルがあるとする
@protocol Title <NSObject>
- (NSString *)title;
- (void)setTitle;
@end
...
id <Title> obj;
...
[obj setTitle:@"本当は怖いグリム童話"];

簡単にまとめると、明示的にクラス名を指定して変数を宣言してるときはドット記法で、id型で受けるべき時はメソッドでっていうのが一応の使い分けかなー。元記事の例の場合はscrollViewはUIScrollView型で変数宣言してて、そのコンテキストではUIScrollViewであることがはっきりしてるのでドット記法でいいと思う。具体的なクラスを想定しているわけではなくて、あるアクセサを持っている何かのオブジェクトっていう扱いをするときは、respondToSelector:とperformSelector:を使うか、プロトコルを定義しといてメソッド呼び出しするか、って感じになると思う。

Key Value Conding

ところで、ObjCにはドット記法とメソッド呼び出しの他にもう一つ、オブジェクトのメンバにアクセスする方法がある。しかもこれはプロパティとドット記法が導入されたObjective-C 2.0になる前からある。例えばさっきのBookクラスのオブジェクトに対してだったらこんなことができる。

[book setValue:@"イチャイチャパラダイス" forKey:@"title"];
NSLog(@"%@", [book valueForKey:@"title"]);

恐ろしいですね。まるでオブジェクトがDictionaryか何かのよう。これはNSObjectの子孫にあたる全てのクラスで使える。そしてこれの恐しいのは、アクセサメソッドやプロパティが宣言されてなくても使えてしまうということ。例えばvalueForKey:@"title"だったら、

  1. titleメソッドが定義されてたらそれを呼ぶ
  2. titleメソッドが無くて、getTitleメソッドが定義されてたらそれを呼ぶ
  3. どっちも無くて、インスタンス変数titleが宣言されてたらそれを返す
  4. どれも無くて、インスタンス変数_titleが宣言されてたらそれを返す

っていう順番で解決されるので、Bookクラスの場合titleをプロパティとして宣言してる、つまりtitleメソッドが定義されてるのでそれが呼ばれる。もしプロパティとして宣言してなくても、title変数があるので3番目でひっかかってそれが返る。ちなみにインスタンス変数が@privateで宣言されてても関係ない。便利なんだけど怖い。1、2はともかく3、4は大分アレげなので、内部でしか使わない意図せず変更して欲しくないインスタンス変数は@privateで宣言した上でname_とかして上のルールにひっかからないようにしておく癖を付けたほうがいいと思う。外部からアクセスしてもいいやつでも名前は変えた上で明示的にプロパティで宣言した方が安全かなー。

元記事の話とは直接関係無いけど余談でした。なんでそんな仕組みがあるのってあたりは気になる人は「Key Value Coding」とかで調べてみるといいと思う。これはこれで便利だし、AppKitとかUIKitとかでは結構使われてるので。

追記

そうだブコメで指摘をもらってたんで追記しようと思ってたのすっかり忘れてた。iPhoneアプリでKey-Value Codingを一番使うであろう場面はCoreDataのNSManagedObjectですね。CoreDataでエンティティを定義してデータをつっこんだり取得したりするときは、NSManagedObjectかそれを継承したクラスがモデルオブジェクトになる。サブクラスを作るときはプロパティを定義しとけばいいんだけど、NSManagedObjectにはもちろん中のデータへのアクセサは定義されてない。じゃあどうするかと言うと、valueForKey:とsetValue:forKey:で値の取得や変更をするってことになる。

あと、これの次の記事で書いたけど、Key-Value Observingって仕組みがとても便利なので、プロパティについて調べたらついでにKey-Value CodingとKey-Value Observingについて調べておくといいと思います。まる。

詳解 Objective-C 2.0詳解 Objective-C 2.0
著者:荻原 剛志
販売元:ソフトバンククリエイティブ
発売日:2008-05-28
おすすめ度:4.0
クチコミを見る
Dynamic Objective-CDynamic Objective-C
著者:木下 誠
販売元:ビー・エヌ・エヌ新社
発売日:2009-03-27
おすすめ度:4.5
クチコミを見る

そろそろRuby会議もあるというのに、そういや最近Ruby全然書いてないfaultierですこんばんわ。じゃあリハビリを兼ねて久々にRamazeさんで遊んでみるか、ついでにいい加減NoSQLブームにも乗ってみるか、みたいな感じでRamaze+MongoDB+Candyで遊んでみることにした。ちなみに社内では今Cassandraがブームなのだけども、Cassandraはちょっと遊びで使ってみるにはオーバースペックだよなーとか思いつつ色々見てたら、HerokuでMongoDBが使えるらしいのでそれを狙ってのMongoDBいじり。

とりあえず入れてみる

aptで探したらmongodbのパッケージもあるんだけど、2010年5月30日時点では1.2.2と若干バージョンが古い。この後オブジェクトマッパーを色々試してみたところ1.4系じゃないと動かなかったりしたので、本家のサイトから最新のバージョン落としてくる。コンパイル済みのパッケージなので展開して適当なとこに置いとくだけでOK。手元の環境はUbuntu 10.04の64bitなのでLinux 64bitってやつを選んだけど、OSX 32/64 bitとかWindows 32/64 bitとかもあった。

$ mongo
MongoDB shell version: 1.4.3
url: test
connecting to: test
type "help" for help
>

ふむ。繋った繋った。このあとチュートリアルとかにある例通りちょこちょこいじってみたけどちゃんと使えるようだ。

Rubyから使う

Rubyのdriverはgemからインストールできるので普通にgem install mongoとかやる。依存モジュールでbsonてのが入るけどなんじゃろ、と思ったんだけど、RubyのオブジェクトをMongoDBのBSONオブジェクトにシリアライズするモジュールだけ別のパッケージにしてあるらしい。んで、bsonはC拡張があって、そっちを入れてないとこんなメッセージが出る。

rb(main):001:0> require 'mongo'

**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance.
  You can install the extension as follows:
  gem install bson_ext

  If you continue to receive this message after installing, make sure that the
  bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.

=> true

まぁローカルの開発環境だと別にどっちでもいいんだけど、サーバに置くときはどうせ使うだろうしbson_extも入れておく。

# coding: utf-8
require 'mongo'

con   = Mongo::Connection.new # 何もオプション指定しないと localhostの28017ポートに繋ぐ
db    = con.db('candy')
udons = db.collection('Udon')
udons.insert({
  'name'  => 'ぶっかけ',
  'type'  => '冷たいうどん',
  'price' => 350
})
udons.insert({
  'name'  => 'かけ',
  'type'  => '温かいうどん',
  'price' => 300
})
udons.insert({
  'name'  => '釜玉',
  'type'  => 'かまあげうどん',
  'price' => 400
})

p db.collection_names  #=> [ 'system.indexes', 'Udon' ]
p udons.count          #=> 3
p udons.find_one       #=> {"_id"=>{"$oid"=>"4c0273712e119e3fd7000001"}, "name"=>"ぶっかけ", "type"=>"冷たいうどん", "price"=>350}
p udons.find('price' => {'$gte' => 350}).count #=> 2

こんな感じでさくっと使えた。ほー。あとはMapReduceとか試してみようかと思ったけどまぁそれはおいおい。

オブジェクトマッパー

ぶっちゃけmongo-ruby-driverだけでも十分色々できるし、O/Rマッパー…あーいやRelationじゃないから、O/Dマッパーか、は無くてもいいかなと思ったけども、Ramazeとかで使うことを考えたらやっぱりあった方がいい(工夫してやれば既存のヘルパーとか使えそうなので)。んでちょろっと見てみたら、結構色々あるんだけど、どれもARっぽいんだよなぁ。なんというか、ちょっと過剰なのでもっと薄いのがいい。そしてMongoMapperもMongoidもActiveSupportとかvalidation用のライブラリとか入れちゃう。Railsで使うんならいいんだけど、というかRailsでARの代わりに使うのを想定してるっぽくて(ActiveModelってのがあるんだね。最近Rails全然見てないので知らないんだけどRailsもModel抽象化の流れなんかな)、悩ましい。つーかActiveSupportは入れないで欲しいなぁ…。

個人的には一覧の最後にあったCandyが良さげに見えた。なにしろ「ゴールはActiveRecordやDataMapperのミラーじゃない」ってREADME冒頭で明言してるし、mongo-ruby-driver以外には依存してないのが素敵。使い方もCandy::Pieceをincludeするだけとシンプル。

# coding: utf-8
require 'candy'

class Udon
  include Candy::Piece
end

kamaage = Udon.new
kamaage.name = 'かまあげ'
kamaage.type = 'かまあげうどん'
kamaage.price = 350

ちなみに、何も指定しなければこれでlocalhostの28017ポートに繋いで、candyというデータベースのUdonコレクションにアクセスする。つまりこのコードだと、一個前のコードと同じデータベースの同じコレクションから取ってくることになる。これはCandyクラスかUdonクラスのクラスメソッドで参照/変更できる。Candyクラスで設定すると全てのデフォルト設定になるし、クラスメソッドで設定するとそのクラスのデフォルト設定になる。Ramazeだったら、model/init.rbあたりでこう書いとく。

# coding: utf-8

require 'candy'

Candy.host = "mongdb.example.com"
Candy.port = "24423"
Candy.db   = "udonapp"

require __DIR__('udon')

Udon.collection = 'menrui' # クラス名じゃないコレクション名にしたい場合

あとsaveメソッドはない。なんかこれもポリシーらしくて、オブジェクトを操作したら即時反映される。method_missingでフックして随時更新をかけてるので、もしvalidationとかしたい場合は、例えばpriceが数字でなければならなくて、1000円超えるようなお高いうどんは売っちゃいけないポリシーにするとしたら、こんな感じのメソッドを生やしてやる。superが肝。

class Udon
  include Candy::Piece

  def price=(val)
    raise '値段は数字だっつってんだろ' unless val.kind_of?(Integer)
    raise 'たけーよアホか' unless val < 1000
    super
  end
end

ちなみにバリデーションとかエラーハンドリングとかnamed scopeとかの簡単な実装例はCandyのドキュメントに載ってるので見てみるといいと思う。まぁ若干面倒ではあるけど、正直こんなのでいいよなーと思った。しばらくこれで遊んでみよう。

…というあたりまで書いて大変致命的なことに気付いたんだけど、Candyは1.9系じゃないと駄目らしいんだけどHerokuってRuby 1.9系だっけ…?多分違うよね…?うーん、まぁ、ちょうど借りてるサーバもリビルドしたことだし、とりあえずそのサーバで動かせればいいか…。

Rubyと比べながらBlocksをいじってみたりBlocksでNSArrayにmapメソッドを生やしてみたりしてきたので、そろそろGrand Central Dispatch(GCD)も試してみる。あんま関係ないけど、グランド・セントラル・ディスパッチってなんか必殺技っぽいよね。じゃあ一緒に高らかに叫んでみようか。せーの、グランド!セントラル!ディスパッチ!!

GCDってなにさ

ドキュメント嫁。

…だけだと流石に不親切なので、一応簡単に説明すると、APIを通してぽんぽん処理をqueueにつっこんでってやると、ランタイムの方でそれを上手いこと並列実行しといてやるよ!安心しろチェリーボーイ共、スレッドのことは俺が面倒見てやるぜ!って仕組み。そんな口調なのかどうかはわかんないけど、まぁ大体そんな感じ(適当)。

例によってRubyと比較

まぁ、こんなコードがあったとします。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  f.call
end
p Time.now - t

んで、それを例によってObjCでBlocksを使って書くとこんなコードになります。ちなみにどっちも別にBlocksやlambdaでやる必要は無いんだけど、この後のコードと比較の為にわざとそうしてるのでスルーしておくれやす。

# ObjC
void (^f)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
};
NSDate d = [NSDate date];
unsigned int i = 20;
while (i--) {
  f();
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

まぁ見ての通り、標準出力に一言言ってから0.25秒sleepするだけの簡単なお仕事を20回ほどやってもらってます。あたり前のことだけど、0.25*20で5秒+αくらいの時間がかかるし、0.25秒っつってんのに5秒待たせるとか相当いい加減なやつだ。

んで、こんな風に、それぞれの処理が独立してるけど一個一個は結構時間かかる、みたいなのは、並列に実行させちゃったらいいんじゃね、みたいなことを偉い人は言いました。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  Thread.new { f.call }
end
(ThreadGroup::Default.list - [Thread.current]).each{|th|th.join}
p Time.now - t

まだ若干怠けてるけど、まぁ0.3秒行かない程度で終わる。5秒に比べたら一瞬みたいなもんだよね。

じゃあ次はそれをObjCで…と言いたいところなんだけど、ObjCで上のRubyのコードをNSThreadってクラスを使って書こうとすると、割と面倒い。特定のコンテキストを別スレッドで実行しようと思うと、detachするのにtargetとselector、つまりスレッドで実行されるオブジェクトとそいつから呼び出されるメソッドがなくちゃいけない。んでもって、作ったスレッドを自分で管理しなきゃいけない。一応適当なサンプルは書いたけど、あんまりこれをObjCで自前で書くことは無いと思う(理由は後述する)。

さて、GCDです

最初の方に言ったけど、大分ざっくり言うとGCDってのは「処理のブロックをキューにつっこんでってやると裏で上手いこと並列に処理してくれる」ものです。要は並列処理のめんどい感じを多少楽にしてくれるのがGCDの兄貴だってことです。兄貴なのか姉貴なのかは知りませんが。どっちかというと僕はお姉さんが好きですがどうでもいいことです。

# ObjC
dispatch_block_t block = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};
//NSLog(@"%@", [NSThread currentThread]);

NSDate d = [NSDate date];

// ここからGCD登場
dispacth_group_t group = disptach_group_create();
disptach_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
unsigned int i = 20;
while (i--) {
  disptch_group_async(group, queue, block);
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// ここまでGCDのお仕事

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

はい。Rubyの方をThreadを使って書き直したときと同じく、0.3秒行かないくらいの時間でさくっと処理してくれました。コメントアウトしてるのを戻せば、ちゃんと別々のスレッドで動いてるのも確認できると思います。ちなみにObjCの文法で書いてるとこを削れば普通にCでも使えます。使いたいときは#include <dispatch/dispatch.h>してください。

変わったところを解説すると、まずブロックの型がdispatch_block_tに変わってる。これは後で出てくるdispatch_group_asyncの引数の型なんだけど、void (^)(void)、つまり何も取らず何も返さないブロックって定義になってるので実はさっきと何も変わってない。

次にdisptach_group_createをしてgroupを作ってるけど、これはまぁ名前通り非同期に実行する処理をグルーピングするためのもの。Rubyの方でもThreadGroupが出てきたけど、あれと一緒で最後のdispatch_group_waitで一連の処理が全部終了するまで待ってやる為に使う。今回は全部の処理が終わるまでの時間が見たかったのと、アプリケーションとかじゃない普通のCUIのコマンドとして作ったときに何も考えずに非同期処理させるとdispatchしたのが終わる前にmainが終わっちゃうのでwaitする必要があったけど、GUIアプリケーションとかデーモンとかだとその心配はないのでグルーピングせずに単にdispatch_asyncしちゃってもいい。

んでここからが本質、dispatch_get_global_queueとdispatch_async(またはdispatch_group_async)。といっても別に大したことではなくて、

  1. queueを用意します
  2. dispatch_asyncにqueueとblockを渡します
  3. あとは裏でよしなにやってくれます

以上。中では「システムの負荷を見てスレッドを作るか待つか決める」「スレッドが一個も空いてなければ作るけど、さぼってるやつがいたら再利用する」「あっちこっちから放り込まれたブロックをどのスレッドに割り当てたら効率良いか考える」とか色々やってんだけど、使う側としてはそんなこと気にする必要無いし、それどころかスレッドが作られてることすら隠蔽されてる。やったのは単に関数にブロックを渡しただけ。ゆとりの僕でもできる簡単なお仕事です。ちなみにdispatchしてwaitするあたりの処理を続けて何回も実行すると、ちゃんとスレッド再利用してるのが確認できる。

他にも、globalって名前が付いた関数があるからにはglobalじゃないqueueを作る関数もあるとか、asyncって名前が付いた関数があるからにはsyncして実行する関数もあるとか、メインスレッドで動作するqueueがあるとか、さっきのコードではわざわざforループ回したけどループにはループ専用のdispatch_applyがあるとか、まぁ色々あるんだけど、Xcodeのあのアホみたいに使い辛いドキュメントビューワの検索窓にdispatch_って入れてやるといっぱい出てくるので見てみてくれればいいかと思います。

でもそれCじゃん

ええ。ここまでは誰がどうみてもCの関数、っていうかさっきも書いた通り実際<dispatch/dispatch.h>をincludeすればCでも使えるAPIです。いやさ、確かにObjCの文法はキモいけどさ、せっかくObjCで書いてるのにCの関数使うってどうなのよって?ご心配なく。ObjCならObjC流に実装する方法はもちろんある。

「並列実行」「どんどんキューにつっこむ」「スレッドの面倒はキューが見てくれる」あたりで、LeopardまでのOSXとかiPhoneSDKとかでアプリを書いたことある人は「それNSOperationとNSOperationQueueでできるじゃん」と思ったはず。これもキューにオペレーションオブジェクトをどんどんつっこんで行けば非同期でよしなにやってくれるクラスで、それ自体はGCDやBlocksとは関係なく前から使える。ので、もともとあんまり自前でNSThreadを作ったり管理したりはやったことなかった。じゃあObjCだと何も変わらないじゃんと思ったけど、Blocksが導入されたことでNSArrayやNSDictionary同様便利な機能が追加されているという。

# ObjC
void (^block)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};

//NSLog(@"%@", [NSThread currentThread]);
NSDate d = [NSDate date];

// こっからNSOpera(ry
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
unsigned int n = 20;
while (n--) {
  [queue addOperationWithBlock:block];
}
[qeueu waitUntilAllOperationsAreFinished];
// ここまでNSOpera(ry

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

おお、これは便利だな。Blocsを使わない場合は事前にNSOperationクラスを継承して独自のOperationクラスを作っとくとか、NSInvocationOperationを使うとかしてたところを、-[NSOperationQueue addOperationWithBlock:]を使えばブロックを渡してやるだけで非同期実行できちゃう。ブロックをひとまとめにしてオペレーションにしてくれるNSBlockOperationってクラスや、オペレーションが終了した後に実行される処理をブロックで設定できる-[NSOperation setCompletionBlock:]ってメソッドが追加されてて、「処理を一括りにして並列実行する」コードが大分書きやすくなっている。

MacRuby

RubyとObjCの話をしてるのにMacRubyさんを完全にスルーするという素敵なプレイをやってのけてきたわけですが、実はMacRubyさんはとっくにGCDに対応してやがります(MacRuby » An Introduction to GCD with MacRuby)。凄いな、ほんと、どこまで行くんだろう。

ついでに

Blocks入門NSArrayにmap生やしたのとこの記事のコードをのサンプルはgistに上げてみたので、まぁ一応一通り動く例になってるはず。あーあと簡易ベンチマーククラスみたいのも作ってみたので入れといた。適当なので実用するのはおすすめしません。Snow Leopardでrubyとrakeが入ってる環境なら、cloneしてきてrakeすればコンパイルできるはず。

参考記事

↑このページのトップヘ