From f02448d26562e63d561c1b3b088f79af409bb2bc Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Thu, 9 Jul 2026 12:38:17 +0200
Subject: [PATCH 1/9] test: scaffold perftester app module
---
local.properties.example | 7 +++
settings.gradle.kts | 1 +
test-apps/perftester/.gitignore | 1 +
test-apps/perftester/build.gradle.kts | 57 +++++++++++++++++++
.../perftester/src/main/AndroidManifest.xml | 23 ++++++++
.../com/revenuecat/perftester/MainActivity.kt | 18 ++++++
.../src/main/res/values/strings.xml | 4 ++
.../perftester/src/main/res/values/themes.xml | 5 ++
8 files changed, 116 insertions(+)
create mode 100644 test-apps/perftester/.gitignore
create mode 100644 test-apps/perftester/build.gradle.kts
create mode 100644 test-apps/perftester/src/main/AndroidManifest.xml
create mode 100644 test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
create mode 100644 test-apps/perftester/src/main/res/values/strings.xml
create mode 100644 test-apps/perftester/src/main/res/values/themes.xml
diff --git a/local.properties.example b/local.properties.example
index 8f5967d9ee..6e9dbb8d6f 100644
--- a/local.properties.example
+++ b/local.properties.example
@@ -74,3 +74,10 @@
## Debug flags
## ──────────────────────────────────────────────
#ENABLE_EXTRA_REQUEST_LOGGING=true
+
+## ──────────────────────────────────────────────
+## Perf tester app (test-apps:perftester)
+## Test Store API key recommended: the SDK selects SimulatedStoreBillingWrapper
+## from the key type, so products resolve without any Play setup.
+## ──────────────────────────────────────────────
+#PERF_TESTER_API_KEY=your_test_store_key
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 78048168ed..d19ac1895f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -74,4 +74,5 @@ include(":detekt-rules")
include(":dokka-hide-internal")
include(":baselineprofile")
include(":test-apps:e2etests")
+include(":test-apps:perftester")
include(":examples:rcttester")
diff --git a/test-apps/perftester/.gitignore b/test-apps/perftester/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/test-apps/perftester/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/test-apps/perftester/build.gradle.kts b/test-apps/perftester/build.gradle.kts
new file mode 100644
index 0000000000..d71037ad80
--- /dev/null
+++ b/test-apps/perftester/build.gradle.kts
@@ -0,0 +1,57 @@
+import java.util.Properties
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.compose.compiler)
+}
+
+val localProperties = Properties().apply {
+ val localPropsFile = rootProject.file("local.properties")
+ if (localPropsFile.exists()) {
+ localPropsFile.inputStream().use { load(it) }
+ }
+}
+
+android {
+ namespace = "com.revenuecat.perftester"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.revenuecat.perftester"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ missingDimensionStrategy("apis", "defaults")
+
+ buildConfigField(
+ "String",
+ "PERF_TESTER_API_KEY",
+ "\"${localProperties.getProperty("PERF_TESTER_API_KEY", "")}\"",
+ )
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation(project(":purchases"))
+
+ implementation(libs.androidx.core)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.activity.compose)
+ implementation(platform(libs.compose.bom))
+ implementation(libs.compose.ui)
+ implementation(libs.compose.material3)
+}
diff --git a/test-apps/perftester/src/main/AndroidManifest.xml b/test-apps/perftester/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..7f02af6efb
--- /dev/null
+++ b/test-apps/perftester/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
new file mode 100644
index 0000000000..26b1e7ebf1
--- /dev/null
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
@@ -0,0 +1,18 @@
+package com.revenuecat.perftester
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme {
+ Text("perftester")
+ }
+ }
+ }
+}
diff --git a/test-apps/perftester/src/main/res/values/strings.xml b/test-apps/perftester/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..467db11f15
--- /dev/null
+++ b/test-apps/perftester/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ RC Perf Tester
+
diff --git a/test-apps/perftester/src/main/res/values/themes.xml b/test-apps/perftester/src/main/res/values/themes.xml
new file mode 100644
index 0000000000..1e7d1438ed
--- /dev/null
+++ b/test-apps/perftester/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
From 5b9d9c41aef7e7a36aa4c45a7991c073163f7466 Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Thu, 9 Jul 2026 12:50:23 +0200
Subject: [PATCH 2/9] test: add perftester result record and JSONL writer
---
test-apps/perftester/build.gradle.kts | 4 ++
.../com/revenuecat/perftester/PerfResult.kt | 38 ++++++++++
.../revenuecat/perftester/ResultsWriter.kt | 10 +++
.../revenuecat/perftester/PerfResultTest.kt | 71 +++++++++++++++++++
4 files changed, 123 insertions(+)
create mode 100644 test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt
create mode 100644 test-apps/perftester/src/main/java/com/revenuecat/perftester/ResultsWriter.kt
create mode 100644 test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt
diff --git a/test-apps/perftester/build.gradle.kts b/test-apps/perftester/build.gradle.kts
index d71037ad80..49bae21ff3 100644
--- a/test-apps/perftester/build.gradle.kts
+++ b/test-apps/perftester/build.gradle.kts
@@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.kotlin.serialization)
}
val localProperties = Properties().apply {
@@ -54,4 +55,7 @@ dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.material3)
+ implementation(libs.kotlinx.serialization.json)
+
+ testImplementation(libs.androidx.junit)
}
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt
new file mode 100644
index 0000000000..3cc10cd9ed
--- /dev/null
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt
@@ -0,0 +1,38 @@
+package com.revenuecat.perftester
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+@Serializable
+internal data class PerfResult(
+ @SerialName("schema_version") val schemaVersion: Int = 1,
+ @SerialName("source") val source: String = "perftester-app",
+ @SerialName("timestamp_epoch_ms") val timestampEpochMs: Long,
+ @SerialName("sdk_version") val sdkVersion: String,
+ @SerialName("device_model") val deviceModel: String,
+ @SerialName("android_api_level") val androidApiLevel: Int,
+ @SerialName("workflows_enabled") val workflowsEnabled: Boolean,
+ @SerialName("press_index_in_process") val pressIndexInProcess: Int,
+ @SerialName("process_start_to_press_ms") val processStartToPressMs: Long,
+ @SerialName("had_offerings_cache") val hadOfferingsCache: Boolean,
+ @SerialName("had_remote_config_cache") val hadRemoteConfigCache: Boolean,
+ @SerialName("had_blobs_cache") val hadBlobsCache: Boolean,
+ @SerialName("configure_to_offerings_ms") val configureToOfferingsMs: Double?,
+ @SerialName("get_offerings_ms") val getOfferingsMs: Double?,
+ @SerialName("ui_config_ms") val uiConfigMs: Double?,
+ @SerialName("ui_config_is_default") val uiConfigIsDefault: Boolean?,
+ @SerialName("workflow_resolution_ms") val workflowResolutionMs: Double?,
+ @SerialName("workflow_body_ms") val workflowBodyMs: Double?,
+ @SerialName("workflow_resolved") val workflowResolved: Boolean?,
+ @SerialName("success") val success: Boolean,
+ @SerialName("error_message") val errorMessage: String?,
+ @SerialName("offerings_count") val offeringsCount: Int?,
+ @SerialName("current_offering_id") val currentOfferingId: String?,
+) {
+ fun toJsonLine(): String = json.encodeToString(serializer(), this)
+
+ private companion object {
+ private val json = Json { encodeDefaults = true }
+ }
+}
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/ResultsWriter.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/ResultsWriter.kt
new file mode 100644
index 0000000000..7ffeaef608
--- /dev/null
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/ResultsWriter.kt
@@ -0,0 +1,10 @@
+package com.revenuecat.perftester
+
+import java.io.File
+
+internal class ResultsWriter(private val resultsFile: File) {
+ fun append(result: PerfResult) {
+ resultsFile.parentFile?.mkdirs()
+ resultsFile.appendText(result.toJsonLine() + "\n")
+ }
+}
diff --git a/test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt b/test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt
new file mode 100644
index 0000000000..fc84fb4437
--- /dev/null
+++ b/test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt
@@ -0,0 +1,71 @@
+package com.revenuecat.perftester
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.File
+
+class PerfResultTest {
+
+ private val result = PerfResult(
+ timestampEpochMs = 1720000000000L,
+ sdkVersion = "10.14.0-SNAPSHOT",
+ deviceModel = "Google sdk_gphone64_arm64",
+ androidApiLevel = 34,
+ workflowsEnabled = true,
+ pressIndexInProcess = 1,
+ processStartToPressMs = 1234,
+ hadOfferingsCache = false,
+ hadRemoteConfigCache = false,
+ hadBlobsCache = false,
+ configureToOfferingsMs = 321.5,
+ getOfferingsMs = 300.25,
+ uiConfigMs = 42.0,
+ uiConfigIsDefault = false,
+ workflowResolutionMs = 1.5,
+ workflowBodyMs = 55.0,
+ workflowResolved = true,
+ success = true,
+ errorMessage = null,
+ offeringsCount = 3,
+ currentOfferingId = "default",
+ )
+
+ @Test
+ fun `json line contains exactly the schema field names`() {
+ val parsed = Json.parseToJsonElement(result.toJsonLine()).jsonObject
+ val expectedKeys = setOf(
+ "schema_version", "source", "timestamp_epoch_ms", "sdk_version", "device_model",
+ "android_api_level", "workflows_enabled", "press_index_in_process",
+ "process_start_to_press_ms", "had_offerings_cache", "had_remote_config_cache",
+ "had_blobs_cache", "configure_to_offerings_ms", "get_offerings_ms", "ui_config_ms",
+ "ui_config_is_default", "workflow_resolution_ms", "workflow_body_ms",
+ "workflow_resolved", "success", "error_message", "offerings_count",
+ "current_offering_id",
+ )
+ assertEquals(expectedKeys, parsed.keys)
+ assertEquals("1", parsed["schema_version"]!!.jsonPrimitive.content)
+ assertEquals("perftester-app", parsed["source"]!!.jsonPrimitive.content)
+ }
+
+ @Test
+ fun `nulls are written explicitly so every line has every field`() {
+ val parsed = Json.parseToJsonElement(result.copy(errorMessage = null).toJsonLine()).jsonObject
+ assertTrue(parsed.containsKey("error_message"))
+ }
+
+ @Test
+ fun `writer appends one line per result`() {
+ val file = File.createTempFile("results", ".jsonl")
+ val writer = ResultsWriter(file)
+ writer.append(result)
+ writer.append(result.copy(pressIndexInProcess = 2))
+ val lines = file.readLines()
+ assertEquals(2, lines.size)
+ assertTrue(lines[1].contains("\"press_index_in_process\":2"))
+ file.delete()
+ }
+}
From e65725cb3a18ed40eebf01a8aa2678dd356b8449 Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Thu, 9 Jul 2026 12:59:12 +0200
Subject: [PATCH 3/9] test: add perftester SDK cache inspector and cleaner
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../com/revenuecat/perftester/SdkCaches.kt | 39 +++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 test-apps/perftester/src/main/java/com/revenuecat/perftester/SdkCaches.kt
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/SdkCaches.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/SdkCaches.kt
new file mode 100644
index 0000000000..129c687cf8
--- /dev/null
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/SdkCaches.kt
@@ -0,0 +1,39 @@
+package com.revenuecat.perftester
+
+import android.content.Context
+import java.io.File
+
+/**
+ * Inspects and clears the SDK's on-device caches. Locations mirror the SDK:
+ * - Prefs file: RevenueCatBackupAgent.REVENUECAT_PREFS_FILE_NAME
+ * - Offerings key: DeviceCache offeringsResponseCacheKey ("com.revenuecat.purchases..offeringsResponse")
+ * - Remote config + blobs: RemoteConfigDiskCache/RemoteConfigBlobStore under noBackupFilesDir/RevenueCat
+ */
+internal object SdkCaches {
+
+ private const val RC_PREFS_FILE = "com_revenuecat_purchases_preferences"
+
+ fun hadOfferingsCache(context: Context, apiKey: String): Boolean =
+ context.getSharedPreferences(RC_PREFS_FILE, Context.MODE_PRIVATE)
+ .contains("com.revenuecat.purchases.$apiKey.offeringsResponse")
+
+ fun hadRemoteConfigCache(context: Context): Boolean =
+ File(File(context.noBackupFilesDir, "RevenueCat"), "remote_config")
+ .listFiles()?.isNotEmpty() == true
+
+ fun hadBlobsCache(context: Context): Boolean =
+ File(File(context.noBackupFilesDir, "RevenueCat"), "blobs")
+ .listFiles()?.isNotEmpty() == true
+
+ fun clearAll(context: Context) {
+ clearPrefs(context, RC_PREFS_FILE)
+ clearPrefs(context, "${context.packageName}_preferences_etags")
+ clearPrefs(context, "com_revenuecat_purchases_${context.packageName}_preferences_diagnostics")
+ File(context.filesDir, "RevenueCat").deleteRecursively()
+ File(context.noBackupFilesDir, "RevenueCat").deleteRecursively()
+ }
+
+ private fun clearPrefs(context: Context, name: String) {
+ context.getSharedPreferences(name, Context.MODE_PRIVATE).edit().clear().commit()
+ }
+}
From aab81004b9e5b78540b9587d6aa762ad7f63cee1 Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:09:26 +0200
Subject: [PATCH 4/9] test: add perftester measured configure+getOfferings
cycle
---
.../revenuecat/perftester/PerfCycleRunner.kt | 155 ++++++++++++++++++
1 file changed, 155 insertions(+)
create mode 100644 test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
new file mode 100644
index 0000000000..29921de60d
--- /dev/null
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
@@ -0,0 +1,155 @@
+package com.revenuecat.perftester
+
+import android.content.Context
+import android.os.Build
+import android.os.Process
+import android.os.SystemClock
+import com.revenuecat.purchases.DangerousSettings
+import com.revenuecat.purchases.InternalRevenueCatAPI
+import com.revenuecat.purchases.Offerings
+import com.revenuecat.purchases.Purchases
+import com.revenuecat.purchases.PurchasesConfiguration
+import com.revenuecat.purchases.UiConfig
+import com.revenuecat.purchases.getOfferingsWith
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
+import java.io.File
+import kotlin.coroutines.resume
+
+@OptIn(InternalRevenueCatAPI::class)
+internal class PerfCycleRunner(
+ private val context: Context,
+ private val apiKey: String,
+) {
+
+ private val resultsFile = File(context.getExternalFilesDir(null), "results.jsonl")
+ private val writer = ResultsWriter(resultsFile)
+
+ val resultsFilePath: String get() = resultsFile.absolutePath
+
+ @Suppress("LongMethod")
+ suspend fun runCycle(workflowsEnabled: Boolean, pressIndex: Int): PerfResult {
+ if (Purchases.isConfigured) {
+ Purchases.sharedInstance.close()
+ }
+ val hadOfferings = SdkCaches.hadOfferingsCache(context, apiKey)
+ val hadRemoteConfig = SdkCaches.hadRemoteConfigCache(context)
+ val hadBlobs = SdkCaches.hadBlobsCache(context)
+ val processStartToPressMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime()
+
+ val dangerousSettings = if (workflowsEnabled) {
+ DangerousSettings.forWorkflows()
+ } else {
+ DangerousSettings()
+ }
+ val configuration = PurchasesConfiguration.Builder(context, apiKey)
+ .dangerousSettings(dangerousSettings)
+ .build()
+
+ val t0 = SystemClock.elapsedRealtimeNanos()
+ Purchases.configure(configuration)
+ val t1 = SystemClock.elapsedRealtimeNanos()
+
+ var t2 = 0L
+ var errorMessage: String? = null
+ var offerings: Offerings? = null
+ val timedOut = runCatching {
+ withTimeout(CALL_TIMEOUT_MILLIS) {
+ suspendCancellableCoroutine { continuation ->
+ Purchases.sharedInstance.getOfferingsWith(
+ onError = { error ->
+ t2 = SystemClock.elapsedRealtimeNanos()
+ errorMessage = error.toString()
+ continuation.resume(Unit)
+ },
+ onSuccess = { received ->
+ t2 = SystemClock.elapsedRealtimeNanos()
+ offerings = received
+ continuation.resume(Unit)
+ },
+ )
+ }
+ }
+ }.isFailure
+ if (timedOut && t2 == 0L) {
+ errorMessage = "getOfferings did not complete within ${CALL_TIMEOUT_MILLIS / MILLIS_PER_SECOND}s"
+ }
+
+ val configureToOfferingsMs = if (t2 != 0L) (t2 - t0).nanosToMillis() else null
+ val getOfferingsMs = if (t2 != 0L) (t2 - t1).nanosToMillis() else null
+ val success = errorMessage == null && offerings != null
+
+ var uiConfigMs: Double? = null
+ var uiConfigIsDefault: Boolean? = null
+ var workflowResolutionMs: Double? = null
+ var workflowBodyMs: Double? = null
+ var workflowResolved: Boolean? = null
+ if (workflowsEnabled && success) {
+ runCatching {
+ withTimeout(CALL_TIMEOUT_MILLIS) {
+ val started = SystemClock.elapsedRealtimeNanos()
+ val uiConfig = Purchases.sharedInstance.awaitGetUiConfig()
+ uiConfigMs = (SystemClock.elapsedRealtimeNanos() - started).nanosToMillis()
+ uiConfigIsDefault = uiConfig == UiConfig()
+ }
+ }
+ val offeringIds = offerings?.let { received ->
+ listOfNotNull(received.current?.identifier) +
+ received.all.keys.filter { it != received.current?.identifier }
+ }.orEmpty()
+ var workflowId: String? = null
+ runCatching {
+ withTimeout(CALL_TIMEOUT_MILLIS) {
+ val started = SystemClock.elapsedRealtimeNanos()
+ workflowId = offeringIds.firstNotNullOfOrNull { offeringId ->
+ Purchases.sharedInstance.workflowIdForOfferingId(offeringId)
+ }
+ workflowResolutionMs = (SystemClock.elapsedRealtimeNanos() - started).nanosToMillis()
+ }
+ }
+ workflowId?.let { id ->
+ workflowResolved = runCatching {
+ withTimeout(CALL_TIMEOUT_MILLIS) {
+ val started = SystemClock.elapsedRealtimeNanos()
+ Purchases.sharedInstance.awaitGetWorkflow(id)
+ workflowBodyMs = (SystemClock.elapsedRealtimeNanos() - started).nanosToMillis()
+ }
+ }.isSuccess
+ }
+ }
+
+ val result = PerfResult(
+ timestampEpochMs = System.currentTimeMillis(),
+ sdkVersion = Purchases.frameworkVersion,
+ deviceModel = "${Build.MANUFACTURER} ${Build.MODEL}",
+ androidApiLevel = Build.VERSION.SDK_INT,
+ workflowsEnabled = workflowsEnabled,
+ pressIndexInProcess = pressIndex,
+ processStartToPressMs = processStartToPressMs,
+ hadOfferingsCache = hadOfferings,
+ hadRemoteConfigCache = hadRemoteConfig,
+ hadBlobsCache = hadBlobs,
+ configureToOfferingsMs = configureToOfferingsMs,
+ getOfferingsMs = getOfferingsMs,
+ uiConfigMs = uiConfigMs,
+ uiConfigIsDefault = uiConfigIsDefault,
+ workflowResolutionMs = workflowResolutionMs,
+ workflowBodyMs = workflowBodyMs,
+ workflowResolved = workflowResolved,
+ success = success,
+ errorMessage = errorMessage,
+ offeringsCount = offerings?.all?.size,
+ currentOfferingId = offerings?.current?.identifier,
+ )
+ writer.append(result)
+ return result
+ }
+
+ private fun Long.nanosToMillis(): Double = this / NANOS_PER_MILLI
+
+ private companion object {
+ private const val CALL_TIMEOUT_MILLIS = 120_000L
+ private const val MILLIS_PER_SECOND = 1_000L
+ private const val NANOS_PER_MILLI = 1_000_000.0
+ }
+}
From 46969bfaf3d1f4ef15502db96e2f077be0402297 Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:28:33 +0200
Subject: [PATCH 5/9] test: add perftester UI with workflows toggle and autorun
---
.../com/revenuecat/perftester/MainActivity.kt | 97 ++++++++++++++++++-
1 file changed, 96 insertions(+), 1 deletion(-)
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
index 26b1e7ebf1..c0c665911b 100644
--- a/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
@@ -3,15 +3,110 @@ package com.revenuecat.perftester
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.lifecycleScope
+import com.revenuecat.purchases.Purchases
+import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
+
+ private var pressIndex = 0
+ private var autorunHandled = false
+
+ @Suppress("LongMethod")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val apiKey = BuildConfig.PERF_TESTER_API_KEY
+ val keyMissing = apiKey.isBlank()
+ val runner = PerfCycleRunner(applicationContext, apiKey)
+
setContent {
+ var workflowsEnabled by remember {
+ mutableStateOf(intent.getStringExtra("workflows")?.toBoolean() ?: false)
+ }
+ var status by remember { mutableStateOf("Results: ${runner.resultsFilePath}") }
+ var running by remember { mutableStateOf(false) }
+
+ val runCycle = {
+ if (!running) {
+ running = true
+ pressIndex += 1
+ status = "Running press #$pressIndex..."
+ lifecycleScope.launch {
+ val result = runner.runCycle(workflowsEnabled, pressIndex)
+ running = false
+ status = buildString {
+ appendLine("press #${result.pressIndexInProcess} workflows=${result.workflowsEnabled}")
+ appendLine("had_offerings_cache=${result.hadOfferingsCache}")
+ appendLine("configure_to_offerings_ms=${result.configureToOfferingsMs}")
+ appendLine("get_offerings_ms=${result.getOfferingsMs}")
+ appendLine("success=${result.success}")
+ result.errorMessage?.let { appendLine("error: $it") }
+ append("file: ${runner.resultsFilePath}")
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ if (!autorunHandled && intent.getStringExtra("autorun")?.toBoolean() == true) {
+ autorunHandled = true
+ runCycle()
+ }
+ }
+
MaterialTheme {
- Text("perftester")
+ Column(
+ modifier = Modifier.fillMaxSize().padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Switch(
+ checked = workflowsEnabled,
+ onCheckedChange = { workflowsEnabled = it },
+ enabled = !running,
+ )
+ Text(if (workflowsEnabled) "workflows" else "baseline")
+ }
+ Button(onClick = runCycle, enabled = !running && !keyMissing) {
+ Text("Configure + getOfferings")
+ }
+ Button(
+ onClick = {
+ if (Purchases.isConfigured) {
+ Purchases.sharedInstance.close()
+ }
+ SdkCaches.clearAll(applicationContext)
+ status = "SDK caches cleared."
+ },
+ enabled = !running,
+ ) {
+ Text("Clear SDK caches")
+ }
+ if (keyMissing) {
+ Text("Set PERF_TESTER_API_KEY in local.properties and rebuild.")
+ }
+ Text(status)
+ }
}
}
}
From 0fad7ea89ef578ee742149417d0aab8282a8b719 Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:40:24 +0200
Subject: [PATCH 6/9] docs: add perftester README
---
test-apps/perftester/README.md | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 test-apps/perftester/README.md
diff --git a/test-apps/perftester/README.md b/test-apps/perftester/README.md
new file mode 100644
index 0000000000..0f395b7b06
--- /dev/null
+++ b/test-apps/perftester/README.md
@@ -0,0 +1,33 @@
+# perftester
+
+Measures `configure()` + `getOfferings()` latency, baseline vs. workflows, from a real app.
+
+## Setup
+
+Set `PERF_TESTER_API_KEY` in `local.properties` (a Test Store key: products then resolve through
+`SimulatedStoreBillingWrapper` with no Play setup) and install:
+
+ ./gradlew :test-apps:perftester:installDebug
+
+## Use
+
+- Toggle picks the arm: baseline or workflows (`DangerousSettings.forWorkflows()`).
+- "Configure + getOfferings" runs one measured cycle: `close()` if configured, record `had_*` cache
+ flags, `configure()`, `getOfferings()`, then (workflows arm) `ui_config` and first workflow body
+ readiness. One JSON line is appended to `results.jsonl` in the app's external files dir per press.
+- "Clear SDK caches" deletes the SDK prefs and cache dirs, so the next press is a cold sample
+ without reinstalling.
+- The first press after install/clear is cold; later presses are warm. The `had_*` flags in the data
+ record which one each sample actually was.
+
+## Scripted runs
+
+ adb shell am force-stop com.revenuecat.perftester
+ adb shell am start -n com.revenuecat.perftester/.MainActivity -e autorun true -e workflows true
+
+Loop that with `sleep` between iterations, then:
+
+ adb pull /sdcard/Android/data/com.revenuecat.perftester/files/results.jsonl .
+
+Each line is one press; aggregate percentiles per arm (`workflows_enabled`) and cache state
+(`had_offerings_cache`, `had_remote_config_cache`).
From cbf086c8e161d6a7b4797fe06b217e6db3e3f5bd Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:59:49 +0200
Subject: [PATCH 7/9] test: guard perftester autorun on missing key and declare
coroutines dep
---
test-apps/perftester/build.gradle.kts | 1 +
.../src/main/java/com/revenuecat/perftester/MainActivity.kt | 2 +-
.../src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt | 3 +++
3 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/test-apps/perftester/build.gradle.kts b/test-apps/perftester/build.gradle.kts
index 49bae21ff3..eb68efd9ce 100644
--- a/test-apps/perftester/build.gradle.kts
+++ b/test-apps/perftester/build.gradle.kts
@@ -56,6 +56,7 @@ dependencies {
implementation(libs.compose.ui)
implementation(libs.compose.material3)
implementation(libs.kotlinx.serialization.json)
+ implementation(libs.coroutines.core)
testImplementation(libs.androidx.junit)
}
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
index c0c665911b..929b4518c5 100644
--- a/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/MainActivity.kt
@@ -65,7 +65,7 @@ class MainActivity : ComponentActivity() {
}
LaunchedEffect(Unit) {
- if (!autorunHandled && intent.getStringExtra("autorun")?.toBoolean() == true) {
+ if (!autorunHandled && !keyMissing && intent.getStringExtra("autorun")?.toBoolean() == true) {
autorunHandled = true
runCycle()
}
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
index 29921de60d..ce2333b307 100644
--- a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
@@ -108,6 +108,9 @@ internal class PerfCycleRunner(
}
}
workflowId?.let { id ->
+ // NOTE: this withTimeout cannot force-cancel awaitGetWorkflow — that SDK API uses
+ // non-cancellable suspendCoroutine, so a hung workflow-body fetch will keep runCycle
+ // waiting past CALL_TIMEOUT_MILLIS. The offerings measurement above is properly bounded.
workflowResolved = runCatching {
withTimeout(CALL_TIMEOUT_MILLIS) {
val started = SystemClock.elapsedRealtimeNanos()
From 2c68499a95b1614d7cfe5f18dd1017e9a56f023c Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:26:57 +0200
Subject: [PATCH 8/9] fix: build perftester against main's required UiConfig
args
#3742 made the UiConfig constructor require app/localizations/variableConfig,
so PerfCycleRunner's `uiConfig == UiConfig()` no longer compiles. Drop the
uiConfigIsDefault diagnostic (keep the ui_config timing) so the app builds
against current main.
Co-Authored-By: Claude Opus 4.8
---
.../main/java/com/revenuecat/perftester/PerfCycleRunner.kt | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
index ce2333b307..487785a80f 100644
--- a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
@@ -9,7 +9,6 @@ import com.revenuecat.purchases.InternalRevenueCatAPI
import com.revenuecat.purchases.Offerings
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration
-import com.revenuecat.purchases.UiConfig
import com.revenuecat.purchases.getOfferingsWith
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
@@ -80,7 +79,7 @@ internal class PerfCycleRunner(
val success = errorMessage == null && offerings != null
var uiConfigMs: Double? = null
- var uiConfigIsDefault: Boolean? = null
+ val uiConfigIsDefault: Boolean? = null
var workflowResolutionMs: Double? = null
var workflowBodyMs: Double? = null
var workflowResolved: Boolean? = null
@@ -88,9 +87,8 @@ internal class PerfCycleRunner(
runCatching {
withTimeout(CALL_TIMEOUT_MILLIS) {
val started = SystemClock.elapsedRealtimeNanos()
- val uiConfig = Purchases.sharedInstance.awaitGetUiConfig()
+ Purchases.sharedInstance.awaitGetUiConfig()
uiConfigMs = (SystemClock.elapsedRealtimeNanos() - started).nanosToMillis()
- uiConfigIsDefault = uiConfig == UiConfig()
}
}
val offeringIds = offerings?.let { received ->
From 1ed047c834c220c43f6f16d731deb60e7fcb6f03 Mon Sep 17 00:00:00 2001
From: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Date: Tue, 14 Jul 2026 19:28:32 +0200
Subject: [PATCH 9/9] test: record post-readiness memory in perftester
---
test-apps/perftester/README.md | 11 ++--
.../revenuecat/perftester/PerfCycleRunner.kt | 20 ++++++++
.../com/revenuecat/perftester/PerfResult.kt | 17 ++++++-
.../perftester/ProcessMemorySnapshot.kt | 50 +++++++++++++++++++
.../revenuecat/perftester/PerfResultTest.kt | 24 ++++++++-
5 files changed, 116 insertions(+), 6 deletions(-)
create mode 100644 test-apps/perftester/src/main/java/com/revenuecat/perftester/ProcessMemorySnapshot.kt
diff --git a/test-apps/perftester/README.md b/test-apps/perftester/README.md
index 0f395b7b06..e0e48578eb 100644
--- a/test-apps/perftester/README.md
+++ b/test-apps/perftester/README.md
@@ -1,6 +1,7 @@
# perftester
-Measures `configure()` + `getOfferings()` latency, baseline vs. workflows, from a real app.
+Measures `configure()` + `getOfferings()` latency and post-readiness memory, baseline vs.
+workflows, from a real app.
## Setup
@@ -14,7 +15,9 @@ Set `PERF_TESTER_API_KEY` in `local.properties` (a Test Store key: products then
- Toggle picks the arm: baseline or workflows (`DangerousSettings.forWorkflows()`).
- "Configure + getOfferings" runs one measured cycle: `close()` if configured, record `had_*` cache
flags, `configure()`, `getOfferings()`, then (workflows arm) `ui_config` and first workflow body
- readiness. One JSON line is appended to `results.jsonl` in the app's external files dir per press.
+ readiness. Memory is sampled immediately before `configure()` and after that readiness work,
+ so the `memory_delta_*` fields measure the memory retained by the cycle after workflows have
+ loaded. One JSON line is appended to `results.jsonl` in the app's external files dir per press.
- "Clear SDK caches" deletes the SDK prefs and cache dirs, so the next press is a cold sample
without reinstalling.
- The first press after install/clear is cold; later presses are warm. The `had_*` flags in the data
@@ -30,4 +33,6 @@ Loop that with `sleep` between iterations, then:
adb pull /sdcard/Android/data/com.revenuecat.perftester/files/results.jsonl .
Each line is one press; aggregate percentiles per arm (`workflows_enabled`) and cache state
-(`had_offerings_cache`, `had_remote_config_cache`).
+(`had_offerings_cache`, `had_remote_config_cache`). RSS fields are in KiB; `rss_anon` is the
+anonymous resident portion and `rss_file` is the file-backed resident portion. This is a
+post-readiness measurement, not a peak-memory measurement.
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
index 487785a80f..9dba61be18 100644
--- a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfCycleRunner.kt
@@ -35,6 +35,7 @@ internal class PerfCycleRunner(
val hadRemoteConfig = SdkCaches.hadRemoteConfigCache(context)
val hadBlobs = SdkCaches.hadBlobsCache(context)
val processStartToPressMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime()
+ val memoryBefore = ProcessMemorySnapshot.capture()
val dangerousSettings = if (workflowsEnabled) {
DangerousSettings.forWorkflows()
@@ -119,6 +120,10 @@ internal class PerfCycleRunner(
}
}
+ // getOfferings plus the workflows readiness calls above are complete here. This is the
+ // retained-state sample, after the workflow data has been parsed and cached.
+ val memoryAfter = ProcessMemorySnapshot.capture()
+
val result = PerfResult(
timestampEpochMs = System.currentTimeMillis(),
sdkVersion = Purchases.frameworkVersion,
@@ -130,6 +135,21 @@ internal class PerfCycleRunner(
hadOfferingsCache = hadOfferings,
hadRemoteConfigCache = hadRemoteConfig,
hadBlobsCache = hadBlobs,
+ memoryBeforeRssKb = memoryBefore.rssKb,
+ memoryAfterRssKb = memoryAfter.rssKb,
+ memoryDeltaRssKb = memoryAfter.rssKb.deltaFrom(memoryBefore.rssKb),
+ memoryBeforeRssAnonKb = memoryBefore.rssAnonKb,
+ memoryAfterRssAnonKb = memoryAfter.rssAnonKb,
+ memoryDeltaRssAnonKb = memoryAfter.rssAnonKb.deltaFrom(memoryBefore.rssAnonKb),
+ memoryBeforeRssFileKb = memoryBefore.rssFileKb,
+ memoryAfterRssFileKb = memoryAfter.rssFileKb,
+ memoryDeltaRssFileKb = memoryAfter.rssFileKb.deltaFrom(memoryBefore.rssFileKb),
+ memoryBeforeJavaHeapKb = memoryBefore.javaHeapKb,
+ memoryAfterJavaHeapKb = memoryAfter.javaHeapKb,
+ memoryDeltaJavaHeapKb = memoryAfter.javaHeapKb.deltaFrom(memoryBefore.javaHeapKb),
+ memoryBeforeNativeHeapKb = memoryBefore.nativeHeapKb,
+ memoryAfterNativeHeapKb = memoryAfter.nativeHeapKb,
+ memoryDeltaNativeHeapKb = memoryAfter.nativeHeapKb.deltaFrom(memoryBefore.nativeHeapKb),
configureToOfferingsMs = configureToOfferingsMs,
getOfferingsMs = getOfferingsMs,
uiConfigMs = uiConfigMs,
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt
index 3cc10cd9ed..163d5adfb2 100644
--- a/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/PerfResult.kt
@@ -6,7 +6,7 @@ import kotlinx.serialization.json.Json
@Serializable
internal data class PerfResult(
- @SerialName("schema_version") val schemaVersion: Int = 1,
+ @SerialName("schema_version") val schemaVersion: Int = 2,
@SerialName("source") val source: String = "perftester-app",
@SerialName("timestamp_epoch_ms") val timestampEpochMs: Long,
@SerialName("sdk_version") val sdkVersion: String,
@@ -18,6 +18,21 @@ internal data class PerfResult(
@SerialName("had_offerings_cache") val hadOfferingsCache: Boolean,
@SerialName("had_remote_config_cache") val hadRemoteConfigCache: Boolean,
@SerialName("had_blobs_cache") val hadBlobsCache: Boolean,
+ @SerialName("memory_before_rss_kb") val memoryBeforeRssKb: Long?,
+ @SerialName("memory_after_rss_kb") val memoryAfterRssKb: Long?,
+ @SerialName("memory_delta_rss_kb") val memoryDeltaRssKb: Long?,
+ @SerialName("memory_before_rss_anon_kb") val memoryBeforeRssAnonKb: Long?,
+ @SerialName("memory_after_rss_anon_kb") val memoryAfterRssAnonKb: Long?,
+ @SerialName("memory_delta_rss_anon_kb") val memoryDeltaRssAnonKb: Long?,
+ @SerialName("memory_before_rss_file_kb") val memoryBeforeRssFileKb: Long?,
+ @SerialName("memory_after_rss_file_kb") val memoryAfterRssFileKb: Long?,
+ @SerialName("memory_delta_rss_file_kb") val memoryDeltaRssFileKb: Long?,
+ @SerialName("memory_before_java_heap_kb") val memoryBeforeJavaHeapKb: Long?,
+ @SerialName("memory_after_java_heap_kb") val memoryAfterJavaHeapKb: Long?,
+ @SerialName("memory_delta_java_heap_kb") val memoryDeltaJavaHeapKb: Long?,
+ @SerialName("memory_before_native_heap_kb") val memoryBeforeNativeHeapKb: Long?,
+ @SerialName("memory_after_native_heap_kb") val memoryAfterNativeHeapKb: Long?,
+ @SerialName("memory_delta_native_heap_kb") val memoryDeltaNativeHeapKb: Long?,
@SerialName("configure_to_offerings_ms") val configureToOfferingsMs: Double?,
@SerialName("get_offerings_ms") val getOfferingsMs: Double?,
@SerialName("ui_config_ms") val uiConfigMs: Double?,
diff --git a/test-apps/perftester/src/main/java/com/revenuecat/perftester/ProcessMemorySnapshot.kt b/test-apps/perftester/src/main/java/com/revenuecat/perftester/ProcessMemorySnapshot.kt
new file mode 100644
index 0000000000..c058939e16
--- /dev/null
+++ b/test-apps/perftester/src/main/java/com/revenuecat/perftester/ProcessMemorySnapshot.kt
@@ -0,0 +1,50 @@
+package com.revenuecat.perftester
+
+import android.os.Debug
+import java.io.File
+
+/** Process memory sampled before configuration and after all requested readiness work completes. */
+internal data class ProcessMemorySnapshot(
+ val rssKb: Long?,
+ val rssAnonKb: Long?,
+ val rssFileKb: Long?,
+ val javaHeapKb: Long?,
+ val nativeHeapKb: Long?,
+) {
+
+ companion object {
+ fun capture(): ProcessMemorySnapshot {
+ val status = runCatching {
+ File("/proc/self/status").useLines { lines ->
+ lines.mapNotNull { line ->
+ val separator = line.indexOf(':')
+ if (separator < 0) {
+ null
+ } else {
+ val key = line.substring(0, separator)
+ val value = line.substring(separator + 1)
+ .trim()
+ .removeSuffix(" kB")
+ .trim()
+ .toLongOrNull()
+ key to value
+ }
+ }.toMap()
+ }
+ }.getOrDefault(emptyMap())
+
+ val memoryInfo = Debug.MemoryInfo()
+ Debug.getMemoryInfo(memoryInfo)
+ return ProcessMemorySnapshot(
+ rssKb = status["VmRSS"],
+ rssAnonKb = status["RssAnon"],
+ rssFileKb = status["RssFile"],
+ javaHeapKb = memoryInfo.getMemoryStat("summary.java-heap")?.toLongOrNull(),
+ nativeHeapKb = memoryInfo.getMemoryStat("summary.native-heap")?.toLongOrNull(),
+ )
+ }
+ }
+}
+
+internal fun Long?.deltaFrom(before: Long?): Long? =
+ if (this != null && before != null) this - before else null
diff --git a/test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt b/test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt
index fc84fb4437..73acfc7803 100644
--- a/test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt
+++ b/test-apps/perftester/src/test/java/com/revenuecat/perftester/PerfResultTest.kt
@@ -21,6 +21,21 @@ class PerfResultTest {
hadOfferingsCache = false,
hadRemoteConfigCache = false,
hadBlobsCache = false,
+ memoryBeforeRssKb = 100,
+ memoryAfterRssKb = 120,
+ memoryDeltaRssKb = 20,
+ memoryBeforeRssAnonKb = 80,
+ memoryAfterRssAnonKb = 95,
+ memoryDeltaRssAnonKb = 15,
+ memoryBeforeRssFileKb = 20,
+ memoryAfterRssFileKb = 25,
+ memoryDeltaRssFileKb = 5,
+ memoryBeforeJavaHeapKb = 50,
+ memoryAfterJavaHeapKb = 60,
+ memoryDeltaJavaHeapKb = 10,
+ memoryBeforeNativeHeapKb = 30,
+ memoryAfterNativeHeapKb = 35,
+ memoryDeltaNativeHeapKb = 5,
configureToOfferingsMs = 321.5,
getOfferingsMs = 300.25,
uiConfigMs = 42.0,
@@ -41,13 +56,18 @@ class PerfResultTest {
"schema_version", "source", "timestamp_epoch_ms", "sdk_version", "device_model",
"android_api_level", "workflows_enabled", "press_index_in_process",
"process_start_to_press_ms", "had_offerings_cache", "had_remote_config_cache",
- "had_blobs_cache", "configure_to_offerings_ms", "get_offerings_ms", "ui_config_ms",
+ "had_blobs_cache", "memory_before_rss_kb", "memory_after_rss_kb", "memory_delta_rss_kb",
+ "memory_before_rss_anon_kb", "memory_after_rss_anon_kb", "memory_delta_rss_anon_kb",
+ "memory_before_rss_file_kb", "memory_after_rss_file_kb", "memory_delta_rss_file_kb",
+ "memory_before_java_heap_kb", "memory_after_java_heap_kb", "memory_delta_java_heap_kb",
+ "memory_before_native_heap_kb", "memory_after_native_heap_kb", "memory_delta_native_heap_kb",
+ "configure_to_offerings_ms", "get_offerings_ms", "ui_config_ms",
"ui_config_is_default", "workflow_resolution_ms", "workflow_body_ms",
"workflow_resolved", "success", "error_message", "offerings_count",
"current_offering_id",
)
assertEquals(expectedKeys, parsed.keys)
- assertEquals("1", parsed["schema_version"]!!.jsonPrimitive.content)
+ assertEquals("2", parsed["schema_version"]!!.jsonPrimitive.content)
assertEquals("perftester-app", parsed["source"]!!.jsonPrimitive.content)
}