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
58 Method m = null;
59 try {
60 m = ThreadMXBean.class.getMethod("findDeadlockedThreads");
61 } catch (NoSuchMethodException ex) {
62
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
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 }