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

Commit a06ea9df authored by Steve Elliott's avatar Steve Elliott
Browse files

Asynchronous notif pipeline evaluation

This CL introduces NotifPipelineChoreographer, which can be used to
"schedule" listener invocations at a later time.

In effect, this replaces the following code:

   foo.addListener { expensive() }
   bar.addListener { expensive() }

With:

   val choreographer: NotifPipelineChoreographer = ...
   choreographer.addOnEvalListener { expensive() }
   foo.addListener { choreographer.schedule() }
   bar.addListener { choreographer.schedule() }

This will coalesce the calls to expensive(), which will be evaluated at
some point in the future when the NotifPipelineChoreographer invokes its
registered OnEvalListeners.

The implementation included is based on android.view.Choreographer,
which will perform a scheduled evaluation at most once per frame, in
conjunction with a 100ms timeout to force evaluation.

The NotifPipelineChoreographer is used to coalesce all evaluations of
ShadeListBuilder.buildList, which includes all Pluggable invalidations
and new emissions from the NotifCollection.

Test: atest, perfetto + unlock phone (transition from keyguard) and
verify ShadeListBuilder.buildList is invoked at most once per frame

Fixes: 217800329

Change-Id: I4be70b495bdda5a32a202a92b9b28f2cff3402c9
parent 135c992b
Loading
Loading
Loading
Loading
+140 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.statusbar.notification.collection

import android.view.Choreographer
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.ListenerSet
import com.android.systemui.util.concurrency.DelayableExecutor
import dagger.Module
import dagger.Provides

/**
 * Choreographs evaluation resulting from multiple asynchronous sources. Specifically, it exposes
 * [schedule], and [addOnEvalListener]; the former will "schedule" an asynchronous invocation of the
 * latter. Multiple invocations of [schedule] before any added listeners are invoked have no effect.
 */
interface NotifPipelineChoreographer {
    /**
     * Schedules all listeners registered with [addOnEvalListener] to be asynchronously executed at
     * some point in the future. The exact timing is up to the implementation.
     */
    fun schedule()

    /** Cancels a pending evaluation triggered by any recent calls to [schedule]. */
    fun cancel()

    /** Adds a listener [Runnable] that will be invoked when the scheduled evaluation occurs. */
    fun addOnEvalListener(onEvalListener: Runnable)

    /** Removes a listener previously registered with [addOnEvalListener]. */
    fun removeOnEvalListener(onEvalListener: Runnable)
}

@Module
object NotifPipelineChoreographerModule {
    @Provides
    @JvmStatic
    @SysUISingleton
    fun provideChoreographer(
        choreographer: Choreographer,
        @Main mainExecutor: DelayableExecutor
    ): NotifPipelineChoreographer = NotifPipelineChoreographerImpl(choreographer, mainExecutor)
}

private const val TIMEOUT_MS: Long = 100

private class NotifPipelineChoreographerImpl(
    private val viewChoreographer: Choreographer,
    private val executor: DelayableExecutor
) : NotifPipelineChoreographer {

    private val listeners = ListenerSet<Runnable>()
    private var timeoutSubscription: Runnable? = null
    private var isScheduled = false

    private val frameCallback = Choreographer.FrameCallback {
        if (isScheduled) {
            isScheduled = false
            timeoutSubscription?.run()
            listeners.forEach { it.run() }
        }
    }

    override fun schedule() {
        if (isScheduled) return
        isScheduled = true
        viewChoreographer.postFrameCallback(frameCallback)
        if (!isScheduled) {
            // Guard against synchronous evaluation of the frame callback.
            return
        }
        timeoutSubscription = executor.executeDelayed(::onTimeout, TIMEOUT_MS)
    }

    override fun cancel() {
        if (!isScheduled) return
        timeoutSubscription?.run()
        viewChoreographer.removeFrameCallback(frameCallback)
    }

    override fun addOnEvalListener(onEvalListener: Runnable) {
        listeners.addIfAbsent(onEvalListener)
    }

    override fun removeOnEvalListener(onEvalListener: Runnable) {
        listeners.remove(onEvalListener)
    }

    private fun onTimeout() {
        if (isScheduled) {
            isScheduled = false
            viewChoreographer.removeFrameCallback(frameCallback)
            listeners.forEach { it.run() }
        }
    }
}

class FakeNotifPipelineChoreographer : NotifPipelineChoreographer {

    var isScheduled = false
    val listeners = ListenerSet<Runnable>()

    fun runIfScheduled() {
        if (isScheduled) {
            isScheduled = false
            listeners.forEach { it.run() }
        }
    }

    override fun schedule() {
        isScheduled = true
    }

    override fun cancel() {
        isScheduled = false
    }

    override fun addOnEvalListener(onEvalListener: Runnable) {
        listeners.addIfAbsent(onEvalListener)
    }

    override fun removeOnEvalListener(onEvalListener: Runnable) {
        listeners.remove(onEvalListener)
    }
}
+9 −5
Original line number Diff line number Diff line
@@ -122,20 +122,23 @@ public class ShadeListBuilder implements Dumpable {

    private List<ListEntry> mReadOnlyNotifList = Collections.unmodifiableList(mNotifList);
    private List<ListEntry> mReadOnlyNewNotifList = Collections.unmodifiableList(mNewNotifList);
    private final NotifPipelineChoreographer mChoreographer;

    @Inject
    public ShadeListBuilder(
            SystemClock systemClock,
            DumpManager dumpManager,
            NotifPipelineChoreographer pipelineChoreographer,
            NotifPipelineFlags flags,
            NotificationInteractionTracker interactionTracker,
            ShadeListBuilderLogger logger,
            DumpManager dumpManager,
            NotificationInteractionTracker interactionTracker
            SystemClock systemClock
    ) {
        Assert.isMainThread();
        mSystemClock = systemClock;
        mLogger = logger;
        mAlwaysLogList = flags.isDevLoggingEnabled();
        mInteractionTracker = interactionTracker;
        mChoreographer = pipelineChoreographer;
        dumpManager.registerDumpable(TAG, this);

        setSectioners(Collections.emptyList());
@@ -148,6 +151,7 @@ public class ShadeListBuilder implements Dumpable {
    public void attach(NotifCollection collection) {
        Assert.isMainThread();
        collection.setBuildListener(mReadyForBuildListener);
        mChoreographer.addOnEvalListener(this::buildList);
    }

    /**
@@ -290,7 +294,7 @@ public class ShadeListBuilder implements Dumpable {

                    mLogger.logOnBuildList();
                    mAllEntries = entries;
                    buildList();
                    mChoreographer.schedule();
                }
            };

@@ -1281,7 +1285,7 @@ public class ShadeListBuilder implements Dumpable {
    private void rebuildListIfBefore(@PipelineState.StateName int state) {
        mPipelineState.requireIsBefore(state);
        if (mPipelineState.is(STATE_IDLE)) {
            buildList();
            mChoreographer.schedule();
        }
    }

+2 −0
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreImpl;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotifPipelineChoreographerModule;
import com.android.systemui.statusbar.notification.collection.coordinator.ShadeEventCoordinator;
import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorsModule;
@@ -103,6 +104,7 @@ import dagger.Provides;
 */
@Module(includes = {
        CoordinatorsModule.class,
        NotifPipelineChoreographerModule.class,
        NotifPanelEventSourceModule.class,
        NotificationSectionHeadersModule.class,
})
+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.statusbar.notification.collection

import android.testing.AndroidTestingRunner
import android.view.Choreographer
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever

@SmallTest
@RunWith(AndroidTestingRunner::class)
class NotifPipelineChoreographerTest : SysuiTestCase() {

    val viewChoreographer: Choreographer = mock()
    val timeoueSubscription: Runnable = mock()
    val executor: DelayableExecutor = mock<DelayableExecutor>().also {
        whenever(it.executeDelayed(any(), anyLong())).thenReturn(timeoueSubscription)
    }

    val pipelineChoreographer: NotifPipelineChoreographer = NotifPipelineChoreographerModule
            .provideChoreographer(viewChoreographer, executor)

    @Test
    fun scheduleThenEvalFrameCallback() {
        // GIVEN a registered eval listener and scheduled choreographer
        var hasEvaluated = false
        pipelineChoreographer.addOnEvalListener {
            hasEvaluated = true
        }
        pipelineChoreographer.schedule()
        val frameCallback: Choreographer.FrameCallback = withArgCaptor {
            verify(viewChoreographer).postFrameCallback(capture())
        }
        // WHEN the choreographer would invoke its callback
        frameCallback.doFrame(0)
        // THEN the choreographer would evaluate, and the timeoutSubscription would have been
        // cancelled
        assertTrue(hasEvaluated)
        verify(timeoueSubscription).run()
    }

    @Test
    fun scheduleThenEvalTimeoutCallback() {
        // GIVEN a registered eval listener and scheduled choreographer
        var hasEvaluated = false
        pipelineChoreographer.addOnEvalListener {
            hasEvaluated = true
        }
        pipelineChoreographer.schedule()
        val frameCallback: Choreographer.FrameCallback = withArgCaptor {
            verify(viewChoreographer).postFrameCallback(capture())
        }
        val runnable: Runnable = withArgCaptor {
            verify(executor).executeDelayed(capture(), anyLong())
        }
        // WHEN the executor would invoke its callback (indicating a timeout)
        runnable.run()
        // THEN the choreographer would evaluate, and the FrameCallback would have been unregistered
        assertTrue(hasEvaluated)
        verify(viewChoreographer).removeFrameCallback(frameCallback)
    }

    @Test
    fun scheduleThenCancel() {
        // GIVEN a scheduled choreographer
        pipelineChoreographer.schedule()
        val frameCallback: Choreographer.FrameCallback = withArgCaptor {
            verify(viewChoreographer).postFrameCallback(capture())
        }
        // WHEN the scheduled run is cancelled
        pipelineChoreographer.cancel()
        // THEN both the FrameCallback is unregistered and the timeout subscription is cancelled.
        verify(viewChoreographer).removeFrameCallback(frameCallback)
        verify(timeoueSubscription).run()
    }
}
 No newline at end of file
+46 −3
Original line number Diff line number Diff line
@@ -111,6 +111,8 @@ public class ShadeListBuilderTest extends SysuiTestCase {

    @Captor private ArgumentCaptor<CollectionReadyForBuildListener> mBuildListenerCaptor;

    private final FakeNotifPipelineChoreographer mPipelineChoreographer =
            new FakeNotifPipelineChoreographer();
    private CollectionReadyForBuildListener mReadyForBuildListener;
    private List<NotificationEntryBuilder> mPendingSet = new ArrayList<>();
    private List<NotificationEntry> mEntrySet = new ArrayList<>();
@@ -127,11 +129,12 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        allowTestableLooperAsMainThread();

        mListBuilder = new ShadeListBuilder(
                mSystemClock,
                mDumpManager,
                mPipelineChoreographer,
                mNotifPipelineFlags,
                mInteractionTracker,
                mLogger,
                mDumpManager,
                mInteractionTracker
                mSystemClock
        );
        mListBuilder.setOnRenderListListener(mOnRenderListListener);

@@ -567,6 +570,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {

        // WHEN the pipeline is kicked off
        mReadyForBuildListener.onBuildList(singletonList(entry));
        mPipelineChoreographer.runIfScheduled();

        // THEN the entry's initialization time is reset
        assertFalse(entry.hasFinishedInitialization());
@@ -1024,26 +1028,38 @@ public class ShadeListBuilderTest extends SysuiTestCase {

        clearInvocations(mOnRenderListListener);
        packageFilter.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        idPromoter.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        section.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        hypeComparator.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        sectionComparator.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        preRenderInvalidator.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());
    }

@@ -1515,6 +1531,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        // WHEN visual stability manager allows group changes again
        mStabilityManager.setAllowGroupChanges(true);
        mStabilityManager.invalidateList();
        mPipelineChoreographer.runIfScheduled();

        // THEN entries are grouped
        verifyBuiltList(
@@ -1553,6 +1570,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        // WHEN section changes are allowed again
        mStabilityManager.setAllowSectionChanges(true);
        mStabilityManager.invalidateList();
        mPipelineChoreographer.runIfScheduled();

        // THEN the section updates
        assertEquals(newSectioner, mEntrySet.get(0).getSection().getSectioner());
@@ -1772,6 +1790,30 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        verifyBuiltList();
    }

    @Test
    public void testMultipleInvalidationsCoalesce() {
        // GIVEN a PreGroupFilter and a FinalizeFilter
        NotifFilter filter1 = new PackageFilter(PACKAGE_5);
        NotifFilter filter2 = new PackageFilter(PACKAGE_0);
        mListBuilder.addPreGroupFilter(filter1);
        mListBuilder.addFinalizeFilter(filter2);

        // WHEN both filters invalidate
        filter1.invalidateList();
        filter2.invalidateList();

        // THEN the pipeline choreographer is scheduled to evaluate, AND the pipeline hasn't
        // actually run.
        assertTrue(mPipelineChoreographer.isScheduled());
        verify(mOnRenderListListener, never()).onRenderList(anyList());

        // WHEN the pipeline choreographer actually runs
        mPipelineChoreographer.runIfScheduled();

        // THEN the pipeline runs
        verify(mOnRenderListListener).onRenderList(anyList());
    }

    @Test
    public void testIsSorted() {
        Comparator<Integer> intCmp = Integer::compare;
@@ -1914,6 +1956,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        }

        mReadyForBuildListener.onBuildList(mEntrySet);
        mPipelineChoreographer.runIfScheduled();
    }

    private void verifyBuiltList(ExpectedEntry ...expectedEntries) {