Caracterização de perfil baseada em gatilhos

O ProfilingManager é compatível com a captura de perfis com base em acionadores do sistema. O sistema gerencia o processo de gravação e fornece o perfil resultante ao app.

Os gatilhos estão vinculados a eventos essenciais para o desempenho. Os perfis gravados pelo sistema fornecem informações detalhadas de depuração para as jornadas ideais do usuário (CUJs) associadas a esses acionadores.

Capturar dados históricos

Muitos gatilhos exigem a análise dos dados históricos que antecedem o evento. O gatilho em si costuma ser uma consequência de um problema, e não a causa raiz. Se você iniciar um perfil somente após o acionador ocorrer, a causa raiz já poderá ser perdida.

Por exemplo, uma operação de longa duração na linha de execução de interface causa um erro O app não está respondendo (ANR). Quando o sistema detecta o ANR e sinaliza o app, a operação pode já ter sido concluída. Iniciar um perfil nesse momento perde o trabalho de bloqueio real.

É impossível prever exatamente quando alguns acionadores vão ocorrer, o que impede o início manual de um perfil com antecedência.

Por que usar a captura baseada em acionadores?

O principal motivo para usar gatilhos de criação de perfil é capturar dados de eventos imprevisíveis em que é impossível para um app começar a gravar manualmente antes que esses eventos ocorram. Os gatilhos de criação de perfil podem ser usados para:

  • Depurar problemas de desempenho:diagnostique ANRs, vazamentos de memória e outros problemas de estabilidade.
  • Otimizar jornadas ideais do usuário:analise e melhore os fluxos, por exemplo, a inicialização do app.
  • Entender o comportamento do usuário:receba insights sobre eventos, como saídas do app iniciadas pelo usuário.

Configurar um gatilho

O código a seguir demonstra como se registrar para o acionador TRIGGER_TYPE_APP_FULLY_DRAWN e aplicar a limitação de taxa a ele.

Kotlin

fun recordWithTrigger() {
    val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java)

    val triggers = ArrayList<ProfilingTrigger>()

    val triggerBuilder = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN)
        .setRateLimitingPeriodHours(1)

    triggers.add(triggerBuilder.build())

    val mainExecutor: Executor = Executors.newSingleThreadExecutor()

    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.resultFilePath
            )
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode=" + profilingResult.errorCode + " errormsg=" + profilingResult.errorMessage
            )
        }
    }

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

}

Java

public void recordWithTrigger() {
  ProfilingManager profilingManager = getApplicationContext().getSystemService(
      ProfilingManager.class);
  List<ProfilingTrigger> triggers = new ArrayList<>();
  ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder(
      ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN);
  triggerBuilder.setRateLimitingPeriodHours(1);
  triggers.add(triggerBuilder.build());

  Executor mainExecutor = Executors.newSingleThreadExecutor();
  Consumer<ProfilingResult> resultCallback =
      new Consumer<ProfilingResult>() {
        @Override
        public void accept(ProfilingResult profilingResult) {
          if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.getResultFilePath());
            setupProfileUploadWorker(profilingResult.getResultFilePath());
          } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode="
                    + profilingResult.getErrorCode()
                    + " errormsg="
                    + profilingResult.getErrorMessage());
          }
        }
      };
  profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback);
  profilingManager.addProfilingTriggers(triggers);

}

O código executa as seguintes etapas:

  1. Acessar o gerenciador: recupera o serviço ProfilingManager.
  2. Definir um gatilho: cria um ProfilingTrigger para TRIGGER_TYPE_APP_FULLY_DRAWN. Esse evento ocorre quando o app informa que concluiu a inicialização e está interativo.
  3. Definir limites de taxa: aplica um limite de taxa de 1 hora a esse acionador específico (setRateLimitingPeriodHours(1)). Isso impede que o app registre mais de um perfil de inicialização por hora.
  4. Registrar listener: chama registerForAllProfilingResults para definir o callback que processa o resultado. Esse callback recebe o caminho do perfil salvo por getResultFilePath().
  5. Adicionar acionadores: registra a lista de acionadores com ProfilingManager usando addProfilingTriggers.
  6. Disparar evento: chama reportFullyDrawn(), que emite o evento TRIGGER_TYPE_APP_FULLY_DRAWN para o sistema, acionando uma coleta de perfil, supondo que um rastreamento em segundo plano do sistema esteja em execução e que haja cota de limitador de taxa disponível. Esta etapa opcional demonstra um fluxo completo, porque seu app precisa chamar reportFullyDrawn() para esse acionador.

Recuperar o trace

O sistema salva os perfis baseados em acionadores no mesmo diretório que os outros perfis. O nome de arquivo para traces acionados segue este formato:

profile_trigger_<profile_type_code>_<datetime>.<profile-type-name>

É possível extrair o arquivo usando o ADB. Por exemplo, para extrair o rastreamento do sistema capturado com o exemplo de código usando o ADB, o comando pode ser assim:

adb pull /data/user/0/com.example.sampleapp/files/profiling/profile_trigger_1_2025-05-06-14-12-40.perfetto-trace

Para detalhes sobre como visualizar esses traces, consulte Recuperar e analisar dados de criação de perfil.

Como funciona o rastreamento em segundo plano

Para capturar dados de antes de um acionador, o SO inicia periodicamente um trace em segundo plano. Se um gatilho ocorrer enquanto esse rastro em segundo plano estiver ativo e seu app estiver registrado para ele, o sistema vai salvar o perfil de rastro no diretório do app. O perfil vai incluir as informações que levaram ao acionador.

Depois que o perfil é salvo, o sistema notifica o app usando o callback fornecido para registerForAllProfilingResults. Esse callback fornece o caminho para o perfil capturado, que pode ser acessado chamando ProfilingResult#getResultFilePath().

Diagrama que mostra como os snapshots de rastreamento em segundo plano funcionam, com um buffer circular que captura dados antes de um evento de acionamento.
Figura 1: como funcionam os snapshots de rastreamento em segundo plano.

Para reduzir o impacto no desempenho do dispositivo e na duração da bateria, o sistema não executa traces em segundo plano continuamente. Em vez disso, ele usa um método de amostragem. O sistema inicia aleatoriamente um rastro em segundo plano dentro de um período definido (com uma duração mínima e máxima). Espaçar esses traces de forma aleatória melhora a cobertura do acionador.

Os perfis acionados pelo sistema têm um tamanho máximo definido pelo sistema, então usam um buffer circular. Quando o buffer está cheio, os novos dados de trace substituem os mais antigos. Como mostrado na Figura 1, um rastro capturado pode não cobrir toda a duração da gravação em segundo plano se o buffer ficar cheio. Em vez disso, ele representa a atividade mais recente que levou ao acionador.

Implementar limitação de taxa específica do acionador

Os acionadores de alta frequência podem consumir rapidamente a cota do limitador de taxa do app. Para entender melhor o limitador de taxa, consulte Como funciona o limitador de taxa. Para evitar que um único tipo de gatilho esgote sua cota, implemente uma limitação de taxa específica para cada gatilho.

O ProfilingManager oferece suporte à limitação de taxa específica de acionadores definida pelo app. Isso permite adicionar outra camada de limitação com base no tempo, além do limitador de taxa atual. Use a API setRateLimitingPeriodHours para definir um tempo de espera específico para um gatilho. Depois que o tempo de espera expirar, será possível acioná-lo novamente.

Depurar gatilhos localmente

Como os traces em segundo plano são executados em momentos aleatórios, é difícil depurar gatilhos localmente. Para forçar um trace em segundo plano para testes, use o seguinte comando ADB:

adb shell device_config put profiling_testing system_triggered_profiling.testing_package_name <com.example.myapp>

Esse comando força o sistema a iniciar um trace contínuo em segundo plano para o pacote especificado, permitindo que cada acionador colete um perfil se o limitador de taxa permitir.

Também é possível ativar outras opções de depuração, por exemplo, desativar o limitador de taxa ao depurar localmente. Para mais informações, consulte Comandos de depuração para criação de perfil local.