1 /* 2 * Copyright (C) 2017 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 libcore.heapmetrics; 18 19 import com.android.ahat.heapdump.AhatClassObj; 20 import com.android.ahat.heapdump.AhatHeap; 21 import com.android.ahat.heapdump.AhatInstance; 22 import com.android.ahat.heapdump.AhatSnapshot; 23 import com.android.ahat.heapdump.RootType; 24 import com.android.ahat.heapdump.Size; 25 26 import java.util.ArrayDeque; 27 import java.util.Collections; 28 import java.util.HashMap; 29 import java.util.HashSet; 30 import java.util.Map; 31 import java.util.Queue; 32 import java.util.Set; 33 import java.util.function.Predicate; 34 35 /** 36 * Representation of the break-down of a heap dump into categories. 37 */ 38 class HeapCategorization 39 { 40 41 /** 42 * Enumeration of the categories used. 43 */ 44 enum HeapCategory { 45 46 /** 47 * Interned strings that are mostly ASCII alphabetic characters, and have a bit of 48 * whitespace. These are probably human-readable text e.g. error messages. 49 */ 50 INTERNED_STRING_TEXT_ISH("internedStringTextIsh"), 51 52 /** 53 * Interned strings that are mostly non-ASCII alphabetic characters. These are probably ICU 54 * data. 55 */ 56 INTERNED_STRING_UNICODE_ALPHABET_ISH("internedStringUnicodeAlphabetIsh"), 57 58 /** 59 * Interned strings that are don't meet the criterea of {@link #INTERNED_STRING_TEXT_ISH} or 60 * {@link #INTERNED_STRING_UNICODE_ALPHABET_ISH}. These are probably code e.g. a regex. 61 */ 62 INTERNED_STRING_CODE_ISH("internedStringCodeIsh"), 63 64 /** Objects in a {@code android.icu} package, or strongly reachable from such an object. */ 65 PACKAGE_ANDROID_ICU("packageAndroidIcu"), 66 67 /** Objects in a {@code android.util} or {@code com.internal.android.util} package, or 68 * strongly reachable from such an object. */ 69 PACKAGE_ANDROID_UTIL("packageAndroidUtil"), 70 71 /** 72 * Objects in a {@code android} package other than {@code android.icu} or 73 * {@code android.util}, or a {@code com.android.internal} package other than 74 * {@code com.android.internal.util}, or strongly reachable from such an object. Includes 75 * {@code app}, {@code widget}, {@code graphics}, {@code os}, and many more. 76 */ 77 ANDROID_FRAMEWORK("androidFramework"), 78 79 /** 80 * Objects in a {@code java.security}, {@code sun.security}, 81 * {@code com.android.org.conscrypt}, or {@code com.android.org.bouncycastle} package, or 82 * strongly reachable from such an object. 83 */ 84 SECURITY("security"), 85 86 /** 87 * Objects in a {@code com.android.org.conscrypt} package, or strongly reachable from such 88 * an object. 89 */ 90 SECURITY_CONSCRYPT("securityConscrypt"), 91 92 /** 93 * Objects in a {@code com.android.org.bouncycastle} package, or strongly reachable from 94 * such an object. 95 */ 96 SECURITY_BOUNCYCASTLE("securityBouncycastle"), 97 98 /** 99 * Objects in a {@code java.security.keystore} package, or strongly reachable from such an 100 * object. 101 */ 102 SECURITY_KEYSTORE("securityKeystore"), 103 104 /** 105 * Objects in a {@code java}, {@code javax}, {@code sun}, {@code com.sun}, or 106 * {@code libcore} package, and strongly reachable only from such objects (i.e. the entire 107 * reference graph is libcore). Excludes interned strings (which are in {@code java.lang} 108 * and have no references). 109 */ 110 PURE_LIBCORE("pureLibcore"), 111 112 /** 113 * The subset of {@link #PURE_LIBCORE} which is static, rather than instance, state. 114 */ 115 PURE_LIBCORE_STATIC("pureLibcoreStatic"), 116 117 /** 118 * Objects which don't fall into any of the above categories. (N.B. This ensures that every 119 * object is in at least one category, but objects may be in more than one of the above.) 120 */ 121 NONE_OF_THE_ABOVE("noneOfTheAbove"), 122 ; 123 124 private final String metricSuffix; 125 HeapCategory(String metricSuffix)126 HeapCategory(String metricSuffix) { 127 this.metricSuffix = metricSuffix; 128 } 129 130 /** 131 * Returns the name for a metric using the given prefix and a category-specific suffix. 132 */ metricName(String metricPrefix)133 String metricName(String metricPrefix) { 134 return metricPrefix + metricSuffix; 135 } 136 } 137 138 /** 139 * Returns the categorization of the given heap dump, counting the retained sizes on the given 140 * heaps. 141 */ of(AhatSnapshot snapshot, AhatHeap... heaps)142 static HeapCategorization of(AhatSnapshot snapshot, AhatHeap... heaps) { 143 HeapCategorization categorization = new HeapCategorization(snapshot, heaps); 144 categorization.initializeFromSnapshot(); 145 return categorization; 146 } 147 148 private final Map<HeapCategory, Size> sizesByCategory = new HashMap<>(); 149 private final AhatSnapshot snapshot; 150 private final AhatHeap[] heaps; 151 HeapCategorization(AhatSnapshot snapshot, AhatHeap[] heaps)152 private HeapCategorization(AhatSnapshot snapshot, AhatHeap[] heaps) { 153 this.snapshot = snapshot; 154 this.heaps = heaps; 155 } 156 157 /** 158 * Returns an analysis of the configured heap dump, giving the retained sizes on the configured 159 * heaps broken down by category. 160 */ sizesByCategory()161 Map<HeapCategory, Size> sizesByCategory() { 162 return Collections.unmodifiableMap(sizesByCategory); 163 } 164 initializeFromSnapshot()165 private void initializeFromSnapshot() { 166 for (AhatInstance rooted : snapshot.getRooted()) { 167 initializeFromRooted(rooted); 168 } 169 } 170 initializeFromRooted(AhatInstance rooted)171 private void initializeFromRooted(AhatInstance rooted) { 172 int categories = 0; 173 if (isInternedString(rooted)) { 174 HeapCategory category = categorizeInternedString(rooted.asString()); 175 incrementSize(rooted, category); 176 categories++; 177 } 178 179 if (isOwnedByClassMatching(rooted, str -> str.startsWith("android.icu."))) { 180 incrementSize(rooted, HeapCategory.PACKAGE_ANDROID_ICU); 181 categories++; 182 } 183 if (isOwnedByClassMatching(rooted, this::isAndroidUtilClass)) { 184 incrementSize(rooted, HeapCategory.PACKAGE_ANDROID_UTIL); 185 categories++; 186 } 187 if (isOwnedByClassMatching(rooted, this::isAndroidFrameworkClass)) { 188 incrementSize(rooted, HeapCategory.ANDROID_FRAMEWORK); 189 categories++; 190 } 191 if (isOwnedByClassMatching(rooted, this::isSecurityClass)) { 192 incrementSize(rooted, HeapCategory.SECURITY); 193 categories++; 194 } 195 if (isOwnedByClassMatching(rooted, str -> str.startsWith("com.android.org.conscrypt."))) { 196 incrementSize(rooted, HeapCategory.SECURITY_CONSCRYPT); 197 categories++; 198 } 199 if (isOwnedByClassMatching(rooted, str -> str.startsWith("com.android.org.bouncycastle."))) { 200 incrementSize(rooted, HeapCategory.SECURITY_BOUNCYCASTLE); 201 categories++; 202 } 203 if (isOwnedByClassMatching(rooted, str -> str.startsWith("android.security.keystore."))) { 204 incrementSize(rooted, HeapCategory.SECURITY_KEYSTORE); 205 categories++; 206 } 207 208 if (!isInternedString(rooted) && !isOwnedByClassMatching(rooted, c -> !isLibcoreClass(c))) { 209 incrementSize(rooted, HeapCategory.PURE_LIBCORE); 210 categories++; 211 } 212 if (rooted.isClassObj() && isLibcoreClass(rooted.asClassObj().getName())) { 213 incrementSize(rooted, HeapCategory.PURE_LIBCORE_STATIC); 214 categories++; 215 } 216 217 if (categories == 0) { 218 incrementSize(rooted, HeapCategory.NONE_OF_THE_ABOVE); 219 } 220 } 221 isInternedString(AhatInstance instance)222 private static boolean isInternedString(AhatInstance instance) { 223 if (!instance.isRoot()) { 224 return false; 225 } 226 for (RootType rootType : instance.getRootTypes()) { 227 if (rootType.equals(RootType.INTERNED_STRING)) { 228 return true; 229 } 230 } 231 return false; 232 } 233 234 /** 235 * Returns a category for an interned {@link String} with the given value. The categorization is 236 * done based on heuristics tuned through experimentation. 237 */ categorizeInternedString(String string)238 private static HeapCategory categorizeInternedString(String string) { 239 int nonAsciiChars = 0; 240 int alphabeticChars = 0; 241 int whitespaceChars = 0; 242 int totalChars = string.length(); 243 for (int i = 0; i < totalChars; i++) { 244 char c = string.charAt(i); 245 if (c > '~') { 246 nonAsciiChars++; 247 } 248 if (Character.isAlphabetic(c)) { 249 alphabeticChars++; 250 } 251 if (Character.isWhitespace(c)) { 252 whitespaceChars++; 253 } 254 } 255 if (nonAsciiChars >= 0.5 * totalChars && alphabeticChars >= 0.5 * totalChars) { 256 // At least 50% non-ASCII and at least 50% alphabetic. There's a good chance that this 257 // is backing some kind of ICU property structure. 258 return HeapCategory.INTERNED_STRING_UNICODE_ALPHABET_ISH; 259 } else if (alphabeticChars >= 0.75 * totalChars && whitespaceChars >= 0.05 * totalChars) { 260 // At least 75% alphabetic and at least 5% whitespace and less than 50% non-ASCII. 261 // There's a good chance this is human-readable text e.g. an error message. 262 return HeapCategory.INTERNED_STRING_TEXT_ISH; 263 } else { 264 // Neither of the above. There's a good chance that this is something code-like e.g. a 265 // regex. 266 return HeapCategory.INTERNED_STRING_CODE_ISH; 267 } 268 } 269 isAndroidUtilClass(String className)270 private boolean isAndroidUtilClass(String className) { 271 return className.startsWith("android.util.") 272 || className.startsWith("com.android.internal.util."); 273 } 274 isAndroidFrameworkClass(String className)275 private boolean isAndroidFrameworkClass(String className) { 276 return (className.startsWith("android.") 277 && !className.startsWith("android.icu.") 278 && !className.startsWith("android.util.")) 279 || 280 (className.startsWith("com.android.internal.") 281 && !className.startsWith("com.android.internal.util.")); 282 } 283 isSecurityClass(String className)284 private boolean isSecurityClass(String className) { 285 return className.startsWith("java.security.") 286 || className.startsWith("sun.security.") 287 || className.startsWith("com.android.org.bouncycastle.") 288 || className.startsWith("com.android.org.conscrypt."); 289 } 290 isOwnedByClassMatching(AhatInstance rooted, Predicate<String> predicate)291 private boolean isOwnedByClassMatching(AhatInstance rooted, Predicate<String> predicate) { 292 // Do a BFS of the strong reference graph looking for matching classes. 293 Set<AhatInstance> visited = new HashSet<>(); 294 Queue<AhatInstance> queue = new ArrayDeque<>(); 295 visited.add(rooted); 296 queue.add(rooted); 297 while (!queue.isEmpty()) { 298 AhatInstance instance = queue.remove(); 299 if (instance.isClassObj()) { 300 // This is the heap allocation for the static state of a class. Check the class. 301 // Don't continue up the reference tree, as every instance of this class has a 302 // reference to it. 303 return predicate.test(instance.asClassObj().getName()); 304 } else if (instance.isPlaceHolder()) { 305 // Placeholders have no retained size and so can be ignored. 306 return false; 307 } else { 308 // This is the heap allocation for the instance state of an object. Check its class. 309 // If it's not a match, continue searching up the strong reference graph. 310 AhatClassObj classObj = instance.getClassObj(); 311 if (predicate.test(classObj.getName())) { 312 return true; 313 } else { 314 for (AhatInstance reference : instance.getHardReverseReferences()) { 315 if (!visited.contains(reference)) { 316 visited.add(reference); 317 queue.add(reference); 318 } 319 } 320 } 321 } 322 } 323 return false; 324 } 325 isLibcoreClass(String name)326 private boolean isLibcoreClass(String name) { 327 return name.startsWith("java.") 328 || name.startsWith("javax.") 329 || name.startsWith("sun.") 330 || name.startsWith("com.sun.") 331 || name.startsWith("libcore."); 332 } 333 334 /** 335 * Increments the stored size for the given category by the retain size of the given rooted 336 * instance on the configured heaps. 337 */ incrementSize(AhatInstance rooted, HeapCategory category)338 private void incrementSize(AhatInstance rooted, HeapCategory category) { 339 Size size = Size.ZERO; 340 for (AhatHeap heap : heaps) { 341 size = size.plus(rooted.getRetainedSize(heap)); 342 } 343 if (sizesByCategory.containsKey(category)) { 344 sizesByCategory.put(category, sizesByCategory.get(category).plus(size)); 345 } else { 346 sizesByCategory.put(category, size); 347 } 348 } 349 } 350