1 /* 2 * Copyright (C) 2025 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 package android.platform.test.ravenwood; 17 18 import com.android.ravenwood.RavenwoodRuntimeNative; 19 20 import java.io.PrintStream; 21 import java.util.HashSet; 22 import java.util.Objects; 23 24 /** 25 * Provides a method call hook that prints almost all (see below) the framework methods being 26 * called with indentation. 27 * 28 * We don't log methods that are trivial, uninteresting, or would be too noisy. 29 * e.g. we don't want to log any logging related methods, or collection APIs. 30 * 31 */ 32 public class RavenwoodMethodCallLogger { RavenwoodMethodCallLogger()33 private RavenwoodMethodCallLogger() { 34 } 35 36 /** We don't want to log anything before ravenwood is initialized. This flag controls it.*/ 37 private static volatile boolean sEnabled = false; 38 39 private static volatile PrintStream sOut = System.out; 40 41 /** Return the current thread's call nest level. */ getNestLevel()42 private static int getNestLevel() { 43 return Thread.currentThread().getStackTrace().length; 44 } 45 46 private static class ThreadInfo { 47 /** 48 * We save the current thread's nest call level here and use that as the initial level. 49 * We do it because otherwise the nest level would be too deep by the time test 50 * starts. 51 */ 52 public final int mInitialNestLevel = getNestLevel(); 53 54 /** 55 * A nest level where shouldLog() returned false. 56 * Once it's set, we ignore all calls deeper than this. 57 */ 58 public int mDisabledNestLevel = Integer.MAX_VALUE; 59 } 60 61 private static final ThreadLocal<ThreadInfo> sThreadInfo = new ThreadLocal<>() { 62 @Override 63 protected ThreadInfo initialValue() { 64 return new ThreadInfo(); 65 } 66 }; 67 68 /** Classes that should be logged. Uses a map for fast lookup. */ 69 private static final HashSet<Class> sIgnoreClasses = new HashSet<>(); 70 static { 71 // The following classes are not interesting... 72 sIgnoreClasses.add(android.util.Log.class); 73 sIgnoreClasses.add(android.util.Slog.class); 74 sIgnoreClasses.add(android.util.EventLog.class); 75 sIgnoreClasses.add(android.util.TimingsTraceLog.class); 76 77 sIgnoreClasses.add(android.util.SparseArray.class); 78 sIgnoreClasses.add(android.util.SparseIntArray.class); 79 sIgnoreClasses.add(android.util.SparseLongArray.class); 80 sIgnoreClasses.add(android.util.SparseBooleanArray.class); 81 sIgnoreClasses.add(android.util.SparseDoubleArray.class); 82 sIgnoreClasses.add(android.util.SparseSetArray.class); 83 sIgnoreClasses.add(android.util.SparseArrayMap.class); 84 sIgnoreClasses.add(android.util.LongSparseArray.class); 85 sIgnoreClasses.add(android.util.LongSparseLongArray.class); 86 sIgnoreClasses.add(android.util.LongArray.class); 87 88 sIgnoreClasses.add(android.text.FontConfig.class); 89 90 sIgnoreClasses.add(android.os.SystemClock.class); 91 sIgnoreClasses.add(android.os.Trace.class); 92 sIgnoreClasses.add(android.os.LocaleList.class); 93 sIgnoreClasses.add(android.os.Build.class); 94 sIgnoreClasses.add(android.os.SystemProperties.class); 95 96 sIgnoreClasses.add(com.android.internal.util.Preconditions.class); 97 98 sIgnoreClasses.add(android.graphics.FontListParser.class); 99 sIgnoreClasses.add(android.graphics.ColorSpace.class); 100 101 sIgnoreClasses.add(android.graphics.fonts.FontStyle.class); 102 sIgnoreClasses.add(android.graphics.fonts.FontVariationAxis.class); 103 104 sIgnoreClasses.add(com.android.internal.compat.CompatibilityChangeInfo.class); 105 sIgnoreClasses.add(com.android.internal.os.LoggingPrintStream.class); 106 107 sIgnoreClasses.add(android.os.ThreadLocalWorkSource.class); 108 109 // Following classes *may* be interesting for some purposes, but the initialization is 110 // too noisy... 111 sIgnoreClasses.add(android.graphics.fonts.SystemFonts.class); 112 113 } 114 115 /** 116 * Return if a class should be ignored. Uses {link #sIgnoreCladsses}, but 117 * we ignore more classes. 118 */ shouldIgnoreClass(Class<?> clazz)119 private static boolean shouldIgnoreClass(Class<?> clazz) { 120 if (sIgnoreClasses.contains(clazz)) { 121 return true; 122 } 123 // Let's also ignore collection-ish classes in android.util. 124 if (java.util.Collection.class.isAssignableFrom(clazz) 125 || java.util.Map.class.isAssignableFrom(clazz) 126 ) { 127 if ("android.util".equals(clazz.getPackageName())) { 128 return true; 129 } 130 return false; 131 } 132 133 switch (clazz.getSimpleName()) { 134 case "EventLogTags": 135 return false; 136 } 137 138 // Following are classes that can't be referred to here directly. 139 // e.g. AndroidPrintStream is package-private, so we can't use its "class" here. 140 switch (clazz.getName()) { 141 case "com.android.internal.os.AndroidPrintStream": 142 return false; 143 } 144 return false; 145 } 146 shouldLog( Class<?> methodClass, String methodName, @SuppressWarnings("UnusedVariable") String methodDescriptor )147 private static boolean shouldLog( 148 Class<?> methodClass, 149 String methodName, 150 @SuppressWarnings("UnusedVariable") String methodDescriptor 151 ) { 152 // Should we ignore this class? 153 if (shouldIgnoreClass(methodClass)) { 154 return false; 155 } 156 // Is it a nested class in a class that should be ignored? 157 var host = methodClass.getNestHost(); 158 if (host != methodClass && shouldIgnoreClass(host)) { 159 return false; 160 } 161 162 var className = methodClass.getName(); 163 164 // Ad-hoc ignore list. They'd be too noisy. 165 if ("create".equals(methodName) 166 // We may apply jarjar, so use endsWith(). 167 && className.endsWith("com.android.server.compat.CompatConfig")) { 168 return false; 169 } 170 171 var pkg = methodClass.getPackageName(); 172 if (pkg.startsWith("android.icu")) { 173 return false; 174 } 175 176 return true; 177 } 178 179 /** 180 * Call this to enable logging. 181 */ enable(PrintStream out)182 public static void enable(PrintStream out) { 183 sEnabled = true; 184 sOut = Objects.requireNonNull(out); 185 186 // It's called from the test thread (Java's main thread). Because we're already 187 // in deep nest calls, we initialize the initial nest level here. 188 sThreadInfo.get(); 189 } 190 191 /** Actual method hook entry point.*/ logMethodCall( Class<?> methodClass, String methodName, String methodDescriptor )192 public static void logMethodCall( 193 Class<?> methodClass, 194 String methodName, 195 String methodDescriptor 196 ) { 197 if (!sEnabled) { 198 return; 199 } 200 final var ti = sThreadInfo.get(); 201 final int nestLevel = getNestLevel() - ti.mInitialNestLevel; 202 203 // Once shouldLog() returns false, we just ignore all deeper calls. 204 if (ti.mDisabledNestLevel < nestLevel) { 205 return; // Still ignore. 206 } 207 final boolean shouldLog = shouldLog(methodClass, methodName, methodDescriptor); 208 209 if (!shouldLog) { 210 ti.mDisabledNestLevel = nestLevel; 211 return; 212 } 213 ti.mDisabledNestLevel = Integer.MAX_VALUE; 214 215 var out = sOut; 216 out.print("# ["); 217 out.print(RavenwoodRuntimeNative.gettid()); 218 out.print(": "); 219 out.print(Thread.currentThread().getName()); 220 out.print("]: "); 221 out.print("[@"); 222 out.printf("%2d", nestLevel); 223 out.print("] "); 224 for (int i = 0; i < nestLevel; i++) { 225 out.print(" "); 226 } 227 out.println(methodClass.getName() + "." + methodName + methodDescriptor); 228 } 229 } 230