shimada-kの日記

ソフトウェア・エンジニアのブログです

httperfでnginxにおける非同期処理の限界を知る

nginxに負荷をかけて遊んでみました。webによくあがっているnginxのベンチマークは適当な静的ファイルを1つ用意してそこに対してアクセスを行うといったものです。

ただそれだとメインメモリのファイルキャッシュにのっているものを返すだけで、オンメモリで完結する処理のはずなので非同期処理が売りのnginxのベンチマークとしては微妙じゃないかなと思うわけです。

そこでメインメモリのキャッシュではまかなえない量のデータを用意してかつ複数ファイルに対してアクセスを行うようにして負荷をかけてみました。負荷をかけるツールはhttperfを使用しました。

ネットワークの帯域がボトルネックになっていないことを確認するために、事前にiperfで帯域を調査しておきます。自宅のLANと使用するシステムはすべて1000BASE-Tで接続されているので理論値では1Gbits/sの速度がでるはずです。

------------------------------------------------------------
Client connecting to 192.168.3.3, TCP port 5001
TCP window size: 23.5 KByte (default)
------------------------------------------------------------
[  3] local 192.168.3.8 port 42975 connected with 192.168.3.3 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec  1.09 GBytes   936 Mbits/sec

まあ実効値はこんなもんです。ネットワーク帯域はフルに使用可能なことが分かったのでOSの設定をしてopen(2)できるファイルディスクリプタの上限値を引き上げます。

まずはユーザプログラムが開くことができるファイルの最大数を引き上げるためlimit.confを変更します。

#ftp             hard    nproc           0
#ftp             -       chroot          /ftp
#@student        -       maxlogins       4

*               soft    nofile          1048560
*               hard    nofile          1048560

再起動してulimit -nで確認します。

shimada-k@backend:~$ ulimit -n
1048560

これでOS側の設定値は変更しました。今度はhttperf側で開くことができるファイル数を変更します。httperfがコードから参照しているFD_SETSIZEの値を変えてビルドしなおさないといけません。

これは/usr/include以下のヘッダファイルを書き換えることで対応します。OS側に設定した値と同一のものを設定しました。

/usr/include/linux/posix_types.h

#define __FD_SETSIZE    1048560

/usr/include/x86_64-linux-gnu/bits/typesizes.h

#define  __FD_SETSIZE       1048560

これと同様の手順をnginxを動かすサーバ上でも行います。httperfに対してはポートの再利用の設定をソケットに対して行うコードを追加してビルドします。

core.cの941行目付近です。

int on = 1; 
setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on));
SYSCALL (BIND,
    result = bind (sd, (struct sockaddr *)&myaddr,
        sizeof (myaddr)));
    if (result == 0)
        break;

これでhttperfのソースをDLしてビルドします。nginxもビルドします。ここまでで限界突破されたhttperfとnginxができますので、open(2)できるファイルディスクリプタの上限値がボトルネックになる可能性はなくなりました。

さらにhttperf側でTCPポートの再利用の設定とTCPのTIME_WAIT対策としてWAIT時間の設定をします。LAN内なので短めに設定してます。これをしないとhttperf側で使用できるポート番号の数がボトルネックとなり、bind(2)がエラーになります。 /proc/sys/net/ipv4/以下のファイルに数値を書き込むことで対応します。

echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
echo 10 > /proc/sys/net/ipv4/tcp_fin_timeout

加えてnginxの設定でworker_connectionsを引き上げておきます。デフォルトでは1024ですが4096にあげておきます。

events {
    worker_connections  4096;
}

実験環境を書いておきます。

クライアント側

  • OS:Debian Wheezy (3.2.0-4-amd64)
  • Core(TM) i7-2600 CPU @ 3.40GHz
  • Memory 8GB
  • 自家ビルドhttperf(0.9.0ベース)

サーバ側

  • OS:Debian Wheezy (3.2.0-4-amd64)
  • Core(TM) i5-3470T CPU @ 2.90GHz
  • Memory 8GB
  • 自家ビルドnginx(1.7.2ベース)

なお今回nginxのworker_processesは1を設定してあります。nginxを動かすマシンは4Coreマシンですが1プロセスで処理を受け付けます。

アクセス先のデータとしては10KByte分のテキストが書き込まれているファイルを3355440個用意しました。合計で32GBになります。ファイルパスは後述するhttpertの引数に列挙してあります。nginx側のサーバには8GBのメインメモリをつんでありますが、用意した32GByte分のデータをすべてキャッシングすることは不可能です。

httperfを実行する際に引数でリクエスト先のパスが書かれたファイルを指定します。ただ毎回同じ内容のファイルを指定するとメインメモリのキャッシュにのってしまうので、ランダムに並べ替えてhttperfで読み込むようにします。毎回ランダムアクセスが発生させることで、ディスクアクセスを一定数発生させることを目的としています。

#!/bin/bash

#-----------------------------------------------
# パスが書かれたファイルをランダムに並べ替えて
# httperfで負荷をかけるプログラム
#-----------------------------------------------

HTTPERF='/home/shimada-k/httperf-0.9.0/src/httperf'
SERVER='192.168.3.3'

if [ ! ${#} -eq 1 ]; then
    echo 'usage: httperf_text [num request rate]'
    exit 0;
fi

RATE=${1}
NUM_CONNS=`expr ${1} \* 50`

# echo ${RATE}
# echo ${NUM_CONNS}

# ランダムに並べ替える
cat list_text | sort -R > list_text_random
echo 'sort file is ready'

# httperfが読み込めるようにnull区切りのファイルにする
cat list_text_random | tr "\n" "\0" > list_text_uri
echo 'httperf uri file is ready'

echo "${HTTPERF} --hog --server=${SERVER} --port=80 --uri=/ --wlog=Y,/home/shimada-k/list_text_uri --rate=${RATE} --num-conns=${NUM_CONNS}"
${HTTPERF} --hog --server=${SERVER} --port=80 --uri=/ --wlog=Y,/home/shimada-k/list_text_uri --rate=${RATE} --num-conns=${NUM_CONNS}

上記シェルスクリプトは第1引数をrateに設定して、rateに50を掛けたものをnum-connsに指定しています。50を掛けている理由は、httperfのReply rateは5秒間隔でスコアを蓄積している*1ため、ある程度長い時間負荷をかけないとReply rateの数値を均一な精度で取得できないからです。今回は上記スクリプトに500から2500まで100刻みで引数を与えてhttperfのスコアを記録しました。

rate second(s) req/s Repy rate(min) Reply rate(avg) Reply rate(max) Net I/O(*106)
500 50.002 500 499.8 500 500 42.3
600 50.003 600 599.8 600 600 50.7
700 50.001 700 699.8 700 700.1 59.2
800 50.001 800 799.9 800 800.1 67.6
900 49.997 900.1 899.9 900.1 900.1 76.1
1000 49.997 1000.1 999.9 1000.1 1000.1 84.5
1100 50.001 1100 1099.9 1100 1100.1 93
1200 49.997 1200.1 1198.9 1199.9 1200.3 101.4
1300 49.995 1300.1 1300.1 1300.1 1300.3 109.9
1400 50.002 1400 1399.7 1399.9 1400.1 118.3
1500 49.997 1500.1 1499.9 1500.1 1500.1 126.8
1600 49.992 1600.3 1599.7 1600.2 1600.7 135.3
1700 49.996 1700.1 1625.9 1700.1 1774.3 143.7
1800 49.997 1800.1 1790.3 1800.1 1809.9 152.2
1900 50.36 1886.4 1899.1 1899.6 1899.7 159.5
2000 49.997 2000.1 1999.8 2000.1 2000.2 169.1
2100 49.994 2100.3 2099.9 2100.2 2100.8 177.5
2200 50.18 2192.1 1904.3 2197.7 2477.6 185.1
2300 50.012 2299.5 1965.9 2292.9 2568.4 193.8
2400 50.882 2358.4 1924.1 2392.9 2791.8 199.3
2500 50.582 2471.3 2061.5 2499.4 2917.1 208.9

上記表が取得した生データです。request/sとReply (min)が一致していればnginx側においてキャパ内の処理ができていると考えていいと思います。すべてのrateにおいてNet I/Oがiperfで取得した帯域以下なので通信がボトルネックにはなっていません。さらにnginxのerrorログにも特にエラーは記載されていないことを確認しているのでファイルopen数の上限もボトルネックにはなっていません。

このままだと見にくいので、Reply rate(min)をrequest数で割った数値をグラフにしてみました。

f:id:shimada-k:20140708020007p:plain

1700付近から投げたリクエスト数に対してReply rateが安定しなくなっていて、rateが2200以降は一気にグラフが下がっています。実験した環境においては2200並列程度がnginxにおける非同期処理の性能限界なのかなと思います。

目的の場所に的確に負荷をかけるためにはボトルネックを1つずつ取り除く必要があり、ハマりポイントが多かったので苦労しました。

テックヒルズ×iBeacon ハッカソンに参加してきました

テックヒルズ×iBeacon ハッカソン

iOS7からCoreLocationに追加された機能でメジャー番号とマイナー番号とUUIDを使って通信します。beacon製品は2013年11月から国内で販売されました。

作成したのはWebsocketとiBeaconを連携させたアプリで主にサバイバルゲームでの使用を提案させて頂きました(サバイバルゲームをやったことはありません)。

f:id:shimada-k:20140629224731j:plain

味方が所持しているbeaconの領域に敵が入ると検知して、「敵が近くにいる!」と教えてくれるアプリになります。beacon側の情報として相対距離が取得できるのでその情報によってより近くに敵がいる場合はUIViewのbackgroundColorを変化させるようにしました。

f:id:shimada-k:20140630231224p:plain

画面に表示されているのはハードコーディングされた仮想user_idです。SNSと連携することでもっと凝った演出ができると考えています。ソースコードgithubに上げておきました。

一番ハマった点は敵がbeaconの領域に入った情報を検知する側のアプリでWebsocketを使用するためにAZSocketIOを使用してるんですが、サーバ側でsocket.ioのバージョンが1.0だとうまく通信できないことでした。

Leap Motion SDK V2 Betaで遊んでみる

Leap Motionの新しいSDKのpublic Beta版が5月28日に公開されました。試してみて分かったことを書いておきます。

まずはビジュアライザーを起動してみます。

f:id:shimada-k:20140619005909p:plain

グーも認識してくれます。

f:id:shimada-k:20140619005922p:plain

X方向に手を重ねるのはこれくらいが限度。これ以上近づけると片方が認識から外れるか指の認識がうまくいかなくなりました。

f:id:shimada-k:20140619005936p:plain

Y方向に手を重ねるのはこれくらいが限度。試した感じだと手の平の部分が重なるとダメみたいです。これ以上重ねると上にある方の手が認識から外れます。

SDK V2の新しい機能としてboneが取り上げられることが多いですが、右手・左手の区別って結構難しいのではないかと思っているので、センサーが検出可能な範囲に手が存在する場合、特定の状況下で右手・左手がどの程度認識できているのか試してみました。

leap motionが認識した手の信頼度を出力するHTMLを作って調査しました。コードはGistにあげておきました。

Gist

左手のデータは左側の青いdivに、右手のデータは右側の赤いdivに出力されます。片方の手しか認識されて無い場合は認識されて無い方のdivのフォントの色をグレーアウトします。

今回は以下の4つのパターンで、左手・右手の判定、信頼度を取得してみました。

  1. 手の甲を上にした片手(左手)
  2. 手の甲を下にした片手(左手)
  3. 手の甲を上にした両手
  4. 手の甲を下にした左手と手の甲を上にした右手

左手・右手はhand.typeで、信頼度はhand.confidenceで取得できます。両方ともSDK V2から使用可能なパラメータです。

手の甲を上にした片手(左手)

信頼度は1なので左手と認識されてます。そのまま手のひらをひっくり返しても左手と認識されたままでした。手のひらをひっくりかえす間は手のセンサーに対して手のひらの角度が変化するので信頼度も下がるようです。

f:id:shimada-k:20140621004356p:plain

手の甲を下にした片手(左手)

右手と認識されました。信頼度も1に近い数値なので右手だと認識されているのでしょう。

f:id:shimada-k:20140621004408p:plain

手の甲を上にした両手

両手とも正しく認識されています。信頼度は両手とも1です。

f:id:shimada-k:20140621011401p:plain

手の甲を下にした左手と手の甲を上にした右手

データは0.9付近と1が交互に出力されています。左手のdivには何も出力されていないので左手は存在しないと認識されているのでしょう。

右手のdivには「same hand detected」が出力されているので同じ手が2本あると認識されています(hand.typeが同じであるかどうかで「same hand detected」を出力しています)。

f:id:shimada-k:20140621012506p:plain

leap motion楽しいです。boneやgestureも試してみたいですね。

nginxは新規リクエストをどう受け付けるのか

nginxで新規のリクエストが発生した場合の処理をソース上で追いかけてみました。

nginxはワーカープロセスを指定した数だけ起動させてそのプロセス内で非同期にリクエストをさばいていますが複数のワーカープロセスが存在した場合、新規リクエストはどう振り分けられるのか気になったからです。

デバッグログを出力させるためにnginxのソースを本家から落としてきてコンパイルしました(現時点で最新版1.7.1)。

configureの引数に「--with-debg」を付けないとnginx.confでログレベルをdebugにしてもデバッグログは出力されません。

デバッグログを出力可能な状態にしてnginxを起動しブラウザでアクセスすると以下のようなログが出ました。

2014/06/12 23:36:47 [debug] 3932#0: epoll: fd:6 ev:0001 d:0970ABD8
2014/06/12 23:36:47 [debug] 3932#0: accept on 0.0.0.0:80, ready: 0
2014/06/12 23:36:47 [debug] 3932#0: posix_memalign: 096F8C00:256 @16
2014/06/12 23:36:47 [debug] 3932#0: *1 accept: 192.168.80.1:62482 fd:3
2014/06/12 23:36:47 [debug] 3932#0: *1 event timer add: 3: 60000:2309328887
2014/06/12 23:36:47 [debug] 3932#0: *1 reusable connection: 1
2014/06/12 23:36:47 [debug] 3932#0: *1 epoll add event: fd:3 op:1 ev:80002001
2014/06/12 23:36:47 [debug] 3932#0: timer delta: 21126
2014/06/12 23:36:47 [debug] 3932#0: posted events 00000000
2014/06/12 23:36:47 [debug] 3932#0: worker cycle

ログを出力している場所を上から順番に検索しました。1番上のメッセージはngx_epoll_process_eventが出力しているものです。

ngx_epoll_process_events

event/modules/ngx_epoll_module.c

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)

この関数の中でepoll_wait(2)しています。さらにmutexを獲得できた場合は後続の処理を行い最後にunlockするといった実装になっています。ロック取得後はhandler経由でngx_event_acceptを呼び出します。

ngx_event_accept

event/ngx_event_accept.c

void
ngx_event_accept(ngx_event_t *ev)

この関数の中でaccept(2)を実行しています。この関数が2番目と4番目のメッセージを出力しています。ログやソケットの設定をしてngx_add_timerを呼び出した後、handler経由でngx_http_init_connectionを呼び出します。

ngx_event_add_timer

event/ngx_event_timer.h

static ngx_inline void
ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer)

この関数はngx_add_timerという命名でevent/ngx_event_timer.hにおいてマクロ化されているためソース上は直接呼び出されません。タイマーの初期化を行っています。タイマーは赤黒木で管理されていました。この関数が実行されて5番目のメッセージが出力されます。

ngx_http_init_connection

http/ngx_http_request.c

void
ngx_http_init_connection(ngx_connection_t *c)

この中でngx_reusable_connectionが呼び出されて6番目のメッセージが出力されます。

ngx_epoll_add_event

event/modules/ngx_epoll_module.c

static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)

この中ではepoll_ctl(2)を呼び出してepollで監視するイベントを追加しています。この関数が実行されることで7番目のメッセージが出力されます。

ngx_process_events_and_timers

event/ngx_event.c

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)

この関数の中で8番目と9番目のメッセージが出力されます。この関数の中でngx_process_eventsが実行されることで一番最初のngx_epoll_process_eventsが呼び出されます。一番最初のngx_epoll_process_eventsはngx_event_actions_t構造体に登録されていて、process_eventsというシンボルで透過的に呼び出される仕組みになっています。

event/ngx_event.h

#define ngx_process_changes  ngx_event_actions.process_changes
#define ngx_process_events   ngx_event_actions.process_events
#define ngx_done_events      ngx_event_actions.done

#define ngx_add_event        ngx_event_actions.add
#define ngx_del_event        ngx_event_actions.del
#define ngx_add_conn         ngx_event_actions.add_conn
#define ngx_del_conn         ngx_event_actions.del_conn

#define ngx_add_timer        ngx_event_add_timer
#define ngx_del_timer        ngx_event_del_timer

実際の呼び出しの部分はマクロで定義されています。

ngx_worker_process_cycle

os/unix/ngx_process_cycle.c

static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)

最後10番目のメッセージを出力しているのがngx_worker_process_cycleです。この関数内は無限ループになっていて、ワーカープロセスの本体です。実はこの関数の中でngx_process_events_and_timersを呼び出しています。

そもそも、ngx_worker_process_cycleを呼び出しているのはmasterプロセスが呼び出すngx_start_worker_process経由で呼び出されたngx_spawn_procesの中でfork(2)した子プロセスです。

ngx_start_worker_processes

os/unix/ngx_process_cycle.c

static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)

この関数の中で設定ファイルで指定されているワーカープロセスの数だけループを回してngx_spawn_processを呼び出しています。その際に引数でngx_worker_process_cycleの関数ポインタを渡しています。

ngx_spawn_process

os/unix/ngx_process.c

ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
    char *name, ngx_int_t respawn)

この中でfork(2)して第2引数で受け取ったprocを子プロセスが実行しています。第2引数はngx_worker_proces_cycleです。

まとめ

各ワーカープロセスはngx_worker_proces_cycleの中でepollでイベントを監視し、最初にmutexを獲得できたプロセスがaccept(2)を実行します。

最近のカーネルでは複数の子プロセスが同時にaccept(2)を実行してもエラーにはならないはず*1ですが、このような実装になっている理由は公式ドキュメント*2で下記のように書いてあります。

If accept_mutex is enabled, worker processes will accept new connections by turn. Otherwise, all worker processes will be notified about new connections, and if volume of new connections is low, some of the worker processes may just waste system resources.

「アクセスがあった時にすべてのワーカープロセスを起こすのはシステムリソースの無駄遣いである」という設計思想を読み取ることができます。

Temasysを使ってSafari@OSXでWebRTC

WebRTC Meetup Tokyo #2に行ってきました。

WebRTCの仕様の策定状況やブラウザ間のサポートの差異をどうしようかみたいな話を聞いてきました。

WebRTCはChrome/FireFox(Opera)は対応してるんですが、Safariやシェアのほとんどを占めているIEなどは対応してないわけです。

そこでブラウザの壁を超えるための手段としてTemasysが紹介されていました。

Temasysはブラウザのプラグインとして配布されていて、さらにTemasysCommunicationsが配布しているadapter.jsを組み込むとgetUserMediaが使用可能になります。今回はOSX用のプラグインをインストールしてSafariで試してみました。

TemasysCommunications/Google-WebRTC-Samples

上記プロジェクトはGoogleリポジトリからforkされていてadapter.jsは本家のものをTemasysプラグイン用に書き換えたものになっています。

getUserMedia

ブラウザで開くと最初にプラグインを有効にするかどうかとカメラへのアクセスの許可のポップアップが出現します(この辺は本家のWebRTCと似てます)

ポップアップをすべてOKするとちゃんとブラウザ上でストリーミングが再生されます。

f:id:shimada-k:20140604225417p:plain

PeerConnection

ローカルストリームでPeer通信をするサンプルです。

f:id:shimada-k:20140604225437p:plain

画像では分かりませんが、音声と動画が映っています。しかしこのサンプルはadapter.js(2014/06/05修正)main.jsのperformance.now()がsafariではサポートされてないので動かすにはDate.now()に書き換えが必要でした。

MDN Performance.now()

function call() {
  callButton.disabled = true;
  hangupButton.disabled = false;
  trace('Starting call');
  //startTime = performance.now();    // ここをコメントアウト
  startTime = Date.now();    // Date.now()に書き換える

この他にもvideoタグはdivで囲ってあることが前提になっていたり、そもそもSafariではwindow.URL.createObjectURLが使えなかったりと、Chrome/Firefoxで動いているものをそのまま動かすにはまだ多くの壁が残っているような気がします。

WebRTCは単体でも面白いんですが、ARや3Dや位置情報とかと組み合わせるともっと面白いと思っているので、今後仕様の策定が進んで特にモバイル環境で環境気にせず動くようになることを期待したいです。

Safariしか試してませんがIEでも動くはずです(たぶん)。

Node.jsでPromiseを使う場合

先日、東京Node学園12限目に参加しました。そこで@jovi0608さんから紹介があったNode-v0.12に含まれる予定のPromiseを試してみました。試してみて分かったことを書いておきます。

Node-v0.12の新機能について

コールバック地獄の例としてよく挙げられる下記のようなコードのようにディレクトリ間でファイル数とファイル名に差分が無いことを確認できればあとはファイルごとのループに書き換えられるような事例ではPromiseの恩恵が受けられます。

引用元:Node.jsにPromiseが再びやって来た!

fs.readdir(dir1, function(...) {
    fs.readFile(file1, function(...) {
        hash.update(data);
        fs.readdir(dir2, function(...) {
            fs.readFile(file2, function(...) {
                ...
            });
        });
    });
});

ただしMySQLを使用した以下のようなコードはWHERE句に直前のクエリの結果を用いているので、各クエリを非同期で実行できないためコールバック関数で順番に処理せざるを得ません。つまりこれ以上ロジックを変えられないためPromiseを使っても意味がありません。まあ当然です。

connection.query('SELECT * FROM user', function(err, rows, fields) {
    if (err) throw err;

    connection.query('SELECT * FROM user_country WHERE user_id = ' + rows[0].user_id, function(err, rows, fields){
        console.log(rows);
        connection.end();
    });
});

Promiseの恩恵を受けられるのは待ち合わせる目的のみでコールバック関数を使用している場合です。コールバック処理のネストが一掃されるわけではありません。

とはいえ、ディレクトリ操作以外にも外部APIへ複数回問い合わせに行き、それぞれの結果が出そろってから何かをするようなケースだとPromiseの恩恵を受けられるはずです。

例としてtwitterAPIを使用し、自分のPOSTをリツイートしたユーザIDを調査するロジックを実装しました。

twitterAPIはリクエストを投げまくるとHTTP#429(Too Many Requests)を返してよこすのでユーザ数を5としました)

promiseが実装されているNode.jsはgithubのmasterブランチに上がっているものを使用します(2014-05-10現在)。

shimada-k@debian:~/node_pjt/sample/chat$ node -v
v0.11.14-pre
shimada-k@debian:~/node_pjt/sample/chat$ node -e 'console.log(process.versions.v8)'
3.25.30

外部APItwitterAPIを使用し、twitterのNodeモジュールはnode-twitterを使用しました。

コールバックを使用した場合

// tweet_idのツイートをリツイートしたuser_idを返す
function getRetweeter(tweet_id) {
    twit.get('/statuses/retweeters/ids.json', {'id' : tweet_id}, function(retweeters) {
        if(retweeters){
            console.log(retweeters.ids);
            return retweeters.ids;
        }
        else{
            var err = Error("[/statuses/retweeters] APIアクセスエラー");
            throw err;
        }
    }); 
}

// 自分のtweetでリツイートされたものを返す
function getRetweeted() {
    var retweeter_ids = new Array();

    twit.get('/statuses/retweets_of_me.json', {'count' : '5'}, function(tweet) {
        if(tweet){
            for(var i = 0; i < tweet.length; i ++){
                console.log(tweet[i].text + '@' + tweet[i].id_str);

                try {
                    var retweeters = getRetweeter(tweet[i].id_str);
                    retweeter_ids.push(getRetweeter(tweet[i].id_str));
                } catch(e) {
                    console.log(e);
                }   
            }   
            //console.log(tweet);
        }   
        else{
            var err = new Error("[/statuses/retweets_of_me] APIアクセスエラー");
            throw err;
        }   
    }); 

    return retweeter_ids;
}

function getRetweeters() {
    try{
        var retweeted = getRetweeted();
        console.log(retweeted);
    } catch(e) {
        console.log(e);
    }   
}

getRetweeters();

Promiseを使用した場合

// tweet_idのツイートをリツイートしたuser_idを返す
function getRetweeter(tweet_id) {
    return new Promise(function(onFulfilled, onRejected) {
        //console.log(tweet_id);
        twit.get('/statuses/retweeters/ids.json', {'id' : tweet_id}, function(retweeters) {
            if(retweeters){
                console.log(retweeters.ids);
                onFulfilled(retweeters.ids);
            }
            else{
                var err = new Error("[/statuses/retweeters] APIアクセスエラー");
                onRejected(err);
            }
        });
    });
}

// 自分のtweetでリツイートされたものを返す
function getRetweeted() {
    var ids = new Array();
    return new Promise(function(onFulfilled, onRejected) {
        twit.get('/statuses/retweets_of_me.json', {'count' : '5'}, function(tweet) {
            if(tweet){
                for(var i = 0; i < tweet.length; i ++){
                    console.log(tweet[i].text + '@' + tweet[i].id_str);
                    ids.push(tweet[i].id_str);
                }   
                //console.log(tweet);
                onFulfilled(ids);
            }   
            else{
                var err = new Error("[/statuses/retweets_of_me] APIアクセスエラー");
                onRejected(err);
            }   
        }); 
    }); 
}

function getRetweeters() {
    return getRetweeted().then(function(ids) {
        return Promise.all(ids.map(function(id) {
            return getRetweeter(id);
        }));
    }).then(
        function(x) {
            console.log(x);
        },  
        function(err) { // all case of errors
            console.log(err.messages);
        }   
    );  
}

getRetweeters();

エラーの捕捉箇所が一元化されて大分見通しが良くなりました。

imlib2のdraw_textで遊んでみる

Rubyの画像処理ライブラリimlib2で画像に文字を埋め込んで遊んでみました。

環境

入力した文字列の文字数によって適切にフォントのサイズと縦横のマージンを計算し、さらに一行におさまる長さで改行を挟んで画像内に表示するコードを書いてみました。

Gist

フォントの大きさの単位はpt(ポイント)指定ですので、フォントサイズを決める時は画像の大きさとの兼ね合いでpx(ピクセル)からptへ変換する必要があります。

また、LinuxはDPIが96なのでpxで計算した値をフォントサイズとすると画像からはみ出してしまいます。

(imlibのコンパイルオプションで決まっているかX11の設定をAPI経由で引っ張ってきているかだと予想しています)

全角・半角が混在してる文字列を解析するのはアルゴリズムが複雑になるので、ASCII文字は全角化します。そのためgemでmojiをインストールしておきます。インストールの仕方はこちらを参照

今回は単色青色の240x240の画像に白い文字を書いてみました。

f:id:shimada-k:20140506030316p:plain

使用フォント:/usr/share/fonts/truetype/ttf-japanese-gothic.ttf

全角化してるためASCII文字はなんか間延びしてる印象。

f:id:shimada-k:20140506152752p:plain

文字数が多てもサイズとマージンが最適化されます。

f:id:shimada-k:20140506152807p:plain

フォントサイズはpt指定ですが、draw_textの引数で渡す座標はpx指定なので注意が必要です。

img.draw_text(font,
    str_entry,
    stringAndSize['offset_horizon'],
    stringAndSize['offset_vertical'] + pt2px((stringAndSize['margin_vertical'] + stringAndSize['size']) * index),
    color)

フォントのパスを追加すればwebからダウンロードしたフォントも使用できます。パスはttfファイルが直下に存在するディレクトリを指定すればOK

# フォントのパス追加
Imlib2::Font.add_path('/home/shimada-k/Development/fonts')
Imlib2::Font.add_path('/home/shimada-k/Development/fonts/mplus-TESTFLIGHT-058')

今回は以下のフォントを使ってみました。

自由の翼フォント(JiyunoTsubasa)

f:id:shimada-k:20140506151630p:plain

うつくし明朝体(UtsukushiMincho)

f:id:shimada-k:20140506151647p:plain

うずらフォント(uzura)

f:id:shimada-k:20140506151656p:plain

フロップデザインフォント(FLOPDesignFont)

f:id:shimada-k:20140506151709p:plain

M+ FONTS(mplus-TESTFLIGHT)

f:id:shimada-k:20140506151717p:plain

ロゴたいぷゴシック(07LogoTypeGothic7)

f:id:shimada-k:20140506151725p:plain

ロゴたいぷゴシックが一番おさまりがいい。