מקרה לדוגמה: ניפוי באגים של קריסה מסוג OOM

קריסות בגלל אין זיכרון פנוי (OOM) (OutOfMemoryError) מתרחשות כשהאפליקציה מנצלת את כל הזיכרון שהוקצה לה ב-Java Heap ולא יכולה להקצות זיכרון חדש. דוחות קריסות של OutOfMemoryError לא חושפים את שורש הבעיה, כי הם מציגים רק את הקריאה להקצאה שנכשלה כשהזיכרון נגמר, ולא את האובייקטים ששומרים את שאר הנתונים בערימה.

ProfilingManager מאפשר לכם לתעד תמונות מצב של הזיכרון מ-Java ממכשירים בשטח ובאופן מקומי, כדי שתוכלו לבדוק את תרשים שמירת האובייקטים בממשק המשתמש של Perfetto ולזהות מה תופס מקום בזיכרון.

תרחיש

נניח שכלי הדיווח על קריסות מסמן אשכול הולך וגדל של קריסות של OutOfMemoryError בסביבת הייצור, כשהמשתמשים מבצעים אינטראקציה עם OomSampleActivity.

כדי לבדוק את הבעיה, אתם רושמים טריגר של TRIGGER_TYPE_OOM ב-ProfilingManager, כדי שהמערכת תתעד באופן אוטומטי תמונת מצב של הזיכרון ב-Java כשמתרחשת שגיאת OutOfMemoryError.

תיעוד תמונת המצב של הזיכרון

ב-Android 17 (רמת API‏ 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 מפעיל תיעוד והקריאה החוזרת (callback) מקבלת את ProfilingResult בהפעלה הבאה של האפליקציה, מעלים את קובץ הפרופיל לשרת או מושכים אותו למחשב הפיתוח באמצעות adb. אם תיעדתם את הפרופיל מגרסת build להפצה שמשתמשת בכיווץ קוד וערפול קוד של R8, בטלו את ערפול הקוד אופליין בתחנת העבודה או בשרת לפני הניתוח. בלי פענוח קוד מעורפל (deobfuscate), R8 מערפל את שמות המחלקות, השדות והשיטות של Java ו-Kotlin בתמונת מצב של הזיכרון הגולמית (לדוגמה, הופך את שמות המחלקות המקוריים ל-a.b).

משתמשים בפקודה trace_processor bundle של Perfetto עם הדגל --proguard-map כדי לארוז את הפרופיל שנוצר יחד עם קובץ ה-R8 mapping.txt שנוצר לאותה גרסת build להפצה:

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, כמו שמתואר בשלבים הבאים, או להפוך את החקירה של ה-OOM לאוטומטית באמצעות סוכן AI.

פותחים את תמונת המצב של הזיכרון שעברה ביטול ערפול בממשק המשתמש של Perfetto ובוחרים באפשרות Heapdump Explorer בסרגל הניווט הימני. בכרטיסייה Flamegraph, כשהאפשרות Measure מוגדרת ל-Object Size, היררכיית השמירה באיור 1 בולטת מיד.

תרשים להבות (flame graph) של Perfetto UI Heapdump Explorer שבו מוצג StatsReporter ששומר 8 מופעים של OomSampleActivity בנפח כולל של 240.28MiB.
איור 1. תרשים להבות (flame graph) של תמונת מצב של הזיכרון שמוצגים בו 8 מופעים שמורים של OomSampleActivity.

בדיקה של תרשים הלהבות באיור 1 חושפת כמה ממצאים חשובים:

  • שמירה של כמות עצומה של זיכרון: מתוך ערימה (heap) כוללת של 254.79MiB (root), com.example.sampleapp.OomSampleActivity שומר על גודל מצטבר של 240.28MiB (94.31% מכל הערימה של האפליקציה), שרובו נמצא במערך צאצא byte[].
  • מספר מופעים של פעילות בזמן אמת: בחירה בפריים OomSampleActivity מציגה את הערך Self Count: 8. למרות שרק מופע אחד של OomSampleActivity אמור להופיע במסך בכל זמן נתון, 8 מופעים נפרדים פועלים בזיכרון בו-זמנית.
  • נתיב האובייקט ששומר על המופעים בזיכרון: קריאת הפריימים ההורים של OomSampleActivity בתרשים הלהבות מראה את שרשרת ההפניות ששומרת על המופעים האלה בזיכרון: com.example.sampleapp.StatsReporter מכיל java.util.concurrent.CopyOnWriteArrayList (שמגובה על ידי java.lang.Object[]), שמפנה ישירות לכל 8 המופעים של OomSampleActivity.

זיהוי שורש הבעיה בקוד

כשהנתיב של שמירת הנתונים מצביע מ-StatsReporter אל OomSampleActivity, בודקים את שתי המחלקות בבסיס הקוד.

‫StatsReporter הוא סינגלטון שמנהל רשימה של קריאות חוזרות (callbacks) מסוג 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 שומר 8 מופעים של OomSampleActivity:

  1. כל מופע של OomSampleActivity מקצה מאגר נתונים זמני של 30MiB byte[] (heavyData ב-Kotlin,‏ mHeavyData ב-Java) ורושם את עצמו כמאזין ב-singleton StatsReporter ב-onCreate.
  2. ‫OomSampleActivity אף פעם לא קורא ל-unregisterListener כשהמערכת משמידה אותו. מכיוון ש-StatsReporter ממשיך להתקיים אחרי הפעילות ומכיל הפניה חזקה לכל מאזין רשום, אוסף האשפה לא יכול להחזיר מופעים שנהרסו או את המערכים שלהם בגודל 30MiB‏ byte[].
  3. בכל פעם שהמערכת משמידה ויוצרת מחדש את OomSampleActivity, למשל במהלך סיבוב מסך במחזור החיים של פעילות ב-Android, מופע נוסף של 30MiB דולף ב-StatsReporter. אחרי 7 סיבובים (8 מופעים × 30MiB ≈ 240MiB), ה-heap מתמלא והאפליקציה קורסת עם OutOfMemoryError.

תיקון הדליפה

כדי לתקן את דליפת הזיכרון, צריך לשנות את onDestroy בפעילות ולקרוא ל-StatsReporter.getInstance().unregisterListener(this), כדי שה-singleton ישחרר את ההפניה שלו כשהמערכת תשמיד את הפעילות:

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);
    }
}

אוטומציה של חקירת OOM באמצעות סוכן AI

במקום לבדוק ידנית את תרשים הלהבות ולעקוב אחרי הפניות בין קבצים, אתם יכולים להשתמש בסוכן תכנות מבוסס-AI שמוגדר עם מיומנות Perfetto AI כדי לנתח את תמונת המצב של הזיכרון ולקשר אותה לבסיס הקוד:

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 בלי לבטל את הרישום שלו, ויוצר תיקון שמוסיף את unregisterListener(this) ב-onDestroy (הדוח הבא נוצר מתיעוד נפרד שהיו בו 7 מופעים שנשמרו בזמן הקריסה):

# 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, תיקון מוצלח מונע את הפעלת הטריגר. כדי לבדוק את ה-heap ולוודא שהדליפה נעלמה, מסובבים את המסך ואז לוכדים תמונת מצב של ה-Java heap לפי דרישה משורת הפקודה:

  1. משביתים את מגביל הקצב (rate_limiter.disabled true) ושומרים את התוצאות הזמניות (delete_temporary_results.disabled true) כדי שהמערכת תשמור את תיעוד שורת הפקודה ב-/data/misc/perfetto-traces/profiling/.
  2. פותחים את OomSampleActivityFixed במכשיר הבדיקה, מסובבים את המסך 8 פעמים או יותר כדי לוודא שהאפליקציה לא קורסת יותר, ומשאירים אותה בחזית.
  3. מפעילים תמונת מצב של הזיכרון מ-Java לפי דרישה (profiling_type 1) באמצעות פקודת המעטפת cmd profiling_service request-profiling:

    adb shell cmd profiling_service request-profiling 1 com.example.sampleapp
    
  4. שולפים את הפרופיל שנשמר מ-/data/misc/perfetto-traces/profiling/ ופותחים אותו בממשק המשתמש של Perfetto.

תרשים להבות (flame graph) של Heapdump Explorer בממשק המשתמש של Perfetto אחרי התיקון, שבו רואים מופע יחיד של OomSampleActivityFixed ששומר על 30.01MiB.
איור 2. תרשים להבות (flame graph) של תמונת מצב של הזיכרון אחרי התיקון, שבו מוצג מופע יחיד של OomSampleActivityFixed.

כמו שמוצג באיור 2, אחרי כמה סיבובי מסך:

  • com.example.sampleapp.OomSampleActivityFixed כולל ספירה עצמית של 1 במקום לצבור מופע בכל סיבוב.
  • הנפח שמתפנה (retained size) המצטבר נשאר יציב על 30.01MiB (מתוך ערימה כוללת של 44.27MiB), מה שמאשר שמנגנון איסוף הזבל מחזיר מופעי פעילות קודמים.

אם קשה לשחזר את הבעיה באופן מקומי

אם ניווט מורכב בין כמה מסכים, סשנים ארוכים או נתוני משתמשים ספציפיים גורמים ל-OutOfMemoryError שאי אפשר לשחזר באופן מהימן במכשיר בדיקה, צריך לאמת את התיקון בסביבת הייצור אחרי ההשקה:

  • עוקבים אחרי אשכול הקריסות של OutOfMemoryError בכלי לדיווח על קריסות, בתור מדד חלופי בכל הגרסאות, כדי לוודא שיש ירידה בקריסות שקשורות ל-OomSampleActivity.
  • בודקים את כל תמונות המצב החדשות של הזיכרון שנאספו מ-TRIGGER_TYPE_OOM בגרסה המעודכנת, כדי לוודא ש-StatsReporter לא שומר יותר מופעים של OomSampleActivity שנהרסו.