Caracterização de perfil baseada em gatilhos

O ProfilingManager oferece suporte à captura de perfis com base em acionadores do sistema. O sistema gerencia o processo de gravação e fornece o perfil resultante ao seu app.

Os acionadores estão vinculados a eventos críticos de 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 acionadores exigem a análise dos dados históricos que levam ao evento. O acionador em si geralmente é uma consequência de um problema, e não a causa raiz. Se você iniciar um perfil somente após a ocorrência do acionador, a causa raiz já poderá ser perdida.

Por exemplo, uma operação de longa duração na linha de execução da interface do usuário causa um erro "O app não está respondendo" (ANR, na sigla em inglês). Quando o sistema detecta o ANR e sinaliza o app, a operação já pode ter terminado. Iniciar um perfil nesse momento perde o trabalho de bloqueio real.

É inviável prever exatamente quando alguns acionadores ocorrem, o que impossibilita iniciar um perfil manualmente com antecedência.

Por que usar a captura baseada em acionadores?

O principal motivo para usar acionadores 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 acionadores de criação de perfil podem ser usados para:

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

Configurar um acionador

O código a seguir demonstra como se registrar no 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 estas etapas:

  1. Receber o gerenciador: recupera o serviço ProfilingManager.
  2. Definir um acionador: cria um ProfilingTrigger para TRIGGER_TYPE_APP_FULLY_DRAWN. Esse evento ocorre quando o app informa que terminou 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 grave 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. Acionar evento: chama reportFullyDrawn(), que emite o TRIGGER_TYPE_APP_FULLY_DRAWN evento para o sistema, acionando uma coleta de perfil, supondo que um trace de segundo plano do sistema estava em execução e que há cota de limitador de taxa disponível. Essa etapa opcional demonstra um fluxo de ponta a ponta porque o app precisa chamar reportFullyDrawn() para esse acionador.

Recuperar o trace

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

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

Você pode extrair o arquivo usando o ADB. Por exemplo, para extrair o trace do sistema capturado com o código de exemplo usando o ADB, ele 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 acionador ocorrer enquanto esse trace de segundo plano estiver ativo e seu app estiver registrado nele, o sistema salvará o perfil de trace no diretório do app. O perfil incluirá informações que levaram ao acionador.

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

Diagrama mostrando como os snapshots de rastreamento em segundo plano funcionam, com um buffer circular capturando dados antes de um evento de acionamento.
Figura 1: como os snapshots de trace de segundo plano funcionam.

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

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

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

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 o limitador de taxa funciona. Para evitar que um único tipo de acionador esgote sua cota, você pode implementar a limitação de taxa específica do acionador.

O ProfilingManager oferece suporte à limitação de taxa específica do acionador definida pelo app. Isso permite adicionar outra camada de limitação baseada em tempo, além do limitador de taxa atual. Use a setRateLimitingPeriodHours API para definir um período de espera específico para um acionador. Depois que o período de espera expirar, você poderá acioná-lo novamente.

Depurar acionadores localmente

Como os traces de segundo plano são executados em horários aleatórios, é difícil depurar acionadores localmente. Para forçar um trace de segundo plano para teste, 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 de segundo plano contínuo para o pacote especificado, permitindo que cada acionador colete um perfil se o limitador de taxa permitir.

Você também pode 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.