Merge "Asynchronous notif pipeline evaluation"

This commit is contained in:
Steve Elliott
2022-02-07 01:22:25 +00:00
committed by Android (Google) Code Review
5 changed files with 299 additions and 10 deletions

View File

@@ -0,0 +1,140 @@
/*
* 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)
}
}

View File

@@ -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,
NotifPipelineFlags flags,
ShadeListBuilderLogger logger,
DumpManager dumpManager,
NotificationInteractionTracker interactionTracker
NotifPipelineChoreographer pipelineChoreographer,
NotifPipelineFlags flags,
NotificationInteractionTracker interactionTracker,
ShadeListBuilderLogger logger,
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();
}
}

View File

@@ -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,
})

View File

@@ -0,0 +1,100 @@
/*
* 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()
}
}

View File

@@ -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,
mNotifPipelineFlags,
mLogger,
mDumpManager,
mInteractionTracker
mPipelineChoreographer,
mNotifPipelineFlags,
mInteractionTracker,
mLogger,
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) {