Add sampling to FGS atom

Only log the FGS atom for a certain set of packages. Each device
will have a random but stable set of apps, determined by the ANDROID_ID.

By default, the sampling rate is 1, so we log all packages.

If the rate is 0.5, we'll long only the half of the packages. Again,
each device will have a random set of the packages, so the same package
will be logged only on half the devices.

Fix: 184867830
Test: atest FrameworksServicesTests:ActivityManagerUtilsTest
Change-Id: I4cac13ec62d9c98b95df57d47d63e725ba1fcfdd
This commit is contained in:
Makoto Onuki
2021-04-23 09:32:16 -07:00
parent f34b066b9e
commit 2c7fdfcfda
4 changed files with 279 additions and 0 deletions

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}