Protolog

O sistema de registro do Android tem como objetivo a acessibilidade universal e a facilidade de uso, assumindo que todos os dados de registro podem ser representados como uma sequência de caracteres. Essa suposição está alinhada com a maioria dos casos de uso, principalmente quando a legibilidade do registro é crucial sem ferramentas especializadas. No entanto, em ambientes que exigem um alto desempenho de registro e tamanhos de registro restritos, o registro baseado em texto pode não ser o ideal. Um desses cenários é o WindowManager, que exige um sistema de geração de registros robusto capaz de processar registros de transição de janela em tempo real com impacto mínimo no sistema.

O ProtoLog é a alternativa para atender às necessidades de registro do WindowManager e serviços semelhantes. Os principais benefícios do ProtoLog em relação ao logcat são:

  • A quantidade de recursos usados para registro é menor.
  • Do ponto de vista do desenvolvedor, é o mesmo que usar o framework de geração de registros padrão do Android.
  • Suporta a ativação ou desativação de instruções de registro no momento da execução.
  • Ainda é possível fazer o registro no logcat, se necessário.

Para otimizar o uso da memória, o ProtoLog emprega um mecanismo de internação de string que envolve calcular e salvar um hash compilado da mensagem. Para melhorar o desempenho, o ProtoLog realiza a internação de strings durante a compilação (para serviços do sistema), registrando apenas o identificador da mensagem e os argumentos no momento de execução. Além disso, ao gerar um rastro do ProtoLog ou receber um relatório de bug, o ProtoLog incorpora automaticamente o dicionário de mensagens criado no momento da compilação, permitindo a decodificação de mensagens de qualquer build.

Com o ProtoLog, a mensagem é armazenada em um formato binário (proto) em um traço do Perfetto. A decodificação da mensagem acontece dentro do trace_processor do Perfetto. O processo consiste em decodificar as mensagens proto binárias, traduzir os identificadores de mensagens em strings usando o dicionário de mensagens incorporado e formatar a string usando argumentos dinâmicos.

O ProtoLog oferece suporte aos mesmos níveis de registro que android.utils.Log, a saber: d, v, i, w, e e wtf.

ProtoLog do lado do cliente

Inicialmente, o ProtoLog era destinado apenas ao lado do servidor do WindowManager, operando em um único processo e componente. Posteriormente, ele foi expandido para abranger o código de shell do WindowManager no processo da interface do sistema, mas o uso do ProtoLog exigia um código de configuração de modelo complexo. Além disso, o registro de Proto era restringido ao servidor do sistema e aos processos da interface do sistema, o que tornava difícil a incorporação a outros processos e exigia uma configuração separada de buffer de memória para cada um. No entanto, o ProtoLog agora está disponível para código do lado do cliente, eliminando a necessidade de código boilerplate adicional.

Ao contrário do código dos serviços do sistema, o código do lado do cliente geralmente pula a internação de string no tempo de compilação. Em vez disso, a string interning ocorre dinamicamente em uma linha de execução em segundo plano. Como resultado, embora o ProtoLog do lado do cliente ofereça vantagens de uso de memória comparáveis ao ProtoLog nos serviços do sistema, ele tem uma sobrecarga de desempenho ligeiramente maior e não tem a vantagem de redução de memória da memória fixada da contraparte do lado do servidor.

Grupos de ProtoLog

As mensagens do ProtoLog são organizadas em grupos chamados ProtoLogGroups, de forma semelhante à organização das mensagens do Logcat por TAG. Esses ProtoLogGroups servem como clusters de mensagens que podem ser ativados ou desativados coletivamente no momento da execução. Além disso, elas controlam se as mensagens precisam ser removidas durante a compilação e onde elas precisam ser registradas (proto, logcat ou ambos). Cada ProtoLogGroup contém as seguintes propriedades:

  • enabled: quando definido como false, as mensagens nesse grupo são excluídas durante a compilação e não estão disponíveis no momento da execução.
  • logToProto: define se esse grupo gera registros com formato binário.
  • logToLogcat: define se esse grupo registra no Logcat.
  • tag: nome da origem da mensagem registrada.

Cada processo que usa o ProtoLog precisa ter uma instância ProtoLogGroup configurada.

Tipos de argumentos compatíveis

Internamente, as strings formatadas do ProtoLog usam android.text.TextUtils#formatSimple(String, Object...), então a sintaxe é a mesma.

O ProtoLog é compatível com os seguintes tipos de argumento:

  • %b: booleano
  • %d, %x: tipo integral (short, integer ou long)
  • %f: tipo de ponto flutuante (float ou duplo)
  • %s: string
  • %%: um caractere literal de porcentagem

Os modificadores de largura e precisão, como %04d e %10b, são aceitos, mas argument_index e flags não.

Usar o ProtoLog em um novo serviço

Para usar o ProtoLog em um novo processo:

  1. Crie uma definição de ProtoLogGroup para esse serviço.

  2. Inicializar a definição antes do primeiro uso (por exemplo, na criação do processo):

    Protolog.init(ProtologGroup.values());

  3. Use Protolog da mesma forma que android.util.Log:

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

Ativar a otimização no tempo de compilação

Para ativar o ProtoLog no tempo de compilação em um processo, é necessário mudar as regras de build e invocar o binário protologtool.

ProtoLogTool é um binário de transformação de código que realiza a internação de string e atualiza a invocação do ProtoLog. Esse binário transforma todas as chamadas de registro ProtoLog, conforme mostrado neste exemplo:

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

em:

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

Neste exemplo, ProtoLog, ProtoLogImpl e ProtoLogGroup são as classes fornecidas como argumentos (podem ser importadas, importadas de forma estática ou caminho completo, importações de curinga não são permitidas) e x é o método de registro.

A transformação é feita no nível da origem. Um hash é gerado com base na string de formato, no nível de registro e no nome do grupo de registro e inserido após o argumento ProtoLogGroup. O código real gerado é inline, e vários novos caracteres de linha são adicionados para preservar o número de linhas no arquivo.

Exemplo:

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

Opções de linha de comando

Uma das principais vantagens do ProtoLog é que ele pode ser ativado ou desativado no tempo de execução. Por exemplo, é possível ter um registro mais detalhado em um build, desativado por padrão, e ativá-lo durante o desenvolvimento local para depurar um problema específico. Esse padrão é usado, por exemplo, no WindowManager com os grupos WM_DEBUG_WINDOW_TRANSITIONS e WM_DEBUG_WINDOW_TRANSITIONS_MIN que ativam diferentes tipos de registro de transição, sendo o primeiro ativado por padrão.

É possível configurar o ProtoLog usando o Perfetto ao iniciar um rastro. Também é possível configurar o ProtoLog localmente usando a linha de comando adb.

O comando adb shell cmd protolog_configuration é compatível com os seguintes argumentos:

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

Dicas para uso eficaz

O ProtoLog usa a atribuição de string para a mensagem e todos os argumentos de string transmitidos. Isso significa que, para aproveitar melhor o ProtoLog, as mensagens precisam isolar valores repetidos em variáveis.

Por exemplo, considere a seguinte instrução:

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

Quando otimizado no momento da compilação, ele se torna o seguinte:

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

Se o ProtoLog for usado no código com argumentos 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");

Isso resulta nas seguintes mensagens na memória:

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)

Se a instrução ProtoLog for escrita como:

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

O buffer na memória seria:

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)

Essa sequência resulta em uma pegada de memória 35% menor.

Leitor do Winscope

A guia do visualizador ProtoLog do Winscope mostra os rastros do ProtoLog organizados em um formato tabular. É possível filtrar rastros por nível de registro, tag, arquivo de origem (onde a instrução ProtoLog está presente) e conteúdo da mensagem. Todas as colunas podem ser filtradas. Clicar no carimbo de data/hora na primeira coluna leva a linha do tempo para o carimbo de data/hora da mensagem. Além disso, clicar em Go to Current Time rola a tabela ProtoLog de volta para o carimbo de data/hora selecionado na linha do tempo:

Visualizador de ProtoLog

Figura 1. Visualizador de ProtoLog