Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application startup duration measurement #1134

Open
wants to merge 1 commit into
base: feature/next-gen
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"))
}

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/smartlook/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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",
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"))
}
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()
}
}
}
29 changes: 29 additions & 0 deletions integration/startup/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<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(":common:logger"))
implementation(project(":integration:agent:internal"))
implementation(project(":instrumentation:runtime:startup"))
}
Empty file.
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
Loading