再生されないバグのデバッグ

2018年10月10日に、私たちのチームはReact Nativeでアプリケーションの新しいバージョンをリリースしました。 私たちはそれを喜んで誇りに思っています。

しかし、恐怖は何かです。数時間後、Androidの障害の数は突然増加します。


Androidで10,000回クラッシュ

Sentryのクラッシュ監視ツールはおかしくなりました。

すべての場合において、 JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView"ようなエラーが表示されます。

React Nativeでは、これは通常、間違ったタイプのプロパティを設定した場合に発生します。 しかし、テスト中にエラーが表示されなかったのはなぜですか? 当社では、各開発者がいくつかのデバイスで新しいリリースを慎重にテストしています。

エラーもかなりランダムに見え、プロパティとシャドウノードのタイプの任意の組み合わせで発生するようです。 たとえば、最初の3つは次のとおりです。


Sentryレポートによると、エラーはどのデバイスでもAndroidのどのバージョンでも発生するようです。


Android 8.0.0のほとんどのクラッシュはクラッシュしますが、これはユーザーベースと一致しています

再生してみましょう!


バグを修正する前の最初のステップは、バグを再現することですよね? 幸いなことに、Sentryログのおかげで、クラッシュが発生する前にユーザーが何をしているかを知ることができます。

Ta-a-ak、見てみましょう...



うーん、ほとんどの場合、ユーザーは単にアプリケーションを開くだけです-ブーム、クラッシュが発生します。

OK、もう一度試してみましょう。 6つのAndroidデバイスにアプリケーションをインストールし、開いて数回終了します。 グリッチはありません! さらに、開発モードでローカルで再生することはできません。

さて、それは無意味なようです。 失敗はまだかなりランダムであり、10%のケースで発生します。 起動時にアプリケーションがクラッシュする可能性が10分の1あるようです。

スタックトレース分析


この失敗を再現するために、それがどこから来たのかを理解してみましょう...


前述のとおり、いくつかの異なるエラーがあります。 そして誰もが似ているが、わずかに異なる痕跡を持っています。

わかりました、最初のものを取りましょう:

 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コードに影響するもの。 2つの仮説が生じます。


現時点では、エラーを再現することはできませんので、最善の戦略は次のとおりです。

  1. 2つのライブラリのいずれかをロールバックし、10%のユーザーにロールアウトします(Playストアで簡単に実行できます)。エラーが続く場合は、複数のユーザーに確認してください。 したがって、仮説を確認または反論します。


    しかし、ロールバックするライブラリを選択する方法は? もちろん、コインを投げることはできますが、これが最良の選択肢ですか?


    要点をつかむ


    前のトレースを詳しく見てみましょう。 おそらくこれは、ライブラリを決定するのに役立ちます。

     /** * 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 java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1は、 mPoolがサイズ10の配列であることを意味しmPoolが、 mPoolSize=-1です。

    さて、 mPoolSize=-1どのように機能mPoolSize=-1か? 上記のrecycleメソッドに加えて、 mPoolSizeを変更する唯一の場所は、 SimplePoolクラスのacquireメソッドです。

     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これはどのように可能ですか?

    Android Studioにブレークポイントを配置し、アプリケーションの起動時に何が起こるかを確認します。 if 、ここにif条件があり、このコードは正常に機能するはずです!

    最後に、啓示!



    DynamicFromMap静的リンクDynamicFromMap参照してください。

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

    慎重に設定されたブレークポイントで[Play]ボタンを数十回クリックすると、 mqt_native_modulesスレッドがReact Nativeを使用してSimplePool.acquireおよびSimplePool.release関数を呼び出し、Reactコンポーネントのスタイルプロパティを制御することがわかります(コンポーネントwidthプロパティ以下)



    ただし、メインストリームmainからもアクセスされます。



    上記では、通常、 react-native-svgコンポーネントのメインスレッドのfillプロパティを更新するfill使用されることがわかります。 実際、 react-native-svgライブラリは、ネイティブsvgアニメーションのパフォーマンスを改善するために、 7番目のバージョンでのみ DynamicFromMap使用を開始しDynamicFromMap

    And-and-and ...関数は2つのスレッドから呼び出すことができますが、 DynamicFromMapはスレッドセーフな方法でSimplePool使用しません。 「スレッドセーフ」と言いますか?

    スレッドの安全性、ちょっとした理論


    シングルスレッドJavaScriptでは、開発者は通常、スレッドセーフに対処する必要はありません。

    一方、Javaは、並列プログラムまたはマルチスレッドプログラムの概念をサポートしています。 複数のスレッドが同じプログラム内で実行でき、潜在的に一般的なデータ構造にアクセスできるため、予期しない結果が生じる場合があります。

    簡単な例を見てみましょう。下の画像は、フローAとBが並行していることを示しています。

    • 整数を読み取ります。
    • 値を増やします。
    • 彼を返します。


    ストリームBは、ストリームAが更新する前にデータ値に潜在的にアクセスできます。 最終的な値が19になるように、2つの別個のステップが必要でした。 代わりに、 18を取得できます。 データの最終状態がフロー操作の相対的な順序に依存するこのような状況は、競合状態と呼ばれます。 問題は、この状態が常に発生するとは限らないことです。 おそらく、上記の場合、スレッドBは値を増やす前に別のジョブを持っているため、スレッドAが値を更新するのに十分な時間が与えられます。 これは、ランダム性と障害を再現できないことを説明しています。

    データ構造は、競合状態のリスクなしに複数のスレッドで同時に操作を実行できる場合、スレッドセーフと見なされます。

    あるスレッドが特定のデータ要素を読み取る場合、別のスレッドはこの要素を変更または削除する権利を持たないようにする必要があります(これを原子性と呼びます)。 前の例では、更新サイクルがアトミックであれば、競合状態を回避できました。 スレッドBは、スレッドAが操作を完了するまで待機してから、自身を開始します。

    私たちの場合、これは起こるかもしれません:



    DynamicFromMapDynamicFromMapへの静的リンクが含まれているため、いくつかのDynamicFromMap呼び出しは異なるスレッドから行われ、同時にSimplePool acquireメソッドをSimplePoolます。

    上の図では、スレッドAはメソッドを呼び出して条件をtrueと評価していますが、 mPoolSize (スレッドBと組み合わせて使用​​)の値を減らすことはまだできていませんが、スレッドBもこのメソッドを呼び出して条件をtrueと評価します 。 その後、各呼び出しはmPoolSizeの値をmPoolSize 、「不可能な」値になります。

    訂正


    修正オプションを検討して、まだブランチに参加していないreact-nativeのプール要求を発見しました。この場合、スレッドセーフが提供されます。



    次に、ユーザー向けにReact Nativeの修正バージョンを公開しました。 クラッシュはようやく修正されました。


    そのため、Jenick Duplessis(React Nativeコアへの貢献者)とMichael Sand( react-native-svgメンテナー)のおかげで、パッチはReact Native 0.57の次のマイナーバージョンに含まれています。

    このバグを修正するには多少の努力が必要でしたが、react-nativeとreact-native-svgを深く掘り下げる絶好の機会でした。 優れたデバッガーといくつかの適切に配置されたブレークポイントが重要です。 この話から何か有益なことも学んだことを願っています!

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


All Articles