Loading core/java/android/view/ViewRootImpl.java +3 −6 Original line number Diff line number Diff line Loading @@ -3013,7 +3013,7 @@ public final class ViewRootImpl implements ViewParent, */ public void notifyRendererOfExpensiveFrame() { if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.notifyExpensiveFrame(); mAttachInfo.mThreadedRenderer.notifyExpensiveFrameWithRateLimit(null); } } Loading @@ -3023,11 +3023,8 @@ public final class ViewRootImpl implements ViewParent, * @hide */ public void notifyRendererOfExpensiveFrame(String reason) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, reason); try { notifyRendererOfExpensiveFrame(); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.notifyExpensiveFrameWithRateLimit(reason); } } Loading core/tests/coretests/src/android/graphics/HardwareRendererTest.java 0 → 100644 +53 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 android.graphics; import static com.google.common.truth.Truth.assertThat; import android.os.SystemClock; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class HardwareRendererTest { @Test public void testNotifyExpensiveFrameWithRateLimit() { final HardwareRenderer renderer = new HardwareRenderer(); // Expect receiving the callback from rate limiter after notifying renderer int initialNotifyCount = renderer.notifyExpensiveFrameWithRateLimit("testing"); assertThat(initialNotifyCount).isEqualTo(1); // Expect the rate limiter won't allow the burst calls of notifying renderer int currentNotifyCount = initialNotifyCount; for (int i = 0; i < 1000; i++) { currentNotifyCount = renderer.notifyExpensiveFrameWithRateLimit("testing"); } assertThat(currentNotifyCount).isEqualTo(initialNotifyCount); // Expect the rate limiter allows the call of notifying renderer after the timeout SystemClock.sleep(100); currentNotifyCount = renderer.notifyExpensiveFrameWithRateLimit("testing"); assertThat(currentNotifyCount).isGreaterThan(initialNotifyCount); } } graphics/java/android/graphics/HardwareRenderer.java +69 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Trace; import android.util.Log; import android.util.TimeUtils; import android.view.Display; Loading @@ -46,6 +47,8 @@ import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.animation.AnimationUtils; import com.android.internal.util.RateLimitingCache; import java.io.File; import java.io.FileDescriptor; import java.lang.annotation.Retention; Loading Loading @@ -189,6 +192,62 @@ public class HardwareRenderer { private @ActivityInfo.ColorMode int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT; private float mDesiredSdrHdrRatio = 1f; private final NotifyRendererRateLimiter mNotifyRendererWorkLoadRateLimiter = createNotifyRendererRateLimiter(); /** * A class for rate limiting of notifying renderer IPC invocation (e.g. notifyExpensiveFrame) * that allows once per period to appropriate control for bursts of calls. */ private static class NotifyRendererRateLimiter extends RateLimitingCache<Void> { private static final long DEFAULT_NOTIFY_PERIOD_MILLIS = 100; private final @NonNull RateLimitingCache.ValueFetcher<Void> mNotifyRendererRunnable; /** Counts when the rate limiter permits to notify the renderer */ private int mNotifyCount; private @Nullable String mNotifyReason; NotifyRendererRateLimiter(@NonNull RateLimitingCache.ValueFetcher<Void> runnable, long periodMillis) { super(periodMillis); mNotifyRendererRunnable = runnable; } private int notifyIfAllow(String reason) { mNotifyReason = reason; get(mNotifyRendererRunnable); return mNotifyCount; } private void incrementNotifyCount() { mNotifyCount++; } private @Nullable String getNotifyReason() { return mNotifyReason; } } private NotifyRendererRateLimiter getNotifyRendererRateLimiter() { return mNotifyRendererWorkLoadRateLimiter; } private NotifyRendererRateLimiter createNotifyRendererRateLimiter() { return new NotifyRendererRateLimiter(() -> { final String notifyReason = getNotifyRendererRateLimiter().getNotifyReason(); final boolean logForReason = notifyReason != null && !notifyReason.isEmpty(); try { final String traceReason = logForReason ? notifyReason : "notifyExpensiveFrame"; Trace.traceBegin(Trace.TRACE_TAG_VIEW, traceReason); notifyExpensiveFrame(); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } getNotifyRendererRateLimiter().incrementNotifyCount(); return null; }, NotifyRendererRateLimiter.DEFAULT_NOTIFY_PERIOD_MILLIS); } /** * Creates a new instance of a HardwareRenderer. The HardwareRenderer will default * to opaque with no light source configured. Loading Loading @@ -1052,6 +1111,16 @@ public class HardwareRenderer { nNotifyExpensiveFrame(mNativeProxy); } /** * Notifies the hardware renderer from the UI thread about upcoming expensive frames with * rate limiting control. * * @hide */ public int notifyExpensiveFrameWithRateLimit(String reason) { return mNotifyRendererWorkLoadRateLimiter.notifyIfAllow(reason); } /** * b/68769804, b/66945974: For low FPS experiments. * Loading Loading
core/java/android/view/ViewRootImpl.java +3 −6 Original line number Diff line number Diff line Loading @@ -3013,7 +3013,7 @@ public final class ViewRootImpl implements ViewParent, */ public void notifyRendererOfExpensiveFrame() { if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.notifyExpensiveFrame(); mAttachInfo.mThreadedRenderer.notifyExpensiveFrameWithRateLimit(null); } } Loading @@ -3023,11 +3023,8 @@ public final class ViewRootImpl implements ViewParent, * @hide */ public void notifyRendererOfExpensiveFrame(String reason) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, reason); try { notifyRendererOfExpensiveFrame(); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.notifyExpensiveFrameWithRateLimit(reason); } } Loading
core/tests/coretests/src/android/graphics/HardwareRendererTest.java 0 → 100644 +53 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 android.graphics; import static com.google.common.truth.Truth.assertThat; import android.os.SystemClock; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class HardwareRendererTest { @Test public void testNotifyExpensiveFrameWithRateLimit() { final HardwareRenderer renderer = new HardwareRenderer(); // Expect receiving the callback from rate limiter after notifying renderer int initialNotifyCount = renderer.notifyExpensiveFrameWithRateLimit("testing"); assertThat(initialNotifyCount).isEqualTo(1); // Expect the rate limiter won't allow the burst calls of notifying renderer int currentNotifyCount = initialNotifyCount; for (int i = 0; i < 1000; i++) { currentNotifyCount = renderer.notifyExpensiveFrameWithRateLimit("testing"); } assertThat(currentNotifyCount).isEqualTo(initialNotifyCount); // Expect the rate limiter allows the call of notifying renderer after the timeout SystemClock.sleep(100); currentNotifyCount = renderer.notifyExpensiveFrameWithRateLimit("testing"); assertThat(currentNotifyCount).isGreaterThan(initialNotifyCount); } }
graphics/java/android/graphics/HardwareRenderer.java +69 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Trace; import android.util.Log; import android.util.TimeUtils; import android.view.Display; Loading @@ -46,6 +47,8 @@ import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.animation.AnimationUtils; import com.android.internal.util.RateLimitingCache; import java.io.File; import java.io.FileDescriptor; import java.lang.annotation.Retention; Loading Loading @@ -189,6 +192,62 @@ public class HardwareRenderer { private @ActivityInfo.ColorMode int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT; private float mDesiredSdrHdrRatio = 1f; private final NotifyRendererRateLimiter mNotifyRendererWorkLoadRateLimiter = createNotifyRendererRateLimiter(); /** * A class for rate limiting of notifying renderer IPC invocation (e.g. notifyExpensiveFrame) * that allows once per period to appropriate control for bursts of calls. */ private static class NotifyRendererRateLimiter extends RateLimitingCache<Void> { private static final long DEFAULT_NOTIFY_PERIOD_MILLIS = 100; private final @NonNull RateLimitingCache.ValueFetcher<Void> mNotifyRendererRunnable; /** Counts when the rate limiter permits to notify the renderer */ private int mNotifyCount; private @Nullable String mNotifyReason; NotifyRendererRateLimiter(@NonNull RateLimitingCache.ValueFetcher<Void> runnable, long periodMillis) { super(periodMillis); mNotifyRendererRunnable = runnable; } private int notifyIfAllow(String reason) { mNotifyReason = reason; get(mNotifyRendererRunnable); return mNotifyCount; } private void incrementNotifyCount() { mNotifyCount++; } private @Nullable String getNotifyReason() { return mNotifyReason; } } private NotifyRendererRateLimiter getNotifyRendererRateLimiter() { return mNotifyRendererWorkLoadRateLimiter; } private NotifyRendererRateLimiter createNotifyRendererRateLimiter() { return new NotifyRendererRateLimiter(() -> { final String notifyReason = getNotifyRendererRateLimiter().getNotifyReason(); final boolean logForReason = notifyReason != null && !notifyReason.isEmpty(); try { final String traceReason = logForReason ? notifyReason : "notifyExpensiveFrame"; Trace.traceBegin(Trace.TRACE_TAG_VIEW, traceReason); notifyExpensiveFrame(); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } getNotifyRendererRateLimiter().incrementNotifyCount(); return null; }, NotifyRendererRateLimiter.DEFAULT_NOTIFY_PERIOD_MILLIS); } /** * Creates a new instance of a HardwareRenderer. The HardwareRenderer will default * to opaque with no light source configured. Loading Loading @@ -1052,6 +1111,16 @@ public class HardwareRenderer { nNotifyExpensiveFrame(mNativeProxy); } /** * Notifies the hardware renderer from the UI thread about upcoming expensive frames with * rate limiting control. * * @hide */ public int notifyExpensiveFrameWithRateLimit(String reason) { return mNotifyRendererWorkLoadRateLimiter.notifyIfAllow(reason); } /** * b/68769804, b/66945974: For low FPS experiments. * Loading