Skip to content

Estabilidade dos testes: esteira de instabilidade e as causas que ela achou - #8

Merged
yan-vidal merged 23 commits into
mainfrom
test/estabilidade-dos-testes
Oct 10, 2026
Merged

yan-vidal merged 23 commits into
mainfrom
test/estabilidade-dos-testes

Conversation

@yan-vidal

Copy link
Copy Markdown
Contributor

Resumo

Esteira de instabilidade (.github/workflows/stress.yml): 16 emuladores repetem em laço os testes que falhavam "às vezes" no gate, metade com a CPU do emulador limitada, com aviso antecipado por status no commit e relatório por teste e condição. Ela achou as causas abaixo; da 1ª rodada (≈140 voltas com falha) à última (1 falha em 7.304 execuções, já corrigida em cd4adb5).

Defeitos do app

  • Teclado a frio: o onCreateInputView fazia oito leituras bloqueantes do DataStore na thread principal (até 420 ms no emulador lento); agora uma (≈60 ms). Logs de diagnóstico da abertura (1º quadro, altura da janela, recriações com motivo).
  • Skins: um quadro que terminava de renderizar depois do close() ficava órfão no cache global.
  • Telas Compose: coleta de fluxos na thread principal (collectAsStateOnMain) — no teste, valor vindo de outra thread recompunha fora dela.

Testes

  • Contaminação: a CanvasGridTest deixava uma gravação real rodando (o BarsTest seguinte achava "● 2:20").
  • Logo após reinstalar, o sistema demora a registrar o teclado: gate e esteira esperam ime list listar o KeyFlow.
  • Testes que agem dentro de uma janela de tempo do produto alongam a janela por gancho …ForTest (desfazer o cancelamento, tira de acentos).
  • Teclado: espera a janela do IME estar onde a view está antes de tocar; na abertura, reabre o teclado se ela não assenta. Saiu a espera às cegas de 5 s (≈5 s a menos por teste de teclado; +24% de voltas na esteira).
  • Medição de onde a janela do teclado fica ao abrir, por dentro (ImeWindowPlacementTest, só com -e placementProbe true) e por fora (scripts/ime-placement.py, também a frio).

Achado que fica como tarefa

A frio, num aparelho lento, a janela do KeyFlow aparece em transição por um instante (medido de fora: 6 de 8 aberturas; Gboard 1 de 8). Pede um perfil com ferramenta de verdade (Android Studio no A32 ou simpleperf). No aparelho rápido não se nota.

Validação

  • Esteira: rodadas 38057718808 → 38087774045 (relatórios no job "Stress report").
  • Local: classes afetadas no emulador, inclusive com a CPU limitada (systemd-run --user --scope -p CPUQuota=300%).

…s que falham "às vezes"

Os testes que ainda dão "instável" no gate (CanvasGrid holdingAKey/rollover, SpellChecker backspace/sugestão,
joystick da BarsTest, OverlayServiceCancelTest, PackOrbBudgetTest) falham uma vez a cada tantas rodadas e
nunca no emulador local. Workflow Stress: os APKs são montados uma vez; 16 emuladores iguais ao do gate
repetem as classes por ~40 min, reinstalando o app a cada volta; metade com o QEMU num cgroup limitado a 2
núcleos (perto do runner privado de 2 vCPU). Cada volta com falha guarda saída bruta, trecho do logcat e
tela; o relatório soma falhas/execuções por teste e condição de CPU e fica vermelho quando acha algo.

Roda por push em ci/esteira-lenta… (o código daquele commit) ou, na main, por Run workflow. Não bloqueia PR.
… teste do mic seguia por baixo

canvasGridDoesNotDisturbTheMicButton inicia uma gravação de verdade e o tearDown só zerava o estado exibido. A
gravação continuava no OverlayService: na esteira de instabilidade, o KeyFlowKeyboardBarsTest seguinte achava
"● 2:20" no lugar do "🎙" em 134 de 134 voltas, e o OverlayServiceCancelTest começava com o serviço já gravando
(o ACTION_RECORD virava "parar e enviar"). No gate o teste seguinte do mesmo shard herdava a mesma gravação.
…— o GitHub recusava a descrição com emoji
…ow reinstalado

Esteira de instabilidade (38057718808): logo depois do `adb install -r`, o sistema reconstrói a lista de
teclados em segundo plano e recusa `ime enable` e `ime set` do KeyFlow ("unrecognized IME ID"). No emulador
lento isso passou de 75 s; o 1º teste de teclado da volta esperou 30 s por um teclado que não tinha como
subir (holdingAKey… "KeyFlowKeyboardView não anexou"). O awaitKeyboardAttached repetia só o `ime set` — sem
o `enable` aceito, nunca voltava — e um toque no campo recriado derrubava a espera (StaleObjectException).

O gate e a esteira esperam `ime list -a -s` listar o KeyFlow depois de instalar (até 180 s, com o tempo no
log); o awaitKeyboardAttached repete `enable` + `set` e tolera o campo recriado.
… teclado

Na esteira (38057718808), typingCommits e slidingToAnotherKey falharam com o teclado já anexado e registrado:
o toque no "q" não chegou ao campo, e o logcat não separa "o toque caiu na janela do probe, por baixo" de "a
grade recebeu e a tecla se perdeu depois". A grade conta os eventos de toque e guarda o último (ação e posição,
sem custo de texto por evento); as mensagens dos testes de digitação trazem isso, o texto do campo e a janela
do IME vista pelo sistema de entrada. A leitura do campo tolera o nó trocado (StaleObjectException).
…ciador de janelas e a tela

Esteira 38061331651: nas 5 falhas da tira de acentos o sistema de entrada via a janela do teclado em
[0,1739][1080,2526] — 126 px (a barra de navegação com o teclado aberto) abaixo do normal [0,1613][1080,2400] —
enquanto a view achava que estava no lugar; o toque calculado pela view caía 53 px acima da grade. Para separar
"janela desenhada no lugar errado" de "só a área de toque errada", a mensagem traz agora a raiz da view (posição e
tamanho), o que o gerenciador de janelas guardou (altura pedida, frame, estado de desenho, leash de insets) e uma
captura da tela na hora da falha, que a esteira traz para a evidência. Conferido com uma falha provisória no
emulador local (a 1ª versão com Regex quebrava no ICU do Android).
…ue só tocam

Esteira 38066247962: typingCommits digitou "1" no lugar de "q" e o rollover "q0" no lugar de "qp", com a grade vendo
o toque no lugar certo. No emulador lento a injeção do DOWN → UP passa dos 250 ms da tira de acentos, o toque vira
"segurar" e soltar no lugar insere a 1ª variante (o número da tecla). accentHoldMsForTest alonga o segurar nos testes
que só tocam (CanvasGridTest, SpellCheckerTest); os testes da tira de acentos seguem com os 250 ms do produto.
…steira compara condições

Esteira 38066247962: as capturas na hora da falha mostraram o teclado desenhado fora do lugar — 126 px abaixo, com a
fileira do espaço cortada, ou no topo da tela —, com o gerenciador de janelas e a view guardando a posição certa: quem
errou foi a camada da animação de entrada. Essa camada é controlada pela tela que recebe o teclado, e o
ImeProbeActivity roda no processo do próprio KeyFlow; no uso real são processos diferentes.

ImeProbeRemoteActivity é o mesmo probe em `:probe`, escolhido na CanvasGridTest por `-e probeProcess remoto` (o
padrão segue o de hoje, sem mudar o gate). A esteira aceita argumentos extras do teste (input instrument_args /
STRESS_INSTRUMENT_ARGS) e o relatório separa as falhas por condição (CPU + argumentos). Conferido no emulador local:
os 7 testes da CanvasGridTest passam com o campo no processo com.keyflow:probe.
…a view está

Esteira (38066247962 e 38070354174): sob carga, a janela do teclado ficou para o sistema de entrada 126 px abaixo, no
topo da tela ou com a geometria do 1º desenho, enquanto a view e o gerenciador de janelas guardavam a posição certa — por
um instante (campo em outro processo) ou por segundos (no mesmo processo). O toque calculado pela view caía fora da grade.

awaitImeWindowSettled espera o frame da janela do IME no `dumpsys input` bater com a raiz da view em duas leituras
seguidas (até 10 s; senão falha dizendo os dois). Roda no fim do awaitKeyboardAttached (todas as classes de teclado) e
antes de cada cálculo de posição de tecla na CanvasGridTest, SpellCheckerTest e LanguageTest. A espera às cegas de 5 s
continua por ora: sai num passo à parte, quando a esteira mostrar que esta basta. 17/17 no emulador local.
…a comparar KeyFlow e Gboard

Mede, durante cada abertura do campo, o frame da janela do IME no sistema de entrada contra o do gerenciador de
janelas — vale para qualquer teclado. É o controle da esteira: se o Gboard, no mesmo emulador e campo, também fica fora
do lugar, é do Android/emulador; se só o KeyFlow, é algo nosso (ex.: o 1º desenho demorando mais que o prazo de
sincronização do Android). Só roda com `-e placementProbe true` (o gate pula). No emulador local, 3 aberturas cada: o
KeyFlow ficou 2 leituras seguidas com a camada do tamanho do 1º desenho ([0,1613][840,1739]); o Gboard, 1 leitura isolada.
… janela e recriações com motivo

Esteira de instabilidade (2026-10-10): a janela do KeyFlow ficava fora do lugar ao abrir, e o Gboard não. O log do teclado
passa a dizer o tempo do onCreateInputView (leituras de config × montar a view), do onStartInputView até o 1º quadro (e o
tamanho da janela nele), cada mudança de altura da janela e cada recriação da view com o motivo. Foi o que mostrou, no
emulador lento, a janela nascendo no tamanho de Dialog (840×126) ou na altura da tela (2272) e o 1º quadro passando de 1 s.
Nada de dado do usuário.
… de oito

O onCreateInputView roda na thread principal bem quando o Android vai mostrar o teclado, e fazia oito runBlocking avulsos
(tema, tamanho, idioma, fileira de números, skin, barras, grades, vibração) — oito idas e voltas entre threads, 224–420 ms
no emulador lento. Com o 1º quadro passando do prazo de 1 s, o Android mostrava a janela ainda em transição (esteira de
instabilidade, 2026-10-10). keyboardSnapshotOnce lê tudo de uma vez, com os mesmos padrões: 61 ms no mesmo emulador.
Leitura fresca (não um cache), para uma config gravada logo antes de abrir o teclado valer já na 1ª montagem.
SettingsRepositoryTest: 29/29, com os dois testes novos do retrato.
…m a frio

scripts/ime-placement.py faz pelo adb o que o ImeWindowPlacementTest faz por dentro: lá a medição rodava no processo
com.keyflow — o mesmo do teclado do KeyFlow e do UiAutomator — e competia com a thread principal do KeyFlow e não com a do
Gboard. --cold encerra o processo do teclado antes de cada abertura (a comparação justa). No emulador lento, a frio: KeyFlow
6 de 8 aberturas com a janela em transição visível (região tocável vazia, tamanho de Dialog), Gboard 1 de 8.
…reabre o teclado

Cada `am instrument` reinicia o processo do app, e o executor de testes e o UiAutomator rodam no mesmo processo do teclado
do KeyFlow: o 1º teste de teclado pega a abertura a frio com a thread principal atrasada, o 1º quadro passa do prazo de 1 s
do Android e a janela fica com a geometria da transição — no processo do teclado, presa por mais de 10 s (esteira
38074785127: 10 de 12 falhas eram "não assentou"). Medido de fora (scripts/ime-placement.py), o mesmo teclado quente fica
no lugar. Na abertura (awaitKeyboardAttached), sem assentar em 5 s o teste fecha o teclado (só se ele estiver de pé) e toca
no campo de novo, até 3 vezes, com um log a cada reabertura; no meio do teste não reabre. Emulador lento: 14/14 duas vezes,
a 1ª rodada com 1 reabertura no teste a frio.
…r em 30 s — e dizem o estado do fim da espera

Esteira 38082896996: commitCancel falhou 1 de 54 voltas (CPU limitada) — o teste dava 2 s ao "concluir" contra a janela de
3 s, e só parar e soltar o gravador passou disso. No emulador local, a frio, targetChange… falhou: o teste levou 19 s e a
janela fechou antes do "desfazer" (esperava PAUSED, achou IDLE). Como no seletor do teclado, a classe usa
OverlayService.cancelWindowMsForTest = 30 s; cancelExpiresAndDiscardsAudio, que testa a janela fechando, volta aos 3 s. O
"concluir" ganha 15 s e tem que encerrar bem antes dos 30. A mensagem de espera era montada no começo (dizia o estado de
antes e o array como endereço); agora é no fim, com os estados por nome. 5/5 duas vezes a partir de processo novo.
…fica órfão no cache

O work() conferia `closed` e só depois guardava o quadro: se o close() caísse entre os dois, o quadro entrava no cache
global de uma orbe já fechada e ficava lá até ser despejado. Achado pelo PackOrbBudgetTest na esteira de instabilidade
(2026-10-10): o teste começava com exatamente um quadro de 156 px (97.344 bytes) de outra orbe no cache, que sumia no meio
dele ("expected 97344 but was 0"). Conferir `closed` de novo depois do put cobre todas as ordens, porque o close() marca
`closed` antes de limpar. PackOrbBudgetTest 20/20 três vezes.
…sentamento faz o mesmo papel

As seis classes esperavam 5 s por um "🎙" que nunca aparece (o mic é desenhado pela BarView); era o tempo para a entrada do
teclado assentar, e tirá-lo sem troca rendeu 4 falhas em 2 rodadas do CI (508f953). O comentário pedia trocar só por uma
condição que esperasse o mesmo assentamento: é o que o awaitKeyboardAttached faz agora (janela do IME onde a view está,
reabrindo o teclado se não assenta). Uns 5 s a menos por teste de teclado. 25/25 no emulador local; a esteira valida sob carga.
Esteira 38087774045 (1 falha em 7.304 execuções): o teclado ficou preso no topo da tela ([120,128][1200,915]), a espera de
assentamento mandou "voltar" e tocou no campo em (540, 283) — mas o teclado ainda cobria o campo, e o toque virou a tecla
"r": o campo terminou "rooo" no lugar de "o". Agora, depois do "voltar", a reabertura espera o teclado sumir do sistema de
entrada (até 3 s) e só então toca; se ele não sai, não toca e a próxima volta da espera tenta de novo.
@yan-vidal
yan-vidal merged commit 0a902d8 into main Oct 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant