diff --git a/app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt b/app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt
new file mode 100644
index 0000000000..c82bb603fe
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt
@@ -0,0 +1,67 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.ui
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import com.github.mikephil.charting.charts.LineChart
+import org.slf4j.LoggerFactory
+
+/**
+ * A [LineChart] that guards its [onDraw] against the MPAndroidChart axis-rendering race.
+ *
+ * MPAndroidChart is not thread-safe: [com.github.mikephil.charting.renderer.AxisRenderer.computeAxisValues]
+ * writes `mEntryCount` and reallocates the `mEntries` array in two separate statements. When the view is
+ * drawn from more than one thread at once, a reader can observe the new `mEntryCount` while `mEntries` is
+ * still the old (shorter) array, throwing an [IndexOutOfBoundsException] from the label renderer.
+ *
+ * This happens in AndroidIDE because Sentry Session Replay records the screen by drawing the view
+ * hierarchy on a background thread, which races the main-thread updates of the memory-usage chart. The
+ * chart is a non-critical diagnostic view, so dropping the occasional frame is preferable to crashing the
+ * whole IDE. The next `invalidate()` recovers cleanly.
+ *
+ */
+class SafeLineChart : LineChart {
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr)
+
+ companion object {
+ private val log = LoggerFactory.getLogger(SafeLineChart::class.java)
+ }
+
+ private var skippedFrames = 0L
+
+ override fun onDraw(canvas: Canvas) {
+ try {
+ super.onDraw(canvas)
+ } catch (e: IndexOutOfBoundsException) {
+ // Transient race in MPAndroidChart's axis renderer (see class doc). Skip this frame.
+ // Only log occasionally to avoid flooding logcat, since onDraw runs every frame.
+ if (skippedFrames++ % 60L == 0L) {
+ log.warn("Skipped {} chart frame(s) due to a transient axis-rendering race", skippedFrames, e)
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/layout_mem_usage.xml b/app/src/main/res/layout/layout_mem_usage.xml
index 2622ff5157..92888099bc 100644
--- a/app/src/main/res/layout/layout_mem_usage.xml
+++ b/app/src/main/res/layout/layout_mem_usage.xml
@@ -21,7 +21,7 @@
android:layout_height="@dimen/editor_mem_usage_view_height">
-