From 3871dee3b3b9eae2b11415d9e9f35cacfc151998 Mon Sep 17 00:00:00 2001 From: Tomas Chladek Date: Thu, 19 Dec 2024 14:22:48 +0100 Subject: [PATCH] Application startup duration measurement Signed-off-by: Tomas Chladek --- agent/build.gradle.kts | 1 + app/build.gradle.kts | 1 + app/src/main/java/com/smartlook/app/App.kt | 4 + .../runtime/startup/build.gradle.kts | 28 ++++ .../runtime/startup/proguard-rules.pro | 0 .../startup/src/main/AndroidManifest.xml | 10 ++ .../startup/ApplicationStartupTimekeeper.kt | 149 ++++++++++++++++++ .../android/rum/startup/StartupInstaller.kt | 48 ++++++ integration/startup/build.gradle.kts | 29 ++++ integration/startup/consumer-rules.pro | 0 integration/startup/proguard-rules.pro | 0 .../startup/src/main/AndroidManifest.xml | 10 ++ .../startup/StartupConfigurator.kt | 86 ++++++++++ .../integration/startup/StartupInstaller.kt | 40 +++++ .../startup/StartupModuleConfiguration.kt | 21 +++ settings.gradle | 6 +- 16 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 instrumentation/runtime/startup/build.gradle.kts create mode 100644 instrumentation/runtime/startup/proguard-rules.pro create mode 100644 instrumentation/runtime/startup/src/main/AndroidManifest.xml create mode 100644 instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/ApplicationStartupTimekeeper.kt create mode 100644 instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/StartupInstaller.kt create mode 100644 integration/startup/build.gradle.kts create mode 100644 integration/startup/consumer-rules.pro create mode 100644 integration/startup/proguard-rules.pro create mode 100644 integration/startup/src/main/AndroidManifest.xml create mode 100644 integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupConfigurator.kt create mode 100644 integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupInstaller.kt create mode 100644 integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupModuleConfiguration.kt diff --git a/agent/build.gradle.kts b/agent/build.gradle.kts index 634ff667..aa7b409c 100644 --- a/agent/build.gradle.kts +++ b/agent/build.gradle.kts @@ -25,5 +25,6 @@ dependencies { api(project(":integration:crash")) api(project(":integration:anr")) api(project(":integration:networkrequest")) + api(project(":integration:startup")) } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4197432e..abe89be5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,7 @@ dependencies { // TODO: this is here just so we do not have duplicate logic, it is not publicly available //implementation("com.cisco.android:rum-common-utils:24.4.10-2246") + implementation(project(":common:logger")) implementation(project(":common:utils")) implementation(project(":agent")) //TODO: Below dependency can be removed once we uncomment the plugin id. diff --git a/app/src/main/java/com/smartlook/app/App.kt b/app/src/main/java/com/smartlook/app/App.kt index 5407c28c..1c285ae5 100644 --- a/app/src/main/java/com/smartlook/app/App.kt +++ b/app/src/main/java/com/smartlook/app/App.kt @@ -19,6 +19,8 @@ package com.smartlook.app import android.app.Application import com.cisco.android.rum.integration.agent.api.AgentConfiguration import com.cisco.android.rum.integration.agent.api.CiscoRUMAgent +import com.smartlook.sdk.common.logger.Logger +import com.smartlook.sdk.log.LogAspect import java.net.URL class App : Application() { @@ -28,6 +30,8 @@ class App : Application() { // TODO: Reenable with the bridge support // BridgeManager.bridgeInterfaces += TomasBridgeInterface() + Logger.allowedLogAspects = LogAspect.ALL + val agentConfig = AgentConfiguration( url = URL("https://alameda-eum-qe.saas.appd-test.com"), appName = "smartlook-android", diff --git a/instrumentation/runtime/startup/build.gradle.kts b/instrumentation/runtime/startup/build.gradle.kts new file mode 100644 index 00000000..73a6471a --- /dev/null +++ b/instrumentation/runtime/startup/build.gradle.kts @@ -0,0 +1,28 @@ +import plugins.ConfigAndroidLibrary +import plugins.ConfigPublish +import utils.artifactIdProperty +import utils.artifactPrefix +import utils.instrumentationPrefix +import utils.versionProperty + +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-parcelize") +} + +apply() +apply() + +ext { + set(artifactIdProperty, "$artifactPrefix$instrumentationPrefix${project.name}") + set(versionProperty, Configurations.sdkVersionName) +} + +android { + namespace = "com.cisco.android.rum.startup" +} + +dependencies { + api(project(":common:utils")) +} diff --git a/instrumentation/runtime/startup/proguard-rules.pro b/instrumentation/runtime/startup/proguard-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/instrumentation/runtime/startup/src/main/AndroidManifest.xml b/instrumentation/runtime/startup/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a030d4a3 --- /dev/null +++ b/instrumentation/runtime/startup/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/ApplicationStartupTimekeeper.kt b/instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/ApplicationStartupTimekeeper.kt new file mode 100644 index 00000000..3d128875 --- /dev/null +++ b/instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/ApplicationStartupTimekeeper.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cisco.android.rum.startup + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import com.smartlook.sdk.common.utils.adapters.ActivityLifecycleCallbacksAdapter +import com.smartlook.sdk.common.utils.extensions.forEachFast + +object ApplicationStartupTimekeeper { + + private val handler = Handler(Looper.getMainLooper()) + + private var firstTimestamp = 0L + private var isColdStartCompleted = false + + var isEnabled = true + + val listeners: MutableList = arrayListOf() + + internal fun onInit() { + firstTimestamp = System.currentTimeMillis() + } + + internal fun onCreate(application: Application) { + handler.twoConsecutivePosts { + isColdStartCompleted = true + + if (isEnabled) { + val duration = System.currentTimeMillis() - firstTimestamp + listeners.forEachFast { it.onColdStarted(duration) } + } + } + + application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks) + } + + private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter { + + private var createdActivityCount = 0 + private var startedActivityCount = 0 + private var resumedActivityCount = 0 + + private var firstActivityCreateTimestamp = 0L + private var isWarmStartPending = false + + private var firstActivityStartTimestamp = 0L + private var isHotStartPending = false + + override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) { + createdActivityCount++ + + if (isColdStartCompleted && createdActivityCount == 1) { + firstActivityCreateTimestamp = System.currentTimeMillis() + isWarmStartPending = true + } + } + + override fun onActivityPreStarted(activity: Activity) { + startedActivityCount++ + + if (isColdStartCompleted && !isWarmStartPending && !isHotStartPending) { + firstActivityStartTimestamp = System.currentTimeMillis() + isHotStartPending = true + } + } + + override fun onActivityResumed(activity: Activity) { + resumedActivityCount++ + + if (resumedActivityCount == 1 && (isHotStartPending || isWarmStartPending)) + handler.twoConsecutivePosts { + if (isHotStartPending) { + if (isEnabled) { + val duration = System.currentTimeMillis() - firstActivityStartTimestamp + listeners.forEachFast { it.onHotStarted(duration) } + } + + isHotStartPending = false + } + + if (isWarmStartPending) { + if (isEnabled) { + val duration = System.currentTimeMillis() - firstActivityCreateTimestamp + listeners.forEachFast { it.onWarmStarted(duration) } + } + + isWarmStartPending = false + } + } + } + + override fun onActivityPaused(activity: Activity) { + resumedActivityCount-- + } + + override fun onActivityStopped(activity: Activity) { + startedActivityCount-- + } + + override fun onActivityDestroyed(activity: Activity) { + createdActivityCount-- + } + } + + private fun Handler.twoConsecutivePosts(action: () -> Unit) { + post { + post(action) + } + } + + interface Listener { + + /** + * The application is launched from a completely inactive state. + * Kill the app > press the application icon. + */ + fun onColdStarted(duration: Long) + + /** + * The application is launched after being recently closed or moved to the background, but still resides in memory. + * Open the app > press back button > press the app icon. + */ + fun onWarmStarted(duration: Long) + + /** + * The application is already running in the background and is brought to the foreground. + * Open the app > press home button > press the app icon. + */ + fun onHotStarted(duration: Long) + } +} diff --git a/instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/StartupInstaller.kt b/instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/StartupInstaller.kt new file mode 100644 index 00000000..653ce99d --- /dev/null +++ b/instrumentation/runtime/startup/src/main/java/com/cisco/android/rum/startup/StartupInstaller.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cisco.android.rum.startup + +import android.app.Application +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri + +internal class StartupInstaller : ContentProvider() { + + override fun onCreate(): Boolean { + ApplicationStartupTimekeeper.onCreate(context as Application) + return true + } + + override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? = null + + override fun getType(uri: Uri): String? = null + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0 + + private companion object { + + init { + ApplicationStartupTimekeeper.onInit() + } + } +} diff --git a/integration/startup/build.gradle.kts b/integration/startup/build.gradle.kts new file mode 100644 index 00000000..8746d644 --- /dev/null +++ b/integration/startup/build.gradle.kts @@ -0,0 +1,29 @@ +import plugins.ConfigAndroidLibrary +import plugins.ConfigPublish +import utils.artifactIdProperty +import utils.artifactPrefix +import utils.integrationPrefix +import utils.versionProperty + +plugins { + id("com.android.library") + id("kotlin-android") +} + +apply() +apply() + +ext { + set(artifactIdProperty, "$artifactPrefix$integrationPrefix${project.name}") + set(versionProperty, Configurations.sdkVersionName) +} + +android { + namespace = "com.cisco.android.rum.integration.startup" +} + +dependencies { + implementation(project(":common:logger")) + implementation(project(":integration:agent:internal")) + implementation(project(":instrumentation:runtime:startup")) +} diff --git a/integration/startup/consumer-rules.pro b/integration/startup/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/integration/startup/proguard-rules.pro b/integration/startup/proguard-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/integration/startup/src/main/AndroidManifest.xml b/integration/startup/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a09c218f --- /dev/null +++ b/integration/startup/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupConfigurator.kt b/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupConfigurator.kt new file mode 100644 index 00000000..647c52d4 --- /dev/null +++ b/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupConfigurator.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cisco.android.rum.integration.startup + +import android.content.Context +import com.cisco.android.rum.integration.agent.internal.AgentIntegration +import com.cisco.android.rum.integration.agent.internal.config.ModuleConfigurationManager +import com.cisco.android.rum.integration.agent.internal.config.RemoteModuleConfiguration +import com.cisco.android.rum.integration.agent.internal.extension.find +import com.cisco.android.rum.startup.ApplicationStartupTimekeeper +import com.smartlook.sdk.common.logger.Logger +import com.smartlook.sdk.common.utils.extensions.optBooleanNull +import com.smartlook.sdk.log.LogAspect + +internal object StartupConfigurator { + + private const val TAG = "StartupConfigurator" + private const val MODULE_NAME = "startup" + + init { + AgentIntegration.registerModule(MODULE_NAME) + } + + fun attach(context: Context) { + AgentIntegration.obtainInstance(context).listeners += installationListener + + ApplicationStartupTimekeeper.listeners += applicationStartupTimekeeperListener + } + + private val applicationStartupTimekeeperListener = object : ApplicationStartupTimekeeper.Listener { + override fun onColdStarted(duration: Long) { + Logger.d(LogAspect.SDK_METHODS, TAG) { "onColdStarted(duration: $duration ms)" } + // TODO send data + } + + override fun onWarmStarted(duration: Long) { + Logger.d(LogAspect.SDK_METHODS, TAG) { "onWarmStarted(duration: $duration ms)" } + // TODO send data + } + + override fun onHotStarted(duration: Long) { + Logger.d(LogAspect.SDK_METHODS, TAG) { "onHotStarted(duration: $duration ms)" } + // TODO send data + } + } + + private val configManagerListener = object : ModuleConfigurationManager.Listener { + override fun onRemoteModuleConfigurationsChanged(manager: ModuleConfigurationManager, remoteConfigurations: List) { + Logger.privateD(LogAspect.SDK_METHODS, TAG, { "onRemoteModuleConfigurationsChanged(remoteConfigurations: $remoteConfigurations)" }) + setModuleConfiguration(remoteConfigurations) + } + } + + private fun setModuleConfiguration(remoteConfigurations: List) { + Logger.privateD(LogAspect.SDK_METHODS, TAG, { "setModuleConfiguration(remoteConfigurations: $remoteConfigurations)" }) + + val remoteConfig = remoteConfigurations.find(MODULE_NAME)?.config + + ApplicationStartupTimekeeper.isEnabled = remoteConfig?.optBooleanNull("enabled") ?: true + } + + private val installationListener = object : AgentIntegration.Listener { + override fun onInstall(context: Context) { + Logger.privateD(LogAspect.SDK_METHODS, TAG, { "onInstall()" }) + + val integration = AgentIntegration.obtainInstance(context) + integration.moduleConfigurationManager.listeners += configManagerListener + + setModuleConfiguration(integration.moduleConfigurationManager.remoteConfigurations) + } + } +} diff --git a/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupInstaller.kt b/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupInstaller.kt new file mode 100644 index 00000000..845637d3 --- /dev/null +++ b/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupInstaller.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cisco.android.rum.integration.startup + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri + +internal class StartupInstaller : ContentProvider() { + + override fun onCreate(): Boolean { + StartupConfigurator.attach(context!!) + return true + } + + override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? = null + + override fun getType(uri: Uri): String? = null + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0 +} diff --git a/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupModuleConfiguration.kt b/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupModuleConfiguration.kt new file mode 100644 index 00000000..51991f35 --- /dev/null +++ b/integration/startup/src/main/java/com/cisco/android/rum/integration/startup/StartupModuleConfiguration.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cisco.android.rum.integration.startup + +import com.cisco.android.rum.integration.agent.module.ModuleConfiguration + +class StartupModuleConfiguration : ModuleConfiguration diff --git a/settings.gradle b/settings.gradle index eec6e0be..5ea7d29b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,12 +10,14 @@ include ':common:otel:api', // Instrumentation modules include ':instrumentation:runtime:crash', - ':instrumentation:runtime:anr' + ':instrumentation:runtime:anr', + ':instrumentation:runtime:startup' // Integration include ':integration:agent:api', ':integration:agent:module', - ':integration:agent:internal' + ':integration:agent:internal', + ':integration:startup' // Integration tests // TODO disable it for now because of different artifactory include ':integration-run'