Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ import com.blankj.utilcode.util.SizeUtils
import com.blankj.utilcode.util.ThreadUtils
import com.github.mikephil.charting.components.AxisBase
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.components.LegendEntry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import com.github.mikephil.charting.formatter.IAxisValueFormatter
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
Expand Down Expand Up @@ -165,6 +168,20 @@ import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong

/**
* Build one explicit [LegendEntry] per dataset (label + line color) for the memory-usage chart.
*
* Setting these via `chart.legend.setCustom(...)` keeps the legend's entry list exactly in sync
* with the current datasets, so MPAndroidChart's `LegendRenderer` never lazily recomputes a stale
* list when the dataset count changes (Gradle daemons connecting) — the desync behind the
* `IndexOutOfBoundsException` in ADFA-4327. Top-level + `internal` so it is unit-testable without an
* Activity instance.
*/
internal fun buildMemUsageLegendEntries(datasets: List<ILineDataSet>): List<LegendEntry> =
datasets.map { dataset ->
LegendEntry(dataset.label, Legend.LegendForm.DEFAULT, Float.NaN, Float.NaN, null, dataset.color)
}

/**
* Base class for EditorActivity which handles most of the view related things.
*
Expand Down Expand Up @@ -284,11 +301,16 @@ abstract class BaseEditorActivity :
}

if (dataChanged) {
_binding?.memUsageView?.chart?.apply {
data.notifyDataChanged()
notifyDataSetChanged()
invalidate()
}
runCatching {
_binding?.memUsageView?.chart?.apply {
// Refresh explicit legend entries so the live "<name> - <MB>" labels stay
// current while keeping the entry list consistent with the datasets.
legend.setCustom(buildMemUsageLegendEntries(data.dataSets))
data.notifyDataChanged()
notifyDataSetChanged()
invalidate()
}
}.onFailure { log.warn("Failed to update memory usage chart", it) }
}
}

Expand Down Expand Up @@ -818,6 +840,10 @@ abstract class BaseEditorActivity :

isDragEnabled = false
description.isEnabled = false
// Legend stays enabled; its entries are set EXPLICITLY in resetMemUsageChart /
// memoryUsageListener (see buildMemUsageLegendEntries) so the upstream LegendRenderer never
// auto-recomputes a stale entry list when the dataset count changes as Gradle daemons
// connect — that desync was the IndexOutOfBoundsException in ADFA-4327.
xAxis.axisLineColor = colorAccent
axisRight.axisLineColor = colorAccent

Expand Down Expand Up @@ -872,18 +898,24 @@ abstract class BaseEditorActivity :

binding.memUsageView.chart.setBackgroundColor(bgColor)

binding.memUsageView.chart.apply {
data = LineData(*datasets)
axisRight.textColor = textColor
axisLeft.textColor = textColor
legend.textColor = textColor

data.setValueTextColor(textColor)
setBackgroundColor(bgColor)
setGridBackgroundColor(bgColor)
notifyDataSetChanged()
invalidate()
}
runCatching {
binding.memUsageView.chart.apply {
data = LineData(*datasets)
axisRight.textColor = textColor
axisLeft.textColor = textColor
legend.textColor = textColor

// Set explicit, count-consistent legend entries so the auto-legend never
// recomputes a stale list against the new dataset count (ADFA-4327 crash).
legend.setCustom(buildMemUsageLegendEntries(datasets.toList()))

data.setValueTextColor(textColor)
setBackgroundColor(bgColor)
setGridBackgroundColor(bgColor)
notifyDataSetChanged()
invalidate()
}
}.onFailure { log.warn("Failed to reset memory usage chart", it) }
}

private fun getMemUsageLineColorFor(proc: MemoryUsageWatcher.ProcessMemoryInfo): Int =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.activities.editor

import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/**
* Unit tests for [buildMemUsageLegendEntries] (ADFA-4327 / Sentry APPDEVFORALL-31).
*
* The mem-usage chart crashed in MPAndroidChart's `LegendRenderer.renderLegend` with an
* `IndexOutOfBoundsException` when the auto-computed legend desynced from the dataset count as that
* count changed at runtime (Gradle daemons connecting → `resetMemUsageChart` rebuilds the datasets).
* The fix sets the legend entries EXPLICITLY so they always match the datasets. These tests pin that
* invariant — one entry per dataset, label/color carried through, and the entry count tracking the
* dataset count across a count change (the exact condition that triggered the renderer desync).
*
* The literal draw-time IndexOutOfBoundsException is a race inside the third-party renderer and is
* verified on-device (A56 recording on the PR); this test guards the anti-desync invariant the fix
* relies on.
*/
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class MemUsageLegendEntriesTest {

private fun dataSet(label: String, color: Int): LineDataSet =
LineDataSet(listOf(Entry(0f, 0f), Entry(1f, 1f)), label).also { it.color = color }

@Test
fun `produces one entry per dataset with matching label and color`() {
val datasets: List<ILineDataSet> =
listOf(
dataSet("Code on the Go - 12.00MB", 0xFF0000FF.toInt()),
dataSet("gradle daemon - 34.00MB", 0xFF00FF00.toInt()),
)

val entries = buildMemUsageLegendEntries(datasets)

assertThat(entries).hasSize(2)
assertThat(entries.map { it.label })
.containsExactly("Code on the Go - 12.00MB", "gradle daemon - 34.00MB")
.inOrder()
assertThat(entries.map { it.formColor })
.containsExactly(0xFF0000FF.toInt(), 0xFF00FF00.toInt())
.inOrder()
assertThat(entries.map { it.form })
.containsExactly(Legend.LegendForm.DEFAULT, Legend.LegendForm.DEFAULT)
}

@Test
fun `entry count tracks dataset count across a count change`() {
// 1 dataset (IDE only) -> 3 datasets (Gradle tooling + daemon connect). The legend entry list
// must always equal the current dataset count; that consistency is exactly what stops the
// LegendRenderer from indexing a stale entry array (the ADFA-4327 crash).
val before = buildMemUsageLegendEntries(listOf(dataSet("ide", 1)))
assertThat(before).hasSize(1)

val after =
buildMemUsageLegendEntries(
listOf(dataSet("ide", 1), dataSet("tooling", 2), dataSet("daemon", 3)),
)
assertThat(after).hasSize(3)
assertThat(after.map { it.label }).containsExactly("ide", "tooling", "daemon").inOrder()
}

@Test
fun `empty datasets produce empty entries`() {
assertThat(buildMemUsageLegendEntries(emptyList())).isEmpty()
}
}
Loading