From 5d8c900391a8f545edb6cfead62e1b12fbf8fa6f Mon Sep 17 00:00:00 2001 From: Michael Comella Date: Tue, 21 Jul 2020 15:28:02 -0700 Subject: [PATCH] For #12802: add StorageStats glean metrics. --- app/metrics.yaml | 79 +++++++++++++++++++ .../org/mozilla/fenix/FenixApplication.kt | 13 +++ .../mozilla/fenix/perf/StorageStatsMetrics.kt | 66 ++++++++++++++++ .../fenix/perf/StorageStatsMetricsTest.kt | 61 ++++++++++++++ docs/metrics.md | 4 + 5 files changed, 223 insertions(+) create mode 100644 app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt create mode 100644 app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt diff --git a/app/metrics.yaml b/app/metrics.yaml index 37a9de4f1..483e4e2c8 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -3236,3 +3236,82 @@ autoplay: notification_emails: - fenix-core@mozilla.com expires: "2021-02-01" + +storage.stats: + query_stats_duration: + send_in_pings: + - metrics + type: timing_distribution + description: > + How long it took to query the device for the StorageStats that contain the + file size information. The docs say it may be expensive so we want to + ensure it's not too expensive. This value is only available on Android + 8+. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" + app_bytes: + send_in_pings: + - metrics + type: memory_distribution + description: > + The size of the app's APK and related files as installed: this is expected + to be larger than download size. This is the output of + [StorageStats.getAppBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getAppBytes()) + so see that for details. This value is only available on Android 8+. A + similar value may be available on the Google Play dashboard: we can use + this value to see if that value is reliable enough. + memory_unit: byte + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" + cache_bytes: + send_in_pings: + - metrics + type: memory_distribution + description: > + The size of all cached data in the app. This is the output of + [StorageStats.getCacheBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getCacheBytes()) + so see that for details. This value is only available on Android 8+. + memory_unit: byte + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" + data_dir_bytes: + send_in_pings: + - metrics + type: memory_distribution + description: > + The size of all data minus `cache_bytes`. This is the output of + [StorageStats.getDataBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getDataBytes()) + except we subtract the value of `cache_bytes` so the cache is not measured + redundantly; see that method for details. This value is only available on + Android 8+. + memory_unit: byte + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 201ba43d1..e9ea55bbc 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -44,6 +44,7 @@ import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.perf.StorageStatsMetrics import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.WebPushEngineIntegration @@ -205,12 +206,24 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } } + fun queueMetrics() { + if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics. + taskQueue.runIfReadyOrQueue { + // Because it may be slow to capture the storage stats, it might be preferred to + // create a WorkManager task for this metric, however, I ran out of + // implementation time and WorkManager is harder to test. + StorageStatsMetrics.report(this.applicationContext) + } + } + } + initQueue() // We init these items in the visual completeness queue to avoid them initing in the critical // startup path, before the UI finishes drawing (i.e. visual completeness). queueInitExperiments() queueInitStorageAndServices() + queueMetrics() } private fun startMetricsIfEnabled() { diff --git a/app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt b/app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt new file mode 100644 index 000000000..af1463612 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import android.app.usage.StorageStats +import android.app.usage.StorageStatsManager +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.PRIVATE +import androidx.annotation.WorkerThread +import androidx.core.content.getSystemService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.mozilla.fenix.GleanMetrics.StorageStats as Metrics + +/** + * A collection of functions related to measuring the [StorageStats] of the application such as data + * dir size. + * + * Unfortunately, this API is only available on API 26+ so the data will only be reported for those + * platforms. + */ +@RequiresApi(Build.VERSION_CODES.O) // StorageStatsManager +object StorageStatsMetrics { + + fun report(context: Context) { + GlobalScope.launch(Dispatchers.IO) { + reportSync(context) + } + } + + // I couldn't get runBlockingTest to work correctly so I moved the functionality under test to + // a synchronous function. + @VisibleForTesting(otherwise = PRIVATE) + @WorkerThread // queryStatsForUid + fun reportSync(context: Context) { + // I don't expect this to ever be null so we don't report if so. + context.getSystemService()?.let { storageStatsManager -> + val appInfo = context.applicationInfo + val storageStats = Metrics.queryStatsDuration.measure { + // The docs say queryStatsForPackage may be slower if the app uses + // android:sharedUserId so we the suggested alternative. + // + // The docs say this may be slow: + // > This method may take several seconds to complete, so it should only be called + // > from a worker thread. + // + // So we call from a worker thread and measure the duration to make sure it's not + // too slow. + storageStatsManager.queryStatsForUid(appInfo.storageUuid, appInfo.uid) + } + + // dataBytes includes the cache so we subtract it. + val justDataDirBytes = storageStats.dataBytes - storageStats.cacheBytes + + Metrics.dataDirBytes.accumulate(justDataDirBytes) + Metrics.appBytes.accumulate(storageStats.appBytes) + Metrics.cacheBytes.accumulate(storageStats.cacheBytes) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt b/app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt new file mode 100644 index 000000000..e66a69fb8 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import android.app.usage.StorageStats +import android.app.usage.StorageStatsManager +import android.content.Context +import androidx.core.content.getSystemService +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.GleanMetrics.StorageStats as Metrics + +@RunWith(FenixRobolectricTestRunner::class) // gleanTestRule +class StorageStatsMetricsTest { + + @get:Rule + val gleanTestRule = GleanTestRule(testContext) + + @RelaxedMockK private lateinit var mockContext: Context + @RelaxedMockK private lateinit var storageStats: StorageStats + + @Before + fun setUp() { + MockKAnnotations.init(this) + + every { + mockContext.getSystemService()?.queryStatsForUid(any(), any()) + } returns storageStats + } + + @Test + fun `WHEN reporting THEN the values from the storageStats are accumulated`() { + every { storageStats.appBytes } returns 100 + every { storageStats.cacheBytes } returns 200 + every { storageStats.dataBytes } returns 1000 + + StorageStatsMetrics.reportSync(mockContext) + + assertEquals(100, Metrics.appBytes.testGetValue().sum) + assertEquals(200, Metrics.cacheBytes.testGetValue().sum) + assertEquals(800, Metrics.dataDirBytes.testGetValue().sum) + } + + @Test + fun `WHEN reporting THEN the query duration is measured`() { + StorageStatsMetrics.reportSync(mockContext) + assertTrue(Metrics.queryStatsDuration.testHasValue()) + } +} diff --git a/docs/metrics.md b/docs/metrics.md index c9482a603..0d85cf5b0 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -311,6 +311,10 @@ The following metrics are added to the ping: | search.default_engine.code |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be the search engine identifier. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | | search.default_engine.name |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be the search engine name. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | | search.default_engine.submission_url |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be he base URL we use to build the search query for the search engine. For example: https://mysearchengine.com/?query=%s. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | +| storage.stats.app_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of the app's APK and related files as installed: this is expected to be larger than download size. This is the output of [StorageStats.getAppBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getAppBytes()) so see that for details. This value is only available on Android 8+. A similar value may be available on the Google Play dashboard: we can use this value to see if that value is reliable enough. |[1](todo)||2020-12-21 | | +| storage.stats.cache_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all cached data in the app. This is the output of [StorageStats.getCacheBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getCacheBytes()) so see that for details. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | +| storage.stats.data_dir_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all data minus `cache_bytes`. This is the output of [StorageStats.getDataBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getDataBytes()) except we subtract the value of `cache_bytes` so the cache is not measured redundantly; see that method for details. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | +| storage.stats.query_stats_duration |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |How long it took to query the device for the StorageStats that contain the file size information. The docs say it may be expensive so we want to ensure it's not too expensive. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | ## startup-timeline