perfetto-analyzer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/bin/cli.js +117 -0
- package/package.json +31 -0
- package/skill/SKILL.md +234 -0
- package/skill/references/adb-quick-start.md +139 -0
- package/skill/references/metrics-interpretation.md +130 -0
- package/skill/references/perfetto-traces-reference.md +116 -0
- package/skill/scripts/analyze_metrics.py +408 -0
- package/skill/scripts/collect_trace.py +204 -0
- package/skill/scripts/configs/all.textproto +111 -0
- package/skill/scripts/configs/battery.textproto +65 -0
- package/skill/scripts/configs/cpu.textproto +68 -0
- package/skill/scripts/configs/frames.textproto +62 -0
- package/skill/scripts/configs/memory.textproto +57 -0
- package/skill/scripts/generate_report.py +397 -0
- package/skill/scripts/parse_perfetto_pb.py +469 -0
- package/skill/scripts/requirements.txt +1 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Interpretação de Métricas do Perfetto
|
|
2
|
+
|
|
3
|
+
## Memória
|
|
4
|
+
|
|
5
|
+
### Heap Nativo (heapprofd)
|
|
6
|
+
|
|
7
|
+
| Métrica | O que significa | Red Flag |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `unreleased_bytes` | Bytes alocados que ainda não foram liberados | > 50 MB acumulado sugere leak |
|
|
10
|
+
| `total_bytes` | Total bruto alocado (inclui o que foi liberado depois) | — |
|
|
11
|
+
| `alloc_count` | Número de chamadas `malloc()` | — |
|
|
12
|
+
| Churn ratio | `total / unreleased` | > 10x sugere muitas alocações temporárias |
|
|
13
|
+
|
|
14
|
+
**Modos de visualização:**
|
|
15
|
+
- **Unreleased malloc size** (padrão): foca em memória que *ainda está* alocada → detecta leaks
|
|
16
|
+
- **Total malloc size**: inclui memória liberada → detecta heap churn / GC pressure
|
|
17
|
+
- **Unreleased malloc count**: conta alocs, ignora tamanho → útil quando leak é muitos objetos pequenos
|
|
18
|
+
- **Total malloc count**: total de chamadas malloc → detecta hot paths de alocação
|
|
19
|
+
|
|
20
|
+
### ART Heap Dump (Java/Kotlin)
|
|
21
|
+
|
|
22
|
+
| Métrica | O que significa | Red Flag |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `instance_count` | Instâncias vivas de cada classe | Número inesperadamente alto de singletons |
|
|
25
|
+
| `total_size` | Bytes retidos por todas as instâncias | > 100 MB em heap de app médio é preocupante |
|
|
26
|
+
| Dominators | Objeto que retém outro na memória | Activity/Fragment aparecendo como dominator = leak |
|
|
27
|
+
|
|
28
|
+
**Nota crítica:** "A análise de heap profiling não é retroativa — só mostra alocações após o rastreamento iniciar."
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## CPU
|
|
33
|
+
|
|
34
|
+
### CPU Sampling (perf events)
|
|
35
|
+
|
|
36
|
+
| Métrica | O que significa | Red Flag |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `self_count` | Amostras onde a função é folha (onde o CPU estava de fato) | Alto = função gargalo real |
|
|
39
|
+
| `cumulative_count` | Amostras onde a função aparece em qualquer posição do stack | Alto = função chamada frequentemente por hot paths |
|
|
40
|
+
| Frequência de amostragem | Amostras por segundo (padrão: 100 Hz) | Frequência baixa → menos resolução |
|
|
41
|
+
|
|
42
|
+
### Scheduling
|
|
43
|
+
|
|
44
|
+
| Métrica | O que significa | Red Flag |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `dur` | Duração que uma thread ficou no estado "running" | Thread com poucos picos longos → não multi-threaded |
|
|
47
|
+
| Context switches | Trocas de contexto | > 10k/s pode indicar contention |
|
|
48
|
+
| Main thread CPU% | % do tempo total de CPU na main thread | > 20% = operações bloqueantes na UI thread |
|
|
49
|
+
|
|
50
|
+
### Throttling e Frequência
|
|
51
|
+
|
|
52
|
+
- Se CPU estiver a < 50% da frequência máxima → thermal throttling ou battery saver
|
|
53
|
+
- `cpu_idle` events frequentes + `cpu_frequency` baixo = CPU economizando energia (pode ser OK)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Frames
|
|
58
|
+
|
|
59
|
+
### Frame Timeline (Android 12+ / API 31+)
|
|
60
|
+
|
|
61
|
+
| Campo | Valores | Significado |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `present_type` | `On-time Present`, `Late Present`, `Early Present` | Se foi entregue no vsync correto |
|
|
64
|
+
| `on_time_finish` | 0 ou 1 | 0 = frame chegou tarde → dropped |
|
|
65
|
+
| `jank_type` | ver abaixo | Causa do jank |
|
|
66
|
+
|
|
67
|
+
### Tipos de Jank (jank_type)
|
|
68
|
+
|
|
69
|
+
| Tipo | Causa | Solução |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `App Deadline Missed` | App não terminou o frame a tempo | Reduzir trabalho na UI thread |
|
|
72
|
+
| `SurfaceFlinger CPU Deadline Missed` | SurfaceFlinger não compôs a tempo | Reduzir layers/overdraw |
|
|
73
|
+
| `SurfaceFlinger GPU Deadline Missed` | GPU não finalizou renderização a tempo | Otimizar shaders, reduzir complexidade visual |
|
|
74
|
+
| `Buffering` | App produz frames mais rápido que display consome | Normal em alguns cenários |
|
|
75
|
+
| `None` | Frame foi entregue normalmente | — |
|
|
76
|
+
|
|
77
|
+
### Thresholds de Latência de Frame
|
|
78
|
+
|
|
79
|
+
| Percentil | Threshold 60fps | Threshold 90fps | Threshold 120fps |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| P50 | ≤ 16.67ms | ≤ 11.1ms | ≤ 8.33ms |
|
|
82
|
+
| P95 | ≤ 33.3ms | ≤ 22.2ms | ≤ 16.67ms |
|
|
83
|
+
| P99 | ≤ 50ms | ≤ 33.3ms | ≤ 25ms |
|
|
84
|
+
|
|
85
|
+
**Dropped frames %:**
|
|
86
|
+
- < 0.1%: Excelente
|
|
87
|
+
- 0.1%–1%: Bom
|
|
88
|
+
- 1%–5%: Aceitável (usuário pode perceber em cenários específicos)
|
|
89
|
+
- > 5%: Ruim (jank visível)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Battery
|
|
94
|
+
|
|
95
|
+
### Contadores Principais
|
|
96
|
+
|
|
97
|
+
| Contador | Unidade | Significado |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `batt_charge_uah` | µAh (microampere-hora) | Carga total da bateria; delta = consumo |
|
|
100
|
+
| `batt_current_ua` | µA | Corrente instantânea; negativo = descarregando |
|
|
101
|
+
| `batt_capacity_pct` | % | % de carga; delta = % consumido no trace |
|
|
102
|
+
| `batt_voltage_uv` | µV | Tensão; queda rápida = discharge rate alto |
|
|
103
|
+
|
|
104
|
+
**Conversão:** 1 µAh = 3.6 mC (milliCoulombs)
|
|
105
|
+
|
|
106
|
+
### Power Rails (Qualcomm/Google Tensor)
|
|
107
|
+
|
|
108
|
+
Os power rails medem consumo por subsistema em mW:
|
|
109
|
+
|
|
110
|
+
| Rail | O que mede |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `power.rails.cpu.big` | CPU cores de alto desempenho |
|
|
113
|
+
| `power.rails.cpu.little` | CPU cores de eficiência |
|
|
114
|
+
| `power.rails.gpu` | GPU |
|
|
115
|
+
| `power.rails.mem.vddq` | Memória RAM |
|
|
116
|
+
| `power.rails.ddr.a/b/c` | Interfaces de memória |
|
|
117
|
+
|
|
118
|
+
**Referência de consumo típico (varia por device):**
|
|
119
|
+
- App idle: ~5–20 mW total
|
|
120
|
+
- App ativo (UI, rede): ~100–300 mW
|
|
121
|
+
- App com vídeo/jogo: ~500–2000 mW
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## O que olhar primeiro (checklist rápido)
|
|
126
|
+
|
|
127
|
+
1. **Frames** → % dropped + P95. Se > 1% dropped, há jank, investigue CPU/GPU timeline.
|
|
128
|
+
2. **CPU** → Processos com mais CPU time. Main thread alta? → operações bloqueantes.
|
|
129
|
+
3. **Memória** → unreleased_bytes crescendo ao longo do trace? → leak. Churn alto? → GC.
|
|
130
|
+
4. **Battery** → Drain rate acima do esperado? → wake locks, sync agressivo, GPS.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Referência: Tipos de Traces Perfetto
|
|
2
|
+
|
|
3
|
+
## Quando usar cada tipo
|
|
4
|
+
|
|
5
|
+
| Tipo | Use quando... | Dados coletados |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `memory` | App consome muita RAM, cresce ao longo do tempo, crashes por OOM | Heap nativo (heapprofd), ART heap dumps, contadores de memória |
|
|
8
|
+
| `cpu` | App trava, UI não responde, ANR, processamento lento | CPU sampling, scheduling, frequência de cores |
|
|
9
|
+
| `frames` | Animações travando, scroll não fluido, UI parecendo "lenta" | Frame timeline, jank classification, vsync events |
|
|
10
|
+
| `battery` | Bateria acabando rápido, app continua drenando em background | Battery counters, power rails, wake locks |
|
|
11
|
+
| `all` | Não sabe onde está o problema, análise geral inicial | Tudo acima combinado (buffer maior, mais pesado) |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Configs textproto (resumo)
|
|
16
|
+
|
|
17
|
+
Cada arquivo em `scripts/configs/` usa os seguintes `data_sources`:
|
|
18
|
+
|
|
19
|
+
### memory.textproto
|
|
20
|
+
```
|
|
21
|
+
android.heapprofd → Native heap (C/C++ malloc/free)
|
|
22
|
+
android.java_hprof → ART heap dump (Java/Kotlin objects)
|
|
23
|
+
linux.sys_stats → meminfo counters
|
|
24
|
+
linux.ftrace → mm_event, lowmemorykiller
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### cpu.textproto
|
|
28
|
+
```
|
|
29
|
+
linux.perf → CPU sampling (perf_event_config)
|
|
30
|
+
linux.ftrace → sched_switch, sched_wakeup, cpu_frequency
|
|
31
|
+
linux.sys_stats → stat_cpu_times, cpufreq
|
|
32
|
+
linux.process_stats → lista de processos/threads
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### frames.textproto
|
|
36
|
+
```
|
|
37
|
+
android.surfaceflinger.frametimeline → Frame timeline (Android 12+)
|
|
38
|
+
android.atrace → gfx, view, wm, am, input categories
|
|
39
|
+
linux.ftrace → sched_switch, drm_vblank, gpu_mem
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### battery.textproto
|
|
43
|
+
```
|
|
44
|
+
android.power → battery_counters + power rails
|
|
45
|
+
linux.ftrace → wakeup_source_activate/deactivate, suspend_resume
|
|
46
|
+
linux.sys_stats → cpu_times para correlação
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### all.textproto
|
|
50
|
+
Combina todos acima. Usa buffer de 128 MB.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Limitações por API Level
|
|
55
|
+
|
|
56
|
+
| Feature | API Level mínimo |
|
|
57
|
+
|---|---|
|
|
58
|
+
| heapprofd (native heap) | API 26 (Android 8.0) |
|
|
59
|
+
| ART heap dumps | API 28 (Android 9.0) |
|
|
60
|
+
| Frame Timeline completo | API 31 (Android 12) |
|
|
61
|
+
| Power rails | Depende do hardware (Qualcomm/Tensor) |
|
|
62
|
+
| Perfetto daemon nativo | API 29 (Android 10) |
|
|
63
|
+
|
|
64
|
+
**Para Android < 10:** Use `adb shell tracebox` ou o binário Perfetto estático do AOSP.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Duração recomendada por caso de uso
|
|
69
|
+
|
|
70
|
+
| Cenário | Duração |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Startup do app | 5–10s |
|
|
73
|
+
| Fluxo de navegação específico | 10–20s |
|
|
74
|
+
| Scroll / animação | 5–15s |
|
|
75
|
+
| Análise de memory leak | 30–60s (interagir muito com o app) |
|
|
76
|
+
| Análise de battery | 60–120s |
|
|
77
|
+
| Análise completa (all) | 30–60s |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Coleta manual via adb (referência)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Listar devices
|
|
85
|
+
adb devices
|
|
86
|
+
|
|
87
|
+
# Push config pro device
|
|
88
|
+
adb push config.textproto /data/local/tmp/perfetto_config.textproto
|
|
89
|
+
|
|
90
|
+
# Iniciar trace (30 segundos)
|
|
91
|
+
adb shell perfetto \
|
|
92
|
+
--config /data/local/tmp/perfetto_config.textproto \
|
|
93
|
+
--out /data/local/tmp/trace.pb \
|
|
94
|
+
--time 30s
|
|
95
|
+
|
|
96
|
+
# Aguardar e baixar
|
|
97
|
+
adb pull /data/local/tmp/trace.pb ./trace.pb
|
|
98
|
+
|
|
99
|
+
# Limpar device
|
|
100
|
+
adb shell rm /data/local/tmp/trace.pb /data/local/tmp/perfetto_config.textproto
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Visualização no Perfetto UI
|
|
106
|
+
|
|
107
|
+
Para abrir o trace coletado no browser:
|
|
108
|
+
1. Acesse [perfetto.dev/ui](https://perfetto.dev/ui)
|
|
109
|
+
2. Clique em "Open trace file"
|
|
110
|
+
3. Selecione o arquivo `.pb`
|
|
111
|
+
|
|
112
|
+
O Perfetto UI oferece:
|
|
113
|
+
- Timeline com todos os eventos
|
|
114
|
+
- Flamegraph de heap/CPU (selecione região no timeline)
|
|
115
|
+
- SQL Explorer para queries customizadas
|
|
116
|
+
- Comparação entre múltiplos traces
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Analisa métricas brutas do trace e gera insights acionáveis."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# Thresholds de referência (Android best practices)
|
|
10
|
+
THRESHOLDS = {
|
|
11
|
+
"frame_p50_ms": 8.0, # 120fps → 8.3ms, 60fps → 16.7ms
|
|
12
|
+
"frame_p95_ms": 16.67, # Máximo para experiência fluida a 60fps
|
|
13
|
+
"frame_p99_ms": 32.0, # Tolerável mas problemático
|
|
14
|
+
"dropped_pct_warn": 1.0, # >1% dropped é sinal de jank
|
|
15
|
+
"dropped_pct_critical": 5.0,
|
|
16
|
+
"heap_churn_ratio": 10.0, # total_alloc / unreleased > 10 = churn alto
|
|
17
|
+
"cpu_main_thread_pct": 20.0, # Main thread não deve ter > 20% do CPU total
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def analyze_memory(memory_data):
|
|
22
|
+
issues = []
|
|
23
|
+
insights = []
|
|
24
|
+
native_allocs = memory_data.get("native_top_allocators", [])
|
|
25
|
+
art_objects = memory_data.get("art_retained_objects", [])
|
|
26
|
+
|
|
27
|
+
total_unreleased = sum(a.get("unreleased_bytes", 0) for a in native_allocs)
|
|
28
|
+
total_allocated = sum(a.get("total_bytes", 0) for a in native_allocs)
|
|
29
|
+
|
|
30
|
+
if total_unreleased > 0:
|
|
31
|
+
unreleased_mb = total_unreleased / 1024 / 1024
|
|
32
|
+
insights.append(f"Heap nativo total não-liberado: {unreleased_mb:.1f} MB")
|
|
33
|
+
|
|
34
|
+
# Detectar heap churn
|
|
35
|
+
if total_unreleased > 0 and total_allocated > 0:
|
|
36
|
+
churn_ratio = total_allocated / total_unreleased
|
|
37
|
+
if churn_ratio > THRESHOLDS["heap_churn_ratio"]:
|
|
38
|
+
issues.append({
|
|
39
|
+
"severity": "warning",
|
|
40
|
+
"category": "memory",
|
|
41
|
+
"title": "Heap Churn Elevado",
|
|
42
|
+
"detail": f"Ratio alocado/não-liberado = {churn_ratio:.1f}x. "
|
|
43
|
+
f"Muitas alocações temporárias causam GC frequente e stuttering.",
|
|
44
|
+
"action": "Revisar pools de objetos, reutilizar buffers, evitar alocações em hot paths.",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# Top callstacks como insights
|
|
48
|
+
top_3 = native_allocs[:3]
|
|
49
|
+
for i, alloc in enumerate(top_3):
|
|
50
|
+
mb = alloc.get("unreleased_bytes", 0) / 1024 / 1024
|
|
51
|
+
stack = alloc.get("stack", [])
|
|
52
|
+
top_fn = stack[0] if stack else "desconhecida"
|
|
53
|
+
if mb > 1.0:
|
|
54
|
+
insights.append(f"Top alocador #{i+1}: {top_fn} ({mb:.1f} MB não-liberados)")
|
|
55
|
+
|
|
56
|
+
# ART objects
|
|
57
|
+
total_art_mb = sum(o.get("total_size", 0) for o in art_objects) / 1024 / 1024
|
|
58
|
+
if total_art_mb > 50:
|
|
59
|
+
issues.append({
|
|
60
|
+
"severity": "warning",
|
|
61
|
+
"category": "memory",
|
|
62
|
+
"title": "Muitos Objetos Java Retidos",
|
|
63
|
+
"detail": f"{total_art_mb:.0f} MB de objetos Java/Kotlin retidos no heap. "
|
|
64
|
+
f"Pode indicar memory leak ou referências fortes desnecessárias.",
|
|
65
|
+
"action": "Verificar WeakReference, LiveData, ViewModel, e Listeners que podem vazar.",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"issues": issues,
|
|
70
|
+
"insights": insights,
|
|
71
|
+
"summary": {
|
|
72
|
+
"total_unreleased_mb": round(total_unreleased / 1024 / 1024, 2),
|
|
73
|
+
"total_allocated_mb": round(total_allocated / 1024 / 1024, 2),
|
|
74
|
+
"total_art_retained_mb": round(total_art_mb, 2),
|
|
75
|
+
"top_native_allocators": native_allocs[:10],
|
|
76
|
+
"top_art_objects": art_objects[:10],
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def analyze_cpu(cpu_data):
|
|
82
|
+
issues = []
|
|
83
|
+
insights = []
|
|
84
|
+
sched = cpu_data.get("scheduling_summary", [])
|
|
85
|
+
samples = cpu_data.get("top_cpu_callstacks", [])
|
|
86
|
+
freq = cpu_data.get("cpu_frequency", [])
|
|
87
|
+
|
|
88
|
+
total_cpu_time = sum(p.get("total_cpu_seconds", 0) for p in sched)
|
|
89
|
+
|
|
90
|
+
# Identificar o processo do app (o maior não-sistema entre os top processos)
|
|
91
|
+
# Processos de sistema comuns a excluir da análise de dominância
|
|
92
|
+
SYSTEM_PROCS = {"/system/bin/", "/vendor/bin/", "kworker", "kswapd", "system_server",
|
|
93
|
+
"com.android.systemui", "com.android.phone", "traced_perf", "traced_probes"}
|
|
94
|
+
|
|
95
|
+
def is_system(name):
|
|
96
|
+
return any(s in name for s in SYSTEM_PROCS)
|
|
97
|
+
|
|
98
|
+
# Callstacks filtrados para o processo do app
|
|
99
|
+
app_name = ""
|
|
100
|
+
app_procs = [p for p in sched if not is_system(p.get("process_name", ""))]
|
|
101
|
+
if app_procs:
|
|
102
|
+
top_app = app_procs[0]
|
|
103
|
+
app_name = top_app["process_name"]
|
|
104
|
+
app_pct = top_app["total_cpu_seconds"] / total_cpu_time * 100 if total_cpu_time else 0
|
|
105
|
+
insights.append(
|
|
106
|
+
f"Processo com mais CPU: {top_app['process_name']} — "
|
|
107
|
+
f"{top_app['total_cpu_seconds']:.2f}s ({app_pct:.1f}% do total)"
|
|
108
|
+
)
|
|
109
|
+
if app_pct > 40:
|
|
110
|
+
# Identifica ofensores reais nos callstacks do app
|
|
111
|
+
app_stacks = [s for s in samples if s.get("process_name") == app_name]
|
|
112
|
+
top_offenders = app_stacks[:3]
|
|
113
|
+
offender_lines = []
|
|
114
|
+
for s in top_offenders:
|
|
115
|
+
fn = s.get("function_name", "?")
|
|
116
|
+
thread = s.get("thread_name", "?")
|
|
117
|
+
count = s.get("sample_count", 0)
|
|
118
|
+
offender_lines.append(f"`{fn}` ({thread}, {count} amostras)")
|
|
119
|
+
offender_text = "; ".join(offender_lines) if offender_lines else "ver tabela de callstacks"
|
|
120
|
+
|
|
121
|
+
issues.append({
|
|
122
|
+
"severity": "warning",
|
|
123
|
+
"category": "cpu",
|
|
124
|
+
"title": f"App com Alto Uso de CPU ({app_pct:.0f}%)",
|
|
125
|
+
"detail": f"{top_app['process_name']} consumiu {top_app['total_cpu_seconds']:.2f}s de CPU "
|
|
126
|
+
f"({app_pct:.0f}% do total). Principais ofensores: {offender_text}.",
|
|
127
|
+
"action": "Investigar as funções listadas na tabela 'Top Funções por Amostras de CPU'. "
|
|
128
|
+
"Funções de ART interpreter indicam código Kotlin/Java não otimizado (Compose recomposição excessiva). "
|
|
129
|
+
"Funções em RenderThread indicam gargalo de GPU/composição. "
|
|
130
|
+
"Abrir o .pb no perfetto.dev/ui para flame graph interativo.",
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
# Context switches elevados
|
|
134
|
+
total_switches = sum(p.get("context_switches", 0) for p in sched)
|
|
135
|
+
if total_switches > 10000:
|
|
136
|
+
insights.append(f"Total de context switches: {total_switches:,} — pode indicar contention entre threads")
|
|
137
|
+
|
|
138
|
+
# Frequência de CPU — emuladores reportam 1-2 (escala relativa, não MHz)
|
|
139
|
+
valid_freqs = [f for f in freq if f.get("avg_freq_mhz", 0) >= 100]
|
|
140
|
+
if valid_freqs:
|
|
141
|
+
overall_avg = sum(f["avg_freq_mhz"] for f in valid_freqs) / len(valid_freqs)
|
|
142
|
+
insights.append(f"Frequência média de CPU: {overall_avg:.0f} MHz")
|
|
143
|
+
|
|
144
|
+
# Detectar padrões problemáticos nos frames JVM
|
|
145
|
+
jvm = cpu_data.get("jvm_callstacks", [])
|
|
146
|
+
total_jvm_samples = max((j.get("sample_count", 0) for j in jvm), default=1)
|
|
147
|
+
|
|
148
|
+
def jvm_hit(pattern):
|
|
149
|
+
# max() para não duplicar quando múltiplos métodos da mesma classe aparecem no mesmo sample
|
|
150
|
+
counts = [j["sample_count"] for j in jvm if pattern in j.get("function_name", "")]
|
|
151
|
+
return max(counts) if counts else 0
|
|
152
|
+
|
|
153
|
+
recomposer_hits = jvm_hit("Recomposer")
|
|
154
|
+
snapshot_hits = jvm_hit("SnapshotStateObserver")
|
|
155
|
+
coroutine_hits = jvm_hit("BaseContinuationImpl")
|
|
156
|
+
|
|
157
|
+
if recomposer_hits > 0 or snapshot_hits > 0:
|
|
158
|
+
pct = round(max(recomposer_hits, snapshot_hits) / total_jvm_samples * 100)
|
|
159
|
+
issues.append({
|
|
160
|
+
"severity": "warning",
|
|
161
|
+
"category": "cpu",
|
|
162
|
+
"title": f"Recomposição Excessiva do Compose ({pct}% das amostras JVM)",
|
|
163
|
+
"detail": (
|
|
164
|
+
f"O Recomposer aparece em {recomposer_hits} amostras e "
|
|
165
|
+
f"SnapshotStateObserver em {snapshot_hits} amostras. "
|
|
166
|
+
f"Indica que composables estão recompondo com frequência desnecessária."
|
|
167
|
+
),
|
|
168
|
+
"action": (
|
|
169
|
+
"1. Usar o Layout Inspector (Android Studio) → 'Recomposition Counts' para "
|
|
170
|
+
"identificar quais composables recompõem mais. "
|
|
171
|
+
"2. Envolver leituras de estado derivado em `remember { derivedStateOf {} }`. "
|
|
172
|
+
"3. Garantir que lambdas passados a composables são estáveis (`@Stable`, "
|
|
173
|
+
"`rememberUpdatedState`). "
|
|
174
|
+
"4. Evitar leituras de `State` no escopo de composables pais desnecessariamente."
|
|
175
|
+
),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if coroutine_hits > 30:
|
|
179
|
+
issues.append({
|
|
180
|
+
"severity": "warning",
|
|
181
|
+
"category": "cpu",
|
|
182
|
+
"title": f"Alta Frequência de Resumption de Coroutines ({coroutine_hits} amostras)",
|
|
183
|
+
"detail": (
|
|
184
|
+
f"`BaseContinuationImpl.resumeWith` aparece em {coroutine_hits} amostras. "
|
|
185
|
+
f"Muitas coroutines sendo suspensas e retomadas por frame."
|
|
186
|
+
),
|
|
187
|
+
"action": (
|
|
188
|
+
"Verificar se coroutines em loops ou `LaunchedEffect` estão sendo lançadas "
|
|
189
|
+
"desnecessariamente a cada recomposição. Usar `rememberCoroutineScope()` "
|
|
190
|
+
"e cancelar jobs anteriores antes de relançar."
|
|
191
|
+
),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"issues": issues,
|
|
196
|
+
"insights": insights,
|
|
197
|
+
"summary": {
|
|
198
|
+
"total_cpu_seconds": round(total_cpu_time, 2),
|
|
199
|
+
"total_context_switches": total_switches,
|
|
200
|
+
"top_processes": sched[:10],
|
|
201
|
+
"top_callstacks": samples[:10],
|
|
202
|
+
"jvm_callstacks": jvm[:20],
|
|
203
|
+
"app_callstacks": cpu_data.get("app_callstacks", []),
|
|
204
|
+
"cpu_frequencies": valid_freqs,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def analyze_frames(frames_data):
|
|
210
|
+
issues = []
|
|
211
|
+
insights = []
|
|
212
|
+
|
|
213
|
+
total = frames_data.get("total_frames", 0)
|
|
214
|
+
dropped = frames_data.get("dropped_frames", 0)
|
|
215
|
+
dropped_pct = frames_data.get("dropped_pct", 0)
|
|
216
|
+
percentiles = frames_data.get("frame_duration_percentiles_ms", {})
|
|
217
|
+
jank_causes = frames_data.get("jank_causes", {})
|
|
218
|
+
|
|
219
|
+
if total == 0:
|
|
220
|
+
insights.append("Nenhum dado de frame encontrado no trace. O app pode não ter renderizado nada durante a coleta.")
|
|
221
|
+
return {"issues": issues, "insights": insights, "summary": frames_data}
|
|
222
|
+
|
|
223
|
+
insights.append(f"Total de frames analisados: {total}")
|
|
224
|
+
|
|
225
|
+
# Dropped frames
|
|
226
|
+
if dropped_pct >= THRESHOLDS["dropped_pct_critical"]:
|
|
227
|
+
issues.append({
|
|
228
|
+
"severity": "critical",
|
|
229
|
+
"category": "frames",
|
|
230
|
+
"title": f"Jank Crítico: {dropped_pct:.1f}% de Frames Dropados",
|
|
231
|
+
"detail": f"{dropped} de {total} frames não foram entregues no tempo ({dropped_pct:.1f}%). "
|
|
232
|
+
f"Usuário percebe stuttering evidente.",
|
|
233
|
+
"action": "Usar Perfetto UI para identificar qual fase (CPU, GPU, input) causa os drops. "
|
|
234
|
+
"Verificar operações síncronas na UI thread.",
|
|
235
|
+
})
|
|
236
|
+
elif dropped_pct >= THRESHOLDS["dropped_pct_warn"]:
|
|
237
|
+
issues.append({
|
|
238
|
+
"severity": "warning",
|
|
239
|
+
"category": "frames",
|
|
240
|
+
"title": f"Frames Dropados: {dropped_pct:.1f}%",
|
|
241
|
+
"detail": f"{dropped} frames dropados de {total} total.",
|
|
242
|
+
"action": "Investigar picos de CPU/GPU que coincidam com os drops no timeline.",
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
# Latência de frames
|
|
246
|
+
p95 = percentiles.get("p95", 0)
|
|
247
|
+
p99 = percentiles.get("p99", 0)
|
|
248
|
+
|
|
249
|
+
if p95 > THRESHOLDS["frame_p99_ms"]:
|
|
250
|
+
issues.append({
|
|
251
|
+
"severity": "critical",
|
|
252
|
+
"category": "frames",
|
|
253
|
+
"title": f"Alta Latência de Frame (P95 = {p95:.1f}ms)",
|
|
254
|
+
"detail": f"95% dos frames levam mais de {p95:.1f}ms para renderizar (threshold: 16.67ms). "
|
|
255
|
+
f"P99 = {p99:.1f}ms.",
|
|
256
|
+
"action": "Otimizar composição de layout (remover layers desnecessárias, usar hardware layers). "
|
|
257
|
+
"Verificar animações e overdraw.",
|
|
258
|
+
})
|
|
259
|
+
elif p95 > THRESHOLDS["frame_p95_ms"]:
|
|
260
|
+
issues.append({
|
|
261
|
+
"severity": "warning",
|
|
262
|
+
"category": "frames",
|
|
263
|
+
"title": f"Latência de Frame Elevada (P95 = {p95:.1f}ms)",
|
|
264
|
+
"detail": f"Frames no P95 excedem o budget de 16.67ms.",
|
|
265
|
+
"action": "Verificar render passes, bitmaps grandes e complexity de shaders.",
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
# Causas de jank
|
|
269
|
+
if jank_causes:
|
|
270
|
+
top_cause = max(jank_causes, key=jank_causes.get)
|
|
271
|
+
insights.append(f"Causa principal de jank: {top_cause} ({jank_causes[top_cause]} ocorrências)")
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
"issues": issues,
|
|
275
|
+
"insights": insights,
|
|
276
|
+
"summary": {
|
|
277
|
+
"total_frames": total,
|
|
278
|
+
"dropped_frames": dropped,
|
|
279
|
+
"dropped_pct": dropped_pct,
|
|
280
|
+
"frame_percentiles_ms": percentiles,
|
|
281
|
+
"jank_causes": jank_causes,
|
|
282
|
+
"jank_jvm_callstacks": frames_data.get("jank_jvm_callstacks", []),
|
|
283
|
+
"jank_app_callstacks": frames_data.get("jank_app_callstacks", []),
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def analyze_battery(battery_data, duration_seconds):
|
|
289
|
+
issues = []
|
|
290
|
+
insights = []
|
|
291
|
+
counters = battery_data.get("battery_counters", [])
|
|
292
|
+
power_rails = battery_data.get("power_rails", [])
|
|
293
|
+
|
|
294
|
+
# Charge drenada em microampere-horas
|
|
295
|
+
charge_counter = next((c for c in counters if c.get("counter_name") == "batt_charge_uah"), None)
|
|
296
|
+
if charge_counter:
|
|
297
|
+
delta_uah = abs(charge_counter.get("delta", 0))
|
|
298
|
+
delta_mc = delta_uah * 3.6 # uAh → mC (milliCoulombs)
|
|
299
|
+
if delta_mc > 0:
|
|
300
|
+
insights.append(f"Carga drenada: {delta_mc:.1f} mC em {duration_seconds:.0f}s")
|
|
301
|
+
drain_rate = delta_mc / duration_seconds if duration_seconds else 0
|
|
302
|
+
insights.append(f"Taxa de drain: {drain_rate:.2f} mC/s")
|
|
303
|
+
if drain_rate > 5.0:
|
|
304
|
+
issues.append({
|
|
305
|
+
"severity": "warning",
|
|
306
|
+
"category": "battery",
|
|
307
|
+
"title": "Alto Consumo de Bateria",
|
|
308
|
+
"detail": f"Taxa de drain de {drain_rate:.2f} mC/s durante o trace. "
|
|
309
|
+
f"Pode resultar em bateria drenando em horas.",
|
|
310
|
+
"action": "Verificar wake locks, sync periódico, GPS, e jobs em background.",
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
# Power rails
|
|
314
|
+
if power_rails:
|
|
315
|
+
top_rail = power_rails[0]
|
|
316
|
+
insights.append(f"Maior consumidor de energia: {top_rail.get('rail_name', 'N/A')} "
|
|
317
|
+
f"({top_rail.get('total_mw', 0):.1f} mW)")
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"issues": issues,
|
|
321
|
+
"insights": insights,
|
|
322
|
+
"summary": {
|
|
323
|
+
"battery_counters": counters,
|
|
324
|
+
"power_rails": power_rails[:5],
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def build_recommendations(all_issues):
|
|
330
|
+
"""Gera recomendações consolidadas ordenadas por severidade."""
|
|
331
|
+
severity_order = {"critical": 0, "warning": 1, "info": 2}
|
|
332
|
+
sorted_issues = sorted(all_issues, key=lambda x: severity_order.get(x["severity"], 99))
|
|
333
|
+
return [
|
|
334
|
+
{
|
|
335
|
+
"priority": i + 1,
|
|
336
|
+
"severity": issue["severity"],
|
|
337
|
+
"title": issue["title"],
|
|
338
|
+
"detail": issue["detail"],
|
|
339
|
+
"action": issue["action"],
|
|
340
|
+
"category": issue["category"],
|
|
341
|
+
}
|
|
342
|
+
for i, issue in enumerate(sorted_issues)
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def main():
|
|
347
|
+
parser = argparse.ArgumentParser(description="Analisa métricas do trace Perfetto")
|
|
348
|
+
parser.add_argument("--input", default="/tmp/perfetto_parsed.json", help="JSON parseado")
|
|
349
|
+
parser.add_argument("--output", default="/tmp/perfetto_insights.json", help="JSON de insights")
|
|
350
|
+
args = parser.parse_args()
|
|
351
|
+
|
|
352
|
+
if not os.path.exists(args.input):
|
|
353
|
+
print(f"ERRO: Arquivo não encontrado: {args.input}", file=sys.stderr)
|
|
354
|
+
sys.exit(1)
|
|
355
|
+
|
|
356
|
+
with open(args.input) as f:
|
|
357
|
+
data = json.load(f)
|
|
358
|
+
|
|
359
|
+
duration = data.get("trace_duration_seconds", 0)
|
|
360
|
+
|
|
361
|
+
print("Analisando memória...")
|
|
362
|
+
mem_analysis = analyze_memory(data.get("memory", {}))
|
|
363
|
+
|
|
364
|
+
print("Analisando CPU...")
|
|
365
|
+
cpu_analysis = analyze_cpu(data.get("cpu", {}))
|
|
366
|
+
|
|
367
|
+
print("Analisando frames...")
|
|
368
|
+
frame_analysis = analyze_frames(data.get("frames", {}))
|
|
369
|
+
|
|
370
|
+
print("Analisando battery...")
|
|
371
|
+
battery_analysis = analyze_battery(data.get("battery", {}), duration)
|
|
372
|
+
|
|
373
|
+
all_issues = (
|
|
374
|
+
mem_analysis["issues"]
|
|
375
|
+
+ cpu_analysis["issues"]
|
|
376
|
+
+ frame_analysis["issues"]
|
|
377
|
+
+ battery_analysis["issues"]
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
all_insights = (
|
|
381
|
+
mem_analysis["insights"]
|
|
382
|
+
+ cpu_analysis["insights"]
|
|
383
|
+
+ frame_analysis["insights"]
|
|
384
|
+
+ battery_analysis["insights"]
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
recommendations = build_recommendations(all_issues)
|
|
388
|
+
|
|
389
|
+
output = {
|
|
390
|
+
"trace_duration_seconds": duration,
|
|
391
|
+
"recommendations": recommendations,
|
|
392
|
+
"insights": all_insights,
|
|
393
|
+
"memory": mem_analysis["summary"],
|
|
394
|
+
"cpu": cpu_analysis["summary"],
|
|
395
|
+
"frames": frame_analysis["summary"],
|
|
396
|
+
"battery": battery_analysis["summary"],
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
with open(args.output, "w") as f:
|
|
400
|
+
json.dump(output, f, indent=2, default=str)
|
|
401
|
+
|
|
402
|
+
critical = sum(1 for r in recommendations if r["severity"] == "critical")
|
|
403
|
+
warnings = sum(1 for r in recommendations if r["severity"] == "warning")
|
|
404
|
+
print(f"\nAnálise concluída: {critical} críticos, {warnings} avisos → {args.output}")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
main()
|