Xamarin.iOS製Appのプロファイリングについての備忘録

  • "メモリ"と"パフォーマンス(関数呼び出し)"の2種類を取得できる
  • "パフォーマンス"はシミュレータ上で実行した時のみ取得可能
  • プロファイル結果ファイルを圧縮する設定にすると解析が不可能(実際は不可能では無いが止まったかのように遅い)
  • "パフォーマンス"の解析結果をGUIで見る方法が不明("メモリ"はHeapShotアプリで可能)
  • 解析ツールの場所は/Library/Frameworks/Mono.framework/Versions/Current/bin/mprof-report

"パフォーマンス"の解析の方法

$ /Library/Frameworks/Mono.framework/Versions/Current/bin/mprof-report --method-sort=total --traces --out=result.txt --verbose --maxframes=0 profiler-output.mlpd

呼出元関数を出力したい場合は--maxframes引数の数値を大きくする

_WebTryThreadLock(bool) 例外の原因

Xamarin.iOSで開発中のアプリで、特定の操作で100%クラッシュするという報告をもらった。
操作としてはUIButtonなボタンとUITapGestureRecognizerを追加したUIViewと仮想キーボード上のキーの3カ所を同時に数回タップするというもの。発見した人凄い。

エラーの詳細

エラー発生時に得られる情報は以下となる。エラー毎にログの内容は多少異なるが、特長としては必ずWebThreadという名前のスレッド内で例外が起きる。

コンソールログ
bool _WebTryThreadLock(bool), 0x3ca430: Multiple locks on web thread not allowed! Please file a bug. Crashing now...
以下略
デバイスログ
(略)
Exception Type:  EXC_BAD_ACCESS (SIGABRT)
Exception Codes: KERN_INVALID_ADDRESS at 0xbbadbeef
Crashed Thread:  2

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0:
0   libsystem_kernel.dylib        	0x30b0d004 0x30b0c000 + 4100
1   libsystem_kernel.dylib        	0x30b0d1fa 0x30b0c000 + 4602
2   CoreFoundation                	0x37e903ec 0x37e03000 + 578540
3   CoreFoundation                	0x37e8f0ea 0x37e03000 + 573674
4   CoreFoundation                	0x37e1249e 0x37e03000 + 62622
5   CoreFoundation                	0x37e12366 0x37e03000 + 62310
6   GraphicsServices              	0x362f5432 0x362f1000 + 17458
7   UIKit                         	0x30cc3cce 0x30c92000 + 203982
8   MYAPP                         	0x00189228 0x1000 + 1606184
9   MYAPP                         	0x0013d158 0x1000 + 1294680
10  MYAPP                         	0x0007e7c8 0x1000 + 513992
11  MYAPP                         	0x0073c6c4 0x1000 + 7583428
12  MYAPP                         	0x00f14168 0x1000 + 15806824
13  MYAPP                         	0x00f83494 0x1000 + 16262292
14  MYAPP                         	0x00f861b4 0x1000 + 16273844
15  MYAPP                         	0x00f86408 0x1000 + 16274440
16  MYAPP                         	0x00f171d0 0x1000 + 15819216
17  MYAPP                         	0x00f119be 0x1000 + 15796670
18  MYAPP                         	0x000021b0 0x1000 + 4528

Thread 1 name:  Dispatch queue: com.apple.libdispatch-manager
Thread 1:
0   libsystem_kernel.dylib        	0x30b0d3a8 0x30b0c000 + 5032
1   libdispatch.dylib             	0x347e7f04 0x347e4000 + 16132
2   libdispatch.dylib             	0x347e7c22 0x347e4000 + 15394

Thread 2 name:  WebThread
Thread 2 Crashed:
0   libsystem_kernel.dylib        	0x30b1d32c 0x30b0c000 + 70444
1   libsystem_c.dylib             	0x330ae208 0x33061000 + 315912
2   libsystem_c.dylib             	0x330a7298 0x33061000 + 287384
3   MYAPP                         	0x00f219aa 0x1000 + 15862186
4   MYAPP                         	0x00f12bb4 0x1000 + 15801268
5   libsystem_c.dylib             	0x330b87e6 0x33061000 + 358374
6   JavaScriptCore                	0x363e6fe8 0x36315000 + 860136
7   WebCore                       	0x311f77ec 0x311f1000 + 26604
8   CoreFoundation                	0x37e90b14 0x37e03000 + 580372
9   CoreFoundation                	0x37e8ed50 0x37e03000 + 572752
10  CoreFoundation                	0x37e8f02a 0x37e03000 + 573482
11  CoreFoundation                	0x37e1249e 0x37e03000 + 62622
12  CoreFoundation                	0x37e12366 0x37e03000 + 62310
13  WebCore                       	0x3129ac9c 0x311f1000 + 695452
14  libsystem_c.dylib             	0x3306f72e 0x33061000 + 59182
15  libsystem_c.dylib             	0x3306f5e8 0x33061000 + 58856

(以下略)

原因究明

WebTryThreadLockキーワード

WebTryThreadLockというキーワードから検索出来る情報は「UIスレッド以外からのUIを操作した」というのが一番多い。しかしこの場合コンソールログに

Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now...

と表示されるので今回のエラーとは関係がない模様。そしてなによりこのアプリはマルチスレッドを使っていない。

WebThreadキーワード

iOSでは自動的に作られるスレッドなんだろうか?自分で制御しているスレッドではないのでどうにもならない。またこのキーワードからは何も得られなかった。

XCodeデバッグ

Xamarin Studioからデバッグした場合、例外の発生と共にデバッグが終わってしまう。なのでXCodeデバッグしてみた。
例外発生付近のアセンブラのコードをみているとスレッドロックが失敗して0x000000000なアドレスに0xbbadbeefというデータを書き込んでいた(Assert的処理だろうか)。不正なアドレスに書き込みしているのでEXC_BAD_ACCESS。0xbbadbeefはBad Beef(悪い牛肉)という意味だろうか。
つまりデバイスログにあるEXC_BAD_ACCESSは2次的なものであって、直接の例外理由ではないことが判った。
ではどうしてスレッドロックがおかしくなったのか?
さすがにこれ以上アセンブラレベルで解析するのは辛いので諦めた。

コードを精査

手間なので後回しにしていた「書いてあるコードを一行ずつコメントアウトして逐一動作をチェック」をおこなってようやくエラーがでなくなった。
場所はUIButtonのTouchUpInsideイベントを使っている箇所だった。



結局具体的な原因は分からないままである。
対処療法的になるがコード中のResponderChain系イベントを使っている箇所を、全てUIGestureRecognizerに置き換えることでエラーがでなくなった。

iOSに設定されているプロキシの自動構成スクリプトを使ってみたい

.NETのHttpWebRequestクラスに対してiOSWiFi接続に設定されている自動構成スクリプトの設定を適用するにはどうしたらいいかの調査。

.NET本来の方法

System.Net.WebRequest.GetSystemWebProxy()関数で取得すればあとは勝手にやってくれる模様。この方式をXamarin.iOSで使うと環境変数HTTP_PROXYをごにょごにょしているだけだった。これでは使えない。

iOS本来の方法

CFNetworkCopyProxiesForAutoConfigurationScript関数に対して自動構成スクリプトの内容とURLを渡せば適切なプロキシのアドレスを返してくれる模様。プロキシアドレスが取得できればそれをSystem.Net.WebProxyクラスに渡してやればよい。
これをXamarin.iOSで書いてみた。

string url = "http://www.google.co.jp";

// 自動構成スクリプトの内容は本来なら別の手段で取得するべき
string script = @"function FindProxyForURL(url,host) {
 if(isPlainHostName(host)||
    isInNet(host,""127.0.0.1"",""255.255.255.255""))
    return ""DIRECT"";
 else if(isPlainHostName(host) ||
    isInNet(host,""192.0.0.0"",""255.0.0.0""))
    return ""DIRECT"";
 else
    return ""PROXY myproxy:8888; DIRECT"";
}
";

MonoTouch.CoreFoundation.CFProxy[] proxy = MonoTouch.CoreFoundation.CFNetwork.GetProxiesForAutoConfigurationScript(new NSString(script), NSUrl.FromString(url));

この場合何故かCFNetwork.GetProxiesForAutoConfigurationScriptの内部のObjective-Cの段階で例外が発生してしまう。リフレクションで調べるとCFNetworkCopyProxiesForAutoConfigurationScript関数は引数が3つだが、Xamarin.iOSは引数を2つしか渡していない。これが原因だろうか(バグ?)。

ならばと強引に呼んでみた。

[DllImport("/System/Library/Frameworks/CFNetwork.framework/CFNetwork")]
static extern IntPtr CFNetworkCopyProxiesForAutoConfigurationScript(IntPtr proxyAutoConfigurationScript, IntPtr targetURL, IntPtr error);

// 自動構成スクリプトの内容は本来なら別の手段で取得するべき
string script = @"function FindProxyForURL(url,host) {
 if(isPlainHostName(host)||
    isInNet(host,""127.0.0.1"",""255.255.255.255""))
    return ""DIRECT"";
 else if(isPlainHostName(host) ||
    isInNet(host,""192.0.0.0"",""255.0.0.0""))
    return ""DIRECT"";
 else
    return ""PROXY myproxy:8888; DIRECT"";
}
";

NSObject error = new NSObject();
NSString aaa = new NSString(script);
NSUrl u = NSUrl.FromString(url);
IntPtr ret = CFNetworkCopyProxiesForAutoConfigurationScript(aaa.Handle, u.Handle, error.Handle);
NSArray array = new NSArray(ret);
NSDictionary[] dictionaryArray = NSArray.ArrayFromHandle<NSDictionary>(array.Handle);
array.Dispose();
System.Console.WriteLine("p={0}",dictionaryArray[0]); // => myproxy:8888

とりあえずプロキシアドレスを取得することができた。あとはこのコードを使用に耐えられるようにしなければならないが、

等を考えるとこのままではコードが足りなさすぎる。ここで時間切れ。

TimeZoneInfo.ConvertTimeFromUtcの戻り値のKindがおかしい

Xamarin.iOS v6.3.0.255と.NET v4.5で以下のコードの戻り値が異なっていた。

DateTime t1 = new DateTime(2013, 4, 19, 9, 0, 0, DateTimeKind.Utc);
DateTime t2 = TimeZoneInfo.ConvertTimeFromUtc(t1, TimeZoneInfo.Local);

t2.Kindは、

  • Xamarin.iOSはUnspecified
  • .NETはLocal

になる。
.NETの方が正しいと思うのだが。Xamarin.iOSのバグだろうか。

bug #9150

Xamarin Studio v4.0.3*1とXamarin.iOS v6.3.0.255の組み合わせで突然デバッグできなくなってしまった。
症状としてはブレークポイントのところでブレークする瞬間にSystem.NotImplementedException例外が起きるようになった。
この組み合わせで数日間まったく問題がなかったのでまったく原因が推測できない。
しかしググるとまさしくこの問題がBugzillaに登録されていた。
幸いパッチが添付されていたのでこれを適用したMonoDevelopをビルドして該当DLLをXamarin Studioに導入してみることにした。

Monodevelop自体のビルド方法はここを参考にした。

既に最新のMonoFrameworkとXcodeはインストールしてあるので、まずgitからソースコードを取得。

cd ~
mkdir temp
cd temp
git clone git://github.com/mono/monodevelop.git
pushd monodevelop
<||
ソースコードが取得できたらパッチを適用。次に以下のコマンドでビルド。
>||
export ACLOCAL_FLAGS="-I /Library/Frameworks/Mono.framework/Versions/Current/share/aclocal"
export PATH="/Library/Frameworks/Mono.framework/Versions/Current/bin:$PATH"
export DYLD_FALLBACK_LIBRARY_PATH=/Library/Frameworks/Mono.framework/Versions/Current/lib:/lib:/usr/lib
./configure --profile=mac
make

これでMono.Debugging.Soft.dllとMono.Debugging.Soft.dll.mdbができあがる。
この2つのファイルをXamarin Studio.appの中の同名ファイルと置き換えて問題が起きないことを確認した。

*1:v4.0.4.2でも同様だった

launchctlで定期的にMonoTouch製Appをリリースビルドするには

MonoTouchのリリースビルドはコンソールから以下のコマンドで可能。

/Applications/MonoDevelop.app/Contents/MacOS/mdtool build ソリューションファイル名 -c:'コンフィグ名'

コンフィグ名とは'Ad-Hoc|iPhone'や'Release|iPhone'のこと。

上記をふまえるとビルド用シェルスクリプトは以下のようになる。

#/bin/sh

ulimit -n 1024
security unlock-keychain -p パスワード /Users/myuser/Library/Keychains/login.keychain
/Applications/MonoDevelop.app/Contents/MacOS/mdtool build ソリューションファイル名 -c:'コンフィグ名'

mdtoolはAppへ署名するためにキーチェインにアクセスしようとする。GUIの場合はアクセス許可の旨のダイアログが表示されるが、コンソールからアクセスするときはアクセスが拒否されてエラーで終わってしまう。なのでmdtoolの前にsecurityコマンドでキーチェーンをアンロックする必要がある。
ulimitはmdtoolが1プロセスで大量のファイルをオープンすることがあるので制限を解除する必要がある。

次にビルド用シェルスクリプトをlaunchctlに登録する。
以下のようなplistファイルを/Library/LaunchDaemons/に作成する。
ここでplistファイルのパーミッションは644。オーナーはrootにする必要がある。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.myapp.autobuild</string>
  <key>ProgramArguments</key>
  <array>
    <string>起動するコマンドへのフルパス(シェルスクリプト)</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Minute</key>
    <integer>00</integer>
  </dict>
  <key>RunAtLoad</key>
  <false/>
  <key>ExitTimeout</key>
  <integer>600</integer>
  <key>UserName</key>
  <string>myuser</string>
  <key>GroupName</key>
  <string>staff</string>
  <key>OnDemand</key>
  <true/>
  <key>SessionCreate</key>
  <true/>
</dict>
</plist>

LabelとProgramArgumentsはlaunchctlの仕様上必ず必要だが、MonoTouchをビルドするために必要なものとしてさらにUserName、GroupName、SessionCreateが必要になる。

UserName、GroupNameはMonoTouchのライセンスファイルへのアクセスに必要となる。
MonoTouchはライセンスファイルがユーザー毎に登録されている。この値を省略した場合はmdtoolがライセンスファイルにアクセスできず正しくビルドできない。よってMonoTouchをアクティベートしたユーザーを明示的に指定しなければならない。

SessionCreateはキーチェーンにアクセスの為に必要となる。
シェルスクリプト内のsecurityコマンドでアンロックをしているがこれだけでは足りない模様。SessionCreateを明示的にtrueにすることでアクセスできるようになる。


追記 2013/03/21 10:25
ulimitを追加した。

App自身で強制終了するには

Appが自分自身で強制終了したい場合、MonoTouchでは以下のコードで強制終了が可能になる。

UIApplication.SharedApplication.PerformSelector(new Selector("terminateWithSuccess"), null, 0f);

試してはいないがもう一つ見つけたのがInfo.plistでUIApplicationExitsOnSuspendを有効にした上で、

Environment.Exit(0);

この場合はAppがマルチタスク非対応になってしまう。

アプリ審査において、App自身による強制終了はユーザーに判りやすいUIで終了させることが重要であってコード(方法)は不問のようだが、これらのコードを使って審査を通るかどうかは不明。