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

Commit 5ed08eb2 authored by Matt Buckley's avatar Matt Buckley Committed by Android (Google) Code Review
Browse files

Merge "Add plumbing for ADPF FMQ" into main

parents 3b0704fd 7f89ab37
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@
package android.os;

import android.os.IHintSession;
import android.hardware.power.ChannelConfig;
import android.hardware.power.SessionConfig;
import android.hardware.power.SessionTag;

@@ -27,6 +28,9 @@ interface IHintManager {
     * Creates a {@link Session} for the given set of threads and associates to a binder token.
     * Returns a config if creation is not supported, and HMS had to use the
     * legacy creation method.
     *
     * Throws UnsupportedOperationException if ADPF is not supported, and IllegalStateException
     * if creation is supported but fails.
     */
    IHintSession createHintSessionWithConfig(in IBinder token, in int[] threadIds,
            in long durationNanos, in SessionTag tag, out @nullable SessionConfig config);
@@ -38,4 +42,12 @@ interface IHintManager {

    void setHintSessionThreads(in IHintSession hintSession, in int[] tids);
    int[] getHintSessionThreadIds(in IHintSession hintSession);

    /**
     * Returns FMQ channel information for the caller, which it associates to a binder token.
     *
     * Throws IllegalStateException if FMQ channel creation fails.
     */
    ChannelConfig getSessionChannel(in IBinder token);
    oneway void closeSessionChannel();
}
+4 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

#define LOG_TAG "PerformanceHintNativeTest"

#include <aidl/android/hardware/power/ChannelConfig.h>
#include <aidl/android/hardware/power/SessionConfig.h>
#include <aidl/android/hardware/power/SessionTag.h>
#include <aidl/android/hardware/power/WorkDuration.h>
@@ -54,6 +55,9 @@ public:
    MOCK_METHOD(ScopedAStatus, getHintSessionThreadIds,
                (const std::shared_ptr<IHintSession>& hintSession, ::std::vector<int32_t>* tids),
                (override));
    MOCK_METHOD(ScopedAStatus, getSessionChannel,
                (const ::ndk::SpAIBinder& in_token, hal::ChannelConfig* _aidl_return), (override));
    MOCK_METHOD(ScopedAStatus, closeSessionChannel, (), (override));
    MOCK_METHOD(SpAIBinder, asBinder, (), (override));
    MOCK_METHOD(bool, isRemote, (), (override));
};
+186 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server.power.hint;

import static android.os.Flags.adpfUseFmqChannel;

import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
import static com.android.server.power.hint.Flags.powerhintThreadCleanup;

@@ -26,6 +28,8 @@ import android.app.ActivityManagerInternal;
import android.app.StatsManager;
import android.app.UidObserver;
import android.content.Context;
import android.hardware.power.ChannelConfig;
import android.hardware.power.IPower;
import android.hardware.power.SessionConfig;
import android.hardware.power.SessionTag;
import android.hardware.power.WorkDuration;
@@ -39,6 +43,7 @@ import android.os.Message;
import android.os.PerformanceHintManager;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -69,6 +74,7 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

@@ -91,9 +97,21 @@ public final class HintManagerService extends SystemService {
    @GuardedBy("mLock")
    private final ArrayMap<Integer, ArrayMap<IBinder, ArraySet<AppHintSession>>> mActiveSessions;

    // Multi-level map storing all the channel binder token death listeners.
    // First level is keyed by the UID of the client process owning the channel.
    // Second level is the tgid of the process, which will often just be size one.
    // Each channel is unique per (tgid, uid) pair, so this map associates each pair with an
    // object that listens for the death notification of the binder token that was provided by
    // that client when it created the channel, so we can detect when the client process dies.
    @GuardedBy("mChannelMapLock")
    private ArrayMap<Integer, TreeMap<Integer, ChannelItem>> mChannelMap;

    /** Lock to protect mActiveSessions and the UidObserver. */
    private final Object mLock = new Object();

    /** Lock to protect mChannelMap. */
    private final Object mChannelMapLock = new Object();

    @GuardedBy("mNonIsolatedTidsLock")
    private final Map<Integer, Set<Long>> mNonIsolatedTids;

@@ -110,6 +128,9 @@ public final class HintManagerService extends SystemService {

    private AtomicBoolean mConfigCreationSupport = new AtomicBoolean(true);

    private final IPower mPowerHal;
    private int mPowerHalVersion;

    private static final String PROPERTY_SF_ENABLE_CPU_HINT = "debug.sf.enable_adpf_cpu_hint";
    private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager";

@@ -131,12 +152,22 @@ public final class HintManagerService extends SystemService {
            mNonIsolatedTids = null;
        }
        mActiveSessions = new ArrayMap<>();
        mChannelMap = new ArrayMap<>();
        mNativeWrapper = injector.createNativeWrapper();
        mNativeWrapper.halInit();
        mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate();
        mUidObserver = new MyUidObserver();
        mAmInternal = Objects.requireNonNull(
                LocalServices.getService(ActivityManagerInternal.class));
        mPowerHal = injector.createIPower();
        mPowerHalVersion = 0;
        if (mPowerHal != null) {
            try {
                mPowerHalVersion = mPowerHal.getInterfaceVersion();
            } catch (RemoteException e) {
                throw new IllegalStateException("Could not contact PowerHAL!", e);
            }
        }
    }

    private ServiceThread createCleanUpThread() {
@@ -151,6 +182,10 @@ public final class HintManagerService extends SystemService {
        NativeWrapper createNativeWrapper() {
            return new NativeWrapper();
        }
        IPower createIPower() {
            return IPower.Stub.asInterface(
                ServiceManager.waitForDeclaredService(IPower.DESCRIPTOR + "/default"));
        }
    }

    private boolean isHalSupported() {
@@ -344,6 +379,16 @@ public final class HintManagerService extends SystemService {
                        }
                    }
                }
                synchronized (mChannelMapLock) {
                    // Clean up the uid's session channels
                    final TreeMap<Integer, ChannelItem> uidMap = mChannelMap.get(uid);
                    if (uidMap != null) {
                        for (Map.Entry<Integer, ChannelItem> entry : uidMap.entrySet()) {
                            entry.getValue().closeChannel();
                        }
                        mChannelMap.remove(uid);
                    }
                }
            });
        }

@@ -383,6 +428,113 @@ public final class HintManagerService extends SystemService {
        }
    }

    /**
     * Creates a channel item in the channel map if one does not exist, then returns
     * the entry in the channel map.
     */
    public ChannelItem getOrCreateMappedChannelItem(int tgid, int uid, IBinder token) {
        synchronized (mChannelMapLock) {
            if (!mChannelMap.containsKey(uid)) {
                mChannelMap.put(uid, new TreeMap<Integer, ChannelItem>());
            }
            TreeMap<Integer, ChannelItem> map = mChannelMap.get(uid);
            if (!map.containsKey(tgid)) {
                ChannelItem item = new ChannelItem(tgid, uid, token);
                item.openChannel();
                map.put(tgid, item);
            }
            return map.get(tgid);
        }
    }

    /**
     * This removes an entry in the binder token callback map when a channel is closed,
     * and unregisters its callbacks.
     */
    public void removeChannelItem(Integer tgid, Integer uid) {
        synchronized (mChannelMapLock) {
            TreeMap<Integer, ChannelItem> map = mChannelMap.get(uid);
            if (map != null) {
                ChannelItem item = map.get(tgid);
                if (item != null) {
                    item.closeChannel();
                    map.remove(tgid);
                }
                if (map.isEmpty()) {
                    mChannelMap.remove(uid);
                }
            }
        }
    }

    /**
     * Manages the lifecycle of a single channel. This includes caching the channel descriptor,
     * receiving binder token death notifications, and handling cleanup on uid termination. There
     * can only be one ChannelItem per (tgid, uid) pair in mChannelMap, and channel creation happens
     * when a ChannelItem enters the map, while destruction happens when it leaves the map.
     */
    private class ChannelItem implements IBinder.DeathRecipient {
        @Override
        public void binderDied() {
            removeChannelItem(mTgid, mUid);
        }

        ChannelItem(int tgid, int uid, IBinder token) {
            this.mTgid = tgid;
            this.mUid = uid;
            this.mToken = token;
            this.mLinked = false;
            this.mConfig = null;
        }

        public void closeChannel() {
            if (mLinked) {
                mToken.unlinkToDeath(this, 0);
                mLinked = false;
            }
            if (mConfig != null) {
                try  {
                    mPowerHal.closeSessionChannel(mTgid, mUid);
                } catch (RemoteException e) {
                    throw new IllegalStateException("Failed to close session channel!", e);
                }
                mConfig = null;
            }
        }

        public void openChannel() {
            if (!mLinked) {
                try {
                    mToken.linkToDeath(this, 0);
                } catch (RemoteException e) {
                    throw new IllegalStateException("Client already dead", e);
                }
                mLinked = true;
            }
            if (mConfig == null) {
                try {
                    // This method uses PowerHAL directly through the SDK,
                    // to avoid needing to pass the ChannelConfig through JNI.
                    mConfig = mPowerHal.getSessionChannel(mTgid, mUid);
                } catch (RemoteException e) {
                    removeChannelItem(mTgid, mUid);
                    throw new IllegalStateException("Failed to create session channel!", e);
                }
            }
        }

        ChannelConfig getConfig() {
            return mConfig;
        }

        // To avoid accidental double-linking / unlinking
        boolean mLinked;
        final int mTgid;
        final int mUid;
        final IBinder mToken;
        ChannelConfig mConfig;
    }

    final class CleanUpHandler extends Handler {
        // status of processed tid used for caching
        private static final int TID_NOT_CHECKED = 0;
@@ -570,6 +722,18 @@ public final class HintManagerService extends SystemService {
        return mService;
    }

    @VisibleForTesting
    Boolean hasChannel(int tgid, int uid) {
        synchronized (mChannelMapLock) {
            TreeMap<Integer, ChannelItem> uidMap = mChannelMap.get(uid);
            if (uidMap != null) {
                ChannelItem item = uidMap.get(tgid);
                return item != null;
            }
            return false;
        }
    }

    // returns the first invalid tid or null if not found
    private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) {
        // Make sure all tids belongs to the same UID (including isolated UID),
@@ -709,6 +873,28 @@ public final class HintManagerService extends SystemService {
            }
        }

        @Override
        public ChannelConfig getSessionChannel(IBinder token) {
            if (mPowerHalVersion < 5 || !adpfUseFmqChannel()) {
                return null;
            }
            java.util.Objects.requireNonNull(token);
            final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
            final int callingUid = Binder.getCallingUid();
            ChannelItem item = getOrCreateMappedChannelItem(callingTgid, callingUid, token);
            return item.getConfig();
        };

        @Override
        public void closeSessionChannel() {
            if (mPowerHalVersion < 5 || !adpfUseFmqChannel()) {
                return;
            }
            final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
            final int callingUid = Binder.getCallingUid();
            removeChannelItem(callingTgid, callingUid);
        };

        @Override
        public long getHintSessionPreferredRate() {
            return mHintSessionPreferredRate;
+13 −15
Original line number Diff line number Diff line
@@ -34,11 +34,8 @@

#include "jni.h"

using aidl::android::hardware::power::SessionConfig;
using aidl::android::hardware::power::SessionHint;
using aidl::android::hardware::power::SessionMode;
using aidl::android::hardware::power::SessionTag;
using aidl::android::hardware::power::WorkDuration;
namespace hal = aidl::android::hardware::power;

using android::power::PowerHintSessionWrapper;

namespace android {
@@ -95,10 +92,11 @@ static jlong createHintSession(JNIEnv* env, int32_t tgid, int32_t uid,

static jlong createHintSessionWithConfig(JNIEnv* env, int32_t tgid, int32_t uid,
                                         std::vector<int32_t> threadIds, int64_t durationNanos,
                                         int32_t sessionTag, SessionConfig& config) {
                                         int32_t sessionTag, hal::SessionConfig& config) {
    auto result =
            gPowerHalController.createHintSessionWithConfig(tgid, uid, threadIds, durationNanos,
                                                            static_cast<SessionTag>(sessionTag),
                                                            static_cast<hal::SessionTag>(
                                                                    sessionTag),
                                                            &config);
    if (result.isOk()) {
        jlong session_ptr = reinterpret_cast<jlong>(result.value().get());
@@ -140,12 +138,12 @@ static void updateTargetWorkDuration(int64_t session_ptr, int64_t targetDuration
}

static void reportActualWorkDuration(int64_t session_ptr,
                                     const std::vector<WorkDuration>& actualDurations) {
                                     const std::vector<hal::WorkDuration>& actualDurations) {
    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
    appSession->reportActualWorkDuration(actualDurations);
}

static void sendHint(int64_t session_ptr, SessionHint hint) {
static void sendHint(int64_t session_ptr, hal::SessionHint hint) {
    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
    appSession->sendHint(hint);
}
@@ -155,7 +153,7 @@ static void setThreads(int64_t session_ptr, const std::vector<int32_t>& threadId
    appSession->setThreads(threadIds);
}

static void setMode(int64_t session_ptr, SessionMode mode, bool enabled) {
static void setMode(int64_t session_ptr, hal::SessionMode mode, bool enabled) {
    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
    appSession->setMode(mode, enabled);
}
@@ -189,7 +187,7 @@ static jlong nativeCreateHintSessionWithConfig(JNIEnv* env, jclass /* clazz */,
        return 0;
    }
    std::vector<int32_t> threadIds(tidArray.get(), tidArray.get() + tidArray.size());
    SessionConfig config;
    hal::SessionConfig config;
    jlong out = createHintSessionWithConfig(env, tgid, uid, std::move(threadIds), durationNanos,
                                            sessionTag, config);
    if (out <= 0) {
@@ -223,7 +221,7 @@ static void nativeReportActualWorkDuration(JNIEnv* env, jclass /* clazz */, jlon
    ScopedLongArrayRO arrayActualDurations(env, actualDurations);
    ScopedLongArrayRO arrayTimeStamps(env, timeStamps);

    std::vector<WorkDuration> actualList(arrayActualDurations.size());
    std::vector<hal::WorkDuration> actualList(arrayActualDurations.size());
    for (size_t i = 0; i < arrayActualDurations.size(); i++) {
        actualList[i].timeStampNanos = arrayTimeStamps[i];
        actualList[i].durationNanos = arrayActualDurations[i];
@@ -232,7 +230,7 @@ static void nativeReportActualWorkDuration(JNIEnv* env, jclass /* clazz */, jlon
}

static void nativeSendHint(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jint hint) {
    sendHint(session_ptr, static_cast<SessionHint>(hint));
    sendHint(session_ptr, static_cast<hal::SessionHint>(hint));
}

static void nativeSetThreads(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jintArray tids) {
@@ -244,13 +242,13 @@ static void nativeSetThreads(JNIEnv* env, jclass /* clazz */, jlong session_ptr,

static void nativeSetMode(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jint mode,
                          jboolean enabled) {
    setMode(session_ptr, static_cast<SessionMode>(mode), enabled);
    setMode(session_ptr, static_cast<hal::SessionMode>(mode), enabled);
}

static void nativeReportActualWorkDuration2(JNIEnv* env, jclass /* clazz */, jlong session_ptr,
                                            jobjectArray jWorkDurations) {
    int size = env->GetArrayLength(jWorkDurations);
    std::vector<WorkDuration> workDurations(size);
    std::vector<hal::WorkDuration> workDurations(size);
    for (int i = 0; i < size; i++) {
        jobject workDuration = env->GetObjectArrayElement(jWorkDurations, i);
        workDurations[i].workPeriodStartTimestampNanos =
+153 −0
Original line number Diff line number Diff line
@@ -26,12 +26,15 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -39,9 +42,12 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.content.Context;
import android.hardware.power.ChannelConfig;
import android.hardware.power.IPower;
import android.hardware.power.SessionConfig;
import android.hardware.power.SessionTag;
import android.hardware.power.WorkDuration;
@@ -50,6 +56,7 @@ import android.os.IBinder;
import android.os.IHintSession;
import android.os.PerformanceHintManager;
import android.os.Process;
import android.os.RemoteException;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -130,12 +137,15 @@ public class HintManagerServiceTest {
    @Mock
    private HintManagerService.NativeWrapper mNativeWrapperMock;
    @Mock
    private IPower mIPowerMock;
    @Mock
    private ActivityManagerInternal mAmInternalMock;
    @Rule
    public final CheckFlagsRule mCheckFlagsRule =
            DeviceFlagsValueProvider.createCheckFlagsRule();

    private HintManagerService mService;
    private ChannelConfig mConfig;

    private static Answer<Long> fakeCreateWithConfig(Long ptr, Long sessionId) {
        return new Answer<Long>() {
@@ -149,6 +159,9 @@ public class HintManagerServiceTest {
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mConfig = new ChannelConfig();
        mConfig.readFlagBitmask = 1;
        mConfig.writeFlagBitmask = 2;
        when(mNativeWrapperMock.halGetHintSessionPreferredRate())
                .thenReturn(DEFAULT_HINT_PREFERRED_RATE);
        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
@@ -170,6 +183,8 @@ public class HintManagerServiceTest {
                any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[2],
                    SESSION_IDS[2]));

        when(mIPowerMock.getInterfaceVersion()).thenReturn(5);
        when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig);
        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
        LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
    }
@@ -252,6 +267,9 @@ public class HintManagerServiceTest {
            NativeWrapper createNativeWrapper() {
                return mNativeWrapperMock;
            }
            IPower createIPower() {
                return mIPowerMock;
            }
        });
        return mService;
    }
@@ -261,6 +279,9 @@ public class HintManagerServiceTest {
            NativeWrapper createNativeWrapper() {
                return new NativeWrapperFake();
            }
            IPower createIPower() {
                return mIPowerMock;
            }
        });
        return mService;
    }
@@ -728,6 +749,102 @@ public class HintManagerServiceTest {
        verify(mNativeWrapperMock, never()).halSetMode(anyLong(), anyInt(), anyBoolean());
    }

    @Test
    public void testGetChannel() throws Exception {
        HintManagerService service = createService();
        Binder token = new Binder();

        // Should only call once, after caching the first call
        ChannelConfig config = service.getBinderServiceInstance().getSessionChannel(token);
        ChannelConfig config2 = service.getBinderServiceInstance().getSessionChannel(token);
        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
        assertEquals(config.readFlagBitmask, mConfig.readFlagBitmask);
        assertEquals(config.writeFlagBitmask, mConfig.writeFlagBitmask);
        assertEquals(config2.readFlagBitmask, mConfig.readFlagBitmask);
        assertEquals(config2.writeFlagBitmask, mConfig.writeFlagBitmask);
    }

    @Test
    public void testGetChannelTwice() throws Exception {
        HintManagerService service = createService();
        Binder token = new Binder();

        service.getBinderServiceInstance().getSessionChannel(token);
        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
        service.getBinderServiceInstance().closeSessionChannel();
        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));

        clearInvocations(mIPowerMock);

        service.getBinderServiceInstance().getSessionChannel(token);
        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
        service.getBinderServiceInstance().closeSessionChannel();
        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
    }

    @Test
    public void testGetChannelFails() throws Exception {
        HintManagerService service = createService();
        Binder token = new Binder();

        when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenThrow(RemoteException.class);

        assertThrows(IllegalStateException.class, () -> {
            service.getBinderServiceInstance().getSessionChannel(token);
        });
    }


    @Test
    public void testGetChannelBadVersion() throws Exception {
        when(mIPowerMock.getInterfaceVersion()).thenReturn(3);
        HintManagerService service = createService();
        Binder token = new Binder();

        reset(mIPowerMock);
        when(mIPowerMock.getInterfaceVersion()).thenReturn(3);
        when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig);

        ChannelConfig channel = service.getBinderServiceInstance().getSessionChannel(token);
        verify(mIPowerMock, times(0)).getSessionChannel(eq(TGID), eq(UID));
        assertNull(channel);
    }

    @Test
    public void testCloseChannel() throws Exception {
        HintManagerService service = createService();
        Binder token = new Binder();

        service.getBinderServiceInstance().getSessionChannel(token);
        service.getBinderServiceInstance().closeSessionChannel();
        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
    }

    @Test
    public void testCloseChannelFails() throws Exception {
        HintManagerService service = createService();
        Binder token = new Binder();

        service.getBinderServiceInstance().getSessionChannel(token);

        doThrow(RemoteException.class).when(mIPowerMock).closeSessionChannel(anyInt(), anyInt());

        assertThrows(IllegalStateException.class, () -> {
            service.getBinderServiceInstance().closeSessionChannel();
        });
    }

    @Test
    public void testDoubleClose() throws Exception {
        HintManagerService service = createService();
        Binder token = new Binder();

        service.getBinderServiceInstance().getSessionChannel(token);
        service.getBinderServiceInstance().closeSessionChannel();
        service.getBinderServiceInstance().closeSessionChannel();
        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
    }

    // This test checks that concurrent operations from different threads on IHintService,
    // IHintSession and UidObserver will not cause data race or deadlock. Ideally we should also
    // check the output of threads' reportActualDuration performance to detect lock starvation
@@ -935,4 +1052,40 @@ public class HintManagerServiceTest {
        a.reportActualWorkDuration2(WORK_DURATIONS_FIVE);
        verify(mNativeWrapperMock, never()).halReportActualWorkDuration(anyLong(), any(), any());
    }

    @Test
    public void testChannelDiesWhenTokenDies() throws Exception {
        HintManagerService service = createService();

        class DyingToken extends Binder {
            DeathRecipient mToNotify;
            @Override
            public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
                mToNotify = recipient;
                super.linkToDeath(recipient, flags);
            }

            public void fakeDeath() {
                mToNotify.binderDied();
            }
        }

        DyingToken token = new DyingToken();

        service.getBinderServiceInstance().getSessionChannel(token);
        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
        assertTrue(service.hasChannel(TGID, UID));

        token.fakeDeath();

        assertFalse(service.hasChannel(TGID, UID));
        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));

        clearInvocations(mIPowerMock);

        token = new DyingToken();
        service.getBinderServiceInstance().getSessionChannel(token);
        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
        assertTrue(service.hasChannel(TGID, UID));
    }
}