perf(app): lighter stress demo + profileable release build
Some checks failed
CI / build (push) Has been cancelled
Some checks failed
CI / build (push) Has been cancelled
- StressTestScreen: use MinMaxLTTB LOD (caps output <= pixelWidth, ~half the vertices vs MinMax's ~2x) and move the sample generator loop to Dispatchers.Default so it stops contending with the UI thread. - SignalGenerator: run the shared producer loop off the main thread. - Add a release buildType (R8 + resource shrink, debuggable=false, debug-signed for on-device profiling); proguard-rules.pro needs no keeps (no kotlinx.serialization; Compose/coroutines ship consumer rules). - minSdk 26 -> 24. Off-main producer un-starves the feed (280 -> ~1300 push/s); release build (AOT+R8) vs debug at matched load: CPU -12%, fps +48%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0e3360e4a0
commit
e827deb806
4 changed files with 74 additions and 33 deletions
|
|
@ -10,7 +10,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId = "dev.dtrentin.chart.demo"
|
||||
minSdk = 26
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
|
|
@ -23,6 +23,20 @@ android {
|
|||
|
||||
buildFeatures { compose = true }
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true // R8 — the perf lever
|
||||
isShrinkResources = true // strip unused resources
|
||||
isDebuggable = false // let ART fully optimize (perf test)
|
||||
// debug key so the release apk installs on a dev device (profiling build, not store build)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets["main"].kotlin.srcDirs("src/main/kotlin")
|
||||
}
|
||||
|
||||
|
|
|
|||
12
app/proguard-rules.pro
vendored
Normal file
12
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Release R8 rules for :app (profiling build).
|
||||
#
|
||||
# Intentionally minimal. Consumer rules ship with the libraries:
|
||||
# - Jetpack Compose (androidx.compose.*) — bundled consumer R8 rules
|
||||
# - kotlinx-coroutines — bundled consumer R8 rules (keeps ServiceLoader/volatile)
|
||||
# - AGP auto-keeps the manifest entry point (.MainActivity)
|
||||
#
|
||||
# No kotlinx.serialization in this app (chart-realtime does not apply the
|
||||
# serialization plugin), so no @Serializer keep rules are required.
|
||||
#
|
||||
# App-specific keeps are added below ONLY when a build/run failure proves them
|
||||
# necessary. Do NOT blanket-keep.
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package dev.dtrentin.chart.demo
|
||||
|
||||
import dev.dtrentin.chart.RealtimeChartState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Synthetic multi-signal producer. Single coroutine pushes one sample per active signal
|
||||
|
|
@ -34,9 +36,13 @@ suspend fun CoroutineScope.runSignalGenerator(
|
|||
|
||||
val tickIntervalMs = (1_000L / sampleRateHz).coerceAtLeast(1L)
|
||||
val dt = 1f / sampleRateHz.toFloat()
|
||||
|
||||
// Push work off Main: sample generation + state.push run on Dispatchers.Default so they
|
||||
// don't contend with Compose draw on the Main dispatcher. withContext inherits the caller's
|
||||
// Job → cancelling the caller (e.g. leaving the composable) cancels this loop (no leak).
|
||||
withContext(Dispatchers.Default) {
|
||||
val startMs = System.currentTimeMillis()
|
||||
var t = 0f
|
||||
|
||||
while (isActive) {
|
||||
val ts = startMs + (t * 1000f).toLong()
|
||||
for (i in signalNames.indices) {
|
||||
|
|
@ -49,3 +55,4 @@ suspend fun CoroutineScope.runSignalGenerator(
|
|||
delay(tickIntervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import dev.dtrentin.chart.RealtimeChart
|
|||
import dev.dtrentin.chart.RealtimeChartState
|
||||
import dev.dtrentin.chart.interaction.InteractionConfig
|
||||
import dev.dtrentin.chart.interaction.rememberChartInteractionState
|
||||
import dev.dtrentin.chart.lod.MinMaxLodStrategy
|
||||
import dev.dtrentin.chart.lod.MinMaxLttbLodStrategy
|
||||
import dev.dtrentin.chart.model.AxisConfig
|
||||
import dev.dtrentin.chart.model.AxisLabelMode
|
||||
import dev.dtrentin.chart.model.ChartConfig
|
||||
|
|
@ -39,8 +39,10 @@ import dev.dtrentin.chart.render.DecimalAxisFormatter
|
|||
import dev.dtrentin.chart.render.TimeAxisFormatter
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val STRESS_SIGNAL_COUNT = 8
|
||||
private const val STRESS_SAMPLE_HZ = 200
|
||||
|
|
@ -58,8 +60,9 @@ private val STRESS_COLORS = listOf(
|
|||
|
||||
/**
|
||||
* Stress test: 8 simultaneous signals @ 200 Hz each (1600 pushes/sec total). Uses
|
||||
* MinMax LoD (preserves peaks under heavy throughput). 10 s window — 16k samples
|
||||
* visible at once.
|
||||
* MinMaxLTTB LoD (preserves min/max envelope, output capped at pixelWidth). 10 s window —
|
||||
* 16k samples visible at once. Producer runs on [Dispatchers.Default] to keep sample
|
||||
* generation off the Main dispatcher (away from Compose draw).
|
||||
*/
|
||||
@Composable
|
||||
fun StressTestScreen() {
|
||||
|
|
@ -81,7 +84,7 @@ fun StressTestScreen() {
|
|||
),
|
||||
render = RenderConfig(
|
||||
theme = chartTheme,
|
||||
lodStrategy = MinMaxLodStrategy(),
|
||||
lodStrategy = MinMaxLttbLodStrategy(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -96,6 +99,10 @@ fun StressTestScreen() {
|
|||
SignalConfig(color = STRESS_COLORS[i], strokeWidth = 1.5f),
|
||||
)
|
||||
}
|
||||
// Push work off Main: generation + state.push run on Dispatchers.Default so they
|
||||
// don't contend with Compose draw on the Main dispatcher. withContext inherits the
|
||||
// effect's Job → leaving the composable cancels this loop (no leak).
|
||||
withContext(Dispatchers.Default) {
|
||||
val tickIntervalMs = (1_000L / STRESS_SAMPLE_HZ).coerceAtLeast(1L)
|
||||
val startMs = System.currentTimeMillis()
|
||||
var n = 0L
|
||||
|
|
@ -115,6 +122,7 @@ fun StressTestScreen() {
|
|||
delay(tickIntervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pushPerSec by remember { mutableIntStateOf(0) }
|
||||
var totalPushes by remember { mutableLongStateOf(0L) }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue