Protolog

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

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

  • Usa menos recursos para geração de registros.
  • Do ponto de vista de um desenvolvedor, ele funciona da mesma forma que o framework de registro em log padrão do Android.
  • Permite ativar ou desativar instruções de registro em tempo de execução.
  • Também é possível fazer o registro no logcat.

Para otimizar o uso da memória, o ProtoLog usa um mecanismo de interning de strings. Esse mecanismo calcula e salva um hash compilado da mensagem. Para melhorar o desempenho, o ProtoLog realiza o internamento de strings durante a compilação para serviços do sistema. Ele grava apenas o identificador e os argumentos da mensagem no tempo de execução. Quando você gera um rastreamento do ProtoLog ou recebe um relatório de bug, o ProtoLog incorpora automaticamente o dicionário de mensagens criado no momento da compilação. Isso permite a decodificação de mensagens de qualquer build.

O ProtoLog armazena mensagens em um formato binário (proto) em um rastreamento do Perfetto. A decodificação de mensagens ocorre no trace_processor do Perfetto. O processo decodifica as mensagens proto binárias, traduz os identificadores de mensagens em strings usando o dicionário de mensagens incorporado e formata a string usando argumentos dinâmicos.

O ProtoLog é compatível com os mesmos níveis de registro do android.utils.Log, que são 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. Mais tarde, ele foi expandido para incluir o código do shell do WindowManager no processo da interface do sistema. No entanto, usar o ProtoLog exigia um código de configuração boilerplate complexo. Além disso, o registro de Proto foi restrito ao servidor do sistema e aos processos da interface do sistema. Isso dificultava a incorporação em outros processos e exigia uma configuração separada de buffer de memória para cada um. 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 de serviços do sistema, o código do lado do cliente geralmente ignora o internamento de strings no tempo de compilação. Em vez disso, o internamento de strings 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 em serviços do sistema, ele incorre em uma sobrecarga de desempenho um pouco maior e não tem a vantagem de redução de memória da memória fixada que a contraparte do lado do servidor oferece.

Grupos ProtoLog

As mensagens do ProtoLog são organizadas em grupos chamados ProtoLogGroups, semelhante a como as mensagens do Logcat são organizadas por TAG. Esses ProtoLogGroups servem como clusters de mensagens que podem ser ativados ou desativados durante a execução. Eles também controlam se as mensagens são removidas durante a compilação e onde são registradas (proto, logcat ou ambos). Cada ProtoLogGroup inclui as seguintes propriedades:

  • enabled: quando definido como false, as mensagens nesse grupo são excluídas durante a compilação e não ficam disponíveis no tempo de execução.
  • logToProto: define se este grupo gera registros com formato binário.
  • logToLogcat: define se este grupo faz login 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

O ProtoLog formata strings internamente usando 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 (curto, inteiro ou longo)
  • %f: tipo de ponto flutuante (float ou double)
  • %s: string
  • %%: um caractere de porcentagem literal

Modificadores de largura e precisão, como %04d e %10b, são compatíveis. No entanto, argument_index e flags não são compatíveis.

Usar o ProtoLog em um novo serviço

Para usar o ProtoLog em um novo serviço, siga estas etapas:

  1. Crie uma definição ProtoLogGroup para esse serviço.
  2. Inicialize a definição antes do primeiro uso. Por exemplo, inicialize-o durante a 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 momento da compilação

Para ativar o ProtoLog no momento da compilação em um processo, mude as regras de build e invoque o binário protologtool.

ProtoLogTool é um binário de transformação de código que realiza o internamento de strings e atualiza a invocação do ProtoLog. Esse binário transforma cada chamada de registro de 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 estaticamente ou com caminho completo. Importações curinga não são permitidas) e x é o método de geração de registros.

A transformação é feita no nível da origem. Um hash é gerado com base na string de formato, no nível e no nome do grupo de registros e inserido após o argumento ProtoLogGroup. O código gerado real é inserido em linha, e vários novos caracteres de nova linha são adicionados para preservar a numeração 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 em tempo de execução. Por exemplo, você pode 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, permitindo 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 rastreamento. 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 o internamento de strings para a mensagem e todos os argumentos de string transmitidos. Isso significa que, para aproveitar mais 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 é traduzido para isto:

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

Se o ProtoLog for usado no código com os 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, em vez disso, a instrução ProtoLog fosse escrita como:

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

O buffer na memória ficaria assim:

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 rastreamentos do ProtoLog organizados em um formato tabular. É possível filtrar rastreamentos 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. Ao clicar no carimbo de data/hora na primeira coluna, a linha do tempo é transportada para o carimbo de data/hora da mensagem. Além disso, clicar em Ir para o horário atual 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