lionscope について
lionscopeは、開発者やAIエージェント向けのブラウザベースのAndroidプロファイラー兼レイアウトインスペクターです。性能、レイアウト、ネットワーク、ログを共通のタイムラインに表示し、オプションのSDKでコードレベルの情報も取得できます。
主な機能
- 01CPU、メモリ、フレーム、ログの共通タイムライン
- 02View属性とCompose画面の検査
- 03メインスレッドの停止箇所と発生元の追跡
- 04HTTP通信の表示とレスポンスのモック
- 05エミュレーターとUSBデバイスの操作
活用シーン
- 01アプリの停止や低速なフレームの調査
- 02レイアウトツリーの確認とView属性の編集
- 03リクエスト時間の分析やモック応答のテスト
- 04AIエージェントによるデバイス・計測データの参照
MCP 対応
MCPまたはCLIを通じて、デバイスとプロファイリングのデータをAIエージェントに提供します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
16掲載時のツール一覧 · 今日に更新
このツールにパラメーターはありません。
詳細
Lionscope
Lionscopeは、ブラウザーで使える軽量なAndroidプロファイラー兼レイアウトインスペクターで、AIエージェントからも利用できます。Android Studioは不要です。npx lionscope@latestを実行して、デバイスとアプリを選ぶだけです。
- プロファイル:1つのアプリについて、CPU、メモリ、フレーム描画、メインスレッドの停止、レイアウトのインフレーション、logcatを、共通の時間軸で確認できます。SDKを使うと、各停止について、ブロックしたコードの行と、その処理がどこから来たのか(RxJavaのチェーンと、それを購読した行、コルーチン、postされたRunnable、クリック)も表示されます。
- インスペクト:画面をフリーズしてビューをクリックすると、その属性を確認できます。SDKを使うと、XMLレイアウトとその定義元の行、ビューを所有するFragment、Compose画面のコンポーザブルとそれを呼び出すKotlinの行、さらに実行中のアプリでViewのプロパティ(パディング、サイズ、カラーピッカーで選ぶ色、テキストなど)をリアルタイムに編集する機能も利用できます。
- ネットワーク:SDKのOkHttpインターセプターを使うと、アプリのHTTPリクエスト(ヘッダー、ボディ、所要時間)を同じ時間軸で確認し、ダッシュボードからレスポンスや失敗をモックできます。
- 操作:エミュレーター(ヘッドレスで起動することもできます)やUSBデバイスをグラフのすぐ横に表示して操作でき、地図上で別の場所に移動できます。
- AIエージェント:同じデータを、MCP経由のツールまたはCLIとして利用できます(Claude Code、Cursor、VS Code、Codexなど)。インスペクターでビューを選択し、「この要素」についてエージェントに質問できます。
軽量性も保たれています。サンプルアプリをエミュレーターで実行して測定したところ、アプリのアイドル時にサーバーが使用するCPUは1コアの1%未満、スクロール中は約4%で、メモリ使用量は100~150 MBでした。SDKがアプリに追加するCPU負荷は約0.5%です。
次の2つのモードで動作します。
- ADBのみ(アプリの変更不要):すべてのデータを
adb shell(/proc、dumpsys、logcat)経由で収集します。 - アプリのデバッグビルドでオプションのSDKを使用:最前面の画面(Activity、Fragment、DialogFragment)、ステージごとの内訳を含むフレーム単位の
FrameMetrics(画面ごとにグループ化)、停止の原因となったアプリのコード行と処理の発生元を示すメインスレッドの停止(オプトインのStrictMode違反も含む)、レイアウトのインフレーション時間、レイアウトの定義元と所有者を含むレイアウトインスペクター独自のビュー階層、Compose画面のコンポーザブル、Viewのリアルタイム編集、さらに(OkHttpClientに1行追加するだけで)アプリのHTTPトラフィックとモックレスポンスを確認できます。
ヘッドレスエミュレーター上の同梱サンプルアプリを、Device列から操作しています。すべてのグラフにまたがる画面の帯、RenderingレーンのSDK FrameMetrics、タイムライン下のlogcat、ジャンクの多い順に画面を表示するScreensタブを確認できます。
ダッシュボードに表示される情報
ダッシュボードは1ページで構成されています。上部のバーではデバイスとアプリを選択し、セッションを開始または停止できます。左側にはエミュレーターの画面、中央には共通の時間軸(直近1分、5分、10分)を持つ積み重ね表示のタイムラインチャートがあり、一時停止もできます。チャートの下にあるドロワーにはアプリのlogcatが表示されます(バーをドラッグしてサイズを変更、バーをダブルクリックして最大化できます。行をクリック、Shiftキーを押しながらクリック、またはCmd/Ctrlキーを押しながらクリックして選択し、Cmd/Ctrl+Cを押すとコピーできます)。SDKを使用している場合は、右側にScreens、Stalls、Layouts、Networkのタブも表示されます。1280 px未満では、画面は上部のバーから開くドロワーとして表示されます(Profilerビューの場合。他の2つのビューではページ上に表示されたままです)。
上部のバーの右側にあるスイッチで、ページの用途を切り替えられます。
- Profiler(デフォルト):上記すべてを表示します。
- Inspector:デバイスとLayout Inspectorのみを表示し、 ツリーと属性を並べて確認できるように、十分なスペースを確保します。
- Device:デバイス画面のみを大きく表示し、AI エージェントに操作させる方法も確認できます( AI エージェントでの使用を参照)。
Inspector ビューと Device ビューでは、グラフと logcat が非表示になり、描画も停止するため、非表示のものが
ブラウザーで再描画され続けることはありません。実行中のセッションでは記録が続き、グラフと logcat(フィルターも含む)は
戻ったときに切り替え前の状態で表示されます。ビューはアドレスの一部です(http://localhost:8701/?view=inspect、?view=device)。そのため、ブックマークでき、
ページには最後に使用したビューが記憶されます。
| パネル | ソース | 間隔 |
|---|---|---|
| CPU % + スレッド | /proc/<pid>/stat | 1 s |
| メモリー(PSS:Java / Native / Graphics / Code / Other) | dumpsys meminfo --local <pid> | 3 s |
| フレーム(FPS、ジャンク、p50/p90 フレーム時間) | dumpsys gfxinfo <pid> framestats reset(デバイス側の経過時間には /proc/uptime を使用) | 0.5 s |
| Logcat(アプリの PID のみ) | logcat -v epoch --pid=<pid> | ストリーム |
| プロセスの開始/再起動/終了マーカー | pidof <pkg> | 1 s |
アプリのデバッグビルドに SDK がインストールされている場合、ダッシュボードには次も表示されます。
| パネル | ソース(アプリ内 SDK) |
|---|---|
| Screens レーン + すべてのグラフにわたる色合い | Activity / Fragment / DialogFragment のライフサイクル(最前面の画面) |
レンダリング/フレーム時間(ソース SDK) | Window.OnFrameMetricsAvailableListener(API 24+) |
| フレーム内訳(input … swap、delay、GPU) | FrameMetrics の各ステージを 500 ms ごとに集計 |
| 画面ごとの表(フレーム、ジャンク %、p90、最上位ステージ) | 上記のデータを画面ごとにグループ化 |
| メインスレッドのレーン(停止ごとに 1 本のバー) | メインスレッドのウォッチドッグ、StrictMode 違反をティックとして表示 |
| Stalls タブ(原因、イベント、スタックの詳細) | サンプリングされたメインスレッドのスタック、StrictMode 違反(オプトイン) |
| Layouts タブ、Inflate 列、金色のレーンマーカー | XML レイアウトのインフレーション時間(Android 10+) |
| Views 列(画面ごとの数と深さ) | 画面の最初のフレーム後に取得した View 階層のスナップショット |
| Network タブ、Network レーン、モック | アプリの OkHttpClient における Lionscope.okHttpInterceptor() |
SDK のフレームデータが流れている間は、gfxinfo のポーリングが一時停止します(重複した観測や reset の競合を防ぐため)。SDK のフレームデータが止まってから 3 秒後に再開します。ダッシュボードには、SDK が接続されているか(バージョンも含む)、または追加方法が表示されます。
Screens タブでは、画面をジャンク % の高い順に並べ、それぞれの支配的なフレームステージを表示します(パネルの幅を広げると p90 と遅いフレームも表示されます)。画面名には、アプリ独自の画面エイリアス(home_tab、legacy_page_1)があればそれを使用し、なければクラス名を使用します。

Device 列から、メインスレッドをブロックするサンプルの Home ボタンをタップした後の Stalls タブです(直接、RxJava チェーン経由、コルーチン経由)。問題箇所は発生元別にグループ化されています。作業の発生元、発生頻度、継続時間が分かります。行別では、ブロックを引き起こした行を順位付けします。各停止は、継続時間、行、発生元とともに一覧表示されます。StrictMode がオンの場合は、ディスクとネットワークの違反もスタックとともに一覧に加わります。
メインスレッドをブロックしている行を見つける
SDK(0.2.0+ SDK and server)では、ウォッチドッグスレッドが 50 ms ごとにメインスレッドへティックを送信します。しきい値以内にティックが実行されない場合、ブロックが終わるまで 50 ms ごとにメインスレッドのスタックをサンプリングし、その後 1 件の停止を報告します。報告内容は、停止の開始時刻、継続時間、最前面の画面、最も頻繁にサンプリングされたスタックです(ブロックは 30 s で打ち切られます)。ダッシュボードでは、停止を原因となった行(Class.method (File.kt:87))ごとに順位付けします。これは、該当コードをインフレートする生成済み ViewBinding/DataBinding クラスを除いた、最初のアプリフレームです。スタックにアプリフレームがない場合は、Android、Java、Kotlin、AndroidX パッケージ以外の最初のライブラリフレームが対象になります(サードパーティ SDK によるメインスレッドのブロック)。スタックはフレームワークのフレームを折りたたんで表示され、デバッガーが接続されている間に発生した停止にはフラグが付きます。
しきい値は Stalls タブで選択します(100、150、250、500、または 1000 ms。デフォルトは 150 ms)。ウォッチドッグが実行されるのは、プロトコル v2 でダッシュボードに接続され、アクティビティが開始されている間だけです。
作業の発生元(0.9.0)。 RxJava の observeOn(AndroidSchedulers.mainThread()) または Dispatchers.Main 上のコルーチンを使う場合、メインスレッドのスタックに表示されるのはスケジューラーまたはディスパッチャーとラムダであり、スケジュールした側ではありません。そのため、各停止には発生元も付与されます。Stalls タブの発生元別ビューでは、原因となった行と同様に順位付けされ(件数、合計時間、最長ブロック時間)、行を選択するとイベントを絞り込めます。
- RxJava:実行されたタスクのオペレーター(
observeOn、subscribeOn、timerなど)と、チェーンをサブスクライブした行(HomeViewModel.load (HomeViewModel.kt:40)。停止の詳細にそのスタックが表示されます)、さらにチェーンの最終コンシューマーです。サブスクライブ箇所は SDK の RxJava 2 フック(以下を参照)から取得されます。フックがない場合でも、発生元にはオペレーターと、その実行時の最初のアプリフレームが表示されます。 - Coroutine:ディスパッチャー(
Dispatchers.Main、Compose のAndroidUiDispatcher)、再開された継続(現在の行にあるHomeViewModel$refresh$1)、およびアプリが kotlinx.coroutines のデバッグモード(デバッグ用のApplication.onCreateでのSystem.setProperty("kotlinx.coroutines.debug", "on"))を実行している場合は、コルーチンの名前と ID(CoroutineNameによるrefresh-cart#2)です。 - Handler:投稿された
Runnable(lambda in HomeFragment、LiveData.postValue)またはメッセージを処理したHandler、Input(入力イベント、クリック、長押し)、Frame(レイアウトと描画、アニメーター、Compose のフレーム)、Lifecycle(アクティビティ、サービス、レシーバーのコールバック)、Idle(アイドルハンドラー)です。
発生元はスタックの最外側のフレーム(Looper のディスパッチ)から読み取られます。SDK 0.9.0 は、スタックが保持される 64 フレームを超えていてもこれを送信するため、古い SDK による停止でも、スタックが Looper まで到達していれば発生元を特定できます。Main スレッドレーンのツールチップには、各バーの発生元が表示されます。
RxJava のトラッキング(デフォルトで有効、SDK 0.9.0、RxJava 2)。 Stalls タブのスイッチをオンにすると、セッション中に 2 種類の
RxJavaPlugins フックがインストールされます。1 つは、メインスレッドが実行中の Rx タスクを示すスケジュールハンドラーです。もう 1 つは、各 observeOn サブスクリプションのスタックを保持するサブスクライブフックです(VM が取得したままの形で保持し、最大で毎秒 100 件までです。停止が発生した場合にのみフレームに変換され、チェーンとともに破棄されます)。各フックは、アプリが事前にインストールしていたフックを呼び出します。セッションが終了したとき、またはスイッチをオフにしたときにフックは削除されます。ただし、その後アプリがフックを置き換えている場合は、アプリのチェーン内にパススルーとして残り、二重にインストールされることはありません。RxJavaPlugins がロックされている場合、プロファイリングセッション外(ダッシュボードのない検査専用リンク)、または RxJava 2 を使用していないアプリでは、何もインストールされません(RxJava は SDK のコンパイル時のみの依存関係です)。API 36 エミュレーターでの測定では、スケジュールハンドラーのコストは測定できず、サブスクライブフックは observeOn サブスクリプションごとに 1~3 µs でした(スタック深度は 14~58)。これは、RxJava のアセンブリトラッキングのようにサブスクライブ時にスタックを実体化した場合の 13~14 µs と比べた値です。RxJava 3 にはまだフックしていません(その停止では引き続きスタックから発生元を取得します)。SDK のコンシューマー向け ProGuard ルールでは、難読化されたデバッグビルドでも、フックが読み取る RxJava オペレーターとフィールドの名前が保持されます。
StrictMode(オプトイン、デフォルトではオフ)。 Stalls タブには StrictMode の切り替えスイッチがあります(Android 9 / API 28 以降)。オンにすると、SDK はアプリの現在のメインスレッドおよびプロセスの StrictMode ポリシーを拡張し、ディスクの読み取り/書き込み、ネットワーク、カスタムの低速呼び出し、およびクローズ漏れの検出を追加して、各違反をスタックとともに報告します。オフにしたとき、またはリンクを閉じたときには、アプリのポリシーを復元します。死亡、ダイアログ、フラッシュ、または Dropbox のペナルティがあるポリシーや、アプリ独自の penaltyListener があるように見えるポリシーには手を加えません。また、ネットワーク違反時にアプリを終了するペナルティを再設定することもありません。その場合、Stalls タブには何も追加されなかったことが表示されます。違反はスタックごとに毎秒 1 件にまとめられ、全体では最大で毎秒 10 件です。
レイアウト:XML のインフレにかかるコスト
SDK(0.4.0+ の SDK とサーバー)を使用すると、Layouts タブには、アプリが AppCompatActivity のインフレーターを通じてメインスレッド上でインフレートする XML レイアウトが表示されます。インフレートの回数、合計時間、平均時間、最長時間、最も時間のかかったインフレートで作成されたビュー、および実行された画面を確認できます。合計時間の順に並んでおり、列をクリックすると、その列で並べ替えられます。Android フレームワークやライブラリに由来するレイアウト(abc_、mtrl_、design_ など)は、Show library layouts をオンにするまで非表示です。SDK が名前を特定できないレイアウトは、最初のビューと XML の行番号で表示されます(Button:12、斜体)。別のレイアウト内でインフレートされるレイアウト(<include> や複合ビュー独自のレイアウトなど)も、独自のコストであるため、nested と表示された独自の行に表示されます。ただし、その時間は外側のレイアウトの時間にすでに含まれているため、行の時間を加算する必要はありません。
サンプルの画面を開いた後の Layouts タブです。fragment_home が単一のインフレートとしては最も時間がかかっています。インクルードされたカード(view_dashboard_card、nested)がその大部分を占め、item_row は RecyclerView の 29 行にわたって時間が積み重なっています。
Screens タブには Inflate 列(画面上でのインフレート時間。各インフレートを一度だけ計上し、ネストされたものは除外します。ツールチップには最も時間のかかったレイアウトが表示されます)と、Views 列(画面の最新のビュー階層におけるビュー数と深さ。ツールチップには最も深いパスが表示されます)が追加されます。16 ms 以上かかるトップレベルのインフレートは、Main thread レーンのベースライン下のレールにも金色のバーとして表示されます。バーにカーソルを合わせると、レイアウト、所要時間、ビュー数、画面が表示されます。クリックすると Layouts タブが開きます。
測定方法:SDK は各 Activity の作成前に LayoutInflater.Factory2 を設定し、レイアウトの最初のビューから最後のビューまでの作成時間を計測してから、処理を引き継ぎます(AppCompat によるビューの置き換えや MaterialButton などには手を加えません)。Gradle プラグインもアプリの変更も不要です。数値は下限値として捉えてください。パーサーのセットアップは含まれておらず、デバッグ可能なビルドはリリースビルドより遅くなります。
測定されないもの:アプリケーションコンテキスト経由または Service 内でのインフレート、バックグラウンドスレッド上でのインフレート、トラッキングがオフの間に作成された Activity 内でのインフレート、すでに LayoutInflater ファクトリーが設定されている Activity 内でのインフレート、AppCompatActivity ではない画面(通常の Activity と ComponentActivity の画面。ただし、これらのビュー階層は取得されます)。レイアウト測定にはAndroid 10(API 29)以降が必要です。それより前のバージョンでは、その旨がタブに表示され、何もインストールされません。インフレートのトラッキングはデフォルトでオンになっており、Stalls タブの StrictMode の横に切り替えスイッチがあります。オフにすると記録が停止し、その後に開いた画面にはファクトリーが設定されません(再接続後も、セッションで再びオンにするまで同様です)。オンの間に開いた画面には、設定済みのファクトリーが維持されます。
アプリが独自のファクトリーを設定する場合。 SDK がファクトリーを設定するのは、Activity のインフレーターに何も設定されていない場合のみです。アプリ(またはライブラリ)が後から同じインフレーターに LayoutInflater.setFactory2 を呼び出すと、たとえば super.onCreate の後に onCreate 内で呼び出した場合、Android は A factory has already been set on this LayoutInflater をスローします。最初の Activity は SDK がサーバーから通知を受け取る前に作成されるため、ダッシュボードのスイッチでは対処できません。代わりに、デバッグ用マニフェストでアプリのインフレート測定をオフにしてください(ビュー階層の取得は引き続き機能します)。
<application>
<meta-data android:name="io.github.risyadul.lionscope.inflateTracking" android:value="false" />
</application>
スタックトレースを添えて、ぜひ Issue を作成してください。
ビュー階層。 画面を訪問するたびに一度、最初のフレームの後、メインスレッドがアイドル状態のときに、SDK はウィンドウのビュー階層を走査します(反復処理で、上限は 20,000 ビュー)。その後、ビュー数とビューグループ数、深さ、最も深いビューへのパス、最も一般的なビュークラスを報告します。Android 10 より前のバージョンを含め、SDK がサポートするすべての API レベルで動作します。
レイアウトインスペクター:ビューを選択して、その出どころを確認する
デバイスツールバーで Inspect を押します(エミュレーターでも実機でも使用でき、プロファイリングセッションは不要です)。画面がスナップショットで固定され、Layouts の隣に Inspector タブが開きます。ポインターの下にあるビューをハイライト表示し(ボックス上に Class#id とサイズが表示されます)、クリックして選択します。選択したビューのパディングとマージンが画面上に表示され、タブにはクラス、ID、テキスト、境界とサイズ、表示状態、アルファ値、レイアウトの幅と高さ、パディング、マージン、フラグ、所有する Fragment と Activity、およびビューが展開された XML レイアウトと行番号(fragment_cart.xml:33)が表示されます。Compose の画面では、ノードは コンポーザブル(Button、ProductCard)として表示され、呼び出し元の Kotlin ファイルと行番号(CartScreen.kt:87)も表示されます。横に表示されるツリーは折りたたみ可能で、選択に追従し、矢印キー(↑↓で移動、→で展開、←で閉じる)を使用でき、クラス、ID、テキスト、ソースファイル、遷移先を検索できます。行にポインターを合わせると、画面上のビューがハイライト表示されます。Esc または切り替えボタンでライブ画面に戻ります。Inspect が有効な間は、アプリの最前面の画面が変わると約 0.7 秒後にスナップショットが更新されます(継続的には更新されません)。更新ボタンを押すと新しいスナップショットが取得されます。
- **ツリーの取得元。** アプリに SDK が組み込まれている場合(Lionscope 0.5.0 以降)、SDK はアプリ自身のビューを走査し、バッジには `SDK` と表示されます。SDK 0.9.0 以降では、プロファイリングセッションは不要です。ダッシュボードでデバイスのインスペクター(インスペクタービューまたはタブ)を表示している間、およびそのデバイスを最後にインスペクトしてから(あなたまたはエージェントが)5 分間、サーバーはそのデバイス上のデバッグ可能な SDK アプリへのインスペクト専用リンクを維持します。このリンクでは、SDK は画面の最前面にあるものを報告し、インスペクトに応答するだけです。Start を押すまでは、フレーム、ウォッチドッグ、StrictMode、レイアウトのレポートはありません。リンクが表示された後の最初のインスペクトでは、アプリが接続するまで最大 6 秒待つことがあります(接続されなかった場合、バッジには `SDK connecting` と表示されます)。SDK 0.5.0 または 0.5.1 のアプリは、引き続きセッション中のみリンクされます(バッジ:`SDK needs a session`)。別の Lionscope サーバーがすでにデバイスのリンクを保持している場合、バッジには `SDK link busy` と表示されます。Android 10 以降では、ダイアログを含むアプリのすべてのウィンドウが対象です。Android 10 未満では、最前面の Activity とそのダイアログが対象で、レイアウトソースはありません。0.9.0 以降、SDK は Compose も読み取ります(下記参照)。それ以外の場合(アプリに SDK がない、古い SDK を使用している、読み取れない Compose、他のアプリ、システム UI)、サーバーは UI ヘルパー(副作用についてはエージェントツール側の注意事項を参照)または `uiautomator dump` で読み取ったアクセシビリティツリーにフォールバックし、バッジには理由とともに `uiautomator` と表示されます。uiautomator ではノード数が少なく、ソース、オーナー、パディング、マージンも取得できません。これらのフィールドにはダッシュが表示されます。
- **Compose。** SDK(セッションの有無を問わず、0.9.0 以降)は、Android Studio のレイアウトインスペクターと同じ方法で、すべての `ComposeView`(`setContent {}` を使った Activity、XML レイアウト内の ComposeView、Compose ダイアログ、ポップアップウィンドウ)のコンポジションを読み取ります。SDK は起動時に、何かがコンポーズされる前に Compose のツールデータを有効にします。ノードは、**コードから呼び出された**コンポーザブルです。そのため、`Button` はノードになりますが、その内部の `Surface`、`Row`、`Layout` はノードになりません(呼び出し元の位置が androidx Compose、Material、Navigation 内にある呼び出しは、あなたが呼び出した最も近いコンポーザブルにまとめられます。サードパーティライブラリの内部は表示されます)。ソースは**呼び出し位置**、つまり関数が定義された場所ではなく、呼び出しがあるファイルと行番号です。ID は `testTag`、テキスト、コンテンツの説明、フラグはセマンティクスから取得されます。遅延リストの項目はそのリストの下に、`AndroidView` の View はそのコンポーザブルの下に、`ComposeView` はその周囲の XML ビューの下に表示されます。境界はレイアウトされるコンテンツボックス(パディング修飾子の内側)です。コンポーザブルにはアルファ、パディング、マージン、レイアウトパラメーターはありません。`Lionscope.trackNavigation`(下記参照)を使うと、オーナーにはコンポーザブルが描画されるナビゲーション先が表示されます。画面上で選択すると、何も描画せず(背景、境界線、画像、テキストがない。画面全体に広がるアイドル状態のトーストやスナックバーのホストなど)、テキスト、コンテンツの説明、テストタグ、クリック操作もないコンポーザブルは選択対象から除外されますが、ツリーには残ります。同じ場所をもう一度クリックすると、その下のビューに移動します。Jetpack Compose 1.6 以降、および Android 上の Compose Multiplatform 1.8 以降に対応し、ソース情報を有効にしてコンパイルされている必要があります(デバッグビルドではデフォルトで有効です)。
- **Compose がフォールバックする場合。** SDK の起動前にコンポーズされた Compose 画面(Lionscope を後から手動でインストールした場合、またはアプリが `` でオプトアウトした場合)は読み取れません。バッジにはその理由が表示されます(アプリを再起動してください)。Compose のソース情報がないビルドも同様です。ツールデータはコンポジションを少し遅くし、メモリ使用量を少し増やします(Android Studio のインスペクターと同様です)。そのため、Compose 画面のデバッグ時のフレーム時間はやや悲観的になります。キャプチャはメインスレッドで実行されます。エミュレーターで 30 項目の遅延リストの場合は約 10 ms、低速なスマートフォンではその数倍かかります。コンポジションの読み取りは 200 ms で停止し(その時点でスナップショットに切り詰められたことが記録されます)。
- **行番号は開始タグの終わりの位置です。** aapt2 は閉じる `>` の行を記録するため、28 行目から 33 行目にまたがる `Button` の場合、33 と報告されます。コードで構築されたビュー、ViewStub、メインスレッド以外でインフレートされたビューにはソースがありません。また、インフレーション追跡がオフのときに作成された Activity のビュー(または SDK でオプトアウトされたビュー)にもソースはありません。追跡をオンにして画面を再度開いてください。
- **プライバシー。** スクリーンショットと画面上のテキストはあなたのマシン内に留まり、スナップショットはサーバーのメモリ内だけに保持されます(直近の数件)。パスワードフィールドは読み取られません。Compose の場合、パスワードセマンティクスを持つテキストフィールド(`PasswordVisualTransformation`、セキュアテキストフィールド)またはパスワードのコンテンツタイプを持つフィールドが対象です。一度パスワードフィールドとして認識されたフィールドは、「パスワードを表示」でその設定が解除されても、引き続きマスキングされます。`KeyboardType.Password` のみを設定する Compose フィールドは、画面上にテキストが表示され、他のフィールドと同様に読み取られます。AI エージェントに見えるのは、_あなたが_選択した要素のテキストと切り抜き画像のみです(ウィンドウ全体や画面の広い範囲は見えません)。エージェントが自ら選択した要素から取得できるのは、構造情報(クラス、ID、レイアウトソース、オーナー、境界)のみです。エージェントが選択したビューには、ダッシュボードで **AI** と表示されます。「AI エージェントでの使用」を参照してください。
#### ビューをリアルタイムで編集する
アプリで SDK 0.9.0 以降を使用している場合、選択した View の属性を実行中のアプリで変更し、レイアウトファイルに手を加える前に修正を試せます。変更できる属性は、表示状態、アルファ値、有効状態、パディングとマージン(ボックスモデルの図には辺ごとに数値を入力します)、レイアウトの幅と高さ(match_parent、wrap_content、またはピクセル値)、移動量、標高、背景色です。また、TextView ではテキスト、文字サイズ(sp)、文字色(#RRGGBB または #AARRGGBB)も変更できます。フィールドを編集して Enter キーを押すか、フィールドから移動すると変更が適用されます(Esc キーを押すと元に戻ります)。サーバーは SDK を通じて変更を適用し、直後に新しいスナップショットを取得して、ビューの選択状態を維持します。
色フィールドのスウォッチを開くと、カラーピッカーが表示されます。正方形と色相・アルファのスライダーをドラッグするか、画面で使用されている色または最近使用した色のいずれかをクリックします。または、Pick を押して、マウスで静止画面上の任意のピクセルをクリックします(Esc キーで選択をキャンセルします)。色は、マウスボタンを離したときに送信され、操作中に毎回送信されることはありません。
編集にはプロファイリングセッションが必要です(Start を押します)。アプリが編集を受け取るのは、ウェルカムメッセージにリンクの証明情報が含まれていたリンクからのみです(セキュリティモデルを参照)。検査専用リンクには証明情報がありません。セッションがない場合、インスペクターにはすべての値が読み取り専用で表示され、Start を押すよう案内されます。リンクキーをアプリに渡せなかったセッションの場合も、その旨が表示されます。
- 一時的な変更です。 編集内容は、元に戻すまで、アプリがそのプロパティ自体を設定するまで(RecyclerView の行の再バインドやデータバインディングなど)、またはビューが再作成されるまで(画面の回転、新しい Activity や Fragment の作成など)有効です。SDK は各値の元の状態を保持します。編集したフィールドごとに Revert ボタンが表示され、タブのヘッダーには「N 個のライブ編集(一時的)」と Revert all が表示されます。アプリがその後に設定した値には手を加えず、元に戻す操作で上書きすることはありません。
- 編集できないもの。 Composable(コードで変更してください。タブには呼び出し元のコード位置が表示されます)、uiautomator のスナップショット、SDK 0.9.0 より古いアプリ、パスワードフィールドのテキストは編集できません。パスワードフィールドのテキストが読み取り、表示、設定されることはありません。マージンの編集には
MarginLayoutParamsが必要です。ウィンドウのルートビューには編集可能なレイアウトサイズがなく、自動サイズ調整される TextView には編集可能な文字サイズがありません。 - 背景色を設定すると、背景全体が置き換わります。 元に戻すまで、ビューの背景はシェイプ、リップル、画像ではなく単色になり、背景色が着色されないよう背景のティントも無効になります。元に戻すと元の Drawable とティントが復元され、その時点でのビューのパディングは維持されます。テキストを編集すると、元に戻すまでスタイルが失われます。元のテキストは、スパン付きの装飾テキストとして復元されます。ただし、アプリ独自のスパン(クリック可能なリンクなど)が装飾した場合は、プレーンテキストとして復元されます。
- アプリ独自のコードが実行されます。 プラットフォームのセッター(
setPadding、setTextなど)のみが呼び出されますが、アプリのリスナーが動作する場合があります。たとえば、TextWatcherは編集されたテキストを受け取り、カスタムビューのsetPaddingのオーバーライドが実行されます。値には上限と下限があります(パディングは 0~2000 px、マージンは ±2000 px、サイズは最大 10000 px、テキストは制御文字と双方向テキスト上書き文字を含まない最大 500 文字、文字サイズは 1~200 sp)。また、編集はデバイスごとにレート制限されます。 - デバイスを操作できるエージェントは、セッション中に
set_view_propertyとrevert_view_propertiesを使って同じ操作を行えます(inspect_screenとget_inspector_selectionには、ノードが受け付けるプロパティ、または受け付けるプロパティがない理由が表示されます)。 - 元に戻す操作にもセッションが必要です。 セッション中に行った編集は、セッションの停止後もアプリ内に残ります(その時点でビューの状態になっています)。編集を元に戻すには、再度セッションを開始するか、アプリを再起動してください。
ネットワーク:リクエストとモック
SDK 0.9.0 以降では、アプリの OkHttpClient に1行追加するだけで、プロファイリングセッション中に Network タブにアプリの HTTP リクエストが一覧表示されます。メソッド、ホストとパス、ステータス、所要時間、サイズが表示され、失敗、保留中、モックされたリクエストには印が付きます。テキスト、メソッド、ステータスクラス(2xx … 5xx、失敗)で絞り込めます。リクエストを選択すると、概要、リクエストとレスポンスのヘッダー、本文(JSON は整形表示)、およびタイミング(ヘッダーの待機時間、その後の本文の読み取り時間)が表示されます。タイムラインの Network レーンには、各リクエストが開始から終了までのバーとしてステータス別に色分けして表示されます。クリックすると、タブで開きます。
// Retrofit (OkHttp 4.x or 5.x): add it last, after the app's own interceptors
val client = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(Lionscope.okHttpInterceptor())
.build()
val retrofit = Retrofit.Builder().baseUrl("https://api.example.com/").client(client).build()
// Ktor on Android (OkHttp engine), in androidMain
val http = HttpClient(OkHttp) { engine { addInterceptor(Lionscope.okHttpInterceptor()) } }
リリースビルドでは、同じ行に lionscope-noop を指定します。このインターセプターはリクエストをそのまま通過させます。Lionscope は OkHttp を追加せず、アプリが使用する OkHttp を使います。最後に追加すると、アプリのインターセプター(認証、ロギング)が送信する内容をキャプチャし、モックされたレスポンスもそれらを通って戻ります。代わりに addNetworkInterceptor で追加すると、記録はしますが、モックは行いません。
キャプチャ対象。 プロファイリングセッションの実行中に限ります(Inspector のセッションなしリンクではキャプチャせず、ダッシュボードがない場合もキャプチャしません。その場合は単純な chain.proceed になります)。また、SDK が実際のサーバーへのリンクだと確認した接続に限ります(セキュリティモデルを参照。確認できなかった場合はダッシュボードに表示されます)。テキスト形式の本文(JSON、XML、text/*、フォーム、GraphQL)はそれぞれ先頭の64 KBをキャプチャします。その他の本文(画像、マルチパート、protobuf、ダウンロード)はサイズと種類のみを記録します。サーバー送信イベントと WebSocket アップグレードは、ストリームを表示せずに示します。アプリが自身で Accept-Encoding: gzip を設定した本文は、表示用に解凍されます。Brotli などのエンコーディングには印を付けますが、デコードはしません。Lionscope がレスポンス本文を読み取ることはありません。アプリが読み取る内容をコピーするため、アプリは本文をちょうど1回読み取り、アプリが読み取りを終えた時点で(一覧上の)リクエストも終了します。Lionscope 内のエラーによって、アプリの呼び出しが失敗したり変更されたりすることはありません。記録されるリクエストは毎秒最大20件で、それを超える分は件数のみが記録されます。
プライバシー。 値はデバイス上で ‹redacted› に置き換えられ、さらにサーバー上でも置き換えられます。対象は Authorization、Proxy-Authorization、Cookie、Set-Cookie の各ヘッダー、および名前に token、secret、api-key / api_key / apikey、auth、session、password / passwd、credential、signature、jwt、appcheck のいずれかを含むヘッダー、クエリ、フラグメントの各パラメーター、フォームフィールド、JSON キー(深さを問いません)、XML 要素または属性です。また、名前が key、code、sig、pass、pwd のいずれかであるものや、-key、_key、Key(sig、pass、pwd についても同様)のいずれかで終わるものも対象です。URL 内のユーザー名とパスワード(user:pass@)、および Location、Content-Location、Referer、Link ヘッダー内の URL のクエリも置き換えられます。エラーメッセージ内の URL や同様のユーザー名とパスワードのペアも置き換えられます。置き換えは名前に基づいて行われるため、無害な名前の値("data": "<token>")や GraphQL クエリのテキスト内の秘密情報は検出されません。リクエストはサーバーのメモリ内にのみ保持され(直近10分間、最大5000件または32 MB)、ディスクには書き込まれません。AI エージェントには一覧、ヘッダー、タイミングが渡されますが、本文が渡されるのは --allow-network-bodies を指定して明示的に許可した場合のみです(AI エージェントでの使用を参照)。
モック。 リクエスト上の この応答をモック(またはモック一覧の 新しいモック)をクリックすると、モックエディターが開きます。メソッド(または任意)、URL パターン(完全一致、プレフィックス、または * が / を含む任意の文字列に一致する glob。パターンに ? が含まれていない限り、クエリは無視されます)、そして応答(ステータス、ヘッダー、最大 256 KB の本文)または失敗(タイムアウトまたは接続エラー。アプリには IOException として認識されます)を設定できます。最大 30 秒の遅延を指定することもできます。モックされたリクエストはネットワークに到達しません。複数のルールが一致する場合は、完全一致、プレフィックス、glob の順に優先され、次にパターンが長いもの、任意のメソッドよりそのメソッドに対するルール、最後に新しいルールが優先されます。ルールは実行中のセッションに属し、セッションが停止すると消去されます。ルールが有効な間は、どのビューでもトップバーに N 個のモックが有効 と表示され(アプリにモックが届いているかどうかも表示されます)、モックされた各リクエストには モック済み とマークされ、アプリの logcat にはモックされたリクエストごとに Lionscope の行が 1 つ追加されます。ルールは最大 32 個です。
Inspector ビューと Device ビューでは Network タブは表示されませんが、リクエストは引き続き記録され、モックのインジケーターも表示されたままです。クリックするとモック一覧に戻ります。試すには、サンプルアプリの More → Open the network demo から小さなローカル API を呼び出します。node android-sdk/sample/sample-api.mjs で起動し、エミュレーターからは http://10.0.2.2:8745 にアクセスします。
ダッシュボードからエミュレーターを操作する
デバイス選択メニューには、電源がオフのものも含め、すべての AVD が一覧表示されます。起動すると、ホスト GPU 上でウィンドウなしで(-no-window -gpu host)エミュレーターを起動します。起動方法は、Quick Boot スナップショットを読み込み、シャットダウン時に保存する Quick boot、または Cold boot(-no-snapshot-load)から選択できます。その画面は Device 列に表示され、エミュレーターのウィンドウと同様に操作できます。クリックとドラッグでタッチし、Alt+ドラッグでピンチ操作(2 本の指が中心を挟んで対称に動きます)、ホイールでスクロールします。画面にフォーカスがあるときに入力でき、Esc は「戻る」です。ツールバーでは、「戻る」「ホーム」「最近使ったアプリ」「電源」、画面の回転、フル解像度のスクリーンショット、フレームサイズ(短辺が 360、540、または 720 px)を操作できます。ミラーリングはプロファイリングセッションの有無にかかわらず動作し、列が開いていてタブが表示されている間だけストリーミングします。
クリップボードを共有(ツールバー。デフォルトで有効)すると、クリップボードが同期されます。パソコンでコピーしたテキストは、画面をクリックまたはフォーカスしたときに Android のクリップボードに送られるため、Android の長押しメニューの 貼り付け で利用できます。また、Android 内でコピーしたテキストはパソコンのクリップボードに送られます(「エミュレーターからコピーしました」)。画面上で Cmd/Ctrl+V を押すと、フォーカス中のフィールドに直接貼り付けられます。最大 32 KB の Unicode テキストに対応しています。クリップボードを読み取るにはブラウザーのクリップボード権限が必要です(Chrome では一度だけ確認されます)。Firefox 系ブラウザー(Firefox、Zen)と Safari はこの機能を提供していないため、画面上で Cmd/Ctrl+V を押してクリップボードの内容を送信してください(画面にその案内が表示されます)。Android からのコピーはすべてのブラウザーで利用できます。切り替えをオフにすると、どちらのクリップボードも読み取りも書き込みもされません(明示的な操作である Cmd/Ctrl+V による貼り付けは引き続き利用できます)。
エミュレーターをシャットダウン(Device 列およびデバイス選択メニューの横)すると、実行中のエミュレーターが終了します。そのエミュレーターでプロファイリングセッションが実行中の場合は、先に停止します。エミュレーターを Lionscope 以外(Android Studio など)で起動した場合や、セッションが実行中の場合は、Lionscope が確認を求めます。Lionscope が起動したエミュレーターもサーバー停止時にシャットダウンされますが、自分で起動したエミュレーターはシャットダウンされません。
Lionscope はエミュレーター独自の gRPC エンドポイントと通信します。このエンドポイントはエミュレーターの検出ファイル(avd/running/pid_<pid>.ini。ポートとアクセストークンが記載されています)から取得します。フレームはホスト側でエミュレーターがキャプチャし、画面に変化があった場合にのみ PNG として送信されます。そのため、エミュレーター内の Android に余分な処理は発生せず、アプリの測定値にも影響しません。M シリーズの Mac で 540 px の場合、約 58 fps、1 フレームあたり約 140 KB、入力からフレーム表示まで約 55 ms です。
エミュレーターがソフトウェアレンダリング(SwiftShader など。例:-gpu host を指定せずに -no-window で起動)を行っている場合、ダッシュボードに警告が表示されます。その場合、フレーム時間は実機を正しく反映しません。
ダッシュボードから実機を操作する
USB 接続したスマートフォンを選ぶと、Device 列に画面が表示され、エミュレーターと同じ操作(タッチ、マルチタッチ、ホイール、キー、クリップボードの共有、戻る/ホーム/最近使ったアプリ/電源、アプリが対応している場合は画面回転、スクリーンショット、フレームサイズ)が使えます。スマートフォンには何もインストールされません。Lionscope は同梱の scrcpy サーバー(v5.0.1、Apache-2.0、公開されている SHA-256 と照合済み)を /data/local/tmp に転送し、列を開いている間は adb 経由で実行します。ミラーリングが停止すると、サーバーは自動的に削除されます。ミラーリング中、スマートフォンの画面は点灯したままになり(scrcpy の stay_awake)、その後、元の設定に戻ります。スマートフォンのハードウェアエンコーダーが H.264 動画を送信し、ブラウザーが WebCodecs でデコードします(Chrome、Firefox 130+、Safari 16.4+。ダッシュボードは localhost で開く必要があります)。
ミラーリング中はスマートフォンのハードウェアエンコーダーに多少負荷がかかるため、測定をできるだけ正確に行うには Device 列を閉じてください。
Xiaomi/HyperOS(および一部の OEM ビルド)では、開発者向けオプションの USB デバッグ(セキュリティ設定) を有効にするまで、スマートフォンが入力の注入を拒否します。画面のミラーリングは引き続き行われ、ダッシュボードに有効にする設定が表示されます。
デバイスの位置を変更する
位置情報(Device 列のツールバーにあるピン)を使うと、Android Studio エミュレーターの Location コントロールと同じように、デバイスを別の場所に移動できます。地図をクリックする(またはピンをドラッグする)か、-6.1754, 106.8272 と入力するか、座標を含む Google Maps または OpenStreetMap のリンクを貼り付けてから、位置を設定をクリックします。よく使う地点は(このブラウザーに)保存できます。Lionscope がデバイスを移動するとピンが紫色になり、リセットすると元に戻ります。
- エミュレーターでは、指定した地点が GPS 位置として設定されます(エミュレーターの gRPC エンドポイント経由、またはエンドポイントがない場合は
adb emu geo fix)。アプリには通常の位置情報として認識されます。リセットすると、エミュレーターが元々持っていた位置情報に戻ります。 - **実機(Android 12 以降)**では、Android のテスト用位置情報プロバイダーを使います。Lionscope は adb シェルでモック位置情報を許可し(
appops set com.android.shell android:mock_location allow)、gps、network、fusedプロバイダーをテスト用のものに置き換えて、毎秒指定した地点を送信します(cmd location providers …)。Google Play サービスの融合位置情報もそれに従います。アプリからはLocation.isMock()が true と認識されるため、モック位置情報を拒否するアプリでは利用できません。スマートフォンには何もインストールされません。 - リセット時、サーバー停止時、またはサーバーが強制終了されたりスマートフォンの接続が解除されたり(あるいは一時的にオフラインになったり)した後、Lionscope サーバーが次にスマートフォンを検出したときに、スマートフォンは実際の位置情報に戻ります。
/data/local/tmpのマーカーファイルに、設定したサーバーと復元する内容が記録されます。別の起動中の Lionscope サーバーが移動させたスマートフォンには手を加えません(位置の設定は拒否されます)が、リセットすると、Lionscope が移動させたスマートフォンは元に戻ります。リセットすると、モック位置情報アプリが追加したものも含め、すべてのgps、network、fusedテスト用プロバイダーが削除されます。 - デバイスの位置情報スイッチがオフの場合は、パネルにその旨が表示されます。この場合、アプリには位置情報がまったく通知されません。
地図のタイルは OpenStreetMap(tile.openstreetmap.org)から取得され、パネルが開いている間だけ読み込まれます。ダッシュボードがインターネットから読み込むのはこれだけです。入力または貼り付けた座標が、お使いのマシンの外に送信されることはありません(表示する地図タイルを除く)。
要件
- Node.js ≥ 22.12
- Android SDK platform-tools(
adb)。Lionscope はLIONSCOPE_ADB、$ANDROID_HOME、$ANDROID_SDK_ROOT、~/Library/Android/sdk、PATHの順に検索します。 - USB デバッグが有効で、Android 7.0(API 24)以降を実行しているデバイスまたはエミュレーター(Lionscope は
pidof、toybox のdate +%s%3N、および非 PTY のadb shellに依存します)。
クイックスタート
npx lionscope@latest
# open http://localhost:8701
@latest を付けると、npx は新しいリリースを確認します。npx lionscope のみを実行すると、以前にキャッシュされたバージョンがそのまま使われることがあります。
ソースから実行する場合(npm パッケージは最新の git タグより遅れている場合があります):
git clone https://github.com/risyadul/Lionscope.git && cd Lionscope && npm install && npm run lionscope。
デバイスとアプリを選び、Start を押します。ADB のみを使用するモードでは、デバイスへのインストールは不要です。npx lionscope --help に、オプション(--port、--device-port、--adb、--allow-device-control、--allow-network-bodies)と、AI エージェント向けの mcp および query コマンドが表示されます。 lionscope パッケージは、単一のバンドルファイルとビルド済みダッシュボードで構成され、実行時依存関係はありません。
SDK の追加(任意、デバッグビルドのみ)
SDK は JitPack で公開されています。パブリックアーティファクトのため、認証情報やオプトイン用プロパティは不要です。
// settings.gradle (dependencyResolutionManagement) or app/build.gradle repositories
maven { url "https://jitpack.io"; content { includeGroup "com.github.risyadul.Lionscope" } }
// app/build.gradle
debugImplementation "com.github.risyadul.Lionscope:lionscope:0.9.0"
プロジェクトで dependencyResolutionManagement を使用している場合、maven ブロックは settings.gradle に記述します。それ以外の場合は、モジュールの repositories に記述します。content { includeGroup … } フィルターにより、JitPack が Lionscope 以外を提供しないようにします。
コードの変更は不要です。SDK は androidx.startup を通じて自動的に起動し、デバッグ可能なビルドのアプリのメインプロセスでのみ動作します。セッションの開始時には、選択したデバイスに対してサーバーが adb reverse tcp:8700 tcp:<gateway port> を実行し、セッションの停止時に解除します。アプリが androidx.startup の InitializationProvider を削除している場合は、Application.onCreate から Lionscope.install(context) を呼び出してください。
画面名。 デフォルトでは、画面名にはクラス名(CheckoutActivity、HomeFragment)が使われます。Activity または Fragment に、空白以外の String value を持つオブジェクトを返す public な引数なしの getScreen() がある場合(getScreen()?.value。たとえば、ScreenName インターフェースを実装する enum)、そのエイリアスが使われます。サンプルアプリの ScreenName.kt は、この規約の例です。SDK はリフレクションを通じて読み取るため、共有インターフェースは必要ありません。
Navigation Compose。 単一 Activity の Compose アプリは、SDK からは 1 つの Activity として扱われます。NavController を SDK に伝えると、すべての destination が画面になります(Screens レーン、画面ごとのジャンク、停止、Inspector の画面とオーナー)。
val navController = rememberNavController()
Lionscope.trackNavigation(navController) // once per controller; repeated calls are free
NavHost(navController, startDestination = "catalog") { /* ... */ }
画面名には destination のルートパターンが使われ、引数は含まれません。product/{id}(クエリのプレースホルダーは名前から除外)、または型安全なルートの場合はクラス名(ProductDetail)です。dialog() destination はダイアログとして扱われます。この機能は、アプリの Fragment が前面にない場合に適用されます。ネストされた NavHost がある場合は、最も内側に表示されているものが優先されるため、それぞれを登録してください。lionscope-noop にも同じ呼び出しがあるため、リリースビルドでもコンパイルできます。Kotlin Multiplatform(org.jetbrains.androidx.navigation:navigation-compose を使った Compose Multiplatform)では、SDK は Android の依存関係であるため、expect/actual 関数などを通じて androidMain から呼び出してください。
// commonMain
@Composable expect fun TrackNavigation(navController: NavHostController)
// androidMain
@Composable actual fun TrackNavigation(navController: NavHostController) = Lionscope.trackNavigation(navController)
// iosMain (and other targets)
@Composable actual fun TrackNavigation(navController: NavHostController) = Unit
Voyager と Navigation 3 は、まだ追跡されていません。
SDK の詳細:minSdk 23(フレームメトリクスには API 24 が必要)、Java 8 バイトコード、Kotlin メタデータ 2.0。実行時依存関係は androidx.startup のみです(androidx.fragment、Compose UI、Navigation、Kotlin 標準ライブラリはホストアプリから提供され、アプリにある場合にのみ使用されます)。
デモアプリ:android-sdk/sample。
ネットワーク(任意)。 コード変更が必要な SDK 機能はこれだけです。ネットワークタブとモックを使用するには、アプリの OkHttpClient(Retrofit、または Ktor の OkHttp エンジン)に Lionscope.okHttpInterceptor() を追加してください。OkHttp 4.x または 5.x はアプリから提供されます。
リリースビルドに含めない
debugImplementationを使うと、リリースビルドにはLionscopeの要素が一切含まれません。クラスも、InitializationProviderエントリーも、SDKによって追加されるINTERNET権限も含まれません。さらに、アプリがデバッグ可能(FLAG_DEBUGGABLE)でなければ、SDKは何もしません。
API(Lionscope.install()、Lionscope.isActive()、Lionscope.trackNavigation()、Lionscope.okHttpInterceptor())を明示的に呼び出すアプリでは、リリースビルドでもコンパイルできるようにする必要があります。そのようなアプリ向けに、同じAPIを持つno-opアーティファクトを用意しています(インターセプターはリクエストをそのまま通します)。
releaseImplementation "com.github.risyadul.Lionscope:lionscope-noop:0.9.0"
ダッシュボードが接続されていない場合の負荷
- ライフサイクルコールバックは現在のトップ画面を記録するだけです。接続がない間は何も送信されません。
- Compose UIを使うアプリでは、Composeのツールデータが最初から有効になっています(Inspectorが後で読み取れるようにするためです)。そのため、Android StudioのInspectorを使う場合と同様に、コンポジションが少し遅くなり、メモリ使用量も少し増えます。
io.github.risyadul.lionscope.composeInspectionのメタデータでオプトアウトできます(その場合、Inspectorはそこでuiautomatorを使います)。 FrameMetricsリスナーは、ダッシュボードがアプリを受け入れている間(そのパッケージのアクティブなセッションがある間)だけ登録されるため、それ以外のときにフレームごとの処理は発生しません。- アプリがフォアグラウンドにある間、SDKは
127.0.0.1:8700への接続を1 → 2 → 5秒間隔で試みます(待ち受けているものがない場合は、ローカル接続が拒否されます)。ダッシュボードがないままフォアグラウンドで10分経過すると、試行間隔を15秒に延ばします。アプリがバックグラウンドに移ってから30秒後に試行を停止し、アプリがフォアグラウンドに戻ると再開します。 - メインスレッドのウォッチドッグスレッドは、プロトコルv2でダッシュボードがアプリを受け入れ、アクティビティが開始されている間だけ存在します。StrictModeは有効にするまでオフで、レポータースレッドは最初の有効化以降、アイドル状態(処理なし)を保ちます。
- プロファイリングセッション外での
Lionscope.okHttpInterceptor()は、volatile読み取りを1回行い、chain.proceedを呼び出すだけです。 - RxJavaフックはプロファイリングセッション中だけインストールされます(RxJavaのトラッキングを参照)。セッションが一度もなかったプロセスにはフックがありません。Looperのメッセージロギングは使用しません(
Printerを使うと、メインスレッドのメッセージごとに約3 µs、コルーチンのディスパッチごとに約17 µsの負荷が発生します)。 - スレッドはバックグラウンド優先度で実行されます(ただし、アプリがビジー状態でも処理が飢餓状態にならないよう、フレームメトリクススレッドとウォッチドッグスレッドは除きます)。上限付きの送信キューは、キューが増え続ける代わりに古いメッセージを破棄します。SDKのエラーはSDK内に封じ込められ、アプリのクラッシュハンドラーには届きません。
AI エージェントでの使用
AI コーディングエージェント(Claude Code、Cursor、VS Code、Codex、Gemini CLI、Windsurf、Claude Desktopなど)は、Lionscopeが記録した内容を読み取り、プロファイリングセッションを実行できます。「エミュレーターでショップアプリをプロファイリングして、最もカクつく画面と、メインスレッドをブロックしている行を教えて」といった使い方ができます。ダッシュボードに表示されるものと同じ数値を取得します(画面、停止、レイアウト、Logcatの数値は、1つの共有モジュールから取得されます)。また、ダッシュボードはエージェントの隣で開いたままにできます。
3つの接続方法で、ツールセットは共通です。
- stdio経由のMCP:
npx -y lionscope mcp。--port(デフォルトは8701)で指定した、すでに実行中のLionscopeサーバーを使います。実行中のサーバーがない場合は、独自プロセスで起動します。その場合、エージェントが切断するまでダッシュボードはhttp://localhost:8701で利用できます。使用中のサーバーが停止すると、次のツール呼び出しでサーバーを起動するか、別のクライアントが起動したサーバーに接続します。ログはstderrに出力されます。 - HTTP経由のMCP(Streamable HTTP):実行中のサーバー(
npx lionscope)のhttp://localhost:8701/mcpを使います。ウェブページ以外のクライアント向けです。ダッシュボードとは異なるOriginを指定したリクエストは拒否されます。デスクトップのハーネスはNodeプロセスから接続し、Originを送信しません(VS CodeとCursorのMCPクライアントはNodeの拡張機能ホストで動作します)。ブラウザーベースのクライアント(VS Code for the Web、MCP Inspectorのページなど)では使えないため、stdioコマンドを指定してください。 - シェル:
npx lionscope query <tool>を実行すると、ツールのJSON結果が出力されます。コマンドの実行しかできないエージェント向けです。 | ツール | 内容 | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 読み取り(常時) |status(デバイス、エミュレーター、セッション、SDK リンク)、list_apps、get_location(Lionscope がデバイスに設定した位置情報、エミュレーターの GPS 測位)、get_summary(直近 1/5/10 分間の CPU、メモリ、FPS、ジャンク、停止、上位の画面とレイアウト)、get_screens、get_stalls(原因となった行別、および発生元別:Rx チェーンの subscribe 箇所、コルーチン、投稿された Runnable)、get_strictmode、get_layouts、get_view_tree、search_logcat| | セッション(常時) |start_session、stop_session、set_sdk_options(停止のしきい値、StrictMode、インフレーションの追跡、RxJava の追跡) | | インスペクター(常時) |get_inspector_selection(ダッシュボードの Layout Inspector で選択した要素の属性、View の XML レイアウトと行、またはコンポーザブルの Kotlin 呼び出し箇所、所有する Fragment、Activity、遷移先、祖先と子孫、およびスクリーンショットの切り抜き。エージェントが選択したノードの構造のみ)、select_inspector_node(ダッシュボードでノードをハイライト表示します。AI としてマークされています) | | ネットワーク(常時) |get_network(直近 1/5/10 分間のアプリの HTTP リクエスト:メソッド、URL、ステータス、所要時間、サイズ、モックの有無。メソッド、ステータスクラス、ホスト、テキストでフィルター可能)、get_network_request(1 件のリクエストのヘッダーと所要時間。本文を取得するには--allow-network-bodiesが必要です) | | デバイス(オプトイン、下記参照) |get_ui_tree(画面の要素:テキスト、説明、ID、境界、フラグ)、inspect_screen(Layout Inspector のスナップショット。ダッシュボードにも表示されます)、set_view_property/revert_view_properties(スナップショットの View をライブで一時的に変更:表示状態、アルファ、パディング、マージン、サイズ、平行移動、標高、色、TextView のテキストとサイズ。SDK 0.9.0 以降、セッション中)、tap_element、wait_for、scroll_to(テキスト、ID、説明で要素を検索)、screenshot(短辺 720 px)、tap、swipe、type_text、press_key(ナビゲーションキーと編集キー。電源、スリープ、ロックは不可)、launch_app、boot_emulator、shutdown_emulator、set_network_mock、clear_network_mocks(レスポンスまたは失敗をモックします。ネットワークを参照)、set_location、clear_location(デバイスを移動します。位置情報を参照) | 「このボタンの位置がずれているのはなぜですか?」 ダッシュボードのレイアウトインスペクターで要素を選択し(Inspect を選んでからクリックします)、「この要素」について質問します。エージェントはget_inspector_selectionを呼び出し、インスペクタータブに表示されている情報(クラス、ID、テキスト、境界、パディング、マージン、レイアウトの幅と高さ、view_dashboard_actions.xmlの 15 行目、所有する Fragment)に加え、要素を最大 512 px で切り抜いたスクリーンショットを取得します(ウィンドウルートの場合、または画面の半分を超える範囲を占める要素の場合は取得されません)。この行は開始タグが終わる位置で、ツールもそのように示します。デバイス制御が有効な場合、inspect_screenはスナップショットを自ら取得し(ノードを見つけるクエリを使用し、表示されているスナップショットを置き換えます)、select_inspector_nodeは該当するノードを指し、AI と表示します。エージェント自身が選択したノードについては、get_inspector_selectionはその構造のみを返します(クラス、ID、レイアウトのソース、所有者、境界)。テキストや画像は返しません。デバイス制御のないエージェントは、この方法で画面を読み取ることはできません。アプリに SDK がない場合、スナップショットは uiautomator から取得され、レイアウトのソースや所有者は含まれません。デバイス制御が有効で、SDK 0.9.0 以降とプロファイリングセッションがある場合、エージェントは実行中のアプリで修正を試すこともできます。set_view_property { nodeId, property: "padding", value: [16, 8, 16, 8] }は、次のスナップショットでの要素の表示を返し、revert_view_propertiesは元に戻します(いずれの変更も一時的です。ビューのライブ編集を参照してください)。
「チェックアウトが失敗するのはなぜですか?」 アプリでインターセプターを有効にすると、get_network は関連するリクエスト(status: "5xx" または "failed")を一覧表示し、get_network_request は特定のリクエストのヘッダー、タイミング、エラーを返します。認証情報のように見えるヘッダーの値は、すでにマスキングされています。サーバーまたは MCP クライアントで --allow-network-bodies を指定して実行しない限り、本文はツールの結果に含まれません(例:npx -y lionscope mcp --allow-network-bodies、または lionscope query network-request --id 12 --allow-network-bodies)。指定した場合、本文は他のアプリ文字列と同様に、信頼できないデータとしてマークされます。デバイス制御が有効な場合、set_network_mock は URL に対して用意された応答、エラーステータス、またはタイムアウトを返します(ダッシュボードでは AI ルールとして表示されます)。clear_network_mocks はそれを削除します。完了したら削除するようエージェントに依頼するか、セッションを停止してください。
セットアップ
各ハーネスは npx -y lionscope mcp を自分で起動します。引数に --allow-device-control を追加すると、
デバイスの操作をエージェントに許可できます。HTTP 版は、npx lionscope で起動した
サーバーと通信します。各ツールのドキュメントに記載されている形式は、2026 年 10 月時点のものです。
Claude Code(ドキュメント)、またはプラグインをインストールすると、 サーバーとスキルをまとめて追加できます。
claude mcp add lionscope -- npx -y lionscope mcp
# or, against a running server:
claude mcp add --transport http lionscope http://localhost:8701/mcp
プロジェクトの .mcp.json を通じて共有するには --scope project を、すべてのプロジェクトで使うには --scope user を追加します。
Cursor(ドキュメント):プロジェクト内の .cursor/mcp.json、または
すべてのプロジェクトで使う場合は ~/.cursor/mcp.json を使用します。
{
"mcpServers": {
"lionscope": { "type": "stdio", "command": "npx", "args": ["-y", "lionscope", "mcp"] }
}
}
HTTP の場合:"lionscope": { "url": "http://localhost:8701/mcp" }。
VS Code(GitHub Copilot エージェントモード、ドキュメント):
ワークスペース内の .vscode/mcp.json を使用します。トップレベルのキーは servers です。
{
"servers": {
"lionscope": { "type": "stdio", "command": "npx", "args": ["-y", "lionscope", "mcp"] }
}
}
HTTP の場合:"lionscope": { "type": "http", "url": "http://localhost:8701/mcp" }。またはターミナルから実行します:
code --add-mcp '{"name":"lionscope","type":"stdio","command":"npx","args":["-y","lionscope","mcp"]}'。
Codex(ドキュメント):~/.codex/config.toml を使用するか、
codex mcp add lionscope -- npx -y lionscope mcp を実行します。
[mcp_servers.lionscope]
command = "npx"
args = ["-y", "lionscope", "mcp"]
HTTP の場合:command と args の代わりに url = "http://localhost:8701/mcp" を指定します。
Gemini CLI(ドキュメント):プロジェクト内の .gemini/settings.json、
または ~/.gemini/settings.json を使用します。
{
"mcpServers": {
"lionscope": { "command": "npx", "args": ["-y", "lionscope", "mcp"] }
}
}
HTTP の場合:ドキュメントに記載されているように "lionscope": { "httpUrl": "http://localhost:8701/mcp" } を指定します(新しいリリースでは
"url": "http://localhost:8701/mcp", "type": "http" も使用できます。これは gemini mcp add --transport http が書き込みます)。ターミナルから追加するには、
gemini mcp add -s user lionscope npx -- -y lionscope mcp を実行します(-- により、-y は npx に渡されます)。
Windsurf(2026 年 6 月に Devin Desktop に名称変更、ドキュメント):
macOS と Linux では ~/.config/devin/mcp_config.json、Windows では %APPDATA%\devin\mcp_config.json を使用します(プロジェクトでは
.devin/mcp_config.json も使用できます)。Gemini CLI と同じ mcpServers エントリーを設定します。HTTP の場合:
Cascade エージェントには "lionscope": { "serverUrl": "http://localhost:8701/mcp" }、
Devin Local エージェントには "lionscope": { "url": "http://localhost:8701/mcp" } を指定します。
Claude Desktop(ドキュメント):設定 →
開発者 → 設定を編集を選ぶと claude_desktop_config.json が開きます(macOS では ~/Library/Application Support/Claude/、
Windows では %APPDATA%\Claude\)。Gemini CLI と同じ mcpServers エントリーを追加し、Claude Desktop を再起動します。
stdio エントリーを使用してください。Claude Desktop のカスタムコネクターは Anthropic 側からリモートサーバーに接続するため、
localhost には接続できません。
その他の MCP クライアント:stdio サーバーとして npx -y lionscope mcp を実行するか、
http://localhost:8701/mcp に接続します(Streamable HTTP、認証なし、ループバックのみ)。
エージェントスキル
MCP ツールは各呼び出しで何ができるかを示します。一方、Lionscope スキル(skills/lionscope/SKILL.md)は、タスクでツールをどう使うかをエージェントに伝えます。どこから始めるか、表示が乱れた画面、行別・発生元別に確認するメインスレッドのブロック、Inspector でユーザーが選択した要素(ライブ編集で修正を試す方法も含む)、アプリのリクエストとモックされたエラー状態、修正前後の確認についてのプレイブックが含まれています。MCP サーバーがあってもなくても動作し、ない場合はシェルから npx lionscope query を使用します。これは 1 つの Agent Skills フォルダーなので、SKILL.md を読み込むすべてのハーネスで使用できます。配置場所だけが異なります。
Claude Code:プラグイン(MCP サーバーとスキルを一括でインストール)。2 つのプラグインから 1 つを選んでください。
/plugin marketplace add risyadul/Lionscope
/plugin install lionscope@lionscope # read tools and sessions
/plugin install lionscope-device-control@lionscope # also the device tools (taps, edits, mocks, emulators)
ターミナルから claude plugin install lionscope --marketplace risyadul/Lionscope を実行すると、2 つの手順をまとめて行えます。プラグインは独自に npx -y lionscope@<version> mcp を実行します。手動で追加した lionscope MCP サーバーは削除してください。削除しないと、エージェントに各ツールが 2 回ずつ表示されます。新しいリリースは /plugin marketplace update lionscope で取得できます(または /plugin でマーケットプレイスの自動更新を有効にしてください)。
どのハーネスでも、skills CLI を使用する場合(vercel-labs/skills は各エージェントのスキルの保存場所を把握しています):npx skills add risyadul/Lionscope を実行します(エージェントを選ぶには -a codex、-a opencode などを追加し、プロジェクトではなくユーザー用にインストールするには -g を追加します)。
手動の場合:skills/lionscope フォルダーをコピーするか、SKILL.md を lionscope という名前のフォルダーにダウンロードし、次の場所に配置します。
| ハーネス | プロジェクト | ユーザー用 |
|---|---|---|
| Claude Code | .claude/skills/lionscope/ | ~/.claude/skills/lionscope/ |
| Codex | .agents/skills/lionscope/ | ~/.agents/skills/lionscope/ |
| OpenCode | .opencode/skills/lionscope/(.agents/skills/ と .claude/skills/ も読み込みます) | ~/.config/opencode/skills/lionscope/ |
| Hermes Agent | ~/.hermes/skills/lionscope/、または hermes skills install https://raw.githubusercontent.com/risyadul/Lionscope/main/skills/lionscope/SKILL.md | |
| Cursor、Gemini CLI、GitHub Copilot、… | 各製品のスキルフォルダー(ドキュメントを参照) |
コピーしたスキルは、コピーした時点のバージョンのままです。Lionscope を更新した後、もう一度コピーしてください。skills CLI でインストールしたものは npx skills update で更新できます。このスキルは、作成時に使用した npm のバージョンを固定しています(npx -y lionscope@<version> query)。そのため、シェルコマンドと説明されているツールのバージョンが一致します。
エージェントにデバイスを操作させる
デバイスツールはデフォルトで無効です。エージェントはデータを読み取り、セッションを開始または停止できますが、画面には触れられません。--allow-device-control を指定して有効にしてください。1 つの MCP クライアントで有効にする場合(npx -y lionscope mcp --allow-device-control。例:claude mcp add lionscope -- npx -y lionscope mcp --allow-device-control)、またはサーバー全体で有効にする場合(npx lionscope --allow-device-control。/mcp でも有効になります)があります。これらはエミュレーターでも実機でも adb(screencap、uiautomator、input、am start -W)経由で動作するため、デバイスを手に持ってできることは何でも実行できます。個人データの入ったスマートフォンを接続している間は、無効のままにしてください。このサーバーが起動していないエミュレーターは、confirm: true の場合にのみシャットダウンされます。
操作対象を指定する方法は 2 つあります。
- 要素で指定(正確):
get_ui_treeで画面のアクセシビリティツリーを読み取ります(下記参照)。tap_element { text: "Checkout" }、wait_for { text: "Order placed" }、scroll_to { id: "row_title", text: "Row 40" }は、表示テキストまたはコンテンツ説明、リソース ID(login_button)または説明で要素を検索し、要素の正確な中央をタップします。セレクターが複数の要素に一致する場合は何もタップせず、候補を返します。nthで 1 つを選択できます。type_text { text, field: { id: "email" } }は、最初にフィールドをタップします。パスワードフィールドのテキストは返されません。 - 座標で指定:
screenshotを撮影してから、スクリーンショット上のピクセル座標でtap/swipeを実行します。ツリーで検出できない場合に使用してください。Jetpack Compose の要素にリソース ID が付与されるのは、Modifier.testTagとtestTagsAsResourceId = trueを指定した場合のみです(それ以外の場合はテキストまたはコンテンツ説明を使用してください)。また、WebView、キャンバス、ゲーム、動画コンテンツはツリーに含まれないことがあります。
ツリーは Lionscope の UI ヘルパーから取得されます(Android 7.0 以降)。これはサーバーが adb 経由でデバイス上で実行する小さなプログラムで、アクセシビリティ(UiAutomation)接続を 1 つ開いたままにし、数百ミリ秒で画面を読み取ります。アニメーションが停止するのを待つことはありません。最後に読み取ってから 20 秒後まで接続が維持され、その間、アプリからはアクセシビリティが有効に見えます。これによって動作やパフォーマンスが変わるアプリもあり、ほかの UI オートメーション(Appium、uiautomator テスト)は接続できません。ヘルパーを使用できない場合は uiautomator dump にフォールバックします。この処理には 1〜2 秒かかり、画面がアイドル状態である必要があります。アニメーションが続く画面では(再試行後に)失敗し、エラーにスクリーンショットを使う方法が提示されます。座標はデバイスの現在の回転状態(wm size とディスプレイの回転から取得し、ダイアログのウィンドウからは取得しません)に従います。画面が回転する前に撮影したスクリーンショットは、誤った場所をタップする代わりに拒否されるため、新しく撮影してください。type_text で入力できるのは印字可能な ASCII 文字で、%s の並び(スペースを表す adb 独自のエスケープ)は入力できません。launch_app は am start -W でランチャーアクティビティを起動し、Android の起動時間を返します。デバイスごとに同時に実行できるデバイス操作は最大 2 つです。それを超えるとビジー状態を示す応答が返されます。
シェルから
npx lionscope query --list # every tool, its description and input schema
npx lionscope query status
npx lionscope query summary --window 5m
npx lionscope query stalls --include-stacks
npx lionscope query logcat --level E --limit 50
npx lionscope query start-session --serial emulator-5554 --package-name com.example.shop
npx lionscope query screenshot --allow-device-control --out screen.png
npx lionscope query inspector-selection --out element.png # what you selected in the Inspector
npx lionscope query network --status 5xx --window 10m
npx lionscope query network-request --id 12 --allow-network-bodies
引数には --name value フラグ(ケバブケースまたはキャメルケース)か、--json '{"window":"5m"}' を指定します。get_/search_ の接頭辞を付けなくても、短い名前を使用できます。結果は stdout に JSON で出力されます。失敗した場合は終了コードが 1 になり、stdout に {"ok": false, "error": "…"} が出力されます。query には実行中のサーバーが必要です(ポートを選ぶには --port を指定します)。
安全上の注意
- すべてのデータはこのマシン内にとどまります。エンドポイントは 127.0.0.1 で待ち受け、ウェブページから送信される可能性のあるリクエストを拒否します (セキュリティモデルを参照)。
- ログ行、タグ、画面、レイアウト、クラス名、スタックフレームはプロファイル対象のアプリに由来します。また、Inspector の結果に含まれるビューのテキスト、コンテンツの説明、リソース ID、画像の切り抜きも同様です。ツールの結果では、これらに信頼できないデータのマークが付けられます。エージェントもそのように扱ってください。ログ行やラベルが指示になることはありません。
- 画面上のテキストが Inspector を通じてエージェントに渡されるのは、_あなた_が選択した 1 つの要素に対する
get_inspector_selection(その要素の切り抜き画像を含み、ウィンドウ全体や画面の広い範囲は含みません)と、inspect_screen(デバイス制御。画像は含みません)のみです。エージェントが自分で選択した要素(ダッシュボードで AI と表示されるselect_inspector_node)から得られるのは、クラス、ID、レイアウトのソース、所有者、境界情報のみです。パスワードフィールドは読み取られず、スクリーンショットがブラウザーにキャッシュされることもありません。 - HTTP リクエストの URL、ヘッダー、本文はアプリとそのサーバーに由来し、これらにも信頼できないデータのマークが付けられます。ヘッダー、URL、フォーム、JSON、XML 内の認証情報と思われる値は、デバイスから送信される前に秘匿化されます。本文がエージェントに渡されるのは
--allow-network-bodiesを指定した場合のみです。 --allow-device-controlは、AI が許可なくデバイスを操作するのを防ぐためのものであり、セキュリティ境界ではありません。どのローカルプロセスもすでに adb を使用できるためです。同様に、--allow-network-bodies(どのローカルプロセスもダッシュボードのソケットを読み取れます)は、必要な場合を除き、リクエストとレスポンスの本文が AI のコンテキストに含まれないようにするためのものです。
アーキテクチャ
device laptop (everything listens on 127.0.0.1)
app (debug) + :lionscope SDK ── TCP :8700 ── adb reverse ──▶ server/ SDK gateway (NDJSON)
adbd ◀───────────── adb shell: /proc, dumpsys, logcat ─────── server/ collectors
│
▼
session store (10 min)
│ HTTP + WebSocket :8701
▼
dashboard/ in the browser
emulator ◀─────────── gRPC (EmulatorController, token) ─────── server/ emulator manager + /ws/screen
phone ◀── adb forward ── scrcpy-server (H.264 + control) ───── server/ scrcpy session + /ws/screen
| ディレクトリ | 内容 |
|---|---|
protocol/ | サーバーとダッシュボードが共有する通信契約です。型(wire.ts)、zod による検証(index.ts)、SDK のゴールデンライン(fixtures/)、および insights/ が含まれます。insights/ は、エージェントのツールが再利用するダッシュボードの図表(画面、停止、レイアウト、logcat フィルター)を純粋関数として提供します |
server/ | Node.js + TypeScript:adb コレクターとパーサー、セッションストア(10 分間のリングバッファー)、WebSocket ゲートウェイ、SDK ゲートウェイ、および /mcp、/api/agent、lionscope mcp / query から利用する AI エージェントのツールを提供する agent/ |
dashboard/ | サーバーから配信される Vite + React + uPlot のシングルページアプリです |
android-sdk/ | Gradle ビルド::lionscope(デバッグ SDK)、:lionscope-noop(リリース用 no-op)、:sample(デモアプリ) |
cli/ | lionscope npm パッケージです。サーバーを esbuild で dist/lionscope.mjs にバンドルし、ビルド済みダッシュボードも含みます |
SDK は WebSocket ではなく、1 行につき 1 つの JSON オブジェクトを使い、通常の TCP でサーバーと通信します。usesCleartextTraffic=false が設定されたアプリ(または localhost を許可しないネットワークセキュリティ設定のアプリ)では ws://localhost がブロックされますが、通常のソケットはそのポリシーの対象外だからです。Kotlin のエンコーダーとサーバーのバリデーターは、どちらも同じゴールデンファイル protocol/fixtures/device-messages.ndjson を使ってテストされています。
SDK の hello にはプロトコルバージョンが含まれます(0.2.0 以降は 2:stall、StrictMode、config メッセージ)。サーバーは、そのバージョンと自身のバージョンのうち低い方(ネゴシエートされたバージョン。1 から 1000 まで受け付けます)を含む welcome で応答します。サーバーが認識しないメッセージタイプの行は、接続を閉じるのではなくスキップしてカウントし、認識済みのタイプは引き続き厳密に検証します。0.1.1 以降では、異なるリリースの SDK とサーバーでも動作します。0.1.1 のサーバーまたは SDK はプロトコル 1 をネゴシエートするため、stall と StrictMode は利用できません。サーバーは、不正な stall または StrictMode メッセージを接続を閉じずに破棄します。0.1.0 のサーバーはプロトコルバージョン 1 のみを受け付け、未知のメッセージタイプを受信すると接続を閉じます。
ネットワークインスペクターを使うには、SDK とサーバーの両方でデバイスプロトコル 7 が必要です(SDK とサーバーが 0.9.0 以降)。古いサーバーではインターセプターはパススルーとして動作し、古い SDK はリクエストを送信しません。HTTP のリクエスト行とレスポンス行のサイズは最大 256 KB です(その他の SDK 行は 64 KB の上限が適用されます)。サーバーのモックルールは、フラットな行として最大 512 KB まで SDK に送信されます。
stall の発生元情報では、デバイスプロトコル 9 を使用します(SDK とサーバーが 0.9.0)。stall にはスタックの最外側のフレーム、RxJava フックによる検出結果、コルーチン名を含められ、config には RxJava の切り替え設定が含まれます。古いサーバーでは、SDK は従来どおり stall を送信し、RxJava フックはオフのままです。古い SDK からの stall には、引き続きスタックから発生元情報が付与されます。
セキュリティモデル
- サーバーは
127.0.0.1のみで待ち受けます。WebSocket のアップグレードは、ループバックの Host/Origin からのみ受け付けます (クロスサイト WebSocket ハイジャックと DNS リバインディングを防ぎます)。ダッシュボードからのすべてのメッセージを検証します。 - AI エージェントのエンドポイント(
/mcp、/api/agent/<tool>)は、サーバー自身のポートでlocalhost/127.0.0.1以外を指す Host を拒否します (DNS リバインディング対策)。/mcpはダッシュボード以外のOriginを拒否し、JSON ツール API はあらゆるOriginを拒否します(CSRF 対策)。また、Content-Type: application/jsonとX-Lionscope-Agent: 1も必要です。これらは、サーバーが決して許可しない CORS プリフライトを経なければウェブページから送信できません。 入力されたテキストがエラーやログに表示されることはありません。デバイス制御ツールは--allow-device-controlが指定された場合にのみ利用できます。このフラグは、AI が求められていないのにスマートフォンを操作するのを防ぐためのものであり、セキュリティ上の境界ではありません(ローカルプロセスはどのみち adb を実行できます)。ツールの結果に含まれるアプリデータには、信頼できないデータであることが明示されます。 - デバイスのシリアル番号とパッケージ名は厳格なパターンで検証され、ホストシェルを介さず、引数配列として
adbに渡されます。 - SDK ゲートウェイ(TCP、
127.0.0.1のみ)は、1 秒以内に最初の行として有効なhelloを要求し、HTTP に見えるリクエストを切断します(ブラウザーは localhost に POST できるためです)。helloと行の送信頻度を制限し、行のサイズと接続数に上限を設けます。また、接続元のデバイスでpidofを使ってhelloの PID を検証するまで、何も応答しません(welcomeも返しません)。どの拒否応答も同じ内容なので、調査対象のアプリをプローブで特定することはできません。Inspect 専用リンク(0.9.0)を持つデバイスごとに専用のゲートウェイリスナーが設けられるため、リンクは常にそのデバイスに結び付けられます。その方法でリンクされるのはデバッグ可能なアプリのみで、Inspect が対象にするのは Android がフォアグラウンドにあると報告したアプリだけです。 Inspect リンク用のadb reverse tcp:8700マッピングは、誰かがそのデバイスを Inspect している間だけ存在し、ほかの実行中の Lionscope サーバーから取得されたり、ほかのサーバーのために削除されたりすることはありません。 - ダッシュボードと画面ソケットには、それ自体のパスワードはありません。同じマシン上の
127.0.0.1に接続できるユーザーのプロセスであれば、ミラーリングされたエミュレーターの操作を含め、誰でも利用できます。これは、127.0.0.1:5037の adb サーバーがすでにローカルプロセスに許可しているアクセスを超えるものではありません。信頼できない人と共有しているマシンでは Lionscope を実行しないでください。 - エミュレーターのアクセストークンはエミュレーターの検出ファイルから読み取られ、サーバーのみが使用します。ログに記録されたり、ブラウザーに送信されたりすることはありません。画面ソケット(
/ws/screen)にも同じ Host/Origin チェック、サイズ上限、入力レート制限が適用されます。AVD 名は、何かを起動する前にemulator -list-avdsの出力と照合されます。このサーバーが起動していないエミュレーターは、ダッシュボードでユーザーが確認した後にのみシャットダウンされ、専用の gRPC エンドポイントか、そのシリアル番号に対するadb emu killを介してのみ終了します(プロセスシグナルは使用しません)。 - 物理デバイスの場合、scrcpy サーバーの jar はプッシュのたびに SHA-256 と照合されます。スマートフォン上で実行するコマンドラインは固定プリセットから構築されるため、ダッシュボードの値がデバイスのシェルに渡ることはありません。また、スマートフォンのプロセスと adb フォワードは、すべての終了経路で削除されます。動画とスマートフォンからのデバイスメッセージはサイズを検証します。ミラーリングの開始時に、adb はローカルポートをスマートフォンへ一時的にフォワードします。Lionscope の接続後、すぐに削除されます。adb サーバーを
adb -a(すべてのインターフェースで待ち受け)で実行していると、このポートがその間ネットワークから到達可能になるため、adb はデフォルトのループバックバインドのままにしてください。 - SDK が送信するのは、画面クラス名/エイリアス、フレームのタイミング、さらに停滞検出が有効な場合はメインスレッドのスタックフレーム(クラス、メソッド、ファイル、行番号。RxJava の追跡時には Rx タスクとコンシューマーのクラス名、およびチェーンをサブスクライブした呼び出しのフレームも含みます。また、デバッグモードではコルーチンの名前も含みます)、そして StrictMode のメッセージのみです。これらはデバッグ可能なビルドからのみ送信されます。Inspect
を実行すると、画面上のテキストを含むビュー階層も送信されます(パスワードフィールドのテキストは送信されません)。
adb reverseマッピングがない場合、デバイス上で127.0.0.1:8700を待ち受ける別のアプリがサーバーとして動作し、このストリームを受信できます。ビュー階層を要求された場合は、それも含まれます。Lionscope で Inspect するデバイスには、信頼できないアプリを入れたままにしないでください。 - アプリで
Lionscope.okHttpInterceptor()を使うと、プロファイリングセッション中に限り、各 HTTP リクエストのメソッド、URL、ヘッダー、タイミング、およびテキスト本文(各最大 64 KB)が SDK から送信されます。認証情報らしき値は送信前に秘匿化されます(Network を参照)。サーバーはすべてのフィールドのサイズを検証し、さらに秘匿化します。他の SDK データと異なり、ネットワークキャプチャとモックには、127.0.0.1:8700のリスナーが adb の信頼する Lionscope サーバーであることの証明が必要です。セッションのリンクを受け入れる前に、サーバーはそのアプリプロセス用のランダムなキーを作成し、android.permission.DUMPで保護された SDK のLinkKeyReceiverにadb shell am broadcastを使って渡します(この権限を持つのは shell とシステムのみなので、ほかのアプリはキーを設定できません)。続いて welcome には、そのキーを使って SDK のhelloに含まれる新しい nonce から計算した HMAC-SHA256 が含まれます。SDK は、検証に成功した場合にのみリクエストを記録し、モックルールを受け入れます。そのため、サーバーになりすましてそのポートを待ち受けるアプリは、ネットワークデータを受信できず、モックを注入したりビューを編集したりすることもできません(0.9.0)。ただし、上記の画面、フレーム、停滞情報、Inspector のビュー階層は引き続き受信できます。リクエストはメモリ内にのみ保存され(10 分、32 MB)、ディスクには書き込まれません。本文が AI エージェントに送られるのは--allow-network-bodiesが指定された場合のみです。モックルールは、そのセッションリンクを通じてのみアプリに送信され、セッションの停止時に消去されます。また、エージェントがモックルールを設定するには--allow-device-controlが必要です(アプリが認識する内容が変わるためです)。 - 位置情報(0.10.0):座標は検証され(WGS 84 の範囲)、固定小数点数として固定コマンドでデバイスに送信されます。物理デバイスでは、Lionscope が位置を変更している間のみシェルのモック位置情報の権限を設定し、その後、以前のモードに戻します。ダッシュボードの CSP は地図用に
https://tile.openstreetmap.orgからの画像を許可します。ブラウザーは Location パネルが開いている間のみ、OSM のタイルポリシーが求めるとおり、ダッシュボードのオリジンを Referer として付けて画像を取得します。 - ライブビューの編集(0.9.0)は、現在の Inspector スナップショットにあるアプリにのみ、そのスナップショットを取得した SDK リンクを通じて適用されます。また、対象はそのアプリの最新キャプチャにあるビューに限られます(SDK が各 View オブジェクトにキーを付与し、再作成されたビューには新しいキーが付与されます)。編集によってアプリの動作が変わるため(無効だったボタンを有効にする、ラベルを書き換えるなど)、SDK は上記のリンク証明を備えたリンク(プロファイリングセッション)でのみ適用し、それ以外には
unauthenticatedを返します。ダッシュボードはフィールドを確定したときにのみ編集を送信します。エージェントが編集するには--allow-device-controlが必要です。値はサーバーと SDK の両方で固定範囲と照合され、デバイスごとにレート制限が適用されます(10 件までのバースト後、毎秒 5 件)。SDK 自体でもレート制限を適用し、最大 256 件を保持し、メインスレッドでの処理待ちは最大 8 件までとし、実行前に接続が終了した編集は破棄します。SDK が呼び出すのはプラットフォームのセッター(および AppCompat の背景色合い)だけで、ほかのアプリコードは呼び出しません。ただし、これらのセッターがアプリのリスナーを実行することはあります。また、パスワードフィールドのテキストを読み取ったり設定したりすることはなく、編集したフィールドがパスワードフィールドに変わった後も、そのテキストを保持しません。
Known limitations
- メインプロセスのみ。 パッケージ名と同じ名前のプロセスのみがプロファイルされます。セカンダリプロセス(
:remoteサービス、WebView のサンドボックス化されたレンダラー)は CPU、メモリ、logcat の対象に含まれず、SDK もそれらのプロセスでは起動しません。 gfxinfoで確認できるのは UI ツールキットのフレームのみです。 ADB のみのモードでは、フレームはdumpsys gfxinfoから取得されます。これは Android の UI ツールキット(HWUI)が描画したフレームのみを確認します。SurfaceViewやTextureViewに描画されるコンテンツ(動画、カメラ、地図、ゲーム、Flutter)はカウントされないため、0 FPS はアプリがフリーズしているという意味ではなく、「UI フレームが描画されなかった」ことを示す場合があります。- デバッグ可能なビルドは遅くなります。 デバッグ可能なビルドで測定した数値は、リリースビルドより遅くなります。問題が_どこにあるか_を見つけるには Lionscope を使い、絶対値の測定には Macrobenchmark または本番環境のモニタリングを使ってください。
- 画面の帰属は 500 ms 単位です。 SDK のフレームは 500 ms の区間に集約され、各区間の終了時に最前面にあった画面に帰属します。画面遷移の最後の瞬間に発生したフレームが、次の画面にカウントされることがあります。
- スタールの検出はサンプリング方式です。 ウォッチドッグは約 50 ms 刻みで動作するため、時間は概算であり、しきい値をわずかに超えるブロックは検出されないことがあります。メインキュー内で他の処理を待つティックの時間も、ブロック時間としてカウントされます。
- 発生元の特定には死角があります。 コルーチンの起動箇所は特定できません(Android にはコルーチンのデバッグプローブがありません)。発生元には再開した継続処理の名前が表示され、名前が表示されるのは kotlinx.coroutines のデバッグモードのみです。セッション開始前に購読された Rx チェーンや、
observeOnがsubscribeを呼び出したスレッドとは別のスレッドで購読されるチェーン(subscribeOnより上にあるobserveOn)には購読箇所がありません。購読箇所が付くのはobserveOnチェーンのみです。Dispatchers.Main.immediateまたはトランポリンによって実行される処理は、ディスパッチのきっかけとなったメッセージ内で実行され、そのメッセージに帰属します。 - 難読化された SDK では名前が短くなります。 スタールの原因が R8 または ProGuard で難読化されたサードパーティ SDK の場合、フレームは
com.vendor.internal.b.a (SourceFile:22)のように表示されます。パッケージ名からメインスレッドをブロックした SDK はわかりますが、行番号から読みやすいソースを特定することはできません。 - StrictMode はアプリのポリシーを共有します。 Lionscope が追加する検出は、アプリ独自の基本ポリシーのペナルティに従うため、logcat に行が追加されることがあります(例:
penaltyLog)。独自のpenaltyListenerを持つアプリはヒューリスティックによって検出されますが、すべてのリスナーを確認できるわけではありません。ネットワーク違反が報告されるのは、アプリのポリシーでメインスレッド上のネットワークアクセスが許可されている場合のみです。 - Inspector での Compose の表示には、難読化されておらず、Compose のソース情報(デフォルト)を含めてコンパイルされ、SDK の起動後にコンポーズされたデバッグビルドが必要です。コンポーザブルとして表示されるのは、コード(および androidx Compose、Material、Navigation 以外のライブラリ)から呼び出されたもののみです。レイアウトを行わないコンポーザブル(エフェクト、
Dialogの呼び出し、プロバイダー)には独自のノードがなく、Dialogのコンテンツはダイアログのウィンドウに表示されます。 - ライブ編集は View のみが対象です。 コンポーザブルは読み取り専用です(再コンポーズのたびに値がコードから取得されます)。編集内容は、元に戻すか、アプリがプロパティを自ら設定するか、ビューが再作成されるまで有効です。
- レイアウト時間は下限値です。 パーサーのセットアップは計測されず、ネストされた
<include>は親の一部としても個別にもカウントされます。また、Activity のインフレーターを通じてメインスレッド上で行われるインフレートのみが計測されます。バックグラウンドスレッド上、アプリケーションコンテキストのインフレーターによるもの、または Compose によるレイアウトのインフレートは計測されません。 - 画面ミラーリング。 エミュレーターでは、gRPC ポートとトークンを含むディスカバリーファイルが必要です(emulator 36.6 でテスト済み)。AVD にはマウスがないため、ホイール操作は短いタッチドラッグでスクロールします。入力したキーが確実に認識されるのは印字可能な ASCII 文字です(エミュレーターがそれ以外の文字を取りこぼすことがあります)。それ以外は貼り付けてください。実機では WebCodecs による H.264 デコードに対応したブラウザーが必要です。また、ダッシュボードからの回転が機能するのは、アプリが回転を許可している場合のみです。ミラーリングされた映像は端末自身のエンコーダーの出力であるため、画質と遅延は端末に依存します。Linux と Windows のディスカバリーの場所は Android Studio に準じていますが、これらのシステムではまだ試していません。
- メモリには
dumpsys meminfo --localを使用します。これは、アプリにランタイム統計のダンプを要求せずに取得できる、同じ App Summary の数値です(要求するとポーリングのたびに GC が強制されます)。それでも約 0.3~2 秒かかるため、メモリは 3 秒ごとにサンプリングされます。 - ジャンクは API 31 以降では Android 独自の定義(フレームが期限後に完了すること)に従います。「遅い」フレームとは、1 vsync 間隔より長くかかったフレームです。API 31 未満では、フレームのタイムスタンプから vsync 周期を推定するため、90/120 Hz の端末も正しく測定されます。一部の OEM ビルド(例:Xiaomi の HyperOS、Android 15)では、通常のフレームにベンダーフラグのビットが設定され、
FrameIntervalとFrameStartTimeのヘッダーラベルが入れ替わります。パーサーはどちらにも対応しています。 - ネットワークのキャプチャには OkHttp が必要です。 キャプチャされるのは、
Lionscope.okHttpInterceptor()を指定したOkHttpClient経由のリクエスト(Retrofit、Ktor の OkHttp エンジン、Coil/Glide の OkHttp 連携)だけです。HttpURLConnection、Ktor の CIO エンジン、WebView、gRPC 独自のトランスポート、ネイティブコードは対象外です。OkHttp 4.x と 5.x(4.12 および 5.4 でテスト済み)に対応していますが、3.x には対応していません。リクエストボディは、安全に 2 回書き込める場合(ワンショットでもデュプレックスでもなく、長さが判明していて 1 MB 以下の場合)にのみキャプチャされます。アプリが最後まで読み取る前にレスポンスを閉じると、途中までのデータとして表示されます。ボディは最大 64 KB まで、テキスト形式の場合にのみキャプチャされます。Brotli および zstd のボディはデコードされません。アプリがボディを最後まで読み取ることも閉じることもしないレスポンスは、保留状態のままになります。モックはインターセプター自体で適用されるため、インターセプターを通過するリクエストにのみ応答し、addNetworkInterceptorで追加された場合は適用されません。
設定
| フラグ | 環境変数 | 既定値 | 用途 |
|---|---|---|---|
--port <port> | LIONSCOPE_PORT | 8701 | ダッシュボードの HTTP + WebSocket ポート |
--device-port <port> | LIONSCOPE_DEVICE_PORT | 8700 | SDK ゲートウェイ(TCP、NDJSON)。adb reverse 経由で接続 |
--adb <path> | LIONSCOPE_ADB | 自動 | adb バイナリへのパス |
--allow-device-control | オフ | AI エージェントによるスクリーンショットの撮影、タップ、入力、アプリの起動、エミュレーターの起動/シャットダウン、およびネットワークモックの設定を許可します | |
--allow-network-bodies | オフ | AI エージェントが get_network_request で HTTP リクエストとレスポンスのボディを取得できるようにします | |
LIONSCOPE_DASHBOARD_DIR | 自動 | 配信するビルド済みダッシュボード |
フラグは環境変数より優先されます。LIONSCOPE_DASHBOARD_DIR が設定されていない場合、サーバーは npm パッケージに同梱されたダッシュボード、またはクローン内の dashboard/dist を配信します。127.0.0.1 以外でリッスンするオプションはありません。
開発
npm install
npm run dev # server (tsx watch, --dev allows the Vite origin) + Vite on http://localhost:5173
npm test # all unit tests (protocol, server, dashboard)
npm run test:coverage
npm run typecheck
npm run lint
npm run format:check # or `npm run format` to fix
npm run build # production dashboard build
npm run build:cli # the npm package in cli/ (dashboard + bundled server); `cd cli && npm pack` to try it
Android SDK(JDK 17 と Android SDK が必要です):
cd android-sdk
./gradlew :lionscope:test :lionscope-noop:assemble :sample:assembleDebug
./gradlew :lionscope:publishToMavenLocal :lionscope-noop:publishToMavenLocal # same coordinates as JitPack
公開バージョンは lionscopeVersion Gradle プロパティ(既定値は 0.9.0)に基づきます。JitPack では git タグが使用されます。貢献方法については、CONTRIBUTING.md をご覧ください。
ライセンス
Apache License, Version 2.0 の下でライセンスされています。NOTICE もご覧ください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。