Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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)
}
}
}
}
2 changes: 1 addition & 1 deletion app/src/main/res/layout/layout_mem_usage.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
android:layout_height="@dimen/editor_mem_usage_view_height">

<!-- Need to wrap this in a layout so that we can apply margins at runtime-->
<com.github.mikephil.charting.charts.LineChart
<com.itsaky.androidide.ui.SafeLineChart
android:id="@+id/chart"
android:layout_width="0dp"
android:layout_height="0dp"
Expand Down
Loading