perfetto-analyzer 1.0.0 → 1.0.1

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 CHANGED
@@ -1,22 +1,38 @@
1
1
  # perfetto-analyzer
2
2
 
3
- AI skill for Android performance analysis using [Perfetto](https://perfetto.dev).
3
+ > Skill de IA para análise de performance Android com [Perfetto](https://perfetto.dev) — funciona com Claude Code, Cursor, GitHub Copilot, Codex, Antigravity e Devin.
4
4
 
5
- Automatically installs into all supported AI agents on your machine.
5
+ Instale uma vez. Seu assistente de IA vira um especialista em performance Android: coleta traces Perfetto via `adb`, analisa callstacks de CPU, alocações de memória, timelines de frames e contadores de bateria, e gera um relatório markdown estruturado com recomendações acionáveis.
6
6
 
7
- ## Installation
7
+ ---
8
8
 
9
- ```bash
10
- # Run once — installs for all supported agents
11
- npx perfetto-analyzer install
9
+ ## Índice
12
10
 
13
- # Or install globally (auto-installs via postinstall)
14
- npm install -g perfetto-analyzer
15
- ```
11
+ - [Agentes de IA suportados](#agentes-de-ia-suportados)
12
+ - [Pré-requisitos](#pré-requisitos)
13
+ - [Instalação](#instalação)
14
+ - [Como usar](#como-usar)
15
+ - [Modo A — Coletar trace de um device conectado](#modo-a--coletar-trace-de-um-device-conectado)
16
+ - [Modo B — Analisar um arquivo .pb existente](#modo-b--analisar-um-arquivo-pb-existente)
17
+ - [Tipos de trace](#tipos-de-trace)
18
+ - [Entendendo o relatório](#entendendo-o-relatório)
19
+ - [Resumo Executivo](#resumo-executivo)
20
+ - [Memória](#memória)
21
+ - [CPU](#cpu)
22
+ - [Frames](#frames)
23
+ - [Bateria](#bateria)
24
+ - [Recomendações](#recomendações)
25
+ - [Referência de métricas](#referência-de-métricas)
26
+ - [Solução de problemas](#solução-de-problemas)
27
+ - [Desinstalar](#desinstalar)
28
+
29
+ ---
16
30
 
17
- ## Supported AI Agents
31
+ ## Agentes de IA suportados
18
32
 
19
- | Agent | Skills directory |
33
+ O instalador configura automaticamente a skill em todos os agentes encontrados na sua máquina:
34
+
35
+ | Agente | Diretório da skill |
20
36
  |---|---|
21
37
  | Claude Code | `~/.claude/skills/perfetto-analyzer/` |
22
38
  | GitHub Copilot | `~/.github/copilot/skills/perfetto-analyzer/` |
@@ -25,39 +41,386 @@ npm install -g perfetto-analyzer
25
41
  | Codex (OpenAI) | `~/.codex/skills/perfetto-analyzer/` |
26
42
  | Devin | `~/.devin/skills/perfetto-analyzer/` |
27
43
 
28
- Directories are created automatically if they don't exist.
44
+ Os diretórios são criados automaticamente caso não existam.
45
+
46
+ ---
47
+
48
+ ## Pré-requisitos
49
+
50
+ | Ferramenta | Como verificar | Instalação |
51
+ |---|---|---|
52
+ | `adb` | `adb version` | [Android SDK Platform-Tools](https://developer.android.com/tools/releases/platform-tools) |
53
+ | Python 3.9+ | `python3 --version` | [python.org](https://python.org) |
54
+ | Lib Python do Perfetto | `python3 -c "import perfetto"` | `pip install perfetto` |
29
55
 
30
- ## Uninstall
56
+ **Configuração do device:**
57
+ 1. Ative as **Opções do Desenvolvedor** no Android (toque 7 vezes em "Número da versão" nas configurações)
58
+ 2. Ative **Depuração USB** dentro das Opções do Desenvolvedor
59
+ 3. Conecte via USB e aceite a solicitação de autorização no device
60
+ 4. Verifique com `adb devices` — o device deve aparecer como `device` (não `offline` ou `unauthorized`)
61
+
62
+ ---
63
+
64
+ ## Instalação
65
+
66
+ ### Opção 1 — npx (sem instalação prévia)
31
67
 
32
68
  ```bash
33
- npx perfetto-analyzer uninstall
69
+ npx perfetto-analyzer install
70
+ ```
71
+
72
+ ### Opção 2 — instalação global
73
+
74
+ ```bash
75
+ npm install -g perfetto-analyzer
76
+ ```
77
+
78
+ O script `postinstall` copia os arquivos da skill automaticamente.
79
+
80
+ ### Verificar instalação
81
+
82
+ Após instalar, reinicie seu assistente de IA e pergunte:
83
+
84
+ > "Analise a performance do meu app Android"
85
+
86
+ O assistente deve pedir o nome do pacote, tipo de trace e duração — isso confirma que a skill está ativa.
87
+
88
+ ---
89
+
90
+ ## Como usar
91
+
92
+ ### Modo A — Coletar trace de um device conectado
93
+
94
+ Descreva o que quer medir em linguagem natural:
95
+
96
+ ```
97
+ Analise a performance de com.meuapp.debug — quero verificar jank na tela de feed
98
+ ```
99
+
100
+ ```
101
+ Meu app com.exemplo.app está usando muita memória. Colete um trace de 30 segundos.
102
+ ```
103
+
104
+ ```
105
+ O uso de CPU está alto em com.empresa.app, colete um trace de cpu por 15 segundos
106
+ ```
107
+
108
+ O assistente vai:
109
+
110
+ 1. **Verificar** se o device está conectado e o adb está funcionando
111
+ 2. **Checar** se o app está aberto — se não estiver, abre automaticamente via launcher
112
+ 3. **Coletar** o trace Perfetto (você verá uma contagem regressiva em tempo real)
113
+ 4. **Analisar** callstacks de CPU, alocações de memória, timelines de frames e contadores de bateria
114
+ 5. **Gerar** um relatório markdown completo com hotspots e recomendações
115
+
116
+ > **Durante a coleta:** interaja com o app normalmente — role, navegue, acione o fluxo lento que quer analisar. O trace captura o que acontece no device em tempo real.
117
+
118
+ Se não souber o package name do app, pergunte:
119
+
120
+ ```
121
+ Como descubro o package name do meu app?
122
+ ```
123
+
124
+ O assistente vai sugerir:
125
+
126
+ ```bash
127
+ adb shell pm list packages | grep <nome-do-app>
128
+ ```
129
+
130
+ ---
131
+
132
+ ### Modo B — Analisar um arquivo .pb existente
133
+
134
+ Se você já tem um arquivo de trace Perfetto (`.pb`):
135
+
136
+ ```
137
+ Analise este trace: ~/Downloads/meu_trace.pb
34
138
  ```
35
139
 
36
- ## What it does
140
+ ```
141
+ Tenho um trace perfetto em /tmp/trace.pb, consegue encontrar memory leaks?
142
+ ```
143
+
144
+ Nenhuma conexão com device é necessária — o assistente analisa o arquivo diretamente.
145
+
146
+ ---
147
+
148
+ ## Tipos de trace
149
+
150
+ Escolha o tipo de trace conforme o que quer investigar:
151
+
152
+ | Tipo | O que captura | Indicado para |
153
+ |---|---|---|
154
+ | `frames` | Timeline de frames, causas de jank, amostras de CPU durante frames dropados | Travadas, scroll lento, jank na UI |
155
+ | `memory` | Alocações de heap nativo, retenção de objetos Java/Kotlin | Memory leaks, uso alto de heap, GC pressure |
156
+ | `cpu` | Amostragem de CPU com callstacks completos (Java, Kotlin, nativo) | Operações lentas, gargalos de CPU, funções quentes |
157
+ | `battery` | Contadores de drain da bateria, power rails | Drain rápido de bateria, wake locks, atividade em background |
158
+ | `all` | Todos os anteriores combinados | Auditoria completa de performance |
159
+
160
+ **Durações recomendadas:**
161
+ - `frames` / `cpu`: 10–15 segundos (acione a interação lenta uma vez)
162
+ - `memory`: 30–60 segundos (navegue por várias telas para acumular alocações)
163
+ - `battery`: 60–120 segundos (precisa de tempo suficiente para medir drain significativo)
164
+
165
+ ---
37
166
 
38
- After installation, ask your AI assistant to analyze an Android app's performance:
167
+ ## Entendendo o relatório
39
168
 
40
- - **Collect a trace** from a connected device via `adb`
41
- - **Analyze CPU, memory, frames (jank), and battery** from a `.pb` Perfetto trace
42
- - **Generate a markdown report** with insights and actionable recommendations
169
+ O relatório é dividido em cinco seções. Veja o que cada uma significa.
43
170
 
44
- ### Prerequisites
171
+ ### Resumo Executivo
172
+
173
+ ```
174
+ ## Resumo Executivo
175
+ > 🔴 43% das amostras de CPU na main thread — operações bloqueantes detectadas na UI
176
+ > 🟡 12% de frames dropados (P95: 48ms) — jank visível para o usuário
177
+ > 🟢 Memória estável — nenhum padrão de leak detectado
178
+ ```
179
+
180
+ Lista ranqueada dos problemas mais críticos, do mais ao menos impactante. Comece aqui para decidir o que corrigir primeiro.
181
+
182
+ Ícones de severidade:
183
+ - 🔴 **Crítico** — impacta diretamente a experiência do usuário, corrija imediatamente
184
+ - 🟡 **Atenção** — problema perceptível, deve ser endereçado
185
+ - 🟢 **OK** — dentro dos thresholds aceitáveis
186
+
187
+ ---
188
+
189
+ ### Memória
190
+
191
+ ```
192
+ ## Memória
45
193
 
46
- - `adb` installed and in PATH (Android SDK Platform-Tools)
47
- - Python 3.9+
48
- - `pip install perfetto`
194
+ ### Maiores Alocadores (Heap Nativo)
195
+ | Callstack | Bytes não liberados | % do total |
196
+ |-----------------------------------|---------------------|------------|
197
+ | Bitmap.createBitmap (ImageLoader) | 18,4 MB | 34% |
198
+ | RenderScript.create | 8,1 MB | 15% |
49
199
 
50
- ## Usage example
200
+ ### Objetos Java/Kotlin Retidos (ART)
201
+ | Classe | Instâncias | Bytes |
202
+ |---------------------------|------------|---------|
203
+ | android.graphics.Bitmap | 847 | 42,3 MB |
204
+ | com.meuapp.cache.DataCache| 1 | 12,1 MB |
205
+
206
+ ### Alertas
207
+ - ⚠️ Bytes não liberados crescendo ao longo do trace — possível memory leak no ImageLoader
208
+ ```
209
+
210
+ **Conceitos-chave:**
211
+
212
+ | Termo | Significado |
213
+ |---|---|
214
+ | **Bytes não liberados** | Memória alocada e ainda não liberada — valores altos = possível leak |
215
+ | **Total de bytes** | Todas as alocações, incluindo as já liberadas — alto = muitos objetos temporários (GC pressure) |
216
+ | **Heap churn** | Razão `total / não liberado` > 10× = muitas alocações de curta duração → considere object pooling |
217
+ | **Dominators** | Objeto que mantém outro vivo na memória — Activity/Fragment como dominator = leak clássico |
218
+
219
+ **O que observar:**
220
+ - Bytes não liberados crescendo monotonicamente → memory leak
221
+ - A mesma classe aparecendo repetidamente no topo → hotspot de alocação
222
+ - `Activity` ou `Fragment` na lista de objetos retidos → problema de gerenciamento de ciclo de vida
223
+
224
+ ---
225
+
226
+ ### CPU
227
+
228
+ ```
229
+ ## CPU
230
+
231
+ ### Hotspots do App — Código Kotlin/Java
232
+ | Função | Thread | Amostras |
233
+ |-----------------------------------------------|-------------|----------|
234
+ | com.meuapp.feed.FeedAdapter.onBindViewHolder | main | 142 |
235
+ | com.meuapp.image.ImageDecoder.decode | AsyncTask#1 | 98 |
236
+
237
+ ### APIs Android Ativas na Main Thread
238
+ | Função | Amostras |
239
+ |-------------------------------------------------|----------|
240
+ | androidx.compose.runtime.Recomposer · recompose | 201 |
241
+ | android.graphics.Bitmap · createScaledBitmap | 87 |
242
+
243
+ ### Top Frames Nativos
244
+ | Função | Thread | Amostras |
245
+ |-----------------------|--------------|----------|
246
+ | libhwui.so!drawBitmap | RenderThread | 134 |
247
+ ```
248
+
249
+ **Conceitos-chave:**
250
+
251
+ | Termo | Significado |
252
+ |---|---|
253
+ | **Amostras** | Quantas vezes a CPU foi pega executando essa função — mais amostras = mais tempo gasto |
254
+ | **Main thread** | Thread de UI do Android — trabalho pesado aqui causa jank diretamente |
255
+ | **Hotspots do app** | Seu código (filtrado pelo package name) — as funções que você pode de fato alterar |
256
+ | **APIs Android** | Funções do Android/Compose/AndroidX — indicam em que seu código está gastando tempo |
257
+ | **Frames nativos** | Código C/C++ em libs do sistema — `libhwui` = renderização, `libart` = overhead do runtime |
258
+
259
+ **O que observar:**
260
+ - Funções do seu pacote na main thread → mova o trabalho pesado para uma coroutine/thread em background
261
+ - `Bitmap.createScaledBitmap` ou decodificação de imagem na main thread → use Glide/Coil com carregamento assíncrono
262
+ - Funções de recomposição do Compose dominando → verifique recomposições desnecessárias com `@Stable` / `remember`
263
+ - Funções de GC (`art::gc::`) → reduza criação de objetos temporários, use object pools
264
+
265
+ ---
266
+
267
+ ### Frames
268
+
269
+ ```
270
+ ## Frames
271
+
272
+ | Métrica | Valor | Threshold OK |
273
+ |-----------------|-----------|--------------|
274
+ | Total de frames | 1.842 | — |
275
+ | Frames dropados | 221 (12%) | < 1% |
276
+ | Frame P50 | 14ms | ≤ 16,67ms |
277
+ | Frame P95 | 48ms | ≤ 33,3ms |
278
+ | Frame P99 | 112ms | ≤ 50ms |
279
+
280
+ ### Causas de Jank
281
+ | Causa | Ocorrências |
282
+ |------------------------------------|-------------|
283
+ | App Deadline Missed | 187 |
284
+ | SurfaceFlinger GPU Deadline Missed | 34 |
285
+ ```
286
+
287
+ **Thresholds de frames dropados:**
288
+
289
+ | % Dropados | Avaliação |
290
+ |---|---|
291
+ | < 0,1% | Excelente |
292
+ | 0,1% – 1% | Bom |
293
+ | 1% – 5% | Aceitável (usuário pode perceber em cenários específicos) |
294
+ | > 5% | Ruim — jank visível |
295
+
296
+ **Thresholds de latência de frame (display 60 Hz):**
297
+
298
+ | Percentil | OK | Atenção | Crítico |
299
+ |---|---|---|---|
300
+ | P50 | ≤ 16,67ms | 16–33ms | > 33ms |
301
+ | P95 | ≤ 33,3ms | 33–50ms | > 50ms |
302
+ | P99 | ≤ 50ms | 50–100ms | > 100ms |
303
+
304
+ **Causas de jank explicadas:**
305
+
306
+ | Causa | Origem | Correção |
307
+ |---|---|---|
308
+ | `App Deadline Missed` | Seu app não terminou o frame a tempo | Reduza trabalho na UI thread, use `LaunchedEffect`, mova lógica para `ViewModel` |
309
+ | `SurfaceFlinger CPU Deadline Missed` | O compositor do sistema estava lento | Reduza o número de layers, evite overdraw |
310
+ | `SurfaceFlinger GPU Deadline Missed` | A renderização da GPU demorou muito | Simplifique shaders, reduza complexidade visual, use hardware layers |
311
+ | `Buffering` | App produz frames mais rápido do que o display consome | Geralmente benigno |
312
+
313
+ ---
314
+
315
+ ### Bateria
316
+
317
+ ```
318
+ ## Bateria
319
+
320
+ | Métrica | Valor |
321
+ |---------------------|----------|
322
+ | Carga drenada | 284 mC |
323
+ | Duração do trace | 60s |
324
+ | Taxa média de drain | 4,7 mC/s |
325
+
326
+ ### Top Consumidores
327
+ | Componente | Consumo estimado |
328
+ |------------|-----------------|
329
+ | CPU (big) | 180 mW |
330
+ | GPU | 95 mW |
331
+ | Memória | 40 mW |
332
+ ```
333
+
334
+ **Referência de consumo típico:**
335
+
336
+ | Estado do app | Consumo total típico |
337
+ |---|---|
338
+ | App em idle | 5–20 mW |
339
+ | App ativo (UI, rede) | 100–300 mW |
340
+ | Vídeo / jogo | 500–2000 mW |
341
+
342
+ **Contadores principais:**
343
+
344
+ | Contador | Significado |
345
+ |---|---|
346
+ | `batt_charge_uah` | Carga da bateria em µAh — o delta ao longo do trace = consumo |
347
+ | `batt_current_ua` | Corrente instantânea em µA — negativo = descarregando |
348
+ | Power rails (`cpu.big`, `gpu`, etc.) | Consumo por subsistema em mW (devices Qualcomm/Tensor) |
349
+
350
+ **O que observar:**
351
+ - `CPU (big)` dominando → operações computacionalmente caras nos cores de alto desempenho; considere `THREAD_PRIORITY_BACKGROUND` para trabalho não crítico
352
+ - Drain alto em repouso → wake locks, sync agendado, acesso a localização em background
353
+ - GPU alta mesmo em telas simples → overdraw, layouts complexos, animações desnecessárias
354
+
355
+ ---
356
+
357
+ ### Recomendações
358
+
359
+ ```
360
+ ## Recomendações
361
+
362
+ 1. **Mova a decodificação de imagens para fora da main thread** — `ImageDecoder.decode` foi chamado
363
+ 98 vezes na main thread, contribuindo para o P95 de 48ms. Use Coil ou Glide com carregamento
364
+ baseado em coroutines.
365
+
366
+ 2. **Reduza recomposições do Compose** — Recomposer mostra 201 amostras. Audite leituras de state
367
+ em composables, aplique `@Stable` em data classes e use `remember` para cálculos custosos.
368
+
369
+ 3. **Corrija memory leak no ImageLoader** — Bytes não liberados cresceram de 12 MB para 31 MB
370
+ durante o trace. Verifique se `BitmapPool.clear()` é chamado em `onStop()` / `onDestroy()`.
371
+ ```
372
+
373
+ Cada recomendação contém:
374
+ - **Título** — o que corrigir
375
+ - **Evidência** — o que o trace mostrou e a métrica específica que acionou o alerta
376
+ - **Ação** — um passo concreto para resolver o problema
377
+
378
+ ---
379
+
380
+ ## Referência de métricas
381
+
382
+ ### Checklist de triagem rápida
383
+
384
+ Use esta sequência ao receber um relatório pela primeira vez:
385
+
386
+ 1. **Frames** → `% dropados > 1%`? Se sim, verifique a timeline de CPU e as causas de jank.
387
+ 2. **CPU** → A main thread está acima de 20% do total de CPU? Se sim, procure chamadas bloqueantes.
388
+ 3. **Memória** → Os bytes não liberados estão crescendo ao longo do tempo? Se sim, há um leak.
389
+ 4. **Bateria** → A taxa de drain está acima do esperado para o estado do app? Se sim, verifique wake locks e atividade em background.
390
+
391
+ ### Perfetto UI
392
+
393
+ Para investigação profunda além do relatório, abra o arquivo `.pb` em [perfetto.dev/ui](https://perfetto.dev/ui):
394
+ - **Flame chart** → visualize a hierarquia completa de callstacks
395
+ - **Thread state** → veja exatamente quando as threads estão rodando, dormindo ou bloqueadas
396
+ - **SQL query** → execute queries customizadas via Trace Processor integrado
397
+
398
+ ---
399
+
400
+ ## Solução de problemas
401
+
402
+ | Erro | Causa | Solução |
403
+ |---|---|---|
404
+ | `no devices/emulators found` | Device não conectado ou depuração USB desativada | Execute `adb devices`, verifique as Opções do Desenvolvedor |
405
+ | `offline` em `adb devices` | Autorização não aceita no device | Desconecte e reconecte o USB, aceite o prompt no device |
406
+ | `ImportError: No module named 'perfetto'` | Biblioteca Python não instalada | `pip install perfetto` |
407
+ | `Error opening trace file` | Arquivo `.pb` corrompido ou versão incompatível | Valide o arquivo em [perfetto.dev/ui](https://perfetto.dev/ui) |
408
+ | Trace coletado mas dados zerados | Daemon `traced` não está rodando | `adb shell setprop persist.traced.enable 1 && adb shell start traced` |
409
+ | `permission denied` no device | App não é debuggable ou sem root | Use um build debug, ou `adb shell run-as <package>` |
410
+ | App não abre automaticamente | Package name incorreto ou app não instalado | Verifique com `adb shell pm list packages \| grep <nome>` |
411
+
412
+ ---
413
+
414
+ ## Desinstalar
415
+
416
+ ```bash
417
+ npx perfetto-analyzer uninstall
418
+ ```
51
419
 
52
- > "Analyze the performance of com.myapp.debug, collect a 30s frames trace"
420
+ Remove a skill de todos os diretórios dos agentes suportados.
53
421
 
54
- The skill will:
55
- 1. Verify the device is connected
56
- 2. Launch the app if it isn't running
57
- 3. Collect the Perfetto trace
58
- 4. Parse callstacks and frame timelines
59
- 5. Generate a report with hotspots and jank causes
422
+ ---
60
423
 
61
- ## License
424
+ ## Licença
62
425
 
63
426
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perfetto-analyzer",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI skill for Android performance analysis using Perfetto — works with Claude Code, Cursor, GitHub Copilot, Codex, Antigravity and Devin",
5
5
  "bin": {
6
6
  "perfetto-analyzer": "bin/cli.js"
@@ -11,15 +11,26 @@ data_sources {
11
11
  config {
12
12
  name: "android.heapprofd"
13
13
  heapprofd_config {
14
- sampling_interval_bytes: 4096
14
+ sampling_interval_bytes: 8192
15
15
  process_cmdline: "__PACKAGE__"
16
16
  shmem_size_bytes: 8388608
17
- block_client: true
17
+ block_client: false
18
18
  all_heaps: true
19
19
  }
20
20
  }
21
21
  }
22
22
 
23
+ # Heap dump Java/Kotlin (ART) — objetos retidos e grafo de referências
24
+ data_sources {
25
+ config {
26
+ name: "android.java_hprof"
27
+ java_hprof_config {
28
+ process_cmdline: "__PACKAGE__"
29
+ dump_smaps: true
30
+ }
31
+ }
32
+ }
33
+
23
34
  # --- CPU ---
24
35
  data_sources {
25
36
  config {
@@ -10,10 +10,10 @@ data_sources {
10
10
  config {
11
11
  name: "android.heapprofd"
12
12
  heapprofd_config {
13
- sampling_interval_bytes: 4096
13
+ sampling_interval_bytes: 8192
14
14
  process_cmdline: "__PACKAGE__"
15
15
  shmem_size_bytes: 8388608
16
- block_client: true
16
+ block_client: false
17
17
  all_heaps: true
18
18
  }
19
19
  }
@@ -174,7 +174,7 @@ def generate_report(data, package, trace_type):
174
174
  lines += ["| Processo | Tempo CPU (s) | Context Switches |"]
175
175
  lines += ["|---|---|---|"]
176
176
  for proc in top_procs[:10]:
177
- name = proc.get("process_name", "?")[:50]
177
+ name = (proc.get("process_name") or "?")[:50]
178
178
  t = proc.get("total_cpu_seconds", 0)
179
179
  sw = proc.get("context_switches", 0)
180
180
  lines.append(f"| `{name}` | {t:.3f}s | {sw:,} |")
@@ -209,7 +209,7 @@ def generate_report(data, package, trace_type):
209
209
  lines += ["|---|---|---|"]
210
210
  for cs in app_callstacks[:20]:
211
211
  fn = _shorten_jvm_name(cs.get("function_name", "?"))
212
- thread = cs.get("thread_name", "?")[:25]
212
+ thread = (cs.get("thread_name") or "?")[:25]
213
213
  count = cs.get("sample_count", 0)
214
214
  lines.append(f"| `{fn}` | `{thread}` | {count} |")
215
215
  lines.append("")
@@ -225,7 +225,7 @@ def generate_report(data, package, trace_type):
225
225
  fn = cs.get("function_name", "?")
226
226
  fn = _demangle(fn)
227
227
  fn = fn[:60] if len(fn) > 60 else fn
228
- thread = cs.get("thread_name", "?")[:25]
228
+ thread = (cs.get("thread_name") or "?")[:25]
229
229
  count = cs.get("sample_count", 0)
230
230
  lines.append(f"| `{fn}` | `{thread}` | {count} |")
231
231
  lines.append("")
@@ -302,7 +302,7 @@ def generate_report(data, package, trace_type):
302
302
  lines += ["|---|---|---|"]
303
303
  for j in jank_app[:15]:
304
304
  short = _shorten_jvm_name(j.get("function_name", "?"))
305
- thread = j.get("thread_name", "?")[:25]
305
+ thread = (j.get("thread_name") or "?")[:25]
306
306
  lines.append(f"| `{short}` | `{thread}` | {j.get('sample_count', 0)} |")
307
307
  lines.append("")
308
308
  else: