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 {
|
defaultConfig {
|
||||||
applicationId = "dev.dtrentin.chart.demo"
|
applicationId = "dev.dtrentin.chart.demo"
|
||||||
minSdk = 26
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.1.0"
|
versionName = "0.1.0"
|
||||||
|
|
@ -23,6 +23,20 @@ android {
|
||||||
|
|
||||||
buildFeatures { compose = true }
|
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")
|
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
|
package dev.dtrentin.chart.demo
|
||||||
|
|
||||||
import dev.dtrentin.chart.RealtimeChartState
|
import dev.dtrentin.chart.RealtimeChartState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synthetic multi-signal producer. Single coroutine pushes one sample per active signal
|
* 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 tickIntervalMs = (1_000L / sampleRateHz).coerceAtLeast(1L)
|
||||||
val dt = 1f / sampleRateHz.toFloat()
|
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()
|
val startMs = System.currentTimeMillis()
|
||||||
var t = 0f
|
var t = 0f
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val ts = startMs + (t * 1000f).toLong()
|
val ts = startMs + (t * 1000f).toLong()
|
||||||
for (i in signalNames.indices) {
|
for (i in signalNames.indices) {
|
||||||
|
|
@ -48,4 +54,5 @@ suspend fun CoroutineScope.runSignalGenerator(
|
||||||
t += dt
|
t += dt
|
||||||
delay(tickIntervalMs)
|
delay(tickIntervalMs)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import dev.dtrentin.chart.RealtimeChart
|
||||||
import dev.dtrentin.chart.RealtimeChartState
|
import dev.dtrentin.chart.RealtimeChartState
|
||||||
import dev.dtrentin.chart.interaction.InteractionConfig
|
import dev.dtrentin.chart.interaction.InteractionConfig
|
||||||
import dev.dtrentin.chart.interaction.rememberChartInteractionState
|
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.AxisConfig
|
||||||
import dev.dtrentin.chart.model.AxisLabelMode
|
import dev.dtrentin.chart.model.AxisLabelMode
|
||||||
import dev.dtrentin.chart.model.ChartConfig
|
import dev.dtrentin.chart.model.ChartConfig
|
||||||
|
|
@ -39,8 +39,10 @@ import dev.dtrentin.chart.render.DecimalAxisFormatter
|
||||||
import dev.dtrentin.chart.render.TimeAxisFormatter
|
import dev.dtrentin.chart.render.TimeAxisFormatter
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
private const val STRESS_SIGNAL_COUNT = 8
|
private const val STRESS_SIGNAL_COUNT = 8
|
||||||
private const val STRESS_SAMPLE_HZ = 200
|
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
|
* 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
|
* MinMaxLTTB LoD (preserves min/max envelope, output capped at pixelWidth). 10 s window —
|
||||||
* visible at once.
|
* 16k samples visible at once. Producer runs on [Dispatchers.Default] to keep sample
|
||||||
|
* generation off the Main dispatcher (away from Compose draw).
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun StressTestScreen() {
|
fun StressTestScreen() {
|
||||||
|
|
@ -81,7 +84,7 @@ fun StressTestScreen() {
|
||||||
),
|
),
|
||||||
render = RenderConfig(
|
render = RenderConfig(
|
||||||
theme = chartTheme,
|
theme = chartTheme,
|
||||||
lodStrategy = MinMaxLodStrategy(),
|
lodStrategy = MinMaxLttbLodStrategy(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -96,6 +99,10 @@ fun StressTestScreen() {
|
||||||
SignalConfig(color = STRESS_COLORS[i], strokeWidth = 1.5f),
|
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 tickIntervalMs = (1_000L / STRESS_SAMPLE_HZ).coerceAtLeast(1L)
|
||||||
val startMs = System.currentTimeMillis()
|
val startMs = System.currentTimeMillis()
|
||||||
var n = 0L
|
var n = 0L
|
||||||
|
|
@ -115,6 +122,7 @@ fun StressTestScreen() {
|
||||||
delay(tickIntervalMs)
|
delay(tickIntervalMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var pushPerSec by remember { mutableIntStateOf(0) }
|
var pushPerSec by remember { mutableIntStateOf(0) }
|
||||||
var totalPushes by remember { mutableLongStateOf(0L) }
|
var totalPushes by remember { mutableLongStateOf(0L) }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue