केस स्टडी: ओओएम क्रैश को डीबग करना

आउट ऑफ़ मेमोरी (OutOfMemoryError) क्रैश तब होता है, जब आपके ऐप्लिकेशन के लिए तय किया गया Java हीप भर जाता है और नई मेमोरी का बंटवारा नहीं हो पाता. OutOfMemoryError क्रैश रिपोर्ट के स्टैक ट्रेस से, असल वजह का पता शायद ही चलता है. ऐसा इसलिए होता है, क्योंकि वे सिर्फ़ उस ऐलोकेशन कॉल को दिखाते हैं जो मेमोरी खत्म होने पर फ़ेल हो गया था. वे उन ऑब्जेक्ट को नहीं दिखाते जो बाकी हीप को बनाए रखते हैं.

ProfilingManager की मदद से, फ़ील्ड में मौजूद डिवाइसों और लोकल डिवाइसों से Java हीप डंप कैप्चर किए जा सकते हैं. इससे Perfetto यूज़र इंटरफ़ेस (यूआई) में ऑब्जेक्ट रिटेंशन ग्राफ़ की जांच की जा सकती है. साथ ही, यह पता लगाया जा सकता है कि मेमोरी का इस्तेमाल कहां हो रहा है.

परिदृश्य

मान लें कि आपके क्रैश की रिपोर्टिंग करने वाले टूल ने, प्रोडक्शन में OutOfMemoryError क्रैश के बढ़ते क्लस्टर को फ़्लैग किया है. ऐसा तब होता है, जब उपयोगकर्ता OomSampleActivity के साथ इंटरैक्ट करते हैं.

इसकी जांच करने के लिए, TRIGGER_TYPE_OOM ट्रिगर को ProfilingManager के साथ रजिस्टर करें, ताकि OutOfMemoryError होने पर सिस्टम अपने-आप Java हीप डंप को कैप्चर कर ले.

हीप डंप को कैप्चर करना

Android 17 (एपीआई लेवल 37) और इसके बाद के वर्शन पर, OutOfMemoryError क्रैश होने पर हीप डंप कैप्चर करने के लिए, ProfilingTrigger.TRIGGER_TYPE_OOM के साथ ट्रिगर-आधारित प्रोफ़ाइलिंग को कॉन्फ़िगर करें. जब आपके ऐप्लिकेशन में कोई ऐसा OutOfMemoryError आता है जिसे मैनेज नहीं किया जा सकता, तो सिस्टम अपने-आप एक नया PROFILING_TYPE_JAVA_HEAP_DUMP कैप्चर करना शुरू कर देता है.

क्रैश होने पर ऐप्लिकेशन की प्रोसेस बंद हो जाती है. इसलिए, सिस्टम बैकग्राउंड में हीप डंप को सेव करने की प्रोसेस पूरी करता है और नतीजे को सूची में जोड़ता है. इसके बारे में ज़्यादा जानने के लिए, ट्रिगर की लाइफ़साइकल देखें. ऐप्लिकेशन के स्टार्टअप की प्रोसेस पूरी होने के बाद, बैकग्राउंड Executor पर ग्लोबल रिज़ल्ट लिसनर को रजिस्टर करें, ताकि ऐप्लिकेशन के स्टार्टअप की प्रोसेस धीमी न हो. इसके लिए, registerForAllProfilingResults का इस्तेमाल करें, ताकि अगली बार जब उपयोगकर्ता ऐप्लिकेशन को लॉन्च करे, तो उसे सूची में मौजूद ProfilingResult मिल सके. addProfilingTriggers के उलट, registerForAllProfilingResults को हर बार ऐप्लिकेशन चालू होने पर कॉल करना ज़रूरी है. , ऐप्लिकेशन के रीस्टार्ट होने पर भी बना रहता है. इसे सिर्फ़ एक बार या ट्रिगर अपडेट करते समय कॉल करना ज़रूरी होता है:

Kotlin

@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN)
fun registerOomTrigger(context: Context) {
    val profilingManager =
        context.getSystemService(ProfilingManager::class.java) ?: return
    val callbackExecutor: Executor = Executors.newSingleThreadExecutor()

    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (
            profilingResult.errorCode == ProfilingResult.ERROR_NONE &&
            profilingResult.triggerType == ProfilingTrigger.TRIGGER_TYPE_OOM
        ) {
            val heapDumpPath = profilingResult.resultFilePath
            // Schedule a background job to compress and upload heapDumpPath
        }
    }

    profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback)

    val oomTrigger = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM)
        .build()
    profilingManager.addProfilingTriggers(listOf(oomTrigger))
}

Java

@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN)
public static void registerOomTrigger(Context context) {
    ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class);
    if (profilingManager == null) return;
    Executor callbackExecutor = Executors.newSingleThreadExecutor();

    Consumer<ProfilingResult> resultCallback =
        profilingResult -> {
            if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE
                && profilingResult.getTriggerType() == ProfilingTrigger.TRIGGER_TYPE_OOM) {
                String heapDumpPath = profilingResult.getResultFilePath();
                // Schedule a background job to compress and upload heapDumpPath
            }
        };

    profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback);

    ProfilingTrigger oomTrigger =
        new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM)
            .build();
    profilingManager.addProfilingTriggers(Collections.singletonList(oomTrigger));
}

प्रोफ़ाइल को डिकोड करना

जब कोई OutOfMemoryError कैप्चर को ट्रिगर करता है और अगली बार ऐप्लिकेशन लॉन्च होने पर आपके कॉलबैक को ProfilingResult मिलता है, तो प्रोफ़ाइल फ़ाइल को अपने सर्वर पर अपलोड करें या adb की मदद से उसे अपनी डेवलपमेंट मशीन पर खींचें. अगर आपने रिलीज़ के लिए तैयार बिल्ड से प्रोफ़ाइल कैप्चर की है, जो R8 कोड छोटा करने और अस्पष्ट बनाने का इस्तेमाल करता है, तो विश्लेषण करने से पहले, अपने वर्कस्टेशन या सर्वर पर इसे ऑफ़लाइन डिकोड करें. डीऑब्फ़स्केशन के बिना, R8 रॉ हीप डंप में Java और Kotlin क्लास, फ़ील्ड, और तरीके के नामों को ऑब्फ़स्केट करता है. उदाहरण के लिए, ओरिजनल क्लास के नामों को a.b में बदलना.

कैप्चर की गई प्रोफ़ाइल को उस रिलीज़ के लिए तैयार बिल्ड के लिए जनरेट की गई R8 mapping.txt फ़ाइल के साथ पैकेज करने के लिए, --proguard-map फ़्लैग के साथ Perfetto के trace_processor bundle कमांड का इस्तेमाल करें:

trace_processor bundle \
  --proguard-map /path/to/mapping.txt \
  profile_trigger-type-7_2026-05-06-14-12-40-748_uid-10316.perfetto-java-heap-dump \
  deobfuscated_oom.tar

यह कमांड deobfuscated_oom.tar जनरेट करता है. यह एक बेहतर संग्रह है, जो ओरिजनल प्रोफ़ाइल को उसकी डीऑब्फ़स्केशन मैपिंग के साथ बंडल करता है. Perfetto यूज़र इंटरफ़ेस (यूआई) में इस संग्रह को खोलने या trace_processor की मदद से क्वेरी करने पर, Perfetto सभी ओरिजनल क्लास, फ़ील्ड, और मेथड के नाम अपने-आप वापस ले आता है. ज़्यादा जानकारी के लिए, Perfetto का सिंबलाइज़ेशन और डिकोड करना दस्तावेज़ देखें.

हीप डंप का विश्लेषण करना

Perfetto यूज़र इंटरफ़ेस (यूआई) में, डीऑब्फ़स्केट किए गए हीप डंप की मैन्युअल तरीके से जांच की जा सकती है. इसके लिए, यहां दिया गया तरीका अपनाएं. इसके अलावा, एआई एजेंट की मदद से, ओओएम की जांच को ऑटोमेट किया जा सकता है.

Perfetto के यूज़र इंटरफ़ेस (यूआई) में, डीऑब्फ़स्केट किए गए हीप डंप को खोलें. इसके बाद, बाईं ओर मौजूद नेविगेशन बार में, हीप डंप एक्सप्लोरर को चुनें. फ़्लेमग्राफ़ टैब में, मेज़र को ऑब्जेक्ट साइज़ पर सेट करने पर, इमेज 1 में मौजूद रिटेंशन की हैरारकी तुरंत दिखती है.

Perfetto यूज़र इंटरफ़ेस (यूआई) हीपडंप एक्सप्लोरर का फ़्लेम ग्राफ़, जिसमें StatsReporter को OomSampleActivity के आठ इंस्टेंस को सेव करते हुए दिखाया गया है. इनका कुल साइज़ 240.28 MiB है.
पहली इमेज. हीप डंप का फ़्लेम ग्राफ़, जिसमें OomSampleActivity के आठ रिटेन किए गए इंस्टेंस दिख रहे हैं.

इमेज 1 में दिए गए फ़्लेम ग्राफ़ की जांच करने पर, कई अहम नतीजे सामने आते हैं:

  • मेमोरी का ज़्यादा इस्तेमाल: 254.79 MiB के कुल हीप (root) में से, com.example.sampleapp.OomSampleActivity कुल साइज़ का 240.28 MiB (पूरे ऐप्लिकेशन हीप का 94.31%) इस्तेमाल करता है. इसमें से ज़्यादातर हिस्सा, चाइल्ड byte[] कलेक्शन में मौजूद होता है.
  • एक से ज़्यादा लाइव गतिविधि इंस्टेंस: OomSampleActivity फ़्रेम को चुनने पर, सेल्फ़ काउंट: 8 दिखता है. किसी भी समय स्क्रीन पर OomSampleActivity का सिर्फ़ एक इंस्टेंस मौजूद होना चाहिए. हालांकि, मेमोरी में एक साथ आठ अलग-अलग इंस्टेंस मौजूद हैं.
  • ऑब्जेक्ट पाथ को बनाए रखना: फ़्लेम ग्राफ़ में OomSampleActivity के पैरंट फ़्रेम को पढ़ने से, रेफ़रंस चेन का पता चलता है. इससे उन इंस्टेंस को बनाए रखा जाता है: com.example.sampleapp.StatsReporter में java.util.concurrent.CopyOnWriteArrayList (java.lang.Object[] की मदद से) होता है, जो सभी आठ OomSampleActivity इंस्टेंस को सीधे तौर पर रेफ़र करता है.

कोड में मौजूद असल वजह का पता लगाना

रिटेंशन पाथ StatsReporter से OomSampleActivity पर ले जाता है. इसलिए, अपने कोड बेस में दोनों क्लास की जांच करें.

StatsReporter एक सिंगलटन है, जो StatsListener कॉलबैक की सूची को मैनेज करता है. साथ ही, बैकग्राउंड पोलिंग थ्रेड से समय-समय पर अपडेट पोस्ट करता है:

Kotlin

class StatsReporter private constructor() {
    fun interface StatsListener {
        fun onReportStats(stats: String)
    }

    private val listeners: MutableList<StatsListener> = CopyOnWriteArrayList()
    private var isReporting = false

    init {
        startReporting()
    }

    fun registerListener(listener: StatsListener) {
        if (!listeners.contains(listener)) {
            listeners.add(listener)
        }
    }

    fun unregisterListener(listener: StatsListener) {
        listeners.remove(listener)
    }

    private fun startReporting() {
        if (isReporting) return
        isReporting = true

        Thread({
            while (true) {
                try {
                    // Poll every 10 seconds
                    Thread.sleep(10000)
                } catch (e: InterruptedException) {
                    Thread.currentThread().interrupt()
                    break
                }

                for (listener in listeners) {
                    // Send periodic mock stats update from the background thread
                    listener.onReportStats("Update from StatsReporter background polling thread")
                }
            }
        }, "StatsReporterPollingThread").start()
    }

    companion object {
        private var instance: StatsReporter? = null

        @JvmStatic
        fun getInstance(): StatsReporter {
            return instance ?: StatsReporter().also { instance = it }
        }
    }
}

Java

public class StatsReporter {
    public interface StatsListener {
        void onReportStats(String stats);
    }

    private static StatsReporter sInstance;

    private final List<StatsListener> mListeners = new CopyOnWriteArrayList<>();
    private boolean mIsReporting = false;

    private StatsReporter() {
        startReporting();
    }

    public static StatsReporter getInstance() {
        if (sInstance == null) {
            sInstance = new StatsReporter();
        }
        return sInstance;
    }

    public void registerListener(StatsListener listener) {
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
        }
    }

    public void unregisterListener(StatsListener listener) {
        mListeners.remove(listener);
    }

    private void startReporting() {
        if (mIsReporting) return;
        mIsReporting = true;

        new Thread(() -> {
            while (true) {
                try {
                    // Poll every 10 seconds
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }

                for (StatsListener listener : mListeners) {
                    // Send periodic mock stats update from the background thread
                    listener.onReportStats("Update from StatsReporter background polling thread");
                }
            }
        }, "StatsReporterPollingThread").start();
    }
}

इसके बाद, OomSampleActivity की जांच करें:

Kotlin

class OomSampleActivity : Activity(), StatsReporter.StatsListener {
    // Allocate some memory.
    private val heavyData = ByteArray(30 * 1024 * 1024)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_oom_sample)

        StatsReporter.getInstance().registerListener(this)
    }

    override fun onReportStats(stats: String) {
        // Read from heavyData so R8 doesn't strip it out as dead code during release builds
        Log.d(TAG, "Received stats in heavy leaked activity: $stats | Data size: ${heavyData.size}")
    }

    companion object {
        private const val TAG = "OomSampleActivity"
    }
}

Java

public class OomSampleActivity extends Activity implements StatsReporter.StatsListener {
    private static final String TAG = "OomSampleActivity";

    // Allocate some memory.
    private final byte[] mHeavyData = new byte[30 * 1024 * 1024];

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_oom_sample);

        StatsReporter.getInstance().registerListener(this);
    }

    @Override
    public void onReportStats(String stats) {
        // Read from mHeavyData so R8 does not strip it out as dead code during release builds
        Log.d(TAG, "Received stats in heavy leaked activity: " + stats + " | Data size: " + mHeavyData.length);
    }
}

कोड से यह पुष्टि होती है कि StatsReporter में OomSampleActivity के आठ इंस्टेंस क्यों मौजूद हैं:

  1. हर OomSampleActivity इंस्टेंस, 30 MiB byte[] बफ़र को ऐलोकेट करता है (Kotlin में heavyData, Java में mHeavyData) और खुद को onCreate में सिंगलटन StatsReporter पर लिसनर के तौर पर रजिस्टर करता है.
  2. जब सिस्टम unregisterListener को खत्म करता है, तब OomSampleActivity उसे कभी कॉल नहीं करता. StatsReporter, गतिविधि के खत्म होने के बाद भी मौजूद रहता है और रजिस्टर किए गए हर लिसनर का रेफ़रंस रखता है. इसलिए, गार्बेज कलेक्टर, खत्म किए गए इंस्टेंस या उनके 30 MiB byte[] कलेक्शन को फिर से हासिल नहीं कर सकता.
  3. जब भी सिस्टम OomSampleActivity को बंद करके फिर से बनाता है, जैसे कि Android गतिविधि पूरी होने की प्रोसेस के दौरान स्क्रीन को घुमाने पर, StatsReporter में 30 MiB का एक और इंस्टेंस लीक हो जाता है. सात बार रोटेट करने के बाद (8 इंस्टेंस × 30 MiB ≈ 240 MiB), हीप भर जाता है और ऐप्लिकेशन OutOfMemoryError के साथ क्रैश हो जाता है.

लीक को ठीक करना

मेमोरी लीक की समस्या को ठीक करने के लिए, गतिविधि में onDestroy को बदलें और StatsReporter.getInstance().unregisterListener(this) को कॉल करें, ताकि सिस्टम के गतिविधि को खत्म करने पर सिंगलटन अपना रेफ़रंस रिलीज़ कर सके:

Kotlin

class OomSampleActivityFixed : Activity(), StatsReporter.StatsListener {
    // Allocate some memory.
    private val heavyData = ByteArray(30 * 1024 * 1024)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_oom_sample)

        // Register the activity as a listener
        StatsReporter.getInstance().registerListener(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        // FIX: Unregister the listener to prevent the memory leak
        // when the Activity is destroyed (for example, during screen rotation).
        StatsReporter.getInstance().unregisterListener(this)
    }

    override fun onReportStats(stats: String) {
        // Read from heavyData so R8 doesn't strip it out as dead code during release builds
        Log.d(TAG, "Received stats in fixed heavy activity: $stats | Data size: ${heavyData.size}")
    }

    companion object {
        private const val TAG = "OomSampleFixed"
    }
}

Java

public class OomSampleActivityFixed extends Activity implements StatsReporter.StatsListener {
    private static final String TAG = "OomSampleFixed";

    // Allocate some memory.
    private final byte[] mHeavyData = new byte[30 * 1024 * 1024];

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_oom_sample);

        // Register the activity as a listener
        StatsReporter.getInstance().registerListener(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // FIX: Properly unregister the listener to prevent the memory leak
        // when the Activity is destroyed (e.g., during screen rotation).
        StatsReporter.getInstance().unregisterListener(this);
    }

    @Override
    public void onReportStats(String stats) {
        // Read from mHeavyData so R8 does not strip it out as dead code during release builds
        Log.d(TAG, "Received stats in fixed heavy activity: " + stats + " | Data size: " + mHeavyData.length);
    }
}

एआई एजेंट की मदद से, ओओएम की जांच को ऑटोमेट करना

फ़्लेम ग्राफ़ की मैन्युअल तरीके से जांच करने और फ़ाइलों में रेफ़रंस ट्रेस करने के बजाय, Perfetto एआई स्किल के साथ कॉन्फ़िगर किए गए एआई कोडिंग एजेंट का इस्तेमाल किया जा सकता है. इससे हीप डंप का विश्लेषण किया जा सकता है और उसे अपने कोड बेस से जोड़ा जा सकता है:

Analyze the Perfetto OOM heap dump at </path/to/profile>, which corresponds to
the codebase located at </path/to/codebase> using the Perfetto AI skill and find
the root cause of the OutOfMemoryError.

एजेंट, trace_processor का इस्तेमाल करके हीप डंप की क्वेरी करता है. साथ ही, StatsReporter से लीक हुए OomSampleActivity इंस्टेंस तक के डोमिनेटर पाथ को ट्रेस करता है. इसके बाद, आपके कोड बेस में मौजूद दोनों क्लास की जांच करता है. मैन्युअल इंटरवेंशन के बिना, यह उसी नतीजे पर पहुंचता है कि OomSampleActivity, onCreate में एक लिसनर को रजिस्टर करता है. इसके लिए, वह उसे अनरजिस्टर नहीं करता और onDestroy में unregisterListener(this) को जोड़कर एक पैच जनरेट करता है. यहां दी गई रिपोर्ट, एक अलग कैप्चर से जनरेट की गई थी. क्रैश के समय, इसमें सात इंस्टेंस मौजूद थे:

# Perfetto OOM Heap Dump Analysis Report

## 1. Orienting Context & First-Pass Triage

| Property | Value |
| :--- | :--- |
| **Process Name** | `com.example.sampleapp` |
| **Sample Timestamp (`graph_sample_ts`)** | `3433221241585` |
| **Leaf Retained Class** | `byte[]` (7 instances) |
| **Leaf Self Size** | `220,201,044` bytes (~210 MiB) |

### Dominator Tree Retention Path

Querying the `android.memory.heap_graph.dominator_tree` standard library module identifies the following largest dominated class path from GC root to leaf allocation:

    [ROOT_JAVA_FRAME] com.example.sampleapp.StatsReporter [1]
      -> java.util.concurrent.CopyOnWriteArrayList [1]
      -> java.lang.Object[] [1]
      -> com.example.sampleapp.OomSampleActivity [7]
      -> byte[] [7] (220,201,044 bytes)
## 2. Root Cause Analysis

> **Important:** The `OutOfMemoryError` is caused by an **Activity listener leak**: `OomSampleActivity` (`app/src/main/java/com/example/sampleapp/OomSampleActivity.java#L8-L27`) registers itself as a listener on the long-lived `StatsReporter` (`app/src/main/java/com/example/sampleapp/StatsReporter.java#L6-L61`) singleton in `onCreate()`, but never unregisters itself in `onDestroy()`.

### Step-by-Step Retention Mechanism

1. **Per-Activity 30 MiB Allocation:**
   Each instance of `OomSampleActivity` (`OomSampleActivity.java#L11-L12`) allocates a 30 MiB byte array (`30 * 1024 * 1024 = 31,457,280` bytes):
   ```java
   // OomSampleActivity.java:12
   private final byte[] mHeavyData = new byte[30 * 1024 * 1024];
   ```

2. **Strong Reference Registration in `onCreate()`:**
   When created, `OomSampleActivity.onCreate()` (`OomSampleActivity.java#L15-L20`) registers `this` with `StatsReporter` (`StatsReporter.java#L29-L33`):
   ```java
   // OomSampleActivity.java:19
   StatsReporter.getInstance().registerListener(this);
   ```

3. **GC Root Pinning via Background Thread & Singleton:**
   `StatsReporter` (`StatsReporter.java#L11-L60`) is both a static singleton (`sInstance`) and the owner of an active background thread (`StatsReporterPollingThread`, which holds a `[ROOT_JAVA_FRAME]` GC root reference while sleeping/looping in `startReporting()`). It stores listeners in `mListeners` (`CopyOnWriteArrayList<StatsListener>`).

4. **Leak Accumulation Across Activity Recreations:**
   Because `OomSampleActivity` does not override `onDestroy()` to call `StatsReporter.unregisterListener(this)`, every destroyed instance of `OomSampleActivity` remains strongly reachable in `mListeners`. After 7 Activity creations (7 * 30 MiB = 220,200,960 bytes plus array headers = `220,201,044` bytes), the Java heap is exhausted.

---

## 3. Actionable Implementation Plan

### 1. Unregister the Listener in `OomSampleActivity.onDestroy()`

Update `OomSampleActivity.java` to unregister the listener when the `Activity` is destroyed (matching the fix in `OomSampleActivityFixed.java`):

```diff
--- a/app/src/main/java/com/example/sampleapp/OomSampleActivity.java
+++ b/app/src/main/java/com/example/sampleapp/OomSampleActivity.java
@@ -19,4 +19,10 @@
         StatsReporter.getInstance().registerListener(this);
     }

+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        StatsReporter.getInstance().unregisterListener(this);
+    }
+
     @Override
```

### 2. Optional Architectural Hardening in `StatsReporter`

* **Lifecycle-Aware Registration:** Have `StatsReporter.registerListener()` accept a `LifecycleOwner` and attach a `DefaultLifecycleObserver` that automatically invokes `unregisterListener()` on `ON_DESTROY`.
* **Weak References:** Store `WeakReference<StatsListener>` entries in `StatsReporter` and evict cleared references during iteration so forgotten `unregisterListener()` calls cannot retain entire `Activity` hierarchies.

समस्या हल होने की पुष्टि करना

पुष्टि करें कि मेमोरी लीक की समस्या हल हो गई है. इसके लिए, टेस्ट डिवाइस पर स्थानीय तौर पर या प्रोडक्शन में जांच करें.

अगर समस्या स्थानीय तौर पर फिर से आ रही है

TRIGGER_TYPE_OOM सिर्फ़ तब हीप डंप कैप्चर करता है, जब कोई OutOfMemoryError होता है. समस्या ठीक होने पर, ट्रिगर को फ़ायर होने से रोका जाता है. हीप की जांच करने और यह पुष्टि करने के लिए कि लीक ठीक हो गया है, स्क्रीन को घुमाएं. इसके बाद, कमांड लाइन से, मांग पर Java हीप डंप कैप्चर करें:

  1. अनुरोध संख्या सीमित करने की सुविधा बंद करें (rate_limiter.disabled true) और अस्थायी नतीजे सेव करें (delete_temporary_results.disabled true), ताकि सिस्टम कमांड-लाइन कैप्चर को /data/misc/perfetto-traces/profiling/ में सेव रखे.
  2. अपने टेस्ट डिवाइस पर OomSampleActivityFixed खोलें. इसके बाद, स्क्रीन को आठ या उससे ज़्यादा बार घुमाएं, ताकि यह पुष्टि की जा सके कि ऐप्लिकेशन अब क्रैश नहीं होता. साथ ही, इसे फ़ोरग्राउंड में रखें.
  3. cmd profiling_service request-profiling शेल कमांड का इस्तेमाल करके, मांग पर Java हीप डंप (profiling_type 1) ट्रिगर करें:

    adb shell cmd profiling_service request-profiling 1 com.example.sampleapp
    
  4. रिटेन की गई प्रोफ़ाइल को /data/misc/perfetto-traces/profiling/ से पुल करें और उसे Perfetto यूज़र इंटरफ़ेस (यूआई) में खोलें.

समस्या ठीक होने के बाद, Perfetto यूज़र इंटरफ़ेस (यूआई) के हीपडंप एक्सप्लोरर का फ़्लेम ग्राफ़. इसमें OomSampleActivityFixed का एक इंस्टेंस दिख रहा है, जो 30.01 MiB मेमोरी का इस्तेमाल कर रहा है.
दूसरी इमेज. समस्या ठीक होने के बाद, हीप डंप का फ़्लेम ग्राफ़, जिसमें OomSampleActivityFixed का एक इंस्टेंस दिख रहा है.

जैसा कि इमेज 2 में दिखाया गया है, स्क्रीन को कई बार घुमाने के बाद:

  • com.example.sampleapp.OomSampleActivityFixed में सेल्फ़ काउंट 1 है. यह हर रोटेशन पर इंस्टेंस इकट्ठा नहीं करता.
  • इसका कुल रिटेन किया गया साइज़, 30.01 MiB (44.27 MiB के कुल हीप में से) पर स्थिर रहता है. इससे यह पुष्टि होती है कि गार्बेज कलेक्टर, पिछली गतिविधि के इंस्टेंस को फिर से हासिल करता है.

अगर समस्या को स्थानीय तौर पर फिर से पैदा करना मुश्किल है

जब कई स्क्रीन वाले जटिल नेविगेशन, लंबे समय तक चलने वाले सेशन या उपयोगकर्ता के खास डेटा की वजह से, कोई OutOfMemoryError होती है, तो उसे टेस्ट डिवाइस पर फिर से नहीं बनाया जा सकता. ऐसे में, रोल आउट के बाद प्रोडक्शन में गड़बड़ी को ठीक करने की पुष्टि करें:

  • अपने क्रैश रिपोर्टिंग टूल में OutOfMemoryError क्रैश क्लस्टर को मॉनिटर करें. इससे आपको यह पता चलेगा कि OomSampleActivity से जुड़े क्रैश कम हो रहे हैं या नहीं.
  • अपडेट की गई रिलीज़ से इकट्ठा किए गए किसी भी नए TRIGGER_TYPE_OOM हीप डंप की जांच करके पुष्टि करें कि StatsReporter अब खत्म हो चुके OomSampleActivity इंस्टेंस को सेव नहीं रखता.