Protolog

System logowania Androida został zaprojektowany z myślą o powszechnej dostępności i łatwości obsługi. Zakłada się, że wszystkie dane logu można przedstawić jako ciąg znaków. To założenie jest zgodne z większością przypadków użycia, zwłaszcza gdy czytelność logów jest kluczowa bez użycia specjalistycznych narzędzi. W środowiskach wymagających wysokiej wydajności rejestrowania i ograniczonej wielkości logów rejestrowanie oparte na tekście może nie być optymalne. Jednym z takich scenariuszy jest WindowManager, który wymaga solidnego systemu rejestrowania, który może obsługiwać dzienniki przejść okien w czasie rzeczywistym przy minimalnym wpływie na system.

ProtoLog to alternatywne rozwiązanie, które zaspokaja potrzeby w zakresie logowania w przypadku WindowManager i podobnych usług. Główne zalety ProtoLog w porównaniu z logcat:

  • Ilość zasobów używanych do rejestrowania jest mniejsza.
  • Z punktu widzenia dewelopera jest to takie samo jak korzystanie z domyślnego frameworka logowania Androida.
  • Umożliwia włączanie i wyłączanie instrukcji logowania w czasie działania programu.
  • W razie potrzeby nadal możesz logować się w logcat.

Aby zoptymalizować wykorzystanie pamięci, ProtoLog używa mechanizmu internizacji ciągów znaków, który polega na obliczaniu i zapisywaniu skompilowanego skrótu wiadomości. Aby zwiększyć wydajność, ProtoLog wykonuje podczas kompilacji (w przypadku usług systemowych) internizację ciągów znaków, rejestrując w czasie działania tylko identyfikator wiadomości i argumenty. Dodatkowo podczas generowania śladu ProtoLog lub uzyskiwania raportu o błędzie ProtoLog automatycznie uwzględnia słownik wiadomości utworzony w czasie kompilacji, co umożliwia dekodowanie wiadomości z dowolnej kompilacji.

W przypadku ProtoLog wiadomość jest przechowywana w formacie binarnym (proto) w śladzie Perfetto. Dekodowanie wiadomości odbywa się w trace_processor Perfetto. Proces ten polega na dekodowaniu binarnych wiadomości proto, tłumaczeniu identyfikatorów wiadomości na ciągi znaków za pomocą wbudowanego słownika wiadomości i formatowaniu ciągu znaków za pomocą argumentów dynamicznych.

ProtoLog obsługuje te same poziomy rejestrowania co android.utils.Log, czyli: d, v, i, w, ewtf.

ProtoLog po stronie klienta

Początkowo ProtoLog był przeznaczony wyłącznie dla serwera WindowManager, działającego w ramach jednego procesu i komponentu. Później rozszerzono go o kod powłoki WindowManager w procesie interfejsu systemu, ale korzystanie z ProtoLog wymagało skomplikowanego kodu konfiguracji. Ponadto rejestrowanie w protokole Proto było ograniczone do procesów serwera systemowego i interfejsu systemowego, co utrudniało włączenie go do innych procesów i wymagało skonfigurowania oddzielnego bufora pamięci dla każdego z nich. Jednak ProtoLog jest teraz dostępny w przypadku kodu po stronie klienta, co eliminuje potrzebę stosowania dodatkowego kodu szablonowego.

W przeciwieństwie do kodu usług systemowych kod po stronie klienta zwykle pomija internizację ciągów znaków w czasie kompilacji. Zamiast tego internizowanie ciągów znaków odbywa się dynamicznie w wątku w tle. W rezultacie, chociaż ProtoLog po stronie klienta zapewnia podobne korzyści w zakresie wykorzystania pamięci jak ProtoLog w usługach systemowych, wiąże się z nieco większym obciążeniem wydajnościowym i nie ma zalety w postaci zmniejszenia zużycia pamięci przypiętej, jak jego odpowiednik po stronie serwera.

Grupy ProtoLog

Wiadomości ProtoLog są uporządkowane w grupy nazywane ProtoLogGroups, podobnie jak wiadomości Logcat są uporządkowane według TAG. Te ProtoLogGroups służą jako klastry wiadomości, które można zbiorczo włączać i wyłączać w czasie działania programu. Dodatkowo kontrolują, czy wiadomości mają być usuwane podczas kompilacji i gdzie mają być rejestrowane (w protokole, logcat lub w obu tych miejscach). Każdy element ProtoLogGroup zawiera te właściwości:

  • enabled: jeśli ta opcja jest ustawiona na false, wiadomości w tej grupie są wykluczane podczas kompilacji i nie są dostępne w czasie działania programu.
  • logToProto: określa, czy ta grupa rejestruje dane w formacie binarnym.
  • logToLogcat: określa, czy ta grupa rejestruje dane w logcat.
  • tag: Nazwa źródła zalogowanej wiadomości.

Każdy proces korzystający z ProtoLog musi mieć skonfigurowaną instancję ProtoLogGroup.

Obsługiwane typy argumentów

Wewnętrznie ciągi formatu ProtoLog używają android.text.TextUtils#formatSimple(String, Object...), więc ich składnia jest taka sama.

ProtoLog obsługuje te typy argumentów:

  • %b - wartość logiczna
  • %d, %x – typ całkowity (short, integer lub long)
  • %f – typ zmiennoprzecinkowy (float lub double).
  • %s – ciąg znaków
  • %% – dosłowny znak procenta

Modyfikatory szerokości i precyzji, takie jak %04d i %10b, są obsługiwane, ale argument_index i flags nie są.

Używanie ProtoLog w nowej usłudze

Aby użyć ProtoLog w nowym procesie:

  1. Utwórz ProtoLogGroupdefinicję tej usługi.

  2. Zainicjuj definicję przed jej pierwszym użyciem (np. podczas tworzenia procesu):

    Protolog.init(ProtologGroup.values());

  3. Używaj Protolog w taki sam sposób jak android.util.Log:

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

Włączanie optymalizacji w czasie kompilacji

Aby włączyć w procesie ProtoLog w czasie kompilacji, musisz zmienić reguły kompilacji i wywołać plik binarny protologtool.

ProtoLogTool to binarny plik przekształcający kod, który wykonuje internizację ciągów znaków i aktualizuje wywołanie ProtoLog. Ten plik binarny przekształca każde wywołanie ProtoLogrejestrowania, jak pokazano w tym przykładzie:

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

do:

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

W tym przykładzie ProtoLog, ProtoLogImplProtoLogGroup to klasy podane jako argumenty (można je importować, importować statycznie lub używać pełnej ścieżki, importowanie z użyciem symbolu wieloznacznego jest niedozwolone), a x to metoda rejestrowania.

Przekształcenie jest wykonywane na poziomie źródła. Skrót jest generowany na podstawie ciągu formatu, poziomu logu i nazwy grupy logów, a następnie wstawiany po argumencie ProtoLogGroup. Rzeczywisty wygenerowany kod jest wstawiany w tekście, a liczba nowych znaków końca wiersza jest dodawana w celu zachowania numeracji wierszy w pliku.

Przykład:

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"],
}

Opcje wiersza poleceń

Jedną z głównych zalet ProtoLog jest możliwość włączania i wyłączania tej funkcji w czasie działania programu. Możesz na przykład w kompilacji włączyć bardziej szczegółowe logowanie, które jest domyślnie wyłączone, a następnie włączyć je podczas lokalnego tworzenia, aby debugować konkretny problem. Ten wzorzec jest używany np. w klasie WindowManager z grupami WM_DEBUG_WINDOW_TRANSITIONSWM_DEBUG_WINDOW_TRANSITIONS_MIN, które umożliwiają rejestrowanie różnych typów przejść. Pierwsza z nich jest domyślnie włączona.

Podczas rozpoczynania śledzenia możesz skonfigurować ProtoLog za pomocą Perfetto. Możesz też skonfigurować ProtoLog lokalnie za pomocą wiersza poleceń adb.

Polecenie adb shell cmd protolog_configuration obsługuje te argumenty:

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

Wskazówki dotyczące skutecznego korzystania

ProtoLog używa internizacji ciągów znaków zarówno w przypadku wiadomości, jak i przekazywanych argumentów w postaci ciągów znaków. Oznacza to, że aby uzyskać większe korzyści z ProtoLog, wiadomości powinny wyodrębniać powtarzające się wartości do zmiennych.

Rozważmy na przykład to stwierdzenie:

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

Po zoptymalizowaniu w czasie kompilacji kod wygląda tak:

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

Jeśli w kodzie użyto ProtoLog z argumentami 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");

W pamięci pojawią się te wiadomości:

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)

Jeśli zamiast tego instrukcja ProtoLog wyglądałaby tak:

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

Bufor w pamięci będzie wyglądać tak:

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)

Ta sekwencja powoduje, że zużycie pamięci jest o 35% mniejsze.

Przeglądarka Winscope

Karta przeglądarki ProtoLog w Winscope wyświetla ślady ProtoLog w formacie tabelarycznym. Możesz filtrować ślady według poziomu logowania, tagu, pliku źródłowego (w którym znajduje się instrukcja ProtoLog) i treści wiadomości. Wszystkie kolumny można filtrować. Kliknięcie sygnatury czasowej w pierwszej kolumnie powoduje przejście osi czasu do sygnatury czasowej wiadomości. Dodatkowo kliknięcie Przejdź do bieżącego czasu powoduje przewinięcie tabeli ProtoLog z powrotem do sygnatury czasowej wybranej na osi czasu:

Przeglądarka ProtoLog

Rysunek 1. Przeglądarka ProtoLog