04.05.2020 admin

Устранение невоспроизводимого сбоя

Немного технический знаний об ошибках

10 октября 2018 года наша команда выпустила новую версию приложения React Native. Мы рады предоставить вам ее новые возможности!

Но, о, ужас! Через несколько часов после релиза мы неожиданно получили статистику об огромном количестве сбоев Android.

10 000 сбоев на Android

Все новые ошибки имели тип JSApplicationIllegalArgumentException Error при обновлении свойства ‘left’ в теневом узле типа: RCTView «.

В React Native это обычно происходит при установке свойства неправильного типа. Но почему мы не обнаружили эту ошибку при тестировании приложения? Наши новые релизы были хорошо протестированы каждым членом команды на панели из нескольких устройств.

Кроме того, ошибки кажутся совершенно случайными, хаотичными. Они появляются для любой комбинации свойства и типа теневого узла. Например, это первые три из всех пришедших.

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

Если доверять отчетам Sentry, те же ошибки высветились на устройствах всех типов и со всеми версиями Android.

Большинство сбоев на Android 8.0.0, но это соотношение соответствует статистике типов Android нашей пользовательской базы

 

Давайте воспроизводем!

Итак, первым шагом к исправлению ошибки является ее воспроизведение, верно? К счастью, благодаря записям Sentry мы знаем, что делали пользователи перед возникновением сбоя.

Итак, посмотрим сюда.

Sentry панировочные сухари не так полезны

Хорошо, теперь мы знаем, что в большинстве случаев сбой возникал сразу в момент открытия пользователями приложения.

Попробуем это повторить. Установим приложение из магазина на 6 различных устройств Android и запустим его. Не повезло, сбой приложения не случился! Увы, его также невозможно воспроизвести локально в режиме разработки.

Хорошо, наши действия кажутся бессмысленными. В любом случае, сбои также кажутся совершенно случайными. Процент сбоев от общего числа загрузивших равен примерно на 10%. Похоже, что у нас есть примерно 1 шанс из 10, чтобы получить зависшее в момент запуска приложение.

Анализируем ошибку загрузки (stacktrace)

Чтобы воспроизвести этот сбой, давайте попробуем понять, откуда он взялся. Для этого нам нужно мыслить как сбой!

детективная работа

Дело в том, что у нас есть несколько разных ошибок. И все они имеют похожие, но не одинаковые stacktrace.

Давайте возьмем для анализа первый код.

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

adowNodeProp(ViewManagersPropertyCache.java:113)

Мы обнаружили место зарождения сбоя в Android / Support / V4 / Util /

Pools.java.

Хм, его корни уходят в глубины библиотеки поддержки Android. Боюсь, расследование в этой области может занять слишком много времени. Я же хочу найти самый быстрый способ исправить проблему для своих пользователей, поэтому потрачу еще немного времени на выяснение причины ошибки.

Давайте найдем другой путь

Другой способ найти основную причину ошибки — проверить новейшие изменения, внесенные в эту версию программы. Особенно нас интересуют изменения, оказывающие влияние на собственный код Android. На данный момент у нас есть 2 гипотезы:

— мы обновили Native Navigation, навигационное решение, которое использует собственные фрагменты на Android для каждого экрана;-мы модернизировали react-native-svg. Там было использовано несколько исключений, связанных с компонентами SVG, но при этом не имевших к этому никакого отношения, так что, возможно, это не очень хорошее место для поиска нашей ошибки.

Поскольку в данный момент мы пока не можем воспроизвести ошибку, лучше всего нам поступить следующим образом.

1) Понизить рейтинг одной из двух библиотек.

2) Откатить версии у 10% пользователей, благо, это несложно сделать с

помощью Play Store

3) Перепроверить несколько пользователей, если произойдет сбой в новой

версии.

Таким образом, мы подтвердим или опровергнем нашу гипотезу.

Но как выбрать, какую из библиотек необходимо понизить?

Одно из решений — подбросить монетку, но вряд ли случайность является достойным основанием для принятия решения.

Как добраться до сути происходящего

Давайте углубимся в трассировку стека, которую мы ранее выбрали, чтобы попробовать определиться с выбором понижаемой библиотеки.

/**

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

}

В данный момент мы находимся над местом сбоя. Ошибка была java.lang.ArrayIndexOutOfBoundsException: length = 10; index = -1 означает, что mPool — это массив размером 10, но mPoolSize = -1.

Хорошо, как можно получить с mPoolSize = -1? В дополнение к методу recycle, указанному выше, единственное место, где модифицируется mPoolSize, находится в методе захвата класса 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;

}

Таким образом, mPoolSize может быть равен -1 только если нажать строку mPoolSize—, тогда как mPoolSize = 0. Но как это возможно, если выше указано условие, что mPoolSize> 0?

Давайте установим точку остановки в Android Studio и проверим, что произойдет, если мы запустим приложение. Я имею в виду, при условии if, поскольку этот кусок кода не может работать со сбоями!

И, наконец, открытие!

 

открытие

Смотрите, DynamicFromMap  — это статическая ссылка на SimplePool.

private static final Pools.SimplePool<DynamicFromMap> sPool = new

Pools.SimplePool<>(10);

После нескольких десятков нажатий кнопки запуска с тщательно расположенными точками прерывания мы видим, что SimplePool.acquire и SimplePool.release вызываются в потоках mqt_native_modules для управления свойствами стиля компонента React (ниже ширины width компонента).

из потока RN

Но они также призвали основной поток!

из SVG-потока

Выше мы видим, что он используется для обновления реквизита заполнения в главном fill потоке как это свойственно компоненту react-native-svg! Действительно, оказывается, что react-native-svg использовал DynamicFromMap только начиная с 7-й версии библиотеки, чтобы улучшить производительность svg-анимаций.

Да, эту функцию можно вызвать из двух потоков, но DynamicFromMap не использует SimplePool в поточно-ориентированном режиме. «Thread safe»(поток безопасности), говорите вы? Для тех из вас, кто не знает этот термин, я объясню его в следующей части. Если вы уже знаете, что это, не стесняйтесь пропустить главу.

 

Немного теории про поток безопасности

Поскольку JavaScript является однопоточным, его разработчикам обычно не приходится заниматься безопасностью потоков.

С другой стороны, Java поддерживает концепцию параллельных или многопоточных программ. Несколько потоков могут выполняться в одной программе и потенциально способны получить доступ к общей структуре данных. Именно это может привести к неожиданным результатам.

Давайте разберем в качестве примера изображение ниже. Обратите внимание на оба потока — A и B.

— Прочитайте целое число их памяти.

— Увеличьте его значение.

— Верните его.

объяснение нитей

Поток B может получить доступ к значению данных раньше, чем поток A завершит его обновление. Мы ожидали, что два отдельных приращения приведут к конечному значению 19. То, что мы потенциально получили вместо этого, равно 18. Такой сценарий, в котором конечное состояние данных зависит от относительного порядка операций потока, называется условием гонки.

Проблема с условиями гонки заключается в том, что они не обязательно происходят постоянно. В приведенном выше случае присутствует допущение, что поток B проделал большую работу, прежде чем увеличить значение, предоставив потоку A достаточно времени для обновления значения. Это объясняет присутствие фактора случайности и невозможность воспроизведения нашей ошибки.

Структура данных называется потоко-ориентированной, если операции могут выполняться одновременно несколькими потоками без риска возникновения условий гонки.

Когда один поток выполняет чтение для определенного элемента данных, ни одному другому потоку не должно быть разрешено изменять или удалять этот элемент (это называется атомарностью). В нашем предыдущем примере, если бы циклы обновления были атомарными, условия гонки можно было бы избежать. Поток B ждал бы завершения работы потока A, а затем продолжал бы движение.

В нашем случае, произойти может следующее:

объяснение нитей

Поскольку DynamicFromMap содержит статическую ссылку на SimplePool, несколько DynamicFromMap, вызываемых из разных потоков, могут одновременно вызывать метод захвата в SimplePool.

На изображении выше поток A вызывает метод, оценивая условие как истинное, но значение mPoolSize (которое используется совместно с потоком B) еще не было уменьшено, тогда как поток B одновременно вызывает метод и уже оценивает условие, что также верно. Затем каждый отдельный вызов будет впоследствии уменьшать значение mPoolSize, и таким образом вы получите невозможное значение.

Исправляем ошибку

Исследуя, как это исправить, мы обнаружили PR на react-native, который не был объединен.

Исправление сбоя

Затем мы загрузили в магазин исправленную версию приложения и отправили уведомление пользователям. Ошибка была окончательно исправлена, ура!

Ооочень много аварий

Наконец, благодаря помощи Яника Дуплессиса (основного разработчика React Native) и Микаэля Санда (сопровождающего react-native-svg), исправление будет включено в следующий выпуск версии 0.57 для React Native.

Эта ошибка потребовала некоторых усилий на ее устранение, но и стала прекрасной возможностью глубже покопаться в Reaction-native и Reaction-native-svg. Хороший отладчик и несколько хорошо расположенных точек остановки имеют большое значение. Надеюсь, вы узнали что-то полезное!

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Contact

Давайте работать вместе!

Пишите нам и найдем точки соприкосновения, может станем партнерами, а может поможем вам зайти в нашу чудесную нишу

Вы разработчик?

Пишите! Нам постоянно нужны новые кадры, либо можем помочь в продвижении вашего приложения

Новичок?

Поможем быстро войти в нишу, не тратя годы на понимание

Давно в нише?

Рады будем пообщаться как на темы whitehat, так и blackhat тематики ^_^ + всегда есть что обсудить по поводу рекламных сетей

ПИШИ В TELEGRAM!

Contact