diff options
4 files changed, 279 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 5a1a505d7aa8..938e36e4b42c 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -5904,6 +5904,10 @@ public final class ActiveServices { * @param durationMs Only meaningful for EXIT event, the duration from ENTER and EXIT state. */ private void logForegroundServiceStateChanged(ServiceRecord r, int state, int durationMs) { + if (!ActivityManagerUtils.shouldSamplePackageForAtom( + r.packageName, mAm.mConstants.mDefaultFgsAtomSampleRate)) { + return; + } FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.appInfo.uid, r.shortInstanceName, diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index c8363dd59005..bf574521b895 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -97,6 +97,7 @@ final class ActivityManagerConstants extends ContentObserver { static final String KEY_BOOT_TIME_TEMP_ALLOWLIST_DURATION = "boot_time_temp_allowlist_duration"; static final String KEY_FG_TO_BG_FGS_GRACE_DURATION = "fg_to_bg_fgs_grace_duration"; static final String KEY_FGS_START_FOREGROUND_TIMEOUT = "fgs_start_foreground_timeout"; + static final String KEY_FGS_ATOM_SAMPLE_RATE = "fgs_atom_sample_rate"; private static final int DEFAULT_MAX_CACHED_PROCESSES = 32; private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000; @@ -137,6 +138,7 @@ final class ActivityManagerConstants extends ContentObserver { private static final int DEFAULT_BOOT_TIME_TEMP_ALLOWLIST_DURATION = 10 * 1000; private static final long DEFAULT_FG_TO_BG_FGS_GRACE_DURATION = 5 * 1000; private static final int DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS = 10 * 1000; + private static final float DEFAULT_FGS_ATOM_SAMPLE_RATE = 1; // 100 % // Flag stored in the DeviceConfig API. /** @@ -430,6 +432,13 @@ final class ActivityManagerConstants extends ContentObserver { */ volatile long mFgsStartForegroundTimeoutMs = DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS; + /** + * Sample rate for the FGS westworld atom. + * + * If the value is 0.1, 10% of the installed packages would be sampled. + */ + volatile float mDefaultFgsAtomSampleRate = DEFAULT_FGS_ATOM_SAMPLE_RATE; + private final ActivityManagerService mService; private ContentResolver mResolver; private final KeyValueListParser mParser = new KeyValueListParser(','); @@ -629,6 +638,9 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_FGS_START_FOREGROUND_TIMEOUT: updateFgsStartForegroundTimeout(); break; + case KEY_FGS_ATOM_SAMPLE_RATE: + updateFgsAtomSamplePercent(); + break; default: break; } @@ -933,6 +945,13 @@ final class ActivityManagerConstants extends ContentObserver { DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS); } + private void updateFgsAtomSamplePercent() { + mDefaultFgsAtomSampleRate = DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_FGS_ATOM_SAMPLE_RATE, + DEFAULT_FGS_ATOM_SAMPLE_RATE); + } + private void updateImperceptibleKillExemptions() { IMPERCEPTIBLE_KILL_EXEMPT_PACKAGES.clear(); IMPERCEPTIBLE_KILL_EXEMPT_PACKAGES.addAll(mDefaultImperceptibleKillExemptPackages); @@ -1145,6 +1164,8 @@ final class ActivityManagerConstants extends ContentObserver { pw.println(mFlagFgsStartRestrictionEnabled); pw.print(" "); pw.print(KEY_DEFAULT_FGS_STARTS_RESTRICTION_CHECK_CALLER_TARGET_SDK); pw.print("="); pw.println(mFgsStartRestrictionCheckCallerTargetSdk); + pw.print(" "); pw.print(KEY_FGS_ATOM_SAMPLE_RATE); + pw.print("="); pw.println(mDefaultFgsAtomSampleRate); pw.println(); if (mOverrideMaxCachedProcesses >= 0) { diff --git a/services/core/java/com/android/server/am/ActivityManagerUtils.java b/services/core/java/com/android/server/am/ActivityManagerUtils.java new file mode 100644 index 000000000000..dd2414866542 --- /dev/null +++ b/services/core/java/com/android/server/am/ActivityManagerUtils.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.am; + +import android.app.ActivityThread; +import android.provider.Settings; +import android.util.ArrayMap; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * To store random utility methods... + */ +public class ActivityManagerUtils { + private ActivityManagerUtils() { + } + + private static Integer sAndroidIdHash; + + @GuardedBy("sHashCache") + private static final ArrayMap<String, Integer> sHashCache = new ArrayMap<>(); + + private static String sInjectedAndroidId; + + /** Used by the unit tests to inject an android ID. Do not set in the prod code. */ + @VisibleForTesting + static void injectAndroidIdForTest(String androidId) { + sInjectedAndroidId = androidId; + sAndroidIdHash = null; + } + + /** + * Return a hash between [0, MAX_VALUE] generated from the android ID. + */ + @VisibleForTesting + static int getAndroidIdHash() { + // No synchronization is required. Double-initialization is fine here. + if (sAndroidIdHash == null) { + final String androidId = Settings.Secure.getString( + ActivityThread.currentApplication().getContentResolver(), + Settings.Secure.ANDROID_ID); + sAndroidIdHash = getUnsignedHashUnCached( + sInjectedAndroidId != null ? sInjectedAndroidId : androidId); + } + return sAndroidIdHash; + } + + /** + * Return a hash between [0, MAX_VALUE] generated from a package name, using a cache. + * + * Because all the results are cached, do not use it for dynamically generated strings. + */ + @VisibleForTesting + static int getUnsignedHashCached(String s) { + synchronized (sHashCache) { + final Integer cached = sHashCache.get(s); + if (cached != null) { + return cached; + } + final int hash = getUnsignedHashUnCached(s); + sHashCache.put(s.intern(), hash); + return hash; + } + } + + /** + * Return a hash between [0, MAX_VALUE] generated from a package name. + */ + private static int getUnsignedHashUnCached(String s) { + try { + final MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update(s.getBytes()); + return unsignedIntFromBytes(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @VisibleForTesting + static int unsignedIntFromBytes(byte[] longEnoughBytes) { + return (extractByte(longEnoughBytes, 0) + | extractByte(longEnoughBytes, 1) + | extractByte(longEnoughBytes, 2) + | extractByte(longEnoughBytes, 3)) + & 0x7FFF_FFFF; + } + + private static int extractByte(byte[] bytes, int index) { + return (((int) bytes[index]) & 0xFF) << (index * 8); + } + + /** + * @return whether a package should be logged, using a random value based on the ANDROID_ID, + * with a given sampling rate. + */ + public static boolean shouldSamplePackageForAtom(String packageName, float rate) { + if (rate <= 0) { + return false; + } + if (rate >= 1) { + return true; + } + final int hash = getUnsignedHashCached(packageName) ^ getAndroidIdHash(); + + return (((double) hash) / Integer.MAX_VALUE) <= rate; + } +} diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerUtilsTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerUtilsTest.java new file mode 100644 index 000000000000..96103e36ae92 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerUtilsTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.am; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +@SmallTest +public class ActivityManagerUtilsTest { + @Test + public void getAndroidIdHash() { + // getAndroidIdHash() essentially returns a random a value. Just make sure it's + // non-negative. + assertThat(ActivityManagerUtils.getAndroidIdHash()).isAtLeast(0); + } + + @Test + public void getUnsignedHashCached() { + assertThat(ActivityManagerUtils.getUnsignedHashCached("x")).isEqualTo( + ActivityManagerUtils.getUnsignedHashCached("x")); + + assertThat(ActivityManagerUtils.getUnsignedHashCached("x")).isNotEqualTo( + ActivityManagerUtils.getUnsignedHashCached("y")); + } + + @Test + public void shouldSamplePackage_sampleNone() { + final int numTests = 100000; + for (int i = 0; i < numTests; i++) { + assertThat(ActivityManagerUtils.shouldSamplePackageForAtom("" + i, 0)) + .isFalse(); + } + } + + @Test + public void shouldSamplePackage_sampleAll() { + final int numTests = 100000; + + for (int i = 0; i < numTests; i++) { + assertThat(ActivityManagerUtils.shouldSamplePackageForAtom("" + i, 1)) + .isTrue(); + } + } + + /** + * Make sure, with the same android ID, an expected rate of the packages are selected. + */ + @Test + public void shouldSamplePackage_sampleSome_fixedAndroidId() { + checkShouldSamplePackage_fixedAndroidId(0.1f); + checkShouldSamplePackage_fixedAndroidId(0.5f); + checkShouldSamplePackage_fixedAndroidId(0.9f); + } + + /** + * Make sure, the same package is selected on an expected rate of the devices. + */ + @Test + public void shouldSamplePackage_sampleSome_fixedPackage() { + checkShouldSamplePackage_fixedPackage(0.1f); + checkShouldSamplePackage_fixedPackage(0.5f); + checkShouldSamplePackage_fixedPackage(0.9f); + } + + private void checkShouldSamplePackage_fixedPackage(float sampleRate) { + checkShouldSamplePackage(sampleRate, sampleRate, true, false); + } + + private void checkShouldSamplePackage_fixedAndroidId(float sampleRate) { + checkShouldSamplePackage(sampleRate, sampleRate, false, true); + } + + @Test + public void testSheckShouldSamplePackage() { + // Just make sure checkShouldSamplePackage is actually working... + try { + checkShouldSamplePackage(0.3f, 0.6f, false, true); + fail(); + } catch (AssertionError expected) { + } + try { + checkShouldSamplePackage(0.6f, 0.3f, true, false); + fail(); + } catch (AssertionError expected) { + } + } + + private void checkShouldSamplePackage(float inputSampleRate, float expectedRate, + boolean fixedPackage, boolean fixedAndroidId) { + final int numTests = 100000; + + try { + int numSampled = 0; + for (int i = 0; i < numTests; i++) { + final String pkg = fixedPackage ? "fixed-package" : "" + i; + ActivityManagerUtils.injectAndroidIdForTest( + fixedAndroidId ? "fixed-android-id" : "" + i); + + if (ActivityManagerUtils.shouldSamplePackageForAtom(pkg, inputSampleRate)) { + numSampled++; + } + assertThat(ActivityManagerUtils.getUnsignedHashCached(pkg)).isEqualTo( + ActivityManagerUtils.getUnsignedHashCached(pkg)); + } + final double actualSampleRate = ((double) numSampled) / numTests; + + assertThat(actualSampleRate).isWithin(0.05).of(expectedRate); + } finally { + ActivityManagerUtils.injectAndroidIdForTest(null); + } + } +} |