Leap Motion SDK V2 Betaで遊んでみる
Leap Motionの新しいSDKのpublic Beta版が5月28日に公開されました。試してみて分かったことを書いておきます。
まずはビジュアライザーを起動してみます。
グーも認識してくれます。
X方向に手を重ねるのはこれくらいが限度。これ以上近づけると片方が認識から外れるか指の認識がうまくいかなくなりました。
Y方向に手を重ねるのはこれくらいが限度。試した感じだと手の平の部分が重なるとダメみたいです。これ以上重ねると上にある方の手が認識から外れます。
SDK V2の新しい機能としてboneが取り上げられることが多いですが、右手・左手の区別って結構難しいのではないかと思っているので、センサーが検出可能な範囲に手が存在する場合、特定の状況下で右手・左手がどの程度認識できているのか試してみました。
leap motionが認識した手の信頼度を出力するHTMLを作って調査しました。コードはGistにあげておきました。
左手のデータは左側の青いdivに、右手のデータは右側の赤いdivに出力されます。片方の手しか認識されて無い場合は認識されて無い方のdivのフォントの色をグレーアウトします。
今回は以下の4つのパターンで、左手・右手の判定、信頼度を取得してみました。
- 手の甲を上にした片手(左手)
- 手の甲を下にした片手(左手)
- 手の甲を上にした両手
- 手の甲を下にした左手と手の甲を上にした右手
左手・右手はhand.typeで、信頼度はhand.confidenceで取得できます。両方ともSDK V2から使用可能なパラメータです。
手の甲を上にした片手(左手)
信頼度は1なので左手と認識されてます。そのまま手のひらをひっくり返しても左手と認識されたままでした。手のひらをひっくりかえす間は手のセンサーに対して手のひらの角度が変化するので信頼度も下がるようです。
手の甲を下にした片手(左手)
右手と認識されました。信頼度も1に近い数値なので右手だと認識されているのでしょう。
手の甲を上にした両手
両手とも正しく認識されています。信頼度は両手とも1です。
手の甲を下にした左手と手の甲を上にした右手
データは0.9付近と1が交互に出力されています。左手のdivには何も出力されていないので左手は存在しないと認識されているのでしょう。
右手のdivには「same hand detected」が出力されているので同じ手が2本あると認識されています(hand.typeが同じであるかどうかで「same hand detected」を出力しています)。
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.
「アクセスがあった時にすべてのワーカープロセスを起こすのはシステムリソースの無駄遣いである」という設計思想を読み取ることができます。
*1:prefork サーバーと thundering herd 問題 http://d.hatena.ne.jp/naoya/20070311/1173629378
*2:Core functionality http://nginx.org/en/docs/ngx_core_module.html#accept_mutex
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するとちゃんとブラウザ上でストリーミングが再生されます。
PeerConnection
ローカルストリームでPeer通信をするサンプルです。
画像では分かりませんが、音声と動画が映っています。しかしこのサンプルはadapter.js(2014/06/05修正)main.jsのperformance.now()がsafariではサポートされてないので動かすにはDate.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や位置情報とかと組み合わせるともっと面白いと思っているので、今後仕様の策定が進んで特にモバイル環境で環境気にせず動くようになることを期待したいです。
Node.jsでPromiseを使う場合
先日、東京Node学園12限目に参加しました。そこで@jovi0608さんから紹介があったNode-v0.12に含まれる予定のPromiseを試してみました。試してみて分かったことを書いておきます。
コールバック地獄の例としてよく挙げられる下記のようなコードのようにディレクトリ間でファイル数とファイル名に差分が無いことを確認できればあとはファイルごとのループに書き換えられるような事例では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の恩恵を受けられるはずです。
例としてtwitterのAPIを使用し、自分のPOSTをリツイートしたユーザIDを調査するロジックを実装しました。
(twitterのAPIはリクエストを投げまくると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
外部APIはtwitterのAPIを使用し、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で画像に文字を埋め込んで遊んでみました。
環境
- OS:Debian Squeeze
- Ruby:ruby 1.8.7
- libimlib2-ruby:0.5.2-2
入力した文字列の文字数によって適切にフォントのサイズと縦横のマージンを計算し、さらに一行におさまる長さで改行を挟んで画像内に表示するコードを書いてみました。
フォントの大きさの単位はpt(ポイント)指定ですので、フォントサイズを決める時は画像の大きさとの兼ね合いでpx(ピクセル)からptへ変換する必要があります。
また、LinuxはDPIが96なのでpxで計算した値をフォントサイズとすると画像からはみ出してしまいます。
(imlibのコンパイルオプションで決まっているかX11の設定をAPI経由で引っ張ってきているかだと予想しています)
全角・半角が混在してる文字列を解析するのはアルゴリズムが複雑になるので、ASCII文字は全角化します。そのためgemでmojiをインストールしておきます。インストールの仕方はこちらを参照
今回は単色青色の240x240の画像に白い文字を書いてみました。
使用フォント:/usr/share/fonts/truetype/ttf-japanese-gothic.ttf
全角化してるためASCII文字はなんか間延びしてる印象。
文字数が多てもサイズとマージンが最適化されます。
フォントサイズは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)
うつくし明朝体(UtsukushiMincho)
うずらフォント(uzura)
フロップデザインフォント(FLOPDesignFont)
M+ FONTS(mplus-TESTFLIGHT)
ロゴたいぷゴシック(07LogoTypeGothic7)
ロゴたいぷゴシックが一番おさまりがいい。
MySQLでインデックスが効いてないとつまりどうなるのか
MySQLでインデックスを使用しないクエリを投げると実際遅いことは分かるんですけど、マジックワード感があるのでなぜ遅くなるのか考えてみました。
すごく大きなテーブルを作ってインデックスを使用したSELECTとそうでないSELECTを投げてperfで調査しました。
環境
perfをインストールします
apt-get install google-perftools linux-tools-2.6.32
これでperfが動く環境になりました。次に実験用のテーブルを作りガツっとデータをINSERTします。今回使用するテーブルは下記のような構造にしました。1ユーザごとに100件のレコードを保持していることを想定して、100万ユーザ分のデータをINSERTすることにします。
mysql> DESC owning_muffins; +------------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | user_id | bigint(20) unsigned | NO | | NULL | | | flavor_id | int(11) | NO | | NULL | | | blueberry | tinyint(4) | YES | | NULL | | | strawberry | tinyint(4) | YES | | NULL | | | chocochip | tinyint(4) | YES | | NULL | | | nuts | tinyint(4) | YES | | NULL | | | created_at | datetime | YES | | NULL | | +------------+---------------------+------+-----+---------+----------------+ 8 rows in set (0.00 sec)
INSERTするバッチ
require 'rubygems' require 'mysql' # insertData.rb # データを大量にinsertするコード # テーブルはユーザごとに100のデータを持っていることを想定 # ユーザは100万人とする # insertするデータ数は100万×100で100000000 def insertData con = Mysql::connect('host', 'user', 'password', 'db_name') sql = 'SET NAMES utf8' con.query sql for user_id in 1..1000000 do for has_muffin in 1..100 do query_string = 'INSERT INTO owning_muffins (user_id, flavor_id, blueberry, strawberry, chocochip, nuts, created_at) values(' << user_id.to_s << ',' << rand(10).to_s << ',' << rand(2).to_s << ',' << rand(2).to_s << ',' << rand(2).to_s << ',' << rand(2).to_s << ',\'' << Time.now.strftime("%Y-%m-%d %H:%M:%S") << '\');' # puts query_string con.query query_string end end end insertData()
バッチ終了後には100×100万で1億レコードのテーブルになります。
#ちなみにこのINSERTバッチ、実行完了まで丸2日かかりましたw
これでデータが入ったのでSELECTするバッチを実行してperfで計測します。
require 'rubygems' require 'mysql' # fetchData.rb # データをselectするコード # インデックスを効かせたselectか効かないselectか選択する # 引数付きで実行するとインデックスを使用しない検索を行う def fetchData(has_index = true) con = Mysql::connect('host', 'user', 'password', 'db_name') sql = 'SET NAMES utf8' con.query sql id = rand(100000000) user_id = rand(1000000) if has_index then ids = '' for id in 1..100 do if id == 100 then ids << (user_id * 100 + id).to_s else ids << (user_id * 100 + id).to_s << ',' end end # puts ids query_string = 'SELECT * FROM owning_muffins WHERE id in (' << ids << ')' else query_string = 'SELECT * FROM owning_muffins WHERE user_id = ' << user_id.to_s end #puts query_string data = con.query query_string ret_array = Array.new while(record = data.fetch_hash()) ret_array.push(record) end # 標準出力に表示する # for i in 0..ret_array.length - 1 do # puts ret_array[i]['id'] # end end # 引数があればインデックスを無効にする if ARGV[0] then has_index = false else has_index = true end fetchData(has_index)
インデックスを使用したクエリをたたくものとインデックスをしないクエリをたたく2種類の計測を行いました。
MySQLがどんな風に子プロセスor子スレッドを作ってるか分からないのでシステム全体でイベントを計測します(-aオプションを使用する場合はrootにならないとダメです)。さらに誤差を考えてそれぞれ10回ずつ実行した平均をとります。計測コマンドは下記の通り。
# インデックスを使用した場合 perf stat -a -r 10 -e cpu-clock,task-clock,page-faults,cpu-migrations,L1-dcache-load-misses,L1-dcache-store-misses,L1-dcache-prefetch-misses,L1-icache-load-misses,L1-icache-prefetch-misses,LLC-load-misses,LLC-store-misses,LLC-prefetch-misses ruby fetchData.rb # インデックスを使用しない場合 perf stat -a -r 10 -e cpu-clock,task-clock,page-faults,cpu-migrations,L1-dcache-load-misses,L1-dcache-store-misses,L1-dcache-prefetch-misses,L1-icache-load-misses,L1-icache-prefetch-misses,LLC-load-misses,LLC-store-misses,LLC-prefetch-misses ruby fetchData.rb hoge
計測結果です。まずはインデックスが効いている場合
score | event |
---|---|
229.827713 | cpu-clock-msecs |
229.937963 | task-clock-msecs |
2676 | page-faults |
3 | CPU-migrations |
34293 | L1-dcache-load-misses |
1799415 | L1-dcache-store-misses |
559063 | L1-dcache-prefetch-misses |
1103446 | L1-icache-load-misses |
not counted | L1-icache-prefetch-misses |
1074416 | LLC-load-misses |
123611 | LLC-store-misses |
444980 | LLC-prefetch-misses |
実行にかかった時間は0.057323544秒でした(10回の平均値)
次はインデックスが効いていない場合
score | event |
---|---|
115113.325665 | cpu-clock-msecs |
115114.371156 | task-clock-msecs |
4654 | page-faults |
40 | CPU-migrations |
11365832 | L1-dcache-load-misses |
1050214193 | L1-dcache-store-misses |
211685824 | L1-dcache-prefetch-misses |
53432908 | L1-icache-load-misses |
not counted | L1-icache-prefetch-misses |
164818024 | LLC-load-misses |
171005281 | LLC-store-misses |
119857977 | LLC-prefetch-misses |
実行にかかった時間は28.792978184秒でした(10回の平均値)
実行時間はおおよそ502倍になっています。最初io-waitが頻発してるのかと思っていましたがそうではなさそうです。cpu-clockと総実行時間から逆算したio-waitの時間が0に近いので他の要因であると考えます。
実行時間が502倍なのでインデックスが効いている場合の各値を502倍してインデックスが効いていない場合の数値と比較してみました。
#各数値が実行時間と正比例する前提の方法論なので乱暴ですが俯瞰して比較する際の指針にはなりえるかも、と考えました。
index_not_use / (502 * index_use) | event |
---|---|
0.9971710235 | cpu-clock-msecs |
0.996701955 | task-clock-msecs |
0.003462475541 | page-faults |
0.02654514983 | CPU-migrations |
0.6598454059 | L1-dcache-load-misses |
1.161964851 | L1-dcache-store-misses |
0.7538370338 | L1-dcache-prefetch-misses |
0.09640602364 | L1-icache-load-misses |
not counted | L1-icache-prefetch-misses |
0.3054067844 | LLC-load-misses |
2.754221392 | LLC-store-misses |
0.5362569033 | LLC-prefetch-misses |
L1とLLC(ラスト・レベル・キャッシュ。今回使用したマシンは3次までCPUキャッシュがあるのでL3キャッシュのこと)のSTOREキャッシュミスが比較的多いことが分かります。なのでデータをキャッシングしておく際のオーバーヘッドが大きいようです。
- キャッシュを参照
- キャッシュに無い
- メモリか他のキャッシュからデータを取ってきて自分の所に保存
これを都度繰り返しているということになります。フルスキャンなのでそのようになるのでしょう。
#なぜL1キャッシュよりL3キャッシュのミスの方が割合が多いのかというのはプロセスマイグレーションの話とかCPUキャッシュのウェイ数とかが関係してきそうなのでこれ以上深追いはしたくない。
twitterの投稿画面風UIの作り方
iPhone版twitterアプリのようなアップロードUIを作ってみました。
こういうやつです。上記はtwitterのtweet入力画面です。
テキストを入力する場所があって、その下にキーボードがある。さらにその間にツールバーのような領域があってボタンを配置できる。というもの。twitterの場合は位置情報とカメラとフォトライブラリの3つのボタンがあります。
調べていくとUITextViewのAccessoryViewを使えば実現できることが分かりました。
作ってみたものはSingleViewApplicationをベースにNavigationControllerを入れて初期画面のWriteボタンをタップしたら入力用の画面へ遷移する。遷移先にはUITextViewと、UIViewにラップされたツールバーがあってツールバーにカメラボタンが含まれているというものです。
#カメラボタンを押した時の挙動は実装してないのでログしかでません。。
#UITextViewに表示されている英文はデフォの文章です
AccessoryView用のUIViewはInterfaceBuilderのDockエリアに置くことです。実験した環境ではDockエリアでは無いところに置くとキーボードの下に隠れてしまいました。
InterfaceBuilder上の親子関係はこんな感じ。
ちなみにchatworkのモバイルアプリでも同じようなUIが使われてます。わりと一般的なUIのはずなのに日本語で解説してるサイトが少なすぎる。
Dockエリアに置いている状態だとInterfaceBuilder上で見た目が確認できないのでAccessoryViewを弄るときは一度どこかのサブビューに置いてからの方がよいと思います。
ソース全文
// // MUInputViewController.m // modernUpload // // Created by 島田克弥 on 2014/03/15. // Copyright (c) 2014年 shimada-k. All rights reserved. // #import "MUInputViewController.h" @interface MUInputViewController () @property (weak, nonatomic) IBOutlet UITextView *sendTextView; @property (strong, nonatomic) IBOutlet UIView *accessoryView; - (IBAction)startCamera:(UIBarButtonItem *)sender; - (IBAction)writeDone:(UIBarButtonItem *)sender; @end @implementation MUInputViewController - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Custom initialization } return self; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 入力状態にする [self editAction:self]; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. _sendTextView.delegate = self; // キーボードが出てくる時の通知を受け取るよう設定 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; // キーボードが閉じらる時の通知を受け取るよう設定 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } - (IBAction)writeDone:(UIBarButtonItem *)sender { [_sendTextView resignFirstResponder]; } - (void)editAction:(id)sender { [_sendTextView becomeFirstResponder]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } #pragma mark - Text view delegate methods - (BOOL)textViewShouldBeginEditing:(UITextView *)textView{ if (_sendTextView.inputAccessoryView == nil) { _sendTextView.inputAccessoryView = _accessoryView; } return YES; } - (BOOL)textViewShouldEndEditing:(UITextView *)textView { [textView resignFirstResponder]; return YES; } #pragma mark - Responding to keyboard events - (void)keyboardWillShow:(NSNotification *)notification { NSDictionary *userInfo = [notification userInfo]; // キーボードが表示完了後の場所と大きさを取得する NSValue *aValue = [userInfo objectForKey:UIKeyboardFrameEndUserInfoKey]; CGRect keyboardRect = [aValue CGRectValue]; keyboardRect = [self.view convertRect:keyboardRect fromView:nil]; CGFloat keyboardTop = keyboardRect.origin.y; CGRect newTextViewFrame = self.view.bounds; // キーボードの大きさに応じてテキスト表示領域を再計算する newTextViewFrame.size.height = keyboardTop - self.view.bounds.origin.y; // キーボードのアニメーション時間を取得する NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; NSTimeInterval animationDuration; [animationDurationValue getValue:&animationDuration]; // アニメーション実行準備 [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:animationDuration]; // UITextViewの大きさを変更する _sendTextView.frame = newTextViewFrame; // アニメーションの開始 [UIView commitAnimations]; } - (void)keyboardWillHide:(NSNotification *)notification { NSDictionary *userInfo = [notification userInfo]; // キーボードのアニメーション時間を取得する NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; NSTimeInterval animationDuration; [animationDurationValue getValue:&animationDuration]; [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:animationDuration]; // UITextViewの大きさを元に戻す _sendTextView.frame = self.view.bounds; // アニメーションの開始 [UIView commitAnimations]; } /* #pragma mark - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { // Get the new view controller using [segue destinationViewController]. // Pass the selected object to the new view controller. } */ - (IBAction)startCamera:(UIBarButtonItem *)sender { NSLog(@"写真がとれたらいいね"); } @end
参考サイト