DUICUO

メジャーアップグレード!btrace 2.0 の技術原理を公開

背景紹介

1年以上前、Systraceをベースとした高性能トレーシングツール「btrace」(別名RheaTrace)を正式にオープンソース化しました。現在、ByteDanceでは10以上のプロダクトチームが日常的なパフォーマンス最適化にbtraceを使用しています。この1年間で、コミュニティと社内から、ユーザーエクスペリエンス、パフォーマンス、モニタリングデータなど、多くのフィードバックをいただきました。そのフィードバックをまとめると、主に以下の3つのカテゴリーに分けられます。

  • ユーザーエクスペリエンス:Windowsは多くのユーザーを抱えていますが、btrace 1.0はWindowsをサポートしていません。デスクトップ版のスクリプトはSystraceとPython 2.7に依存しているため、環境設定が非常に複雑です。さらに、モバイル版は外部ストレージへのアクセス権限に依存しており、初期使用時に中断が発生しやすいです。さらに、出力が大きいため、ウェブページの読み込み速度が遅くなります。
  • パフォーマンス エクスペリエンス: 大規模アプリケーションには数百万のインストルメンテーションがあり、その結果、パフォーマンスがほぼ 100% 低下し、パフォーマンスの最適化が課題となります。
  • 監視データ:トレース分析プロセス中に一部の情報が欠落しており、時間消費の原因が不明です。例えば、現在のトレースには同期ロック情報のみが含まれており、ReentrantLockなどの他のロック情報が欠落しています。また、レンダリングされた監視にはシステムのクリティカルパス情報のみが含まれており、ビジネスレイヤーの情報が欠落しています。

一方、Androidシステムが進化を続けるにつれ、GoogleはSystraceツールを徐々に放棄し、Perfettoツールを積極的に推進し始めました。さらに、SDカードの権限制限が厳格化されたため、btraceはAndroidの上位バージョンで互換性の問題に遭遇しました。

このような背景から、開発者のニーズにより適切に対応するために、最も頻繁に報告され、集中的に発生していた問題に対処するとともに、Googleがリリースした新機能への対応や互換性問題の修正も行い、btraceを大幅に刷新することを決定しました。現在、btrace 2.0では、ユーザーエクスペリエンス、パフォーマンス、モニタリングデータにおいて大幅な改善が実現しており、主な改善点は以下のとおりです。

  • ユーザーエクスペリエンス: Windowsに対応しました!さらに、スクリプト実装をPythonからJavaに変更し、各種権限要件を廃止したことで、スクリプトツールの可用性の問題によるユーザー中断をほぼゼロに削減しました。さらに、トレース出力をPBプロトコルに変更したことで、出力サイズが70%削減され、ウェブページの読み込み速度が7倍向上しました!
  • パフォーマンスエクスペリエンス:メソッドトレースロジックを大幅にリファクタリングすることで、Appメソッドトレースの基盤構造が文字列から整数に変更され、メモリ使用量が80%削減されました。ストレージはmmapに変更され、ロックフリーキューロジックが最適化され、精密なインストルメンテーション戦略が提供されました。完全なインストルメンテーションシナリオでは、パフォーマンスの低下はさらに15%にまで削減されました。
  • モニタリングデータ: 4つの新しいデータモニタリング機能が追加されました。その中には、詳細なレンダリングデータ収集という重要な新機能も含まれています。さらに、バインダー、スレッド起動、待機/通知/パーク/アンパークなどの詳細なデータも追加されました。

次に、btrace 2.0 の重要なアップグレードをより深く理解していただくために、上記の 3 つの改善方向の具体的なメカニズムと実装原則について詳しく説明します。

原則が明らかに

Perfettoの紹介

Perfetto と Systrace はどちらも Android システムのパフォーマンス分析とデバッグのためのツールですが、次のような違いがあります。

Systrace は、Android SDK に含まれるツールで、さまざまなシステムプロセスのタイミングイベントをキャプチャして分析し、システムパフォーマンスのボトルネックを分析するためのグラフィカルインターフェースを提供します。Systrace がキャプチャできるイベントには、CPU、メモリ、ネットワーク、ディスク I/O、レンダリングなどが含まれます。Systrace は、カーネル空間とユーザー空間の両方でタイミングイベントをキャプチャして解析し、HTML ファイルに記録します。開発者はこれらのログを Chrome ブラウザで分析できます。Systrace は開発者がシステムのボトルネックを特定するのに効果的ですが、特に大量のデータを処理する場合には、パフォーマンスが理想的とは言えません。

Perfetto は、Systrace のパフォーマンスを最適化するために設計された、オーバーヘッドの少ない新しいトレース収集ツールです。Perfetto は、Systrace よりも高速で詳細なトレース収集を提供することを目指しており、他のクロスプラットフォームツールとの統合をサポートしています。Perfetto はトレースデータをバイナリ形式で記録し、ProtoBuf ベースのデータ交換形式でエクスポートするため、Grafana、SQLite、BigQuery などの他の分析および可視化ツールとの統合が可能です。Perfetto は、CPU 使用率、ネットワーク バイトストリーム、タッチ入力、レンダリングなど、幅広い種類のデータを収集します。Systrace と比較して、Perfetto は優れたパフォーマンスとカスタマイズ性を提供します。

したがって、Perfetto は Systrace よりも高度で優れた代替手段であり、より強力なデータ収集および分析機能、優れたパフォーマンス、優れたカスタマイズ性を備え、開発者により包括的で詳細なパフォーマンス分析およびデバッグ ツールを提供することがわかります。

全体的なプロセス

まず、btrace データ収集の全体的なプロセスを理解しましょう。

全体のプロセスは次の 3 つの段階に分かれています。

アプリのコンパイル中:アプリケーションのコンパイルフェーズでは、メソッド数値インストルメンテーションとメソッド文字列インストルメンテーションの2つのインストルメンテーションモードを提供しています。メソッド数値インストルメンテーションはメソッド名のみを記録する必要がある場合に適しており、メソッド文字列インストルメンテーションはメソッドパラメータの値を同時に記録できます。さらに、疑わしい時間のかかるコードを自動的に識別してインストルメントする、高精度なインストルメンテーションエンジンをサポートしています。

アプリケーション実行時:アプリケーション実行時の主なタスクは、apptrace情報の収集です。数値型の情報はmmapロックレスキューを介して収集され、文字列型の情報はシステム関数を介してatraceに直接書き込まれます。同時に、atraceへの書き込みロジックはプロキシ化され、LFRBの高性能書き込みソリューションに置き換えられます。

デスクトップスクリプト:デスクトップスクリプトは、主にアプリケーションの動作を制御し、トレース収集機能の有効化/無効化を行うために使用されます。さらに、収集された apptrace および atrace データをエンコードし、ftrace とマージする役割も担います。

技術の公開

1. ユーザーエクスペリエンス

ユーザーエクスペリエンスに関する問題は、ユーザーからのフィードバックで最も多く寄せられました。分析の結果、これらの問題は主にストレージ権限、Systrace環境、Python環境、トレースアーティファクトが大きすぎること、そしてPerffettoウェブページの読み込み速度が遅いことに起因していることが判明しました。これらの問題に対して、以下の最適化を実施しました。

権限の最適化

データ処理のために、デスクトップスクリプトはアプリデータにアクセスする必要があります。アプリレベルでは、データをパブリックSDカードに保存するのが最も便利な方法です。しかし、Android Q以降、Googleは外部ストレージへのフルアクセスを制限しました。requestLegacyExternalStorageはこの問題を一時的に解決できますが、長期的にはSDカードへのアクセスが完全には利用できなくなります。

この問題を解決するために、ポート経由で外部データにアクセスするためのHTTPサーバーを構築しました。しかし、このサーバーにアクセスするには、依然としてサービスアドレスを特定する必要があります。そこで、adbの転送機能を使用します。この機能は、PCからモバイルポートにデータを転送し、モバイルポートから返されたデータを取得する転送メカニズムを確立します。これにより、localhostを使用してデータにアクセスできるようになります。

上記により、スクリプトによるアプリデータの読み取り問題は解決しました。しかし、maxAppTraceBufferSizeやmainThreadOnlyといったスクリプトパラメータをアプリが読み取るという問題は依然として残っています。btrace 1.0では、設定ファイルを指定のディレクトリにプッシュすることで、実行時に動的な調整を行うことができます。ただし、これにはSDカードへのアクセス権が必要です。アクセス権への依存を完全に排除するには、新しい解決策を導入する必要があります。

最初に思いつく解決策は、`adb forward` を使った逆アプローチ、つまり `adb reverse` です。これにより、モバイルポートからPCにデータを転送し、モバイル端末からPCへのアクセスが可能になります。同様に、スクリプト内でHttpServerを起動してデータを受信することもできます。ただし、これはネットワークリクエストであるため、アプリはバックグラウンドスレッドでしかパラメータを読み取ることができません。これは、特にパラメータをリアルタイムで有効にする必要がある場合に不便です。

新しい解決策を検討しました。デスクトップスクリプトで「adb setprop」を使用してスマートフォンのパラメータを設定し、アプリが「__system_property_get」を使用してパラメータを読み取ります。パラメータプロパティ名が「debug.」で始まっている限り、権限は必要ありません。

 // 桌面脚本设置参数Adb.call("shell", "setprop", "debug.rhea.startWhenAppLaunch", "1"); // 手机运行时读取参数static jboolean JNI_startWhenAppLaunch(JNIEnv *env, jobject thiz) { char value[PROP_VALUE_MAX]; __system_property_get("debug.rhea.startWhenAppLaunch", value); return value[0] == '1'; }
環境最適化

btrace 1.0はSystraceをベースとしており、Python 2.7に大きく依存しています。しかし、Python 2.7は公式に非推奨となっており、Androidエンジニアの多くはPythonに精通していないため、環境の問題解決に多大な時間を浪費しています。そこで、SystraceをPerfettoに切り替え、スクリプトをAndroidエンジニアにとってより馴染みのあるJava言語で書き直す予定です。ユーザーはJavaとadb環境さえあれば、btrace 2.0を簡単に利用できるようになります。

製品の最適化

btrace 1.0 は Systrace に基づいて HTML テキストデータを出力しますが、テキストコンテンツが大きい、読み込み速度が遅い、さらにはトレースの表示をサポートするために別のサービスが必要になるなどの問題が発生することがよくあります。Google の新しいパフォーマンス分析プラットフォームである Perfetto は、Systrace を含む複数のデータ形式の解析をサポートしています。Perfetto は、構造化データの保存と転送に使用される軽量で効率的なデータシリアル化形式である Protocol Buffers (pb) もサポートしています。Perfetto はイベントログ形式として pb を使用して、システムイベントデータを効率的かつスケーラブルに記録します。pb の構造化データストレージにより、ファイルサイズが小さくなり、解析速度が速くなります。そのため、btrace 2.0 ではデータ形式を HTML から pb に変更し、出力ファイルサイズを縮小して、Web ページでのトレースの読み込み速度を大幅に向上させました。

まず、Perfetto の pb データ形式について簡単に紹介し、次に収集した apptrace および atrace データを pb 形式にエンコードする方法と、それをシステムの ftrace と統合する方法について説明します。

Perfetto pbは一連のTracePacketで構成されています。公式ドキュメントはhttps://perfetto.dev/docs/reference/trace-packet-protoでご覧いただけます。このセクションでは、btraceで使用されるTracePacketの一種であるFtraceEventBundleについて説明します。

FtraceEventBundleは、Androidのシステムトレースデータを収集するためのメカニズムです。多数のFtraceEventsで構成されており、スケジューリング、割り込み、メモリ管理、ファイルシステムなど、様々なシステム動作を記録するために使用できます。btraceは主にPrintFtraceEventを利用してメソッドトレース情報を記録します。具体的な使用方法については、以下に簡単な例を示します。

 int threadId = 10011; FtraceEventBundle.Builder bundle = FtraceEventBundle.newBuilder() .addEvent( FtraceEvent.newBuilder() .setPid(threadId) // 线程内核pid,就是tid .setTimestamp(System.nanoTime()) .setPrint( Ftrace.PrintFtraceEvent.newBuilder() // buf 格式是B|$pid|$msg\n 这里pid 是实际// 进程ID,`\n` 是必须项.setBuf("B|10010|someEvent\n"))) .addEvent( FtraceEvent.newBuilder() .setPid(threadId) .setTimestamp(System.nanoTime() + TimeUnit.SECONDS.toNanos(2)) .setPrint( Ftrace.PrintFtraceEvent.newBuilder() .setBuf("E|10010|\n"))) .setCpu(0); Trace trace = Trace.newBuilder() .addPacket( TracePacketOuterClass.TracePacket.newBuilder() .setFtraceEvents(bundle)).build(); try (FileOutputStream out = new FileOutputStream("demo.pb")) { trace.writeTo(out); }

上記の例では、次のトレースが生成されます。

次に、実行時に収集されたapptrace情報をpb形式に変換する方法を紹介します。この操作はデスクトップスクリプトで実行されます。

まず、スクリプトは adb http 経由で携帯電話の mmap マッピング ファイルを取得し、ファイルの内容を解析します。

 // 读取mapping,我们将mapping 内置到了apk 的assets 目录Map<Integer, String> mapping = Mapping.get(); // 开始解码并保存解码后的结果List<Frame> result = new ArrayList<>(); byte[] bytes = FileUtils.readFileToByteArray(traceFile); ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); while (buffer.hasRemaining()) { long a = buffer.getLong(); long b = buffer.getLong(); // 分别解析出startTime / duration / tid / methodId long startTime = a >>> 19; long dur0 = a & 0x7FFFF; long dur1 = (b >>> 38) & 0x3FFFFFF; long dur = (dur0 << 26) + dur1; int tid = (int) ((b >>> 23) & 0x7FFF); int mid = (int) (b & 0x7FFFFF); // 记录相应的开始与结束Trace result.add(new Frame(Frame.B, startTime, dur, pid, tid, mid, mapping)); result.add(new Frame(Frame.E, startTime, dur, pid, tid, mid, mapping)); } // 排序result.sort(Comparator.comparingLong(frame -> frame.time));

その後、リストは上記で紹介したFtraceEventBundleを使用してエンコードできますが、ここでは詳しく説明しません。atraceの処理方法も同様であるため、これ以上の説明は省略します。

次に、収集した apptrace、atrace、および system ftrace をマージする方法を説明します。

前述の通り、Perfetto pb は一連の TracePacket で構成されています。一般的に言えば、ビジネスで収集された Trace を TracePacket にカプセル化し、それをシステムの TracePacket コレクションに追加するだけで、Trace のマージが完了します。

 Trace.Builder systemTrace = Trace.parseFrom(systraceStream).toBuilder(); FtraceEventBundle.Builder bundle = ...; for (int i = 0; i < events.size(); i++) { bundle.addEvent(events.get(i).toEvent()); } systemTrace.addPacket(TracePacket.newBuilder().setFtraceEvents(bundle).build());

しかし、これはアプリケーション層で収集されるapptrace/atraceのタイムスタンプがシステムトレースのタイムスタンプと一致しているという前提に基づいています。実際には、実際のテスト結果に基づくと、デバイスによってBOOTTIMEやMONOTONIC TIMEなど、異なるトレースタイムスタンプが使用される場合があります。つまり、どのタイムスタンプを使用しても、アプリケーション層は限られた数のデバイスとしか互換性がありません。この問題を解決するために、トレース情報の記録を開始する際、まずBOOTTIMEとMONOTONIC TIMEの初期セットを記録します。その後、タイムスタンプを記録する際には、一貫してMONOTONIC時間を使用します。

最後に、スクリプトはftraceのタイムスタンプを解析して判定を行います。MONOTONICに近い場合はMONOTONICを使用し、BOOTTIMEに近い場合はBOOTTIMEを使用します。各関数のBOOTTIMEを個別に記録しているわけではありませんが、MONOTONICと初期時間の差を差し引くことでBOOTTIMEを計算できます。

 if (Math.abs(systemFtraceTime - monotonicTime) < Math.abs(systemFtraceTime - bootTime)) { Log.d("System is monotonic time."); } else { long diff = bootTime - monotonicTime; Log.d("System is BootTime. time diff is " + diff); for (Event e: events) { e.time += diff; } }

2. パフォーマンス経験

ランタイム最適化

btrace 1.0は、インストルメンテーションにSystraceモードを厳密に依存し、メソッドの先頭と末尾に「Trace.beginSection」と「endSection」を挿入します。しかし、「beginSection」パラメータは文字列であり、数百万のメソッドをインストルメントすると、数百万の文字列が数百万の余分なメモリを占有することになり、メモリに大きな負担がかかるだけでなく、データI/Oの永続性にも大きな負担がかかります。さらに、文字列データのサイズは固定されておらず、ロックやLFRB方式でしかデータを記録できないため、効率的な同時データ書き込みを実現できません。データはバッファにキャッシュするしかありませんが、バッファが小さすぎるとデータが失われやすく、大きすぎるとメモリの無駄が発生します。

btrace バージョン 2.0 は、メソッド ID を数値化することで、メソッド実行情報を mmap マッピングファイルに記録します。メソッド ID のサイズは固定されているため、アトミック操作を使用してストレージデータの位置を計算でき、ロックフリーの同時書き込みを実現します。同時に、メソッドの数値ストレージはメモリ使用量を削減し、IO の永続化に伴う負荷も軽減されます。

一方、`Trace.beginSection`メソッドと`endSection`メソッドは、各メソッドの開始時刻と終了時刻、スレッドID、メソッドIDを記録する必要があることがわかりました。スレッドIDとメソッドIDは繰り返し記録されるため、メモリが無駄に消費されていました。そこで、開始時刻、メソッド実行時刻、スレッドID、メソッドIDの情報を1つのレコードに同時に記録することで最適化し、合計2バイトのロングバイトを占有することで、メモリを最大限に活用しました。

具体的なインストルメンテーション ロジックについては、次の擬似コードを参照してください。

 // 业务代码public void appLogic() { long begin = nativeTraceBegin(); // 业务逻辑nativeTraceEnd(begin, 10010); } // 插桩逻辑long nativeTraceBegin() { return nanoTime(); } void nativeTraceEnd(long begin, int mid) { long dur = nanoTime() - begin; int tid = gettid(); write(begin, dur, tid, mid); }

メソッド時間消費データ記録形式の例:

メソッドのインストルメンテーションはJavaレイヤーで実行されますが、データ取得はmmapを用いてネイティブレイヤーで実装されています。そのため、JNI呼び出しが頻繁に発生します。非JNIメソッドが通常のJNIメソッドを呼び出す場合、または通常のJNIメソッドから戻る場合、スレッド状態の切り替えが必要になります。スレッド状態の切り替えにはGCロック操作が伴うため、パフォーマンスに大きなオーバーヘッドが発生します。

Android システムに詳しい方なら、高頻度の JNI 呼び出しに特化したパフォーマンス最適化がシステムで行われていることをご存知かもしれません。これは、@CriticalNative および @FastNative アノテーションによって実現されています。@FastNative はネイティブメソッドのパフォーマンスを最大 2 倍向上させ、@CriticalNative は最大 4 倍向上させます。

システムのアプローチに従い、メソッド呼び出しを高速化するためにメソッドに `@CriticalNative` アノテーションを追加しました。ただし、 `@CriticalNative` アノテーションは API を隠蔽するため、直接使用することはできません。同じ結果を得るには、 `@CriticalNative` アノテーションを定義した JAR ファイルを作成し、 `compileOnly` 依存関係を使用してプロジェクトに含めます。関連するアノテーション定義はソースコードから参照されます。

 // ref: https://cs.android.com/android/platform/superproject/+/master:libcore/dalvik/src/main/java/dalvik/annotation/optimization/CriticalNative.java;l=26?q=criticalnative&sq= @Retention(RetentionPolicy.CLASS) // Save memory, don't instantiate as an object at runtime. @Target(ElementType.METHOD) public @interface CriticalNative {}

具体的な使用規則については、以下のコードを参照してください。

 // Java 方法定义,必须是static,不能用synchronized,参数类型必须是基本类型@CriticalNative public static long nativeTraceBegin(); // Critical JNI 方法,不再需要声明JNIEnv 与jclass 参数static jlong Binary_nativeTraceBegin() { ... } // 动态绑定JNINativeMethod t = {"nativeTraceBegin", "(I)J", (void *) JNI_CriticalTraceBegin}; env->RegisterNatives(clazz, &t, 1);

@CriticalNative/@FastNativeはAndroid 8.0以降でサポートされている機能です。8.0より前のバージョンのデバイスでは、メソッドシグネチャに感嘆符!を追加することでFastNativeを有効にすることもできます。

 // Fast JNI 方法,和普通JNI 方法一样需要JNIEnv 参数与jclass 参数static jlong Binary_nativeTraceBegin(JNIEnv *, jclass) { ... } // 动态绑定JNINativeMethod t = {"nativeTraceBegin", "!(I)J", (void *) Binary_nativeTraceBegin}; env->RegisterNatives(clazz, &t, 1);

ID デジタル取得を最適化する上記の方法は、特定のデータ デコードおよびマッピング ソリューションを紹介した前の製品最適化セクションですでに説明されているため、ここでは繰り返しません。

メソッドIDの取得はパフォーマンスとメモリ使用量の面でメリットをもたらしますが、限界もあります。コンパイル時にIDにマッピングされたコンテンツのみを記録でき、実行時に動的に生成されたコンテンツを記録することはできません。そのため、メソッドIDの保存に加えて、主に細粒度のbtraceモニタリングデータとメソッドパラメータ値を記録するための文字列データの保存もサポートしています。このアプローチはbtrace 1.0のLFRBアプローチに類似しており、ここでは詳しく説明しません。トレースデータの大部分はメソッドIDで構成されているため、mmapによってLFRBの負荷が軽減され、btrace 1.0と比較してLFRBの負荷が大幅に軽減され、バッファサイズを適切に削減できるようになりました。

精密杭打ち

パフォーマンス最適化のもう一つの要素はインストルメンテーションです。アプリケーション内のメソッド数が増えると、インストルメンテーションを必要とするメソッド数も増え、時間の経過とともにインストルメンテーションによるパフォーマンスの低下が顕著になります。btrace 1.0はtraceFilterFilePath設定を提供し、ユーザーはインストルメントするメソッドとしないメソッドを選択できます。これにより、パフォーマンスとインストルメンテーションのバランス調整の負担をユーザーに移しながら、柔軟な設定が可能になります。

バージョン 2.0 では、ユーザーが重視する時間のかかるメソッドを正確に識別し、時間のかからないメソッドをインストルメンテーション ルールから正確に除外できるインテリジェント ルール セットを確立して、インテリジェントで正確なインストルメンテーション エクスペリエンスを実現することを目指しています。

Android アプリのソースコードは最終的にバイトコードにコンパイルされます。Android 仮想マシンは 200 を超えるバイトコード命令をサポートしていますが、パフォーマンスのボトルネックとなる可能性のある命令は通常、I/O 読み取り、同期バイトコード、リフレクション、Gson パース関数呼び出しなど、少数かつ簡単に列挙できるものです。コンパイル時には、関連する命令を呼び出すメソッドを潜在的に時間のかかるメソッドとして扱います。残りの時間を消費しない関数は、パフォーマンスの問題を引き起こさないためインストルメンテーションされません。これにより、インストルメンテーションの範囲が大幅に狭まります。

前述の時間を要する特性を踏まえ、私たちは洗練されたステークドライブ方式を設計しました。これにより、ユーザーはそれぞれの状況に応じて適切なステークドライブ方式を選択できます。サポートされている設定方式は以下の通りです。

 # 对锁相关的方法插桩-tracesynchronize # 对Native方法的调用点插桩-tracenative # 对Aidl方法插桩-traceaidl # 对包含循环的方法插桩-traceloop # 关闭默认耗时方法的调用插桩-disabledefaultpreciseinject # 开启大方法插桩,方法调用数超过40 -tracelargemethod 40 # 该方法的调用方需要进行插桩-traceclassmethods rhea.sample.android.app.PreciseInjectTest { test } # 被该注解修饰的方法需要被插桩-tracemethodannotation org.greenrobot.eventbus.Subscribe # 该Class的所有方法均会被插桩-traceclass io.reactivex.internal.observers.LambdaObserver # 该方法的参数信息会在Trace中保留-allowclassmethodswithparametervalues rhea.sample.android.app.RheaApplication { printApplicationName(*java.lang.String); }

計測を改良した結果、Douyin の計測数は 94% 削減され、比較的完全なトレース データが保持されながら、パフォーマンスが大幅に向上しました。

要約すると、時間のかかる関数をインストルメントすることの利点と欠点を比較検討することで、過剰なインストルメントによる不要なパフォーマンスの低下を回避しながら、時間のかかる関数に関する情報を可能な限り多く取得することができます。

3. 監視データ

データの監視はTraceの中核であり、Traceがユーザーに実用的な価値をもたらすかどうかに関わっています。従来のTrace実行方法に加えて、バージョン2.0ではレンダリング監視、バインダー監視、ブロッキング監視、スレッド作成監視という4つの主要機能が追加されました。以下では、関連する背景と実装原則を紹介します。

レンダリング監視

AndroidシステムはRenderThreadの重要な実行ロジックのトレースポイントを提供していますが、提供される情報だけでは、レンダリングの問題に影響を与える特定のビジネスコードを直感的に分析するには不十分です。次の画像は、atraceでレンダリングスレッドをトレースした例です。

そのため、記録とレンダリングのためのキービューノードを追加することで、この情報の表示を改良・拡張しました。次の画像は最適化された結果を示しています。

レンダリング監視の基本原理は次の図に示されています。

  • LayoutInflater プロキシは、View がインフレートされたときにそのレイアウト情報を取得し、View の RenderNode とネイティブ レイヤーの RenderNode の関係によって、View のレイアウト情報を RenderNode の名前フィールドにバインドします。
  • SyncFrameState フェーズのRenderNode::prepareTreeImplメソッドや RenderPipeline フェーズのRenderNodeDrawable::forceDrawメソッドなど、レンダリング フェーズの主要ノードをフックし、RenderNode が属する View のレイアウト情報を Trace に記録します。
バインダーモニタリング

BinderはAndroidにおけるプロセス間通信の非常に重要な手段です。しかし、パフォーマンス分析を行うと、Binderプロセスが時間を消費していることが判明することがあります。Androidシステムのatraceは、Binderの時間を消費する監視情報を提供しますが、それがどのような種類のBinder呼び出しであるかは提供しません(下図を参照)。

btrace の Binder 拡張機能は、Binder によって呼び出されるインターフェースとメソッド名を解析して表示することを目的としており、次の効果を実現します。

基本的な原則は、plt を使用してIPCThreadState::transactフックし、バインダー呼び出しのコードと interfaceName を Parcel& データ パラメータに記録することです。

 status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags);

しかし、Parcel構造は非公開であるため、データからinterfaceName情報を解析することは困難です。そこで、アプローチを変更し、 Parcel::writeInterfaceToken使用してinterfaceNameとParcelの関連付け情報を記録し、 IPCThreadState::transactでクエリを実行してinterfaceNameを取得しました。

 status_t Parcel::writeInterfaceToken(const char* interface) { // 记录this Parcel 与interface 名称的关联} status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { // 查询Parcel data 对应的interface // 记录Trace RHEA_ATRACE("binder transact[%s:%d]", name.c_str(), code); }

これには、interfaceName やコードなど、次の情報が記録されます。

 binder transact[android.content.pm.IPackageManager:5]

さらに、コードを対応するBinder呼び出しメソッドに解析する必要があります。AIDLでは、interfaceName$Stubクラスの静的フィールドに、各コードとそれに対応するBinder呼び出し名が記録されます。トレースキャプチャの最後に、リフレクションを通じてコードと名前のマッピングを取得し、トレースアーティファクトに保存できます。

 #android.os.IHintManager TRANSACTION_createHintSession:1 TRANSACTION_getHintSessionPreferredRate:2 #miui.security.ISecurityManager TRANSACTION_activityResume:27 TRANSACTION_addAccessControlPass:6

最後に、デスクトップ スクリプトを使用してランタイム トレースのコードが処理され、実際のメソッド名に置き換えられます。

ブロック監視

ロック監視はパフォーマンス監視において非常に重要な部分です。Androidシステムのatraceは、同期されたロック競合のトレース情報を提供します。例えば、下の画像は、メインスレッドがロック取得時にスレッド16105とロック競合が発生したことを示しています。これは、スレッドブロッキングを最適化するための重要な情報となります。

ただし、スレッドのブロッキングはロック競合だけが原因ではなく、wait/parkなどの要因によるスレッドの待機も含まれます。例えば、ReentrantLockの基盤実装ではparkとunparkが利用されています。btraceのブロッキング監視は、このブロッキング情報を提供します。以下はwait/notifyの関連付けの例です。ロックオブジェクトの情報を取得することで、現在のスレッドのwaitに対応するnotify呼び出しの位置を特定できます。

wait 関数は park 関数と同様に動作しますが、ここではより一般的な wait/notify の組み合わせを使用して説明します。

wait と notify はどちらも Object によって直接定義されたメソッドであり、本質的には JNI メソッドです。これらの呼び出しは JNI フックを通じて記録できます。

 public final native void wait(long timeoutMillis, int nanos) throws InterruptedException; public final native void notify();

対応するフックメソッドでは、フックの実行がTraceと対応するthis(つまりロックオブジェクト)のidentityHashCodeによって記録されます。このように、identityHashCodeを介してマッピング関係を確立できます。

 static void Object_waitJI(JNIEnv *env, jobject java_this, jlong ms, jint ns) { ATRACE_FORMAT("Object#wait(obj:0x%x, timeout:%d)", Object_identityHashCodeNative(env, nullptr, java_this), ms); Origin_waitJI(env, java_this, ms, ns); } static void Object_notify(JNIEnv *env, jobject java_this) { ATRACE_FORMAT("Object#notify(obj:0x%x)", Object_identityHashCodeNative(env, nullptr, java_this)); Origin_notify(env, java_this); }
スレッド作成の監視

トレースを解析すると、異常なスレッドに遭遇することがあります。このような場合、スレッドが作成された場所を解析する必要があることがよくありますが、従来のトレースではこの情報が不足しています。そこで、btrace はスレッド作成監視データを追加します。その基本原理は、`pthread_create` をプロキシし、スレッドの作成と作成されたスレッドの tid の両方を記録することです。しかし、`pthread_create` が完了した時点では、作成されたスレッド ID は不明です。システムソースコードを解析すると、`p​​thread_t` は基本的に `pthread_internal_t` ポインタであり、`pthread_internal_t` は作成されたスレッド ID を記録することがわかります。

 // https://cs.android.com/android/platform/superproject/+/master:bionic/libc/bionic/pthread_internal.h struct pthread_internal_t { struct pthread_internal_t *next; struct pthread_internal_t *prev; pid_t tid; }; int pthread_create_proxy(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) { BYTEHOOK_STACK_SCOPE(); int ret = BYTEHOOK_CALL_PREV(pthread_create_proxy, thread, attr, start_routine, arg); if (ret == 0) { ATRACE_FORMAT("pthread_create tid=%lu", ((pthread_internal_t *) *thread)->tid); } return ret; }

最終結果は図に示されています。例えば、スレッド16125が新しく作成されたスレッドであることがわかった場合、その作成場所を分析し、スレッドプールの実装に置き換える必要があります。

pthread_create tid=16125を検索するだけで、対応する作成スタックを見つけることができます。

要約と展望

以上介绍了btrace 2.0 的主要优化点,更多优化还需要在日常使用中去体会。2.0 不是终点,是新征程的起点,我们还将围绕下面几点持续优化,将btrace 优化到极致:

使用体验:深入优化使用体验,比如支持不定长时间Trace 采集,优化采集耗时。

性能体验:持续探索性能优化,正面与侧面优化双结合,提供更加极致性能体验。

监控数据:在Java 与ART 虚拟机基础之上,建设包括内存、C/C++、JavaScript 等更多更全的监控能力。

使用场景:提供线上场景接入与使用方案,帮助解决线上疑难问题。

生态建设:围绕btrace 2.0 建设完善生态,通过性能诊断与性能防劣化,自动发现存量与增量性能问题。

最后,欢迎大家深入讨论与交流,一起协作构建极致btrace 工具!

参加しませんか

抖音Android 基础技术团队是一个深度追求极致的团队,我们专注于性能、架构、包大小、稳定性、基础库、编译构建等方向的深耕,保障超大规模团队的研发效率和数亿用户的使用体验。目前北京、上海、深圳都有人才需要,欢迎有志之士与我们共同建设亿级用户全球化APP!

你可以进入字节跳动招聘官网查询「抖音基础技术Android」相关职位,也可以邮件联系:[email protected] 咨询相关信息或者直接发送简历内推!