Skip to content

Commit

Permalink
Start app start up root span early to detect terminations
Browse files Browse the repository at this point in the history
  • Loading branch information
bidetofevil committed Feb 27, 2025
1 parent 44c1642 commit 2e79bb4
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,17 @@ internal class EmbraceSpanImpl(
) : PersistableEmbraceSpan {

private val startedSpan: AtomicReference<io.opentelemetry.api.trace.Span?> = AtomicReference(null)

@Volatile
private var spanStartTimeMs: Long? = null

@Volatile
private var spanEndTimeMs: Long? = null

@Volatile
private var status = Span.Status.UNSET
private var updatedName: String? = null

private val systemEvents = ConcurrentLinkedQueue<EmbraceSpanEvent>()
private val customEvents = ConcurrentLinkedQueue<EmbraceSpanEvent>()
private val systemAttributes = ConcurrentHashMap<String, String>().apply {
Expand Down Expand Up @@ -230,6 +237,8 @@ internal class EmbraceSpanImpl(
}
}

override fun getStartTimeMs(): Long? = spanStartTimeMs

override fun addAttribute(key: String, value: String): Boolean {
if (customAttributes.size < limits.getMaxCustomAttributeCount() && limits.isAttributeValid(key, value, spanBuilder.internal)) {
synchronized(customAttributes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ interface PersistableEmbraceSpan : EmbraceSpan, ImplicitContextKeyed {
*/
fun setStatus(statusCode: StatusCode, description: String = "")

fun getStartTimeMs(): Long?

override fun storeInContext(context: Context): Context = context.with(embraceSpanContextKey, this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.opentelemetry.sdk.common.Clock
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference

/**
* Records startup traces based on the data that it is provided. It adjusts what it logs based on what data has been provided and
Expand Down Expand Up @@ -96,11 +97,9 @@ internal class AppStartupTraceEmitter(
private var firstFrameRenderedMs: Long? = null

@Volatile
private var sdkInitThreadName: String? = null

@Volatile
private var sdkInitEndedInForeground: Boolean? = null
private var recordColdStart = true

private val appStartupRootSpan = AtomicReference<PersistableEmbraceSpan?>(null)
private val startupRecorded = AtomicBoolean(false)
private val dataCollectionComplete = AtomicBoolean(false)
private val endWithFrameDraw: Boolean = startupHasRenderEvent(versionChecker)
Expand All @@ -114,7 +113,21 @@ internal class AppStartupTraceEmitter(
}

override fun firstActivityInit(timestampMs: Long?) {
firstActivityInitStartMs = timestampMs ?: nowMs()
val activityInitTimeMs = timestampMs ?: nowMs()
firstActivityInitStartMs = activityInitTimeMs

val sdkInitStartMs = startupServiceProvider()?.getSdkInitStartMs()
val sdkInitEndMs = startupServiceProvider()?.getSdkInitEndMs()
if (sdkInitStartMs != null && sdkInitEndMs != null) {
applicationActivityCreationGap(sdkInitEndMs)?.let { gap ->
recordColdStart = gap <= SDK_AND_ACTIVITY_INIT_GAP
startTrace(
isColdStart = recordColdStart,
sdkInitStartMs = sdkInitStartMs,
activityInitTimeMs = activityInitTimeMs
)
}
}
}

override fun startupActivityPreCreated(timestampMs: Long?) {
Expand All @@ -124,7 +137,7 @@ internal class AppStartupTraceEmitter(
override fun startupActivityInitStart(timestampMs: Long?) {
startupActivityInitStartMs = (timestampMs ?: nowMs()).apply {
if (firstActivityInitStartMs == null) {
firstActivityInitStartMs = this
firstActivityInit(this)
}
}
}
Expand All @@ -140,7 +153,7 @@ internal class AppStartupTraceEmitter(
override fun startupActivityResumed(
activityName: String,
collectionCompleteCallback: (() -> Unit)?,
timestampMs: Long?
timestampMs: Long?,
) {
startupActivityName = activityName
startupActivityResumedMs = timestampMs ?: nowMs()
Expand All @@ -152,7 +165,7 @@ internal class AppStartupTraceEmitter(
override fun firstFrameRendered(
activityName: String,
collectionCompleteCallback: (() -> Unit)?,
timestampMs: Long?
timestampMs: Long?,
) {
startupActivityName = activityName
firstFrameRenderedMs = timestampMs ?: nowMs()
Expand All @@ -167,7 +180,7 @@ internal class AppStartupTraceEmitter(
endTimeMs: Long,
attributes: Map<String, String>,
events: List<EmbraceSpanEvent>,
errorCode: ErrorCode?
errorCode: ErrorCode?,
) {
additionalTrackedIntervals.add(
TrackedInterval(
Expand Down Expand Up @@ -208,59 +221,60 @@ internal class AppStartupTraceEmitter(
}
}

@Suppress("CyclomaticComplexMethod", "ComplexMethod")
private fun startTrace(isColdStart: Boolean, sdkInitStartMs: Long, activityInitTimeMs: Long) {
val rootSpan = if (isColdStart) {
val processStartTimeMs =
if (versionChecker.isAtLeast(VERSION_CODES.N)) {
processCreatedMs
} else if (applicationInitStartMs != null) {
applicationInitStartMs
} else {
sdkInitStartMs
}

spanService.startSpan(
name = "app-startup-cold",
startTimeMs = processStartTimeMs,
)
} else {
spanService.startSpan(
name = "app-startup-warm",
startTimeMs = activityInitTimeMs,
)
}

if (rootSpan != null) {
appStartupRootSpan.set(rootSpan)
} else {
logger.trackInternalError(
type = InternalErrorType.APP_LAUNCH_TRACE_FAIL,
throwable = IllegalStateException("App startup trace could not be started")

Check warning on line 251 in embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt#L249-L251

Added lines #L249 - L251 were not covered by tests
)
}
}

private fun recordStartup() {
val startupService = startupServiceProvider() ?: return
val sdkInitStartMs = startupService.getSdkInitStartMs()
val sdkInitEndMs = startupService.getSdkInitEndMs()
val processStartTimeMs: Long? =
if (versionChecker.isAtLeast(VERSION_CODES.N)) {
processCreatedMs
} else {
applicationInitStartMs ?: sdkInitStartMs
}

val traceEndTimeMs: Long? =
if (endWithFrameDraw) {
firstFrameRenderedMs
} else {
startupActivityResumedMs
}

sdkInitEndedInForeground = startupService.endedInForeground()
sdkInitThreadName = startupService.getInitThreadName()

if (processStartTimeMs != null && traceEndTimeMs != null && sdkInitEndMs != null) {
val gap = applicationActivityCreationGap(sdkInitEndMs)
if (gap != null) {
val startupTrace: EmbraceSpan? = if (!spanService.initialized()) {
null
} else if (gap <= SDK_AND_ACTIVITY_INIT_GAP) {
recordColdTtid(
traceStartTimeMs = processStartTimeMs,
applicationInitEndMs = applicationInitEndMs,
sdkInitStartMs = sdkInitStartMs,
sdkInitEndMs = sdkInitEndMs,
firstActivityInitMs = firstActivityInitStartMs,
activityInitStartMs = startupActivityInitStartMs,
activityInitEndMs = startupActivityInitEndMs,
traceEndTimeMs = traceEndTimeMs
)
} else {
val warmStartTimeMs = firstActivityInitStartMs ?: startupActivityInitStartMs
warmStartTimeMs?.let { startTime ->
recordWarmTtid(
traceStartTimeMs = startTime,
activityInitStartMs = startupActivityInitStartMs,
activityInitEndMs = startupActivityInitEndMs,
traceEndTimeMs = traceEndTimeMs,
)
}
}

if (startupTrace != null) {
recordAdditionalIntervals(startupTrace)
}
if (traceEndTimeMs != null && sdkInitEndMs != null) {
appStartupRootSpan.get()?.let { startupTrace ->
recordTtid(
applicationInitEndMs = if (recordColdStart) applicationInitEndMs else null,
sdkInitStartMs = if (recordColdStart) startupService.getSdkInitStartMs() else null,
sdkInitEndMs = if (recordColdStart) sdkInitEndMs else null,
firstActivityInitMs = if (recordColdStart) firstActivityInitStartMs else null,
activityInitStartMs = startupActivityInitStartMs,
activityInitEndMs = startupActivityInitEndMs,
traceEndTimeMs = traceEndTimeMs
)
recordAdditionalIntervals(startupTrace)
}
}
}
Expand All @@ -283,8 +297,7 @@ internal class AppStartupTraceEmitter(
}

@Suppress("CyclomaticComplexMethod", "ComplexMethod")
private fun recordColdTtid(
traceStartTimeMs: Long,
private fun recordTtid(
applicationInitEndMs: Long?,
sdkInitStartMs: Long?,
sdkInitEndMs: Long?,
Expand All @@ -294,24 +307,24 @@ internal class AppStartupTraceEmitter(
traceEndTimeMs: Long,
): EmbraceSpan? {
return if (!startupRecorded.get()) {
spanService.startSpan(
name = "app-startup-cold",
startTimeMs = traceStartTimeMs,
)?.apply {
appStartupRootSpan.get()?.apply {
addTraceMetadata()

if (stop(endTimeMs = traceEndTimeMs)) {
startupRecorded.set(true)
}

if (applicationInitEndMs != null) {
spanService.recordCompletedSpan(
name = "process-init",
parent = this,
startTimeMs = traceStartTimeMs,
endTimeMs = applicationInitEndMs,
)
getStartTimeMs()?.let { traceStartTimeMs ->
if (applicationInitEndMs != null) {
spanService.recordCompletedSpan(
name = "process-init",
parent = this,
startTimeMs = traceStartTimeMs,
endTimeMs = applicationInitEndMs,
)
}
}

if (sdkInitStartMs != null && sdkInitEndMs != null) {
spanService.recordCompletedSpan(
name = "embrace-init",
Expand All @@ -320,16 +333,17 @@ internal class AppStartupTraceEmitter(
endTimeMs = sdkInitEndMs,
)
}

val lastEventBeforeActivityInit = applicationInitEndMs ?: sdkInitEndMs
val firstActivityInit = firstActivityInitMs ?: activityInitStartMs
if (lastEventBeforeActivityInit != null && firstActivityInit != null) {
if (lastEventBeforeActivityInit != null && firstActivityInitMs != null) {
spanService.recordCompletedSpan(
name = "activity-init-gap",
parent = this,
startTimeMs = lastEventBeforeActivityInit,
endTimeMs = firstActivityInit,
endTimeMs = firstActivityInitMs,
)
}

if (activityInitStartMs != null && activityInitEndMs != null) {
spanService.recordCompletedSpan(
name = "activity-create",
Expand Down Expand Up @@ -357,47 +371,6 @@ internal class AppStartupTraceEmitter(
}
}

private fun recordWarmTtid(
traceStartTimeMs: Long,
activityInitStartMs: Long?,
activityInitEndMs: Long?,
traceEndTimeMs: Long,
): EmbraceSpan? {
return if (!startupRecorded.get()) {
spanService.startSpan(
name = "app-startup-warm",
startTimeMs = traceStartTimeMs,
)?.apply {
addTraceMetadata()

if (stop(endTimeMs = traceEndTimeMs)) {
startupRecorded.set(true)
}
if (activityInitStartMs != null && activityInitEndMs != null) {
spanService.recordCompletedSpan(
name = "activity-create",
parent = this,
startTimeMs = activityInitStartMs,
endTimeMs = activityInitEndMs,
)
val uiLoadSpanName = if (endWithFrameDraw) {
"first-frame-render"
} else {
"activity-resume"
}
spanService.recordCompletedSpan(
name = uiLoadSpanName,
parent = this,
startTimeMs = activityInitEndMs,
endTimeMs = traceEndTimeMs,
)
}
}
} else {
null
}
}

private fun applicationActivityCreationGap(sdkInitEndMs: Long): Long? =
duration(applicationInitEndMs ?: sdkInitEndMs, firstActivityInitStartMs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ class FakePersistableEmbraceSpan(
statusDescription = description
}

override fun getStartTimeMs(): Long? = spanStartTimeMs

override fun addAttribute(key: String, value: String): Boolean {
attributes[key] = value
return true
Expand Down

0 comments on commit 2e79bb4

Please sign in to comment.