เครื่องมือและประสิทธิภาพ

การแก้ปัญหาเกี่ยวกับหน่วยความจำด้วย Firebase Crashlytics

ลำโพง

  • Polina Govorkova

    นักพัฒนาซอฟต์แวร์ระดับอาวุโส

เกี่ยวกับ

เกี่ยวกับการเสวนา

ทำให้หน่วยความจำที่ใช้ของแอปมีประสิทธิภาพและไม่เกิดข้อผิดพลาดในการแบ่งกลุ่มด้วย Firebase Crashlytics เซสชันนี้จะสำรวจชุดเครื่องมือแก้ปัญหาหน่วยความจำที่ครอบคลุมซึ่งมีอยู่ในคอนโซล Firebase และใน Android Studio เราจะเริ่มด้วยการพูดถึงวิธีใช้ประโยชน์จากข้อมูลเชิงลึกเกี่ยวกับ ANR และรายงาน GWP-ASan เพื่อวิเคราะห์ข้อบกพร่องด้านหน่วยความจำที่ตรวจจับได้ยาก จากนั้นจะเจาะลึกเครื่องมือล่าสุดสำหรับเหตุการณ์หน่วยความจำไม่เพียงพอ (OOM) และการปิดแอปเนื่องจากหน่วยความจำไม่เพียงพอ (MLK) ซึ่งออกแบบมาเพื่อช่วยให้คุณรับมือและปรับตัวให้เข้ากับขีดจำกัดหน่วยความจำใหม่ที่เปิดตัวใน Android 17 โดยเฉพาะ

สรุป

  • ข้อผิดพลาดแอปพลิเคชันไม่ตอบสนองมักเกิดจากคอขวดในเบื้องหลังหรือหน่วยความจำหมด ไม่ใช่ภาระงาน CPU ที่ใช้งานอยู่ Firebase Crashlytics จะค้นหา ApplicationExitInfo เพื่อแสดงการติดตามแบบมัลติเธรดและระบุเธรดที่บล็อกรูท
  • ข้อขัดข้องเนื่องจากหน่วยความจำไม่เพียงพอจะเกิดขึ้นเมื่อแอปใช้หน่วยความจำ Java Heap เกินขีดจำกัดระหว่างรันไทม์ Crashlytics ผสานรวมกับ ProfilingManager ของ Android 17 เพื่อรวบรวมสแนปชอตฮีปที่ถอดรหัสแล้ว ซึ่งตรวจสอบได้ใน Perfetto หรือผ่าน Android CLI
  • ตัวจำกัดหน่วยความจำของ Android 17 จะสิ้นสุดกระบวนการแต่ละรายการที่ใช้ RAM ทั้งหมดมากเกินไป รวมถึงการจัดสรรแบบเนทีฟ Crashlytics จะจัดกลุ่มการสิ้นสุดเหล่านี้ไว้ในแดชบอร์ดควบคู่ไปกับฮีปดัมป์เพื่อช่วยแยกหน่วยความจำรั่วไหลของ C++ ออกจากการจัดสรรฮีปที่มีการจัดการ
  • ความเสียหายของหน่วยความจำแบบเนทีฟมักปรากฏในเทรดของระบบมากกว่าในโค้ดของแอป Crashlytics รองรับ GWP-ASan และ Arm MTE และรายงาน GWP-ASan จะระบุตำแหน่งการจัดสรรและการจัดสรรคืนที่แน่นอนสำหรับข้อบกพร่อง เช่น การใช้งานหลังช่วงใช้ฟรี (Use After Free)

แหล่งข้อมูล

Gemini Notebook

หัวข้อ

  • firebase-crashlytics
  • android-memory
  • anr
  • หน่วยความจำไม่เพียงพอ
  • profiling-manager
  • perfetto
  • gwp-asan
  • arm-mte

ข้อความถอดเสียง

บทนำ

  • Polina Govorkova

    สวัสดีทุกคน

  • ยินดีต้อนรับสู่การแก้ปัญหาเกี่ยวกับหน่วยความจำด้วย Firebase Crashlytics

  • เดี๋ยวนะ ชื่อตำแหน่งในองค์กรคืออะไร

  • มาลองเปลี่ยนแนวกันสักหน่อย

  • การทำให้แอปมีประสิทธิภาพและทำงานได้อย่างราบรื่นด้วย Firebase Crashlytics

  • โอ้ ฟังดูดีขึ้นนะ

  • ฉันชื่อ Polina เป็นนักพัฒนาซอฟต์แวร์อาวุโสในทีม Firebase Crashlytics และเป็นผู้บรรยายในวันนี้

  • เมื่อถึงจุดหนึ่งในวงจรการพัฒนา คุณจะรู้สึกว่าได้แก้ไขข้อบกพร่องทั้งหมดแล้ว

  • ข้อผิดพลาดในการพิมพ์การแยกวิเคราะห์ JSON, การตรวจสอบตัวชี้ค่า Null ที่ถูกลืม ทั้งหมดนี้จะหายไป

  • คุณอาจถามตัวเองว่า "ฉันทำเสร็จแล้วหรือยัง"

  • เรามีข่าวร้ายจะบอกคุณ เพราะคุณไม่ได้เป็นเช่นนั้น

  • เพราะสิ่งที่จะเหลืออยู่คือปัญหาที่ยากจริงๆ

  • และผมขอยืนยันในวันนี้ว่าปัญหาที่ยากที่สุด 2 หมวดหมู่ที่คุณจะต้องเผชิญคือความสามารถในการตอบสนองและหน่วยความจำ

  • ข่าวดีคือเราพร้อมช่วยเหลือคุณ

  • วันนี้เราจะแนะนำเครื่องมือที่มีอยู่และเครื่องมือใหม่ใน Android และ Crashlytics เพื่อช่วยคุณค้นหาสาเหตุของปัญหาการตอบสนองที่แสดงผ่านข้อผิดพลาดแอปพลิเคชันไม่ตอบสนอง รวมถึงข้อยกเว้นหน่วยความจำไม่เพียงพอ หน่วยความจำรั่วไหล และความเสียหายของหน่วยความจำแบบเนทีฟ

  • มาดูรายละเอียดกัน

  • ทำไมปัญหาด้านการตอบสนองและหน่วยความจำจึงเป็นที่รู้จักกันดี

  • ลองดูข้อบกพร่องแบบคลาสสิก: คุณประกาศสตริง ตั้งค่าเป็น Null และพยายามอ่านความยาวของสตริง ณ จุดใดจุดหนึ่ง

  • สิ่งที่คุณจะได้รับ

  • NullPointerException ที่คุ้นเคย

  • แต่จริงๆ แล้วชีวิตที่นี่ง่ายมาก เพราะสแต็กเทรซของข้อยกเว้นจะชี้ไปยังบรรทัดโค้ดที่ทำให้แอปขัดข้องอย่างชัดเจน

  • ดูสแต็กเทรซทางด้านซ้าย

    java.lang.NullPointerException
      at MainActivity.onCreate(MainActivity.kt:42)
      at android.app.Activity.performCreate
      at android.os.Handler.dispatchMessage
      at android.os.Looper.loopOnce
      at android.app.ActivityThread.main
  • เราจะเห็นได้ทันทีว่าปัญหาอยู่ที่บรรทัด 42 ใน MainActivity.kt

  • และแนวทางนี้ใช้ได้กับข้อบกพร่องเล็กๆ น้อยๆ ส่วนใหญ่

  • ตอนนี้เมื่อเกิดปัญหาด้านการตอบสนองหรือหน่วยความจำ เช่น แอปพลิเคชันไม่ตอบสนองทางด้านขวา สแต็กเทรซแทบจะไม่ชี้ไปยังสาเหตุที่แท้จริงเลย

    "main" prio=5 tid=1 Blocked
      | group="main" sCount=1 dsCount=0 flags=1
      | state=S schedstat=( 1450239 48192 145 )
      at DataManager.getData(DataManager.kt:25)
      - waiting to lock <0x0a1b> held by thread 12
      at MainActivity.onResume(MainActivity.kt:50)
      at android.app.Activity.performResume
      at android.os.Handler.dispatchMessage
      at android.os.Looper.loopOnce
      at android.app.ActivityThread.main
  • ในกรณีที่ดีที่สุด ระบบจะแสดงเฉพาะผลลัพธ์ ซึ่งเป็นเทรดหรือการจัดสรรที่ไม่เป็นอันตรายซึ่งบังเอิญทำงานอยู่ในขณะที่แอปค้างหรือหน่วยความจำไม่เพียงพอ

  • ทั้งที่สาเหตุของปัญหาที่แท้จริงเกิดขึ้นก่อนหน้านั้นนานแล้ว หรืออาจเกิดขึ้นที่อื่นโดยสิ้นเชิง

  • โชคดีที่เมื่อเกิดปัญหานี้ขึ้น คุณไม่ต้องคาดเดาไปเอง เพราะ Firebase Crashlytics มีคำตอบให้เรา

  • แต่ก่อนที่เราจะเริ่มแก้ปัญหา โปรดยกมือขึ้นหากคุณคุ้นเคยกับ Crashlytics

  • เยี่ยมเลย

  • ดีใจที่ได้เห็นทุกคนยกมือขึ้น

  • สำหรับผู้ที่เพิ่งเคยใช้งาน Firebase Crashlytics คือโซลูชัน Error Reporting แบบเรียลไทม์ที่ให้ข้อมูลภาคสนาม พร้อมด้วยข้อมูลเชิงลึกตามบริบทและที่ขับเคลื่อนด้วย AI, เคล็ดลับการแก้ปัญหา และสัญญาณที่ชัดเจนเกี่ยวกับรุ่นใหม่ๆ ทั้งหมดนี้เพื่อช่วยให้คุณแก้ไขปัญหาได้เร็วขึ้น

  • และ Crashlytics ก็เป็นที่ชื่นชอบของนักพัฒนาซอฟต์แวร์ โดยเป็นโซลูชันการรายงานข้อขัดข้องที่ได้รับความนิยมมากที่สุดบนอุปกรณ์เคลื่อนที่ ซึ่งขับเคลื่อนแอปที่มาพร้อมเครื่องมากกว่า 370,000 แอป รวมถึง 57% ของแอป Android ยอดนิยม

  • มาดูตัวอย่างที่ชัดเจนเพื่อดูว่า Crashlytics ช่วยเราติดตามข้อบกพร่องในการทำงานได้อย่างไร

  • วันนี้ฉันอยากเล่าเรื่องราวของ ChromaClip ให้คุณฟัง

  • ซึ่งเป็นเครื่องมือแยก Color Palette วิดีโอที่ฉันสร้างขึ้น และฉันอยากจะบอกคุณว่าฉันจัดการกับข้อบกพร่องด้านการปรับเปลี่ยนตามอุปกรณ์และหน่วยความจำอย่างไรในระหว่างที่พัฒนาเครื่องมือนี้

  • ดังนั้น แนวคิดเบื้องหลัง ChromaClip จึงเรียบง่ายมาก นั่นคือผู้ใช้เลือกวิดีโอ เช่น คลิปพระอาทิตย์ตกที่สวยงามหรือฉากที่มีการปรับสีที่ยอดเยี่ยม จากนั้นแอปจะวิเคราะห์เฟรมเพื่อสร้างจานสีที่กำหนดเอง ซึ่งผู้ใช้สามารถนำไปใช้กับการออกแบบ ศิลปะดิจิทัล มู้ดบอร์ด หรืออะไรก็ได้

  • เรื่องราวของเราเริ่มต้นขึ้นก่อนที่ผมจะเปิดตัวเวอร์ชัน 1.0

  • ฉันอยากให้แน่ใจว่าฟีเจอร์นี้ทำงานได้อย่างเสถียรก่อนเปิดตัว จึงได้ทำการทดสอบขั้นสุดท้ายในอุปกรณ์ 2 เครื่อง โดยเลือกวิดีโอ แตะปุ่มเพื่อดึง Color Palette ออกมา และโชคดีที่ได้เห็นกล่องโต้ตอบที่นักพัฒนาแอป Android ทุกคนชื่นชอบ

  • ChromaClip ไม่ตอบสนอง

  • นี่คือข้อผิดพลาดเกี่ยวกับแอปพลิเคชันไม่ตอบสนอง หรือที่เรียกว่า ANR

การแก้ไขข้อบกพร่องและการวิเคราะห์ ANR

  • Polina Govorkova

    แล้วเราจะทำอย่างไรในสถานการณ์นี้ นอกเหนือจากการตื่นตระหนก

  • ตามสัญชาตญาณแรกของเรา เนื่องจากโทรศัพท์ทดสอบเสียบเข้ากับแล็ปท็อปโดยตรง เราจึงเปิด Android Studio และตรวจสอบ Logcat

  • เราจะเห็นสแต็กเทรซของเทรดหลัก

    14:32:15.105  1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
        PID: 14205
        Reason: Input dispatching timed out
        Dumped ANR trace to /data/anr/traces.txt
    "main" prio=5 tid=1 Native
      | group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
      | sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
      | state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
      | stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
      at sun.misc.Unsafe.park(Native method)
      - waiting on an unknown object
      at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
      at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
      at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
      at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
      at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
      at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)
  • ข้อมูลนี้บอกอะไรกับเรา

  • อันดับแรก ที่ด้านบนสุด เราได้รับการยืนยันว่าเป็น ANR ที่ทำงานใน MainActivity

  • มีอะไรอีกไหม

  • เราเห็นสาเหตุแล้ว ซึ่งก็คือการส่งอินพุตหมดเวลา

  • ซึ่งหมายความว่าผู้ใช้แตะหน้าจอ

  • ใช่ เราทำ

  • แต่เทรดหลักไม่ว่างจนไม่สามารถประมวลผลเหตุการณ์นั้นภายในระยะหมดเวลาของ Watchdog 5 วินาทีของ Android

  • จากนั้นเรายังเห็นว่าระบบดัมพ์ไฟล์ลงใน traces.txt และด้านล่างนั้น logcat จะพิมพ์สแต็กเทรซสำหรับเทรดหลักของเรา

  • โปรดสังเกตว่าสถานะเท่ากับ S ตรงนั้น

  • เธรดหลักของเราไม่ได้ใช้ CPU อย่างต่อเนื่องด้วยซ้ำ

  • S หมายถึงการนอนหลับ (Sleeping)

  • เหตุใดเทรดหลักจึงหยุดทำงานขณะที่เรารอ

  • มาดูสแตกเฟรมอย่างละเอียดกัน

  • ตรงกลางของการติดตามนี้ เราจะเห็นว่าเทรดหลักของเราอยู่ใน Unsafe.park เพื่อรอออบเจ็กต์ที่ไม่รู้จัก

  • ทำไมเราถึงไม่รู้ว่ากำลังรออะไรอยู่

  • อันที่จริงแล้ว สำหรับ ANR บางรายการ เช่น เมื่อเทรดถูกบล็อกโดยการล็อกแบบซิงค์ของ Java แบบคลาสสิก สแต็กเทรซของเทรดหลักจะบอกเราว่ากำลังรออะไรอยู่ และอะไรที่กำลังถือล็อกอยู่

  • แต่เมื่อคุณใช้พื้นฐานการทำงานพร้อมกันในระดับที่สูงขึ้นเบื้องหลัง สิ่งที่ Logcat บอกเราก็คือเรากำลังรอออบเจ็กต์ที่ไม่รู้จัก

  • ดังนั้น เราจึงไม่ทราบว่าเรากำลังรอออบเจ็กต์ใด เทรดผู้ปฏิบัติงานในเบื้องหลังใดที่เรากำลังรออยู่ หรือเหตุใดจึงใช้เวลานานกว่า 5 วินาที

  • และเนื่องจากเรากำลังทดสอบในเครื่อง เราจึงสามารถดึง traces.txt นั้นออกจากอุปกรณ์และเลื่อนดูเทรดที่ไม่ได้ทำงานหลายสิบรายการด้วยตนเอง และอาจตามล่าหาผู้กระทำผิดหรืออาจใช้โทเค็นเพื่อถาม AI

  • ในบางกรณี วิธีนี้อาจได้ผล และในความเป็นจริง วิธีนี้จะได้ผลสำหรับกรณีนี้

  • แต่ฉันไม่อยากอ่านดัมพ์เทรดดิบด้วยตนเองจริงๆ และบอกตามตรงว่านั่นเป็นวิธีที่ล้าสมัยมาก ฉันรู้ว่าแนวทางนี้ใช้ไม่ได้เลยเมื่อเราอยู่ในขั้นตอนการผลิต

  • เราแค่ไม่มีสิทธิ์เข้าถึงไฟล์ traces.txt ในโทรศัพท์ของผู้ใช้โดยตรงใช่ไหม

  • โชคดีที่เราผสานรวมกับ Firebase Crashlytics

  • เนื่องจากช่วยลดความจำเป็นในการค้นหาและเข้าถึงสาเหตุของ ANR ได้แม้ในเวอร์ชันที่ใช้งานจริง

  • วิธีการ

  • นี่คือจุดที่ ApplicationExitInfo API ของ Android เข้ามามีบทบาท

  • ตั้งแต่ Android 11 เป็นต้นไป เมื่อใดก็ตามที่ระบบแจ้งว่ามี ANR หรือสิ้นสุดกระบวนการของแอป ระบบปฏิบัติการจะบันทึกข้อมูลเมตาการออกที่สมบูรณ์ ซึ่งรวมถึงสาเหตุที่แน่ชัดของข้อขัดข้อง สถานะหน่วยความจำ และการติดตามของระบบที่สมบูรณ์สำหรับทุกชุดข้อความที่ทำงานอยู่ในกระบวนการของคุณในขณะนั้น

  • และที่ดีที่สุดคือคุณไม่จำเป็นต้องติดตั้งปลั๊กอินเพิ่มเติมหรือเขียนโค้ดแม้แต่บรรทัดเดียว

  • ตราบใดที่คุณใช้ Crashlytics SDK 18.3.5 ขึ้นไป ฟีเจอร์นี้จะใช้งานได้ทันที

  • ดังนั้น ทันทีที่แอปรีสตาร์ท Crashlytics SDK จะค้นหา ApplicationExitInfo โดยอัตโนมัติ บันทึกดัมพ์แบบมัลติเธรดทั้งหมดพร้อมกับข้อมูลการตรวจวัดระยะไกลของอุปกรณ์ และส่งไปยังแบ็กเอนด์ของ Crashlytics

  • มาเปิดคอนโซล Crashlytics และดูว่ามีข้อมูลอะไรบ้าง

  • นี่คือมุมมองรายละเอียดปัญหาในคอนโซล Firebase Crashlytics สำหรับ ANR และเราจะเห็น DefaultDispatcher worker 1 รายการทันทีในแท็บสแต็กเทรซ

    DefaultDispatcher-worker-1 (Root blocking)
    
    dalvik.system.VMRuntime.allocateNode (Native method)
    android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
    android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
    com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
    com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
    kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
    kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
    java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
    java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
    java.lang.Thread.run (Thread.java:923)
  • อันที่จริงแล้ว เทรดนี้เป็นเทรดที่ Crashlytics แสดงไว้ที่ด้านบนสุด ซึ่งเป็นจุดคอขวดสำหรับเทรดหลักของเรา และ Crashlytics จะทำเครื่องหมายด้วยป้ายที่ชื่อว่า "การบล็อกรูท"

  • ตามที่เคล็ดลับเครื่องมือบนป้ายอธิบายไว้ เทรดผู้ปฏิบัติงานนี้เป็นเทรดที่บล็อกเทรดที่ทริกเกอร์ ANR จริงๆ

  • นอกจากนี้ Crashlytics ยังมีป้ายอัจฉริยะสำหรับสาเหตุอื่นๆ ด้วย

  • ดังนั้น หากเทรดค้างขณะอ่านไฟล์ขนาดใหญ่หรือทำการค้นหาฐานข้อมูลที่ช้ามาก แล้วทำให้เกิด ANR เทรดจะได้รับการติดแท็กเป็นการบล็อกรูท I/O

  • และหากมีเทรด 2 เทรดที่ติดขัดเนื่องจากรอซึ่งกันและกัน Crashlytics จะติดแท็กเทรดเหล่านั้นว่าติดล็อก

  • คราวนี้เรามาดูสแตกเฟรมภายในเทรดการบล็อกรูทกัน

  • เรามี PaletteExtractor processVideo ที่เรียกใช้ FrameDecoder getUncompressedFrame ซึ่งไฮไลต์เป็นสีน้ำเงิน และติดอยู่ภายใน createBitmap โดยพยายามจัดสรรหน่วยความจำสำหรับเฟรมเดียว

  • เนื่องจากเรารู้ว่าไม่ได้รอการล็อกอื่น และเรารู้ว่าไม่ได้อยู่ในการเรียกใช้เครือข่าย ผู้ต้องสงสัยหลักของเราจึงต้องเป็นหน่วยความจำ และเราสามารถยืนยันได้ทันทีโดยตรวจสอบหน่วยความจำว่างของอุปกรณ์ในคอนโซล Crashlytics ด้วยเช่นกัน

  • ดู 2 ตำแหน่งที่ Crashlytics แสดงข้อมูลนี้

  • ในแท็บข้อมูลของเหตุการณ์ที่เฉพาะเจาะจงซึ่งอยู่ถัดจากแท็บสแต็กเทรซที่เราเพิ่งสำรวจไป และที่ด้านขวาบนซึ่งแสดงค่าเฉลี่ยของเหตุการณ์ทั้งหมด

  • และในทั้ง 2 กรณีนี้ ดูเหมือนว่าแทบจะไม่มีอะไรเลย ไม่ถึง 100 KB ด้วยซ้ำ

  • ลองคิดดูนะ

  • สิ่งที่เกิดขึ้นที่นี่คือเนื่องจากกระบวนการไม่มีหน่วยความจำ Android Runtime จึงใช้เวลาทั้งหมดไปกับการรัน Garbage Collection อย่างบ้าคลั่งเพื่อพยายามหาพื้นที่ว่างให้เพียงพอสำหรับบิตแมปถัดไป แต่ก็ไม่เคยได้เลย

  • การหยุดการทำงานของเทรดผู้ปฏิบัติงานและทำให้ UI ไม่ทำงานเป็นเวลานานกว่า 5 วินาที

  • เราจึงมีเคล็ดลับง่ายๆ มาฝาก

  • ก่อนที่คุณจะได้รับ เมื่อใดก็ตามที่คุณได้รับ ANR ที่จับคู่กับ RAM ที่มีพื้นที่ว่างเหลือน้อยแบบนี้ นั่นเป็นหลักฐานที่ชัดเจนว่า ANR เป็นปัญหาหน่วยความจำรั่วไหลหรือปัญหา Bloat

  • คราวนี้คำถามคือแอปของเราเป็นเหยื่อหรือเป็นตัวที่กักตุนหน่วยความจำไว้เอง

  • ลองย้อนกลับไปดูรายการปัญหาของโปรเจ็กต์และดูว่ามีสิ่งอื่นใดที่เกี่ยวข้องอีกหรือไม่

การตรวจสอบข้อยกเว้นเกี่ยวกับหน่วยความจำไม่เพียงพอ

  • Polina Govorkova

    และดูตรงนั้นสิ

  • ที่ด้านล่าง เราจะเห็นรายการปัญหา และเหนือ ANR ที่มี 8 เหตุการณ์ เราจะเห็นข้อยกเว้น OutOfMemory

  • ดังนั้น เราอาจเป็นผู้ร้ายตัวจริงก็ได้

  • และนั่นก็เป็นเบาะแสสำคัญ

  • เนื่องจากใน Android ข้อผิดพลาด OutOfMemoryError ของ Java หรือเรียกสั้นๆ ว่า OOM ไม่ได้หมายความว่าโทรศัพท์มี RAM เหลือน้อยเท่านั้น

  • ซึ่งหมายความว่ากระบวนการของแอปของเราเองใช้หน่วยความจำเกินขีดจำกัดฮีป Java ของ Android Runtime

  • ดังนั้น ณ จุดนี้ ทฤษฎีการทำงานของฉันคือ ANR และ OOM เป็น 2 ด้านของเหรียญเดียวกัน

  • ChromaClip อาจติดขัดในการรวบรวมขยะนานพอที่จะทำให้เกิด ANR หรือใช้ฮีปจนหมด ซึ่งส่งผลให้เกิด OOM ทั้งนี้ขึ้นอยู่กับช่วงเวลาระหว่างการทดสอบ

  • มาตรวจสอบข้อยกเว้น OutOfMemory เพื่อให้แน่ใจกัน

  • โดยเริ่มจากสแต็กเทรซ

    java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
     at android.graphics.Bitmap.createBitmap
     at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
     at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)
  • โปรดรอสักครู่

  • Bitmap.createBitmap ฟังดูคุ้นๆ นะ

  • เราเคยเห็นอันนั้นมาก่อนไหม

  • แต่ครั้งนี้ให้ดูว่ามีการเรียกใช้จากที่ใด

  • จัดสรร 256 ไบต์ไม่สำเร็จเมื่อเปิดกิจกรรมการตั้งค่า

  • เรากำลังตกหลุมพรางเดิมอีกครั้ง

  • หากแอปขัดข้องเนื่องจากไม่สามารถขยายไอคอนการตั้งค่าขนาดเล็กได้ กิจกรรมการตั้งค่าก็ไม่ใช่ตัวร้าย

  • เป็นเพียงผู้เรียกที่โชคร้ายซึ่งขอหน่วยความจำเพียงเล็กน้อยเมื่อไม่มีหน่วยความจำเหลืออยู่

  • ดังนั้น หากต้องการค้นหาตัวการที่แท้จริง เราต้องซูมออกและดูทุกอย่างที่อยู่ในหน่วยความจำเมื่อเกิดข้อขัดข้อง

  • กล่าวคือ เราต้องการฮีปดัมป์ของ Java

  • ที่โต๊ะทำงาน หากคุณทราบวิธีทริกเกอร์ OOM คุณสามารถดึงฮีปดัมป์ของ Java ด้วยตนเองได้โดยใช้เครื่องมือสร้างโปรไฟล์หน่วยความจำของ Android Studio

  • แต่เมื่อ OOM เกิดขึ้นโดยไม่คาดคิดในการทดสอบหรือในสภาพแวดล้อมจริง วิธีนี้จะใช้ไม่ได้ผลอีกครั้ง

การบันทึกและการวิเคราะห์ฮีปดัมป์

  • Polina Govorkova

    นี่จึงเป็นอีกหนึ่งช่วงเวลาที่ Firebase Crashlytics ได้เฉิดฉาย

  • นอกเหนือจากข้อมูลการออกจากแอปพลิเคชันแล้ว ตอนนี้ Crashlytics ยังผสานรวมกับ ProfilingManager API ของ Android ด้วย

  • ProfilingManager API ใน Android 17 ได้รับการเพิ่มประสิทธิภาพด้วยการทำโปรไฟล์หน่วยความจำที่ระบบทริกเกอร์ ซึ่งเป็นสิ่งที่เราต้องการในกรณีนี้

  • ซึ่งหมายความว่าเมื่อแอปพบข้อผิดพลาด OutOfMemory หรือละเมิดขีดจำกัดหน่วยความจำของระบบ ระบบปฏิบัติการจะจับภาพสแนปชอตฮีปในเบื้องหลัง จากนั้นใน Crashlytics คอลเล็กชันที่ทริกเกอร์จะพร้อมใช้งานทันทีโดยไม่มีค่าใช้จ่าย ตราบใดที่คุณใช้ Crashlytics SDK 20.1.0 ขึ้นไป

  • จากนั้น Crashlytics จะจัดกลุ่มการหยุดทำงานเนื่องจาก OOM และตัวจำกัดหน่วยความจำโดยอัตโนมัติ เราจะพูดถึงเรื่องนี้เพิ่มเติมในภายหลังในแดชบอร์ด

  • และเมื่อคุณเลือกใช้การรวบรวมฮีปดัมพ์ผ่านคอนโซล Firebase หรือ CLI แล้ว Crashlytics จะเริ่มรวบรวมสแนปชอตฮีปที่บันทึกไว้โดยอัตโนมัติ และยกเลิกการสร้างความสับสนให้คุณ โดยพื้นฐานแล้ว คุณจะได้รับข้อมูลเอกซเรย์แบบเต็มของทุกอย่างที่อยู่ในฮีป Java ของแอป ณ ขณะที่เกิดข้อขัดข้อง

  • นี่คือลักษณะของ OOM ของ ChromaClip ในคอนโซล Crashlytics เมื่อบันทึกฮีปแล้ว

  • ที่ด้านบน คุณจะเห็นคำอธิบายสาเหตุที่ Java ใช้ฮีปจนหมดและสาเหตุที่เกิดเหตุการณ์ดังกล่าว

  • จากนั้น คุณจะเห็นเคล็ดลับการแก้ปัญหาที่เป็นประโยชน์และตัวชี้ไปยังแหล่งข้อมูลภายนอกด้วย

  • ในแท็บสแต็กเทรซด้านล่าง เหนือกิจกรรมการตั้งค่าที่ทำให้เข้าใจผิดนั้น Crashlytics จะแจ้งว่าสแต็กเทรซ OOM ไม่ใช่เชิงกำหนด เพื่อให้ผู้ใช้ไม่ต้องเสียเวลาดูสแต็กเทรซเหมือนที่เราเพิ่งทำไป

  • จากนั้น Crashlytics จะแนบไฟล์ Perfetto ที่ถอดรหัสซอร์สโค้ดที่สร้างความสับสนแล้ว ซึ่งก็คือฮีปดัมป์สำหรับเซสชันนั้น

  • และเราจะดาวน์โหลดไฟล์ได้จากที่นี่

  • เราสามารถเรียกดูดัมพ์ทั้งหมดใน Google Cloud Storage หรือจะเปิดใน Perfetto UI ก็ได้

  • ดังนั้น การคลิก "วิเคราะห์" ใน Perfetto UI จะเปิดเครื่องมือสำรวจฮีปดัมป์ของ Perfetto

  • คราวนี้ ใครเคยเห็นอันนี้แล้วบ้าง ยกมือขึ้น

  • โอเค มีคนยกมือ 2-3 คน

  • สำหรับฉันแล้ว มุมมองนี้ดูน่ากลัวมาก แต่ในกรณีนี้ ปัญหากลับค่อนข้างง่าย

  • ดูชุดแถบขนาดใหญ่ที่ครอบคลุมเกือบตลอดทั้งฮีป Java

  • ดังนั้น ส่วนใหญ่แล้วจะเป็นอาร์เรย์ของออบเจ็กต์คู่ ซึ่งแต่ละออบเจ็กต์จะรวมสีและอินสแตนซ์สตริงบางรายการไว้

  • โห

  • ฉันคิดว่าฉันรู้ว่าเกิดอะไรขึ้น

  • โค้ดของเราจะจัดสรรสีและข้อมูลสนับสนุนบางอย่างสำหรับทุกพิกเซลในทุกเฟรม และเก็บข้อมูลเหล่านั้นไว้หลายล้านรายการในลิสต์

  • จึงไม่น่าแปลกใจที่กิจกรรมการตั้งค่าไม่สามารถจัดสรร 256 ไบต์เหล่านั้นได้

  • อย่างน้อยก็ไขปริศนาได้ข้อหนึ่งแล้ว

  • มาแก้ปัญหากัน

  • และในตอนนี้ เรามี 2 ตัวเลือก

  • ดังนั้น เนื่องจาก Perfetto ได้ชี้ให้เราเห็น PaletteExtractor แล้ว เราจึงสามารถเปิด IDE และเขียนลูปใหม่ด้วยตนเอง หรือใช้ Agent การเขียนโค้ด AI ก็ได้

  • หรือเราจะข้ามการตรวจสอบฮีปด้วยตนเองไปเลย แล้วใช้ประโยชน์จาก Android CLI แทนก็ได้

    # 1. Install Android CLI & Perfetto skills
    $ android skills add android-cli perfetto
    
    # 2. List recent heap dumps from Crashlytics
    $ android signals heaps list --kind oom --timeframe=90d
    
    # 3. Download a heap dump as a Perfetto trace
    $ android signals heaps download <heap-dump-id> \
        --output ./heaps
    
    # 4. Query top retained objects in the heap dump
    $ trace_processor query ./heaps/oom.perfetto-trace ...
    
    # 5. Fetch matching Crashlytics events
    $ android signals heaps describe <heap-dump-id>
  • ดังนั้น การติดตั้งทักษะ Android CLI และ Perfetto จะช่วยให้คุณแสดงรายการและดาวน์โหลดฮีปดัมป์ของ Crashlytics ที่ผ่านการถอดรหัสซอร์สโค้ดแล้ว ค้นหาออบเจ็กต์ที่คงอยู่สูงสุดจากฮีปดัมป์เหล่านี้ผ่าน trace_processor และดึงข้อมูลปัญหา Crashlytics ที่ตรงกัน เช่น คีย์ที่กำหนดเอง บันทึก เบรดครัมบ์ การเปิดตัวที่เกี่ยวข้อง และอื่นๆ ทั้งหมดนี้ทำได้จากเทอร์มินัล

  • หรือจะดูที่ด้านล่างของสไลด์ก็ได้

  • อย่าเรียกใช้คำสั่งนี้ด้วยตนเอง

  • อีกครั้ง ให้ถาม AI Coding Agent ที่คุณชื่นชอบด้วยภาษาธรรมชาติเพื่อตรวจสอบฮีปดัมพ์ OOM ล่าสุดโดยใช้คำสั่ง Android และส่งการแก้ไข

    # Or ask your favorite LLM in natural language
    > Using `android` command, inspect the latest heap dump
      for my OOM issue and propose a fix
    ● Ran android signals heaps list · download · describe
    ● Analyzed ./heaps/oom.perfetto-trace (perfetto)
    ● Updated PaletteExtractor.kt (+6 -4 lines)
  • และจะมีบริบทเพียงพอที่จะดำเนินการทั้งขั้นตอนให้คุณ

  • ซึ่งก็คือการแสดงรายการ OOM, ดาวน์โหลดการติดตาม, เรียกใช้การวิเคราะห์ และสร้างแพตช์โค้ดในโปรเจ็กต์ของคุณโดยตรง

  • และดูแพตช์นี้ในไฟล์ PaletteExtractor

    // PaletteExtractor.kt
    fun extractPalette(framePixels: IntArray): SparseIntArray {
    -   val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
    -   for (px in framePixels) {
    -       pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
    -   }
    
    +   val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
    +   for (i in framePixels.indices step 16) {
    +       val argb = framePixels[i]
    +       colorHistogram.put(argb, colorHistogram.get(argb) + 1)
    +   }
    +   return colorHistogram
    }
  • ตอนนี้เราจะลดจำนวนตัวอย่างเฟรมและนับจำนวนเต็มดิบโดยตรง แทนที่จะจัดสรรออบเจ็กต์หลายล้านรายการ

  • ดังนั้น เรามาใช้การแก้ไข ปิดปัญหา อัปเดตเวอร์ชัน และทดสอบต่อไปกัน

  • หวังว่าข้อผิดพลาดหน่วยความจำไม่เพียงพอจะหายไป และเนื่องจากเราจะไม่ต้องเสียเวลาไปกับการเก็บขยะอีกต่อไป หวังว่า ANR ที่น่ารำคาญนั้นจะหายไปด้วย

  • เราจึงติดตั้งใช้งานบิลด์แพตช์ในอุปกรณ์ทดสอบ เลือกคลิปใหม่ และเริ่มดึงจานสี

  • และ UI ก็ค้างอีกครั้ง

  • ANR อื่นๆ

  • สิ่งที่น่าสนใจคือเมื่อทดสอบเพิ่มเติม เราพบว่าอุปกรณ์บางเครื่องยังคงมี ANR อยู่ แต่อุปกรณ์อื่นๆ แอปกลับขัดข้องแบบสุ่ม และค่อนข้างบ่อยด้วย บ่อยกว่า OOM เหล่านั้นด้วยซ้ำ

  • แต่เราแก้ไข OOM แล้ว แล้วเกิดอะไรขึ้น

การตรวจสอบปัญหาเกี่ยวกับหน่วยความจำของระบบ

  • Polina Govorkova

    เอาล่ะ กลับไปที่ Crashlytics กัน

  • ข่าวดีก่อน

  • ดูรายการปัญหาของเรา

  • ข้อยกเว้นหน่วยความจำไม่เพียงพอหายไปอย่างสมบูรณ์ตามที่เราคาดหวังไว้

  • ข่าวร้ายคือ

  • ตอนนี้ ChromaClip มีชีวิตรอดนานขึ้นอีกนิด จึงมีเวลาค้นพบสิ่งที่ซ่อนอยู่ข้างใต้

  • ดังนั้น ANR จึงไม่หายไป

  • และตอนนี้เรามีการขัดข้องของระบบ 2 รายการที่ด้านล่าง ซึ่งโชคดีที่มีเหตุการณ์เพียงไม่กี่รายการ

  • และที่ด้านบนสุดซึ่งมี 11 เหตุการณ์ เรามี Memory Limiter Kill ซึ่งโชคดีที่ได้รับการติดป้ายว่ามีฮีปดัมป์แล้ว

  • เดี๋ยวนะ

  • การหยุดทำงานโดยตัวจำกัดหน่วยความจำคืออะไร

  • มีใครเคยได้ยินเรื่องนี้บ้างไหม ช่วยยกมือขึ้นหน่อย

  • โอเค

  • ไพ่ที่หายากบางชุด

  • มาเรียนรู้ไปด้วยกัน

  • หากคุณไม่เคยเห็นการปิดแอปโดย Memory Limiter มาก่อน นั่นเป็นเพราะฟีเจอร์นี้เป็นฟีเจอร์ใหม่ใน Android 17

  • และ Java OOM จะทริกเกอร์ก็ต่อเมื่อคุณใช้หน่วยความจำเกินขีดจำกัดฮีป Java ที่มีการจัดการ แต่เมื่อหน่วยความจำที่ใช้ทั้งหมดของแอปรวม RAM แบบเนทีฟด้วย เช่น บัฟเฟอร์กราฟิกและการจัดสรร C++ ก่อนหน้านี้จะไม่มีเพดานต่อแอป

  • ดังนั้นเราจึงมี Low Memory Killer ซึ่งเป็นแบบส่วนกลาง

  • โดยจะเข้ามาช่วยเมื่อแอปบางแอปใช้ RAM ของระบบจนหมดจนทำให้โทรศัพท์ทั้งเครื่องทำงานช้าลง

  • ตอนนี้ตั้งแต่ Android 17 เป็นต้นไป ตัวจำกัดหน่วยความจำจะจำกัดหน่วยความจำที่แอปแต่ละแอปใช้

  • ดังนั้น เมื่อกระบวนการใช้ RAM มากเกินไป ระบบปฏิบัติการจะสิ้นสุดการทำงานของแอปนั้นๆ

  • และ Crashlytics จะบันทึกรายละเอียดทั้งหมดเหล่านั้นและจัดกลุ่มการสิ้นสุดไว้ด้วยกันในแดชบอร์ด

  • ดังนั้น การคลิกเข้าไปที่การปิดแอปโดยตัวจำกัดหน่วยความจำใน Crashlytics จะทำให้คุณได้รับคำอธิบายว่าทำไมระบบปฏิบัติการจึงเข้ามาแทรกแซง เช่นเดียวกับ OOM คุณจึงไม่จำเป็นต้องจำสิ่งที่ฉันเพิ่งพูดไป

  • นอกจากนี้ คุณยังจะได้รับเคล็ดลับเกี่ยวกับวิธีจัดการกับปัญหาในขั้นตอนถัดไป และลิงก์ไปยังเอกสารการบังคับใช้หน่วยความจำของ Android โดยตรง

  • และเช่นเดียวกับ OOM สแต็กเทรซจะระบุว่าเฟรมที่ไม่มีความผิดเป็นสาเหตุเมื่อระบบปฏิบัติการหยุดทำงาน และแดชบอร์ด Crashlytics จะไฮไลต์เฟรมดังกล่าว

  • ปัจจุบัน การหยุดทำงานของ Memory Limiter มักเกิดขึ้นในโค้ดแบบเนทีฟ เนื่องจากสภาพแวดล้อมแบบเนทีฟไม่มีเลเยอร์ที่จัดการความปลอดภัยและการจัดสรรหน่วยความจำ

  • ไม่เช่นนั้นจะเกิด OOM

  • ดังนั้นคำถามแรกของเราคือ ออบเจ็กต์ Kotlin ของเราเก็บการจัดสรรที่ใช้หน่วยความจำเนทีฟจำนวนมากไว้หรือไม่

  • หรือเป็นโค้ด C++ ดิบที่รั่วไหลออกมาเอง

  • มาคลิก "วิเคราะห์ใน Perfetto UI" เพื่อดูสาเหตุกัน

  • ดูแท็บภาพรวมใน Perfetto ฮีปดัมป์ Explorer

  • เราเห็นว่าในส่วนข้อมูลทั่วไป RSS แบบไม่ระบุตัวตนบวกกับ Swap แสดงให้เห็นว่าหน่วยความจำของกระบวนการทั้งหมดถึง 4 GB

  • ตกลง

  • แต่ด้านล่างนั้น เราจะเห็นว่าในส่วนไบต์ที่เก็บไว้โดยฮีป ฮีปที่มีการจัดการทั้งหมดของเรา ซึ่งรวมถึงหน่วยความจำแบบเนทีฟที่เชื่อมโยงกับออบเจ็กต์ Kotlin เช่น บิตแมป มีขนาดเพียง 42 MB

  • ตกลง

  • ดังนั้นใน 10 วินาที เราจึงตัดโค้ด Kotlin และ Java ทั้งหมดออก

  • ดังนั้น สาเหตุจึงต้องมาจากการรั่วไหลจากเลเยอร์ C++ แบบเนทีฟ

  • และเนื่องจาก ChromaClip มีเลเยอร์ C++ แบบบางสำหรับการประมวลผลเฟรม เราจึงไม่ต้องมองหาที่ไหนไกล

  • เมื่อชี้ Agent การเขียนโค้ดหรือเครื่องมือวิเคราะห์โปรไฟล์แบบเนทีฟของ Android Studio ไปที่ NativePaletteEngine.cpp processFrame ก็จะพบบั๊กทันที

    // NativePaletteEngine.cpp
    void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
        uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
        transformColorSpace(rgba, scratch, size);
    
        // BUG: Allocated every frame, but never freed!
    +   delete[] scratch; // Free native buffer when done
    }
  • ดังนั้นในทุกเฟรมวิดีโอ โค้ด C++ ของเราจะจัดสรรบัฟเฟอร์ชั่วคราว ซึ่งจะไม่ชั่วคราวหากคุณไม่ปล่อยบัฟเฟอร์นั้น ถือเป็นข้อผิดพลาดที่มือใหม่มักทำกัน

  • ได้เวลาเชื่อมโยงข้อมูลแล้ว

  • เกิดอะไรขึ้น

  • เมื่อเราแก้ไขการรั่วไหลของ Java แล้ว การรั่วไหลของ C++ จะมีเวลาสะสมและทำงานอย่างไม่เป็นระเบียบ

  • ใน Android 17 ตัวจำกัดหน่วยความจำจะตรวจจับและปิดแอป

  • แต่ในอุปกรณ์รุ่นเก่าที่ยังไม่มีตัวจำกัดหน่วยความจำ ปัญหานี้จะแสดงเป็น ANR

  • ในที่สุดทุกอย่างก็สมเหตุสมผล

  • มาเพิ่ม delete[] scratch ตรงนี้ อัปเดตเวอร์ชัน และเผยแพร่การอัปเดตกัน

  • เมื่อการปิดแอปและ ANR ของ Memory Limiter หมดไปแล้ว เราก็พร้อมที่จะเปิดตัวเวอร์ชัน 1.0

  • น่าตื่นเต้น

  • และด้วยความเร็วและความเสถียรของ ChromaClip ในตอนนี้ แอปของเราจึงได้รับความนิยมอย่างมาก

  • ผู้คนหลายพันคนอัปโหลดและรีมิกซ์วิดีโอทุกวัน

  • แต่เมื่อมีการเข้าชมทั้งหมด โปรดจำการขัดข้องของระบบที่ลึกลับซึ่งเราเห็นก่อนหน้านี้ที่ด้านล่างของแดชบอร์ด

  • พวกเขาก็วิ่งกันวุ่นเลย

  • จู่ๆ ก็มีจำนวนเพิ่มขึ้นอย่างมาก

  • และงานของเรายังไม่จบเพียงเท่านี้

การตรวจสอบข้อขัดข้องที่เกิดจากความเสียหายของหน่วยความจำของระบบ

  • Polina Govorkova

    ดังนั้นเมื่อเปิดการขัดข้องของระบบใน Crashlytics ให้ดูที่สแต็กเทรซ

  • ไม่มีโค้ดแม้แต่บรรทัดเดียวจากแอปของเราเอง

  • RenderThread ขัดข้องลึกภายใน libc.so ขณะเพิ่มหน่วยความจำ

  • และเคล็ดลับอีกอย่างหนึ่งคือ เมื่อเทรดระบบขัดข้องภายในตัวจัดสรรหน่วยความจำโดยไม่มีโค้ดแอปของคุณอยู่ภายใน นั่นเป็นสัญญาณที่ชัดเจนว่าหน่วยความจำเนทีฟเสียหาย

  • ดังนั้น โค้ด C++ ของเราจึงทำให้บัฟเฟอร์เสียหายหรือปล่อยบัฟเฟอร์ก่อนหน้านี้ และเทรดการแสดงผลก็สะดุดกับบัฟเฟอร์ในภายหลัง

  • แล้วเราจะจับผู้กระทำผิดตัวจริงในขณะที่เกิดการทุจริตได้อย่างไร

  • Android มีเครื่องมืออันทรงพลัง 2 รายการ และ Crashlytics ก็รองรับทั้ง 2 รายการนี้ตั้งแต่แกะกล่อง

  • อย่างแรกคือ GWP-ASan ซึ่งเป็นตาข่ายนิรภัยน้ำหนักเบาที่สร้างขึ้นมาเพื่อการใช้งานจริงโดยเฉพาะ

  • โดยจะสุ่มตัวอย่างการจัดสรรฮีปแบบเนทีฟเพียงเล็กน้อยและล้อมรอบด้วยหน้าป้องกัน เพื่อให้โอเวอร์เฮดของ CPU และหน่วยความจำเกือบเป็นศูนย์

  • ในเซสชันของผู้ใช้หลายพันเซสชันในสภาพแวดล้อมจริง เมื่อข้อบกพร่องแตะหน่วยความจำที่ว่างหรืออยู่นอกขอบเขตในการจัดสรรที่สุ่มตัวอย่าง GWP-ASan จะตรวจจับข้อบกพร่องนั้นทันที

  • อย่างไรก็ตาม เนื่องจากโดยธรรมชาติแล้ว GWP-ASan เป็นแบบความน่าจะเป็น เมื่อคุณทดสอบสิ่งต่างๆ ในเครื่อง คุณจึงไม่ต้องการใช้โทรศัพท์ทดสอบเป็นเวลา 16 ชั่วโมงขึ้นไปเพื่อให้ได้ตัวอย่างแบบสุ่ม

  • แล้วคุณจะได้อะไรบ้าง

  • สำหรับการแก้ไขข้อบกพร่องในเครื่องแบบเชิงกำหนด คุณมี ARM MTE หรือ Memory Tagging Extension

  • MTE ใช้ตัวชี้ฮาร์ดแวร์และแท็กหน่วยความจำเพื่อตรวจสอบการจัดสรรทุกครั้ง และพร้อมใช้งานในอุปกรณ์ ARM v9 เช่น Pixel 8 ขึ้นไป

  • ดังนั้น ให้ใช้ร่วมกับ GWP-ASan ในเวอร์ชันที่ใช้งานจริง แล้วคุณจะได้รับประโยชน์จากทั้ง 2 อย่าง

  • และตามที่คุณอาจคาดไว้ เมื่อเครื่องมือใดเครื่องมือหนึ่งตรวจพบบั๊ก Crashlytics จะติดป้ายกำกับปัญหาโดยอัตโนมัติและให้คุณกรองตาม GWP-ASan หรือ MTE และปักหมุดการ์ดข้อมูลที่มีสาเหตุที่แน่ชัดของความเสียหายในแดชบอร์ด

  • มาดูสิ่งที่เกิดขึ้นในคอนโซล Crashlytics เมื่อ GWP-ASan ตรวจพบข้อขัดข้อง

  • ถัดจากแท็บสแต็กเทรซปกติ เรามีสแต็กเทรซหน่วยความจำ

  • และที่ด้านบน เราจะเห็นสาเหตุที่แน่นอนของความเสียหาย ซึ่งก็คือ Use After Free

  • และแทนที่จะแสดงข้อขัดข้องของเทรดการแสดงผลใน libc.so ตอนนี้ GWP-ASan จะแสดงประวัติทั้งหมดของบัฟเฟอร์นั้นภายใน effect_pipeline.cc ของเราเอง

  • ดังนั้นในส่วน "จัดสรรโดย" เราจะเห็นว่าบัฟเฟอร์สร้างขึ้นที่ใด

  • และใต้ Deallocated By เราจะเห็นตัวการที่ทำให้หน่วยความจำว่างจากใต้เทรดการแสดงผล

  • ซึ่งซ่อนอยู่ในบรรทัดที่ 208 ในไปป์ไลน์เอฟเฟกต์ของเรา

  • เราจึงเปิดโค้ดแบบเนทีฟใน EffectPipeline และเห็นข้อบกพร่องที่กำลังยิ้มเยาะเราอยู่

    // effect_pipeline.cc
    void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
        // BUG: Freed on worker thread while RenderThread is still reading it!
    +   std::lock_guard<std::mutex> lock(lut_mutex_);
    
        releaseLutBuffer();       // Frees old color-grading LUT
        loadFilterPreset(preset); // Allocates new color-grading LUT
    }
  • เมื่อใดก็ตามที่ผู้ใช้เปลี่ยนฟิลเตอร์สีในชุดข้อความเบื้องหลัง เราจะเพิ่มพื้นที่ว่างในบัฟเฟอร์เก่าขณะที่ RenderThread ยังคงอ่านอยู่

  • เราจึงเพิ่ม Mutex Lock รอบๆ บัฟเฟอร์สวอป ส่งอัปเดต และสุดท้ายก็ดูรายงาน GWP-ASan และข้อขัดข้องลึกลับทั้งหมดหายไปเกือบชั่วข้ามคืน

  • และเมตริกผู้ใช้ที่ไม่พบข้อขัดข้องก็พุ่งสูงขึ้นเป็น 100%

  • ไชโย

  • จำได้ไหมว่าตอนเริ่มพูดคุย เราถามตัวเองว่า "ฉันทำเสร็จแล้วหรือยัง"

  • ก่อนที่จะถูกโจมตีโดยบอสที่ชื่อว่า "การตอบสนอง" และ "หน่วยความจำ" ในหนังสือ

  • ตอนนี้เราได้แก้ไขปัญหา ANR, ข้อผิดพลาดหน่วยความจำไม่เพียงพอ, การปิดแอปเนื่องจากหน่วยความจำไม่เพียงพอ และข้อขัดข้องที่เกิดจากความเสียหายของหน่วยความจำแบบเนทีฟทั้งหมดแล้ว

  • และแม้ว่าสแต็กเทรซจะโกหกและผู้ร้ายซ่อนตัวอยู่ที่ไหนสักแห่งในชุดข้อความใน C++, Android และ Firebase Crashlytics ก็มีเครื่องมือให้คุณติดตามและจัดส่งได้อย่างมั่นใจ

สรุปและการลงชื่อสมัครทดลองใช้ก่อนเปิดตัว

  • Polina Govorkova

    มาสรุปสิ่งที่คุณใช้ได้ทันทีเมื่อกลับไปที่แล็ปท็อปกัน ข้อมูลเชิงลึกเกี่ยวกับเทรด ANR และข้อมูลเมตาอื่นๆ ที่เกี่ยวข้องผ่าน ApplicationExitInfo, การรวบรวมทริกเกอร์การหยุดทำงานของ OOM และตัวจำกัดหน่วยความจำโดยอัตโนมัติผ่าน ProfilingManager รวมถึงการรองรับ GWP-ASan และ Arm MTE อย่างเต็มรูปแบบสำหรับความเสียหายของหน่วยความจำแบบเนทีฟ

  • และเราจะเปิดตัวฟีเจอร์ฮีปดัมป์ในภาคสนามในเวอร์ชันตัวอย่างแบบส่วนตัวบน Android 17 ขึ้นไป

  • หากต้องการเป็นคนกลุ่มแรกๆ ที่ได้ลองใช้การเก็บรวบรวมฮีปดัมพ์อัตโนมัติใน Firebase Crashlytics และวิเคราะห์ด้วย Perfetto และ Android CLI โปรดถ่ายภาพสไลด์นี้ไว้และลงชื่อสมัครใช้ที่ลิงก์ดังกล่าว

  • ตอนนี้เราได้พูดคุยกันไปเกือบหมดแล้ว เราขอขอบคุณที่สละเวลามาเข้าร่วมเซสชันในวันนี้

  • ขอให้เทรดหลักของคุณไม่ถูกบล็อก, ฮีปของคุณสะอาด และการเปิดตัวครั้งถัดไปของคุณเสถียรที่สุดเท่าที่เคยมีมา

  • ขอให้สนุกกับการแก้ไขข้อบกพร่อง

  • ขอขอบคุณอีกครั้ง

รหัส

ข้อมูลโค้ด

ตัวอย่างข้อความที่ 01:45 ใน "บทนำ"
java.lang.NullPointerException
  at MainActivity.onCreate(MainActivity.kt:42)
  at android.app.Activity.performCreate
  at android.os.Handler.dispatchMessage
  at android.os.Looper.loopOnce
  at android.app.ActivityThread.main
ตัวอย่างข้อความที่ 01:58 ใน "บทนำ"
"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 flags=1
  | state=S schedstat=( 1450239 48192 145 )
  at DataManager.getData(DataManager.kt:25)
  - waiting to lock <0x0a1b> held by thread 12
  at MainActivity.onResume(MainActivity.kt:50)
  at android.app.Activity.performResume
  at android.os.Handler.dispatchMessage
  at android.os.Looper.loopOnce
  at android.app.ActivityThread.main
ตัวอย่างข้อความที่ 04:45 ใน "การแก้ไขข้อบกพร่องและการวิเคราะห์ ANR"
14:32:15.105  1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
    PID: 14205
    Reason: Input dispatching timed out
    Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
  | sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
  | state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
  | stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
  at sun.misc.Unsafe.park(Native method)
  - waiting on an unknown object
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
  at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
  at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
  at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
  at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
  at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)
ตัวอย่างข้อความที่ 08:08 ใน "การแก้ไขข้อบกพร่องและการวิเคราะห์ ANR"
DefaultDispatcher-worker-1 (Root blocking)

dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)
ตัวอย่างข้อความที่ 11:54 ใน "การตรวจสอบข้อยกเว้นหน่วยความจำไม่เพียงพอ"
java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
 at android.graphics.Bitmap.createBitmap
 at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
 at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)
Shell snippet at 15:54 in "Capturing and analyzing heap dumps"
# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto

# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d

# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
    --output ./heaps

# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...

# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id>
ตัวอย่างข้อความที่ 16:27 ใน "การบันทึกและวิเคราะห์ฮีปดัมป์"
# Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
  for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)
ข้อมูลโค้ด Kotlin ที่นาที 16:47 ใน "การบันทึกและวิเคราะห์ฮีปดัมป์"
// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
-   val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
-   for (px in framePixels) {
-       pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
-   }

+   val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+   for (i in framePixels.indices step 16) {
+       val argb = framePixels[i]
+       colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+   }
+   return colorHistogram
}
ข้อมูลโค้ด C++ ที่ 21:06 ใน "การตรวจสอบปัญหาเกี่ยวกับหน่วยความจำแบบเนทีฟ"
// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
    uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
    transformColorSpace(rgba, scratch, size);

    // BUG: Allocated every frame, but never freed!
+   delete[] scratch; // Free native buffer when done
}
ข้อมูลโค้ด C++ ที่นาที 25:14 ใน "การตรวจสอบข้อขัดข้องที่เกิดจากความเสียหายของหน่วยความจำแบบเนทีฟ"
// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
    // BUG: Freed on worker thread while RenderThread is still reading it!
+   std::lock_guard<std::mutex> lock(lut_mutex_);

    releaseLutBuffer();       // Frees old color-grading LUT
    loadFilterPreset(preset); // Allocates new color-grading LUT
}