1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.window;
18 
19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
20 import static android.view.Surface.FRAME_RATE_CATEGORY_DEFAULT;
21 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
22 import static android.view.SurfaceControl.FRAME_RATE_SELECTION_STRATEGY_OVERRIDE_CHILDREN;
23 import static android.view.SurfaceControl.FRAME_RATE_SELECTION_STRATEGY_PROPAGATE;
24 
25 import android.annotation.IntDef;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.content.Context;
29 import android.os.PerformanceHintManager;
30 import android.os.Trace;
31 import android.util.Log;
32 import android.view.Display;
33 import android.view.SurfaceControl;
34 
35 import java.io.PrintWriter;
36 import java.util.ArrayList;
37 import java.util.function.Supplier;
38 
39 /**
40  * A helper class to manage performance related hints for a process.  This helper is used for both
41  * long-lived and transient hints.
42  *
43  * @hide
44  */
45 public class SystemPerformanceHinter {
46     private static final String TAG = "SystemPerformanceHinter";
47 
48     private static final int HINT_NO_OP = 0;
49     // Change app and SF wakeup times to allow sf more time to composite a frame
50     public static final int HINT_SF_EARLY_WAKEUP = 1 << 0;
51     // Force max refresh rate
52     public static final int HINT_SF_FRAME_RATE = 1 << 1;
53     // Boost CPU & GPU clocks
54     public static final int HINT_ADPF = 1 << 2;
55     // Convenience constant for SF only flags
56     public static final int HINT_SF = HINT_SF_EARLY_WAKEUP | HINT_SF_FRAME_RATE;
57     // Convenience constant for all the flags
58     public static final int HINT_ALL = HINT_SF_EARLY_WAKEUP | HINT_SF_FRAME_RATE | HINT_ADPF;
59 
60     // Hints that are applied per-display and require a display root surface
61     private static final int HINT_PER_DISPLAY = HINT_SF_FRAME_RATE;
62     // Hints that are global (not per-display)
63     private static final int HINT_GLOBAL = HINT_SF_EARLY_WAKEUP | HINT_ADPF;
64 
65     @IntDef(prefix = {"HINT_"}, value = {
66             HINT_SF_EARLY_WAKEUP,
67             HINT_SF_FRAME_RATE,
68             HINT_ADPF,
69     })
70     private @interface HintFlags {}
71 
72     /**
73      * A provider for the root to apply SurfaceControl hints which will be inherited by all children
74      * of that root.
75      * @hide
76      */
77     public interface DisplayRootProvider {
78         /**
79          * @return the SurfaceControl to apply hints for the given displayId.
80          */
getRootForDisplay(int displayId)81         @Nullable SurfaceControl getRootForDisplay(int displayId);
82     }
83 
84     /**
85      * A session where high performance is needed.
86      * @hide
87      */
88     public class HighPerfSession implements AutoCloseable {
89         private final @HintFlags int hintFlags;
90         private final String reason;
91         private final int displayId;
92         private String mTraceName;
93 
HighPerfSession(@intFlags int hintFlags, int displayId, @NonNull String reason)94         protected HighPerfSession(@HintFlags int hintFlags, int displayId, @NonNull String reason) {
95             this.hintFlags = hintFlags;
96             this.reason = reason;
97             this.displayId = displayId;
98         }
99 
100         /** Makes this session active. It is no-op if this session is already active. */
start()101         public void start() {
102             if (!mActiveSessions.contains(this)) {
103                 startSession(this);
104             }
105         }
106 
107         /**
108          * Closes this session.
109          */
110         @Override
close()111         public void close() {
112             endSession(this);
113         }
114 
115         @Override
finalize()116         public void finalize() {
117             close();
118         }
119 
asyncTraceBegin()120         boolean asyncTraceBegin() {
121             if (!Trace.isTagEnabled(mTraceTag)) {
122                 mTraceName = null;
123                 return false;
124             }
125             if (mTraceName == null) {
126                 mTraceName = "PerfSession-d" + displayId + "-" + reason;
127             }
128             Trace.asyncTraceForTrackBegin(mTraceTag, TAG, mTraceName,
129                     System.identityHashCode(this));
130             return true;
131         }
132 
asyncTraceEnd()133         boolean asyncTraceEnd() {
134             if (mTraceName == null) {
135                 return false;
136             }
137             Trace.asyncTraceForTrackEnd(mTraceTag, TAG, System.identityHashCode(this));
138             return true;
139         }
140     }
141 
142     /**
143      * A no-op implementation of a session.
144      */
145     private class NoOpHighPerfSession extends HighPerfSession {
NoOpHighPerfSession()146         public NoOpHighPerfSession() {
147             super(HINT_NO_OP, Display.INVALID_DISPLAY, "");
148         }
149 
150         @Override
start()151         public void start() {
152         }
153 
154         @Override
close()155         public void close() {
156             // Do nothing
157         }
158     }
159 
160     /** The tag category of trace. */
161     public long mTraceTag = Trace.TRACE_TAG_APP;
162 
163     // The active sessions
164     private final ArrayList<HighPerfSession> mActiveSessions = new ArrayList<>();
165     private final SurfaceControl.Transaction mTransaction;
166     private final PerformanceHintManager mPerfHintManager;
167     private @Nullable PerformanceHintManager.Session mAdpfSession;
168     private @Nullable DisplayRootProvider mDisplayRootProvider;
169 
170     /**
171      * Constructor for the hinter.
172      * @hide
173      */
SystemPerformanceHinter(@onNull Context context, @Nullable DisplayRootProvider displayRootProvider)174     public SystemPerformanceHinter(@NonNull Context context,
175             @Nullable DisplayRootProvider displayRootProvider) {
176         this(context, displayRootProvider, null /* transactionSupplier */);
177     }
178 
179     /**
180      * Constructor for the hinter.
181      * @hide
182      */
SystemPerformanceHinter(@onNull Context context, @Nullable DisplayRootProvider displayRootProvider, @Nullable Supplier<SurfaceControl.Transaction> transactionSupplier)183     public SystemPerformanceHinter(@NonNull Context context,
184             @Nullable DisplayRootProvider displayRootProvider,
185             @Nullable Supplier<SurfaceControl.Transaction> transactionSupplier) {
186         mDisplayRootProvider = displayRootProvider;
187         mPerfHintManager = context.getSystemService(PerformanceHintManager.class);
188         mTransaction = transactionSupplier != null
189                 ? transactionSupplier.get()
190                 : new SurfaceControl.Transaction();
191     }
192 
193     /**
194      * Sets the current ADPF session, required if you are using HINT_ADPF.  It is the responsibility
195      * of the caller to manage up the ADPF session.
196      * @hide
197      */
setAdpfSession(PerformanceHintManager.Session adpfSession)198     public void setAdpfSession(PerformanceHintManager.Session adpfSession) {
199         mAdpfSession = adpfSession;
200     }
201 
202     /** Creates a session that requires high performance. */
createSession(@intFlags int hintFlags, int displayId, @NonNull String reason)203     public HighPerfSession createSession(@HintFlags int hintFlags, int displayId,
204             @NonNull String reason) {
205         if (hintFlags == HINT_NO_OP) {
206             throw new IllegalArgumentException("Not allow empty hint flags");
207         }
208         if (mDisplayRootProvider == null && (hintFlags & HINT_SF_FRAME_RATE) != 0) {
209             throw new IllegalArgumentException(
210                     "Using SF frame rate hints requires a valid display root provider");
211         }
212         if (mAdpfSession == null && (hintFlags & HINT_ADPF) != 0) {
213             throw new IllegalArgumentException("Using ADPF hints requires an ADPF session");
214         }
215         if ((hintFlags & HINT_PER_DISPLAY) != 0) {
216             if (mDisplayRootProvider.getRootForDisplay(displayId) == null) {
217                 // Just log an error and return early if there is no root as there could be races
218                 // between when a display root is removed and when a hint session is requested
219                 Log.v(TAG, "No display root for displayId=" + displayId);
220                 Trace.instant(TRACE_TAG_WINDOW_MANAGER, "PerfHint-NoDisplayRoot: " + displayId);
221                 return new NoOpHighPerfSession();
222             }
223         }
224         return new HighPerfSession(hintFlags, displayId, reason);
225     }
226 
227     /**
228      * Starts a new session that requires high performance.
229      */
startSession(@intFlags int hintFlags, int displayId, @NonNull String reason)230     public HighPerfSession startSession(@HintFlags int hintFlags, int displayId,
231             @NonNull String reason) {
232         final HighPerfSession session = createSession(hintFlags, displayId, reason);
233         if (session.hintFlags != HINT_NO_OP) {
234             startSession(session);
235         }
236         return session;
237     }
238 
239     /** Starts the session that requires high performance. */
startSession(HighPerfSession session)240     private void startSession(HighPerfSession session) {
241         final boolean isTraceEnabled = session.asyncTraceBegin();
242         int oldGlobalFlags = calculateActiveHintFlags(HINT_GLOBAL);
243         int oldPerDisplayFlags = calculateActiveHintFlagsForDisplay(HINT_PER_DISPLAY,
244                 session.displayId);
245         mActiveSessions.add(session);
246         int newGlobalFlags = calculateActiveHintFlags(HINT_GLOBAL);
247         int newPerDisplayFlags = calculateActiveHintFlagsForDisplay(HINT_PER_DISPLAY,
248                 session.displayId);
249 
250         boolean transactionChanged = false;
251         // Per-display flags
252         if (nowEnabled(oldPerDisplayFlags, newPerDisplayFlags, HINT_SF_FRAME_RATE)) {
253             SurfaceControl displaySurfaceControl = mDisplayRootProvider.getRootForDisplay(
254                     session.displayId);
255             mTransaction.setFrameRateSelectionStrategy(displaySurfaceControl,
256                     FRAME_RATE_SELECTION_STRATEGY_OVERRIDE_CHILDREN);
257             // smoothSwitchOnly is false to request a higher framerate, even if it means switching
258             // the display mode will cause would jank on non-VRR devices because keeping a lower
259             // refresh rate would mean a poorer user experience.
260             mTransaction.setFrameRateCategory(
261                     displaySurfaceControl, FRAME_RATE_CATEGORY_HIGH, /* smoothSwitchOnly= */ false);
262             transactionChanged = true;
263             if (isTraceEnabled) {
264                 asyncTraceBegin(HINT_SF_FRAME_RATE, session.displayId);
265             }
266         }
267 
268         // Global flags
269         if (nowEnabled(oldGlobalFlags, newGlobalFlags, HINT_SF_EARLY_WAKEUP)) {
270             mTransaction.setEarlyWakeupStart();
271             transactionChanged = true;
272             if (isTraceEnabled) {
273                 asyncTraceBegin(HINT_SF_EARLY_WAKEUP, Display.INVALID_DISPLAY);
274             }
275         }
276         if (nowEnabled(oldGlobalFlags, newGlobalFlags, HINT_ADPF)) {
277             mAdpfSession.sendHint(PerformanceHintManager.Session.CPU_LOAD_UP);
278             if (isTraceEnabled) {
279                 asyncTraceBegin(HINT_ADPF, Display.INVALID_DISPLAY);
280             }
281         }
282         if (transactionChanged) {
283             mTransaction.applyAsyncUnsafe();
284         }
285     }
286 
287     /**
288      * Ends a session that requires high performance.
289      */
endSession(HighPerfSession session)290     private void endSession(HighPerfSession session) {
291         final boolean isTraceEnabled = session.asyncTraceEnd();
292         int oldGlobalFlags = calculateActiveHintFlags(HINT_GLOBAL);
293         int oldPerDisplayFlags = calculateActiveHintFlagsForDisplay(HINT_PER_DISPLAY,
294                 session.displayId);
295         mActiveSessions.remove(session);
296         int newGlobalFlags = calculateActiveHintFlags(HINT_GLOBAL);
297         int newPerDisplayFlags = calculateActiveHintFlagsForDisplay(HINT_PER_DISPLAY,
298                 session.displayId);
299 
300         boolean transactionChanged = false;
301         // Per-display flags
302         if (nowDisabled(oldPerDisplayFlags, newPerDisplayFlags, HINT_SF_FRAME_RATE)) {
303             SurfaceControl displaySurfaceControl = mDisplayRootProvider.getRootForDisplay(
304                     session.displayId);
305             mTransaction.setFrameRateSelectionStrategy(displaySurfaceControl,
306                     FRAME_RATE_SELECTION_STRATEGY_PROPAGATE);
307             // smoothSwitchOnly is false to request a higher framerate, even if it means switching
308             // the display mode will cause would jank on non-VRR devices because keeping a lower
309             // refresh rate would mean a poorer user experience.
310             mTransaction.setFrameRateCategory(displaySurfaceControl, FRAME_RATE_CATEGORY_DEFAULT,
311                     /* smoothSwitchOnly= */ false);
312             transactionChanged = true;
313             if (isTraceEnabled) {
314                 asyncTraceEnd(HINT_SF_FRAME_RATE);
315             }
316         }
317 
318         // Global flags
319         if (nowDisabled(oldGlobalFlags, newGlobalFlags, HINT_SF_EARLY_WAKEUP)) {
320             mTransaction.setEarlyWakeupEnd();
321             transactionChanged = true;
322             if (isTraceEnabled) {
323                 asyncTraceEnd(HINT_SF_EARLY_WAKEUP);
324             }
325         }
326         if (nowDisabled(oldGlobalFlags, newGlobalFlags, HINT_ADPF)) {
327             mAdpfSession.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
328             if (isTraceEnabled) {
329                 asyncTraceEnd(HINT_ADPF);
330             }
331         }
332         if (transactionChanged) {
333             mTransaction.applyAsyncUnsafe();
334         }
335     }
336 
337     /**
338      * Checks if checkFlags was previously not set and is now set.
339      */
nowEnabled(@intFlags int oldFlags, @HintFlags int newFlags, @HintFlags int checkFlags)340     private boolean nowEnabled(@HintFlags int oldFlags, @HintFlags int newFlags,
341                                @HintFlags int checkFlags) {
342         return (oldFlags & checkFlags) == 0 && (newFlags & checkFlags) != 0;
343     }
344 
345     /**
346      * Checks if checkFlags was previously set and is now not set.
347      */
nowDisabled(@intFlags int oldFlags, @HintFlags int newFlags, @HintFlags int checkFlags)348     private boolean nowDisabled(@HintFlags int oldFlags, @HintFlags int newFlags,
349                                 @HintFlags int checkFlags) {
350         return (oldFlags & checkFlags) != 0 && (newFlags & checkFlags) == 0;
351     }
352 
353     /**
354      * @return the combined hint flags for all active sessions, filtered by {@param filterFlags}.
355      */
calculateActiveHintFlags(@intFlags int filterFlags)356     private @HintFlags int calculateActiveHintFlags(@HintFlags int filterFlags) {
357         int flags = 0;
358         for (int i = 0; i < mActiveSessions.size(); i++) {
359             flags |= mActiveSessions.get(i).hintFlags & filterFlags;
360         }
361         return flags;
362     }
363 
364     /**
365      * @return the combined hint flags for all active sessions for a given display, filtered by
366      *         {@param filterFlags}.
367      */
calculateActiveHintFlagsForDisplay(@intFlags int filterFlags, int displayId)368     private @HintFlags int calculateActiveHintFlagsForDisplay(@HintFlags int filterFlags,
369             int displayId) {
370         int flags = 0;
371         for (int i = 0; i < mActiveSessions.size(); i++) {
372             final HighPerfSession session = mActiveSessions.get(i);
373             if (session.displayId == displayId) {
374                 flags |= mActiveSessions.get(i).hintFlags & filterFlags;
375             }
376         }
377         return flags;
378     }
379 
asyncTraceBegin(@intFlags int flag, int displayId)380     private void asyncTraceBegin(@HintFlags int flag, int displayId) {
381         final String prefix = switch (flag) {
382             case HINT_SF_EARLY_WAKEUP -> "PerfHint-early_wakeup";
383             case HINT_SF_FRAME_RATE -> "PerfHint-framerate";
384             case HINT_ADPF -> "PerfHint-adpf";
385             default -> "PerfHint-" + flag;
386         };
387         final String name = displayId != Display.INVALID_DISPLAY
388                 ? (prefix + "-d" + displayId) : prefix;
389         Trace.asyncTraceForTrackBegin(mTraceTag, TAG, name,
390                 flag ^ System.identityHashCode(this));
391     }
392 
asyncTraceEnd(@intFlags int flag)393     private void asyncTraceEnd(@HintFlags int flag) {
394         Trace.asyncTraceForTrackEnd(mTraceTag, TAG, flag ^ System.identityHashCode(this));
395     }
396 
397     /**
398      * Dumps the existing sessions.
399      */
dump(PrintWriter pw, String prefix)400     public void dump(PrintWriter pw, String prefix) {
401         final String innerPrefix = prefix + "  ";
402         pw.println(prefix + TAG + ":");
403         pw.println(innerPrefix + "Active sessions (" + mActiveSessions.size() + "):");
404         for (int i = 0; i < mActiveSessions.size(); i++) {
405             final HighPerfSession s = mActiveSessions.get(i);
406             pw.println(innerPrefix + "  reason=" + s.reason
407                     + " flags=" + s.hintFlags
408                     + " display=" + s.displayId);
409         }
410     }
411 }
412