Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5fac4ec0 authored by Makoto Onuki's avatar Makoto Onuki Committed by Automerger Merge Worker
Browse files

Merge "Add sampling to FGS atom" into sc-dev am: 89f1f81a

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14297074

Change-Id: I503bd31e58864fbb9aa0f63239dec79dec4ce9f6
parents c55583af 89f1f81a
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -5933,6 +5933,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,
+21 −0
Original line number Diff line number Diff line
@@ -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) {
+124 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+130 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}