Caracterização de perfil baseada em gatilhos

O ProfilingManager oferece suporte à captura de perfis com base em gatilhos 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 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 gatilhos.

Capturar dados históricos

Muitos acionadores exigem a análise dos dados históricos que antecederam o evento. O gatilho em si geralmente é uma consequência de um problema, e não a causa raiz. Se você iniciar um perfil somente depois que o gatilho ocorrer, a causa raiz já poderá ter sido 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 já pode ter sido concluída. Iniciar um perfil nesse momento perde o trabalho de bloqueio real.

É impossível prever exatamente quando alguns gatilhos vão ocorrer, o que impede iniciar um perfil manualmente 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.
  • Otimize jornadas ideais do usuário:analise e melhore fluxos, por exemplo, a inicialização do app.
  • Entender o comportamento do usuário:receba insights sobre eventos, por exemplo, saídas do app iniciadas pelo usuário.

Configurar um gatilho

O código a seguir demonstra como se registrar para o gatilho 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. Get the manager: 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 uma hora a esse gatilho 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 gatilhos: registra a lista de gatilhos com ProfilingManager usando addProfilingTriggers.
  6. Ativar 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 estava em execução e que há uma cota de limitador de taxa disponível. Esta etapa opcional demonstra um fluxo de ponta a ponta porque seu app precisa chamar reportFullyDrawn() para esse gatilho.

Recuperar o trace

O sistema salva os perfis baseados em acionadores no mesmo diretório que outros perfis. O nome do arquivo para rastreamentos 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 código de exemplo usando o ADB, ele pode ter esta aparência:

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 o rastreamento em segundo plano funciona

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

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 rastreamento em segundo plano funcionam.

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

Os perfis acionados pelo sistema têm um tamanho máximo definido pelo sistema e, por isso, usam um buffer circular. Quando o buffer está cheio, os novos dados de rastreamento substituem os mais antigos. Como mostrado na Figura 1, um rastreamento 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 antecede o gatilho.

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

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

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

Depurar gatilhos localmente

Como os rastreamentos em segundo plano são executados em horários aleatórios, é difícil acionar a depuração localmente. Para forçar um rastreamento em 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 rastreamento contínuo em segundo plano 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.