Skip to content
This repository was archived by the owner on Oct 18, 2024. It is now read-only.

Commit 908162b

Browse files
committed
feat: add memory usage watcher to show memory usage of the IDE (#1409)
1 parent 14d9f79 commit 908162b

File tree

21 files changed

+1321
-95
lines changed

21 files changed

+1321
-95
lines changed

.idea/deploymentTargetSelector.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ dependencies {
8787
implementation(libs.common.kotlin.coroutines.android)
8888
implementation(libs.common.retrofit)
8989
implementation(libs.common.retrofit.gson)
90+
implementation(libs.common.charts)
91+
implementation(libs.common.hiddenApiBypass)
9092

9193
implementation(libs.google.auto.service.annotations)
9294
implementation(libs.google.gson)

app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,6 @@ class MainActivity : EdgeToEdgeIDEActivity() {
9898
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
9999
}
100100

101-
private fun openOnboarding() {
102-
startActivity(Intent(this, OnboardingActivity::class.java))
103-
}
104-
105101
override fun onInsetsUpdated(insets: Rect) {
106102
super.onInsetsUpdated(insets)
107103
binding.fragmentContainersParent.setPadding(insets.left, 0, insets.right, insets.bottom)

app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ import android.graphics.Color
2323
import android.graphics.Rect
2424
import android.graphics.drawable.GradientDrawable
2525
import android.os.Bundle
26+
import android.os.Process
2627
import android.text.SpannableStringBuilder
2728
import android.text.Spanned
2829
import android.text.TextUtils
2930
import android.text.method.LinkMovementMethod
3031
import android.text.style.ClickableSpan
3132
import android.view.View
32-
import android.view.ViewGroup
3333
import android.view.ViewTreeObserver.OnGlobalLayoutListener
3434
import androidx.activity.OnBackPressedCallback
3535
import androidx.activity.result.ActivityResult
@@ -39,17 +39,25 @@ import androidx.activity.viewModels
3939
import androidx.annotation.GravityInt
4040
import androidx.annotation.StringRes
4141
import androidx.appcompat.app.ActionBarDrawerToggle
42+
import androidx.collection.MutableIntIntMap
4243
import androidx.core.view.GravityCompat
43-
import androidx.core.view.updateLayoutParams
4444
import androidx.core.view.updatePaddingRelative
45+
import com.blankj.utilcode.constant.MemoryConstants
46+
import com.blankj.utilcode.util.ConvertUtils.byte2MemorySize
4547
import com.blankj.utilcode.util.FileUtils
4648
import com.blankj.utilcode.util.KeyboardUtils
4749
import com.blankj.utilcode.util.ThreadUtils
50+
import com.github.mikephil.charting.components.AxisBase
51+
import com.github.mikephil.charting.data.Entry
52+
import com.github.mikephil.charting.data.LineData
53+
import com.github.mikephil.charting.data.LineDataSet
54+
import com.github.mikephil.charting.formatter.IAxisValueFormatter
4855
import com.google.android.material.bottomsheet.BottomSheetBehavior
4956
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
5057
import com.google.android.material.snackbar.Snackbar
5158
import com.google.android.material.tabs.TabLayout
5259
import com.google.android.material.tabs.TabLayout.Tab
60+
import com.itsaky.androidide.R
5361
import com.itsaky.androidide.R.string
5462
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_FILE_TABS
5563
import com.itsaky.androidide.adapters.DiagnosticsAdapter
@@ -82,7 +90,9 @@ import com.itsaky.androidide.utils.ApkInstallationSessionCallback
8290
import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder
8391
import com.itsaky.androidide.utils.InstallationResultHandler.onResult
8492
import com.itsaky.androidide.utils.IntentUtils
93+
import com.itsaky.androidide.utils.MemoryUsageWatcher
8594
import com.itsaky.androidide.utils.flashError
95+
import com.itsaky.androidide.utils.resolveAttr
8696
import com.itsaky.androidide.viewmodel.EditorViewModel
8797
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
8898
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
@@ -94,6 +104,7 @@ import org.greenrobot.eventbus.ThreadMode.MAIN
94104
import org.slf4j.Logger
95105
import org.slf4j.LoggerFactory
96106
import java.io.File
107+
import kotlin.math.roundToLong
97108

98109
/**
99110
* Base class for EditorActivity which handles most of the view related things.
@@ -108,6 +119,8 @@ abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSele
108119
protected var diagnosticInfoBinding: LayoutDiagnosticInfoBinding? = null
109120
protected var filesTreeFragment: FileTreeFragment? = null
110121
protected var editorBottomSheet: BottomSheetBehavior<out View?>? = null
122+
protected val memoryUsageWatcher = MemoryUsageWatcher()
123+
protected val pidToDatasetIdxMap = MutableIntIntMap(initialCapacity = 3)
111124

112125
var isDestroying = false
113126
protected set
@@ -141,9 +154,33 @@ abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSele
141154
}
142155
}
143156

157+
private val memoryUsageListener = MemoryUsageWatcher.MemoryUsageListener { memoryUsage ->
158+
memoryUsage.forEachValue { proc ->
159+
_binding?.memUsageView?.chart?.apply {
160+
val dataset = (data.getDataSetByIndex(pidToDatasetIdxMap[proc.pid]) as LineDataSet?) ?: run {
161+
log.error("No dataset found for process: {}: {}", proc.pid, proc.pname)
162+
return@forEachValue
163+
}
164+
165+
dataset.entries.mapIndexed { index, entry ->
166+
entry.y = byte2MemorySize(proc.usageHistory[index], MemoryConstants.MB).toFloat()
167+
}
168+
169+
dataset.label = "%s - %.2fMB".format(proc.pname, dataset.entries.last().y)
170+
dataset.notifyDataSetChanged()
171+
data.notifyDataChanged()
172+
notifyDataSetChanged()
173+
invalidate()
174+
}
175+
}
176+
}
177+
144178
private var optionsMenuInvalidator: Runnable? = null
145179

146180
companion object {
181+
@JvmStatic protected val PROC_IDE = "IDE"
182+
@JvmStatic protected val PROC_GRADLE_TOOLING = "Gradle Tooling"
183+
@JvmStatic protected val PROC_GRADLE_DAEMON = "Gradle Daemon"
147184

148185
@JvmStatic
149186
protected val log: Logger = LoggerFactory.getLogger(BaseEditorActivity::class.java)
@@ -180,6 +217,8 @@ abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSele
180217
installationCallback = null
181218

182219
if (isDestroying) {
220+
memoryUsageWatcher.stopWatching(true)
221+
memoryUsageWatcher.listener = null
183222
editorActivityScope.cancelIfActive("Activity is being destroyed")
184223
}
185224
}
@@ -258,10 +297,81 @@ abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSele
258297

259298
uiDesignerResultLauncher = registerForActivityResult(StartActivityForResult(),
260299
this::handleUiDesignerResult)
300+
301+
setupMemUsageChart()
302+
watchMemory()
303+
}
304+
305+
private fun setupMemUsageChart() {
306+
binding.memUsageView.chart.apply {
307+
val colorAccent = resolveAttr(R.attr.colorAccent)
308+
309+
isDragEnabled = false
310+
description.isEnabled = false
311+
xAxis.axisLineColor = colorAccent
312+
axisRight.axisLineColor = colorAccent
313+
314+
setPinchZoom(false)
315+
setBackgroundColor(resolveAttr(R.attr.colorSurfaceContainer))
316+
setDrawGridBackground(true)
317+
setScaleEnabled(true)
318+
319+
axisLeft.isEnabled = false
320+
axisRight.valueFormatter = object :
321+
IAxisValueFormatter {
322+
override fun getFormattedValue(value: Float, axis: AxisBase?): String {
323+
return "%dMB".format(value.roundToLong())
324+
}
325+
}
326+
}
327+
}
328+
329+
private fun watchMemory() {
330+
memoryUsageWatcher.listener = memoryUsageListener
331+
memoryUsageWatcher.watchProcess(Process.myPid(), PROC_IDE)
332+
resetMemUsageChart()
333+
}
334+
335+
protected fun resetMemUsageChart() {
336+
val processes = memoryUsageWatcher.getMemoryUsages()
337+
val datasets = Array(processes.size) { index ->
338+
LineDataSet(List(MemoryUsageWatcher.MAX_USAGE_ENTRIES) { Entry(it.toFloat(), 0f) }, processes[index].pname)
339+
}
340+
341+
for ((index, proc) in processes.withIndex()) {
342+
val dataset = datasets[index]
343+
dataset.color = getMemUsageLineColorFor(proc)
344+
dataset.setDrawIcons(false)
345+
dataset.setDrawCircles(false)
346+
dataset.setDrawCircleHole(false)
347+
dataset.setDrawValues(false)
348+
dataset.formLineWidth = 1f
349+
dataset.formSize = 15f
350+
dataset.isHighlightEnabled = false
351+
pidToDatasetIdxMap[proc.pid] = index
352+
}
353+
354+
binding.memUsageView.chart.apply {
355+
data = LineData(*datasets)
356+
notifyDataSetChanged()
357+
invalidate()
358+
}
359+
}
360+
361+
private fun getMemUsageLineColorFor(proc: MemoryUsageWatcher.ProcessMemoryInfo): Int {
362+
return when (proc.pname) {
363+
PROC_IDE -> Color.BLUE
364+
PROC_GRADLE_TOOLING -> Color.RED
365+
PROC_GRADLE_DAEMON -> Color.GREEN
366+
else -> throw IllegalArgumentException("Unknown process: $proc")
367+
}
261368
}
262369

263370
override fun onPause() {
264371
super.onPause()
372+
memoryUsageWatcher.listener = null
373+
memoryUsageWatcher.stopWatching(false)
374+
265375
this.isDestroying = isFinishing
266376
getFileTreeFragment()?.saveTreeState()
267377
}
@@ -270,6 +380,9 @@ abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSele
270380
super.onResume()
271381
invalidateOptionsMenu()
272382

383+
memoryUsageWatcher.listener = memoryUsageListener
384+
memoryUsageWatcher.startWatching()
385+
273386
try {
274387
getFileTreeFragment()?.listProjectFiles()
275388
} catch (th: Throwable) {
@@ -280,7 +393,6 @@ abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSele
280393

281394
override fun onStop() {
282395
super.onStop()
283-
284396
checkIsDestroying()
285397
}
286398

app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,27 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
458458
service.setEventListener(mBuildEventListener)
459459

460460
if (!service.isToolingServerStarted()) {
461-
service.startToolingServer { initializeProject() }
461+
service.startToolingServer { pid ->
462+
memoryUsageWatcher.watchProcess(pid, PROC_GRADLE_TOOLING)
463+
resetMemUsageChart()
464+
465+
service.metadata().whenComplete { metadata, err ->
466+
if (metadata == null || err != null) {
467+
log.error("Failed to get tooling server metadata")
468+
return@whenComplete
469+
}
470+
471+
if (pid != metadata.pid) {
472+
log.warn(
473+
"Tooling server pid mismatch. Expected: {}, Actual: {}. Replacing memory watcher...",
474+
pid, metadata.pid)
475+
memoryUsageWatcher.watchProcess(metadata.pid, PROC_GRADLE_TOOLING)
476+
resetMemUsageChart()
477+
}
478+
}
479+
480+
initializeProject()
481+
}
462482
} else {
463483
initializeProject()
464484
}

app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,15 @@ import com.itsaky.androidide.utils.RecyclableObjectPool
6060
import com.itsaky.androidide.utils.VMUtils
6161
import com.itsaky.androidide.utils.flashError
6262
import com.termux.app.TermuxApplication
63+
import com.termux.shared.reflection.ReflectionUtils
6364
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
6465
import kotlinx.coroutines.DelicateCoroutinesApi
6566
import kotlinx.coroutines.GlobalScope
6667
import kotlinx.coroutines.launch
6768
import org.greenrobot.eventbus.EventBus
6869
import org.greenrobot.eventbus.Subscribe
6970
import org.greenrobot.eventbus.ThreadMode
71+
import org.lsposed.hiddenapibypass.HiddenApiBypass
7072
import org.slf4j.LoggerFactory
7173
import java.lang.Thread.UncaughtExceptionHandler
7274
import java.time.Duration
@@ -115,6 +117,7 @@ class IDEApplication : TermuxApplication() {
115117

116118
EditorColorScheme.setDefault(SchemeAndroidIDE.newInstance(null))
117119

120+
ReflectionUtils.bypassHiddenAPIReflectionRestrictions()
118121
GlobalScope.launch {
119122
IDEColorSchemeProvider.init()
120123
}

app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import com.itsaky.androidide.tooling.api.messages.result.BuildResult
5555
import com.itsaky.androidide.tooling.api.messages.result.GradleWrapperCheckResult
5656
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
5757
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
58+
import com.itsaky.androidide.tooling.api.models.ToolingServerMetadata
5859
import com.itsaky.androidide.tooling.events.ProgressEvent
5960
import com.itsaky.androidide.utils.Environment
6061
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment
@@ -361,6 +362,11 @@ class GradleBuildService : Service(), BuildService, IToolingApiClient,
361362
buildNotification(message, isProgress))
362363
}
363364

365+
override fun metadata(): CompletableFuture<ToolingServerMetadata> {
366+
checkServerStarted()
367+
return server!!.metadata()
368+
}
369+
364370
override fun initializeProject(
365371
params: InitializeProjectParams): CompletableFuture<InitializeResult> {
366372
checkServerStarted()
@@ -428,7 +434,7 @@ class GradleBuildService : Service(), BuildService, IToolingApiClient,
428434
}
429435

430436
if (toolingServerRunner!!.isStarted && listener != null) {
431-
listener.onServerStarted()
437+
listener.onServerStarted(toolingServerRunner!!.pid!!)
432438
} else {
433439
setServerListener(listener)
434440
}

app/src/main/java/com/itsaky/androidide/services/builder/ToolingServerRunner.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.itsaky.androidide.tooling.api.IToolingApiClient
2626
import com.itsaky.androidide.tooling.api.IToolingApiServer
2727
import com.itsaky.androidide.tooling.api.util.ToolingApiLauncher
2828
import com.itsaky.androidide.utils.Environment
29+
import com.termux.shared.reflection.ReflectionUtils
2930
import kotlinx.coroutines.CancellationException
3031
import kotlinx.coroutines.CoroutineName
3132
import kotlinx.coroutines.CoroutineScope
@@ -47,6 +48,7 @@ internal class ToolingServerRunner(
4748
private var observer: Observer?,
4849
) {
4950

51+
internal var pid: Int? = null
5052
private var _job: Job? = null
5153
private var _isStarted = AtomicBoolean(false)
5254

@@ -104,6 +106,9 @@ internal class ToolingServerRunner(
104106
this.environment = envs
105107
}
106108

109+
pid = ReflectionUtils.getDeclaredField(process::class.java, "pid")?.get(process) as Int?
110+
pid ?: throw IllegalStateException("Unable to get process ID")
111+
107112
val inputStream = process.inputStream
108113
val outputStream = process.outputStream
109114
val errorStream = process.errorStream
@@ -134,7 +139,7 @@ internal class ToolingServerRunner(
134139

135140
isStarted = true
136141

137-
listener?.onServerStarted()
142+
listener?.onServerStarted(pid!!)
138143

139144
// we don't need the listener anymore
140145
// also, this might be a reference to the activity
@@ -190,6 +195,6 @@ internal class ToolingServerRunner(
190195
fun interface OnServerStartListener {
191196

192197
/** Called when the tooling API server has been successfully started. */
193-
fun onServerStarted()
198+
fun onServerStarted(pid: Int)
194199
}
195200
}

0 commit comments

Comments
 (0)