| /* |
| * Copyright (C) 2023 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.view; |
| |
| import static android.Manifest.permission.READ_FRAME_BUFFER; |
| import static android.content.pm.PackageManager.PERMISSION_GRANTED; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.content.Context; |
| import android.os.Build; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.GcUtils; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Map; |
| import java.util.WeakHashMap; |
| |
| /** |
| * A thread-safe registry used to track surface controls that are active (not yet released) within a |
| * process, to help debug and identify leaks. |
| * @hide |
| */ |
| public class SurfaceControlRegistry { |
| private static final String TAG = "SurfaceControlRegistry"; |
| |
| /** |
| * An interface for processing the registered SurfaceControls when the threshold is exceeded. |
| */ |
| public interface Reporter { |
| /** |
| * Called when the set of layers exceeds the max threshold. This can be called on any |
| * thread, and must be handled synchronously. |
| */ |
| void onMaxLayersExceeded(WeakHashMap<SurfaceControl, Long> surfaceControls, int limit, |
| PrintWriter pw); |
| } |
| |
| /** |
| * The default implementation of the reporter which logs the existing registered surfaces to |
| * logcat. |
| */ |
| private static class DefaultReporter implements Reporter { |
| public void onMaxLayersExceeded(WeakHashMap<SurfaceControl, Long> surfaceControls, |
| int limit, PrintWriter pw) { |
| final long now = SystemClock.elapsedRealtime(); |
| final ArrayList<Map.Entry<SurfaceControl, Long>> entries = new ArrayList<>(); |
| for (Map.Entry<SurfaceControl, Long> entry : surfaceControls.entrySet()) { |
| entries.add(entry); |
| } |
| // Sort entries by time registered when dumping |
| // TODO: Or should it sort by name? |
| entries.sort((o1, o2) -> (int) (o1.getValue() - o2.getValue())); |
| final int size = Math.min(entries.size(), limit); |
| |
| pw.println("SurfaceControlRegistry"); |
| pw.println("----------------------"); |
| pw.println("Listing oldest " + size + " of " + surfaceControls.size()); |
| for (int i = 0; i < size; i++) { |
| final Map.Entry<SurfaceControl, Long> entry = entries.get(i); |
| final SurfaceControl sc = entry.getKey(); |
| if (sc == null) { |
| // Just skip if the key has since been removed from the weak hash map |
| continue; |
| } |
| |
| final long timeRegistered = entry.getValue(); |
| pw.print(" "); |
| pw.print(sc.getName()); |
| pw.print(" (" + sc.getCallsite() + ")"); |
| pw.println(" [" + ((now - timeRegistered) / 1000) + "s ago]"); |
| } |
| } |
| } |
| |
| // The threshold at which to dump information about all the known active SurfaceControls in the |
| // process when the number of layers exceeds a certain count. This should be significantly |
| // smaller than the MAX_LAYERS (currently 4096) defined in SurfaceFlinger.h |
| private static final int MAX_LAYERS_REPORTING_THRESHOLD = 1024; |
| |
| // The threshold at which to reset the dump state. Needs to be smaller than |
| // MAX_LAYERS_REPORTING_THRESHOLD |
| private static final int RESET_REPORTING_THRESHOLD = 256; |
| |
| // Number of surface controls to dump when the max threshold is exceeded |
| private static final int DUMP_LIMIT = 256; |
| |
| // An instance of a registry that is a no-op |
| private static final SurfaceControlRegistry NO_OP_REGISTRY = new NoOpRegistry(); |
| |
| // Static lock, must be held for all registry operations |
| private static final Object sLock = new Object(); |
| |
| // The default reporter for printing out the registered surfaces |
| private static final DefaultReporter sDefaultReporter = new DefaultReporter(); |
| |
| // The registry for a given process |
| private static volatile SurfaceControlRegistry sProcessRegistry; |
| |
| // Whether call stack debugging has been initialized. This is evaluated only once per process |
| // instance when the first SurfaceControl.Transaction object is created |
| static boolean sCallStackDebuggingInitialized; |
| |
| // Whether call stack debugging is currently enabled, ie. whether there is a valid match string |
| // for either a specific surface control name or surface control transaction method |
| static boolean sCallStackDebuggingEnabled; |
| |
| // The name of the surface control to log stack traces for. Always non-null if |
| // sCallStackDebuggingEnabled is true. Can be combined with the match call. |
| private static String sCallStackDebuggingMatchName; |
| |
| // The surface control transaction method name to log stack traces for. Always non-null if |
| // sCallStackDebuggingEnabled is true. Can be combined with the match name. |
| private static String sCallStackDebuggingMatchCall; |
| |
| // Mapping of the active SurfaceControls to the elapsed time when they were registered |
| @GuardedBy("sLock") |
| private final WeakHashMap<SurfaceControl, Long> mSurfaceControls; |
| |
| // The threshold at which we dump information about the current set of registered surfaces. |
| // Once this threshold is reached, we no longer report until the number of layers drops below |
| // mResetReportingThreshold to ensure that we don't spam logcat. |
| private int mMaxLayersReportingThreshold = MAX_LAYERS_REPORTING_THRESHOLD; |
| private int mResetReportingThreshold = RESET_REPORTING_THRESHOLD; |
| |
| // Whether the current set of layers has exceeded mMaxLayersReportingThreshold, and we have |
| // already reported the set of registered surfaces. |
| private boolean mHasReportedExceedingMaxThreshold = false; |
| |
| // The handler for when the registry exceeds the max threshold |
| private Reporter mReporter = sDefaultReporter; |
| |
| private SurfaceControlRegistry() { |
| mSurfaceControls = new WeakHashMap<>(256); |
| } |
| |
| /** |
| * Sets the thresholds at which the registry reports errors. |
| * @param maxLayersReportingThreshold The max threshold (inclusive) |
| * @param resetReportingThreshold The reset threshold (inclusive) |
| * @hide |
| */ |
| @VisibleForTesting |
| public void setReportingThresholds(int maxLayersReportingThreshold, int resetReportingThreshold, |
| Reporter reporter) { |
| synchronized (sLock) { |
| if (maxLayersReportingThreshold <= 0 |
| || resetReportingThreshold >= maxLayersReportingThreshold) { |
| throw new IllegalArgumentException("Expected maxLayersReportingThreshold (" |
| + maxLayersReportingThreshold + ") to be > 0 and resetReportingThreshold (" |
| + resetReportingThreshold + ") to be < maxLayersReportingThreshold"); |
| } |
| if (reporter == null) { |
| throw new IllegalArgumentException("Expected non-null reporter"); |
| } |
| mMaxLayersReportingThreshold = maxLayersReportingThreshold; |
| mResetReportingThreshold = resetReportingThreshold; |
| mHasReportedExceedingMaxThreshold = false; |
| mReporter = reporter; |
| } |
| } |
| |
| @VisibleForTesting |
| public void setCallStackDebuggingParams(String matchName, String matchCall) { |
| sCallStackDebuggingMatchName = matchName.toLowerCase(); |
| sCallStackDebuggingMatchCall = matchCall.toLowerCase(); |
| } |
| |
| /** |
| * Creates and initializes the registry for all SurfaceControls in this process. The caller must |
| * hold the READ_FRAME_BUFFER permission. |
| * @hide |
| */ |
| @RequiresPermission(READ_FRAME_BUFFER) |
| @NonNull |
| public static void createProcessInstance(Context context) { |
| if (context.checkSelfPermission(READ_FRAME_BUFFER) != PERMISSION_GRANTED) { |
| throw new SecurityException("Expected caller to hold READ_FRAME_BUFFER"); |
| } |
| synchronized (sLock) { |
| if (sProcessRegistry == null) { |
| sProcessRegistry = new SurfaceControlRegistry(); |
| } |
| } |
| } |
| |
| /** |
| * Destroys the previously created registry this process. |
| * @hide |
| */ |
| public static void destroyProcessInstance() { |
| synchronized (sLock) { |
| if (sProcessRegistry == null) { |
| return; |
| } |
| sProcessRegistry = null; |
| } |
| } |
| |
| /** |
| * Returns the instance of the registry for this process, only non-null if |
| * createProcessInstance(Context) was previously called from a valid caller. |
| * @hide |
| */ |
| public static SurfaceControlRegistry getProcessInstance() { |
| synchronized (sLock) { |
| return sProcessRegistry != null ? sProcessRegistry : NO_OP_REGISTRY; |
| } |
| } |
| |
| /** |
| * Adds a SurfaceControl to the registry. |
| */ |
| void add(SurfaceControl sc) { |
| synchronized (sLock) { |
| mSurfaceControls.put(sc, SystemClock.elapsedRealtime()); |
| if (!mHasReportedExceedingMaxThreshold |
| && mSurfaceControls.size() >= mMaxLayersReportingThreshold) { |
| // Dump existing info to logcat for debugging purposes (but don't close the |
| // System.out output stream otherwise we can't print to it after this call) |
| PrintWriter pw = new PrintWriter(System.out, true /* autoFlush */); |
| mReporter.onMaxLayersExceeded(mSurfaceControls, DUMP_LIMIT, pw); |
| mHasReportedExceedingMaxThreshold = true; |
| } |
| } |
| } |
| |
| /** |
| * Removes a SurfaceControl from the registry. |
| */ |
| void remove(SurfaceControl sc) { |
| synchronized (sLock) { |
| mSurfaceControls.remove(sc); |
| if (mHasReportedExceedingMaxThreshold |
| && mSurfaceControls.size() <= mResetReportingThreshold) { |
| mHasReportedExceedingMaxThreshold = false; |
| } |
| } |
| } |
| |
| /** |
| * Returns a hash of this registry and is a function of all the active surface controls. This |
| * is useful for testing to determine whether the registry has changed between creating and |
| * destroying new SurfaceControls. |
| */ |
| @Override |
| public int hashCode() { |
| synchronized (sLock) { |
| // Return a hash of the surface controls |
| return mSurfaceControls.keySet().hashCode(); |
| } |
| } |
| |
| /** |
| * Initializes global call stack debugging if this is a debug build and a filter is specified. |
| * This is a no-op if |
| * |
| * Usage: |
| * adb shell setprop persist.wm.debug.sc.tx.log_match_call <call or \"\" to unset> |
| * adb shell setprop persist.wm.debug.sc.tx.log_match_name <name or \"\" to unset> |
| * adb reboot |
| */ |
| final static void initializeCallStackDebugging() { |
| if (sCallStackDebuggingInitialized || !Build.IS_DEBUGGABLE) { |
| // Return early if already initialized or this is not a debug build |
| return; |
| } |
| |
| sCallStackDebuggingInitialized = true; |
| sCallStackDebuggingMatchCall = |
| SystemProperties.get("persist.wm.debug.sc.tx.log_match_call", null) |
| .toLowerCase(); |
| sCallStackDebuggingMatchName = |
| SystemProperties.get("persist.wm.debug.sc.tx.log_match_name", null) |
| .toLowerCase(); |
| // Only enable stack debugging if any of the match filters are set |
| sCallStackDebuggingEnabled = (!sCallStackDebuggingMatchCall.isEmpty() |
| || !sCallStackDebuggingMatchName.isEmpty()); |
| if (sCallStackDebuggingEnabled) { |
| Log.d(TAG, "Enabling transaction call stack debugging:" |
| + " matchCall=" + sCallStackDebuggingMatchCall |
| + " matchName=" + sCallStackDebuggingMatchName); |
| } |
| } |
| |
| /** |
| * Dumps the callstack if it matches the global debug properties. Caller should first verify |
| * {@link #sCallStackDebuggingEnabled} is true. |
| * |
| * @param call the name of the call |
| * @param tx (optional) the transaction associated with this call |
| * @param sc the affected surface |
| * @param details additional details to print with the stack track |
| */ |
| final void checkCallStackDebugging(@NonNull String call, |
| @Nullable SurfaceControl.Transaction tx, @Nullable SurfaceControl sc, |
| @Nullable String details) { |
| if (!sCallStackDebuggingEnabled) { |
| return; |
| } |
| if (!matchesForCallStackDebugging(sc != null ? sc.getName() : null, call)) { |
| return; |
| } |
| final String txMsg = tx != null ? "tx=" + tx.getId() + " ": ""; |
| final String scMsg = sc != null ? " sc=" + sc.getName() + "": ""; |
| final String msg = details != null |
| ? call + " (" + txMsg + scMsg + ") " + details |
| : call + " (" + txMsg + scMsg + ")"; |
| Log.e(TAG, msg, new Throwable()); |
| } |
| |
| /** |
| * Tests whether the given surface control name/method call matches the filters set for the |
| * call stack debugging. |
| */ |
| @VisibleForTesting |
| public final boolean matchesForCallStackDebugging(@Nullable String name, @NonNull String call) { |
| final boolean matchCall = !sCallStackDebuggingMatchCall.isEmpty(); |
| if (matchCall && !sCallStackDebuggingMatchCall.contains(call.toLowerCase())) { |
| // Skip if target call doesn't match requested caller |
| return false; |
| } |
| final boolean matchName = !sCallStackDebuggingMatchName.isEmpty(); |
| if (matchName && (name == null |
| || !sCallStackDebuggingMatchName.contains(name.toLowerCase()))) { |
| // Skip if target surface doesn't match requested surface |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Returns whether call stack debugging is enabled for this process. |
| */ |
| final static boolean isCallStackDebuggingEnabled() { |
| return sCallStackDebuggingEnabled; |
| } |
| |
| /** |
| * Forces the gc and finalizers to run, used prior to dumping to ensure we only dump strongly |
| * referenced surface controls. |
| */ |
| private static void runGcAndFinalizers() { |
| long t = SystemClock.elapsedRealtime(); |
| GcUtils.runGcAndFinalizersSync(); |
| Log.i(TAG, "Ran gc and finalizers (" + (SystemClock.elapsedRealtime() - t) + "ms)"); |
| } |
| |
| /** |
| * Dumps information about the set of SurfaceControls in the registry. |
| * |
| * @param limit the number of layers to report |
| * @param runGc whether to run the GC and finalizers before dumping |
| * @hide |
| */ |
| public static void dump(int limit, boolean runGc, PrintWriter pw) { |
| if (runGc) { |
| // This needs to run outside the lock since finalization isn't synchronous |
| runGcAndFinalizers(); |
| } |
| synchronized (sLock) { |
| if (sProcessRegistry != null) { |
| sDefaultReporter.onMaxLayersExceeded(sProcessRegistry.mSurfaceControls, limit, pw); |
| pw.println("sCallStackDebuggingInitialized=" + sCallStackDebuggingInitialized); |
| pw.println("sCallStackDebuggingEnabled=" + sCallStackDebuggingEnabled); |
| pw.println("sCallStackDebuggingMatchName=" + sCallStackDebuggingMatchName); |
| pw.println("sCallStackDebuggingMatchCall=" + sCallStackDebuggingMatchCall); |
| } |
| } |
| } |
| |
| /** |
| * A no-op implementation of the registry. |
| */ |
| private static class NoOpRegistry extends SurfaceControlRegistry { |
| |
| @Override |
| public void setReportingThresholds(int maxLayersReportingThreshold, |
| int resetReportingThreshold, Reporter reporter) {} |
| |
| @Override |
| void add(SurfaceControl sc) {} |
| |
| @Override |
| void remove(SurfaceControl sc) {} |
| } |
| } |