Depuración de un error que no se reproduce

El 10 de octubre de 2018, nuestro equipo lanzó una nueva versión de la aplicación en React Native. Estamos contentos y orgullosos de ello.

Pero el horror es algo: después de unas horas, el número de fallas para Android aumenta repentinamente.


10,000 bloqueos para Android

Nuestra herramienta de monitoreo de fallas Sentry se está volviendo loca.

En todos los casos, vemos un error como JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView" .

En React Native, esto generalmente sucede si establece una propiedad con el tipo incorrecto. Pero, ¿por qué no apareció el error durante las pruebas? Para nosotros, cada desarrollador prueba cuidadosamente las nuevas versiones en varios dispositivos.

Los errores también parecen bastante aleatorios, parecen caer en cualquier combinación de propiedades y nodos de tipo sombra. Por ejemplo, aquí están los primeros tres:

  • 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 el error ocurre en cualquier dispositivo y en cualquier versión de Android, a juzgar por el informe de Sentry.


La mayoría de los bloqueos para Android 8.0.0 se bloquean, pero esto es consistente con nuestra base de usuarios

¡Vamos a reproducirlo!


Entonces, el primer paso antes de corregir el error es reproducirlo, ¿verdad? Afortunadamente, gracias a los registros de Sentry, podemos averiguar qué hacen los usuarios antes de que ocurra un bloqueo.

Ta-a-ak, veamos ...



Hmm, en la gran mayoría de los casos, los usuarios simplemente abren la aplicación y, boom, se produce un bloqueo.

Ok, intentemos de nuevo. Instalamos la aplicación en seis dispositivos Android, la abrimos y salimos varias veces. No hay falla! Además, es imposible jugarlo localmente en modo dev.

Vale, eso parece inútil. Las fallas siguen siendo bastante aleatorias y ocurren en el 10% de los casos. Parece que tiene una probabilidad de 1 en 10 de que la aplicación se bloquee al inicio.

Análisis de seguimiento de pila


Para reproducir esta falla, intentemos entender de dónde viene ...


Como se mencionó anteriormente, tenemos varios errores diferentes. Y todos tienen huellas similares, pero ligeramente diferentes.

Ok, tomemos el primero:

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

Entonces el problema está en android/support/v4/util/Pools.java .

Hmm, estamos muy inmersos en la biblioteca de soporte de Android, casi no es posible obtener ningún beneficio aquí.

Encuentra otra forma


Otra forma de encontrar la causa raíz del error es buscar nuevos cambios en la última versión. Especialmente aquellos que afectan el código nativo de Android. Surgen dos hipótesis:

  • Actualizamos Native Navigation , donde se utilizan fragmentos nativos para Android para cada pantalla.
  • Actualizamos react-native-svg . Hubo algunas excepciones relacionadas con los componentes SVG, pero este no es el caso.

No podemos reproducir el error en este momento, por lo que la mejor estrategia es:

  1. Revierta una de las dos bibliotecas. Despliegue para el 10% de los usuarios, lo cual se hace trivialmente en Play Store. Consulte con varios usuarios si el error persiste. Por lo tanto, confirmamos o refutamos la hipótesis.


    Pero, ¿cómo elegir una biblioteca para revertir? Por supuesto, puedes lanzar una moneda, pero ¿es esta la mejor opción?


    Llegar al punto


    Echemos un vistazo más de cerca a la traza anterior. Quizás esto ayudará a determinar la 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; } 

    Hubo un fracaso. Error java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 significa que mPool es una matriz de tamaño 10, pero mPoolSize=-1 .

    Bien, ¿cómo mPoolSize=-1 ? Además del método de recycle anterior, el único lugar para cambiar mPoolSize es el método de acquire de la clase 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; } 

    Por lo tanto, la única forma de obtener un valor negativo de mPoolSize es reducirlo con mPoolSize=0 . Pero, ¿cómo es esto posible con la condición mPoolSize > 0 ?

    Pondremos puntos de interrupción en Android Studio y veremos qué sucede cuando se inicia la aplicación. Quiero decir, aquí está la condición if , ¡este código debería funcionar bien!

    Finalmente, una revelación!



    Consulte DynamicFromMap enlace estático a SimplePool .

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

    Después de varias decenas de clics en el botón Reproducir con puntos de interrupción cuidadosamente establecidos, vemos que los hilos mqt_native_modules llaman a las funciones SimplePool.acquire y SimplePool.release usando React Native para controlar las propiedades de estilo del componente React (debajo de la propiedad de width del componente)



    ¡Pero también son accedidos por main main stream!



    ¡Arriba vemos que se usan para actualizar la propiedad de fill en el hilo principal, generalmente para el componente react-native-svg ! De hecho, la biblioteca react-native-svg comenzó a usar DynamicFromMap solo con la séptima versión para mejorar el rendimiento de las animaciones svg nativas.

    Y-y-y ... se puede SimplePool una función desde dos subprocesos, pero DynamicFromMap no usa SimplePool manera segura para subprocesos. "Hilo seguro", dice?

    Seguridad de hilos, un poco de teoría


    En JavaScript de subproceso único, los desarrolladores generalmente no necesitan lidiar con la seguridad de subprocesos.

    Java, por otro lado, admite el concepto de programas paralelos o multiproceso. Varios hilos pueden ejecutarse dentro del mismo programa y potencialmente pueden acceder a la estructura de datos general, lo que a veces conduce a resultados inesperados.

    Tome un ejemplo simple: la imagen a continuación muestra que los flujos A y B son paralelos:

    • leer un entero;
    • aumentar su valor;
    • devuélvelo.


    La secuencia B puede acceder potencialmente al valor de los datos antes de que la secuencia A lo actualice. Esperábamos dos pasos separados para dar un valor final de 19 . En cambio, podemos obtener 18 . Tal situación en la que el estado final de los datos depende del orden relativo de las operaciones de flujo se denomina condición de carrera. El problema es que esta condición no necesariamente ocurre todo el tiempo. Quizás en el caso anterior, el hilo B tiene otro trabajo antes de proceder a aumentar el valor, lo que da suficiente tiempo para que el hilo A actualice el valor. Esto explica la aleatoriedad y la incapacidad para reproducir la falla.

    Una estructura de datos se considera segura para subprocesos si las operaciones pueden realizarse simultáneamente por múltiples subprocesos sin el riesgo de una condición de carrera.

    Cuando un hilo lee para un elemento de datos en particular, otro hilo no debe tener el derecho de modificar o eliminar este elemento (esto se llama atomicidad). En el ejemplo anterior, si los ciclos de actualización fueran atómicos, las condiciones de carrera podrían haberse evitado. El subproceso B esperará hasta que el subproceso A complete la operación y luego se inicie.

    En nuestro caso, esto puede suceder:



    Debido a que DynamicFromMap contiene un enlace estático a SimplePool , varias llamadas DynamicFromMap provienen de diferentes subprocesos, al tiempo que invoca el método de acquire en SimplePool .

    En la ilustración anterior, el hilo A llama al método, evaluando la condición como verdadera , pero aún no ha logrado reducir el valor de mPoolSize (que se usa junto con el hilo B), mientras que el hilo B también llama a este método y también evalúa la condición como verdadera . Posteriormente, cada llamada reducirá el valor de mPoolSize , dando como resultado el valor "imposible".

    Corrección


    Al estudiar las opciones de corrección, encontramos una solicitud de grupo para react-native , que aún no se ha unido a la rama, y ​​proporciona seguridad de subprocesos en este caso.



    Luego lanzamos una versión fija de React Native para los usuarios. El accidente finalmente se solucionó, ¡salud!


    Entonces, gracias a la ayuda de Jenick Duplessis (colaborador del núcleo React Native) y Michael Sand (mantenedor react-native-svg ), el parche se incluye en la próxima versión menor de React Native 0.57 .

    Se necesitó un esfuerzo para solucionar este error, pero fue una gran oportunidad para profundizar en react-native y react-native-svg. Un buen depurador y algunos puntos de interrupción bien ubicados son importantes. ¡Espero que también hayas aprendido algo útil de esta historia!

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


All Articles