View Javadoc

1   /***
2    * Copyright 2009 Martin Vysny.
3    *
4    * This file is part of WebVM.
5    *
6    * WebVM is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * WebVM is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with WebVM.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package sk.baka.webvm.analyzer;
20  
21  import sk.baka.webvm.analyzer.hostos.Memory;
22  import java.io.File;
23  import java.lang.management.ManagementFactory;
24  import java.lang.management.MemoryPoolMXBean;
25  import java.lang.management.MemoryUsage;
26  import java.lang.management.ThreadInfo;
27  import java.lang.management.ThreadMXBean;
28  import java.lang.reflect.Method;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.Properties;
32  import java.util.logging.Level;
33  import java.util.logging.Logger;
34  import org.apache.commons.io.FileSystemUtils;
35  import org.apache.commons.io.FileUtils;
36  import sk.baka.tools.JavaUtils;
37  import sk.baka.webvm.ThreadDump;
38  import sk.baka.webvm.config.Config;
39  import sk.baka.webvm.misc.MgmtUtils;
40  import static sk.baka.webvm.misc.Constants.*;
41  
42  /***
43   * Analyzes VM problems.
44   * @author Martin Vysny
45   */
46  public final class ProblemAnalyzer {
47  
48      private static final Logger LOG = Logger.getLogger(ProblemAnalyzer.class.getName());
49  
50      /***
51       * Finds deadlocked threads. Uses {@link ThreadMXBean#findDeadlockedThreads} on 1.6, falls back to {@link ThreadMXBean#findMonitorDeadlockedThreads()} on 1.5.
52       * @param bean the thread mx bean
53       * @return ids of deadlocked threads, null if no threads are deadlocked.
54       */
55      public static long[] findDeadlockedThreads(final ThreadMXBean bean) {
56          long[] dt;
57          // try to get the "findDeadlockedThreads()" 1.6 method.
58          Method m = null;
59          try {
60              m = ThreadMXBean.class.getMethod("findDeadlockedThreads");
61          } catch (NoSuchMethodException ex) {
62              // nope. We are on 1.5. We'll use findMonitorDeadlockedThreads()
63          }
64          if (m != null) {
65              try {
66                  dt = (long[]) m.invoke(bean);
67              } catch (Exception ex) {
68                  throw new RuntimeException(ex);
69              }
70          } else {
71              dt = bean.findMonitorDeadlockedThreads();
72          }
73          return dt;
74      }
75  
76      /***
77       * Parses init values from given application.
78       * @param config
79       */
80      public void configure(final Config config) {
81          this.config = new Config(config);
82      }
83      private Config config = null;
84  
85      /***
86       * Parses given property and returns it as an integer. Allows default value to be returned in case of null.
87       * @param props the properties
88       * @param propName the property name
89       * @param defaultValue returned when given property is not specified.
90       * @return parsed property value
91       */
92      public static int parse(final Properties props, final String propName, final int defaultValue) {
93          String arg = props.getProperty(propName);
94          if (arg == null) {
95              return defaultValue;
96          }
97          arg = arg.trim();
98          try {
99              return Integer.parseInt(arg);
100         } catch (final Exception ex) {
101             LOG.log(Level.SEVERE, propName + ": failed to parse '" + arg + "'", ex);
102         }
103         return defaultValue;
104     }
105     /***
106      * The "Free disk space" problem class.
107      */
108     public static final String CLASS_FREE_DISK_SPACE = "Free disk space";
109 
110     private String getFreeDiskSpaceDesc() {
111         return "Triggered when there is less than " + config.minFreeDiskSpaceMb + "Mb of free space on some drive";
112     }
113     /***
114      * The "Deadlocked threads" problem class.
115      */
116     public static final String CLASS_DEADLOCKED_THREADS = "Deadlocked threads";
117     private static final String CLASS_DEADLOCKED_THREADS_DESC = "Triggered when there are some deadlocked threads. Finds cycles of threads that are in "
118             + "deadlock waiting to acquire object monitors (also "
119             + "<a href=\"http://java.sun.com/javase/6/docs/api/java/lang/management/LockInfo.html#OwnableSynchronizer\">ownable synchronizers"
120             + "</a> when running on Java 6).";
121     /***
122      * The "GC CPU Usage" problem class.
123      */
124     public static final String CLASS_GC_CPU_USAGE = "GC CPU Usage";
125 
126     private String getGcCpuUsageDesc() {
127         return "Triggered when GC uses " + config.gcCpuTreshold + "% or more of CPU continuously for "
128                 + (config.gcCpuTresholdSamples * HistorySampler.HISTORY_VMSTAT.getHistorySampleDelayMs() / MILLIS_IN_SECOND) + " seconds";
129     }
130     /***
131      * The "GC Memory cleanup" problem class.
132      */
133     public static final String CLASS_GC_MEMORY_CLEANUP = "GC Memory cleanup";
134 
135     private String getGcMemoryCleanupDesc() {
136         return "Triggered when GC cannot make available more than "
137                 + (HUNDRED_PERCENT - config.memAfterGcUsageTreshold) + "% of memory";
138     }
139     /***
140      * The "Memory status" problem class.
141      */
142     public static final String CLASS_MEMORY_USAGE = "Memory usage";
143 
144     private String getMemUsageDesc() {
145         return "Triggered: never. Reports memory pools which are at least " + config.memUsageTreshold + "% full";
146     }
147     /***
148      * The "Host Memory" problem class.
149      */
150     public static final String CLASS_HOST_MEMORY_USAGE = "Host Virtual Mem";
151 
152     private String getHostMemoryUsageDesc() {
153         return "Triggered when host uses " + config.hostVirtMem + "% or more virtual memory";
154     }
155 
156     /***
157      * Diagnose the VM and returns a list of problem reports.
158      * @param history current history.
159      * @return the list of reports.
160      */
161     public List<ProblemReport> getProblems(final List<HistorySample> history) {
162         final List<ProblemReport> result = new ArrayList<ProblemReport>();
163         result.add(getDeadlockReport());
164         result.add(getGCCPUUsageReport(history));
165         result.add(getMemStatReport());
166         result.add(getGCMemUsageReport());
167         result.add(getFreeDiskspaceReport());
168         result.add(getHostVirtMemReport());
169         return result;
170     }
171 
172     /***
173      * Prepares the {@link #CLASS_HOST_MEMORY_USAGE} report.
174      * @return report
175      */
176     public ProblemReport getHostVirtMemReport() {
177         final MemoryUsage phys = Memory.getPhysicalMemory();
178         if (phys == null) {
179             return new ProblemReport(false, CLASS_HOST_MEMORY_USAGE, "Host memory reporting unsupported on this platform", getHostMemoryUsageDesc());
180         }
181         // buffer/cache information is available?
182         final boolean cbAvailable = (phys.getCommitted() == phys.getUsed());
183         final StringBuilder sb = new StringBuilder();
184         if (cbAvailable) {
185             sb.append("buffers/cache detection not supported, disabled\n");
186         }
187         final MemoryUsage swap = Memory.getSwap();
188         sb.append("Physical memory used: ");
189         sb.append(phys.getCommitted() * HUNDRED_PERCENT / phys.getMax());
190         sb.append("%, minus buffers/cache: ");
191         sb.append(phys.getUsed() * HUNDRED_PERCENT / phys.getMax());
192         sb.append("%\nSwap used: ").append(MgmtUtils.getUsagePerc(swap));
193         sb.append('\n');
194         final long total = phys.getMax() + (swap == null ? 0 : swap.getMax());
195         final long used = phys.getUsed() + (swap == null ? 0 : swap.getUsed());
196         final long usedPerc = used * HUNDRED_PERCENT / total;
197         sb.append("Total virtual memory usage: ");
198         sb.append(usedPerc);
199         sb.append('%');
200         final boolean isProblem = !cbAvailable && (usedPerc >= config.hostVirtMem);
201         return new ProblemReport(isProblem, CLASS_HOST_MEMORY_USAGE, sb.toString(), getHostMemoryUsageDesc());
202     }
203 
204     /***
205      * Prepares the {@link #CLASS_GC_CPU_USAGE} report.
206      * @param history the history
207      * @return report
208      */
209     public ProblemReport getGCCPUUsageReport(final List<HistorySample> history) {
210         int tresholdViolationCount = 0;
211         int maxTresholdViolationCount = 0;
212         int totalAvgTreshold = 0;
213         int avgTresholdViolation = 0;
214         int maxAvgTresholdViolation = 0;
215         for (final HistorySample h : history) {
216             totalAvgTreshold += h.gcCpuUsage;
217             if (h.gcCpuUsage >= config.gcCpuTreshold) {
218                 tresholdViolationCount++;
219                 avgTresholdViolation += h.gcCpuUsage;
220                 if (maxTresholdViolationCount < tresholdViolationCount) {
221                     maxTresholdViolationCount = tresholdViolationCount;
222                     maxAvgTresholdViolation = avgTresholdViolation;
223                 }
224             } else {
225                 tresholdViolationCount = 0;
226                 avgTresholdViolation = 0;
227             }
228         }
229         maxAvgTresholdViolation = maxTresholdViolationCount == 0 ? 0 : maxAvgTresholdViolation / maxTresholdViolationCount;
230         totalAvgTreshold = history.size() == 0 ? 0 : totalAvgTreshold / history.size();
231         if (maxTresholdViolationCount >= config.gcCpuTresholdSamples) {
232             return new ProblemReport(true, CLASS_GC_CPU_USAGE, "GC spent more than " + config.gcCpuTreshold + "% (avg. "
233                     + maxAvgTresholdViolation + "%) of CPU for " + (maxTresholdViolationCount * HistorySampler.HISTORY_VMSTAT.getHistorySampleDelayMs() / MILLIS_IN_SECOND) + " seconds",
234                     getGcCpuUsageDesc());
235         }
236         return new ProblemReport(false, CLASS_GC_CPU_USAGE, "Avg. GC CPU usage last "
237                 + (history.size() * HistorySampler.HISTORY_VMSTAT.getHistorySampleDelayMs() / MILLIS_IN_SECOND) + " seconds: " + totalAvgTreshold + "%",
238                 getGcCpuUsageDesc());
239     }
240 
241     /***
242      * Prepares the {@link #CLASS_MEMORY_STATUS} report.
243      * @return report
244      */
245     public ProblemReport getMemStatReport() {
246         final List<MemoryPoolMXBean> beans = ManagementFactory.getMemoryPoolMXBeans();
247         if (JavaUtils.isBlank(beans)) {
248             return new ProblemReport(false, CLASS_MEMORY_USAGE, "INFO: No memory pool information", getMemUsageDesc());
249         }
250         final StringBuilder sb = new StringBuilder();
251         for (final MemoryPoolMXBean bean : beans) {
252             final MemoryUsage usage = bean.getUsage();
253             if (usage == null || !bean.isCollectionUsageThresholdSupported() || !bean.isUsageThresholdSupported()) {
254                 continue;
255             }
256             final long used = usage.getUsed() * HUNDRED_PERCENT / usage.getMax();
257             if (used >= config.memUsageTreshold) {
258                 sb.append("INFO: Pool [");
259                 sb.append(bean.getName());
260                 sb.append("] is now ");
261                 sb.append(used);
262                 sb.append("% full\n");
263             }
264         }
265         if (sb.length() == 0) {
266             return new ProblemReport(false, CLASS_MEMORY_USAGE, "Heap usage: " + MgmtUtils.getUsagePerc(MgmtUtils.getHeapFromRuntime()), getMemUsageDesc());
267         }
268         sb.append("\nTry performing a GC: this should decrease the memory usage. If not, you may need to increase the memory or check for memory leaks");
269         return new ProblemReport(false, CLASS_MEMORY_USAGE, sb.toString(), getMemUsageDesc());
270     }
271 
272     /***
273      * Prepares the {@link #CLASS_GC_MEMORY_CLEANUP} report.
274      * @return report
275      */
276     public ProblemReport getGCMemUsageReport() {
277         final List<MemoryPoolMXBean> beans = ManagementFactory.getMemoryPoolMXBeans();
278         if (JavaUtils.isBlank(beans)) {
279             return new ProblemReport(false, CLASS_GC_MEMORY_CLEANUP, "INFO: No memory pool information", getGcMemoryCleanupDesc());
280         }
281         final StringBuilder sb = new StringBuilder();
282         for (final MemoryPoolMXBean bean : beans) {
283             final MemoryUsage usage = bean.getCollectionUsage();
284             if (usage == null || !bean.isCollectionUsageThresholdSupported() || !bean.isUsageThresholdSupported()) {
285                 continue;
286             }
287             if (usage.getMax() <= 0) {
288                 continue;
289             }
290             final long used = usage.getUsed() * HUNDRED_PERCENT / usage.getMax();
291             if (used >= config.memAfterGcUsageTreshold) {
292                 sb.append("Pool [");
293                 sb.append(bean.getName());
294                 sb.append("] is ");
295                 sb.append(used);
296                 sb.append("% full after GC\n");
297             }
298         }
299         if (sb.length() == 0) {
300             return new ProblemReport(false, CLASS_GC_MEMORY_CLEANUP, "OK", getGcMemoryCleanupDesc());
301         }
302         sb.append("\nYou may need to increase the memory or check for memory leaks");
303         return new ProblemReport(true, CLASS_GC_MEMORY_CLEANUP, sb.toString(), getGcMemoryCleanupDesc());
304     }
305 
306     /***
307      * Prepares the {@link #CLASS_DEADLOCKED_THREADS} report.
308      * @return report
309      */
310     public static ProblemReport getDeadlockReport() {
311         final StringBuilder sb = new StringBuilder();
312         final ThreadMXBean bean = ManagementFactory.getThreadMXBean();
313         if (bean == null) {
314             return new ProblemReport(false, CLASS_DEADLOCKED_THREADS, "INFO: Report Unavailable - ThreadMXBean null", CLASS_DEADLOCKED_THREADS_DESC);
315         }
316         final long[] dt = findDeadlockedThreads(bean);
317         if (JavaUtils.isEmpty(dt)) {
318             return new ProblemReport(false, CLASS_DEADLOCKED_THREADS, "None", CLASS_DEADLOCKED_THREADS_DESC);
319         }
320         for (final long thread : dt) {
321             final ThreadInfo info = bean.getThreadInfo(thread, Integer.MAX_VALUE);
322             sb.append("Locked thread: ");
323             sb.append(ThreadDump.getThreadMetadata(info));
324             sb.append('\n');
325             sb.append("Stacktrace:");
326             sb.append(printStackTrace(info.getStackTrace()));
327         }
328         return new ProblemReport(true, CLASS_DEADLOCKED_THREADS, sb.toString(), CLASS_DEADLOCKED_THREADS_DESC);
329     }
330 
331     private static String printStackTrace(final StackTraceElement[] stacktrace) {
332         if (JavaUtils.isEmpty(stacktrace)) {
333             return "unknown\n";
334         }
335         final StringBuilder sb = new StringBuilder();
336         sb.append('\n');
337         for (int i = 0; i < stacktrace.length; i++) {
338             sb.append("\tat ");
339             sb.append(stacktrace[i]);
340             sb.append('\n');
341         }
342         return sb.toString();
343     }
344 
345     /***
346      * Analyzes free disk space.
347      * @return free disk space report.
348      */
349     public ProblemReport getFreeDiskspaceReport() {
350         final StringBuilder sb = new StringBuilder();
351         boolean problem = false;
352         for (final File root : File.listRoots()) {
353             try {
354                 final long freeSpaceKb = FileSystemUtils.freeSpaceKb(root.getAbsolutePath());
355                 final long freeSpaceMb = freeSpaceKb / KIBIBYTES;
356                 if (freeSpaceMb < config.minFreeDiskSpaceMb) {
357                     problem = true;
358                     sb.append("Low disk space: ");
359                 }
360                 sb.append(root.getAbsolutePath());
361                 sb.append("  ");
362                 sb.append(FileUtils.byteCountToDisplaySize(freeSpaceKb * KIBIBYTES));
363                 sb.append(" free\n");
364             } catch (Exception ex) {
365                 LOG.log(Level.INFO, "Failed to get free space on " + root.getAbsolutePath(), ex);
366                 sb.append("Failed to get free space on ");
367                 sb.append(root.getAbsolutePath());
368                 sb.append(": ");
369                 sb.append(ex.toString());
370                 sb.append("\n");
371             }
372         }
373         if (sb.length() == 0) {
374             sb.append("OK");
375         }
376         return new ProblemReport(problem, CLASS_FREE_DISK_SPACE, sb.toString(), getFreeDiskSpaceDesc());
377     }
378 }