Network Connection Classはどのようにネットワークの品質を判定しているのか?

  • 44
    Like
  • 0
    Comment
More than 1 year has passed since last update.

いかにしてネットワークの品質の判定するか

ネットワークの品質によって振る舞いを変える(スレッド数や画質を調整する)のに、最初は自前でOkHttpのNetworkInterceptorでRTTを計測してそれをもとにスループットを計算しようとしていたのですが、スパイクに大きく影響されるとリソースが無駄になる(主に閾値周辺のユーザに対して頻繁にリスナーが呼ばれたり、URLで画像のサイズや画質を指定するようなものではキャッシュが効きづらくなる)ことがあるので、とりあえずUMTSなら3G、GPRSなら2Gのように、ネットワークの規格によって振り分けるようにしていました。ただし同じ規格でも速度は国によって大きく異なるので、あまり正確な方法ではありませんでした。
規格でネットワークの品質を判定する方法にもやもやしていたのですが、昨日のF8でFacebookが Network Connection Class というライブラリをオープンソースにしました。

Facebook Launches 3 New Open-Source Tools For Android Developers | TechCrunch

ちょうど気になっていたところなので、どのようにネットワークの品質を判定しているのか、コードを読んで調べてみました。

サンプルアプリから全体像を見る

リポジトリ facebook/network-connection-class にサンプルアプリがあるので、cloneしてビルドしてみます。

Screen Shot 2015-03-27 at 22.50.59.png

ボタンをタップすると計測が始まって、しばらくすると結果が表示されます。MainActivity を見ると明示的にサンプリングの開始と終了を呼んでいます。処理を追ってみると以下のようになっていました。

  1. DeviceBandwidthSampler は内部に Handler を持っていて1秒毎にメッセージを投げる
  2. Handler はメッセージを受け取ったら QTagParser から値を取得する
  3. ExponentialGeometricsAverage が取得した値から移動平均を出す
  4. スループットから品質を判定して、変更されていたらリスナーに通知する仕組みのようです

入力: xt_qtaguid

Network Connection Classは xt_qtaguid からネットワーク情報を取っていました。xt_qtaguid はAndroid 3.0からカーネルに入っているnetfilterモジュールらしいです。私はこの存在を知りませんでした。
/proc 以下にはハードウェアや実行中のプロセスやネットワークに関する情報が配置されていますが、/proc/net/xt_qtaguid/stats にはプロセスIDや読み込まれたバイト数などの情報が書かれていて、QTagParser はこの値を見ています。開いてみるとこのようになっています。

idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
2 eth1 0x0 0 0 1968 19 1105 18 477 11 1491 8 0 0 160 4 561 8 384 6
3 eth1 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
4 eth1 0x0 1000 0 300 4 461 5 224 3 76 1 0 0 385 4 76 1 0 0
5 eth1 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
6 eth1 0x0 1014 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3
7 eth1 0x0 1014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
8 eth1 0x0 10052 0 17067 46 8234 42 17067 46 0 0 0 0 8234 42 0 0 0 0
9 eth1 0x0 10052 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
10 eth1 0x0 10053 0 18291 43 8999 49 18291 43 0 0 0 0 8999 49 0 0 0 0
11 eth1 0x0 10053 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
12 eth0 0x0 0 0 695523 12187 24613415 6404 694510 12179 1013 8 0 0 24613227 6401 0 0 188 3
13 eth0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
14 eth0 0x0 1000 0 116 2 60 1 116 2 0 0 0 0 60 1 0 0 0 0
15 eth0 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
16 lo 0x0 0 0 24367912 4612 221135 3819 24367912 4612 0 0 0 0 221135 3819 0 0 0 0
17 lo 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
18 lo 0x0 1000 0 175251 3093 5932452 3508 175251 3093 0 0 0 0 5932452 3508 0 0 0 0
19 lo 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
20 lo 0x0 1003 0 10662 142 214794 122 10662 142 0 0 0 0 214794 122 0 0 0 0
21 lo 0x0 1003 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
22 lo 0x0 10023 0 15922 236 3328513 320 15922 236 0 0 0 0 3328513 320 0 0 0 0
23 lo 0x0 10023 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
24 lo 0x0 10044 0 23426 378 14884182 628 23426 378 0 0 0 0 14884182 628 0 0 0 0
25 lo 0x0 10044 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

この中からloをスキップして自分のプロセスIDでフィルタして rx_bytes を見ています。/proc 以下は仮想ファイルなので、開く前とあとにスレッドポリシーをセットしててなるほどと思いました。

// QTagParser.java
StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
// ここでファイルを読む
StrictMode.setThreadPolicy(savedPolicy);

計算: ExponentialGeometricAverage

DeviceBandwidthSamplerQTagParser を通して何ミリ秒の間に何バイト読んだのかを取得したので、それをもとに ExponentialGeometricAverage が移動平均を計算します。名前の通り指数関数を適用して幾何平均を取ってスパイクに対応しているようです。

画像はリポジトリのREADMEより

品質の判定

上記で算出されたスループットから品質を以下のようにマッピングしています。

Throughput Quality
〜150 kbps POOR
150 〜 549 kbps MODERATE
550 〜 1999 kbps GOOD
2000 kbps 〜 EXCELLENT
- UNKNOWN

オフラインになった場合に直ちにUNKNOWNにならずにそのままの判定を返してくれるのが地味に嬉しく、接続と切断を繰り返すような不安定な環境においても、何度もリスナーが呼ばれるようなことはありません。

アプリに組み込むには

Network Connection Classは明示的にサンプリングの開始と終了を呼ぶ必要があります。問題はいつ呼ぶかです。私はPicassoのDownloaderのOkHttpのNetworkInterceptorにSamplingInterceptorという自作のクラスを挟んで計測を試みましたが、サムネイルなど小さな画像をたくさん読み込むところでレイテンシ的にスループットが下がってしまい(大きなデータを読むときはEXCELLENTになる)Wi-Fiでも常にPOORに判定されてしまい、スループットの計測は難しいと思いながら別の方法を試しているところです。