RUMM-2599 ApplicationLaunch logic changes#1278
Conversation
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.
|
|
||
| internal interface AppStartTimeProvider { | ||
| /** | ||
| * Provide the application start time in nanoseconds from device boot, or our best guess if |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 == |
There was a problem hiding this comment.
have you added a UnitTest for this case ? I didn't see it, maybe I missed it ?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
But it also means that if event we've got is StartView, another ViewScope will be started immediately after this one. Is it expected?
There was a problem hiding this comment.
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 == |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| val appStartTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(500) | ||
| whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs |
There was a problem hiding this comment.
We can use a random value for appStartTimeNs here.
| 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) |
There was a problem hiding this comment.
| 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<*>>( |
There was a problem hiding this comment.
we need also to delete Forge.validAppLaunchEvent() in RumRawEventExt file in tests
| val childView: RumViewScope = mock() | ||
| val startViewEvent = forge.startViewEvent() | ||
|
|
||
| val fakeEvent = forge.addErrorEvent() |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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(...)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
1901c99 to
65409a6
Compare
65409a6 to
0bc387d
Compare
0xnm
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>) { |
There was a problem hiding this comment.
Can be private I think
| internal fun startApplicationLaunchView(event: RumRawEvent, writer: DataWriter<Any>) { | |
| private fun startApplicationLaunchView(event: RumRawEvent, writer: DataWriter<Any>) { |
| writer | ||
| ) | ||
| childrenScopes.add(viewScope) | ||
| applicationDisplayed = true |
There was a problem hiding this comment.
should we pull this line up? just in case if inner handleEvent also access this flag at some point.
| val scopes = if (fakeEvent is RumRawEvent.StartView) 2 else 1 | ||
| assertThat(testedScope.childrenScopes).hasSize(scopes) |
There was a problem hiding this comment.
minor suggestion (just to distinguish from scopes as collection)
| 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) |
There was a problem hiding this comment.
or we can a bit more randomness and simply do val appStartTimeNs = forge.aLong(min = 0L, max = fakeEvent.eventTime.nanoTime)
There was a problem hiding this comment.
👍 Though I think we probably want to go min, say fakeTime - 1000ms second to fakeTime - 100ms? What do you think?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
| 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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
ca3f6cb to
06bd307
Compare
0xnm
left a comment
There was a problem hiding this comment.
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 == |
There was a problem hiding this comment.
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.
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_startevent is also triggered immediately. The "duration" of theapplication_startevent 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_startfor 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)