Skip to content

Commit

Permalink
Application startup duration measurement (#1151)
Browse files Browse the repository at this point in the history
* Clean unused code

Signed-off-by: Tomas Chladek <[email protected]>

* Application startup duration measurement

Signed-off-by: Tomas Chladek <[email protected]>

---------

Signed-off-by: Tomas Chladek <[email protected]>
  • Loading branch information
TomasChladekSL authored Jan 9, 2025
1 parent 0b7cf4b commit c16aa10
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 5 deletions.
1 change: 1 addition & 0 deletions agent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ dependencies {
api(project(":integration:crash"))
api(project(":integration:anr"))
api(project(":integration:networkrequest"))
api(project(":integration:startup"))
}

2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ dependencies {
//TODO: Below dependency can be removed once we uncomment the plugin id.
implementation(project(":instrumentation:runtime:networkrequest:library"))

implementation(Dependencies.Android.SessionReplay.logger)

implementation(Dependencies.Android.appcompat)
implementation(Dependencies.Android.constraintLayout)
implementation(Dependencies.Android.activityKtx)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/smartlook/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package com.smartlook.app

import android.app.Application
import com.cisco.android.common.logger.Logger
import com.cisco.android.common.logger.consumers.AndroidLogConsumer
import com.cisco.android.rum.integration.agent.api.AgentConfiguration
import com.cisco.android.rum.integration.agent.api.CiscoRUMAgent
import java.net.URL
Expand All @@ -25,6 +27,9 @@ class App : Application() {
override fun onCreate() {
super.onCreate()

if (BuildConfig.DEBUG)
Logger.consumers += AndroidLogConsumer()

// TODO: Reenable with the bridge support
// BridgeManager.bridgeInterfaces += TomasBridgeInterface()

Expand Down
28 changes: 28 additions & 0 deletions instrumentation/runtime/startup/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<ConfigAndroidLibrary>()
apply<ConfigPublish>()

ext {
set(artifactIdProperty, "$artifactPrefix$instrumentationPrefix${project.name}")
set(versionProperty, Configurations.sdkVersionName)
}

android {
namespace = "com.cisco.android.rum.startup"
}

dependencies {
api(project(":common:utils"))
}
4 changes: 4 additions & 0 deletions instrumentation/runtime/startup/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.3.1)" variant="all" version="7.3.1">

</issues>
Empty file.
10 changes: 10 additions & 0 deletions instrumentation/runtime/startup/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<provider
android:name=".StartupInstaller"
android:authorities="${applicationId}.startup-installer"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -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<Listener> = 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)
}
}
Original file line number Diff line number Diff line change
@@ -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<out String>?, selection: String?, selectionArgs: Array<out String>?, 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<out String>?): Int = 0

override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0

private companion object {

init {
ApplicationStartupTimekeeper.onInit()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.cisco.android.rum.integration.agent.internal

import android.content.Context
import com.cisco.android.common.logger.Logger
import com.cisco.android.common.logger.consumers.AndroidLogConsumer
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.session.SessionManager
Expand Down Expand Up @@ -46,8 +45,6 @@ class AgentIntegration private constructor(
val listeners: MutableSet<Listener> = HashSet()

init {
Logger.consumers += AndroidLogConsumer()

registerModule(MODULE_NAME)

val storage = Storage.attach(context)
Expand Down
30 changes: 30 additions & 0 deletions integration/startup/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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<ConfigAndroidLibrary>()
apply<ConfigPublish>()

ext {
set(artifactIdProperty, "$artifactPrefix$integrationPrefix${project.name}")
set(versionProperty, Configurations.sdkVersionName)
}

android {
namespace = "com.cisco.android.rum.integration.startup"
}

dependencies {
implementation(project(":integration:agent:internal"))
implementation(project(":instrumentation:runtime:startup"))

implementation(Dependencies.Android.SessionReplay.logger)
}
Empty file.
4 changes: 4 additions & 0 deletions integration/startup/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.3.1)" variant="all" version="7.3.1">

</issues>
Empty file.
10 changes: 10 additions & 0 deletions integration/startup/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<provider
android:name=".StartupInstaller"
android:authorities="${applicationId}.startup-integration-installer"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
Loading

0 comments on commit c16aa10

Please sign in to comment.