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