Heisenbug — um bug que desaparece quando você tenta depurá-lo. O termo vem do princípio da incerteza de Heisenberg: a observação afeta o comportamento do sistema. No desenvolvimento móvel, Heisenbug é um dos problemas mais difíceis porque os métodos padrão de depuração (logs, breakpoints, código adicional) alteram o estado do programa e escondem o bug. Vamos explorar as causas e os métodos para lidar com erros elusivos.
Principais Pontos
Heisenbug — uma classe de erros que se manifestam em produção ou durante a operação normal, mas desaparecem ao tentar reproduzi-los em um ambiente de depuração. O termo foi cunhado na década de 1980 pelo programador Jim Gray no contexto de sistemas distribuídos, mas é mais relevante hoje para aplicativos móveis devido à sua natureza assíncrona.
A principal razão: as ferramentas padrão de depuração alteram o ambiente de execução. Um breakpoint pausa a thread por vários milissegundos, o logging adiciona E/S síncrona, verificações adicionais alteram a ordem das operações. Em um ambiente multithread, mesmo um atraso de microssegundo pode alterar a ordem de execução das threads e ocultar uma data race.
De acordo com a Microsoft Research (2022), cerca de 15-25% de todos os bugs em aplicativos móveis multithread são classificados como Heisenbug. Ao mesmo tempo, o tempo para encontrar e corrigir um Heisenbug é em média 5 a 10 vezes maior do que um bug normal, devido à impossibilidade de reprodução direta.
Um aplicativo trava em produção ao deslizar rapidamente por uma lista, mas ao conectar ao depurador ou adicionar logs — funciona perfeitamente. Causa: uma data race entre a thread de UI (atualizando RecyclerView) e a thread de fundo (atualizando dados do adaptador). Os logs adicionam um atraso que sincroniza as threads aleatoriamente.
Bohrbug — um bug previsível, estavelmente reproduzível. Nomeado por analogia com o modelo atômico de Bohr: como um átomo, o bug se comporta da mesma forma toda vez que é observado. Exemplo: NullPointerException ao clicar em um botão antes de carregar os dados. Tratado com testes unitários padrão.
Mandelbug — um bug com uma relação causal complexa e caótica (nomeado por analogia com o conjunto de Mandelbrot). Manifesta-se apenas sob uma certa combinação de condições: versão do SO, modelo do dispositivo, estado da rede. Difere do Heisenbug por não desaparecer durante a depuração — o problema é a dificuldade de reprodução, não a mudança de comportamento pelas ferramentas.
Heisenbug — um bug que desaparece precisamente por causa das ferramentas de depuração. Se você adicionar um log — o bug desaparece. Se você colocar um breakpoint — o bug não se manifesta. Se você remover tudo — o bug retorna. A principal causa: alteração do timing durante a depuração.
| Tipo | Reproduzibilidade | Reação à depuração | Exemplo |
|---|---|---|---|
| Bohrbug | 100% | Inalterada | NPE em lista vazia |
| Mandelbug | Caótica | Inalterada | Crash no Android 12, Samsung, bateria baixa |
| Heisenbug | Apenas sem depuração | Desaparece | Race condition que desaparece com logs |
| Schrödinbug | Não se manifesta no código | Aparece ao olhar | Bug visível no código mas nunca dispara |
Race condition — a causa número um do Heisenbug. Duas threads acessam dados compartilhados sem sincronização. O depurador introduz um atraso, fazendo com que as threads se sincronizem naturalmente. Sem o depurador, a ordem de execução é imprevisível.
Erros dependentes de timing — bugs que se manifestam apenas em uma certa velocidade de execução. Por exemplo, uma animação que deve terminar antes do início da próxima operação. No depurador, a animação é mais lenta e a operação tem tempo de começar após o término da animação. Em produção — o oposto.
// Example race condition — typical Heisenbug
class ListViewModel : ViewModel() {
private var items = mutableListOf<String>()
fun loadFromNetwork() {
viewModelScope.launch(Dispatchers.IO) {
val result = api.fetchItems()
items.addAll(result) // ❌ Not thread-safe
}
}
fun getItems(): List<String> = items.toList()
// Race condition: getItems read may overlap with loadFromNetwork write
}
Otimização do compilador — o compilador (JIT, ART, Kotlin/Native) pode reordenar instruções para otimização. Em uma compilação de depuração (debug build), as otimizações estão desativadas e o código executa «como foi escrito». Em uma compilação de lançamento (release build), o compilador altera a ordem das operações, o que pode revelar suposições ocultas no código.
ThreadSanitizer (TSan) — uma ferramenta do Google para detectar data races em C/C++ e Kotlin/Native. Incorporada na compilação e detecta qualquer acesso à memória compartilhada sem sincronização. Ao contrário dos logs, o TSan não afeta o timing porque funciona através de código instrumentado, não de E/S.
Testes determinísticos — substitua a assincronia real por assincronia controlada. Use TestDispatcher (Kotlin), RxJava Plugins ou filas de teste GCD (iOS) para controle total sobre a ordem de execução. Especifique cenários concretos: a thread A executa, depois B, depois A novamente.
Logging cíclico — registro em um buffer circular na memória (não em disco). Quando o bug ocorre, o buffer é salvo em um arquivo. Como escrever na memória leva nanossegundos (em vez de milissegundos para E/S de disco), esse registro não afeta o timing e não mascara o Heisenbug.
class CyclicBuffer(val capacity: Int = 1000) {
private val buffer = ArrayDeque<String>(capacity)
private val lock = Any()
fun log(message: String) {
synchronized(lock) {
if (buffer.size >= capacity) buffer.removeFirst()
buffer.addLast(message)
}
}
fun flush() {
synchronized(lock) { buffer.forEach { fileWriter.write(it) } }
}
}
Logging em produção — se o bug não for reproduzível localmente, colete dados em produção. Use logs do Firebase Crashlytics, Sentry Breadcrumbs ou um logger cíclico personalizado. Importante: o logging deve ser assíncrono e ter impacto mínimo no desempenho.
Isolamento de estado — minimize o estado mutável compartilhado. Cada componente deve ter seu próprio estado isolado, inacessível para escrita direta de outros componentes. Use Unidirectional Data Flow (UDF) — o estado flui em uma direção: Evento → Redutor → Estado → UI.
Abordagem funcional — funções puras sem efeitos colaterais são mais fáceis de testar e depurar. Isole os efeitos colaterais (rede, BD, arquivos) em camadas estritamente definidas (repositório, fonte de dados). Erros de concorrência em código funcional são praticamente impossíveis.
Modo estrito — ative o Android StrictMode na compilação de depuração. Ele detecta violações da política de threads (rede na thread principal, E/S de disco na thread principal) e lança uma exceção. Isso transforma um potencial Heisenbug em um Bohrbug determinístico que é imediatamente visível.
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
}
Revisão de código com foco em assincronia — uma parte obrigatória do processo. Cada pull request deve ser verificado quanto a estado mutável compartilhado, coleções não seguras para threads e falta de sincronização. Use regras lint para proibir automaticamente certos padrões (por exemplo, acessar MutableList sem synchronized).
Perguntas Frequentes
Porque os métodos padrão — breakpoints, logs, print — alteram tanto o ambiente de execução que o bug para de se manifestar. O depurador pausa todas as threads por dezenas de milissegundos. Durante esse tempo, a data race que causou o bug se resolve naturalmente. São necessárias ferramentas que não afetem o timing de execução.
Mandelbug é difícil de reproduzir devido à complexidade das condições, mas as ferramentas de depuração não afetam sua manifestação. Heisenbug desaparece precisamente por causa das ferramentas de depuração. Exemplo de Mandelbug: um crash apenas em dispositivos com Android 11, 3 GB de RAM e nível de bateria abaixo de 15%. Exemplo de Heisenbug: uma data race que desaparece ao adicionar Log.d().
Use detecção de testes flaky — testes que às vezes falham, às vezes passam. No Android, use o Android Test Orchestrator para isolar testes. Adicione StrictMode aos testes de depuração. Instrumente a compilação com ThreadSanitizer. Se um teste for flaky >5% das execuções — considere-o um potencial Heisenbug e investigue antes de mesclar.
Parcialmente. Flow e a concorrência estruturada em Kotlin reduzem a quantidade de estado mutável compartilhado e simplificam o gerenciamento de threads. Mas as corrotinas não garantem segurança de threads: se duas corrotinas compartilham estado, uma data race ainda é possível. Use Mutex para proteger o estado compartilhado ou Channel para passar dados entre corrotinas.
Use um buffer de log cíclico na memória com descarga automática em caso de erro. Adicione monitoramento detalhado via Crashlytics ou Sentry com breadcrumbs personalizados. Para Android, ative a detecção de ANR e verifique os traces. Se o bug for uma data race, o ThreadSanitizer em uma compilação de depuração com carga similar à produção pode revelar o problema.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também