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

Make Glean init in Kotlin fully async #672

Merged
merged 4 commits into from
Feb 13, 2020
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

* General:
* `ping_type` is not included in the `ping_info` any more ([#653](https://github.com/mozilla/glean/pull/653)), the pipeline takes the value from the submission URL.
* Android:
* The `Glean.initialize` method runs mostly off the main thread ([#672](https://github.com/mozilla/glean/pull/672)).
* iOS:
* The baseline ping will now include `reason` codes that indicate why it was
submitted. If an unclean shutdown is detected (e.g. due to force-close), this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ internal object Dispatchers {
* set to false prior to processing the queue, newly launched tasks should be executed
* on the couroutine scope rather than added to the queue.
*/
internal fun flushQueuedInitialTasks() {
internal suspend fun flushQueuedInitialTasks() {
val dispatcherObject = this
// Dispatch a task to flush the pre-init tasks queue. By using `executeTask`
// this will be executed as soon as possible, before other tasks are executed.
Expand Down Expand Up @@ -139,7 +139,7 @@ internal object Dispatchers {
if (overflowCount > 0) {
GleanError.preinitTasksOverflow.addSync(MAX_QUEUE_SIZE + overflowCount)
}
}
}?.join()
}

/**
Expand Down
125 changes: 59 additions & 66 deletions glean-core/android/src/main/java/mozilla/telemetry/glean/Glean.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import androidx.annotation.VisibleForTesting
import androidx.lifecycle.ProcessLifecycleOwner
import com.sun.jna.StringArray
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.telemetry.glean.config.Configuration
import mozilla.telemetry.glean.config.FfiConfiguration
import mozilla.telemetry.glean.utils.getLocaleTag
Expand Down Expand Up @@ -135,95 +137,86 @@ open class GleanInternalAPI internal constructor () {
return
}

setUploadEnabled(uploadEnabled)

registerPings(Pings)

this.applicationContext = applicationContext

this.configuration = configuration
this.gleanDataDir = File(applicationContext.applicationInfo.dataDir, GLEAN_DATA_DIR)

val cfg = FfiConfiguration(
dataDir = this.gleanDataDir.path,
packageName = applicationContext.packageName,
uploadEnabled = uploadEnabled,
maxEvents = this.configuration.maxEvents,
delayPingLifetimeIO = false
)
setUploadEnabled(uploadEnabled)

initialized = LibGleanFFI.INSTANCE.glean_initialize(cfg).toBoolean()
// Execute startup off the main thread.
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.executeTask {
registerPings(Pings)

val cfg = FfiConfiguration(
dataDir = gleanDataDir.path,
packageName = applicationContext.packageName,
uploadEnabled = uploadEnabled,
maxEvents = configuration.maxEvents,
delayPingLifetimeIO = false
)

// If initialization of Glean fails we bail out and don't initialize further.
if (!initialized) {
return
}
initialized = LibGleanFFI.INSTANCE.glean_initialize(cfg).toBoolean()

// If any pings were registered before initializing, do so now.
// We're not clearing this queue in case Glean is reset by tests.
this.pingTypeQueue.forEach { this.registerPingType(it) }
// If initialization of Glean fails we bail out and don't initialize further.
if (!initialized) {
return@executeTask
}

// If this is the first time ever the Glean SDK runs, make sure to set
// some initial core metrics in case we need to generate early pings.
// The next times we start, we would have them around already.
val isFirstRun = LibGleanFFI.INSTANCE.glean_is_first_run().toBoolean()
if (isFirstRun) {
initializeCoreMetrics(applicationContext)
}
// If any pings were registered before initializing, do so now.
// We're not clearing this queue in case Glean is reset by tests.
pingTypeQueue.forEach { registerPingType(it) }

// Deal with any pending events so we can start recording new ones
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.executeTask {
// If this is the first time ever the Glean SDK runs, make sure to set
// some initial core metrics in case we need to generate early pings.
// The next times we start, we would have them around already.
val isFirstRun = LibGleanFFI.INSTANCE.glean_is_first_run().toBoolean()
if (isFirstRun) {
initializeCoreMetrics(applicationContext)
}

// Deal with any pending events so we can start recording new ones
val pingSubmitted = LibGleanFFI.INSTANCE.glean_on_ready_to_submit_pings().toBoolean()
if (pingSubmitted) {
PingUploadWorker.enqueueWorker(applicationContext)
}
}

// Set up information and scheduling for Glean owned pings. Ideally, the "metrics"
// ping startup check should be performed before any other ping, since it relies
// on being dispatched to the API context before any other metric.
metricsPingScheduler = MetricsPingScheduler(applicationContext)
metricsPingScheduler.schedule()
// Set up information and scheduling for Glean owned pings. Ideally, the "metrics"
// ping startup check should be performed before any other ping, since it relies
// on being dispatched to the API context before any other metric.
metricsPingScheduler = MetricsPingScheduler(applicationContext)
metricsPingScheduler.schedule()

// From the second time we run, after all startup pings are generated,
// make sure to clear `lifetime: application` metrics and set them again.
// Any new value will be sent in newly generted pings after startup.
if (!isFirstRun) {
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.executeTask {
// From the second time we run, after all startup pings are generated,
// make sure to clear `lifetime: application` metrics and set them again.
// Any new value will be sent in newly generated pings after startup.
if (!isFirstRun) {
LibGleanFFI.INSTANCE.glean_clear_application_lifetime_metrics()
initializeCoreMetrics(applicationContext)
}
}

// Signal Dispatcher that init is complete
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.flushQueuedInitialTasks()

// Check if the "dirty flag" is set. That means the product was probably
// force-closed. If that's the case, submit a 'baseline' ping with the
// reason "dirty_startup". We only do that from the second run.
if (!isFirstRun) {
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.launch {
if (LibGleanFFI.INSTANCE.glean_is_dirty_flag_set().toBoolean()) {
submitPingByNameSync("baseline", "dirty_startup")
// Note: while in theory we should set the "dirty flag" to true
// here, in practice it's not needed: if it hits this branch, it
// means the value was `true` and nothing needs to be done.
}
// Signal Dispatcher that init is complete
Dispatchers.API.flushQueuedInitialTasks()

// Check if the "dirty flag" is set. That means the product was probably
// force-closed. If that's the case, submit a 'baseline' ping with the
// reason "dirty_startup". We only do that from the second run.
if (!isFirstRun && LibGleanFFI.INSTANCE.glean_is_dirty_flag_set().toBoolean()) {
submitPingByNameSync("baseline", "dirty_startup")
// Note: while in theory we should set the "dirty flag" to true
// here, in practice it's not needed: if it hits this branch, it
// means the value was `true` and nothing needs to be done.
}
}

// At this point, all metrics and events can be recorded.
// This should only be called from the main thread. This is enforced by
// the @MainThread decorator and the `assertOnUiThread` call.
ProcessLifecycleOwner.get().lifecycle.addObserver(gleanLifecycleObserver)
// At this point, all metrics and events can be recorded.
// This should only be called from the main thread. This is enforced by
// the @MainThread decorator and the `assertOnUiThread` call.
MainScope().launch {
ProcessLifecycleOwner.get().lifecycle.addObserver(gleanLifecycleObserver)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to do this here, or could we do it outside of the coroutine (near the top where things like applicationContext are set?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure yet, I don't think there's anything preventing us from moving it there, before the glean init even starts.

The only two reasons for not doing that would be:

  • we don't want to register this if the glean init fails;
  • we really want to test this in Fenix before making a call!

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough on both points. I think I was just coming at it from the angle of -- going off the main thread only to come back to it is more complicated. But, as you say, we don't really know until we try it in the context of Fenix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll make sure to try that first once I get rid of the failing test and have a proper Fenix build for testing!

Copy link
Contributor Author

@Dexterp37 Dexterp37 Feb 12, 2020

Choose a reason for hiding this comment

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

I tested that, but I think we should retain the current behaviour even just to make sure that we don't mistakenly register observers if init fails. Thoughts?

Note that this would also protect us against sudden/immediate going to background while initializing.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good point about not registering if we exit early for some other error during init. 👍 on this, then!


if (!uploadEnabled) {
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.launch {
if (!uploadEnabled) {
DeletionPingUploadWorker.enqueueWorker(applicationContext)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.content.Context
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.testing.WorkManagerTestInitHelper
import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout

Expand Down Expand Up @@ -35,7 +36,7 @@ internal fun testFlushWorkManagerJob(context: Context, workTag: String, timeoutM
return@withTimeout
}
}
} while (true)
} while (isActive)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
package mozilla.telemetry.glean

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Dispatchers as KotlinDispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -63,7 +65,9 @@ class DispatchersTest {
assertEquals("Tasks have not run while in queue", 0, threadCanary)

// Now trigger execution to ensure the tasks fired
Dispatchers.API.flushQueuedInitialTasks()
runBlocking {
Dispatchers.API.flushQueuedInitialTasks()
}

assertEquals("Tasks have executed", 3, threadCanary)
assertEquals("Task queue is cleared", 0, Dispatchers.API.taskQueue.size)
Expand Down Expand Up @@ -97,12 +101,14 @@ class DispatchersTest {
assertEquals("Tasks have not run while in queue", 0, threadCanary.get())

// Now trigger execution to ensure the tasks fired
Dispatchers.API.flushQueuedInitialTasks()
GlobalScope.launch {
Dispatchers.API.flushQueuedInitialTasks()
}

// Wait for the flushed tasks to be executed.
runBlocking {
withTimeoutOrNull(2000) {
while (threadCanary.get() != 3) {
while (isActive && (threadCanary.get() != 3 || Dispatchers.API.taskQueue.size > 0)) {
delay(1)
}
} ?: assertTrue("Timed out waiting for tasks to execute", false)
Expand Down Expand Up @@ -152,7 +158,7 @@ class DispatchersTest {
runBlocking {
// Ensure that all the required jobs have been added to the list.
withTimeoutOrNull(2000) {
while (counter.get() < 100) {
while (isActive && counter.get() < 100) {
delay(1)
}
} ?: assertEquals("Timed out waiting for tasks to execute", 100, counter.get())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import android.content.Context;

import androidx.test.core.app.ApplicationProvider;
import androidx.work.testing.WorkManagerTestInitHelper;

import org.junit.Before;
Expand All @@ -25,7 +24,7 @@ public class GleanFromJavaTest {
// callable from Java. If something goes wrong, it should complain about missing
// methods at build-time.

private Context appContext = ApplicationProvider.getApplicationContext();
private Context appContext = TestUtilKt.getContextWithMockedInfo("java-test");

@Before
public void setup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.test.core.app.ApplicationProvider
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.testing.WorkManagerTestInitHelper
import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.json.JSONObject
Expand Down Expand Up @@ -197,7 +198,7 @@ internal fun waitForEnqueuedWorker(
if (getWorkerStatus(context, workTag).isEnqueued) {
return@withTimeout
}
} while (true)
} while (isActive)
}
}

Expand Down