Débogage d'un bogue qui ne joue pas

Le 10 octobre 2018, notre équipe a publié une nouvelle version de l'application sur React Native. Nous en sommes ravis et fiers.

Mais l'horreur est quelque chose: après quelques heures, le nombre d'échecs pour Android augmente soudainement.


10000 plantages pour Android

Notre outil de surveillance des accidents Sentry devient fou.

Dans tous les cas, nous voyons une erreur comme JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView" .

Dans React Native, cela se produit généralement si vous définissez une propriété avec le mauvais type. Mais pourquoi l'erreur n'est-elle pas apparue lors des tests? Chez nous, chaque développeur teste soigneusement les nouvelles versions sur plusieurs appareils.

Les erreurs semblent également plutôt aléatoires, elles semblent tomber sur n'importe quelle combinaison de propriétés et de types de nœuds fantômes. Par exemple, voici les trois premiers:

  • 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

Il semble que l'erreur se produise sur n'importe quel appareil et dans n'importe quelle version d'Android, à en juger par le rapport Sentry.


La plupart des plantages pour Android 8.0.0 se bloquent, mais cela est cohérent avec notre base d'utilisateurs

Jouons-y!


Donc, la première étape avant de corriger le bogue est de le reproduire, non? Heureusement, grâce aux journaux Sentry, nous pouvons découvrir ce que les utilisateurs font avant qu'un crash ne se produise.

Ta-a-ak, voyons voir ...



Hmm, dans la grande majorité des cas, les utilisateurs ouvrent simplement l'application et - boom, un crash se produit.

Ok, réessayons. Nous installons l'application sur six appareils Android, l'ouvrons et la quittons plusieurs fois. Pas de problème! De plus, il est impossible de le jouer localement en mode dev.

D'accord, cela semble inutile. Les échecs sont encore assez aléatoires et se produisent dans 10% des cas. Il semble que vous ayez 1 chance sur 10 que l'application se bloque au démarrage.

Analyse de trace de pile


Pour reproduire cet échec, essayons de comprendre d'où il vient ...


Comme mentionné précédemment, nous avons plusieurs erreurs différentes. Et tout le monde a des traces similaires, mais légèrement différentes.

Ok, prenons le premier:

 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) ... 

Le problème se trouve donc dans android/support/v4/util/Pools.java .

Hmm, nous sommes très profondément dans la bibliothèque de support Android, il n'est guère possible d'obtenir aucun avantage ici.

Trouvez un autre moyen


Une autre façon de trouver la cause première de l'erreur consiste à vérifier les nouvelles modifications apportées à la dernière version. Surtout ceux qui affectent le code Android natif. Deux hypothèses se posent:

  • Nous avons mis à jour la navigation native , où des fragments natifs pour Android sont utilisés pour chaque écran.
  • Nous avons mis à jour react-native-svg . Il y avait quelques exceptions liées aux composants SVG, mais ce n'est guère le cas.

Nous ne pouvons pas reproduire l'erreur pour le moment, donc la meilleure stratégie est:

  1. Annulez l'une des deux bibliothèques. Déployez-la pour 10% des utilisateurs, ce qui est anodin dans le Play Store. Vérifiez auprès de plusieurs utilisateurs si l'échec persiste. Ainsi, nous confirmons ou réfutons l'hypothèse.


    Mais comment choisir une bibliothèque à restaurer? Bien sûr, vous pouvez lancer une pièce, mais est-ce la meilleure option?


    Aller droit au but


    Examinons de plus près la trace précédente. Cela aidera peut-être à déterminer la bibliothèque.

     /** * 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; } 

    Il y a eu un échec. Erreur java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 signifie que mPool est un tableau de taille 10, mais mPoolSize=-1 .

    D'accord, comment mPoolSize=-1 ? En plus de la méthode de recycle ci-dessus, le seul endroit pour modifier mPoolSize est la méthode d' acquire de la 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; } 

    Par conséquent, la seule façon d'obtenir une valeur mPoolSize négative est de la réduire avec mPoolSize=0 . Mais comment est-ce possible avec la condition mPoolSize > 0 ?

    Nous allons mettre des points d'arrêt dans Android Studio et voir ce qui se passe au démarrage de l'application. Je veux dire, voici la condition if , ce code devrait bien fonctionner!

    Enfin, une révélation!



    Voir DynamicFromMap lien statique vers SimplePool .

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

    Après plusieurs dizaines de clics sur le bouton Lecture avec des points d'arrêt soigneusement définis, nous voyons que les threads mqt_native_modules appellent les fonctions SimplePool.acquire et SimplePool.release l'aide de React Native pour contrôler les propriétés de style du composant React (sous la propriété width du composant)



    Mais ils sont également accessibles par le flux principal principal !



    Ci-dessus, nous voyons qu'ils sont utilisés pour mettre à jour la propriété fill dans le flux principal, généralement pour le composant react-native-svg ! En effet, la bibliothèque DynamicFromMap react-native-svg a commencé à utiliser DynamicFromMap uniquement avec la septième version pour améliorer les performances des animations natives svg.

    Et-et-et ... une fonction peut être appelée à partir de deux threads, mais DynamicFromMap n'utilise pas SimplePool manière sécurisée pour les threads. "Thread safe", dites?

    Sécurité des fils, un peu de théorie


    En JavaScript à thread unique, les développeurs n'ont généralement pas besoin de gérer la sécurité des threads.

    Java, d'autre part, prend en charge le concept de programmes parallèles ou multithread. Plusieurs threads peuvent s'exécuter dans le même programme et peuvent potentiellement accéder à la structure générale des données, ce qui conduit parfois à des résultats inattendus.

    Prenons un exemple simple: l'image ci-dessous montre que les flux A et B sont parallèles:

    • lire un entier;
    • augmenter sa valeur;
    • lui rendre.


    Le flux B peut potentiellement accéder à la valeur des données avant que le flux A ne la mette à jour. Nous nous attendions à ce que deux étapes distinctes donnent une valeur finale de 19 . Au lieu de cela, nous pouvons obtenir 18 . Une telle situation où l'état final des données dépend de l'ordre relatif des opérations de flux est appelée condition de concurrence critique. Le problème est que cette condition ne se produit pas nécessairement tout le temps. Dans le cas ci-dessus, le thread B a peut-être un autre travail avant de procéder à l'augmentation de la valeur, ce qui laisse suffisamment de temps au thread A pour mettre à jour la valeur. Cela explique le caractère aléatoire et l'incapacité de reproduire l'échec.

    Une structure de données est considérée comme sécurisée pour les threads si les opérations peuvent être effectuées simultanément par plusieurs threads sans risque de condition de concurrence critique.

    Lorsqu'un thread lit pour un élément de données particulier, un autre thread ne doit pas avoir le droit de modifier ou de supprimer cet élément (c'est ce qu'on appelle l'atomicité). Dans l'exemple précédent, si les cycles de mise à jour étaient atomiques, les conditions de concurrence auraient pu être évitées. Le thread B attendra que le thread A termine l'opération, puis démarre lui-même.

    Dans notre cas, cela peut arriver:



    Étant DynamicFromMap que DynamicFromMap contient un lien statique vers SimplePool , plusieurs appels DynamicFromMap proviennent de threads différents, tout en invoquant la méthode d' acquire dans SimplePool .

    Dans l'illustration ci-dessus, le thread A appelle la méthode, évaluant la condition comme vraie , mais il n'a pas encore réussi à réduire la valeur de mPoolSize (qui est utilisé conjointement avec le thread B), tandis que le thread B appelle également cette méthode et évalue également la condition comme vraie . Par la suite, chaque appel réduira la valeur de mPoolSize , résultant en la valeur "impossible".

    Correction


    En étudiant les options de correction, nous avons trouvé une demande de pool pour react-native , qui n'a pas encore rejoint la branche - et elle offre une sécurité des threads dans ce cas.



    Ensuite, nous avons déployé une version fixe de React Native pour les utilisateurs. Le crash est enfin réglé, bravo!


    Ainsi, grâce à l'aide de Jenick Duplessis (contributeur au noyau React Native) et Michael Sand (mainteneur react-native-svg ), le patch est inclus dans la prochaine version mineure de React Native 0.57 .

    Il a fallu un certain effort pour corriger ce bogue, mais ce fut une excellente occasion de se plonger plus profondément dans react-native et react-native-svg. Un bon débogueur et quelques points d'arrêt bien placés sont importants. J'espère que vous avez également appris quelque chose d'utile de cette histoire!

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


All Articles