Melakukan debug bug yang tidak bisa diputar

Pada 10 Oktober 2018, tim kami merilis versi baru aplikasi pada React Native. Kami senang dan bangga karenanya.

Tapi kengeriannya adalah sesuatu: setelah beberapa jam, jumlah kegagalan untuk Android tiba-tiba meningkat.


10.000 macet untuk Android

Alat pemantauan kecelakaan Sentry kami menjadi gila.

Dalam semua kasus, kita melihat kesalahan seperti JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView" .

Di Bereaksi Asli, ini biasanya terjadi jika Anda menetapkan properti dengan tipe yang salah. Tetapi mengapa tidak muncul kesalahan saat pengujian? Pada kami, setiap pengembang dengan hati-hati menguji rilis baru pada beberapa perangkat.

Kesalahan juga tampak agak acak, mereka tampaknya jatuh pada kombinasi properti dan tipe shadow node. Sebagai contoh, berikut adalah tiga yang pertama:

  • 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

Tampaknya kesalahan terjadi pada perangkat apa pun dan di versi Android mana pun, dilihat dari laporan Sentry.


Sebagian besar mogok untuk Android 8.0.0 mogok, tetapi ini konsisten dengan basis pengguna kami

Ayo mainkan kembali!


Jadi, langkah pertama sebelum memperbaiki bug adalah mereproduksi, kan? Untungnya, berkat Sentry log, kami dapat mengetahui apa yang dilakukan pengguna sebelum terjadi kerusakan.

Ta-a-ak, mari kita lihat ...



Hmm, dalam sebagian besar kasus, pengguna cukup membuka aplikasi dan - boom, terjadi crash.

Oke, mari kita coba lagi. Kami menginstal aplikasi pada enam perangkat Android, membukanya dan keluar beberapa kali. Tidak ada kesalahan! Selain itu, tidak mungkin untuk memainkannya secara lokal dalam mode dev.

Oke, itu sepertinya tidak ada gunanya. Kegagalan masih cukup acak dan terjadi pada 10% kasus. Sepertinya Anda memiliki peluang 1 banding 10 aplikasi akan mogok saat startup.

Analisis jejak tumpukan


Untuk mereproduksi kegagalan ini, mari kita mencoba memahami dari mana asalnya ...


Seperti disebutkan sebelumnya, kami memiliki beberapa kesalahan berbeda. Dan setiap orang memiliki jejak yang serupa tetapi sedikit berbeda.

Ok, mari kita ambil yang pertama:

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

Jadi masalahnya ada di android/support/v4/util/Pools.java .

Hmm, kami sangat dalam di perpustakaan dukungan Android, hampir tidak mungkin untuk mendapatkan manfaat apa pun di sini.

Temukan cara lain


Cara lain untuk menemukan akar penyebab kesalahan adalah memeriksa perubahan baru pada rilis terbaru. Terutama yang memengaruhi kode Android asli. Dua hipotesis muncul:

  • Kami memperbarui Navigasi Asli , tempat fragmen asli untuk Android digunakan untuk setiap layar.
  • Kami memperbarui reaksi-asli-svg . Ada beberapa pengecualian yang terkait dengan komponen SVG, tetapi ini jarang terjadi.

Kami tidak dapat mereproduksi kesalahan saat ini, jadi strategi terbaik adalah:

  1. Kembalikan salah satu dari dua pustaka tersebut. Gulir keluar untuk 10% pengguna, yang secara sepele dilakukan di Play Store. Periksa dengan beberapa pengguna jika kegagalan berlanjut. Dengan demikian, kami mengkonfirmasi atau membantah hipotesis tersebut.


    Tetapi bagaimana cara memilih perpustakaan untuk memutar kembali? Tentu saja, Anda bisa melempar koin, tetapi apakah ini pilihan terbaik?


    Langsung ke intinya


    Mari kita lihat lebih dekat jejak sebelumnya. Mungkin ini akan membantu menentukan perpustakaan.

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

    Ada yang gagal. Kesalahan java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 berarti mPool adalah array ukuran 10, tetapi mPoolSize=-1 .

    Oke, bagaimana mPoolSize=-1 ? Selain metode recycle atas, satu-satunya tempat untuk mengubah mPoolSize adalah metode acquire dari kelas 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; } 

    Oleh karena itu, satu-satunya cara untuk mendapatkan nilai mPoolSize negatif adalah menguranginya dengan mPoolSize=0 . Tetapi bagaimana ini mungkin dengan kondisi mPoolSize > 0 ?

    Kami akan meletakkan breakpoints di Android Studio dan melihat apa yang terjadi ketika aplikasi dijalankan. Maksud saya, ini syaratnya, kode ini seharusnya berfungsi dengan baik!

    Akhirnya, wahyu!



    Lihat DynamicFromMap tautan statis ke SimplePool .

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

    Setelah beberapa klik tombol Play dengan mengatur breakpoint dengan hati-hati, kita melihat bahwa thread mqt_native_modules memanggil fungsi SimplePool.acquire dan SimplePool.release menggunakan React Native untuk mengontrol properti style dari komponen React (di bawah properti width komponen)



    Tetapi mereka juga diakses oleh arus utama utama !



    Di atas, kita melihat bahwa mereka digunakan untuk memperbarui properti fill di aliran utama, biasanya untuk komponen react-native-svg ! Memang, perpustakaan react-native-svg mulai menggunakan DynamicFromMap hanya dengan versi ketujuh untuk meningkatkan kinerja animasi svg asli.

    Dan-dan-dan ... suatu fungsi dapat dipanggil dari dua utas, tetapi DynamicFromMap tidak menggunakan SimplePool cara yang aman. "Utas aman," katakan?

    Keamanan utas, sedikit teori


    Dalam JavaScript single-threaded, pengembang biasanya tidak perlu berurusan dengan keamanan utas.

    Java, di sisi lain, mendukung konsep program paralel atau multithreaded. Beberapa utas dapat berjalan dalam program yang sama dan berpotensi mengakses struktur data umum, yang terkadang mengarah ke hasil yang tidak terduga.

    Ambil contoh sederhana: gambar di bawah ini menunjukkan bahwa aliran A dan B adalah paralel:

    • baca bilangan bulat;
    • meningkatkan nilainya;
    • kembalikan dia.


    Stream B berpotensi dapat mengakses nilai data sebelum streaming A memperbaruinya. Kami mengharapkan dua langkah terpisah untuk memberikan nilai akhir 19 . Sebaliknya, kita bisa mendapatkan 18 . Situasi di mana keadaan akhir data tergantung pada urutan relatif dari operasi aliran disebut kondisi balapan. Masalahnya adalah bahwa kondisi ini tidak selalu terjadi setiap saat. Mungkin, dalam kasus di atas, utas B memiliki pekerjaan lain sebelum melanjutkan untuk meningkatkan nilai, yang memberi cukup waktu bagi utas A untuk memperbarui nilai. Ini menjelaskan keacakan dan ketidakmampuan untuk mereproduksi kegagalan.

    Struktur data dianggap sebagai thread aman jika operasi dapat dilakukan secara bersamaan oleh banyak utas tanpa risiko kondisi balapan.

    Ketika satu utas membaca untuk elemen data tertentu, utas lain seharusnya tidak memiliki hak untuk mengubah atau menghapus elemen ini (ini disebut atomicity). Pada contoh sebelumnya, jika siklus pembaruan adalah atom, kondisi balapan bisa dihindari. Thread B akan menunggu sampai thread A menyelesaikan operasi, dan kemudian memulai dengan sendirinya.

    Dalam kasus kami, ini dapat terjadi:



    Karena DynamicFromMap berisi tautan statis ke SimplePool , beberapa panggilan DynamicFromMap datang dari utas yang berbeda, sambil meminta metode acquire di SimplePool .

    Dalam ilustrasi di atas, utas A memanggil metode, mengevaluasi kondisi sebagai benar , tetapi belum berhasil mengurangi nilai mPoolSize (yang digunakan bersama dengan utas B), sementara utas B juga menyebut metode ini dan juga mengevaluasi kondisi sebagai benar . Selanjutnya, setiap panggilan akan mengurangi nilai mPoolSize , menghasilkan nilai "tidak mungkin".

    Koreksi


    Mempelajari opsi koreksi, kami menemukan permintaan kumpulan untuk reaksi-asli , yang belum bergabung dengan cabang - dan menyediakan keamanan utas dalam kasus ini.



    Lalu kami meluncurkan versi tetap dari Bereaksi Asli untuk pengguna. Kecelakaan akhirnya diperbaiki, tepuk tangan!


    Jadi, berkat bantuan Jenick Duplessis (kontributor React Native core) dan Michael Sand (maintain react-native-svg maintainer), tambalan tersebut disertakan dalam versi minor berikutnya dari React Native 0.57 .

    Butuh beberapa upaya untuk memperbaiki bug ini, tetapi itu adalah peluang besar untuk mempelajari lebih dalam tentang reaksi-asli dan reaksi-asli-svg. Debugger yang baik dan beberapa breakpoint yang ditempatkan dengan baik adalah penting. Saya harap Anda juga belajar sesuatu yang berguna dari cerita ini!

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


All Articles