409 lines
16 KiB
Java
409 lines
16 KiB
Java
/*
|
|
* 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) {}
|
|
}
|
|
}
|