Skip to content

RUMM-2599 ApplicationLaunch logic changes#1278

Merged
fuzzybinary merged 4 commits into
developfrom
RUMM-2599-application-launch-view
Feb 21, 2023
Merged

RUMM-2599 ApplicationLaunch logic changes#1278
fuzzybinary merged 4 commits into
developfrom
RUMM-2599-application-launch-view

Conversation

@fuzzybinary

Copy link
Copy Markdown
Member

What does this PR do?

ApplicationLaunch now starts immediately when the first event is sent to RUM provided we are in the Foreground. The view's start time is now the process start time (or as close as we can get it), and the application_start event is also triggered immediately. The "duration" of the application_start event is equal to the time from when the process started, until the first event sent to RUM.

Additional Notes

This may change the timing for application_start for some clients, but should make it shorter in most cases since instead of timing to first view (or first event that causes ApplicationLaunch) we time until the first event sent to RUM regardless of what that event is.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

ApplicationLaunch now starts immediately when the first event is sent to RUM provided we are in the Foreground. The view's start time is now the process start time (or as close as we can get it), and the `application_start` event is also triggered immediately. The "duration" of the `application_start` event is equal to the time from when the process started, until the first event sent to RUM.
@fuzzybinary
fuzzybinary requested a review from a team as a code owner February 10, 2023 21:02

internal interface AppStartTimeProvider {
/**
* Provide the application start time in nanoseconds from device boot, or our best guess if

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from device boot
From application boot, no?

Also, can we elaborate on the "our best guess" with the actual implementation?
i.e. from RUM class load in Android < N

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can yes. I left it vague so that if it changes we wouldn't have to worry about updating the documentation or have it be wrong.

@fuzzybinary fuzzybinary Feb 13, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the time is actually nanoseconds since device boot (it's using System.nanoTime or the elapsedRealtime functions, which are both timed from JVM or device boot time.

Also also, I remember another reason I said "our best guess" which is that the specifics would be in the implementation. This is saying to implementers "give us the exact value, or your best guess." The implementation (DefaultAppStartTimeProvider) could be documented with exactly how we're doing it if we want.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now I understand what you meant by "from device boot".
Initially I interpreted it as "we are returning the time difference between the app started and the time you booted your device"

@WorkerThread
override fun handleEvent(event: RumRawEvent, writer: DataWriter<Any>): RumScope {
if (!applicationDisplayed) {
val isForegroundProcess = CoreFeature.processImportance ==

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you added a UnitTest for this case ? I didn't see it, maybe I missed it ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's just that they didn't change. They're 𝕄 start a foreground ViewScope 𝕎 handleEvent(StartView) in RumViewManagerScopeTest. They only test for ViewEvents, but I think other test handle other possible events.

val isForegroundProcess = CoreFeature.processImportance ==
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
if (isForegroundProcess) {
startApplicationLaunchView(event, writer)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it also means that if event we've got is StartView, another ViewScope will be started immediately after this one. Is it expected?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. ApplicationLaunch will then take the time between AppStartTimeProvied.appStartTimeNs and the start time of the new View.

@WorkerThread
override fun handleEvent(event: RumRawEvent, writer: DataWriter<Any>): RumScope {
if (!applicationDisplayed) {
val isForegroundProcess = CoreFeature.processImportance ==

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually curious how this is going to work in the following scenario: app originally started with IMPORTANCE_BACKGROUND, but then came into the foreground while still being active (same process).

In that case (since we resolve processImportance only once, at the CoreFeature initialization time -> at the app start time), we will never report startup time for such cases, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I think that's the correct logic as that would skep application start time, but this is for sure where I'm confused about the logic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if Android also can pre-warm apps (didn't dig into this topic tbh). I would suggest doing some tests using either our sample app or Reddit client. We can maybe emulate start by the user, background invocation using adb (like background invocation can maybe to send some intent to the service, etc).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed that's the expected logic as we don't have any other event than the process start time to compute for the warm app launch. There's no catchable event except for the Activity's lifecycle which is already too late.

Comment on lines +527 to +528
val appStartTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(500)
whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use a random value for appStartTimeNs here.

Suggested change
val appStartTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(500)
whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs
val appStartTimeNs = forge.aPositiveLong()
whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs

@IntForgery(min = Build.VERSION_CODES.N) apiVersion: Int
) {
// Given
val appStartTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(500)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val appStartTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(500)
val appStartTimeNs = forge.aPositiveLong()

RumRawEvent.StartAction::class.java,
RumRawEvent.StartResource::class.java
)
internal val validAppLaunchEventTypes = arrayOf<Class<*>>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need also to delete Forge.validAppLaunchEvent() in RumRawEventExt file in tests

val childView: RumViewScope = mock()
val startViewEvent = forge.startViewEvent()

val fakeEvent = forge.addErrorEvent()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to make sure that it is working with any event, so I would suggest to add a method in the RumRawEventExt file which will return any RumRawEvent available and use it here.

.isCloseTo(System.nanoTime(), Offset.offset(TimeUnit.MILLISECONDS.toNanos(100)))
val appStartTimeMs = TimeUnit.NANOSECONDS.toMillis(appStartTimeNs)
argumentCaptor<ActionEvent> {
verify(mockWriter, atLeastOnce()).write(eq(mockEventBatchWriter), capture())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect more calls here? If we expect only one call, we can use times(1), or simply omit it and use verify(mockWriter).write(...)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we do, I think we get 2-3 (which is why I added the atLeastOnce(). I have to verify what they are but I know you get a ViewEvent first, then another, then the ActionEvent with application_started as the type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, it makes sense since we create Application Launch scope and then delegate action to it.

And I guess then the number of interactions vary from the type of the original event handled? Because some events can cause an instant write (like fatal errors).

@fuzzybinary
fuzzybinary force-pushed the RUMM-2599-application-launch-view branch 2 times, most recently from 1901c99 to 65409a6 Compare February 14, 2023 19:39
@fuzzybinary
fuzzybinary force-pushed the RUMM-2599-application-launch-view branch from 65409a6 to 0bc387d Compare February 14, 2023 19:44

@0xnm 0xnm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left few suggestions. However, it seems that unit-tests are failing, so we probably have something wrong in the logic.

fun `M return process start time W appStartTime { N+ }`() {
// GIVEN
val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock()
whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.O

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: can also probably use IntForgery with min = Build.VERSION_CODES.N as for the test below

// region Internal

@WorkerThread
internal fun startApplicationLaunchView(event: RumRawEvent, writer: DataWriter<Any>) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be private I think

Suggested change
internal fun startApplicationLaunchView(event: RumRawEvent, writer: DataWriter<Any>) {
private fun startApplicationLaunchView(event: RumRawEvent, writer: DataWriter<Any>) {

writer
)
childrenScopes.add(viewScope)
applicationDisplayed = true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we pull this line up? just in case if inner handleEvent also access this flag at some point.

Comment on lines +536 to +537
val scopes = if (fakeEvent is RumRawEvent.StartView) 2 else 1
assertThat(testedScope.childrenScopes).hasSize(scopes)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor suggestion (just to distinguish from scopes as collection)

Suggested change
val scopes = if (fakeEvent is RumRawEvent.StartView) 2 else 1
assertThat(testedScope.childrenScopes).hasSize(scopes)
val scopesCount = if (fakeEvent is RumRawEvent.StartView) 2 else 1
assertThat(testedScope.childrenScopes).hasSize(scopesCount)

testedScope.applicationDisplayed = false
val fakeEvent = forge.validAppLaunchEvent()
val fakeEvent = forge.anyRumEvent()
val appStartTimeNs = fakeEvent.eventTime.nanoTime - TimeUnit.MILLISECONDS.toNanos(500)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or we can a bit more randomness and simply do val appStartTimeNs = forge.aLong(min = 0L, max = fakeEvent.eventTime.nanoTime)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Though I think we probably want to go min, say fakeTime - 1000ms second to fakeTime - 100ms? What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is ok for me. We don't have thresholds to filter out too big times, so we can accept any positive value as min (we just try to avoid hardcoded values in our tests).

@IntForgery(min = Build.VERSION_CODES.N) apiVersion: Int
) {
// Given
val appStartTimeNs = fakeEvent.eventTime.nanoTime - TimeUnit.MILLISECONDS.toNanos(500)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val appStartTimeNs = fakeEvent.eventTime.nanoTime - TimeUnit.MILLISECONDS.toNanos(500)
val appStartTimeNs = forge.aLong(min = 0L, max = fakeEvent.eventTime.nanoTime)

instrumentation.waitForIdleSync()

// one for the Application start action
expectedEvents.add(ExpectedApplicationStart())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we align the naming used? Seems we have ApplicationStart and ApplicationLaunch at the same time.

UPD: seems we are a bit late, both definitions already live in the codebase.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix so it's "ApplicationStartEvent" and "ApplicaitonLaunchView" which I know doesn't make things that much clearer but at least it will be consistent.

Some tests were using "anyRumEvent" that would break if a new view started. Fix so we can exclude that event for those tests.
The test to ensure that events sent before the application was displayed that orphan silently, should not use the logger was failing because any events that are sent to the ViewManagerScope when the application is not displayed are no longer orphaned, as they go to the ApplicationLaunch view. Occasionally, the test would log something unrelated to orphaning which was failing the test.
@fuzzybinary
fuzzybinary force-pushed the RUMM-2599-application-launch-view branch from ca3f6cb to 06bd307 Compare February 15, 2023 21:22

@0xnm 0xnm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks good now, let's wait for the product decision first then before merging.

@WorkerThread
override fun handleEvent(event: RumRawEvent, writer: DataWriter<Any>): RumScope {
if (!applicationDisplayed) {
val isForegroundProcess = CoreFeature.processImportance ==

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed that's the expected logic as we don't have any other event than the process start time to compute for the warm app launch. There's no catchable event except for the Activity's lifecycle which is already too late.

@fuzzybinary
fuzzybinary merged commit dfd6c81 into develop Feb 21, 2023
@fuzzybinary
fuzzybinary deleted the RUMM-2599-application-launch-view branch February 21, 2023 20:03
@xgouchet xgouchet added this to the 1.18.0 milestone Dec 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants