1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.io.InputStream;
21 import java.io.Serializable;
22 import java.security.CodeSource;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.Date;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Scanner;
31
32 /**
33  * Histogramme mémoire.
34  * @author Emeric Vernat
35  */

36 public class HeapHistogram implements Serializable {
37     private static final long serialVersionUID = 2163916067335213382L;
38
39     @SuppressWarnings("all")
40     private final List<ClassInfo> classes;
41     @SuppressWarnings("all")
42     private final List<ClassInfo> permGenClasses;
43     private final Date time;
44     private long totalHeapBytes;
45     private long totalHeapInstances;
46     private long totalPermGenBytes;
47     private long totalPermgenInstances;
48     private boolean sourceDisplayed;
49     private boolean deltaDisplayed; // deltaDisplayed kept for backward compatibility with all collect servers
50
51     public HeapHistogram(InputStream in, boolean jrockit) {
52         time = new Date();
53         final Scanner sc = new Scanner(in, "UTF-8");
54         final List<ClassInfo> classInfos = scan(sc, jrockit);
55
56         classes = new ArrayList<>();
57         permGenClasses = new ArrayList<>();
58
59         for (final ClassInfo classInfo : classInfos) {
60             if (classInfo.isPermGen()) {
61                 permGenClasses.add(classInfo);
62                 totalPermGenBytes += classInfo.getBytes();
63                 totalPermgenInstances += classInfo.getInstancesCount();
64             } else {
65                 classes.add(classInfo);
66                 totalHeapBytes += classInfo.getBytes();
67                 totalHeapInstances += classInfo.getInstancesCount();
68             }
69             if (!sourceDisplayed && classInfo.getSource() != null) {
70                 sourceDisplayed = true;
71             }
72         }
73         if (!jrockit) {
74             sc.next("Total");
75             final long totalInstances = sc.nextLong();
76             final long totalBytes = sc.nextLong();
77             assert totalInstances == totalPermgenInstances + totalHeapInstances;
78             assert totalBytes == totalPermGenBytes + totalHeapBytes;
79         }
80         sort();
81     }
82
83     public void add(HeapHistogram second) {
84         final Map<String, ClassInfo> classesMap = new HashMap<>(1024);
85         final Map<String, ClassInfo> permGenMap = new HashMap<>(1024);
86         for (final ClassInfo classInfo : classes) {
87             addClassInfo(classInfo, classesMap);
88         }
89         for (final ClassInfo classInfo : permGenClasses) {
90             addClassInfo(classInfo, permGenMap);
91         }
92         for (final ClassInfo classInfo : second.getHeapHistogram()) {
93             addClassInfo(classInfo, classesMap);
94         }
95         for (final ClassInfo classInfo : second.getPermGenHistogram()) {
96             addClassInfo(classInfo, permGenMap);
97         }
98         totalHeapBytes += second.getTotalHeapBytes();
99         totalHeapInstances += second.getTotalHeapInstances();
100         totalPermGenBytes += second.getTotalPermGenBytes();
101         totalPermgenInstances += second.getTotalPermGenInstances();
102         classes.clear();
103         classes.addAll(classesMap.values());
104         permGenClasses.clear();
105         permGenClasses.addAll(permGenMap.values());
106         sort();
107         sourceDisplayed = sourceDisplayed || second.isSourceDisplayed();
108     }
109
110     private void addClassInfo(ClassInfo newClInfo, Map<String, ClassInfo> map) {
111         final ClassInfo oldClInfo = map.get(newClInfo.getName());
112         if (oldClInfo == null) {
113             map.put(newClInfo.getName(), newClInfo);
114         } else {
115             oldClInfo.add(newClInfo);
116         }
117     }
118
119     public Date getTime() {
120         return time;
121     }
122
123     public List<ClassInfo> getHeapHistogram() {
124         return Collections.unmodifiableList(classes);
125     }
126
127     public long getTotalHeapInstances() {
128         return totalHeapInstances;
129     }
130
131     public long getTotalHeapBytes() {
132         return totalHeapBytes;
133     }
134
135     public List<ClassInfo> getPermGenHistogram() {
136         return Collections.unmodifiableList(permGenClasses);
137     }
138
139     public long getTotalPermGenInstances() {
140         return totalPermgenInstances;
141     }
142
143     public long getTotalPermGenBytes() {
144         return totalPermGenBytes;
145     }
146
147     public boolean isSourceDisplayed() {
148         return sourceDisplayed;
149     }
150
151     /**
152      * @deprecated deltaDisplayed kept only for backward compatibility with all collect servers
153      * @return false
154      */

155     @Deprecated
156     boolean isDeltaDisplayed() {
157         return deltaDisplayed;
158     }
159
160     private void sort() {
161         final Comparator<ClassInfo> classInfoReversedComparator = Collections
162                 .reverseOrder(new ClassInfoComparator());
163         Collections.sort(permGenClasses, classInfoReversedComparator);
164         Collections.sort(classes, classInfoReversedComparator);
165     }
166
167     static final class ClassInfoComparator implements Comparator<ClassInfo>, Serializable {
168         private static final long serialVersionUID = 1L;
169
170         /** {@inheritDoc} */
171         @Override
172         public int compare(ClassInfo classInfo1, ClassInfo classInfo2) {
173             return Long.compare(classInfo1.getBytes(), classInfo2.getBytes());
174         }
175     }
176
177     private void skipHeader(Scanner sc, boolean jrockit) {
178         final String nextLine = sc.nextLine();
179         // avant jdk 9, il y a une ligne blanche puis le header, mais en jdk 9 il y a juste le header
180         if (nextLine.isEmpty()) {
181             sc.nextLine();
182         }
183         if (!jrockit) {
184             sc.skip("-+");
185             sc.nextLine();
186         }
187     }
188
189     private List<ClassInfo> scan(Scanner sc, boolean jrockit) {
190         final Map<String, ClassInfo> classInfoMap = new HashMap<>(1024);
191         sc.useRadix(10);
192         skipHeader(sc, jrockit);
193
194         final String nextLine;
195         if (jrockit) {
196             nextLine = "[0-9.]+%";
197         } else {
198             nextLine = "[0-9]+:";
199         }
200         while (sc.hasNext(nextLine)) {
201             final ClassInfo newClInfo = new ClassInfo(sc, jrockit);
202             addClassInfo(newClInfo, classInfoMap);
203         }
204         return new ArrayList<>(classInfoMap.values());
205     }
206
207     /**
208      * Données sur une classe.
209      * @author Emeric Vernat
210      */

211     public static class ClassInfo implements Serializable {
212         private static final long serialVersionUID = 6283636454450216347L;
213         private static final Map<Character, String> ARRAY_TYPES = new HashMap<>();
214
215         static {
216             ARRAY_TYPES.put('Z', "boolean");
217             ARRAY_TYPES.put('C', "char");
218             ARRAY_TYPES.put('B', "byte");
219             ARRAY_TYPES.put('S', "short");
220             ARRAY_TYPES.put('I', "int");
221             ARRAY_TYPES.put('J', "long");
222             ARRAY_TYPES.put('F', "float");
223             ARRAY_TYPES.put('D', "double");
224             ARRAY_TYPES.put('L', "object");
225         }
226
227         private long instances;
228         private long bytes;
229         private final String jvmName;
230         private final String name;
231         private final boolean permGen;
232         private final String source;
233
234         ClassInfo(Scanner sc, boolean jrockit) {
235             super();
236
237             sc.next();
238             if (jrockit) {
239                 bytes = parseLongWithK(sc.next());
240                 instances = sc.nextLong();
241             } else {
242                 instances = sc.nextLong();
243                 bytes = sc.nextLong();
244             }
245             jvmName = sc.next();
246             permGen = jvmName.charAt(0) == '<';
247             name = convertJVMName();
248             source = findSource();
249             if (sc.hasNext("\\([a-zA-Z.@0-9-ea]*\\)")) {
250                 // in jdk 9: (module)
251                 sc.next();
252             }
253         }
254
255         void add(ClassInfo classInfo) {
256             assert getName().equals(classInfo.getName());
257             this.bytes += classInfo.getBytes();
258             this.instances += classInfo.getInstancesCount();
259         }
260
261         public String getName() {
262             return name;
263         }
264
265         public long getInstancesCount() {
266             return instances;
267         }
268
269         public long getBytes() {
270             return bytes;
271         }
272
273         boolean isPermGen() {
274             return permGen;
275         }
276
277         public String getSource() {
278             return source;
279         }
280
281         private String findSource() {
282             // on exclue les classes de PermGen et les classes générées dynamiquement
283             if (jvmName.endsWith("Klass>") || jvmName.startsWith("sun.reflect.")) {
284                 return null;
285             }
286             try {
287                 final Class<?> clazz = Class.forName(jvmName);
288                 return findSource(clazz);
289             } catch (final LinkageError | ClassNotFoundException e) {
290                 // dans jonas en OSGI, par exemple avec des classes Quartz, il peut survenir
291                 // des LinkageError (rq: NoClassDefFoundError hérite également de LinkageError)
292                 // et on suppose qu'il y a une seule webapp et que la plupart des classes peuvent être chargées
293                 // sinon il y a une ClassNotFoundException et on retourne null
294                 return null;
295             }
296         }
297
298         private static String findSource(Class<?> clazz) {
299             final CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
300             if (codeSource != null && codeSource.getLocation() != null) {
301                 String src = codeSource.getLocation().toString();
302                 if (src.startsWith("file:/")) {
303                     src = src.substring("file:/".length());
304                 } else if (src.startsWith("vfs:/")) {
305                     // "vfs:/" pour jboss 6.0
306                     src = src.substring("vfs:/".length());
307                 } else if (src.startsWith("reference:file:/")) {
308                     // "reference:file:/" pour les bundles osgi
309                     src = src.substring("reference:file:/".length());
310                 }
311                 if (src.endsWith(".jar") || src.endsWith(".war")) {
312                     src = src.intern();
313                 }
314                 return src;
315             }
316             return null;
317         }
318
319         private String convertJVMName() {
320             String result;
321             final int index = jvmName.lastIndexOf('[');
322
323             if (index != -1) {
324                 final char code = jvmName.charAt(index + 1);
325                 if (code == 'L') {
326                     result = jvmName.substring(index + 2, jvmName.length() - 1);
327                 } else {
328                     result = ARRAY_TYPES.get(code);
329                     if (result == null) {
330                         result = jvmName;
331                     }
332                 }
333                 final StringBuilder sb = new StringBuilder(result);
334                 for (int i = 0; i <= index; i++) {
335                     sb.append("[]");
336                 }
337                 result = sb.toString();
338             } else {
339                 result = jvmName;
340             }
341             return result.intern();
342         }
343
344         public static long parseLongWithK(String text) {
345             assert !text.isEmpty();
346             if (text.charAt(text.length() - 1) == 'k') {
347                 String t = text.substring(0, text.length() - 1);
348                 if (t.charAt(0) == '+') {
349                     t = t.substring(1);
350                 }
351                 return 1024 * Long.parseLong(t);
352             }
353             // inutile car le total n'est pas lu
354             //            else if (text.endsWith("kB")) {
355             //                return 1024 * Long.parseLong(text.substring(0, text.length() - 2));
356             //            }
357             return Long.parseLong(text);
358         }
359     }
360 }
361