Главная » Хабрахабр » [Перевод] Отладка бага, который не воспроизводится

[Перевод] Отладка бага, который не воспроизводится

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

Но ужас-то какой: через несколько часов внезапно увеличивается количество сбоев под Android.


10 000 сбоев под Android

Наш инструмент мониторинга сбоев Sentry сходит с ума.
Во всех случаях мы видим ошибку типа JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView".

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

Например, вот первые три: Также ошибки кажутся довольно случайными, они как будто выпадают на любой комбинации свойств и типа shodow ноды.

  • 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

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

Большинство сбоев под Android 8.0.0 падает, но это согласуется с нашей пользовательской базой
Итак, первый шаг перед исправлением бага — воспроизвести его, верно? К счастью, благодаря логам Sentry, мы можем узнать, что делают пользователи перед появлением сбоя.

Та-а-ак, посмотрим…

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

Устанавливаем приложение на шести устройствах Android, открываем его и выходим несколько раз. Хорошо, попробуем повторить. Тем более невозможно воспроизвести его локально в dev-режиме. Нет сбоя!

Сбои всё равно довольно случайны и происходят в 10% случаев. Ладно, это кажется бессмысленным. Похоже, что у вас шанс 1 из 10, что приложение упадёт при запуске.

Чтобы воспроизвести этот сбой, давайте попробуем понять, откуда он берётся…

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

Хорошо, возьмём первую из них:

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

Итак, проблема в android/support/v4/util/Pools.java.

Хм, мы очень глубоко в библиотеке поддержки Android, вряд ли тут можно извлечь какую-то пользу.

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

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

На данный момент мы не можем воспроизвести ошибку, поэтому лучшая стратегия:

  1. Откатить обратно одну из двух библиотек.Выкатить её для 10% пользователей, что тривиально делается в Play Store.Проверить у нескольких пользователей, сохранился ли сбой. Таким образом мы подтвердим или опровергнем гипотезу.

    Конечно, можно подбросить монетку, но разве это оптимальный вариант? Но как выбрать библиотеку для отката?

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

    /** * Simple (non-synchronized) pool of objects. * * @param The pooled type. */
    public static class SimplePool implements Pool if (mPoolSize < mPool.length) { mPool[mPoolSize] = instance; mPoolSize++; return true; } return false; }

    Здесь произошёл сбой. Ошибка java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 означает, что mPool — массив размером 10, но mPoolSize=-1.

    Кроме метода recycle выше, единственным местом изменения mPoolSize является метод acquire класса SimplePool: Ладно, как получился mPoolSize=-1?

    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 — это уменьшить его при mPoolSize=0. Но как это возможно с условием mPoolSize > 0?

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

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

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

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

    Но к ним также обращается основной поток main!

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

    «Потокобезопасным», говорите? И-и-и… функцию можно вызвать из двух потоков, но DynamicFromMap не использует SimplePool потокобезопасным способом.

    В однопоточном JavaScript разработчикам обычно не нужно иметь дело с потокобезопасностью.

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

    Возьмём простой пример: на изображении ниже показано, что потоки A и B параллельно:

    • считывают целое число;
    • увеличивают его значение;
    • возвращают его.

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

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

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

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

    Впоследствии каждый вызов уменьшит значение mPoolSize, в результате чего и получается «невозможное» значение. На иллюстрации выше поток A вызывает метод, оценивая условие как true, но он ещё не успел уменьшить значение mPoolSize (которое используется совместно с потоком B), в то время как поток B тоже вызывает этот метод и тоже оценивает условие как true.

    Изучая варианты исправления, мы обнаружили пул-реквест на react-native, который ещё не влился в ветку — и он обеспечивает потокобезопасность в данном случае.

    Сбой наконец-то исправлен, ура! Затем мы выкатили исправленную версию React Native для пользователей.

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

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


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Пишем торговых роботов с помощью графического фреймворка StockSharp. Часть 1

Один из них – бесплатная платформа StockSharp, которую можно использовать для профессиональной разработки торговых терминалов и торговых роботов на языке C#. В нашем блоге мы много пишем о технологиях и полезных инструментах, связанных с биржевой торговлей. API, с целью создания ...

[Перевод] Сверхинтеллект: идея, не дающая покоя умным людям

Расшифровка выступления на конференции Web Camp Zagreb Мачея Цегловского, американского веб-разработчика, предпринимателя, докладчика и социального критика польского происхождения. В 1945 году, когда американские физики готовились к испытанию атомной бомбы, кому-то пришло в голову спросить, не может ли такое испытание зажечь ...