ProtoLog

Android 記錄系統旨在提供通用的無障礙和易用性,假設所有記錄資料都能以字元序列表示。這項假設與大多數用途相符,尤其是在沒有專用工具的情況下,記錄檔的可讀性就顯得格外重要。不過,在需要高記錄效能和限制記錄大小的環境中,以文字為基礎的記錄功能可能不是最佳選擇。其中一個情境是 WindowManager,因為它需要可處理即時視窗轉換記錄的強大記錄系統,且對系統的影響要降到最低。

ProtoLog 是解決 WindowManager 和類似服務記錄需求的替代方案。ProtoLog 相較於 logcat 的主要優點如下:

  • 用於記錄的資源較少。
  • 從開發人員的角度來看,這與使用預設的 Android 記錄架構相同。
  • 支援在執行階段啟用或停用記錄陳述式。
  • 如有需要,仍可記錄至 Logcat。

為最佳化記憶體用量,ProtoLog 採用字串內部化機制,用於計算及儲存訊息的編譯雜湊。為提升效能,ProtoLog 會在編譯期間 (針對系統服務) 執行字串內部化,只在執行階段記錄訊息 ID 和引數。此外,在產生 ProtoLog 追蹤記錄或取得錯誤報告時,ProtoLog 會自動整合在編譯時建立的訊息字典,讓您從任何版本中解碼訊息。

使用 ProtoLog 時,訊息會以二進位格式 (proto) 儲存在 Perfeto 追蹤記錄中。訊息解碼作業會在 Perfetto 的 trace_processor 中進行。這個程序包括解碼二進位元 Proto 訊息、使用內嵌訊息字典將訊息 ID 轉譯為字串,以及使用動態引數設定字串格式。

ProtoLog 支援與 android.utils.Log 相同的記錄層級,也就是:dviwewtf

用戶端 ProtoLog

最初,ProtoLog 僅適用於 WindowManager 的伺服器端,在單一程序和元件中運作。後來,我們將其擴充至涵蓋系統 UI 處理程序中的 WindowManager 殼層程式碼,但 ProtoLog 的使用方式需要複雜的重複不變設定程式碼。此外,Proto 記錄功能僅限於系統伺服器和系統 UI 程序,因此要將其整合至其他程序相當費力,而且需要為每個程序設定個別的記憶體緩衝區。不過,ProtoLog 現在已可用於用戶端程式碼,因此不需要額外的標準程式碼。

與系統服務程式碼不同,用戶端程式碼通常會略過編譯時間字串內部化。而是在背景執行緒中動態執行字串內部化。因此,雖然用於用戶端的 ProtoLog 可提供與系統服務上的 ProtoLog 相近的記憶體用量優勢,但其效能開銷會略高,且缺乏用於伺服器端的 ProtoLog 所提供的固定記憶體縮減優勢。

ProtoLog 群組

ProtoLog 訊息會分組為 ProtoLogGroups,類似 TAGLogcat 訊息分組的方式。這些 ProtoLogGroups 是訊息的叢集,可在執行階段一併啟用或停用。此外,它們還會控制是否應在編譯期間移除訊息,以及應記錄訊息的位置 (proto、logcat 或兩者皆是)。每個 ProtoLogGroup 都包含下列屬性:

  • enabled:如果設為 false,系統會在編譯期間排除這個群組中的訊息,且無法在執行階段使用。
  • logToProto:定義這個群組是否以二進位格式記錄。
  • logToLogcat:定義這個群組是否記錄至 Logcat。
  • tag:記錄訊息的來源名稱。

每個使用 ProtoLog 的程序都必須設定 ProtoLogGroup 例項。

支援的引數類型

在內部,ProtoLog 會使用 android.text.TextUtils#formatSimple(String, Object...) 格式化字串,因此語法相同。

ProtoLog 支援下列引數類型:

  • %b - 布林值
  • %d%x - 整數型別 (short、integer 或 long)
  • %f - 浮點類型 (float 或 double)
  • %s - 字串
  • %% - 常值百分比字元

系統支援寬度和精確度修飾符 (例如 %04d%10b),但不支援 argument_indexflags

在新服務中使用 ProtoLog

如要在新程序中使用 ProtoLog:

  1. 為這項服務建立 ProtoLogGroup 定義。

  2. 在首次使用前初始化定義 (例如在建立程序時):

    Protolog.init(ProtologGroup.values());

  3. 使用 Protolog 的方式與 android.util.Log 相同:

    ProtoLog.v(WM_SHELL_STARTING_WINDOW, "create taskSnapshot surface for task: %d", taskId);

啟用編譯時最佳化

如要在程序中啟用編譯時間 ProtoLog,您必須變更其建構規則並叫用 protologtool 二進位檔。

ProtoLogTool 是執行字串內部化並更新 ProtoLog 叫用作業的程式碼轉換二進位檔。這個二進位檔會轉換每個 ProtoLog 記錄呼叫,如以下範例所示:

ProtoLog.x(ProtoLogGroup.GROUP_NAME, "Format string %d %s", value1, value2);

轉換為:

if (ProtoLogImpl.isEnabled(GROUP_NAME)) {
    int protoLogParam0 = value1;
    String protoLogParam1 = String.valueOf(value2);
    ProtoLogImpl.x(ProtoLogGroup.GROUP_NAME, 1234560b0100, protoLogParam0, protoLogParam1);
}

在這個範例中,ProtoLogProtoLogImplProtoLogGroup 是做為引數提供的類別 (可匯入、靜態匯入或完整路徑,不允許萬用字元匯入),而 x 則是記錄方法。

轉換作業是在來源層級完成。系統會根據格式字串、記錄層級和記錄群組名稱產生雜湊碼,並插入 ProtoLogGroup 引數之後。實際產生的程式碼會內嵌,並新增多個行字元,以便保留檔案中的行號。

例子:

genrule {
    name: "wm_shell_protolog_src",
    srcs: [
        ":protolog-impl", // protolog lib
        ":wm_shell_protolog-groups", // protolog groups declaration
        ":wm_shell-sources", // source code
    ],
    tools: ["protologtool"],
    cmd: "$(location protologtool) transform-protolog-calls " +
        "--protolog-class com.android.internal.protolog.ProtoLog " +
        "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
        "--loggroups-jar $(location :wm_shell_protolog-groups) " +
        "--viewer-config-file-path /system_ext/etc/wmshell.protolog.pb " +
        "--legacy-viewer-config-file-path /system_ext/etc/wmshell.protolog.json.gz " +
        "--legacy-output-file-path /data/misc/wmtrace/shell_log.winscope " +
        "--output-srcjar $(out) " +
        "$(locations :wm_shell-sources)",
    out: ["wm_shell_protolog.srcjar"],
}

指令列選項

ProtoLog 的主要優點之一,就是您可以在執行階段啟用或停用它。舉例來說,您可以在建構中加入更多詳細的記錄資訊 (預設為停用),並在本機開發期間啟用這些資訊,以便對特定問題進行偵錯。例如,在 WindowManager 中使用此模式時,可透過群組 WM_DEBUG_WINDOW_TRANSITIONSWM_DEBUG_WINDOW_TRANSITIONS_MIN 啟用不同類型的轉換記錄,其中前者會在預設情況下啟用。

您可以在啟動追蹤時,使用 Perfetto 設定 ProtoLog。您也可以使用 adb 指令列在本機設定 ProtoLog。

adb shell cmd protolog_configuration 指令支援下列引數:

help
  Print this help text.

groups (list | status)
  list - lists all ProtoLog groups registered with ProtoLog service"
  status <group> - print the status of a ProtoLog group"

logcat (enable | disable) <group>"
  enable or disable ProtoLog to logcat

有效使用訣竅

ProtoLog 會為傳遞的訊息和任何字串引數使用字串內部化。也就是說,為了讓 ProtoLog 發揮更大效益,訊息應將重複值分隔為變數。

例如,請看以下敘述:

Protolog.v(MY_GROUP, "%s", "The argument value is " + argument);

在編譯期間進行最佳化後,會轉換為以下內容:

ProtologImpl.v(MY_GROUP, 0x123, "The argument value is " + argument);

如果在程式碼中使用 ProtoLog,並搭配引數 A,B,C

Protolog.v(MY_GROUP, "%s", "The argument value is A");
Protolog.v(MY_GROUP, "%s", "The argument value is B");
Protolog.v(MY_GROUP, "%s", "The argument value is C");
Protolog.v(MY_GROUP, "%s", "The argument value is A");

這會導致記憶體中出現以下訊息:

Dict:
  0x123: "%s"
  0x111: "The argument value is A"
  0x222: "The argument value is B"
  0x333: "The argument value is C"

Message1 (Hash: 0x123, Arg1: 0x111)
Message2 (Hash: 0x123, Arg2: 0x222)
Message3 (Hash: 0x123, Arg3: 0x333)
Message4 (Hash: 0x123, Arg1: 0x111)

如果 ProtoLog 陳述式寫成以下形式:

Protolog.v(MY_GROUP, "The argument value is %s", argument);

記憶體緩衝區最終會變成:

Dict:
  0x123: "The argument value is %s" (24 b)
  0x111: "A" (1 b)
  0x222: "B" (1 b)
  0x333: "C" (1 b)

Message1 (Hash: 0x123, Arg1: 0x111)
Message2 (Hash: 0x123, Arg2: 0x222)
Message3 (Hash: 0x123, Arg3: 0x333)
Message4 (Hash: 0x123, Arg1: 0x111)

這個序列可讓記憶體用量減少 35%。

Winscope 檢視器

Winscope 的 ProtoLog 檢視器分頁會以表格格式顯示 ProtoLog 追蹤記錄。您可以依記錄層級、標記、來源檔案 (ProtoLog 陳述式所在位置) 和訊息內容篩選追蹤記錄。所有資料欄皆可篩選。點選第一欄中的時間戳記,即可將時間軸轉移至訊息時間戳記。此外,按一下「Go to Current Time」,ProtoLog 表格會捲動至時間軸中選取的時間戳記:

ProtoLog 檢視器

圖 1. ProtoLog 檢視器