diff --git a/unity/memory-utilities/AndroidProcessStats.cs b/unity/memory-utilities/AndroidProcessStats.cs
new file mode 100644
index 00000000..745a659c
--- /dev/null
+++ b/unity/memory-utilities/AndroidProcessStats.cs
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.IO;
+using UnityEngine;
+
+///
+/// A utility class for fetching Android process information using Android's ActivityManager via JNI.
+///
+///
+/// Performance Warning: Querying the ActivityManager via JNI is relatively slow and creates garbage
+/// on both the C# and Java sides. This should be done sparingly (e.g., once every few seconds),
+/// rather than every frame.
+///
+public static class AndroidProcessStats
+{
+ // Importance constants defined by android.app.ActivityManager$RunningAppProcessInfo
+
+ public const int IMPORTANCE_FOREGROUND = 100;
+ public const int IMPORTANCE_FOREGROUND_SERVICE = 125;
+ public const int IMPORTANCE_PERCEPTIBLE = 230;
+ public const int IMPORTANCE_CANT_SAVE_STATE = 170;
+ public const int IMPORTANCE_SERVICE = 300;
+ public const int IMPORTANCE_CACHED = 400;
+
+ ///
+ /// Fetches process information for all processes running under this app's UID.
+ /// This includes the main game process, any background services, or isolated WebViews.
+ ///
+ /// A list of processes and their OS-level importance.
+ public static System.Collections.Generic.List GetAllRunningAppProcesses()
+ {
+ var processes = new System.Collections.Generic.List();
+ if (Application.platform != RuntimePlatform.Android)
+ {
+ // Fallback for Unity Editor so testing can continue without crashing
+ processes.Add(new AppProcessInfo {
+ pid = System.Diagnostics.Process.GetCurrentProcess().Id,
+ processName = "Unity Editor (Fallback)",
+ importance = -1
+ });
+ return processes;
+ }
+
+ try
+ {
+ // Note on JNI (Java Native Interface):
+ // We use AndroidJavaClass and AndroidJavaObject to call Java methods from C#.
+ // The 'using' blocks ensure that the JNI local references are disposed of immediately,
+ // preventing memory leaks in the local reference table.
+ using (var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic("currentActivity"))
+ using (var context = activity.Call("getApplicationContext"))
+ using (var activityManager = context.Call("getSystemService", "activity"))
+ {
+ // Call ActivityManager.getRunningAppProcesses()
+ var runningProcesses = activityManager.Call("getRunningAppProcesses");
+ if (runningProcesses != null)
+ {
+ int count = runningProcesses.Call("size");
+ for (int i = 0; i < count; i++)
+ {
+ using (var processInfo = runningProcesses.Call("get", i))
+ {
+ var info = new AppProcessInfo();
+ info.pid = processInfo.Get("pid");
+ info.processName = processInfo.Get("processName");
+ info.importance = processInfo.Get("importance");
+
+ // Get the component causing this importance (if any).
+ // This helps identify *why* a process is kept alive (e.g., a specific background service).
+ using (var component = processInfo.Get("importanceReasonComponent"))
+ {
+ if (component != null)
+ {
+ info.reasonComponent = component.Call("flattenToShortString");
+ }
+ }
+ processes.Add(info);
+ }
+ }
+ }
+ }
+ }
+ catch (System.Exception e)
+ {
+ Debug.LogError($"[AndroidProcessStats] JNI Query Failed: {e.Message}");
+ }
+ return processes;
+ }
+
+ ///
+ /// Struct containing basic OS information about an app process.
+ ///
+ public struct AppProcessInfo
+ {
+ public int pid;
+ public string processName;
+ public int importance;
+ public string reasonComponent;
+ }
+}
+
+///
+/// Holds a snapshot of memory usage metrics read directly from the Linux procfs.
+///
+public struct UnityMemorySnapshot
+{
+ public int oomScoreAdj; // Process priority adjustment score (-1000 to +1000)
+ public long totalRssKb; // Total Resident Set Size in memory
+ public long anonRssKb; // Anonymous RSS (Java heap + unmanaged Native engine heap)
+ public long swapKb; // Compressed memory swapped into zRAM
+
+ ///
+ /// The sum of anonymous RSS and swap. This is often considered the "gold standard"
+ /// for measuring the background footprint of a process, as it represents memory
+ /// that cannot be discarded by the OS (unlike clean file-backed pages).
+ ///
+ public long anonRssPlusSwap => anonRssKb + swapKb;
+}
+
+///
+/// A utility class for directly reading Linux process virtual files (/proc).
+///
+///
+/// Reading from /proc is extremely fast and avoids the heavy overhead of JNI or
+/// allocating large Java objects. This makes it suitable for higher-frequency polling
+/// on a background thread.
+///
+public static class AndroidNativeDiagnostics
+{
+ ///
+ /// Reads the OOM (Out Of Memory) score adjustment directly from virtual Linux storage.
+ /// The higher the score, the more likely the process is to be killed by the low memory killer (LMK).
+ ///
+ /// The process ID.
+ /// The OOM score adjustment, or -9999 if it cannot be read.
+ public static int GetOomScoreAdj(int pid)
+ {
+ // /proc/[pid]/oom_score_adj contains the adjustment score set by Android's ActivityManager.
+ string path = $"/proc/{pid}/oom_score_adj";
+ if (!File.Exists(path))
+ {
+ return -9999;
+ }
+
+ try
+ {
+ string contents = File.ReadAllText(path).Trim();
+ if (int.TryParse(contents, out int score))
+ {
+ return score;
+ }
+ }
+ catch
+ {
+ }
+ return -9999;
+ }
+
+ ///
+ /// Parses /proc/{pid}/status in a single pass to fetch raw memory pages.
+ ///
+ /// The process ID.
+ /// A UnityMemorySnapshot containing RSS and Swap values.
+ public static UnityMemorySnapshot CaptureMemorySnapshot(int pid)
+ {
+ var snapshot = new UnityMemorySnapshot();
+ snapshot.oomScoreAdj = GetOomScoreAdj(pid);
+
+ // /proc/[pid]/status contains human-readable status information about the process,
+ // including detailed memory consumption breakdowns.
+ string path = $"/proc/{pid}/status";
+ if (!File.Exists(path))
+ {
+ return snapshot;
+ }
+
+ try
+ {
+ string[] lines = File.ReadAllLines(path);
+ foreach (string line in lines)
+ {
+ if (line.StartsWith("VmRSS:", StringComparison.OrdinalIgnoreCase))
+ {
+ snapshot.totalRssKb = ParseKbValue(line);
+ }
+ else if (line.StartsWith("RssAnon:", StringComparison.OrdinalIgnoreCase))
+ {
+ snapshot.anonRssKb = ParseKbValue(line);
+ }
+ else if (line.StartsWith("VmSwap:", StringComparison.OrdinalIgnoreCase))
+ {
+ snapshot.swapKb = ParseKbValue(line);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"[NativeDiagnostics] Failed parsing /proc/self/status: {e.Message}");
+ }
+
+ return snapshot;
+ }
+
+ private static long ParseKbValue(string line)
+ {
+ if (string.IsNullOrEmpty(line))
+ {
+ return 0;
+ }
+
+ try
+ {
+ // 1. Split by the colon first (e.g., "RssAnon: 154388 kB")
+ string[] colonParts = line.Split(':');
+ if (colonParts.Length < 2)
+ {
+ return 0; // Guard rail: Skip if no colon found
+ }
+
+ // 2. Extract the value portion string at index 1 (" 154388 kB")
+ string valueSection = colonParts[1];
+ string[] spaceParts = valueSection.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+
+ // 3. Extract the first string element at index 0 ("154388") and parse it
+ if (spaceParts.Length > 0 && long.TryParse(spaceParts[0], out long value))
+ {
+ return value;
+ }
+ }
+ catch (System.Exception e)
+ {
+ // Prevent a single malformed line from interrupting the telemetry update loop
+ Debug.LogWarning($"[NativeDiagnostics] Skipping malformed line '{line}': {e.Message}");
+ }
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/unity/memory-utilities/GameMemoryMonitor.cs b/unity/memory-utilities/GameMemoryMonitor.cs
new file mode 100644
index 00000000..ea7c2cc3
--- /dev/null
+++ b/unity/memory-utilities/GameMemoryMonitor.cs
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using UnityEngine;
+
+///
+/// A MonoBehaviour that monitors memory and process state for the application.
+/// It uses a background thread for high-frequency polling of lightweight /proc stats,
+/// and throttles heavier JNI calls on the main thread to avoid frame drops.
+///
+public class GameMemoryMonitor : MonoBehaviour
+{
+ public bool showDebugOverlay = true;
+ private string cachedOverlayText = "";
+ private float nextJniUpdateTime = 0f;
+ private float nextLogTime = 0f;
+
+ private List knownProcesses = new List();
+ private readonly object processLock = new object();
+
+ private Dictionary pidStatsMap = new Dictionary();
+ private readonly object statsLock = new object();
+
+ private Thread pollingThread;
+ private volatile bool isRunning = true;
+
+ private string[] pidColorHexes = new string[] {
+ "#FF5555", "#55FF55", "#FFFF55", "#5555FF",
+ "#FF55FF", "#55FFFF", "#FFFFFF", "#AAAAAA",
+ "#FF9999", "#9999FF"
+ };
+
+ ///
+ /// Container for tracking memory trends specific to a Process ID.
+ ///
+ public class PidStats
+ {
+ public string processName;
+ public string colorHex;
+
+ // Tracks the highest memory seen during each OOM adj state
+ public Dictionary maxMemPerOom = new Dictionary();
+ // Tracks time spent at various memory levels during each OOM adj state
+ public Dictionary> memHistograms = new Dictionary>();
+
+ // Latest snapshot data for GUI presentation
+ public int lastOomAdj;
+ public int lastImportance;
+ public long lastAnonRssKb;
+ public long lastSwapKb;
+ public string lastReasonComponent;
+ }
+
+ private void Start()
+ {
+ // PERFORMANCE: Start a background thread to read /proc files.
+ // File I/O (even virtual files like /proc) can occasionally block.
+ // Doing this off the main thread ensures we never stutter the game's framerate.
+ pollingThread = new Thread(BackgroundPollingLoop);
+ pollingThread.Start();
+ }
+
+ private void OnDestroy()
+ {
+ isRunning = false;
+ if (pollingThread != null)
+ {
+ if (!pollingThread.Join(500))
+ {
+ pollingThread.Abort();
+ }
+ }
+ }
+
+ private void OnApplicationPause(bool isPaused)
+ {
+ if (isPaused)
+ {
+ Debug.Log("[MemoryMonitor] Game minimized. Querying entry stats...");
+ LogDiagnostics("BACKGROUND_TRANSITION_START");
+ }
+ else
+ {
+ Debug.Log("[MemoryMonitor] Game resumed. Querying active stats...");
+ LogDiagnostics("FOREGROUND_RESUME");
+ }
+ }
+
+ ///
+ /// The background loop that continuously reads memory data.
+ /// Runs on a separate thread.
+ ///
+ private void BackgroundPollingLoop()
+ {
+ float pollInterval = 0.1f; // Poll at 10Hz to catch quick transitions
+
+ while (isRunning)
+ {
+ List currentProcesses;
+
+ // Thread Safety: Lock when reading the shared list of known processes
+ // which is updated by the main thread.
+ lock (processLock)
+ {
+ currentProcesses = new List(knownProcesses);
+ }
+
+ lock (statsLock)
+ {
+ foreach (var proc in currentProcesses)
+ {
+ if (!pidStatsMap.ContainsKey(proc.pid))
+ {
+ pidStatsMap[proc.pid] = new PidStats {
+ processName = proc.processName,
+ colorHex = pidColorHexes[pidStatsMap.Count % pidColorHexes.Length]
+ };
+ }
+
+ var stats = pidStatsMap[proc.pid];
+ UnityMemorySnapshot mem = AndroidNativeDiagnostics.CaptureMemorySnapshot(proc.pid);
+
+ if (mem.oomScoreAdj != -9999)
+ {
+ long currentTotal = mem.anonRssPlusSwap;
+
+ // Update Max
+ if (!stats.maxMemPerOom.ContainsKey(mem.oomScoreAdj))
+ {
+ stats.maxMemPerOom[mem.oomScoreAdj] = 0;
+ }
+ if (currentTotal > stats.maxMemPerOom[mem.oomScoreAdj])
+ {
+ stats.maxMemPerOom[mem.oomScoreAdj] = currentTotal;
+ }
+
+ // Update Histogram using absolute MB to prevent corruption when max changes
+ long bucketMb = currentTotal / 1024;
+
+ if (!stats.memHistograms.ContainsKey(mem.oomScoreAdj))
+ {
+ stats.memHistograms[mem.oomScoreAdj] = new Dictionary();
+ }
+ if (!stats.memHistograms[mem.oomScoreAdj].ContainsKey(bucketMb))
+ {
+ stats.memHistograms[mem.oomScoreAdj][bucketMb] = 0f;
+ }
+ stats.memHistograms[mem.oomScoreAdj][bucketMb] += pollInterval;
+
+ // Update latest snapshot
+ stats.lastOomAdj = mem.oomScoreAdj;
+ stats.lastImportance = proc.importance;
+ stats.lastAnonRssKb = mem.anonRssKb;
+ stats.lastSwapKb = mem.swapKb;
+ stats.lastReasonComponent = proc.reasonComponent;
+ }
+ }
+ }
+
+ Thread.Sleep(100);
+ }
+ }
+
+ private void Update()
+ {
+ if (!showDebugOverlay)
+ {
+ return;
+ }
+
+ // PERFORMANCE: Throttle the heavy JNI queries to once every 2 seconds.
+ // ActivityManager.getRunningAppProcesses() does IPC (Inter-Process Communication)
+ // to the system server, which is slow and creates garbage. We do NOT want to call this every frame.
+ if (Time.time > nextJniUpdateTime)
+ {
+ nextJniUpdateTime = Time.time + 2.0f;
+ var newProcs = AndroidProcessStats.GetAllRunningAppProcesses();
+
+ // Thread Safety: Lock when writing the shared list of known processes.
+ lock (processLock)
+ {
+ knownProcesses = newProcs;
+ }
+ RefreshOverlayText();
+ }
+
+ // Print histograms every 60 seconds
+ if (Time.time > nextLogTime && Time.time > 10.0f)
+ {
+ nextLogTime = Time.time + 60.0f;
+ LogHistogramSummary();
+ }
+ }
+
+ private void LogHistogramSummary()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine("[MemoryMonitor] --- 60s Periodic Histogram Summary ---");
+
+ lock (statsLock)
+ {
+ foreach (var kvp in pidStatsMap)
+ {
+ int pid = kvp.Key;
+ var stats = kvp.Value;
+ sb.AppendLine($"PID: {pid} ({stats.processName})");
+
+ foreach (var oomKvp in stats.maxMemPerOom)
+ {
+ int oom = oomKvp.Key;
+ long maxMem = oomKvp.Value;
+
+ float timeAbove95 = 0f;
+ long thresholdMb = (long)(maxMem * 0.95f) / 1024;
+
+ if (stats.memHistograms.ContainsKey(oom))
+ {
+ foreach (var bucketKvp in stats.memHistograms[oom])
+ {
+ if (bucketKvp.Key >= thresholdMb)
+ {
+ timeAbove95 += bucketKvp.Value;
+ }
+ }
+ }
+
+ sb.AppendLine($" OOM {oom}: Max Mem = {(maxMem / 1024f):F2} MB, Time >95% Peak = {timeAbove95:F1}s");
+ }
+ }
+ }
+ sb.AppendLine("---------------------------------------------------");
+ Debug.Log(sb.ToString());
+ }
+
+ private void RefreshOverlayText()
+ {
+ var sb = new StringBuilder();
+
+ lock (statsLock)
+ {
+ foreach (var kvp in pidStatsMap)
+ {
+ int pid = kvp.Key;
+ var stats = kvp.Value;
+
+ string calculatedProcGroup = MapToProcGroup(stats.lastOomAdj);
+
+ sb.AppendLine($"PID: {pid} ({stats.processName})");
+ sb.AppendLine($"OOM Adj: {stats.lastOomAdj} | Imp: {stats.lastImportance}");
+ sb.AppendLine($"State: {calculatedProcGroup}");
+
+ if (Application.platform == RuntimePlatform.Android)
+ {
+ sb.AppendLine($"Current Anon+Swap: {((stats.lastAnonRssKb + stats.lastSwapKb) / 1024f):F2} MB");
+
+ if (stats.maxMemPerOom.ContainsKey(stats.lastOomAdj))
+ {
+ sb.AppendLine($"Max at OOM {stats.lastOomAdj}: {(stats.maxMemPerOom[stats.lastOomAdj] / 1024f):F2} MB");
+ }
+
+ if (!string.IsNullOrEmpty(stats.lastReasonComponent))
+ {
+ sb.AppendLine($"Reason: {stats.lastReasonComponent}");
+ }
+ }
+ sb.AppendLine("-----------------------------");
+ }
+ }
+ cachedOverlayText = sb.ToString();
+ needsFontRecalc = true;
+ }
+
+ private int cachedFontSize = 14;
+ private bool needsFontRecalc = true;
+ private int lastScreenWidth = 0;
+
+ private void OnGUI()
+ {
+ if (!showDebugOverlay || string.IsNullOrEmpty(cachedOverlayText))
+ {
+ return;
+ }
+
+ float rectWidth = Screen.width;
+ float rectHeight = Screen.height * 0.5f;
+ Rect overlayRect = new Rect(0, 0, rectWidth, rectHeight);
+
+ // PERFORMANCE: Recalculate font size only when text changes or screen resizes.
+ // GUIStyle.CalcHeight is an expensive operation that computes layout.
+ // Doing this every frame would cause significant CPU overhead.
+ if (needsFontRecalc || lastScreenWidth != Screen.width)
+ {
+ lastScreenWidth = Screen.width;
+ needsFontRecalc = false;
+
+ // Allow Rich Text in CalcHeight
+ GUIStyle tempStyle = new GUIStyle(GUI.skin.label) { wordWrap = true, padding = new RectOffset(10, 10, 10, 10), richText = true };
+ int bestFont = 10;
+ for (int i = 70; i >= 10; i--)
+ {
+ tempStyle.fontSize = i;
+ if (tempStyle.CalcHeight(new GUIContent(cachedOverlayText), rectWidth) <= rectHeight)
+ {
+ bestFont = i;
+ break;
+ }
+ }
+ cachedFontSize = bestFont;
+ }
+
+ // Draw a translucent black background that doesn't block raycasts
+ Color oldColor = GUI.color;
+ GUI.color = new Color(0, 0, 0, 0.7f);
+ GUI.DrawTexture(overlayRect, Texture2D.whiteTexture);
+ GUI.color = oldColor;
+
+ // Draw the text
+ GUIStyle style = new GUIStyle(GUI.skin.label)
+ {
+ fontSize = cachedFontSize,
+ normal = { textColor = Color.green },
+ wordWrap = true,
+ padding = new RectOffset(10, 10, 10, 10),
+ richText = true
+ };
+
+ GUI.Label(overlayRect, cachedOverlayText, style);
+ }
+
+ private void LogDiagnostics(string contextTag)
+ {
+ var processes = AndroidProcessStats.GetAllRunningAppProcesses();
+
+ foreach (var proc in processes)
+ {
+ UnityMemorySnapshot mem = AndroidNativeDiagnostics.CaptureMemorySnapshot(proc.pid);
+ string calculatedProcGroup = MapToProcGroup(mem.oomScoreAdj);
+
+ string componentInfo = string.IsNullOrEmpty(proc.reasonComponent) ? "" : $" | Reason: {proc.reasonComponent}";
+
+ Debug.Log($"[{contextTag}] [PID: {proc.pid} ({proc.processName})] " +
+ $"OOM Score: {mem.oomScoreAdj} | " +
+ $"OS Importance: {proc.importance} | " +
+ $"Engine Proc State: {calculatedProcGroup} | " +
+ $"Anon RSS + Swap: {(mem.anonRssPlusSwap / 1024f):F2} MB{componentInfo}");
+ }
+ }
+
+ ///
+ /// Maps the raw OOM score adjustment to a human-readable Android process state.
+ ///
+ /// The OOM score adjustment value.
+ /// A string describing the state.
+ private string MapToProcGroup(int adj)
+ {
+ if (adj == 0)
+ {
+ return "0 (FOREGROUND_APP_ADJ): Foreground (focused/visible)";
+ }
+ if (adj == 50)
+ {
+ return "50 (RECENT_FOREGROUND): Briefly bumped to avoid disruption";
+ }
+ if (adj == 100)
+ {
+ return "100 (VISIBLE_APP_ADJ): Visible but not focused";
+ }
+ if (adj == 200)
+ {
+ return "200 (PERCEPTIBLE_APP_ADJ): Background with visible UI (e.g., PiP)";
+ }
+ if (adj >= 201 && adj <= 224)
+ {
+ return $"{adj} (PERCEPTIBLE_LESS_IMPORTANT): Less important perceptible";
+ }
+ if (adj == 225)
+ {
+ return "225 (PERCEPTIBLE_MEDIUM_APP_ADJ): Device state dependent (e.g. mic active)";
+ }
+ if (adj >= 226 && adj <= 249)
+ {
+ return $"{adj} (PERCEPTIBLE_LESS_IMPORTANT): Less important perceptible";
+ }
+ if (adj == 250)
+ {
+ return "250 (PERCEPTIBLE_LOW_APP_ADJ): Low perceptible (e.g. location)";
+ }
+ if (adj >= 300 && adj < 400)
+ {
+ return $"{adj} (BACKUP_APP_ADJ): Performing backup";
+ }
+ if (adj >= 400 && adj < 500)
+ {
+ return $"{adj} (HEAVY_WEIGHT): Heavy weight app";
+ }
+ if (adj >= 500 && adj < 600)
+ {
+ return $"{adj} (SERVICE_ADJ): Background service";
+ }
+ if (adj >= 600 && adj < 700)
+ {
+ return $"{adj} (HOME_APP_ADJ): Home/Launcher";
+ }
+ if (adj >= 700 && adj < 800)
+ {
+ return $"{adj} (PREVIOUS_APP_ADJ): Previous app (user navigated away)";
+ }
+ if (adj >= 800 && adj < 900)
+ {
+ return $"{adj} (SERVICE_B_ADJ): Less important service";
+ }
+ if (adj >= 900)
+ {
+ return $"{adj} (CACHED_APP_ADJ): Cached/Idle (expendable)";
+ }
+ return $"{adj} (UNKNOWN): Unmapped score";
+ }
+}
\ No newline at end of file
diff --git a/unity/memory-utilities/README.md b/unity/memory-utilities/README.md
new file mode 100644
index 00000000..bf28980c
--- /dev/null
+++ b/unity/memory-utilities/README.md
@@ -0,0 +1,8 @@
+# Memory Utilities
+
+This folder contains utility scripts for Unity developers to monitor and track the memory footprint and Android process state (OOM Score Adjustment) of their application.
+
+## Contents
+
+* **`AndroidProcessStats.cs`**: Demonstrates how to use JNI (Java Native Interface) within Unity to link into the Android `ActivityManager` APIs. It provides functionality to fetch the running app processes, their importance, and OS-level state. It also contains lightweight methods to read memory stats directly from the Linux `/proc` filesystem.
+* **`GameMemoryMonitor.cs`**: Shows how to hook this information up for continuous monitoring and debugging. It demonstrates how to poll the Android APIs on a background thread to prevent frame drops, and how to hook into lifecycle events (e.g., `OnApplicationPause`) to track memory changes when the app is backgrounded or resumed.