blob: 27c5c727f5b72fb977a4b2338d63315466e0c91a [file] [log] [blame]
/*
* Copyright (C) 2024 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.art;
import static android.os.IBinder.DeathRecipient;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.CloseGuard;
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.ref.Reference;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
/**
* A helper class that caches a reference to artd, to avoid repetitive calls to `waitForService`,
* as the latter is considered expensive.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class ArtdRefCache {
private static final String TAG = ArtManagerLocal.TAG;
// The 15s timeout is arbitrarily picked.
// TODO(jiakaiz): Revisit this based on real CUJs.
@VisibleForTesting public static final long CACHE_TIMEOUT_MS = 15_000;
@Nullable private static ArtdRefCache sInstance = null;
@NonNull private final Injector mInjector;
@NonNull private final Debouncer mDebouncer;
/**
* A lock that guards the <b>reference</b> to artd.
*
* Warning: This lock does not guard artd itself. Do not hold this lock when calling artd as it
* will prevent parallelism.
*/
@NonNull private final Object mLock = new Object();
@GuardedBy("mLock") private int mPinCount = 0;
@GuardedBy("mLock") @Nullable private IArtd mArtd = null;
public ArtdRefCache() {
this(new Injector());
}
@VisibleForTesting
public ArtdRefCache(@NonNull Injector injector) {
mInjector = injector;
mDebouncer = new Debouncer(CACHE_TIMEOUT_MS, mInjector::createScheduledExecutor);
}
@NonNull
public static synchronized ArtdRefCache getInstance() {
if (sInstance == null) {
sInstance = new ArtdRefCache();
}
return sInstance;
}
/**
* Returns a reference to artd, from the cache or created on demand.
*
* If this method is called when there is no pin, it behaves as if a pin is created and
* immediately destroyed.
*/
@NonNull
public IArtd getArtd() {
synchronized (mLock) {
if (mArtd == null) {
IArtd artd = mInjector.getArtd();
try {
// Force clear the cache when the artd instance is dead.
artd.asBinder().linkToDeath(new CacheDeathRecipient(), 0 /* flags */);
// Cache the instance.
mArtd = artd;
} catch (RemoteException e) {
Utils.logArtdException(e);
// Not expected. Let the caller decide what to do with it.
return artd;
}
}
delayedDropIfNoPinLocked();
return mArtd;
}
}
@GuardedBy("mLock")
private void delayedDropIfNoPinLocked() {
if (mPinCount == 0) {
// During the timeout:
// - If there is no more pinning and unpinning, the cache will be dropped.
// - If there are pinnings and unpinnings, and `mPinCount` never reaches 0 again,
// `dropIfNoPin` will be run, but it will not drop the cache.
// - If there are pinnings and unpinnings, and `mPinCount` reaches 0 again,
// `dropIfNoPin` will be debounced.
mDebouncer.maybeRunAsync(this::dropIfNoPin);
}
}
private void dropIfNoPin() {
synchronized (mLock) {
if (mPinCount == 0) {
mArtd = null;
}
}
}
/**
* A scope that pins any reference to artd, either an existing one or one created within the
* scope. The reference is dropped when there is no more pin within {@link #CACHE_TIMEOUT_MS}.
*/
public class Pin implements AutoCloseable {
private final CloseGuard mGuard = new CloseGuard();
private boolean mClosed = false;
public Pin() {
synchronized (mLock) {
mPinCount++;
}
mGuard.open("close");
}
@Override
public void close() {
try {
mGuard.close();
if (!mClosed) {
mClosed = true;
synchronized (mLock) {
mPinCount--;
Utils.check(mPinCount >= 0);
delayedDropIfNoPinLocked();
}
}
} finally {
// This prevents the GC from running the finalizer during the execution of `close`.
Reference.reachabilityFence(this);
}
}
@SuppressWarnings("Finalize") // Follows the recommended pattern for CloseGuard.
protected void finalize() throws Throwable {
try {
mGuard.warnIfOpen();
close();
} finally {
super.finalize();
}
}
}
private class CacheDeathRecipient implements DeathRecipient {
@Override
public void binderDied() {
// Legacy.
}
@Override
public void binderDied(@NonNull IBinder who) {
synchronized (mLock) {
if (mArtd != null && mArtd.asBinder() == who) {
mArtd = null;
}
}
}
}
/** Injector pattern for testing purpose. */
@VisibleForTesting
public static class Injector {
Injector() {
// Call the getters for various dependencies, to ensure correct initialization order.
ArtModuleServiceInitializer.getArtModuleServiceManager();
}
@NonNull
public IArtd getArtd() {
IArtd artd =
IArtd.Stub.asInterface(ArtModuleServiceInitializer.getArtModuleServiceManager()
.getArtdServiceRegisterer()
.waitForService());
if (artd == null) {
throw new IllegalStateException("Unable to connect to artd");
}
return artd;
}
@NonNull
public ScheduledExecutorService createScheduledExecutor() {
return Executors.newScheduledThreadPool(1 /* corePoolSize */);
}
}
}