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,469 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Parse de um arquivo .pb do Perfetto usando a Trace Processor Python API."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def try_import():
|
|
11
|
+
try:
|
|
12
|
+
from perfetto.trace_processor import TraceProcessor, TraceProcessorConfig
|
|
13
|
+
return TraceProcessor, TraceProcessorConfig
|
|
14
|
+
except ImportError:
|
|
15
|
+
print("ERRO: Biblioteca 'perfetto' não instalada.", file=sys.stderr)
|
|
16
|
+
print("Execute: pip install perfetto", file=sys.stderr)
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def query_to_dicts(tp, sql):
|
|
21
|
+
"""Executa SQL e retorna lista de dicts. Retorna [] em caso de erro."""
|
|
22
|
+
try:
|
|
23
|
+
result = tp.query(sql)
|
|
24
|
+
rows = []
|
|
25
|
+
for row in result:
|
|
26
|
+
rows.append({col: getattr(row, col) for col in result.column_names})
|
|
27
|
+
return rows
|
|
28
|
+
except Exception as e:
|
|
29
|
+
# Tabela não existe neste trace (ex: heap_profile sem heapprofd)
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_memory(tp):
|
|
34
|
+
native_allocs = query_to_dicts(tp, """
|
|
35
|
+
SELECT
|
|
36
|
+
spf.name AS function_name,
|
|
37
|
+
spf.mapping_name,
|
|
38
|
+
SUM(CASE WHEN hpa.size > 0 THEN hpa.size ELSE 0 END) AS alloc_bytes,
|
|
39
|
+
SUM(CASE WHEN hpa.size < 0 THEN -hpa.size ELSE 0 END) AS freed_bytes,
|
|
40
|
+
SUM(CASE WHEN hpa.count > 0 THEN hpa.count ELSE 0 END) AS alloc_count
|
|
41
|
+
FROM heap_profile_allocation hpa
|
|
42
|
+
JOIN stack_profile_callsite spc ON spc.id = hpa.callsite_id
|
|
43
|
+
JOIN stack_profile_frame spf ON spf.id = spc.frame_id
|
|
44
|
+
GROUP BY spf.name, spf.mapping_name
|
|
45
|
+
ORDER BY alloc_bytes DESC
|
|
46
|
+
LIMIT 20
|
|
47
|
+
""")
|
|
48
|
+
|
|
49
|
+
for alloc in native_allocs:
|
|
50
|
+
alloc["unreleased_bytes"] = alloc.get("alloc_bytes", 0) - alloc.get("freed_bytes", 0)
|
|
51
|
+
|
|
52
|
+
art_objects = query_to_dicts(tp, """
|
|
53
|
+
SELECT
|
|
54
|
+
hgc.name AS type_name,
|
|
55
|
+
COUNT(*) AS instance_count,
|
|
56
|
+
SUM(hgo.self_size) AS total_size
|
|
57
|
+
FROM heap_graph_object hgo
|
|
58
|
+
JOIN heap_graph_class hgc ON hgc.id = hgo.type_id
|
|
59
|
+
WHERE hgo.reachable = 1
|
|
60
|
+
GROUP BY hgc.name
|
|
61
|
+
ORDER BY total_size DESC
|
|
62
|
+
LIMIT 20
|
|
63
|
+
""")
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"native_top_allocators": native_allocs,
|
|
67
|
+
"art_retained_objects": art_objects,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def extract_cpu(tp):
|
|
72
|
+
sched_summary = query_to_dicts(tp, """
|
|
73
|
+
SELECT
|
|
74
|
+
p.name AS process_name,
|
|
75
|
+
SUM(sched.dur) / 1e9 AS total_cpu_seconds,
|
|
76
|
+
COUNT(*) AS context_switches
|
|
77
|
+
FROM sched
|
|
78
|
+
JOIN thread t ON t.utid = sched.utid
|
|
79
|
+
JOIN process p ON p.upid = t.upid
|
|
80
|
+
WHERE p.name IS NOT NULL
|
|
81
|
+
GROUP BY p.name
|
|
82
|
+
ORDER BY total_cpu_seconds DESC
|
|
83
|
+
LIMIT 15
|
|
84
|
+
""")
|
|
85
|
+
|
|
86
|
+
cpu_freq = query_to_dicts(tp, """
|
|
87
|
+
SELECT
|
|
88
|
+
cct.cpu,
|
|
89
|
+
AVG(c.value) AS avg_freq_mhz,
|
|
90
|
+
MAX(c.value) AS max_freq_mhz
|
|
91
|
+
FROM counter c
|
|
92
|
+
JOIN cpu_counter_track cct ON cct.id = c.track_id
|
|
93
|
+
WHERE cct.name = 'cpufreq'
|
|
94
|
+
GROUP BY cct.cpu
|
|
95
|
+
ORDER BY cct.cpu
|
|
96
|
+
""")
|
|
97
|
+
|
|
98
|
+
# Top frame nativo de cada sample (contexto de baixo nível)
|
|
99
|
+
top_callstacks = query_to_dicts(tp, """
|
|
100
|
+
SELECT
|
|
101
|
+
spf.name AS function_name,
|
|
102
|
+
t.name AS thread_name,
|
|
103
|
+
p.name AS process_name,
|
|
104
|
+
COUNT(*) AS sample_count
|
|
105
|
+
FROM perf_sample ps
|
|
106
|
+
JOIN stack_profile_callsite spc ON spc.id = ps.callsite_id
|
|
107
|
+
JOIN stack_profile_frame spf ON spf.id = spc.frame_id
|
|
108
|
+
LEFT JOIN thread t ON t.utid = ps.utid
|
|
109
|
+
LEFT JOIN process p ON p.upid = t.upid
|
|
110
|
+
GROUP BY spf.name, t.name, p.name
|
|
111
|
+
ORDER BY sample_count DESC
|
|
112
|
+
LIMIT 20
|
|
113
|
+
""")
|
|
114
|
+
|
|
115
|
+
# Frames Kotlin/Java em toda a cadeia do callsite (identifica ofensores reais)
|
|
116
|
+
jvm_callstacks = query_to_dicts(tp, """
|
|
117
|
+
WITH RECURSIVE callsite_chain(sample_id, frame_id, parent_id, depth) AS (
|
|
118
|
+
SELECT ps.id, spc.frame_id, spc.parent_id, 1
|
|
119
|
+
FROM perf_sample ps
|
|
120
|
+
JOIN stack_profile_callsite spc ON spc.id = ps.callsite_id
|
|
121
|
+
UNION ALL
|
|
122
|
+
SELECT cc.sample_id, spc.frame_id, spc.parent_id, cc.depth + 1
|
|
123
|
+
FROM callsite_chain cc
|
|
124
|
+
JOIN stack_profile_callsite spc ON spc.id = cc.parent_id
|
|
125
|
+
WHERE cc.depth < 60
|
|
126
|
+
)
|
|
127
|
+
SELECT
|
|
128
|
+
spf.name AS function_name,
|
|
129
|
+
spm.name AS mapping_name,
|
|
130
|
+
COUNT(DISTINCT cc.sample_id) AS sample_count
|
|
131
|
+
FROM callsite_chain cc
|
|
132
|
+
JOIN stack_profile_frame spf ON spf.id = cc.frame_id
|
|
133
|
+
LEFT JOIN stack_profile_mapping spm ON spm.id = spf.mapping
|
|
134
|
+
WHERE spf.name LIKE 'com.%'
|
|
135
|
+
OR spf.name LIKE 'kotlin.%'
|
|
136
|
+
OR spf.name LIKE 'androidx.%'
|
|
137
|
+
OR spf.name LIKE 'org.jetbrains.%'
|
|
138
|
+
GROUP BY spf.name, spm.name
|
|
139
|
+
ORDER BY sample_count DESC
|
|
140
|
+
LIMIT 40
|
|
141
|
+
""")
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"scheduling_summary": sched_summary,
|
|
145
|
+
"cpu_frequency": cpu_freq,
|
|
146
|
+
"top_cpu_callstacks": top_callstacks,
|
|
147
|
+
"jvm_callstacks": jvm_callstacks,
|
|
148
|
+
# preenchido em main() após saber o --package
|
|
149
|
+
"app_callstacks": [],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def extract_frames(tp):
|
|
154
|
+
frames = query_to_dicts(tp, """
|
|
155
|
+
SELECT
|
|
156
|
+
aft.layer_name,
|
|
157
|
+
aft.present_type,
|
|
158
|
+
aft.on_time_finish,
|
|
159
|
+
aft.jank_type,
|
|
160
|
+
(aft.ts_end - aft.ts) / 1e6 AS duration_ms
|
|
161
|
+
FROM actual_frame_timeline_slice aft
|
|
162
|
+
ORDER BY aft.ts
|
|
163
|
+
LIMIT 5000
|
|
164
|
+
""")
|
|
165
|
+
|
|
166
|
+
if not frames:
|
|
167
|
+
frames = query_to_dicts(tp, """
|
|
168
|
+
SELECT
|
|
169
|
+
s.name,
|
|
170
|
+
s.dur / 1e6 AS duration_ms
|
|
171
|
+
FROM slice s
|
|
172
|
+
WHERE s.name IN ('Choreographer#doFrame', 'DrawFrame', 'doFrame')
|
|
173
|
+
ORDER BY s.ts
|
|
174
|
+
LIMIT 5000
|
|
175
|
+
""")
|
|
176
|
+
|
|
177
|
+
total = len(frames)
|
|
178
|
+
durations = [f.get("duration_ms", 0) for f in frames if f.get("duration_ms")]
|
|
179
|
+
dropped = sum(
|
|
180
|
+
1 for f in frames
|
|
181
|
+
if f.get("on_time_finish") == 0 or f.get("duration_ms", 0) > 16.67
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
percentiles = {}
|
|
185
|
+
if durations:
|
|
186
|
+
s = sorted(durations)
|
|
187
|
+
n = len(s)
|
|
188
|
+
percentiles = {
|
|
189
|
+
"p50": round(s[int(n * 0.50)], 2),
|
|
190
|
+
"p95": round(s[int(n * 0.95)], 2),
|
|
191
|
+
"p99": round(s[min(int(n * 0.99), n - 1)], 2),
|
|
192
|
+
"max": round(max(s), 2),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
jank_causes = {}
|
|
196
|
+
for f in frames:
|
|
197
|
+
j = str(f.get("jank_type", ""))
|
|
198
|
+
if j and j not in ("None", "0", ""):
|
|
199
|
+
jank_causes[j] = jank_causes.get(j, 0) + 1
|
|
200
|
+
|
|
201
|
+
# Callstacks JVM que ocorreram durante frames janked — identifica o ofensor do jank
|
|
202
|
+
jank_jvm_callstacks = query_to_dicts(tp, """
|
|
203
|
+
WITH jank_windows AS (
|
|
204
|
+
SELECT aft.ts AS frame_start, aft.ts_end AS frame_end
|
|
205
|
+
FROM actual_frame_timeline_slice aft
|
|
206
|
+
WHERE aft.on_time_finish = 0
|
|
207
|
+
),
|
|
208
|
+
jank_samples AS (
|
|
209
|
+
SELECT DISTINCT ps.id AS sample_id
|
|
210
|
+
FROM perf_sample ps
|
|
211
|
+
JOIN jank_windows jw ON ps.ts >= jw.frame_start AND ps.ts <= jw.frame_end
|
|
212
|
+
),
|
|
213
|
+
callsite_chain(sample_id, frame_id, parent_id, depth) AS (
|
|
214
|
+
SELECT js.sample_id, spc.frame_id, spc.parent_id, 1
|
|
215
|
+
FROM jank_samples js
|
|
216
|
+
JOIN perf_sample ps ON ps.id = js.sample_id
|
|
217
|
+
JOIN stack_profile_callsite spc ON spc.id = ps.callsite_id
|
|
218
|
+
UNION ALL
|
|
219
|
+
SELECT cc.sample_id, spc.frame_id, spc.parent_id, cc.depth + 1
|
|
220
|
+
FROM callsite_chain cc
|
|
221
|
+
JOIN stack_profile_callsite spc ON spc.id = cc.parent_id
|
|
222
|
+
WHERE cc.depth < 60
|
|
223
|
+
)
|
|
224
|
+
SELECT
|
|
225
|
+
spf.name AS function_name,
|
|
226
|
+
COUNT(DISTINCT cc.sample_id) AS sample_count
|
|
227
|
+
FROM callsite_chain cc
|
|
228
|
+
JOIN stack_profile_frame spf ON spf.id = cc.frame_id
|
|
229
|
+
WHERE spf.name LIKE 'com.%'
|
|
230
|
+
OR spf.name LIKE 'kotlin.%'
|
|
231
|
+
OR spf.name LIKE 'androidx.%'
|
|
232
|
+
OR spf.name LIKE 'org.jetbrains.%'
|
|
233
|
+
GROUP BY spf.name
|
|
234
|
+
ORDER BY sample_count DESC
|
|
235
|
+
LIMIT 20
|
|
236
|
+
""")
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"total_frames": total,
|
|
240
|
+
"dropped_frames": dropped,
|
|
241
|
+
"dropped_pct": round(dropped / total * 100, 2) if total else 0,
|
|
242
|
+
"frame_duration_percentiles_ms": percentiles,
|
|
243
|
+
"jank_causes": jank_causes,
|
|
244
|
+
"jank_jvm_callstacks": jank_jvm_callstacks,
|
|
245
|
+
# preenchido em main() após saber o --package
|
|
246
|
+
"jank_app_callstacks": [],
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def extract_battery(tp):
|
|
251
|
+
battery_counters = query_to_dicts(tp, """
|
|
252
|
+
SELECT
|
|
253
|
+
ct.name AS counter_name,
|
|
254
|
+
MIN(c.value) AS min_value,
|
|
255
|
+
MAX(c.value) AS max_value,
|
|
256
|
+
MAX(c.value) - MIN(c.value) AS delta
|
|
257
|
+
FROM counter c
|
|
258
|
+
JOIN counter_track ct ON ct.id = c.track_id
|
|
259
|
+
WHERE ct.name IN (
|
|
260
|
+
'batt_charge_uah', 'batt_capacity_pct', 'batt_current_ua',
|
|
261
|
+
'batt_voltage_uv', 'BatteryStats.Batt.Charge.uah'
|
|
262
|
+
)
|
|
263
|
+
GROUP BY ct.name
|
|
264
|
+
""")
|
|
265
|
+
|
|
266
|
+
power_rails = query_to_dicts(tp, """
|
|
267
|
+
SELECT
|
|
268
|
+
ct.name AS rail_name,
|
|
269
|
+
SUM(c.value) / 1e3 AS total_mw
|
|
270
|
+
FROM counter c
|
|
271
|
+
JOIN counter_track ct ON ct.id = c.track_id
|
|
272
|
+
WHERE ct.name LIKE 'power.rails.%'
|
|
273
|
+
GROUP BY ct.name
|
|
274
|
+
ORDER BY total_mw DESC
|
|
275
|
+
LIMIT 10
|
|
276
|
+
""")
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"battery_counters": battery_counters,
|
|
280
|
+
"power_rails": power_rails,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_trace_duration(tp):
|
|
285
|
+
for table in ("sched", "slice", "counter"):
|
|
286
|
+
rows = query_to_dicts(tp, f"SELECT (MAX(ts) - MIN(ts)) / 1e9 AS duration_seconds FROM {table}")
|
|
287
|
+
if rows and rows[0].get("duration_seconds"):
|
|
288
|
+
return rows[0]["duration_seconds"]
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _app_prefix(package):
|
|
293
|
+
"""Deriva o prefixo de pacote para filtrar classes do app.
|
|
294
|
+
|
|
295
|
+
'com.example.app.debug' → 'com.example.app'
|
|
296
|
+
Remove sufixos de build variant conhecidos para que o LIKE funcione
|
|
297
|
+
contra nomes de classe como 'com.example.app.feature.HomeScreen'.
|
|
298
|
+
"""
|
|
299
|
+
BUILD_SUFFIXES = {"debug", "release", "staging", "demo", "qa", "prod", "internal", "beta"}
|
|
300
|
+
parts = package.split(".")
|
|
301
|
+
while parts and parts[-1].lower() in BUILD_SUFFIXES:
|
|
302
|
+
parts.pop()
|
|
303
|
+
return ".".join(parts)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def extract_app_callstacks(tp, package):
|
|
307
|
+
"""Para cada sample de CPU, encontra o frame mais superficial que pertence ao app.
|
|
308
|
+
|
|
309
|
+
Isso responde: 'qual função do MEU código estava ativa quando a amostra foi colhida?'
|
|
310
|
+
O frame mais superficial (menor depth) é o mais próximo de onde a CPU estava,
|
|
311
|
+
dentro do código do app — ou seja, o ponto de entrada que chamou as APIs do framework.
|
|
312
|
+
"""
|
|
313
|
+
if not package:
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
prefix = _app_prefix(package)
|
|
317
|
+
|
|
318
|
+
return query_to_dicts(tp, f"""
|
|
319
|
+
WITH RECURSIVE callsite_chain(sample_id, frame_id, parent_id, depth) AS (
|
|
320
|
+
SELECT ps.id, spc.frame_id, spc.parent_id, 1
|
|
321
|
+
FROM perf_sample ps
|
|
322
|
+
JOIN thread t ON t.utid = ps.utid
|
|
323
|
+
JOIN process p ON p.upid = t.upid
|
|
324
|
+
JOIN stack_profile_callsite spc ON spc.id = ps.callsite_id
|
|
325
|
+
WHERE p.name = '{package}'
|
|
326
|
+
UNION ALL
|
|
327
|
+
SELECT cc.sample_id, spc.frame_id, spc.parent_id, cc.depth + 1
|
|
328
|
+
FROM callsite_chain cc
|
|
329
|
+
JOIN stack_profile_callsite spc ON spc.id = cc.parent_id
|
|
330
|
+
WHERE cc.depth < 60
|
|
331
|
+
),
|
|
332
|
+
top_app AS (
|
|
333
|
+
SELECT cc.sample_id, MIN(cc.depth) AS min_depth
|
|
334
|
+
FROM callsite_chain cc
|
|
335
|
+
JOIN stack_profile_frame spf ON spf.id = cc.frame_id
|
|
336
|
+
WHERE spf.name LIKE '{prefix}.%'
|
|
337
|
+
GROUP BY cc.sample_id
|
|
338
|
+
)
|
|
339
|
+
SELECT
|
|
340
|
+
spf.name AS function_name,
|
|
341
|
+
t.name AS thread_name,
|
|
342
|
+
COUNT(DISTINCT cc.sample_id) AS sample_count
|
|
343
|
+
FROM callsite_chain cc
|
|
344
|
+
JOIN top_app ta ON ta.sample_id = cc.sample_id AND cc.depth = ta.min_depth
|
|
345
|
+
JOIN stack_profile_frame spf ON spf.id = cc.frame_id
|
|
346
|
+
JOIN perf_sample ps ON ps.id = cc.sample_id
|
|
347
|
+
JOIN thread t ON t.utid = ps.utid
|
|
348
|
+
GROUP BY spf.name, t.name
|
|
349
|
+
ORDER BY sample_count DESC
|
|
350
|
+
LIMIT 25
|
|
351
|
+
""")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main():
|
|
355
|
+
parser = argparse.ArgumentParser(description="Parse de trace Perfetto")
|
|
356
|
+
parser.add_argument("--input", required=True, help="Arquivo .pb")
|
|
357
|
+
parser.add_argument("--output", default="/tmp/perfetto_parsed.json", help="JSON de saída")
|
|
358
|
+
parser.add_argument("--package", default="", help="Package do app (ex: com.example.app)")
|
|
359
|
+
args = parser.parse_args()
|
|
360
|
+
|
|
361
|
+
if not os.path.exists(args.input):
|
|
362
|
+
print(f"ERRO: Arquivo não encontrado: {args.input}", file=sys.stderr)
|
|
363
|
+
sys.exit(1)
|
|
364
|
+
|
|
365
|
+
TraceProcessor, TraceProcessorConfig = try_import()
|
|
366
|
+
|
|
367
|
+
size_mb = os.path.getsize(args.input) / 1024 / 1024
|
|
368
|
+
print(f"Trace: {args.input} ({size_mb:.1f} MB)")
|
|
369
|
+
|
|
370
|
+
# load_timeout precisa ser proporcional ao tamanho do trace
|
|
371
|
+
# padrão de 2s é insuficiente para traces > ~5 MB
|
|
372
|
+
timeout_seconds = max(60, int(size_mb * 2))
|
|
373
|
+
print(f"Abrindo trace (timeout: {timeout_seconds}s)...")
|
|
374
|
+
|
|
375
|
+
config = TraceProcessorConfig(load_timeout=timeout_seconds)
|
|
376
|
+
try:
|
|
377
|
+
tp = TraceProcessor(trace=args.input, config=config)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
print(f"ERRO ao abrir trace: {e}", file=sys.stderr)
|
|
380
|
+
print("Verifique se o arquivo .pb não está corrompido.", file=sys.stderr)
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
print("Extraindo memória...")
|
|
384
|
+
memory = extract_memory(tp)
|
|
385
|
+
|
|
386
|
+
print("Extraindo CPU...")
|
|
387
|
+
cpu = extract_cpu(tp)
|
|
388
|
+
|
|
389
|
+
print("Extraindo frames...")
|
|
390
|
+
frames = extract_frames(tp)
|
|
391
|
+
|
|
392
|
+
print("Extraindo battery...")
|
|
393
|
+
battery = extract_battery(tp)
|
|
394
|
+
|
|
395
|
+
duration = get_trace_duration(tp)
|
|
396
|
+
|
|
397
|
+
if args.package:
|
|
398
|
+
print("Extraindo callstacks do app...")
|
|
399
|
+
app_stacks = extract_app_callstacks(tp, args.package)
|
|
400
|
+
cpu["app_callstacks"] = app_stacks
|
|
401
|
+
|
|
402
|
+
# Para frames janked: mesma lógica mas restrita à janela de tempo do jank
|
|
403
|
+
prefix = _app_prefix(args.package)
|
|
404
|
+
pkg = args.package
|
|
405
|
+
jank_app = query_to_dicts(tp, f"""
|
|
406
|
+
WITH jank_windows AS (
|
|
407
|
+
SELECT aft.ts AS frame_start, aft.ts_end AS frame_end
|
|
408
|
+
FROM actual_frame_timeline_slice aft
|
|
409
|
+
WHERE aft.on_time_finish = 0
|
|
410
|
+
),
|
|
411
|
+
jank_samples AS (
|
|
412
|
+
SELECT DISTINCT ps.id AS sample_id
|
|
413
|
+
FROM perf_sample ps
|
|
414
|
+
JOIN thread t ON t.utid = ps.utid
|
|
415
|
+
JOIN process p ON p.upid = t.upid
|
|
416
|
+
JOIN jank_windows jw ON ps.ts >= jw.frame_start AND ps.ts <= jw.frame_end
|
|
417
|
+
WHERE p.name = '{pkg}'
|
|
418
|
+
),
|
|
419
|
+
callsite_chain(sample_id, frame_id, parent_id, depth) AS (
|
|
420
|
+
SELECT js.sample_id, spc.frame_id, spc.parent_id, 1
|
|
421
|
+
FROM jank_samples js
|
|
422
|
+
JOIN perf_sample ps ON ps.id = js.sample_id
|
|
423
|
+
JOIN stack_profile_callsite spc ON spc.id = ps.callsite_id
|
|
424
|
+
UNION ALL
|
|
425
|
+
SELECT cc.sample_id, spc.frame_id, spc.parent_id, cc.depth + 1
|
|
426
|
+
FROM callsite_chain cc
|
|
427
|
+
JOIN stack_profile_callsite spc ON spc.id = cc.parent_id
|
|
428
|
+
WHERE cc.depth < 60
|
|
429
|
+
),
|
|
430
|
+
top_app AS (
|
|
431
|
+
SELECT cc.sample_id, MIN(cc.depth) AS min_depth
|
|
432
|
+
FROM callsite_chain cc
|
|
433
|
+
JOIN stack_profile_frame spf ON spf.id = cc.frame_id
|
|
434
|
+
WHERE spf.name LIKE '{prefix}.%'
|
|
435
|
+
GROUP BY cc.sample_id
|
|
436
|
+
)
|
|
437
|
+
SELECT
|
|
438
|
+
spf.name AS function_name,
|
|
439
|
+
t.name AS thread_name,
|
|
440
|
+
COUNT(DISTINCT cc.sample_id) AS sample_count
|
|
441
|
+
FROM callsite_chain cc
|
|
442
|
+
JOIN top_app ta ON ta.sample_id = cc.sample_id AND cc.depth = ta.min_depth
|
|
443
|
+
JOIN stack_profile_frame spf ON spf.id = cc.frame_id
|
|
444
|
+
JOIN perf_sample ps ON ps.id = cc.sample_id
|
|
445
|
+
JOIN thread t ON t.utid = ps.utid
|
|
446
|
+
GROUP BY spf.name, t.name
|
|
447
|
+
ORDER BY sample_count DESC
|
|
448
|
+
LIMIT 20
|
|
449
|
+
""")
|
|
450
|
+
frames["jank_app_callstacks"] = jank_app
|
|
451
|
+
|
|
452
|
+
tp.close()
|
|
453
|
+
|
|
454
|
+
output = {
|
|
455
|
+
"trace_duration_seconds": round(float(duration), 2),
|
|
456
|
+
"memory": memory,
|
|
457
|
+
"cpu": cpu,
|
|
458
|
+
"frames": frames,
|
|
459
|
+
"battery": battery,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
with open(args.output, "w") as f:
|
|
463
|
+
json.dump(output, f, indent=2, default=str)
|
|
464
|
+
|
|
465
|
+
print(f"\nDados extraídos: {args.output}")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
if __name__ == "__main__":
|
|
469
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
perfetto>=0.0.6
|