/* * 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 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 surfaceControls, int limit, PrintWriter pw) { final long now = SystemClock.elapsedRealtime(); final ArrayList> entries = new ArrayList<>(); for (Map.Entry 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 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 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 * adb shell setprop persist.wm.debug.sc.tx.log_match_name * 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) { return true; } if (name == null) { return false; } return sCallStackDebuggingMatchName.contains(name.toLowerCase()) || name.toLowerCase().contains(sCallStackDebuggingMatchName); } /** * 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) {} } }