diff --git a/.specs/codebase/TESTING.md b/.specs/codebase/TESTING.md index 83328f2c..c6ec0959 100644 --- a/.specs/codebase/TESTING.md +++ b/.specs/codebase/TESTING.md @@ -117,9 +117,24 @@ DeckLock (actions fixadas por SHA, `permissions: contents: read`, evidência env propriedade) → `app/build/reports/coverage/test/debug/`; `scripts/coverage-summary.py` resume por pacote. O CI faz isso no job de unitários, depois de publicar os APKs (fora do caminho crítico), e põe a tabela no resumo do job. Mapa do que nenhum teste de unidade toca, não meta: telas, teclado e serviços são cobertos pelos instrumentados. -- `scripts/ime-placement.py --ime keyflow|gboard [--cold]` mede de FORA do app (adb) onde a janela do teclado fica ao abrir - — sem teste no processo. A frio, no emulador lento: KeyFlow 6/8 aberturas em transição, Gboard 1/8 (desempenho da - abertura a frio é tarefa aberta). +- `scripts/ime-placement.py --ime keyflow|gboard [--cold] [--seconds 12]` mede de FORA do app (adb) onde a janela do teclado + fica ao abrir — sem teste no processo — e em quantos segundos ela fica visível e no lugar de vez. A frio, emulador com a + CPU a 300% e o app compilado (`cmd package compile -m speed -f com.keyflow`), 8 aberturas (2026-10-10): KeyFlow visível + em 2,0 s e no lugar de vez em 2,9 s, com a janela em transição nas 8; Gboard visível em 3,8 s e no lugar em 4,0 s, sem + transição. O KeyFlow é o mais rápido; a diferença é o tipo de janela — a do Gboard tem a altura da tela desde o início + (ele desenha 1080×2272), a nossa tem a altura do teclado (787 px) e passa pelos tamanhos de transição até o 1º quadro + sair. (A comparação antiga, "KeyFlow 6/8 × Gboard 1/8", estava errada: com 4 s de observação o Gboard a frio quase não + chegava a aparecer.) +- **Para onde vai o ~1 s do 1º quadro do teclado a frio no emulador** (traces do Perfetto do `OpeningBenchmarks`, 20 voltas, + e um trace do sistema do Gboard; 2026-10-10): 300–600 ms criando o contexto gráfico (`Creating EGLContext` na + RenderThread — só chamadas `rc… encode`, o protocolo do emulador com a GPU emulada; o Gboard paga 302–318 ms); 60–900 ms + compilando ~10 programas de GPU no 1º desenho (`shader_compile`, sempre `cache_miss`: a GPU do emulador, ANGLE sobre + SwiftShader, não tem `GL_OES_get_program_binary`, então o Android não consegue guardar os programas — `code_cache/` do + KeyFlow e do Gboard ficam vazios — e todo processo novo recompila tudo); ~110 ms esperando o buffer da janela; **20–65 ms + do nosso código** na thread principal. Não é lentidão do app, e o perfil de base não alcança isso. Não medido em aparelho + de verdade: para conferir no A32, `installDebug`, um `adb shell perfetto … gfx view` numa abertura a frio e olhar + `Creating EGLContext`/`shader_compile`; `adb shell run-as com.keyflow ls code_cache` deve mostrar + `com.android.skia.shaders_cache` (programas guardados). - Teste que inicia gravação de verdade para o `OverlayService` no `@After` (a gravação vazava para o teste seguinte). - Os 6 jobs de device sobem **junto** com o de unitários (sem `needs`): preparam o runner e ligam o emulador enquanto ele compila, e então baixam o artefato `apks` da própria rodada (`gh run download`). diff --git a/scripts/ime-placement.py b/scripts/ime-placement.py index 2e370a66..5ad7bcd7 100644 --- a/scripts/ime-placement.py +++ b/scripts/ime-placement.py @@ -12,9 +12,16 @@ --cold encerra o processo do teclado antes de cada abertura: toda abertura começa a frio, como a 1ª depois de instalar ou de o Android matar o processo. É a comparação justa — sem isso o Gboard está sempre quente e o KeyFlow, recém-instalado. + +Cada abertura diz também em quantos segundos o teclado ficou visível e no lugar de vez. Leia isso antes de comparar dois teclados: +"0 deslocadas" de um teclado que nem apareceu na janela de observação não quer dizer nada — foi assim que o Gboard a frio +pareceu melhor que o KeyFlow (2026-10-10): no emulador lento ele leva ~3,3 s para aparecer e a janela era de 4 s. A frio, use +--seconds 12 ou mais. Para comparar com um app de sistema, compile o KeyFlow antes (`cmd package compile -m speed -f +com.keyflow`, como o gate faz): o APK de debug recém-instalado roda interpretado. """ import argparse import re +import statistics import subprocess import sys import time @@ -65,33 +72,62 @@ def main(): adb("ime", "enable", ime) adb("ime", "set", ime) displaced, total = [], 0 + visible_times, settled_times = [], [] for opening in range(1, args.openings + 1): adb("input", "keyevent", "KEYCODE_HOME") time.sleep(1.5) if args.cold: # O probe é do pacote do KeyFlow: com o KeyFlow, encerrar o pacote leva o probe junto (ele reabre abaixo). adb("am", "force-stop", PACKAGES[args.ime]) - if args.ime == "keyflow": - adb("ime", "set", ime) # force-stop tira o KeyFlow de teclado ativo + # O force-stop tira o KeyFlow de teclado ativo, e o `ime set` logo em seguida às vezes não pega: repete até o + # Android confirmar (com o Gboard já confirma na 1ª). + for _ in range(25): + adb("ime", "set", ime) + if adb("settings", "get", "secure", "default_input_method").strip() == ime: + break + time.sleep(0.2) + else: + sys.exit(f"{args.ime} não ficou como teclado ativo depois do force-stop") time.sleep(1.0) adb("am", "start", "-n", PROBES[args.probe], "-f", "0x10008000") time.sleep(2.0) adb("input", "tap", "540", "283") - streak, bad, end = 0, [], time.time() + args.seconds + tapped = time.time() + streak, bad, end = 0, [], tapped + args.seconds + visible_at = settled_at = None while time.time() < end: seen, wm = sample() if not seen or not wm: continue total += 1 + now = time.time() - tapped + if visible_at is None: + visible_at = now if seen != wm and top(seen) < height: bad.append(f"entrada={seen} wm={wm}") streak += 1 + settled_at = None # "no lugar" = de vez: uma leitura fora do lugar depois zera if streak == 2: displaced.append(f"abertura {opening}: {bad[-1]}") else: streak = 0 - print(f"abertura {opening}/{args.openings}: {len(bad)} leituras fora do lugar", flush=True) + if seen == wm and settled_at is None: + settled_at = now + visible_times.append(visible_at) + settled_times.append(settled_at) + when = "NÃO apareceu na janela de observação" if visible_at is None else ( + f"visível em {visible_at:.1f} s, " + ("não assentou" if settled_at is None else f"no lugar de vez em {settled_at:.1f} s") + ) + print(f"abertura {opening}/{args.openings}: {len(bad)} leituras fora do lugar; {when}", flush=True) print(f"{args.ime} ({args.probe}{', a frio' if args.cold else ''}): {len(displaced)} de {args.openings} aberturas deslocadas, {total} leituras — {displaced}") + shown = [t for t in visible_times if t is not None] + settled = [t for t in settled_times if t is not None] + print( + f"tempo desde o toque: visível mediana {statistics.median(shown):.1f} s" if shown else "tempo desde o toque: nunca visível", + f"; no lugar de vez mediana {statistics.median(settled):.1f} s" if settled else "; nunca no lugar", + f"; {args.openings - len(shown)} de {args.openings} aberturas sem aparecer em {args.seconds:.0f} s", + sep="", + ) return 1 if displaced else 0