Depurando um bug que não é reproduzido

Em 10 de outubro de 2018, nossa equipe lançou uma nova versão do aplicativo no React Native. Estamos satisfeitos e orgulhosos disso.

Mas o horror é algo: depois de algumas horas, o número de falhas do Android aumenta repentinamente.


10.000 falhas no Android

Nossa ferramenta de monitoramento de falhas Sentry está ficando louca.

Em todos os casos, vemos um erro como JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView" .

Em React Native, isso geralmente acontece se você definir uma propriedade com o tipo errado. Mas por que o erro não apareceu durante o teste? Em nós, cada desenvolvedor testa cuidadosamente novos lançamentos em vários dispositivos.

Os erros também parecem bastante aleatórios, parecem ocorrer em qualquer combinação de propriedades e digitar nós de sombra. Por exemplo, aqui estão os três primeiros:

  • Error while updating property 'paddingTop' in shadow node of type: RCTView
  • Error while updating property 'height' in shadow node of type: RCTImageView
  • Error while updating property 'fill' of a view managed by: RNSVGPath

Parece que o erro ocorre em qualquer dispositivo e em qualquer versão do Android, a julgar pelo relatório Sentry.


A maioria das falhas do Android 8.0.0 falha, mas isso é consistente com nossa base de usuários

Vamos jogar de volta!


Então, o primeiro passo antes de corrigir o erro é reproduzi-lo, certo? Felizmente, graças aos logs do Sentry, podemos descobrir o que os usuários fazem antes que ocorra uma falha.

Ta-a-ak, vamos ver ...



Hmm, na grande maioria dos casos, os usuários simplesmente abrem o aplicativo e - boom, ocorre uma falha.

Ok, vamos tentar novamente. Instalamos o aplicativo em seis dispositivos Android, abrimos e saímos várias vezes. Nenhuma falha! Além disso, é impossível reproduzi-lo localmente no modo dev.

Ok, isso parece inútil. As falhas ainda são bastante aleatórias e ocorrem em 10% dos casos. Parece que você tem 1 em 10 chances de o aplicativo travar na inicialização.

Análise de rastreamento de pilha


Para reproduzir essa falha, vamos tentar entender de onde ela vem ...


Como mencionado anteriormente, temos vários erros diferentes. E todo mundo tem traços semelhantes, mas um pouco diferentes.

Ok, vamos dar o primeiro:

 java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 at android.support.v4.util.Pools$SimplePool.release(Pools.java:116) at com.facebook.react.bridge.DynamicFromMap.recycle(DynamicFromMap.java:40) at com.facebook.react.uimanager.LayoutShadowNode.setHeight(LayoutShadowNode.java:168) at java.lang.reflect.Method.invoke(Method.java) ... java.lang.reflect.InvocationTargetException: null at java.lang.reflect.Method.invoke(Method.java) ... com.facebook.react.bridge.JSApplicationIllegalArgumentException: Error while updating property 'height' in shadow node of type: RNSVGSvgView at com.facebook.react.uimanager.ViewManagersPropertyCache$PropSetter.updateShadowNodeProp(ViewManagersPropertyCache.java:113) ... 

Portanto, o problema está em android/support/v4/util/Pools.java .

Hmm, nós somos muito profundos na biblioteca de suporte do Android, dificilmente é possível obter qualquer benefício aqui.

Encontre outra maneira


Outra maneira de encontrar a causa raiz do erro é verificar se há novas alterações na versão mais recente. Especialmente aqueles que afetam o código nativo do Android. Duas hipóteses surgem:

  • Atualizamos a Navegação nativa , onde fragmentos nativos para Android são usados ​​para cada tela.
  • Atualizamos react-native-svg . Havia algumas exceções relacionadas aos componentes SVG, mas esse não é o caso.

Não podemos reproduzir o erro no momento, então a melhor estratégia é:

  1. Reverta uma das duas bibliotecas e reveja para 10% dos usuários, o que é trivial na Play Store. Verifique com vários usuários se a falha persistir. Assim, confirmamos ou refutamos a hipótese.


    Mas como escolher uma biblioteca para reverter? Claro, você pode jogar uma moeda, mas essa é a melhor opção?


    Vá direto ao ponto


    Vamos dar uma olhada no rastreamento anterior. Talvez isso ajude a determinar a biblioteca.

     /** * Simple (non-synchronized) pool of objects. * * @param The pooled type. */ public static class SimplePool implements Pool { private final Object[] mPool; private int mPoolSize; ... @Override public boolean release(T instance) { if (isInPool(instance)) { throw new IllegalStateException("Already in the pool!"); } if (mPoolSize < mPool.length) { mPool[mPoolSize] = instance; mPoolSize++; return true; } return false; } 

    Houve um fracasso. Erro java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 significa que mPool é uma matriz de tamanho 10, mas mPoolSize=-1 .

    Ok, como mPoolSize=-1 ? Além do método de recycle acima, o único local para alterar o mPoolSize é o método de SimplePool classe SimplePool :

     public T acquire() { if (mPoolSize > 0) { final int lastPooledIndex = mPoolSize - 1; T instance = (T) mPool[lastPooledIndex]; mPool[lastPooledIndex] = null; mPoolSize--; return instance; } return null; } 

    Portanto, a única maneira de obter um valor mPoolSize negativo é reduzi-lo com mPoolSize=0 . Mas como isso é possível com a condição mPoolSize > 0 ?

    Colocaremos pontos de interrupção no Android Studio e veremos o que acontece quando o aplicativo é iniciado. Quero dizer, aqui está a condição if , este código deve funcionar bem!

    Finalmente, uma revelação!



    Consulte DynamicFromMap link estático para o SimplePool .

     private static final Pools.SimplePool<DynamicFromMap> sPool = new Pools.SimplePool<>(10); 

    Após várias dezenas de cliques no botão Reproduzir com pontos de interrupção definidos com cuidado, vemos que os threads mqt_native_modules chamam as funções SimplePool.release e SimplePool.release usando React Native para controlar as propriedades de estilo do componente React (abaixo da propriedade width do componente)



    Mas eles também são acessados ​​pelo stream principal main !



    Acima, vemos que eles são usados ​​para atualizar a propriedade fill no fluxo principal, geralmente para o componente react-native-svg ! De fato, a biblioteca react-native-svg começou a usar o DynamicFromMap apenas com a sétima versão para melhorar o desempenho das animações svg nativas.

    And-and-and ... uma função pode ser chamada de dois threads, mas DynamicFromMap não usa o SimplePool de maneira segura para threads. "Segmento seguro", diz?

    Segurança da linha, um pouco de teoria


    No JavaScript de thread único, os desenvolvedores geralmente não precisam lidar com a segurança do thread.

    Java, por outro lado, suporta o conceito de programas paralelos ou multithread. Vários encadeamentos podem ser executados no mesmo programa e podem acessar potencialmente a estrutura geral de dados, o que às vezes leva a resultados inesperados.

    Veja um exemplo simples: a imagem abaixo mostra que os fluxos A e B são paralelos:

    • leia um número inteiro;
    • aumentar seu valor;
    • devolva-o.


    O fluxo B pode potencialmente acessar o valor dos dados antes que o fluxo A o atualize. Esperávamos duas etapas separadas para fornecer um valor final de 19 . Em vez disso, podemos obter 18 . Uma situação em que o estado final dos dados depende da ordem relativa das operações de fluxo é chamada de condição de corrida. O problema é que essa condição não ocorre necessariamente o tempo todo. Talvez, no caso acima, o encadeamento B tenha outro trabalho antes de continuar a aumentar o valor, o que fornece tempo suficiente para o encadeamento A atualizar o valor. Isso explica a aleatoriedade e a incapacidade de reproduzir a falha.

    Uma estrutura de dados é considerada segura para threads se as operações puderem ser executadas simultaneamente por vários threads sem o risco de uma condição de corrida.

    Quando um encadeamento lê um elemento de dados específico, outro encadeamento não deve ter o direito de modificar ou excluir esse elemento (isso é chamado atomicidade). No exemplo anterior, se os ciclos de atualização fossem atômicos, as condições da corrida poderiam ter sido evitadas. O segmento B aguardará até que o segmento A conclua a operação e inicie a si próprio.

    No nosso caso, isso pode acontecer:



    Como o DynamicFromMap contém um link estático para o SimplePool , várias chamadas de DynamicFromMap vêm de diferentes segmentos, enquanto invocam o método de acquire no SimplePool .

    Na ilustração acima, o segmento A chama o método, avaliando a condição como verdadeira , mas ainda não conseguiu reduzir o valor de mPoolSize (que é usado em conjunto com o segmento B), enquanto o segmento B também chama esse método e também avalia a condição como verdadeira . Posteriormente, cada chamada reduzirá o valor de mPoolSize , resultando no valor "impossível".

    Correção


    Estudando as opções de correção, encontramos uma solicitação de pool para nativo de reação , que ainda não ingressou na ramificação - e fornece segurança de encadeamento neste caso.



    Em seguida, lançamos uma versão fixa do React Native para usuários. O acidente está finalmente resolvido, um brinde!


    Portanto, graças à ajuda de Jenick Duplessis (colaborador do núcleo do React Native) e Michael Sand (mantenedor do react react-native-svg ), o patch está incluído na próxima versão menor do React Native 0.57 .

    Demorou algum esforço para corrigir esse bug, mas foi uma grande oportunidade para aprofundar-se no react-native e react-native-svg. Um bom depurador e alguns pontos de interrupção bem colocados são importantes. Espero que você também tenha aprendido algo útil com essa história!

Source: https://habr.com/ru/post/pt432426/


All Articles