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

Commit b47d056c authored by Jared Duke's avatar Jared Duke
Browse files

Integrate SDK system feature cache behind a flag

Add the SDK system feature cache to the hasSystemFeature query lookup.
This involves populating the initial cache array in system server, and
handing this as a compact int array (<1KB) to each process via
ApplicationSharedMemory.

Currently, the compact array is copied into Java heap for easy and fast
access. Follow-up work will explore reusing the mapped buffer and
relying on JNI and/or MappedByteBuffer for zero-copy access.

Flag: android.content.pm.cache_sdk_system_features
Bug: 326623529
Test: presubmit
Change-Id: I905e82eb7ae3a6714cb3751280b927f8bb379358
parent a5c29f6c
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -101,6 +101,7 @@ import android.content.pm.PermissionInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ProviderInfoList;
import android.content.pm.ServiceInfo;
import android.content.pm.SystemFeaturesCache;
import android.content.res.AssetManager;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
@@ -1341,6 +1342,10 @@ public final class ActivityThread extends ClientTransactionHandler
                ApplicationSharedMemory instance =
                        ApplicationSharedMemory.fromFileDescriptor(
                                applicationSharedMemoryFd, /* mutable= */ false);
                if (android.content.pm.Flags.cacheSdkSystemFeatures()) {
                    SystemFeaturesCache.setInstance(
                            new SystemFeaturesCache(instance.readSystemFeaturesCache()));
                }
                instance.closeFileDescriptor();
                ApplicationSharedMemory.setInstance(instance);
            }
+19 −15
Original line number Diff line number Diff line
@@ -79,6 +79,7 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.SuspendDialogInfo;
import android.content.pm.SystemFeaturesCache;
import android.content.pm.VerifierDeviceIdentity;
import android.content.pm.VersionedPackage;
import android.content.pm.dex.ArtManager;
@@ -804,16 +805,6 @@ public class ApplicationPackageManager extends PackageManager {
                @Override
                public Boolean recompute(HasSystemFeatureQuery query) {
                    try {
                        // As an optimization, check first to see if the feature was defined at
                        // compile-time as either available or unavailable.
                        // TODO(b/203143243): Consider hoisting this optimization out of the cache
                        // after the trunk stable (build) flag has soaked and more features are
                        // defined at compile-time.
                        Boolean maybeHasSystemFeature =
                                RoSystemFeatures.maybeHasFeature(query.name, query.version);
                        if (maybeHasSystemFeature != null) {
                            return maybeHasSystemFeature.booleanValue();
                        }
                        return ActivityThread.currentActivityThread().getPackageManager().
                            hasSystemFeature(query.name, query.version);
                    } catch (RemoteException e) {
@@ -824,12 +815,25 @@ public class ApplicationPackageManager extends PackageManager {

    @Override
    public boolean hasSystemFeature(String name, int version) {
        return mHasSystemFeatureCache.query(new HasSystemFeatureQuery(name, version));
        // We check for system features in the following order:
        //    * Build time-defined system features (constant, very efficient)
        //    * SDK-defined system features (cached at process start, very efficient)
        //    * IPC-retrieved system features (lazily cached, requires per-feature IPC)
        // TODO(b/375000483): Refactor all of this logic, including flag queries, into
        // the SystemFeaturesCache class after initial rollout and validation.
        Boolean maybeHasSystemFeature = RoSystemFeatures.maybeHasFeature(name, version);
        if (maybeHasSystemFeature != null) {
            return maybeHasSystemFeature;
        }

    /** @hide */
    public void disableHasSystemFeatureCache() {
        mHasSystemFeatureCache.disableLocal();
        if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()
                && android.content.pm.Flags.cacheSdkSystemFeatures()) {
            maybeHasSystemFeature =
                    SystemFeaturesCache.getInstance().maybeHasFeature(name, version);
            if (maybeHasSystemFeature != null) {
                return maybeHasSystemFeature;
            }
        }
        return mHasSystemFeatureCache.query(new HasSystemFeatureQuery(name, version));
    }

    /** @hide */
+57 −38
Original line number Diff line number Diff line
@@ -16,9 +16,8 @@

package android.content.pm;

import android.annotation.MainThread;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;

import com.android.internal.annotations.VisibleForTesting;
@@ -35,15 +34,52 @@ import java.util.Collection;
 *
 * @hide
 */
public final class SystemFeaturesCache implements Parcelable {
public final class SystemFeaturesCache {

    // Sentinel value used for SDK-declared features that are unavailable on the current device.
    private static final int UNAVAILABLE_FEATURE_VERSION = Integer.MIN_VALUE;

    // This will be initialized just once, from the process main thread, but ready from any thread.
    private static volatile SystemFeaturesCache sInstance;

    // An array of versions for SDK-defined features, from [0, PackageManager.SDK_FEATURE_COUNT).
    @NonNull
    private final int[] mSdkFeatureVersions;

    /**
     * Installs the process-global cache instance.
     *
     * <p>Note: Usage should be gated on android.content.pm.Flags.cacheSdkSystemFeature(). In
     * practice, this should only be called from 1) SystemServer init, or 2) bindApplication.
     */
    @MainThread
    public static void setInstance(SystemFeaturesCache instance) {
        if (sInstance != null) {
            throw new IllegalStateException("SystemFeaturesCache instance already initialized.");
        }
        sInstance = instance;
    }

    /**
     * Gets the process-global cache instance.
     *
     * Note: Usage should be gated on android.content.pm.Flags.cacheSdkSystemFeature(), and should
     * always occur after the instance has been installed early in the process lifecycle.
     */
    public static @NonNull SystemFeaturesCache getInstance() {
        SystemFeaturesCache instance = sInstance;
        if (instance == null) {
            throw new IllegalStateException("SystemFeaturesCache not initialized");
        }
        return instance;
    }

    /** Clears the process-global cache instance for testing. */
    @VisibleForTesting
    public static void clearInstance() {
        sInstance = null;
    }

    /**
     * Populates the cache from the set of all available {@link FeatureInfo} definitions.
     *
@@ -69,20 +105,28 @@ public final class SystemFeaturesCache implements Parcelable {
        }
    }

    /** Only used by @{code CREATOR.createFromParcel(...)} */
    private SystemFeaturesCache(@NonNull Parcel parcel) {
        final int[] featureVersions = parcel.createIntArray();
        if (featureVersions == null) {
            throw new IllegalArgumentException(
                    "Parceled SDK feature versions should never be null");
        }
        if (featureVersions.length != PackageManager.SDK_FEATURE_COUNT) {
    /**
     * Populates the cache from an array of SDK feature versions originally obtained via {@link
     * #getSdkFeatureVersions()} from another instance.
     */
    public SystemFeaturesCache(@NonNull int[] sdkFeatureVersions) {
        if (sdkFeatureVersions.length != PackageManager.SDK_FEATURE_COUNT) {
            throw new IllegalArgumentException(
                    String.format(
                            "Unexpected cached SDK feature count: %d (expected %d)",
                            featureVersions.length, PackageManager.SDK_FEATURE_COUNT));
                            sdkFeatureVersions.length, PackageManager.SDK_FEATURE_COUNT));
        }
        mSdkFeatureVersions = featureVersions;
        mSdkFeatureVersions = sdkFeatureVersions;
    }

    /**
     * Gets the raw cached feature versions.
     *
     * <p>Note: This should generally only be neded for (de)serialization purposes.
     */
    // TODO(b/375000483): Consider reusing the ApplicationSharedMemory mapping for version lookup.
    public int[] getSdkFeatureVersions() {
        return mSdkFeatureVersions;
    }

    /**
@@ -105,29 +149,4 @@ public final class SystemFeaturesCache implements Parcelable {

        return mSdkFeatureVersions[sdkFeatureIndex] >= version;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel parcel, int flags) {
        parcel.writeIntArray(mSdkFeatureVersions);
    }

    @NonNull
    public static final Parcelable.Creator<SystemFeaturesCache> CREATOR =
            new Parcelable.Creator<SystemFeaturesCache>() {

                @Override
                public SystemFeaturesCache createFromParcel(Parcel parcel) {
                    return new SystemFeaturesCache(parcel);
                }

                @Override
                public SystemFeaturesCache[] newArray(int size) {
                    return new SystemFeaturesCache[size];
                }
            };
}
+32 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.internal.os;

import android.annotation.NonNull;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import dalvik.annotation.optimization.CriticalNative;
@@ -324,4 +325,35 @@ public class ApplicationSharedMemory implements AutoCloseable {
     */
    @FastNative
    private static native long nativeGetSystemNonceBlock(long ptr);

    /**
     * Perform a one-time write of cached SDK feature versions.
     *
     * @throws IllegalStateException if the feature versions have already been written or the ashmem
     *     is immutable.
     * @throws IllegalArgumentException if the provided feature version array is too large.
     */
    public void writeSystemFeaturesCache(@NonNull int[] featureVersions) {
        checkMutable();
        nativeWriteSystemFeaturesCache(mPtr, featureVersions);
    }

    /**
     * Read the cached SDK feature versions previously written to shared memory.
     *
     * Note: The result should generally be cached elsewhere for global reuse.
     */
    // TODO(b/326623529): Consider using a MappedByteBuffer or equivalent to avoid needing a
    // Java copy of the cached data for potentially frequent reads. Alternatively, the JNI query
    // lookup for a given feature could be cheap enough to avoid the cached Java copy entirely.
    public @NonNull int[] readSystemFeaturesCache() {
        checkMapped();
        return nativeReadSystemFeaturesCache(mPtr);
    }

    @FastNative
    private static native void nativeWriteSystemFeaturesCache(long ptr, int[] cache);

    @FastNative
    private static native int[] nativeReadSystemFeaturesCache(long ptr);
}
+77 −5
Original line number Diff line number Diff line
@@ -23,18 +23,67 @@
#include <string.h>
#include <sys/mman.h>

#include <array>
#include <atomic>
#include <cstddef>
#include <new>

#include "core_jni_helpers.h"

#include "android_app_PropertyInvalidatedCache.h"
#include "core_jni_helpers.h"

namespace {

using namespace android::app::PropertyInvalidatedCache;

class alignas(8) SystemFeaturesCache {
public:
    // We only need enough space to handle the official set of SDK-defined system features (~200).
    // TODO(b/326623529): Reuse the exact value defined by PackageManager.SDK_FEATURE_COUNT.
    static constexpr int32_t kMaxSystemFeatures = 512;

    void writeSystemFeatures(JNIEnv* env, jintArray jfeatures) {
        if (featuresLength.load(std::memory_order_seq_cst) > 0) {
            jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
                                 "SystemFeaturesCache already written.");
            return;
        }

        int32_t jfeaturesLength = env->GetArrayLength(jfeatures);
        if (jfeaturesLength > kMaxSystemFeatures) {
            jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
                                 "SystemFeaturesCache only supports %d elements (vs %d requested).",
                                 kMaxSystemFeatures, jfeaturesLength);
            return;
        }
        env->GetIntArrayRegion(jfeatures, 0, jfeaturesLength, features.data());
        featuresLength.store(jfeaturesLength, std::memory_order_seq_cst);
    }

    jintArray readSystemFeatures(JNIEnv* env) const {
        jint jfeaturesLength = static_cast<jint>(featuresLength.load(std::memory_order_seq_cst));
        jintArray jfeatures = env->NewIntArray(jfeaturesLength);
        if (env->ExceptionCheck()) {
            return nullptr;
        }

        env->SetIntArrayRegion(jfeatures, 0, jfeaturesLength, features.data());
        return jfeatures;
    }

private:
    // A fixed length array of feature versions, with |featuresLength| dictating the actual size
    // of features that have been written.
    std::array<int32_t, kMaxSystemFeatures> features = {};
    // The atomic acts as a barrier that precedes reads and follows writes, ensuring a
    // consistent view of |features| across processes. Note that r/w synchronization *within* a
    // process is handled at a higher level.
    std::atomic<int64_t> featuresLength = 0;
};

static_assert(sizeof(SystemFeaturesCache) ==
                      sizeof(int32_t) * SystemFeaturesCache::kMaxSystemFeatures + sizeof(int64_t),
              "Unexpected SystemFeaturesCache size");

// Atomics should be safe to use across processes if they are lock free.
static_assert(std::atomic<int64_t>::is_always_lock_free == true,
              "atomic<int64_t> is not always lock free");
@@ -69,14 +118,25 @@ public:
        latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis = offset;
    }

    // The fixed size cache storage for SDK-defined system features.
    SystemFeaturesCache systemFeaturesCache;

    // The nonce storage for pic.  The sizing is suitable for the system server module.
    SystemCacheNonce systemPic;
};

// Update the expected value when modifying the members of SharedMemory.
// Update the expected values when modifying the members of SharedMemory.
// The goal of this assertion is to ensure that the data structure is the same size across 32-bit
// and 64-bit systems.
static_assert(sizeof(SharedMemory) == 8 + sizeof(SystemCacheNonce), "Unexpected SharedMemory size");
// TODO(b/396674280): Add an additional fixed size check for SystemCacheNonce after resolving
// ABI discrepancies.
static_assert(sizeof(SharedMemory) == 8 + sizeof(SystemFeaturesCache) + sizeof(SystemCacheNonce),
              "Unexpected SharedMemory size");
static_assert(offsetof(SharedMemory, systemFeaturesCache) == sizeof(int64_t),
              "Unexpected SystemFeaturesCache offset in SharedMemory");
static_assert(offsetof(SharedMemory, systemPic) ==
                      offsetof(SharedMemory, systemFeaturesCache) + sizeof(SystemFeaturesCache),
              "Unexpected SystemCachceNonce offset in SharedMemory");

static jint nativeCreate(JNIEnv* env, jclass) {
    // Create anonymous shared memory region
@@ -146,6 +206,16 @@ static jlong nativeGetSystemNonceBlock(JNIEnv*, jclass*, jlong ptr) {
    return reinterpret_cast<jlong>(&sharedMemory->systemPic);
}

static void nativeWriteSystemFeaturesCache(JNIEnv* env, jclass*, jlong ptr, jintArray jfeatures) {
    SharedMemory* sharedMemory = reinterpret_cast<SharedMemory*>(ptr);
    sharedMemory->systemFeaturesCache.writeSystemFeatures(env, jfeatures);
}

static jintArray nativeReadSystemFeaturesCache(JNIEnv* env, jclass*, jlong ptr) {
    SharedMemory* sharedMemory = reinterpret_cast<SharedMemory*>(ptr);
    return sharedMemory->systemFeaturesCache.readSystemFeatures(env);
}

static const JNINativeMethod gMethods[] = {
        {"nativeCreate", "()I", (void*)nativeCreate},
        {"nativeMap", "(IZ)J", (void*)nativeMap},
@@ -157,6 +227,8 @@ static const JNINativeMethod gMethods[] = {
        {"nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis", "(J)J",
         (void*)nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis},
        {"nativeGetSystemNonceBlock", "(J)J", (void*)nativeGetSystemNonceBlock},
        {"nativeWriteSystemFeaturesCache", "(J[I)V", (void*)nativeWriteSystemFeaturesCache},
        {"nativeReadSystemFeaturesCache", "(J)[I", (void*)nativeReadSystemFeaturesCache},
};

static const char kApplicationSharedMemoryClassName[] =
Loading